# Export Improvements Phase B — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add summary block, custom step markers, detail levels (standard/full), and editable preview modal to the export system.
**Architecture:** Backend changes to all 4 export generators (markdown, text, HTML, PSA) + schema additions. Frontend changes to ExportPreviewModal (editable textarea) and SessionDetailPage (detail level dropdown, summary toggle). No migration needed.
**Tech Stack:** Python FastAPI, Pydantic v2, React 19, TypeScript, Tailwind CSS
**Prerequisites:** Phase A frontend must be merged first (branch `feat/export-phase-a`). Task 7 depends on `maxStepIndex` state and `handleCopyForTicket` from Phase A.
---
## Task 1: Add Phase B Fields to Backend Schema
**Files:**
- Modify: `backend/app/schemas/session.py:84-91`
**Step 1: Add `include_summary` and `detail_level` to `SessionExport`**
```python
class SessionExport(BaseModel):
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$")
include_timestamps: bool = True
include_tree_info: bool = True
# Phase A
include_outcome_notes: bool = True
include_next_steps: bool = True
max_step_index: Optional[int] = Field(None, ge=1, description="1-based inclusive step cutoff")
# Phase B
include_summary: bool = False
detail_level: Literal["standard", "full"] = "standard"
```
Note: `Literal` is already imported at line 2.
**Step 2: Run tests to verify no regressions**
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All existing export tests pass (new fields have defaults, so backward-compatible).
**Step 3: Commit**
```bash
git add backend/app/schemas/session.py
git commit -m "feat: add include_summary and detail_level to SessionExport schema"
```
---
## Task 2: Add Frontend Types for Phase B
**Files:**
- Modify: `frontend/src/types/session.ts:79-86`
**Step 1: Add `include_summary` and `detail_level` to `SessionExport` interface**
```typescript
export interface SessionExport {
format: 'text' | 'markdown' | 'html' | 'psa'
include_timestamps?: boolean
include_tree_info?: boolean
include_outcome_notes?: boolean
include_next_steps?: boolean
max_step_index?: number
include_summary?: boolean
detail_level?: 'standard' | 'full'
}
```
**Step 2: Build to verify**
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
Expected: No type errors.
**Step 3: Commit**
```bash
git add frontend/src/types/session.ts
git commit -m "feat(frontend): add include_summary and detail_level to SessionExport type"
```
---
## Task 3: Custom Step Markers (B2) in All 4 Generators
**Files:**
- Modify: `backend/app/services/export_service.py`
This is a pure backend change. Detect custom steps by `node_id.startswith("custom-")` and prefix the step title with `[CUSTOM]`.
**Step 1: Update `generate_markdown_export`** (line 163)
Change:
```python
lines.append(f"### Step {i}: {question}")
```
To:
```python
is_custom = decision.get("node_id", "").startswith("custom-")
prefix = "[CUSTOM] " if is_custom else ""
lines.append(f"### Step {i}: {prefix}{question}")
if is_custom:
lines.append("*Custom step added by engineer*")
```
**Step 2: Update `generate_text_export`** (line 250)
Change:
```python
lines.append(f"\n{i}. {question}")
```
To:
```python
is_custom = decision.get("node_id", "").startswith("custom-")
prefix = "[CUSTOM] " if is_custom else ""
lines.append(f"\n{i}. {prefix}{question}")
```
**Step 3: Update `generate_html_export`** (line 342)
Change:
```python
html_parts.append(f'
Step {i}: {question}
')
```
To:
```python
is_custom = decision.get("node_id", "").startswith("custom-")
custom_badge = 'CUSTOM' if is_custom else ''
html_parts.append(f'{custom_badge}Step {i}: {question}
')
```
**Step 4: Update `generate_psa_export`** (line 413)
Change:
```python
line = f"{i}. {question}"
```
To:
```python
is_custom = decision.get("node_id", "").startswith("custom-")
prefix = "[CUSTOM] " if is_custom else ""
line = f"{i}. {prefix}{question}"
```
**Step 5: Run tests**
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All pass. (Existing tests don't have custom steps, so markers won't appear — no regressions.)
**Step 6: Commit**
```bash
git add backend/app/services/export_service.py
git commit -m "feat: add [CUSTOM] markers to custom steps in all 4 export generators"
```
---
## Task 4: Command Output Truncation + Detail Levels (B3)
**Files:**
- Modify: `backend/app/services/export_service.py`
**Step 1: Add format-aware `_truncate_command_output` helper** (after `_get_command_output` at line 91)
The truncation suffix must match the output format — markdown uses `*(...)*`, text/PSA use plain `(...)`, HTML uses `...`.
```python
def _truncate_command_output(output: str, max_lines: int = 5, fmt: str = "text") -> str:
"""Truncate command output to max_lines for standard detail level.
Args:
fmt: One of "markdown", "text", "html", "psa" — controls suffix formatting.
"""
lines = output.splitlines()
if len(lines) <= max_lines:
return output
truncated = "\n".join(lines[:max_lines])
count = len(lines)
if fmt == "markdown":
suffix = f"*(full output omitted — {count} lines)*"
elif fmt == "html":
suffix = f"(full output omitted — {count} lines)"
else: # text, psa
suffix = f"(full output omitted — {count} lines)"
return f"{truncated}\n{suffix}"
```
**Step 2: Apply truncation in `generate_markdown_export`** (line 168)
After `if command_output := _get_command_output(decision):` add:
```python
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="markdown")
```
**Step 3: Apply truncation in `generate_text_export`** (line 255)
After `if command_output := _get_command_output(decision):` add:
```python
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="text")
```
**Step 4: Apply truncation in `generate_html_export`** (line 347)
After `if command_output := _get_command_output(decision):` add:
```python
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="html")
```
**Step 5: Apply truncation in `generate_psa_export`** (line 421)
After `if command_output := _get_command_output(decision):` add:
```python
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="psa")
```
**Step 6: Run tests**
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All pass (default `detail_level="standard"` but existing test command outputs are short).
**Step 7: Commit**
```bash
git add backend/app/services/export_service.py
git commit -m "feat: add format-aware command output truncation for standard detail level"
```
---
## Task 5: Summary Block Generation (B1)
**Files:**
- Modify: `backend/app/services/export_service.py`
Add summary block generation to all 4 generators. The summary is inserted after metadata, before Evidence/Steps. Only rendered when `options.include_summary is True`.
**Design decision — placeholder policy:** Empty fields (no outcome_notes, no next_steps) are left blank (empty string), NOT filled with `[Edit in preview]`. This avoids placeholder text leaking into copied/exported output. The frontend preview textarea is where users add missing info manually.
**Step 1: Add `_build_summary_fields` helper** (after `_get_outcome_label` at line 113)
```python
def _build_summary_fields(session: Session) -> dict[str, str]:
"""Build auto-populated summary fields from session data.
Empty fields are left blank — users fill them in via the editable preview.
"""
tree_name = session.tree_snapshot.get("name", "Troubleshooting Session")
tree_desc = session.tree_snapshot.get("description", "")
issue = f"{tree_name}: {tree_desc}" if tree_desc else tree_name
if session.completed_at:
status = "Resolved" if getattr(session, "outcome", None) == "resolved" else \
f"Completed — {_get_outcome_label(session) or 'Unknown'}"
else:
step_count = len(session.decisions) if session.decisions else 0
status = f"In Progress — paused at step {step_count}" if step_count else "In Progress"
_raw_notes = getattr(session, 'outcome_notes', None)
resolution = (_raw_notes if isinstance(_raw_notes, str) else '').strip()
_raw_next = getattr(session, 'next_steps', None)
next_steps = (_raw_next if isinstance(_raw_next, str) else '').strip()
return {
"issue": issue,
"impact": "",
"status": status,
"resolution": resolution,
"next_steps": next_steps,
}
```
**Step 2: Add `_escape_markdown_table` helper** (right after `_build_summary_fields`)
Pipe characters and newlines in values break markdown table cells:
```python
def _escape_markdown_table(value: str) -> str:
"""Escape value for use in a markdown table cell."""
return value.replace("|", "\\|").replace("\n", " ")
```
**Step 3: Add summary block to `generate_markdown_export`**
Insert after the tree_info block (after line 138, before the scratchpad section):
```python
if options.include_summary:
summary = _build_summary_fields(session)
esc = _escape_markdown_table
lines.append("## Summary")
lines.append("")
lines.append("| Field | Details |")
lines.append("|-------|---------|")
lines.append(f"| Issue | {esc(summary['issue'])} |")
lines.append(f"| Impact | {esc(summary['impact'])} |")
lines.append(f"| Status | {esc(summary['status'])} |")
lines.append(f"| Resolution | {esc(summary['resolution'])} |")
lines.append(f"| Next Steps | {esc(summary['next_steps'])} |")
lines.append("")
lines.append("---")
lines.append("")
```
**Step 4: Add summary block to `generate_text_export`**
Insert after tree_info block (after line 227, before scratchpad section):
```python
if options.include_summary:
summary = _build_summary_fields(session)
lines.append("SUMMARY")
lines.append("-" * 20)
lines.append(f"Issue: {summary['issue']}")
lines.append(f"Impact: {summary['impact']}")
lines.append(f"Status: {summary['status']}")
lines.append(f"Resolution: {summary['resolution']}")
lines.append(f"Next Steps: {summary['next_steps']}")
lines.append("")
```
**Step 5: Add summary block to `generate_html_export`**
Insert after tree_info `` (after line 321, before scratchpad section):
```python
if options.include_summary:
summary = _build_summary_fields(session)
html_parts.append('Summary
')
html_parts.append('')
for label, value in [("Issue", summary["issue"]), ("Impact", summary["impact"]),
("Status", summary["status"]), ("Resolution", summary["resolution"]),
("Next Steps", summary["next_steps"])]:
html_parts.append(f'| {html.escape(label)} | ')
html_parts.append(f'{html.escape(value)} |
')
html_parts.append('
')
```
**Step 6: Add summary block to `generate_psa_export`**
Insert after `lines.append("")` on line 394 (after the header, before PROBLEM section):
```python
if options.include_summary:
summary = _build_summary_fields(session)
lines.append("--- SUMMARY ---")
lines.append(f"Issue: {summary['issue']}")
lines.append(f"Impact: {summary['impact']}")
lines.append(f"Status: {summary['status']}")
lines.append(f"Resolution: {summary['resolution']}")
lines.append(f"Next Steps: {summary['next_steps']}")
lines.append("")
```
**Step 7: Run tests**
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All pass (summary is opt-in, existing tests don't set `include_summary=True`).
**Step 8: Commit**
```bash
git add backend/app/services/export_service.py
git commit -m "feat: add summary block generation to all 4 export generators"
```
---
## Task 6: Editable Preview Modal (B4)
**Files:**
- Modify: `frontend/src/components/session/ExportPreviewModal.tsx`
**Design decision — edit preservation on summary toggle:** Toggling "Include Summary" re-fetches from the backend, which resets `editedContent`. This is documented and expected — the toast says "Summary updated" so the user understands. Edits are lightweight (engineers tweak a few words), so the cost of re-typing is low vs. the complexity of merging diffs.
**Step 1: Replace read-only preview with editable textarea and add controls**
Rewrite the component:
```tsx
import { useState, useEffect } from 'react'
import { Copy, Download, Check, RotateCcw } from 'lucide-react'
import { Modal } from '@/components/common/Modal'
import { cn } from '@/lib/utils'
interface ExportPreviewModalProps {
isOpen: boolean
onClose: () => void
content: string
filename: string
format: 'markdown' | 'text' | 'html' | 'psa'
onDownload: (content: string) => void
includeSummary?: boolean
onToggleSummary?: (include: boolean) => void
}
export function ExportPreviewModal({
isOpen,
onClose,
content,
filename,
format,
onDownload,
includeSummary = false,
onToggleSummary,
}: ExportPreviewModalProps) {
const [copied, setCopied] = useState(false)
const [editedContent, setEditedContent] = useState(content)
// Sync editedContent when content prop changes (new fetch / summary toggle)
useEffect(() => {
setEditedContent(content)
}, [content])
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(editedContent)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
const handleDownload = () => {
onDownload(editedContent)
onClose()
}
const handleReset = () => {
setEditedContent(content)
}
const handleClose = () => {
setCopied(false)
onClose()
}
const isModified = editedContent !== content
return (
{/* Filename, format info, and controls */}
{/* Editable Content */}
)
}
export default ExportPreviewModal
```
Key changes:
- `` → `