# 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_parts.append(f'') html_parts.append('
{html.escape(label)}{html.escape(value)}
') ``` **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 */}

Filename: {filename} {format === 'markdown' ? 'Markdown' : format === 'html' ? 'HTML' : format === 'psa' ? 'PSA' : 'Plain Text'} {isModified && ( (edited) )}

{onToggleSummary && ( )} {isModified && ( )}
{/* Editable Content */}