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:
@@ -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.
|
||||
Reference in New Issue
Block a user