docs: add Phase B export improvements implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
762
docs/plans/2026-02-13-export-phase-b.md
Normal file
762
docs/plans/2026-02-13-export-phase-b.md
Normal file
@@ -0,0 +1,762 @@
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
## 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_export.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'<h3>Step {i}: {question}</h3>')
|
||||
```
|
||||
To:
|
||||
```python
|
||||
is_custom = decision.get("node_id", "").startswith("custom-")
|
||||
custom_badge = '<span style="background: #7c3aed; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.75em; margin-right: 6px;">CUSTOM</span>' if is_custom else ''
|
||||
html_parts.append(f'<h3>{custom_badge}Step {i}: {question}</h3>')
|
||||
```
|
||||
|
||||
**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_export.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 `_truncate_command_output` helper** (after `_get_command_output` at line 91)
|
||||
|
||||
```python
|
||||
def _truncate_command_output(output: str, max_lines: int = 5) -> str:
|
||||
"""Truncate command output to max_lines for standard detail level."""
|
||||
lines = output.splitlines()
|
||||
if len(lines) <= max_lines:
|
||||
return output
|
||||
truncated = "\n".join(lines[:max_lines])
|
||||
return f"{truncated}\n*(full output omitted — {len(lines)} lines)*"
|
||||
```
|
||||
|
||||
**Step 2: Apply truncation in `generate_markdown_export`** (line 168)
|
||||
|
||||
Change:
|
||||
```python
|
||||
if command_output := _get_command_output(decision):
|
||||
```
|
||||
To:
|
||||
```python
|
||||
if command_output := _get_command_output(decision):
|
||||
if options.detail_level == "standard":
|
||||
command_output = _truncate_command_output(command_output)
|
||||
```
|
||||
|
||||
**Step 3: Apply truncation in `generate_text_export`** (line 255)
|
||||
|
||||
Same pattern — add truncation right after `if command_output := _get_command_output(decision):`:
|
||||
```python
|
||||
if options.detail_level == "standard":
|
||||
command_output = _truncate_command_output(command_output)
|
||||
```
|
||||
|
||||
**Step 4: Apply truncation in `generate_html_export`** (line 347)
|
||||
|
||||
Same pattern after `if command_output := _get_command_output(decision):`:
|
||||
```python
|
||||
if options.detail_level == "standard":
|
||||
command_output = _truncate_command_output(command_output)
|
||||
```
|
||||
|
||||
**Step 5: Apply truncation in `generate_psa_export`** (line 421)
|
||||
|
||||
Same pattern after `if command_output := _get_command_output(decision):`:
|
||||
```python
|
||||
if options.detail_level == "standard":
|
||||
command_output = _truncate_command_output(command_output)
|
||||
```
|
||||
|
||||
**Step 6: Run tests**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_export.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 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`.
|
||||
|
||||
**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."""
|
||||
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() or "[Edit in preview]"
|
||||
|
||||
_raw_next = getattr(session, 'next_steps', None)
|
||||
next_steps = (_raw_next if isinstance(_raw_next, str) else '').strip() or "[Edit in preview]"
|
||||
|
||||
return {
|
||||
"issue": issue,
|
||||
"impact": "[Edit in preview]",
|
||||
"status": status,
|
||||
"resolution": resolution,
|
||||
"next_steps": next_steps,
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 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)
|
||||
lines.append("## Summary")
|
||||
lines.append("")
|
||||
lines.append(f"| Field | Details |")
|
||||
lines.append(f"|-------|---------|")
|
||||
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("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
```
|
||||
|
||||
**Step 3: 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 4: Add summary block to `generate_html_export`**
|
||||
|
||||
Insert after tree_info `</div>` (after line 321, before scratchpad section):
|
||||
|
||||
```python
|
||||
if options.include_summary:
|
||||
summary = _build_summary_fields(session)
|
||||
html_parts.append('<h2>Summary</h2>')
|
||||
html_parts.append('<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">')
|
||||
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'<tr><td style="padding: 6px 12px; border: 1px solid #ddd; font-weight: bold; width: 120px;">{html.escape(label)}</td>')
|
||||
html_parts.append(f'<td style="padding: 6px 12px; border: 1px solid #ddd;">{html.escape(value)}</td></tr>')
|
||||
html_parts.append('</table>')
|
||||
```
|
||||
|
||||
**Step 5: 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 6: Run tests**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_export.py -v`
|
||||
Expected: All pass (summary is opt-in, existing tests don't set `include_summary=True`).
|
||||
|
||||
**Step 7: 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`
|
||||
|
||||
**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)
|
||||
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 (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} title="Export Preview" size="xl">
|
||||
{/* Filename, format info, and controls */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<p className="text-sm text-white/70">
|
||||
Filename: <span className="font-mono text-white">{filename}</span>
|
||||
<span className="ml-3 rounded bg-white/10 px-2 py-0.5 text-xs text-white/70">
|
||||
{format === 'markdown' ? 'Markdown' : format === 'html' ? 'HTML' : format === 'psa' ? 'PSA' : 'Plain Text'}
|
||||
</span>
|
||||
{isModified && (
|
||||
<span className="ml-2 text-xs text-yellow-400">(edited)</span>
|
||||
)}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{onToggleSummary && (
|
||||
<label className="flex items-center gap-2 text-sm text-white/60 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeSummary}
|
||||
onChange={(e) => onToggleSummary(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-white/20 bg-black/50"
|
||||
/>
|
||||
Include Summary
|
||||
</label>
|
||||
)}
|
||||
{isModified && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="flex items-center gap-1 text-xs text-white/40 hover:text-white"
|
||||
title="Reset to original"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editable Content */}
|
||||
<textarea
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
className={cn(
|
||||
'h-96 w-full resize-y rounded-md border border-white/10 bg-black/50 p-4',
|
||||
'font-mono text-sm text-white',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-emerald-400" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy to Clipboard
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExportPreviewModal
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `<pre>` → `<textarea>` with `editedContent` local state
|
||||
- Copy/Download use `editedContent` instead of `content` prop
|
||||
- Reset button appears when content is modified
|
||||
- `onDownload` signature changes to `(content: string) => void` to receive edited content
|
||||
- New optional `includeSummary` + `onToggleSummary` props for summary checkbox
|
||||
- `(edited)` indicator when content has been modified
|
||||
|
||||
**Step 2: Build to verify**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||
|
||||
Fix any type errors from the `onDownload` signature change (see Task 7).
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/session/ExportPreviewModal.tsx
|
||||
git commit -m "feat(frontend): convert ExportPreviewModal to editable textarea with reset"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Wire Up Phase B Controls in SessionDetailPage
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/pages/SessionDetailPage.tsx`
|
||||
|
||||
**Step 1: Add state for detail level and include summary** (after line 34)
|
||||
|
||||
```typescript
|
||||
const [detailLevel, setDetailLevel] = useState<'standard' | 'full'>('standard')
|
||||
const [includeSummary, setIncludeSummary] = useState(false)
|
||||
```
|
||||
|
||||
**Step 2: Update `fetchExportContent` to include Phase B options** (line 92-101)
|
||||
|
||||
```typescript
|
||||
const fetchExportContent = async () => {
|
||||
if (!session) return null
|
||||
const options: SessionExport = {
|
||||
format: exportFormat,
|
||||
include_timestamps: true,
|
||||
include_tree_info: true,
|
||||
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
|
||||
detail_level: detailLevel,
|
||||
include_summary: includeSummary,
|
||||
}
|
||||
return await sessionsApi.export(session.id, options)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Update `handleCopyForTicket` to include Phase B options** (line 140-145)
|
||||
|
||||
```typescript
|
||||
const options: SessionExport = {
|
||||
format: 'psa',
|
||||
include_timestamps: true,
|
||||
include_tree_info: true,
|
||||
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
|
||||
detail_level: detailLevel,
|
||||
include_summary: includeSummary,
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Update `handleDownload` to accept content parameter** (line 159)
|
||||
|
||||
Change:
|
||||
```typescript
|
||||
const handleDownload = () => {
|
||||
if (!exportContent || !session) return
|
||||
const blob = new Blob([exportContent], { type: 'text/plain' })
|
||||
```
|
||||
To:
|
||||
```typescript
|
||||
const handleDownload = (content: string) => {
|
||||
if (!session) return
|
||||
const blob = new Blob([content], { type: 'text/plain' })
|
||||
```
|
||||
|
||||
**Step 5: Add `onToggleSummary` handler**
|
||||
|
||||
```typescript
|
||||
const handleToggleSummary = async (include: boolean) => {
|
||||
setIncludeSummary(include)
|
||||
// Re-fetch with new option
|
||||
if (!session) return
|
||||
const options: SessionExport = {
|
||||
format: exportFormat,
|
||||
include_timestamps: true,
|
||||
include_tree_info: true,
|
||||
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
|
||||
detail_level: detailLevel,
|
||||
include_summary: include,
|
||||
}
|
||||
try {
|
||||
const content = await sessionsApi.export(session.id, options)
|
||||
if (content) setExportContent(content)
|
||||
} catch (err) {
|
||||
console.error('Failed to re-fetch export:', err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 6: Add detail level dropdown** to the export controls area (after step cutoff dropdown, before copy button — after line 408)
|
||||
|
||||
```tsx
|
||||
<select
|
||||
value={detailLevel}
|
||||
onChange={(e) => setDetailLevel(e.target.value as 'standard' | 'full')}
|
||||
aria-label="Detail level"
|
||||
className={cn(
|
||||
'rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="full">Full Detail</option>
|
||||
</select>
|
||||
```
|
||||
|
||||
**Step 7: Update ExportPreviewModal props** (line 516-523)
|
||||
|
||||
```tsx
|
||||
<ExportPreviewModal
|
||||
isOpen={showPreview}
|
||||
onClose={() => setShowPreview(false)}
|
||||
content={exportContent || ''}
|
||||
filename={getFilename()}
|
||||
format={exportFormat}
|
||||
onDownload={handleDownload}
|
||||
includeSummary={includeSummary}
|
||||
onToggleSummary={handleToggleSummary}
|
||||
/>
|
||||
```
|
||||
|
||||
**Step 8: Build to verify**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
|
||||
Expected: No type errors.
|
||||
|
||||
**Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/pages/SessionDetailPage.tsx
|
||||
git commit -m "feat(frontend): add detail level dropdown and summary toggle to export controls"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Backend Tests for Phase B Features
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/tests/test_export.py`
|
||||
|
||||
**Step 1: Add test for custom step markers**
|
||||
|
||||
```python
|
||||
def test_custom_step_markers_psa(self, completed_session):
|
||||
"""Custom steps should have [CUSTOM] prefix in export."""
|
||||
# Add a custom step to decisions
|
||||
completed_session.decisions.append({
|
||||
"node_id": "custom-abc123",
|
||||
"question": "Check Additional Logs",
|
||||
"answer": "Found error in event log",
|
||||
"notes": None,
|
||||
"timestamp": "2024-01-01T12:05:00Z",
|
||||
})
|
||||
options = SessionExport(format="psa")
|
||||
result = generate_psa_export(completed_session, options)
|
||||
assert "[CUSTOM] Check Additional Logs" in result
|
||||
```
|
||||
|
||||
**Step 2: Add test for command output truncation**
|
||||
|
||||
```python
|
||||
def test_command_output_truncation_standard(self, completed_session):
|
||||
"""Standard detail level truncates long command output."""
|
||||
long_output = "\n".join(f"line {i}" for i in range(20))
|
||||
completed_session.decisions[0]["command_output"] = long_output
|
||||
options = SessionExport(format="text", detail_level="standard")
|
||||
result = generate_text_export(completed_session, options)
|
||||
assert "*(full output omitted — 20 lines)*" in result
|
||||
|
||||
def test_command_output_full_detail(self, completed_session):
|
||||
"""Full detail level shows all command output."""
|
||||
long_output = "\n".join(f"line {i}" for i in range(20))
|
||||
completed_session.decisions[0]["command_output"] = long_output
|
||||
options = SessionExport(format="text", detail_level="full")
|
||||
result = generate_text_export(completed_session, options)
|
||||
assert "*(full output omitted" not in result
|
||||
assert "line 19" in result
|
||||
```
|
||||
|
||||
**Step 3: Add test for summary block**
|
||||
|
||||
```python
|
||||
def test_summary_block_psa(self, completed_session):
|
||||
"""Summary block appears when include_summary is True."""
|
||||
options = SessionExport(format="psa", include_summary=True)
|
||||
result = generate_psa_export(completed_session, options)
|
||||
assert "--- SUMMARY ---" in result
|
||||
assert "Issue:" in result
|
||||
assert "Status:" in result
|
||||
|
||||
def test_no_summary_by_default(self, completed_session):
|
||||
"""Summary block should not appear by default."""
|
||||
options = SessionExport(format="psa")
|
||||
result = generate_psa_export(completed_session, options)
|
||||
assert "--- SUMMARY ---" not in result
|
||||
```
|
||||
|
||||
**Step 4: Run all export tests**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_export.py -v`
|
||||
Expected: All tests pass including new ones.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/tests/test_export.py
|
||||
git commit -m "test: add tests for custom step markers, detail levels, and summary block"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Final Build Verification
|
||||
|
||||
**Step 1: Run full backend tests**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest --override-ini="addopts=" -v`
|
||||
Expected: All tests pass.
|
||||
|
||||
**Step 2: Run frontend build**
|
||||
|
||||
Run: `cd /c/Dev/Projects/patherly/frontend && npm run build`
|
||||
Expected: Build succeeds with no errors.
|
||||
|
||||
**Step 3: Verify git status is clean**
|
||||
|
||||
```bash
|
||||
git status
|
||||
git log --oneline feat/export-phase-a --not main | head -20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Acceptance Checklist (Manual QA)
|
||||
|
||||
1. **Editable preview:** Open Preview, edit text, verify Copy/Download use edited content. Click Reset to restore original.
|
||||
2. **Summary toggle:** Check "Include Summary" in preview — export re-fetches with summary block. Uncheck removes it.
|
||||
3. **Detail level:** Select "Full Detail", export a session with long command output — no truncation. Switch to "Standard" — output truncated.
|
||||
4. **Custom step markers:** Export a session with custom steps — should show `[CUSTOM]` prefix.
|
||||
5. **Summary block content:** Summary should auto-populate Issue from tree name, Status from completion state, Resolution from outcome_notes.
|
||||
Reference in New Issue
Block a user