docs(spec): session UI refinements design

Active/Archive accordions; rename group (client-side reassign); central
area shows only last user entry + gate notify/info + active widget; the
verbose model stream moves to a left on-demand panel toggled by the WIP
icon (moved above the composer). Frontend-only; fine thinking-separation
deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 21:13:28 +02:00
co-authored by Claude Opus 4.8
parent 37d40d6680
commit 3d06505819
@@ -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 `<Transcript>` 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:
<chosen option label(s) / free text> })`.
### Central area ([AppShell.tsx](../../../frontend/src/shell/AppShell.tsx) `<main>`)
- **Remove** `<Transcript />` from the centre.
- Render, in order: the **last user entry** (a compact echo), the **important messages**
(the `notify`/`info` list), and **`<WidgetHost>`** (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 && <WorkingSpinner …/>}`
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 `<button>` with an aria-label).
## Components summary
- **Modify:** `AppShell.tsx` (accordions, central area, WIP icon move, left-panel wiring),
`sessionStore.ts` (`lastUserEntry`, expose important messages), `SteerInput.tsx` +
`WidgetHost.tsx` (record last user entry).
- **Create:** `ModelActivityPanel.tsx` (left drawer, renders `transcript`), `CentralStatus.tsx`
(last user entry + important messages). Group rename reuses `RenameDialog`.
- **Retire from the centre:** `Transcript.tsx` is repurposed/inlined into `ModelActivityPanel`
(or `ModelActivityPanel` renders the same markdown); the `Transcript` component can be
deleted if no longer referenced.
## Testing (vitest + RTL + MSW)
- Rail: Active and Archive render as independent accordions; collapsing one does not swap the
other; archived sessions appear under the Archive accordion, not the active list.
- Rename group: the header rename action opens the dialog; submitting reassigns each member
via `setSessionGroup(member.id, newName)` and invalidates the query; the group header shows
the new name after refresh.
- Central area: `<Transcript>`/full model stream is NOT in the centre; the last user input is
echoed after submitting a steer; a reviewer choice is echoed; `notify`/`info` messages render
centrally; `WidgetHost` still renders the active widget.
- Model-activity panel: hidden by default; clicking the WIP icon opens it and it shows the
streamed model text; opening it closes the documents panel (and vice-versa).
- WIP icon: rendered above the composer (not in the rail header); spins while `working`;
is a keyboard-activatable button.
## Out of scope / deferred
- **Fine-grained thinking separation.** Splitting the model's `thinking_delta` (reasoning)
from its narration into a *separate* sidebar stream is deferred. It would require: the
`SessionBridge` mapping a distinct thinking event (today it only maps `text_delta` and drops
others), AND the user's `~/.pi/agent/extensions/aritmolab-provider.mjs` to stop flattening
`thinking_*`→`text_*` (its `convertEvent`). It also needs live verification, currently
blocked by DNS for the inference host. The whole-`text_delta`-stream-to-sidebar approach
here achieves the user's goal without those cross-boundary changes.
- No backend/harness changes. No atomic group-rename command. No change to how the model
stream is produced.