Files
resolutionflow/docs/archive/2026-02-04-session-scratchpad-design.md
Michael Chihlas 89d343d49a chore: archive 11 completed plan documents
Move completed design/implementation docs from docs/plans/ to docs/archive/
to keep the plans folder focused on active and future work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:51:21 -05:00

277 lines
9.1 KiB
Markdown

# Session Scratchpad Design
> **Date:** February 4, 2026
> **Source:** Feature Ideas Brainstorm - Idea 6
> **Priority:** Must-have (per Michael)
> **Category:** Context capture
---
## Overview
A collapsible right sidebar during active sessions for capturing ambient data — IP addresses, error codes, server names, usernames — anything that doesn't fit a specific decision node's notes field.
**Core principle:** Engineers already accumulate scraps of data during troubleshooting (from `ipconfig`, Event Viewer, phone conversations) that live on sticky notes or in their head. This gives it a home and includes it automatically in the export.
**Data model:** Single freeform `Text` column on the `sessions` table with markdown formatting support. Not JSONB — we're starting simple with a plain text field that the engineer writes in freely.
**Save behavior:** Auto-save with 1000ms debounce. No manual save button.
**Layout:** Collapsible right sidebar in `TreeNavigationPage`, 300px when open, 48px toggle strip when collapsed.
---
## Section 1: Data Model
### Database
New `scratchpad` column on `sessions` table:
```python
# SQLAlchemy model (session.py)
scratchpad: Mapped[Optional[str]] = mapped_column(Text, nullable=True, server_default=sa.text("''"))
```
### Migration
```python
# alembic revision -m "add scratchpad to sessions"
def upgrade():
op.add_column('sessions', sa.Column('scratchpad', sa.Text(), nullable=True, server_default=sa.text("''")))
# Backfill existing rows
op.execute("UPDATE sessions SET scratchpad = '' WHERE scratchpad IS NULL")
def downgrade():
op.drop_column('sessions', 'scratchpad')
```
- `server_default=sa.text("''")` ensures new rows get `""` at the database level
- Backfill normalizes existing rows so no code path sees `NULL`
### Schemas
**SessionUpdate** — add field:
```python
scratchpad: Optional[str] = None
```
**SessionResponse** — add field with validator:
```python
scratchpad: str = ""
@validator('scratchpad', pre=True, always=True)
def normalize_scratchpad(cls, v):
return v or ""
```
**New schema — ScratchpadUpdate:**
```python
class ScratchpadUpdate(BaseModel):
scratchpad: str
```
---
## Section 2: Frontend Component Design
### Layout Restructuring
Current `TreeNavigationPage` is a single-column `container mx-auto` div. Restructure to:
```
┌─────────────────────────────────────┬──────────────┐
│ Existing tree navigation content │ Scratchpad │
│ (header, breadcrumb, node card, │ sidebar │
│ notes, back button, shortcuts) │ (300px) │
│ │ │
└─────────────────────────────────────┴──────────────┘
```
- Outer wrapper: `flex` layout
- Main content: `flex-1 min-w-0` (prevents overflow)
- Sidebar: Fixed 300px open, 48px collapsed
- Collapse state persisted in `localStorage` as `scratchpad-collapsed`
### ScratchpadSidebar Component
**File:** `frontend/src/components/session/ScratchpadSidebar.tsx`
**Props:**
```typescript
interface ScratchpadSidebarProps {
sessionId: string
initialContent: string
onSave: (content: string) => Promise<void>
}
```
**Internal state:**
- `content: string` — textarea value, initialized from `initialContent`, **only reset when `sessionId` changes** (via `useEffect` with `[sessionId]` dependency)
- `isCollapsed: boolean` — sidebar open/closed, persisted in localStorage
- `isSaving: boolean` — network request in flight
- `hasUnsavedChanges: boolean``content !== lastSaved`
- `lastSaved: string` — last successfully saved content
- `showPreview: boolean` — toggle between edit and markdown preview
### Save Flow
1. User types → `content` updates → `hasUnsavedChanges = (content !== lastSaved)`
2. After 1000ms of no typing (debounce), if `hasUnsavedChanges`, fire `onSave(content)`
3. Set `isSaving = true` during request
4. On success: `lastSaved = content`, `isSaving = false`, `hasUnsavedChanges = false`
5. On blur: trigger immediate save if `hasUnsavedChanges` (cancel pending debounce)
6. On error: keep `hasUnsavedChanges = true`, show error indicator
### Save Indicator (bottom of sidebar)
| State | Display |
|-------|---------|
| Idle, no changes | Nothing shown |
| Typing, unsaved | "Unsaved changes" (muted text) |
| Saving | "Saving..." with subtle spinner |
| Just saved | "Saved" with checkmark (green, fades after 2s) |
### beforeunload Warning
- Register `window.addEventListener('beforeunload', handler)` only when `hasUnsavedChanges === true`
- Handler calls `e.preventDefault()` to trigger browser's "unsaved changes" dialog
- Clean up listener on unmount
### Markdown Preview
- Small toggle link below textarea: "Preview" / "Edit"
- Preview renders current `content` using existing `MarkdownContent` component
- No new dependencies required
### Dimensions & Animation
- Open width: 300px
- Collapsed width: 48px (notepad icon button)
- Transition: `width 200ms ease`
- Toggle icon: Notepad/pencil icon from Lucide
---
## Section 3: Backend & API
### New Endpoint
```
PATCH /api/v1/sessions/{id}/scratchpad
```
**Request body:**
```json
{ "scratchpad": "- Server IP: 192.168.1.50\n- Error: 0x80070005" }
```
**Response:** `SessionResponse` (full session object)
**Handler logic:**
1. Load session by ID
2. Verify `session.user_id == current_user.id` (ownership check)
3. Set `session.scratchpad = data.scratchpad`
4. Commit and return updated session
This is intentionally separate from the existing PUT endpoint. Auto-saving the scratchpad every second should not send the full session payload (`path_taken`, `decisions`, `custom_steps`) — that risks overwriting concurrent changes from decision tracking in the same session.
### Frontend API Client
```typescript
// sessions.ts
updateScratchpad: (id: string, content: string) =>
api.patch<Session>(`/api/v1/sessions/${id}/scratchpad`, { scratchpad: content })
```
### Router Registration
Add to `backend/app/api/router.py` — the endpoint lives in the existing `sessions.py` endpoints file.
---
## Section 4: Export Integration
Add a "Evidence / Reference" section to all three export generators, inserted **between the header block and "Troubleshooting Steps"**. Only rendered when `scratchpad.strip()` is non-empty.
### Markdown Export
```markdown
## Evidence / Reference
- Server IP: 192.168.1.50
- Error code: 0x80070005
- Affected user: jsmith@contoso.com
---
## Troubleshooting Steps
```
Scratchpad content rendered as-is (user writes markdown, it appears as markdown).
### Text Export
```
EVIDENCE / REFERENCE
--------------------
- Server IP: 192.168.1.50
- Error code: 0x80070005
- Affected user: jsmith@contoso.com
TROUBLESHOOTING STEPS
---------------------
```
### HTML Export
```html
<h2>Evidence / Reference</h2>
<div class="scratchpad" style="white-space: pre-wrap; background: #f9f9f9; padding: 12px; border-radius: 4px; margin-bottom: 20px;">
- Server IP: 192.168.1.50
- Error code: 0x80070005
</div>
<h2>Troubleshooting Steps</h2>
```
Start with `pre-wrap` for HTML export. Future enhancement: render scratchpad markdown to HTML.
---
## Files Changed
| File | Action | Description |
|------|--------|-------------|
| `backend/app/models/session.py` | Modify | Add `scratchpad` field |
| `backend/app/schemas/session.py` | Modify | Add to `SessionUpdate`, `SessionResponse` + new `ScratchpadUpdate` |
| `backend/app/api/endpoints/sessions.py` | Modify | Add PATCH endpoint + scratchpad in all 3 export generators |
| `backend/app/api/router.py` | Modify | Register new PATCH route |
| `backend/alembic/versions/xxx_add_scratchpad.py` | Create | Migration with backfill |
| `frontend/src/components/session/ScratchpadSidebar.tsx` | Create | New sidebar component |
| `frontend/src/pages/TreeNavigationPage.tsx` | Modify | Add flex layout + render sidebar |
| `frontend/src/api/sessions.ts` | Modify | Add `updateScratchpad` method |
| `frontend/src/types/session.ts` | Modify | Add `scratchpad` to Session type |
---
## Design Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Storage type | `Text` column (not JSONB) | Freeform text is simpler; structured entries are a future enhancement |
| Save mechanism | Auto-save with 1000ms debounce | Reduces friction — engineer never thinks about saving |
| API endpoint | Dedicated PATCH (not reuse PUT) | Prevents scratchpad auto-saves from overwriting concurrent decision updates |
| Conflict detection | None (last-write-wins) | Sessions are single-user; multi-user requires broader solution |
| Markdown rendering | Preview toggle (not live) | Keeps the editing experience simple; preview is optional |
| Collapse persistence | localStorage | Survives page refreshes, per-browser preference |
---
## Pairs With (Future)
- **Command Output Capture** (Idea 3): Structured output at nodes + freeform notes in scratchpad = complete evidence
- **Share Progress** (Idea 2): Scratchpad content included in shared summary
---
*Design finalized during brainstorming session, February 4, 2026*