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>
277 lines
9.1 KiB
Markdown
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*
|