feat: add command output capture to troubleshooting sessions (#57)

Engineers can now paste command output during action steps. Output is
stored in the session decisions JSONB, displayed in session review,
included in all 4 export formats with command context, and preserved
in session-to-tree conversions.

- Collapsible "Paste Output" textarea on action nodes with commands
- 10,000 character limit with live character count
- Works on both built-in and custom action steps
- Preloads output when revisiting a step via Go Back
- All exports show commands run alongside captured output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-12 00:57:33 -05:00
parent 9dbb8b8406
commit 577a2fbf2a
7 changed files with 413 additions and 3 deletions

View File

@@ -0,0 +1,239 @@
# Issue #57: Command Output Capture — Implementation Plan
## Overview
Engineers run commands during troubleshooting sessions but the output is lost — exports say "ran this command" but not what it returned. This feature adds a "Paste Output" textarea on action nodes and custom action steps so command output is captured in session data and included in all exports and session review.
**Scope:** Built-in action nodes AND custom action steps.
**Migration:** None required — `decisions` is already a JSONB array with flexible dict entries.
---
## Public Interfaces / Type Changes
- **Backend:** Add `command_output: Optional[str] = Field(None, max_length=10000)` to `DecisionRecord` in `session.py`
- **Frontend:** Add `command_output?: string | null` to `DecisionRecord` type in `session.ts`
- **API:** No endpoint changes — `PUT /api/v1/sessions/{id}` continues to accept full decisions array; now includes optional `command_output`
- **Validation:** Backend enforces 10,000 character hard limit (returns 422 on overflow); frontend shows live character count
---
## Files to Modify
### Backend (3 files)
| File | Change |
|------|--------|
| `backend/app/schemas/session.py` | Add `command_output` field to `DecisionRecord` |
| `backend/app/services/export_service.py` | Render `command_output` in all 4 export formats with command context |
| `backend/app/core/session_to_tree.py` | Include command output when converting session decisions to forked tree nodes |
### Frontend (3 files)
| File | Change |
|------|--------|
| `frontend/src/types/session.ts` | Add `command_output` to `DecisionRecord` type |
| `frontend/src/pages/TreeNavigationPage.tsx` | Add capture UI for both built-in action nodes and custom action steps |
| `frontend/src/pages/SessionDetailPage.tsx` | Render `command_output` in decision review and clipboard copy |
---
## Implementation Steps
### Step 1: Backend Schema
**File:** `backend/app/schemas/session.py`
- Add `command_output: Optional[str] = Field(None, max_length=10000)` to `DecisionRecord`
- The `max_length=10000` provides backend validation — requests exceeding this return 422
### Step 2: Frontend Type
**File:** `frontend/src/types/session.ts`
- Add `command_output?: string | null` to the `DecisionRecord` type
- This ensures the field is not dropped by TypeScript typing during round-trips
### Step 3: TreeNavigationPage — Capture UI for Built-in Action Nodes
**File:** `frontend/src/pages/TreeNavigationPage.tsx`
**State:**
- Add `const [commandOutput, setCommandOutput] = useState('')` (same pattern as existing `notes` state)
- Clear `commandOutput` when node changes (same place `notes` is cleared)
- When revisiting a step, preload existing `command_output` from the decision record if present
**UI — on action nodes with `currentNode.commands?.length > 0`:**
- Render a collapsible "Paste Output (Optional)" section below the commands display
- Inside: a textarea with:
- Placeholder: `"Paste command output here..."`
- Monospace font (`font-mono`), consistent with command code block styling
- `bg-white/10` background styling to match existing design
- Live character count display: `"{count} / 10,000"` shown below the textarea
- Max length enforced on the frontend at 10,000 characters
- Use a `Terminal` icon from `lucide-react` for the section label
**Persistence — in `handleContinue()`:**
- Add `command_output: commandOutput.trim() || null` to the decision record pushed to the session
- Empty or whitespace-only input is normalized to `null` (treated as not provided)
### Step 4: TreeNavigationPage — Capture UI for Custom Action Steps
**File:** `frontend/src/pages/TreeNavigationPage.tsx`
Custom action steps create their decision record at insertion time, which is different from built-in action nodes. The output capture UI and behavior should be the same as Step 3, but persistence requires updating the existing decision rather than creating a new one.
**UI:**
- Same collapsible "Paste Output (Optional)" section as built-in action nodes
- Available when a custom action step has commands defined
**Persistence:**
- Before `handleContinueToDescendant` or `handleCustomBranchComplete` is called, update the current custom step's decision record with `command_output: commandOutput.trim() || null`
- Persist the updated decisions array to the backend before navigation/completion transitions
- This is a wrapper flow around the existing custom step logic — not a replacement of it
### Step 5: Export Service — All 4 Formats
**File:** `backend/app/services/export_service.py`
**Helpers to add:**
- A helper to safely extract and normalize `command_output` from a decision dict (strip whitespace, return `None` if empty)
- A helper to resolve the commands associated with a step for context display:
1. First look up the tree snapshot action node by `node_id`
2. Fallback to custom step metadata by `node_id`
3. Fallback to no command list (just show the output)
**Export rendering per format** (all guarded by `if command_output := decision.get("command_output")`):
**Markdown (`_generate_markdown_export`):**
```
**Commands Run:** `ping 8.8.8.8`, `tracert 8.8.8.8`
**Output:**
```
{output}
```
```
**Text (`_generate_text_export`):**
```
Commands Run: ping 8.8.8.8, tracert 8.8.8.8
Output:
{output with each line indented}
```
**HTML (`_generate_html_export`):**
```html
<p><strong>Commands Run:</strong> <code>ping 8.8.8.8</code>, <code>tracert 8.8.8.8</code></p>
<pre><code>{html.escape(output)}</code></pre>
```
**PSA (`_generate_psa_export`):**
```
Commands: ping 8.8.8.8, tracert 8.8.8.8
Output:
{output with each line indented}
```
### Step 6: SessionDetailPage — Review Display
**File:** `frontend/src/pages/SessionDetailPage.tsx`
**Decision review:**
- After the `action_performed` rendering for each decision, check for `command_output`
- If present, render in a `<pre>` block with monospace styling and preserved whitespace
- Label: "Command Output" with consistent styling
**Clipboard copy (`copyTicketNotes`):**
- After the action performed line, add:
```
Output:
{decision.command_output}
```
- Only include if `command_output` is present and non-empty
### Step 7: Session-to-Tree Conversion
**File:** `backend/app/core/session_to_tree.py`
- When building node descriptions from decisions, check for `command_output`
- If present, append the output text to the node description so forked trees retain the captured output
- Format: include a "Command Output:" label followed by the output text
---
## Edge Cases & Failure Modes
| Scenario | Behavior |
|----------|----------|
| Existing sessions without `command_output` | Render normally with no errors — field is optional |
| Output exceeds 10,000 characters | Frontend prevents input beyond limit; backend returns 422 if somehow exceeded |
| Empty or whitespace-only input | Normalized to `null` — treated as not provided |
| Multiline output, JSON, special characters | Preserved as-is; HTML export escapes all content |
| Steps without commands | Output can still be stored; export shows output even without command context |
| Multi-command action nodes | One shared output field per step (not per command) |
| Revisiting a completed step | Preloads the previously captured output |
---
## Test Cases
### Backend API Tests (`test_sessions.py`)
1. Update a session with `command_output` in a decision record → verify it round-trips correctly on GET
2. Submit `command_output` exceeding 10,000 characters → verify 422 response
3. Submit empty string and whitespace-only `command_output` → verify stored as `null`
### Export Tests
4. Markdown export includes command context and fenced code block for output
5. Text export includes output block with preserved line breaks
6. HTML export includes escaped `<pre><code>` block
7. PSA export includes compact command context and indented output
8. Multi-command action node exports with single shared output block
9. Export of session without any `command_output` renders cleanly (no errors, no empty blocks)
### Custom Action Step Tests
10. Insert custom action step with commands → capture output → continue → verify output stored in decision
11. Custom step output appears in all export formats
### Frontend Behavior Tests
12. Action node with commands shows the "Paste Output" section (collapsed by default)
13. Custom action step with commands shows the "Paste Output" section
14. Action node WITHOUT commands does NOT show the "Paste Output" section
15. Character count updates live as user types
16. Revisiting a step preloads previously captured output
17. Session detail page renders output block with monospace formatting
18. "Copy to clipboard" includes command output when present
---
## Verification Checklist (Manual)
1. `cd frontend && npm run build` — confirm no TypeScript errors
2. Start a session on a tree with action nodes that have commands:
- Paste output into the textarea
- Click Continue
- Verify output persists in the session data
3. Start a session and add a custom action step with commands:
- Paste output
- Continue to next step
- Verify output persists
4. Complete a session → check SessionDetailPage shows the command output with proper formatting
5. Export in all 4 formats → verify output appears correctly formatted in each
6. Use "Copy to clipboard" on a step with output → verify output is included
7. Run a session on a tree WITHOUT commands on action nodes → verify no output section appears
8. Test with existing sessions that have no `command_output` → verify they render and export without errors
9. Test pasting large output (near 10,000 chars) → verify character count and limit work
10. Test pasting multiline output with special characters → verify preservation in review and exports
---
## Assumptions
- One shared output field per step, not per individual command
- Maximum stored output is 10,000 characters
- v1 does not include syntax highlighting or image paste
- No feature flag gating — ships directly
- Collapsed-by-default UI keeps the interface clean for steps where output isn't needed