Read-only document panel, rename, groups, archive (filesystem flag), delete. No verbatim chat persistence — phase documents are the storage. pgvector/app-DB deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
Session Management — Design
Date: 2026-06-29
Status: Approved (design), pending implementation plan
Layers: frontend, backend (Fastify), harness (tht CLI)
Problem
The session rail (NavSessions.tsx) is today a flat list whose only interaction is click = resume (spawns a Pi process). There is no way to read a past session, rename it, organize it, archive it, or delete it. Inspired by Claude's desktop session context menu, we want a complete management surface with five capabilities:
- Vista divisa — a read-only side panel showing the session's stored content.
- Rinomina — rename a session.
- Sposta nel gruppo — organize sessions into groups.
- Archivia — archive a session out of the active list (read-only retention).
- Elimina — permanently delete a session.
Key architectural insight (drives the whole design)
The workflow is phase-based. Each phase persists its own document into the session directory; advancing to the next phase does not require replaying the prior conversation. Therefore the phase documents ARE the persistence — we do not need to store the verbatim chat.
This was the crux decision in brainstorming. It means:
- The "vista divisa" is a read-only document viewer driven by what's on disk, not a chat replay. No new transcript-persistence layer, no correlation of Pi's internal JSONL logs.
- "Resuming an interrupted session" = the normal chat flow re-entering at the last incomplete phase; the earlier phases' documents are the context.
What each phase persists (from workflow.yaml)
| Phase | Document on disk |
|---|---|
| (manifest, always) | session_manifest.yaml — original question, name, status, author, timestamps |
| F3 riscrittura | question.md — revised question + assumptions |
| F4 schema_linking | schema_linking.json |
| F6 cte | cte_plan.json, cte_tests.json |
| F7 sql_finale | sql_final.sql |
| finalize | validation_report.md, evidence.json |
| (decisions, always) | review_decisions.jsonl — decision ledger |
tht session show --json already returns the manifest plus the computed phase
(current phase, folded from the ledger by phase.py)
and has_schema_linking.
Decisions (from brainstorming)
- Read panel content (option C): a status header (
Fase N di 8 · <nome>/Completata/Archiviata) + document cards in phase order (only those that exist) + a collapsible Decisioni block. No verbatim chat. - Resume rule:
status == "finalized"ORarchived == true→ read-only, never resumable. Any other state (open,closed) → resumable into the last incomplete phase. - Row click = open the read-only panel (it no longer resumes). Resume is an explicit "Riprendi" button shown in the panel header only when the session is resumable.
- Archive = filesystem flag. A boolean
archivedfield on the manifest. The active rail filters out archived sessions; a separate "Archivio" view lists them and opens the same read-only panel. (A flag, not a directory move, so id→path resolutionsessions_root / session_idand resume/show keep working unchanged.) - Delete = hard delete of the session directory, behind a confirmation dialog. Available from both the active list and the archive. No trash/soft-delete — the archive is the "soft" tier.
- Groups = manifest field (model A). A nullable
groupstring on the manifest. The rail derives the group list from distinct values. "Sposta nel gruppo ›" lists existing groups + "Nuovo gruppo…" (type a name). No separate registry, no colors/ordering, no persistent empty groups. - Rename = manifest
name(field already exists). The rail showsnameinstead of the question when present.
Data model — manifest changes
In SessionManifest add two fields
(name already exists):
archived: bool = False
group: str | None = None
Both default such that existing manifests load unchanged. They are written by the new
mutating commands below and surfaced by session list/session show.
Harness layer (tht CLI)
New/changed subcommands in
session_cmd.py, with corresponding helpers in
store.py. All mutate the manifest via the
existing touch_manifest/to_yaml pattern and update updated_at/updated_by.
tht session set-name <id> --name <name>→ setmanifest.name.tht session set-group <id> --group <name>→ setmanifest.group(empty string clears it back toNone).tht session archive <id>/tht session unarchive <id>→ fliparchived. (Unarchive is included so an accidental archive is recoverable; it does not change resumability — a finalized session stays read-only.)tht session delete <id>→ remove the session directory (shutil.rmtree). Idempotent error if absent.tht session documents <id> --json→ new read command returning the ordered, available documents for the panel:Document set: original question (from manifest),[ { "phase": "F3", "key": "revised_question", "title": "Domanda rivista", "format": "markdown", "content": "..." }, ... ]question.md,schema_linking.json,sql_final.sql,validation_report.md, and the decision ledger. Only documents that exist on disk are returned. The F6 CTE artifacts (cte_plan.json,cte_tests.json) are intentionally excluded from the v1 panel as intermediate workflow state, not reviewer-facing deliverables; add later if needed. The backend cannot read these files directly because thesessionsroot lives in the workspace YAML (resolved bythtconfig, unknown to Node) — so reading goes throughtht.session list --json→ each row additionally carriesarchived,group,name(in addition to the existingid/status/question/summary/created_at/updated_at/author). The active vs archive split and grouping are done in the frontend from these fields.
Backend layer (Fastify)
New routes in sessions.ts, each delegating to a
new ThtRunner method that shells the matching tht subcommand:
POST /sessions/:id/rename{ name }→tht session set-name.POST /sessions/:id/group{ group }→tht session set-group(group: ""clears).POST /sessions/:id/archive/POST /sessions/:id/unarchive.DELETE /sessions/:id→tht session delete. If a Pi runtime is live for that id, tear it down first (mgr.teardown).GET /sessions/:id/documents→tht session documents --json.GET /sessions→ unchanged route; rows now includearchived/group/name.
Resume guard. POST /sessions/:id/resume must refuse when the session is read-only.
Before spawning, read the manifest (tht.sessionShow); if status == "finalized" or
archived, return 409 with a clear message and do not spawn Pi.
Resume must rebuild phase context (gap to fix). Today PiProcessManager.spawnFor
unconditionally sends /nuova-domanda "kickoff"
(pi-process-manager.ts), and resume
calls the same path — so a "resume" actually kicks off a new question instead of
re-entering the workflow. spawnFor must take a mode: new sessions send
/nuova-domanda, resumed sessions send /riprendi-sessione <id> (the existing prompt
riprendi-sessione.md) so Pi rebuilds
state and lands on the last incomplete phase. Without this, the resume model does not
actually work.
Frontend layer (React + base-ui + Tailwind + React Query)
Types (types.ts)
- Extend
SessionSummarywitharchived: boolean,group: string | null,name: string | null. - Add
SessionDocument { phase: string; key: string; title: string; format: "markdown" | "sql" | "schema-linking" | "decisions" | "text"; content: string }.
API client (sessions.ts)
- Add
renameSession(id, name),setSessionGroup(id, group),archiveSession(id),unarchiveSession(id),deleteSession(id),getSessionDocuments(id).
NavSessions (NavSessions.tsx)
- Row click → open the read-only panel (via a new
onOpenPanel(id)prop), no longer resumes. - Render the active list (filter
!archived) grouped under collapsible group headers derived from distinctgroupvalues, plus a "Senza gruppo" section forgroup == null. - Each row gets a kebab (⋮) menu on hover (base-ui Menu) mirroring the reference
image:
- Vista divisa → open the read-only panel
- Rinomina → rename dialog (sets
name) - Sposta nel gruppo › → submenu of existing groups + "Nuovo gruppo…"
- Archivia →
archiveSession - Elimina (red) → confirmation dialog →
deleteSession
- After each mutation, invalidate the
["sessions"]query.
Archive view
- An "Archivio" entry at the bottom of the rail. Selecting it shows archived sessions
(filter
archived), each opening the same read-only panel. Archived rows offer Vista divisa, Ripristina (unarchive), and Elimina — no resume.
SessionDocumentsPanel (new, left drawer)
- New
frontend/src/shell/SessionDocumentsPanel.tsx. A left column/drawer, opened byonOpenPanel, closed with ✕. - Header: name/question, status chip (
Fase N di 8 · <nome>/Completata/Archiviata), author, created/updated. A "Riprendi" button rendered only when resumable; it triggers the existing resume flow and focuses the center chat. - Body: one card per
SessionDocument, in phase order, rendered by format reusing existing viewers —format: "sql"→ SqlViewer,"schema-linking"→ SchemaLinkingViewer,"markdown"/"text"→ MarkdownView. - Decisioni: the
decisionsdocument rendered as a collapsible list at the bottom.
AppShell (AppShell.tsx)
- Add the left panel column to the existing layout, giving three columns:
[ documents panel (left) | chat (center) | sessions rail (right) ]. - Track two distinct ids: the active chat session (drives
useSessionStream+ the center) and the panel session (read-only, drives the left drawer). Opening the panel does not touch the active chat; "Riprendi" promotes the panel session to the active one.
UX summary
- Layout: three columns, panel on the left as requested, rail stays on the right.
- Context menu: kebab ⋮ on hover, items as above; "Elimina" in red with confirm.
- Rail organization: collapsible group headers + "Senza gruppo"; "Archivio" as a separate section, never mixed into the active list.
Testing approach (TDD per repo convention)
- Harness (pytest): new
store.pyhelpers (set-name/group, archive/unarchive, delete) and thesession documents --jsonshape; manifest round-trips with the new fields;session list --jsonincludes them. - Backend (vitest): each new route maps to the right
thtinvocation (injectedThtRunner/spawn double); resume guard returns 409 for finalized/archived;spawnForsends/riprendi-sessionein resume mode and/nuova-domandain new mode. - Frontend (vitest + RTL + MSW): kebab menu actions call the right endpoints and invalidate the query; grouping/archived filtering renders correctly; the panel renders documents via the right viewers and shows/hides "Riprendi" by resumability; delete-confirm flow.
Out of scope / deferred (with triggers)
- Application database / pgvector. Not introduced. The only DB is the client's
read-only DWH; an app DB is new infrastructure not justified by store/browse/view/
no-resume. Revisit when the archive must outlive the workspace filesystem (→ app
Postgres +
archived_sessions, full-text viatsvector) or when "reuse a semantically similar past session" becomes a goal (→ add avectorcolumn + pgvector). Both are non-breaking additions later. - Keyboard shortcuts (R/A/D/F in the reference image) — polish, addable later.
- Group colors, ordering, persistent empty groups — the model-B registry, only if needed.
- Verbatim chat persistence — explicitly unnecessary given the phase-document model.