# Composer chrome

> What sits around a composer, over a composer host - the agent, model and options pickers, the run chip and context orb, the badge strip, the dock and the picker dialogs.

`zidane-ui/chat/composer` also holds what the desktop app draws around its composer: the controls row (agent, model and options pickers), the status pills (the run chip and the context orb), the badge strip of a conversation, the dock above the prompt, and the picker dialogs its shortcuts open. They read the chrome members of a `ComposerHost`, and each one the host leaves out hides what needs it. The previews below run on the fixture host.

## The host's chrome members

Every member is optional ([`zidane-ui/chat/headless`](https://zidane.si/docs/reference/ui-chat-headless.md)).

| Member | What reads it |
| - | - |
| `controls` | The pickers' writes (`setModel`, `setEffort`, `setModelOptions`, `setAgent`) and their cache estimates (`cacheImpact`). |
| `models` | The model catalog (`catalog(cwd, hostId)`), the Image tab (`images`), provider labels (`providers`). Each is a readable resource. |
| `agents` | The agent profiles a folder offers (`list`), read again on `onChanged`. |
| `prefs` | Picks remembered across apps: effort and options per model, the next chat's model (`lastModelChain`; `lastProvider` / `lastModelByProvider` / `lastAccountByProvider` for the new-session default). |
| `pickerPrefs` | The model picker's per-device preferences (`createModelPickerPrefs`). |
| `context` | A session's gauge (`status`), its breakdown (`breakdown`) and its settled cost (`cost`). |
| `runs` | The current run's counters per session (`meter`), kept by `createRunMeter` from the stream. |
| `hud` | What extensions show with a session: statuses and badges (`onState`, `get`). |

A run meter follows the stream signals you hand it, so keep one per window and feed it from your transport:

```ts
import { createRunMeter } from 'zidane-ui/chat/headless'

const runMeter = createRunMeter()
client.onSignal(signal => runMeter.handle(signal))

const host: ComposerHost = {
  runs: { meter: runMeter.session, noteSeen: runMeter.noteSeen, noteProgress: runMeter.noteProgress, settleCost: runMeter.settleCost },
  // …
}
```

`createServerComposerHost` (`zidane-ui/adapters`) does this over zidane-server: the agent profiles from `agents.list`, the remembered picks in the server's `state.json` (`prefs.get` / `prefs.set`), each offered when the handshake lists it. A session's settled cost and a joined run's progress come from its header (`sessions.getHeader` with `usage`), the strip's snapshot from `hud.get`.

## Controls and status pills

`ComposerControls` is the left cluster: the agent pill, a slot for a retry pill, the model chain picker and, when the model has a reasoning knob or feature toggles, the options pill. With a session the picks write to it; without one they are the next chat's defaults (`defaults`, `onDefaultAgentChange`, `onDefaultModel`). `ComposerStatusPills` is the right cluster: `RunStatusChip` while the session runs and `HostContextOrb` once its context status is known.

```tsx
import { ComposerControls, ComposerStatusPills, useSessionMeters } from 'zidane-ui/chat/composer'

function Footer({ session, running, defaults, setDefaults, refresh }) {
  const meters = useSessionMeters(session, running)
  return (
    <>
      <ComposerControls
        session={session}
        defaults={defaults}
        onDefaultAgentChange={id => setDefaults({ agentId: id })}
        onDefaultModel={pick => setDefaults({ providerKey: pick.providerKey, modelId: pick.modelId })}
        onCommitted={refresh}
        modelId={meters.modelId}
        providerKey={session?.providerKey ?? null}
        activeSupportsReasoning={meters.contextStatus?.supportsReasoning ?? false}
        activeModelOptions={meters.contextStatus?.availableOptions ?? []}
        activeEnabledOptions={meters.contextStatus?.modelOptions ?? {}}
      />
      <ComposerStatusPills
        sessionId={session?.id ?? null}
        running={running}
        contextStatus={meters.contextStatus}
        fetchedCost={meters.fetchedCost}
      />
    </>
  )
}
```

The pickers are also exported one by one (`HostAgentPicker`, `HostModelPicker`, `HostModelOptionsPicker`), for a side chat that resolves its session on the first pick (`commitTo`) and remembers nothing (`remember={false}`). `useCacheHints(sessionId, open)` asks the host what each choice would cost the prompt cache, once per opening. `useCycleAgent` moves a session to its next profile, never mid-run or while it waits on an approval or a question.

## The dock

`ComposerDock` stacks what waits above the prompt, each in a `DockCard` of `DockRow`s that line up with the composer's text. `ApprovalDock` docks the session's next approval (it reads the [approvals host](https://zidane.si/docs/ui-approvals.md) and opens a file edit's review on the modal layer). `ArchivedBanner` stands in for the composer of an archived session. A draft's `ScopeToggle` picks between a folderless chat and a folder. `ServerScopeToggle` is its counterpart on a saved server: the same pill and menu with the server's recent folders (`draft.serverFolders`), its default folder (`draft.remoteRoot`), the local project to clone there on send, Import repository on the server (`draft.importRepoOnServer`) and "Select directory…", which turns the menu into the server's `RemoteFolderBrowser`. Both are built on `ProjectMenu`, the folder pill and its project menu. Its `trigger` opens the same menu from another element: the project home's name uses it.

## The badge strip

`ConversationBadges` draws a conversation's badges: what waits in its inbox (the chat host's interactions), its todos, its queue and running work (the [session host](https://zidane.si/docs/ui-session.md)), and what extensions show with it (`hud`). A badge opens its panel (`onOpenPanel`); the badge of the panel on screen (`onScreen`) closes it. Pass `order` and `onOrderChange` to let the user drag them.

## Dialogs

`ModelPickerDialog`, `EffortPickerDialog` and `AgentPickerDialog` open on the app's modal layer ([`useModal`](https://zidane.si/docs/ui-app.md)). The model dialog commits on close through `commitModelPick`: the session's chain, then the app's default when this computer can run it.

What the pickers read for a session outside its composer (a shortcut): `useSessionContextStatus(session)`, the same keyed `context.status` read as `useSessionMeters`, so one request. `effortPickerTarget(status, session.modelOptions)` (`zidane-ui/chat/headless`) says what the effort dialog sets, `null` when the model has no effort knob and no options: open nothing.

```tsx
const status = useSessionContextStatus(session)
const target = effortPickerTarget(status, session?.modelOptions)
const openEffort = target ? () => modal.open(<EffortPickerDialog session={session} {...target} onApplyEffort={apply} onToggleOption={toggle} />) : undefined
```

## New-session defaults

`useNewSessionDefaults(settings, spaceId)` (`zidane-ui/chat/composer`) is what a new session starts with: the folder last used (`ComposerHost.folders`: `lastUsed`, `setLastUsed`) and its project (`completions.locationOf`), the agent (this chat's pick, the space's, else `build`), the space preset's effort and options, and the model: the space's preset, else the pick the host remembers (`prefs`, its account kept only while `models.catalog` still lists it), else the built-in default. `modelSource` says which (`preset`, `remembered`, `builtin`). `setDefaultModel(pick)` remembers a pick through `prefs` (`{ persist: false }`: this chat only), `setDefaultFolder` and `setDefaultAgentId` set the others.

The desktop app implements `folders` over IPC; zidane-server has no last-used folder, so `createServerComposerHost` leaves it out and a web session starts in the server's first workspace root. The web app passes the agent, the knobs and a preset or remembered model to `sessions.create`, and leaves the model to the server when only the built-in default names one.

## Reference

[`zidane-ui/chat/composer`](https://zidane.si/docs/reference/ui-chat-composer.md), [`zidane-ui/chat/headless`](https://zidane.si/docs/reference/ui-chat-headless.md).
