Z Engine GUI — Surfaces
Status: canonical · the companion to the UI guide. It says which component and which pure function own each surface of the app: the transcript and composer, the one home of every fact, the side panel and overlays, and the pet. Keep it true when a surface moves.
1. Chat surfaces
- Transcript. Turns from
lib/domain/sessionViewandlib/domain/timeline: user cards (Copy), streamed text with a thinking disclosure, tool calls and a one-line receipt.workSection()(timeline/groups.ts) gathers a turn's thinking and tool calls into one work block ahead of its answer; once the turn is done and made two or more calls,WorkSummaryfolds it into one line (workLine(): "Worked for 1m 12s · read 6 files · ran 2 commands"), with failed calls, agents, questions, plans, errors and compaction still in view. Inside, two or more consecutive calls fold into one run (groupTurnItems(),summarizeRun();ToolGroup);Agent,AskUserQuestionandExitPlanMode(SOLO) never fold. Edit, Write and Bash cards open only on failure; inline diffs reuseDiffRows. A fenced block is aCodeBlockwhose colors arrive once it nears the screen and its reply has finished streaming. - Turn actions.
TurnActions, once at the end of a turn besideTurnFooter: Copy the answer (answerText()), Open changes (the Changes tab) andRewindMenu(Rewind to before this prompt). Compaction dividers that end a turn sit after them (splitTrailingCompactions()). - Receipt.
TurnFooterfollowsui.task_report_viewviareceiptPlan()(lib/domain/receipt.ts):quiet(default) shows only a verdict that says something,compactadds file chips and duration · cost,detailedadds tokens, Not applicable and open checks. - Long chats.
Transcriptrenders a window of turns (timeline/window.ts): the newest 30, 20 more each time the top comes into view, and at once back to a turn you jump to; only the newest 6 render in full, the rest fill in throughwhenVisible()as they near the screen.ChatTimelineis the prompt rail: a dot per prompt (two prompts or more, at most 40 spread bysampleEvenly()); a click jumps there. - Compaction. While
view.compacting(fromcompactionStarteduntil itscompactedmarker, a new main-agent reply, the turn's end or idle) the transcript ends in a "Compacting context…" row. Each marker is a divider inLocalCards, "Context compacted · 180k → 24k · Summary", whose disclosure holds the summary. - Decision surfaces (experimental features, all quiet one-liners):
RouteChip("Routed" or "Subagent routed" · effort · model · reason, fromrouteChosen) andTaskViewDivider("N earlier exchanges (X tokens) set aside for this task", with Include full history on the latest; fromtaskViewApplied) sit inLocalCards; the claim badge "Claimed, not checked" is a warn note inTurnFooter(claimNote()inlib/domain/verification.ts);planning/Suggestionsends the transcript withPlanSuggestion,RuleSuggestionandReviewSuggestion(currentSuggestions()insessionView/suggestions.ts: the current turn's cards, at most five kept, plan first only in Default mode). Only task views survive a reopen. - Approvals and questions wait in the composer, not the transcript (
planning/PendingInteractionsinsideComposer): the oldest approval (focused) or else the oldest question takes the draft's place, the draft is kept, and "N more waiting after this" counts the rest.ApprovalCardis phrased byapprovalQuestion()(lib/domain/approvals.ts): Allow once, Always allow… (aMenu: this chat or this project), Deny… with feedback; y / s / p / n while focused. A plan waiting for review is a "Plan ready · Review" row in the transcript (PlanReady), which opens the Plan tab. Every waiting card or button (and the trust banner) hasdata-pending-card, which the island's button scrolls to and focuses. - Composer. One floating glass surface (
ComposerwithComposerInput,ComposerBar,ComposerPlusMenu,ComposerDropZone,ComposerQueue,ComposerAttachments); the mode and model chips arePopovers, the model one with anEffortRowfor reasoning models. Send and Stop are one button whose glyph swaps (Stop while a turn runs and the draft is empty).
2. Calm by default, one home per fact
Calm by default. At rest a surface says one line; the detail waits behind a Disclosure or a click. Never fold away failures or anything waiting on the user: failed steps show their last lines and cards open, error notices open the island card, outcome notes always show, and approvals, questions, plans and the trust banner stay in view and counted.
One home per fact. A summary may link to its home, and the ⌘K palette and shortcuts may repeat any action, but two views never show the same fact.
| Fact | Home |
|---|---|
| Live status: step, retry, result, passing notice | the island (Island: IslandCapsule, IslandCard) |
| How full the context is | the context satellite (ContextBubble, ContextCard) |
| Other chats that need you | the waiting satellite (WaitingBubble), then the Inbox |
| Notice history, turns that finished in the background | the Inbox (components/inbox/) |
| What changed | the side panel's Changes tab (ChangesButton counts this chat's files) |
| The plan and its progress | the side panel's Plan tab (PlanView) |
| Helpers and background jobs | the side panel's Agents tab (WorkPanel) |
| What the model was sent | the side panel's Context tab (PromptInspector) |
- The island's state is the pure
liveStatus()inlib/domain/liveStatus.ts, highest priority first: this chat needs you, a passing notice, provider retry, work in progress, a turn that just ended, the away recap, idle.islandMode()(lib/domain/island.ts) picks its shape:rest(pet and title),live(a step, retry, notice or result with its clock),alert(this chat needs you, with one Answer / Review / Approve button fromislandAction()that focuses the waiting card) orexpanded(the card).islandLine()makes it one line and at most one number;noticeOpensIsland()decides when a notice opens the card. Add a state there, with a test, rather than a new pill or banner. - The island card (
IslandCard, in the island's own surface, not a popover) summarizes and links to the homes above: the notice and its actions, Plan (done of total and the current step), the latest steps, Agents, Context, Waiting, Cost (this turn and the chat) and the last warnings. It closes on a click outside or Esc, which it takes before the side panel does. - The pet shows the same state as body language (section 4).
- Toasts (
pushToast) show on the island and stay in the Inbox: push one only for what the user cannot see; confirm visible actions at the control. - Sidebar rows carry at most one mark (
sidebarMark()); the open chat shows none, as the island names it. Sidebar controls (New chat, Search, Settings) join the title bar only while the sidebar is hidden.
3. The side panel and overlays
The side panel (sidepanel/SidePanel, SidePanelTabs; sidepanel.css) is one glass card beside the stage with a tab band: Changes, Plan, Agents, Context (panelTabs() in lib/domain/sidePanel.ts; a hidden browser tab is reserved). Its state is ui.panel { tab, open, expanded, width }: openPanel(tab, target?, scope?) opens a tab (Changes at a file and scope), togglePanel(tab?) backs ⌘D, the Changes button and the panel toggle. It resizes from its edge handle between 340 and 1100 px, always leaving the stage 400 px (clampPanelWidth(); the width is kept on this machine), or expands over the stage. panelNudge(), followed by followPanelNudges(), opens the Plan tab for a newly pending plan and marks Agents with a dot for a new helper; switching chats is not news. Tab labels hide when the panel is 420 px wide or less; the empty span in the tab band is the pet's panel perch.
| Surface | Components | Logic (lib/domain/ unless noted) |
|---|---|---|
| Changes tab | overlays/DiffPanel with DiffHeader, DiffFileTree, DiffView, DiffRows, DiffCode, DiffFileBar; under 640 px wide it drops the layout switch and slims the file list, under 560 px it stacks the list above the diff (diff.css) | foldContext(), splitRows() in diffRows.ts; highlightLine() in lib/highlight.ts |
| Plan tab | planning/PlanView (review, edit, approve); PlanReady is its row in the transcript | panelPlan() in plans.ts |
| Agents tab | agents/WorkPanel, ApplyCard, AgentRow, JobRow; AgentSummary (with pet/PetSprite) is the one helper layout for its rows and the transcript's Agent card | agentSections(), agentActivity() in agentTree.ts |
| Context tab | overlays/PromptInspector with InspectorMap (a ring of the context window whose legend filters the outline), InspectorOutline, InspectorInsights, InspectorDecisions (while a decision feature runs or has run); InspectorReader while expanded | outlineGroups() in inspectOutline.ts; decisionFacts(), decisionLine() in decisionTrace.ts |
| Settings (full-window page) | settings/SettingsPage, SettingsNav, ScopeMenu, SettingsGroup; the Experimental page is ExperimentalTab with DecisionModelCard (runtime, connection, Test connection) and its DecisionNativeModel row (download, cancel, resume, remove) | SETTINGS_SECTIONS, searchSettings() in settings/searchIndex.ts; foldGroups() in components/settings/folding.ts; listedFeatures(), featureEntries() in settings/features.ts; card wording in settings/decisionModel.ts and settings/nativeModel.ts |
| Palette | overlays/CommandPalette | paletteActions() in lib/paletteActions.ts; rankPalette() in palette.ts |
| Worktree dialog, shell drawer | overlays/WorktreeDialog, overlays/ShellOverlay (inside Composer) | create_worktree; lib/shellStore.ts |
| Home, Inbox, first run | home/, inbox/, onboarding/ | startersFor(), setupChecklist(); inboxSnapshot(); needsOnboarding(), onboardingPose() |
4. The pet
pet/IslandPetfills the island's slot;pet/PetLayer(inAppShell) draws it while it roams, click-through except the pet, below every popover. Spots register withuse:perch(lib/ui/perch.svelte.ts): slotsisland,hero(home),empty(the Inbox'sEmptyStateart),panel(the side panel's tab band); edgescomposer,sidebar(footer). While the side panel is expanded the stage's perches (STAGE_PERCHES: hero, empty, composer) are out of reach. OnlypetBehavior()inlib/domain/pet/behavior.tsmoves it: a new spot is aPerchIdandPERCH_SIZEinperches.tsplus a rule and a test.- Perches are measured in a frame callback after one appears, goes or resizes, on window resize and scroll, and on the live clock's tick (twice a second while a turn runs or the pet is lively).
petMotion.svelte.tsruns a frame loop only while the pet hops, walks, turns, is thrown, drops or swings from your grip. The 3D pet draws throughpetFrames(lib/pet3d/scheduler.ts), onerequestAnimationFrameloop for every view that runs only while one wants frames (60 fps for moves, blinks and morphs, 30 fps for mood loops and helpers), stops while the document is hidden, and draws once per change under Reduce Motion. Breathing is a CSS loop on the HTML.pet-breathwrapper, so an idle pet costs no frames. - Every screen draws
pet/Pet: the 3DPet3DonceloadPet3D()(petView.ts) has loaded it andlib/pet3din their own chunk, else the SVGPetFlat(while loading, without WebGL, and whilewebgl.lost).lib/pet3d/is the only code that importsthree; its one offscreenWebGLRendererdraws every view and copies it into the view's own 2D canvas. The boot splash andPetSpritestay SVG. A new prop or accessory needs its SVG part and its model (lib/pet3d/props.ts,accessories.ts); a new body motion its CSS keyframes and its entry inlib/domain/pet/rig3d.ts; face shapes live once, infaceShapes.ts. - While the pet roams, the island's slot shows
pet/IslandPortrait, so the island keeps its width: the pet's head (framing="portrait"), turned toward the roaming pet (petUi.at, published byPetLayer) byportraitLook()inlib/domain/pet/portrait.ts, in a ring of the status tone; still under Reduce Motion. The slot, the ring and the Off dot are instyles/island-pet.css. ui.companion: lively reacts to the agent and the user and roams whileui.pet.roam; calm reacts only to the agent and stays in the island; off shows the island's dot. Reactions (petUi.react: a boop, a level-up thatPetLayerplays once the island is not asking for you) go throughreactionPose()for both the island's pet and the roaming one.- It only echoes
liveStatus()(the tone tints its aura, the look its body;aria-hidden): never the only carrier of a fact, never interrupting. Under Reduce Motion its springs jump, it neither walks nor blinks, andpetIdleplays no strolls or tricks. Onlylib/runtime/pet.svelte.tsloads and savespet.json(how the pet works).