diff --git a/docs/superpowers/specs/2026-06-29-session-ui-refinements-design.md b/docs/superpowers/specs/2026-06-29-session-ui-refinements-design.md new file mode 100644 index 00000000..799a3ca0 --- /dev/null +++ b/docs/superpowers/specs/2026-06-29-session-ui-refinements-design.md @@ -0,0 +1,155 @@ +# Session UI Refinements — Design + +**Date:** 2026-06-29 +**Status:** Approved (design), pending implementation plan +**Layers:** frontend only (no backend/harness changes). + +## Problem + +Three refinements to the session-management UI (built in the 2026-06-29 session-management +feature): + +1. **Active/Archive is a toggle, should be accordions.** Today a single button at the + bottom of the rail swaps the whole view between the active list and the archive + (`showArchive` state in [AppShell.tsx](../../../frontend/src/shell/AppShell.tsx)). The two + should instead be independent, always-visible collapsible **accordion** sections — the + same pattern already used for the group headers. +2. **No way to rename a group.** Groups are derived from the manifest `group` field; there + is no UI to rename one. +3. **The central area shows the whole conversation.** Today `` renders the + entire streamed `text_delta` model output. The user does not want to see the whole + conversation. The central area should show only the essentials, and the verbose model + stream ("thinking/working") should be available **on demand** in a **left** panel. + +## Decisions (from brainstorming) + +- **Central area = essentials only:** the **last user input / last reviewer choice**, the + gate's **curated "important" messages** (the `notify`/`info` events — currently toasts), + and the **active decision widget** (`WidgetHost`). The full streamed `text_delta` is + **removed** from the centre. +- **Left "Model activity" panel = the verbose model stream, on demand.** The entire + `text_delta` stream (today's `transcript`) moves into a **left** drawer, shown only when + the user opens it. This subsumes the model's "CoT / thinking": the verbose model output + IS the working/thinking. (Fine-grained separation of `thinking_delta` from narration — + which would require changes to the `SessionBridge` and the user's pi provider extension — + is **deferred**; see Out of scope.) +- **The work-in-progress icon moves and becomes the panel trigger.** The `WorkingSpinner` + moves from the right-rail header to **just above the chat composer**. Clicking it + toggles the left "Model activity" panel. It still spins while `working`. +- **"Important phrases" = the gate's `notify`/`info` messages + the active widget's intro** + (the realizable-now interpretation, confirmed). No new model-text parsing. +- **Active/Archive → two accordions**; **rename group** done client-side by reassigning the + group's member sessions (reusing the existing `set-group` mutation). + +## Block 1 — Active / Archive accordions + +Replace the `showArchive` toggle in [AppShell.tsx](../../../frontend/src/shell/AppShell.tsx) +with two collapsible sections, both always rendered: + +- **Active sessions** — expanded by default; contains the existing group accordions + + "No group" section (unchanged internals). +- **Archive (n)** — collapsed by default; contains the archived sessions list. + +Each is a collapsible header (same `▸/▾` button + `aria-expanded` pattern as the group +headers). State: `activeOpen`/`archiveOpen` booleans (or a small `Record`). The bottom +toggle button and `showArchive` state are removed. + +## Block 2 — Rename group + +- **Trigger:** a small **rename affordance on each group header** (an edit/pencil icon + button, or a tiny menu) next to the collapse chevron — visible on hover. +- **Flow:** opens a dialog (reuse [RenameDialog.tsx](../../../frontend/src/shell/RenameDialog.tsx)) + pre-filled with the current group name. On submit with a new non-empty name, reassign every + session currently in that group: `for (s of activeList.filter(s => s.group === old)) await + setSessionGroup(s.id, newName)`, then invalidate `["sessions"]`. +- **Rationale:** groups are a *derived* concept (the manifest `group` field), so renaming = + reassigning members via the existing `setSessionGroup` endpoint. No new backend/harness + code. Non-atomic across N sessions (a mid-loop failure half-renames); acceptable at this + scale, with a `toast.error` on failure. (A future atomic `tht session rename-group` is a + possible enhancement, not needed now.) + +## Block 3 — Central area redesign + Model-activity panel + WIP icon + +### Store ([sessionStore.ts](../../../frontend/src/store/sessionStore.ts)) +- Keep accumulating `text_delta` into `transcript` (now consumed by the LEFT panel, not the + centre). +- Expose the gate `notify`/`info` events for **central** rendering. Concrete rule: the + central list holds the `notify`/`info` messages **since the last recorded user entry** — + it is **cleared whenever `lastUserEntry` is set** (a new user input/choice). So the centre + always shows "the current step": your last action + what the model has surfaced since. (The + store already collects these in `toasts`; add a parallel `stepMessages` array cleared on + `setLastUserEntry`. Whether to also keep the transient toast is an implementation detail — + default: drop the toast for `notify`/`info`, since they now live in the centre.) +- Add `lastUserEntry: { kind: "input" | "choice"; text: string } | null` + a setter. + Reset with the session. + +### Capturing the last user input/choice +- [SteerInput.tsx](../../../frontend/src/shell/SteerInput.tsx): on submit, call + `setLastUserEntry({ kind: "input", text })`. +- [WidgetHost.tsx](../../../frontend/src/shell/WidgetHost.tsx) (and the widget registry's + response path): when the reviewer responds, call `setLastUserEntry({ kind: "choice", text: + })`. + +### Central area ([AppShell.tsx](../../../frontend/src/shell/AppShell.tsx) `
`) +- **Remove** `` from the centre. +- Render, in order: the **last user entry** (a compact echo), the **important messages** + (the `notify`/`info` list), and **``** (the active decision widget — unchanged). +- A new small presentational component (e.g. `CentralStatus`) renders the last-user-entry + + important-messages; `WidgetHost` stays as-is. + +### Model-activity panel (LEFT, on demand) +- A new left drawer `ModelActivityPanel` rendering the active session's `transcript` (the + streamed model text), reusing `react-markdown` like the old `Transcript`. +- Toggled by the WIP icon (below). Header with a title ("Model activity") + a ✕ to close. +- **Left-region coexistence:** the existing read-only + [SessionDocumentsPanel](../../../frontend/src/shell/SessionDocumentsPanel.tsx) (opened by + clicking a rail session) and this Model-activity panel both occupy the left. They are + mutually exclusive: opening one closes the other (a single left-drawer region; track which + is active). The Model-activity panel is for the **active** session; the docs panel is for a + **clicked** (possibly different) session. + +### WIP icon move + trigger +- Remove `WorkingSpinner` from the right-rail header in + [AppShell.tsx](../../../frontend/src/shell/AppShell.tsx) (the `{working && }` + block). +- Place it **just above the chat composer** (in the sticky composer column, above the input + box). It is a **button**: clicking toggles `ModelActivityPanel`. It spins while `working` + (active session && no pending widget) and is still visible (non-spinning) otherwise so it + remains a usable toggle, or only shown when there is model activity/transcript — pick one + in the plan; default: always shown while a session is active, spinning only while `working`. +- Keyboard-accessible (it's a real `