Skip to content

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.

A plan mid-task: rename, retry or cancel a step, add one, stop or drop the plan
Stopped when the run ended: edit, resume or drop
Closed: read-only
Applying with no live run: may have been interrupted
A host without plans.edit or plans.control: read-only
No plan yet

Mount it#

Wrap the panels in a SessionProvider and pass the session id. The data and the actions come from the host.

TSX
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#

HostFromTalks to
createServerSessionHost({ client })zidane-ui/adapterszidane-server. Pass the client of your chat host (createServerHost(…).client).
createFixtureSessionHost(fixture)zidane-ui/session/testingMemory, for demos and tests. DEMO_SESSION_FIXTURE is a session mid-task.
Your ownzidane-ui/session/headlessAnything. 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.

MemberGives
plans.snapshot(id), plans.refresh(id)The session's plans (PlansSnapshot: every kept plan and the one worth a glance)
plans.editStep 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.controlStop and drop a plan (and the run applying it)
plans.retryA 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.pushNowQueue 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.removeRemove an artifact
artifacts.refreshRowRe-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.cancelCancel one item, or all
turns.getA stored turn, for the turn details dialog
turns.fork, turns.editText, turns.deleteFork from a turn, edit its text, delete it
turns.rewindCut the history after a turn (inclusive) or just before it: the turn menu's Rewind and Edit message (useTurnCommands)
details.generateTitle, details.titleStatusGenerate a title, and say how the title came about
details.compact, details.export, details.setFolderCompact the context, export the transcript, attach or detach a folder
details.cacheReportWhat the prompt cache did for the last requests. useSessionCacheReport(id, updatedAt) reads it, SessionCacheFacts shows it in the details dialog's cache slot
usage.summaryToken spend over a range, for the welcome screen
usage.providers, usage.githubSubscription windows and GitHub's API limits, for the usage panel
side.lookup, side.create, side.removeThe /side chat about a session: find it, create it on the first send (on the parent's model), delete it on /reset
side.subscribeThe 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#

PartDoes
TodosPanelA 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.
TodoListViewA todo list, read-only, one row per item with its status glyph
TodoTallyOne chip per status, zeros left out
DiffFileTreeChanged files as folders and files with their +/- counts and project findings. buildDiffTree and visibleRows (zidane-ui/session/headless) make its rows.
QueueTabThe queued messages, edited in place (↵ save, ⌘↵ send now). The attachments slot brings your attachment tiles and previews.
ArtifactsPanelWhat the session produced, by kind. onOpenPath and onRevealPath open a path your way; focus brings a group or a row into view.
ArtifactRowChips, DiffFactsAn 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
SessionHeaderThe folder chip, the server chip, the title (renamable in place) and your toolbar
WelcomeScreenThe greeting and the usage card: totals, streaks, a 26-week heatmap, tokens by model, spend by provider
ProjectHomeA 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.
SessionDetailsDialogRename, generate a title, the facts, compact, export, delete. You pass the facts resolved (model and agent names).
TurnDetailsDialogCopy, fork, edit or delete a turn. onForked and onChanged say what follows.
RunningWorkDialogThe running work with Cancel and Cancel all. showTerminal adds Show terminal to a row.
UsagePanelSubscription windows and credits per provider, then GitHub's limits
SessionShellThe dialogs' default frame. Pass your modal shell as renderShell instead.
SideChatA /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, QueueAttentionRender 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.

TodoListView and TodoTally (a todowrite call's list)
  • Read the session store
  • Add the archive column
  • Write the migration test
  • Update the changelog
1 in progress1 done2 pending
DiffFileTree: folders, a rename, findings; click a file
DiffFileTree with a collapsed folder
QueueTab: two queued messages, one with an attachment; click a row to edit it
Nothing queued
A message you send while the agent works waits here, then goes out when the run ends.
ArtifactsPanel: a pull request, a branch and a commit; right-click a row
QueueTab with nothing queued
Nothing queued
A message you send while the agent works waits here, then goes out when the run ends.
ArtifactsPanel with nothing yet
SessionDetailsDialog: facts, folder, cache, compact and export
TurnDetailsDialog: copy, fork, edit, delete
RunningWorkDialog: a tool call and a background task
UsagePanel: subscription windows and GitHub's limits
SessionHeader: the folder, the title (click it to rename here), a toolbar slot
WelcomeScreen: the greeting and the usage card (Overview, Models, ranges)

Good morning

Wednesday, January 15

ProjectHome: the project, the composer, then its blocks (each card's menu moves or hides it)

app

/Users/ada/code/app
Ask anything about app
InboxPanel: a plan at the head, a question behind it
  • Add a rate limiter to the API
    1. A token bucket per key.
    2. A 429 with Retry-After.
    3. Tests.
InboxPanel with nothing waiting
Nothing waiting on you
SideChat before the first question
Ask about this conversation
The side chat sees the agent's work and never interrupts it.

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.

RemindersPanel: yours, the agent's, a paused one; switch to All, add one, open a history
Reminders off in Settings
No reminders yet
TSX
import { RemindersPanel, SessionProvider } from 'zidane-ui/session'

<SessionProvider host={host}>
  <RemindersPanel sessionId={sessionId} />
</SessionProvider>
MemberGives
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.createThe create form
schedules.updatePause, resume, and keep for good (expiresAt: null)
schedules.remove, schedules.runNow, schedules.firesDelete, 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.

TSX
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.

TypeScript
import { describeSessionHostContract } from 'zidane-ui/session/testing/contract'

describeSessionHostContract('my host', backend => makeHarness(backend), ['plans', 'plans.edit', 'plans.control', 'queue', 'artifacts'])
Entry pointHolds
zidane-ui/sessionSessionProvider, the hooks, the panels, the header, the welcome screen, the dialogs, SideChat and the attention drivers
zidane-ui/session/headlessSessionHost, 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/testingcreateFixtureSessionHost and its demo data
zidane-ui/session/testing/contractdescribeSessionHostContract