Session panels
The desktop app's session surfaces as React components (the Todos, Queue and Artifacts panels, the session header, the welcome screen and the session dialogs) over a host for plans, the queue, artifacts, running work, turns and usage.
zidane-ui/session holds the desktop app's session surfaces: the Todos, Queue and Artifacts panels, the read-only todo list a todowrite call shows, the Diffs panel's changed-files tree, the session header, the welcome screen, and the dialogs for session details, turn details, running work and usage. They read a SessionHost. The Todos panel shows the session's plan: the one the agent applies, else the one that stopped, else the last that closed (a proposal waiting for a decision shows in the Inbox). The previews below run on the fixture host. Click a step to rename it, cancel or retry it, add one, or stop and drop the plan.
Mount it#
Wrap the panels in a SessionProvider and pass the session id. The data and the actions come from the host.
import 'zidane-ui/styles.css'
import { SessionProvider, TodosPanel } from 'zidane-ui/session'
export function Todos({ host, sessionId }) {
return (
<SessionProvider host={host}>
<TodosPanel sessionId={sessionId} />
</SessionProvider>
)
}AppHostProvider (zidane-ui/app) puts host.session in a SessionProvider for you.
Pick a host#
| Host | From | Talks to |
|---|---|---|
createServerSessionHost({ client }) | zidane-ui/adapters | zidane-server. Pass the client of your chat host (createServerHost(…).client). |
createFixtureSessionHost(fixture) | zidane-ui/session/testing | Memory, for demos and tests. DEMO_SESSION_FIXTURE is a session mid-task. |
| Your own | zidane-ui/session/headless | Anything. createPlansState, createQueueState and createArtifactsState keep one store per session and follow the backend's pushes. |
zidane-server answers stored artifacts, not rendered rows, so the server host shows each artifact's label under its raw kind. The server renders a transcript export (sessions.export) and the server host hands it to saveExport (a browser download, say), offering details.export only with one. The side chat is sessions.sideSession, sessions.create with sideOf and sessions.delete. A row re-resolves on the server (artifacts.refresh, with the extensions of the artifact's folder), a title's status is the server's (sessions.titleStatus) the welcome screen sums the server's store (usage.summary, the server's local days) and the usage panel shows the providers' subscription windows, probed with the server's credentials (usage.providers), and GitHub's API limits as the server sees them (usage.github); each is offered when the connection may call it.
What a host offers#
Every member is optional. A panel hides what needs a member the host leaves out.
| Member | Gives |
|---|---|
plans.snapshot(id), plans.refresh(id) | The session's plans (PlansSnapshot: every kept plan and the one worth a glance) |
plans.edit | Step edits through zidane's plan gate: add, rename, cancel (a step is never deleted). Rejects with the gate's reason. Without it the panel is read-only. |
plans.control | Stop and drop a plan (and the run applying it) |
plans.retry | A step's retry and a stopped plan's resume, sent as a message into the run |
queue.items(id), queue.refresh(id) | The messages sent while the agent works, in delivery order |
queue.add, queue.remove, queue.update, queue.attachments, queue.pushNow | Queue a message, drop it, edit it, read its attachments, send it now |
artifacts.groups(id), artifacts.refresh(id) | What the session produced, by kind: pull requests, branches, commits, files |
artifacts.remove | Remove an artifact |
artifacts.refreshRow | Re-resolve a row when the panel opens and on hover (is the pull request merged?) |
runningWork.items(id), runningWork.refresh(id), runningWork.ensure(id) | The session's cancellable tool calls and background tasks, live |
runningWork.cancel | Cancel one item, or all |
turns.get | A stored turn, for the turn details dialog |
turns.fork, turns.editText, turns.delete | Fork from a turn, edit its text, delete it |
turns.rewind | Cut the history after a turn (inclusive) or just before it: the turn menu's Rewind and Edit message (useTurnCommands) |
details.generateTitle, details.titleStatus | Generate a title, and say how the title came about |
details.compact, details.export, details.setFolder | Compact the context, export the transcript, attach or detach a folder |
details.cacheReport | What the prompt cache did for the last requests. useSessionCacheReport(id, updatedAt) reads it, SessionCacheFacts shows it in the details dialog's cache slot |
usage.summary | Token spend over a range, for the welcome screen |
usage.providers, usage.github | Subscription windows and GitHub's API limits, for the usage panel |
side.lookup, side.create, side.remove | The /side chat about a session: find it, create it on the first send (on the parent's model), delete it on /reset |
side.subscribe | The side chat's pickers follow a model pick made elsewhere |
useSessionPlans(id), useSessionQueue(id), useSessionArtifacts(id) and useSessionRunningWork(id) read one session and re-render only when that session changes. useUsageSummary(range) keeps the last summary across mounts (and in localStorage), so the welcome screen paints its numbers on the first frame. useProviderUsage() reads the subscription windows; a ProviderUsageProvider above it probes every minute while the page is visible and focused, and catches up when it gets focus back (autoLoad={false} leaves only the manual refresh).
Parts#
| Part | Does |
|---|---|
TodosPanel | A session's plan: its steps with rename, retry and cancel, the add box, the tally, stop, resume and drop. A closed plan is read-only. |
TodoListView | A todo list, read-only, one row per item with its status glyph |
TodoTally | One chip per status, zeros left out |
DiffFileTree | Changed files as folders and files with their +/- counts and project findings. buildDiffTree and visibleRows (zidane-ui/session/headless) make its rows. |
QueueTab | The queued messages, edited in place (↵ save, ⌘↵ send now). The attachments slot brings your attachment tiles and previews. |
ArtifactsPanel | What the session produced, by kind. onOpenPath and onRevealPath open a path your way; focus brings a group or a row into view. |
ArtifactRowChips, DiffFacts | An artifact row's id, diff and fact chips; a change size. badgeToneClass(tone) and badgeColorStyle(color) color an extension's badge or icon; BADGE_ICONS lists the icon classes an extension may name |
SessionHeader | The folder chip, the server chip, the title (renamable in place) and your toolbar |
WelcomeScreen | The greeting and the usage card: totals, streaks, a 26-week heatmap, tokens by model, spend by provider |
ProjectHome | A folder's new-chat screen: the project (or the greeting), then every panel on the 'home' surface as a block card loaded through a WorkspacePanels host (panels). Put your composer below it, where a conversation has it, so the first send moves nothing. defaultBlocks names the blocks that show at first (without it, every block shows); the user adds the others. With switcher, a click on the project's name opens the composer's folder menu (ProjectMenu) to switch projects. Customize turns on edit mode: the user drags blocks or moves them with arrow buttons, sets each one full or half width, hides it, and brings hidden blocks back from Add block. Outside edit mode, each card's menu has Refresh and Hide. The arrangement and each block's controls are kept per projectKey. blocksUnavailable says blocks do not load here instead. |
SessionDetailsDialog | Rename, generate a title, the facts, compact, export, delete. You pass the facts resolved (model and agent names). |
TurnDetailsDialog | Copy, fork, edit or delete a turn. onForked and onChanged say what follows. |
RunningWorkDialog | The running work with Cancel and Cancel all. showTerminal adds Show terminal to a row. |
UsagePanel | Subscription windows and credits per provider, then GitHub's limits |
SessionShell | The dialogs' default frame. Pass your modal shell as renderShell instead. |
SideChat | A /side chat about a session, over SessionHost.side and the ChatHost (send, interrupt, transcript). Its composer offers /reset. You draw its transcript (renderTranscript) and, if you like, the model pickers (renderPickers, whose commitTo creates the side chat first). createSideRequests() and parseSideCommand (zidane-ui/session/headless) hand a /side question typed in the conversation's composer to it. |
InboxAttention, TodosAttention, QueueAttention | Render nothing: they badge the Inbox, Todos and Queue tabs (WorkspaceActions.setAttention) for the active session, and a live request opens the Inbox (requestAttention; reveal={false} only badges). Pass actions to route them through your own panel actions. |
The Inbox is the chat area's InboxPanel (zidane-ui/chat/interactions): what the agent waits on in a session (ChatHost.interactions), the head request open as a card, the older ones collapsed under it. A resolved card leaves on the exit motion. focus hands keyboard focus to the head card once, and onEscape gives the keyboard back. useInteractionHead(id), usePendingInteractions(id) and useAllInteractions() read the same requests for badges and dots; useAllInteractions needs interactions.all, which the server host leaves out.
- Read the session store
- Add the archive column
- Write the migration test
- Update the changelog
Session details
/Users/ada/code/app
- Status
- Activity
- Model
- Created
- 654d ago
- Updated
- 653d ago
- ID
- Folder
Turn details
Running work
Usage
No signed-in provider reports usage.
Good morning
Wednesday, January 15
app
- PlanAdd a rate limiter to the API
- A token bucket per key.
- A 429 with Retry-After.
- Tests.
collectSessionDiffs turns a transcript's edit events into one patch per change, and groupByPath groups them by file. The desktop app's Diffs tab reads both.
Reminders#
RemindersPanel is the desktop app's Reminders tab: the reminders of the session beside it, or all of them. It has the create form (the time words, read in the browser's time zone, with a preview of when it sends), and each row's actions: pause and resume, run now, delete, keep a repeat the agent set, and its fire history. It says so when Reminders is off, and renders nothing when the host has no schedules.
import { RemindersPanel, SessionProvider } from 'zidane-ui/session'
<SessionProvider host={host}>
<RemindersPanel sessionId={sessionId} />
</SessionProvider>| Member | Gives |
|---|---|
schedules.status, schedules.refresh(), schedules.ensure() | Whether Reminders is on ({ enabled, supported }). Nothing about reminders shows unless it is enabled. |
schedules.all, schedules.forSession(id) | Every reminder, and those whose prompt goes to one session |
schedules.create | The create form |
schedules.update | Pause, resume, and keep for good (expiresAt: null) |
schedules.remove, schedules.runNow, schedules.fires | Delete, run now, and the fire history |
useSchedulesStatus(), useSessionSchedules(id) and useAllSchedules() read them. createSchedulesState (zidane-ui/session/headless) is the store a host builds on, and readReminderWhen(text, now) reads the time words with a typed reason when it fails. createServerSessionHost offers schedules over schedules.* and reads again on schedules:changed. On the fixture host, control.setSchedules(list) stands in for the agent setting one, and control.setSchedulesEnabled(on) for the setting.
Two more places show reminders. The composer's reminders badge counts the session's reminders that will fire and shows the next time (Next 09:00). It opens the reminders tab when your app has one. A sidebar row with scheduleId (SessionRow.scheduleId) shows a clock mark, and SidebarFilterMenu's origin prop adds the Origin row (All, Scheduled, Not scheduled). Pass origin while Reminders is on.
Session actions#
createSessionActions(deps) (zidane-ui/session/headless) is what the keys, menus and palette do to a session: generateTitle, compact, exportMarkdown / exportJson, remove (asks confirm first, in the sidebar's wording), toggleFavorite, copyId, and fork(sessionId, turnId). It runs over session (SessionHost.details, turns) and sessions (ChatHost.sessions or your directory); each action whose capability is missing is absent. refresh, open, copyText, idOf and onError are your app's. Actions never reject except fork: a failure goes to onError.
sessionKeyCommands(actions, session) binds them to the catalog's session keys for useKeyCommands (zidane-ui/keybindings): none without a session, no favorite on the main session.
const actions = createSessionActions({ session: host.session, sessions: host.chat.sessions, confirm, copyText: (t) => navigator.clipboard.writeText(t) })
useKeyCommands(sessionKeyCommands(actions, session))Test your host#
zidane-ui/session/testing/contract is the suite every host runs, in your own vitest. Build your host over a backend that holds the data it is given, and list the members it offers.
import { describeSessionHostContract } from 'zidane-ui/session/testing/contract'
describeSessionHostContract('my host', backend => makeHarness(backend), ['plans', 'plans.edit', 'plans.control', 'queue', 'artifacts'])| Entry point | Holds |
|---|---|
zidane-ui/session | SessionProvider, the hooks, the panels, the header, the welcome screen, the dialogs, SideChat and the attention drivers |
zidane-ui/session/headless | SessionHost, the stores (plans, queue, artifacts, running work, usage), the usage formatting, the diff models, the /side hand-over (createSideRequests, parseSideCommand), the session actions (createSessionActions, sessionKeyCommands) and the labels |
zidane-ui/session/testing | createFixtureSessionHost and its demo data |
zidane-ui/session/testing/contract | describeSessionHostContract |