# 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*
