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:
239
docs/plans/2026-02-12-issue-57-command-output-capture.md
Normal file
239
docs/plans/2026-02-12-issue-57-command-output-capture.md
Normal 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
|
||||
Reference in New Issue
Block a user