# zidane/tui

> The terminal UI for zidane, built on React and OpenTUI.

Import: `import { … } from 'zidane/tui'`. Source: [src/tui/index.tsx](https://github.com/Tahul/zidane/blob/main/src/tui/index.tsx).

Use `runTui` to start the full terminal app from your own launcher. It runs on Bun only. The package also exports the screens and components for a custom shell ([Terminal app](https://zidane.si/docs/tui.md)). `RunTuiOptions` takes the `ChatOptions` fields of `resolveConfig` (`prefix`, `storageDir`, `providers`, `agents`, `store`, `cwd`, extension and plugin scan paths). It adds `extensions` (inline extension definitions), `remoteHost` (run every session on one zidane-server) and `onboarding` (`false` never opens the [setup wizard](https://zidane.si/docs/tui.md#first-launch) at launch).

```ts
import { runTui } from 'zidane/tui'

await runTui({ prefix: '.myapp' })
```

## Functions

### `AgentPickerModal`

Function.

Modal that lists the registered [`AgentProfile`](https://zidane.si/docs/reference/chat.md#AgentProfile)s and lets the user
pick one. Each row shows the label (`✓` on the current agent) and the
profile's description, and, with `cacheImpact`, what switching to it
would re-read in a long session.

`App` does not mount this modal: it switches agents with the `cycleAgent`
binding (shift+tab by default). The modal is exported for hosts that want
to drive agent switching from their own composition.

```ts
function AgentPickerModal(
  props: { agents: AgentRegistry; cacheImpact?: CacheImpactFetcher; currentAgentId: string; onPick: (id: string) => void },
): ReactNode
```

**Parameters**

- `props` (`{ agents: AgentRegistry; cacheImpact?: CacheImpactFetcher; currentAgentId: string; onPick: (id: string) => void }`): Props: the agent registry, the id of the current agent, the callback for a pick, and the host's cache estimate.

**Returns** `ReactNode`

Source: [src/tui/agent-picker.tsx:24](https://github.com/Tahul/zidane/blob/main/src/tui/agent-picker.tsx#L24)

### `App`

Function.

Top-level TUI component. Accepts a fully-resolved `ResolvedConfig` and wires
everything (settings, modal layer, screens, footer) underneath it.

Hosts can either drive this via `runTui()` for the standard bootstrap or
mount `<App config={resolveConfig(...)} />` themselves inside a renderer
they already own.

```ts
function App(props: { config: ResolvedConfig; onboarding?: OnboardingGate }): ReactNode
```

**Parameters**

- `props` (`{ config: ResolvedConfig; onboarding?: OnboardingGate }`): Props: the resolved config, and what to do about the setup wizard at launch (`runTui` decides it; none when left out).

**Returns** `ReactNode`

Source: [src/tui/app.tsx:142](https://github.com/Tahul/zidane/blob/main/src/tui/app.tsx#L142)

### `AuthScreen`

Function.

First-run screen that lists the providers and runs the setup wizard for one.

```ts
function AuthScreen(
  props: { initialConfigureKey?: string; onPick: (p: ProviderAuth) => void },
): ReactNode
```

**Parameters**

- `props` (`{ initialConfigureKey?: string; onPick: (p: ProviderAuth) => void }`): Props: the callback for a configured provider, and an optional provider key to open the wizard on.

**Returns** `ReactNode`

Source: [src/tui/auth/auth-screen.tsx:21](https://github.com/Tahul/zidane/blob/main/src/tui/auth/auth-screen.tsx#L21)

### `Badge`

Function.

A colored label, on its own line or inside a row. For badges inside a
`<text>`, use `<span>` with `useTheme().colors`.

```ts
function Badge(props: BadgeProps): ReactNode
```

**Parameters**

- `props` (`BadgeProps`): See [`BadgeProps`](https://zidane.si/docs/reference/tui.md#BadgeProps).

**Returns** `ReactNode`

Source: [src/tui/kit/basics.tsx:28](https://github.com/Tahul/zidane/blob/main/src/tui/kit/basics.tsx#L28)

### `buildMdStyle`

Function.

Convert the renderer-agnostic `Theme.syntax` map (hex strings + plain
booleans) into an OpenTUI `SyntaxStyle`. Used both for the markdown
structural captures (`markup.heading`, `markup.bold`, …) and the
embedded Tree-sitter language tokens (`keyword`, `string`, `function`,
…) — OpenTUI's `<markdown>` re-uses the same `SyntaxStyle` for the
fenced-code renderable, so one table drives both surfaces.

`overrides`, when set, replaces individual entries by token name —
each override is merged INTO the resolved entry (shallow). Used by
the "on-selection" variant to swap `markup.raw.bg` for the row's
selection surface so inline code chips blend into the highlight
rather than reading as "punched out" rectangles. Top-level keys
absent from the base theme become brand-new entries.

```ts
function buildMdStyle(
  theme: Theme,
  overrides?: Record<string, Partial<StyleEntry>>,
  flattenFg?: RGBA,
): SyntaxStyle
```

**Parameters**

- `theme` (`Theme`): Theme to build the style from.
- `overrides?` (`Record<string, Partial<StyleEntry>>`): Per-token style entries merged over the resolved ones, keyed by token name.
- `flattenFg?` (`RGBA`): When set, every token's `fg` is forced to this color and bold/underline
  are dropped. Used for the "dimmed" variant (agent reasoning) so the whole
  markdown block reads as muted secondary text — without it the syntax
  colors (headings, bold, code) paint at full brightness and the block
  doesn't look dim at all. Italic is preserved (it carries no color).

**Returns** `SyntaxStyle`

Source: [src/tui/theme.ts:63](https://github.com/Tahul/zidane/blob/main/src/tui/theme.ts#L63)

### `ChatScreen`

Function.

The chat view: transcript, prompt composer, approval and interaction blocks, and the footer.

```ts
function ChatScreen(props: ChatScreenProps): ReactNode
```

**Parameters**

- `props` (`ChatScreenProps`): Props, see `ChatScreenProps`.

**Returns** `ReactNode`

Source: [src/tui/chat/chat-screen.tsx:221](https://github.com/Tahul/zidane/blob/main/src/tui/chat/chat-screen.tsx#L221)

### `CompletionPopup`

Function.

Popover above the textarea showing the active provider's items. Provider-
agnostic — reads `label` + `description` off each `CompletionItem`. The
TUI hosts can pass any `CompletionState<TItem>`; the popup never needs
to know what `TItem` is.

Geometry: 1 row of chrome + min(N, visibleRows) item rows + 1 hint row.
`flexShrink: 0` pins the height so a long transcript can't squeeze it.

The popup is invisible (`null`-rendered) when `state.active` is null or
`state.items` is empty — the prompt block keeps its layout calm.

Solid `backgroundColor: SURFACE.modal` is load-bearing: in `PromptBlock`
the popup floats over the transcript via `position: absolute`, and a
transparent fill would let the transcript text bleed through. Pairs
with the modal panel surface so floating UI shares one visual identity.

Title overlays are painted on the top border (same trick as
`PromptHints`): provider label on the left in the chip-id's accent
color, match count on the right in dim text. Both ride absolute
positions so they take no flow space; the popup's height comes
entirely from the bordered body. The accent reuses
`resolveChipColor(...).bg` (foreground only, no background pill) so
the picker title still reads as "this is the X provider" without
carrying the chip pill's heavy visual weight.

```ts
function CompletionPopup(
  props: { state: CompletionState<unknown>; visibleRows?: number },
): ReactNode
```

**Parameters**

- `props` (`{ state: CompletionState<unknown>; visibleRows?: number }`): Props: the completion state to show, and the cap on visible rows.

**Returns** `ReactNode`

Source: [src/tui/completion-popup.tsx:44](https://github.com/Tahul/zidane/blob/main/src/tui/completion-popup.tsx#L44)

### `EffortPickerModal`

Function.

Modal for choosing the thinking effort level. `adaptive` is listed only when the provider supports it.

```ts
function EffortPickerModal(
  props: { cacheImpact?: CacheImpactFetcher; current: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive' | undefined; onPick: (effort: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive') => void; supportsAdaptive: boolean },
): ReactNode
```

**Parameters**

- `props` (`{ cacheImpact?: CacheImpactFetcher; current: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive' | undefined; onPick: (effort: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive') => void; supportsAdaptive: boolean }`): Props: the current level, whether adaptive is supported, the pick callback, and the host's cache estimate.

**Returns** `ReactNode`

Source: [src/tui/effort-picker.tsx:42](https://github.com/Tahul/zidane/blob/main/src/tui/effort-picker.tsx#L42)

### `EmptyState`

Function.

What a list or screen shows when it has nothing to show.

```ts
function EmptyState(props: EmptyStateProps): ReactNode
```

**Parameters**

- `props` (`EmptyStateProps`): See [`EmptyStateProps`](https://zidane.si/docs/reference/tui.md#EmptyStateProps).

**Returns** `ReactNode`

Source: [src/tui/kit/basics.tsx:69](https://github.com/Tahul/zidane/blob/main/src/tui/kit/basics.tsx#L69)

### `ExtensionRenderersProvider`

Function.

The base of the renderer tables: empty, so every row falls back until a
layer above adds renderers (`ExtensionRenderersBridge`). Mounted once near the
app root.

```ts
function ExtensionRenderersProvider(props: { children: ReactNode }): ReactNode
```

**Parameters**

- `props` (`{ children: ReactNode }`): Props.

**Returns** `ReactNode`

Source: [src/tui/extension-renderers.tsx:83](https://github.com/Tahul/zidane/blob/main/src/tui/extension-renderers.tsx#L83)

### `ExtensionsSettingsModal`

Function.

List + toggle modal for discovered extensions. Mirrors the Skills /
MCP pickers in shape:

- **Toggleable** rows: every extension that loaded its module
  successfully — both the ones currently running (`loaded`) AND the
  ones the user already toggled off (`inactive` without an error).
  Without surfacing the latter the user can't flip them back on.
- **Preamble** rows: extensions whose import or `setup()` threw.
  Read-only diagnostic so the user can fix the bug or `rm` the
  folder without leaving the TUI.

Enabled state lives in `settings.enabledExtensions` (mirrors
`enabledSkills` / `enabledMcps`). Toggling OFF filters the live
registry immediately; toggling ON an extension that boot skipped
hot-reloads the registry (see "Hot-load on enable" in
`src/tui/extensions/use-extension-auto-reload.ts`), so its `setup()`
runs and its settings appear without a restart.
Agent-side contributions (tools / hooks) apply on the next run.

```ts
function ExtensionsSettingsModal(
  props: { catalog: readonly LoadedExtension[]; inactive?: readonly LoadedExtension[] },
): ReactNode
```

**Parameters**

- `props` (`{ catalog: readonly LoadedExtension[]; inactive?: readonly LoadedExtension[] }`): Props: the active extensions, and the inactive ones (disabled or failed to load).

**Returns** `ReactNode`

Source: [src/tui/extensions-settings.tsx:32](https://github.com/Tahul/zidane/blob/main/src/tui/extensions-settings.tsx#L32)

### `filterListItems`

Function.

The rows of `items` matching `query`, best first. An empty query keeps
every row in order. Matches the label and the description.

```ts
function filterListItems<T extends ListItem>(items: readonly T[], query: string): T[]
```

**Type parameters**

- `T` (`extends ListItem`)

**Parameters**

- `items` (`readonly T[]`): The rows.
- `query` (`string`): What the user typed.

**Returns** `T[]`

Source: [src/tui/kit/list.tsx:61](https://github.com/Tahul/zidane/blob/main/src/tui/kit/list.tsx#L61)

### `Form`

Function.

A form over a settings schema.

```ts
function Form(props: FormProps): ReactNode
```

**Parameters**

- `props` (`FormProps`): See [`FormProps`](https://zidane.si/docs/reference/tui.md#FormProps).

**Returns** `ReactNode`

**Example**

```ts
<Form extensionId="notes" schema={{ items: { title: { type: 'string', label: 'Title' } } }} onSubmit={save} onCancel={close} />
```

Source: [src/tui/kit/form.tsx:112](https://github.com/Tahul/zidane/blob/main/src/tui/kit/form.tsx#L112)

### `initialFormValues`

Function.

The values a form starts with: `values`, else the wire's, else each
item's default. Values that do not fit their item fall back to the default.

```ts
function initialFormValues(
  wire: WireSettings,
  values?: Readonly<Record<string, unknown>>,
): FormValues
```

**Parameters**

- `wire` (`WireSettings`): The wire settings.
- `values?` (`Readonly<Record<string, unknown>>`): Initial values by key.

**Returns** `FormValues`

Source: [src/tui/kit/form.tsx:75](https://github.com/Tahul/zidane/blob/main/src/tui/kit/form.tsx#L75)

### `InteractionBlock`

Function.

Prompt block for a pending interaction from the agent, such as a question or a plan review. For plan requests it shows the title and the decision picker only, see [`PlanReviewPanel`](https://zidane.si/docs/reference/tui.md#PlanReviewPanel).

```ts
function InteractionBlock(
  props: { abortHint?: string; onResolve: (response: InteractionResponse) => void; request: InteractionRequest },
): ReactNode
```

**Parameters**

- `props` (`{ abortHint?: string; onResolve: (response: InteractionResponse) => void; request: InteractionRequest }`): Props: the request, the resolve callback, and the abort key hint.

**Returns** `ReactNode`

Source: [src/tui/interaction-block.tsx:56](https://github.com/Tahul/zidane/blob/main/src/tui/interaction-block.tsx#L56)

### `KeyHint`

Function.

A row of key hints, like the footer of every modal: `↵ open  esc close`.
Clicking a hint presses its key.

```ts
function KeyHint(props: KeyHintProps): ReactNode
```

**Parameters**

- `props` (`KeyHintProps`): See [`KeyHintProps`](https://zidane.si/docs/reference/tui.md#KeyHintProps).

**Returns** `ReactNode`

**Example**

```ts
<KeyHint hints={[{ key: '↵', label: 'open' }, { key: 'esc', label: 'close' }]} />
```

Source: [src/tui/kit/basics.tsx:50](https://github.com/Tahul/zidane/blob/main/src/tui/kit/basics.tsx#L50)

### `List`

Function.

A selectable, optionally searchable list.

```ts
function List<T extends ListItem = ListItem>(props: ListProps<T>): ReactNode
```

**Type parameters**

- `T` (`extends ListItem`, default `ListItem`)

**Parameters**

- `props` (`ListProps<T>`): See [`ListProps`](https://zidane.si/docs/reference/tui.md#ListProps).

**Returns** `ReactNode`

**Example**

```ts
<List searchable items={notes.map(n => ({ id: n.id, label: n.title }))} onSelect={item => open(item.id)} />
```

Source: [src/tui/kit/list.tsx:92](https://github.com/Tahul/zidane/blob/main/src/tui/kit/list.tsx#L92)

### `Markdown`

Function.

Draws markdown.

```ts
function Markdown(props: MarkdownProps): ReactNode
```

**Parameters**

- `props` (`MarkdownProps`): See [`MarkdownProps`](https://zidane.si/docs/reference/tui.md#MarkdownProps).

**Returns** `ReactNode`

**Example**

```ts
<Markdown>{'# Notes\n\n- one\n- two'}</Markdown>
```

Source: [src/tui/kit/markdown.tsx:30](https://github.com/Tahul/zidane/blob/main/src/tui/kit/markdown.tsx#L30)

### `McpsSettingsModal`

Function.

MCP server picker. Hierarchical: each server is one row, and an
expanded server reveals one row per advertised tool. The full surface:

```
▶ linear  5 of 7  Signed in  stdio  npx -y mcp-remote …      ━━● ▼
      create_issue  Create a new Linear issue                ━━●
      list_issues  Query issues by status / assignee         ━━●
      update_issue  Update fields on an existing issue       ●──
  chrome-devtools  stdio  npx -y chrome-devtools-mcp@latest  ━━● ◀
```

`▶` marks the cursor row; the chevron after the switch says whether a
server is open (`▼`) or closed (`◀`).

Keybindings:

↑ / ↓ / k / j      navigate (over servers AND visible tool rows)
→ / tab            expand the focused server
← / shift+tab      collapse — on a tool row, also snaps the cursor
back to the parent server
enter / space      toggle — server toggles `enabledMcps`; tool toggles
`disabledMcpTools[serverName]`
l                  sign in (server rows only — needs-auth / error)
o                  sign out (server rows only — authed / error /
authorizing)
r                  refresh discovery
esc                close — also cancels an in-flight `authorizing`
row

Status badge legend (server row only):

(the shared `MCP_STATUS_LABEL` words, same as the desktop app)
Signed in         tokens stored + bootstrap connected
Needs sign-in     OAuth wanted, or the server refused the connection
Signing in…       interactive sign-in in flight
Sign-in failed    the last sign-in attempt failed
Couldn't connect  failed for another reason
Key refused       refused the key in its headers

The `N of M` badge on a server header counts ENABLED / TOTAL advertised
tools, drawn only when the tool catalog has loaded. Without a catalog
(server never bootstrapped this profile, cache cold) the badge is
suppressed and the empty-state row under the expanded server explains
how to populate.

Errors (`DiscoveryError[]`) surface in a warn-colored preamble so a
broken `mcps.json` is loud rather than invisible.

```ts
function McpsSettingsModal(
  props: { catalog: readonly DiscoveredMcp[]; errors?: readonly DiscoveryError[]; onCancelLogin: (name: string) => void; onLogin: (name: string) => void | Promise<void>; onLogout: (name: string) => void; onRefresh?: () => void | Promise<void>; toolsByServer: Record<string, readonly McpToolSchema[]> },
): ReactNode
```

**Parameters**

- `props` (`{ catalog: readonly DiscoveredMcp[]; errors?: readonly DiscoveryError[]; onCancelLogin: (name: string) => void; onLogin: (name: string) => void | Promise<void>; onLogout: (name: string) => void; onRefresh?: () => void | Promise<void>; toolsByServer: Record<string, readonly McpToolSchema[]> }`): Props: the discovered MCP servers, discovery errors, tools per server, and the login, logout, cancel and refresh callbacks.

**Returns** `ReactNode`

Source: [src/tui/mcps-settings.tsx:73](https://github.com/Tahul/zidane/blob/main/src/tui/mcps-settings.tsx#L73)

### `MdStyleProvider`

Function.

Gives the markdown views below their syntax styles, built from the current theme.

```ts
function MdStyleProvider(
  props: { children: ReactNode },
): FunctionComponentElement<ProviderProps<MdStyleBundle | null>>
```

**Parameters**

- `props` (`{ children: ReactNode }`): The `children`.

**Returns** `FunctionComponentElement<ProviderProps<MdStyleBundle | null>>`

Source: [src/tui/theme.ts:146](https://github.com/Tahul/zidane/blob/main/src/tui/theme.ts#L146)

### `Modal`

Function.

Responsive modal — picks a width (and optionally a height) based on the
live terminal size.

- On a wide terminal, the modal grows to `maxWidth` so descriptions sit on
  one line and don't wrap.
- On a narrow terminal, the modal shrinks down to `minWidth`, keeping a
  small horizontal margin from the screen edges. Text inside wraps naturally.
- When `maxHeight` is set, the same tier logic applies on the vertical
  axis — anything beyond is the consumer's job (typically a `scrollbox`
  child for long content).

Uses `useTerminalDimensions()` so it reflows on `SIGWINCH` without remount.

```ts
function Modal(props: ModalProps): ReactNode
```

**Parameters**

- `props` (`ModalProps`): Props, see [`ModalProps`](https://zidane.si/docs/reference/tui.md#ModalProps).

**Returns** `ReactNode`

Source: [src/tui/modal.tsx:282](https://github.com/Tahul/zidane/blob/main/src/tui/modal.tsx#L282)

### `ModalRoot`

Function.

Provides the modal layer: renders the active modal over its children, closes it on `esc` or a click outside the panel, and exposes `ModalApi` through [`useModal`](https://zidane.si/docs/reference/tui.md#useModal).

```ts
function ModalRoot(props: { children: ReactNode }): ReactNode
```

**Parameters**

- `props` (`{ children: ReactNode }`): Props: the app subtree to render under the overlay.

**Returns** `ReactNode`

Source: [src/tui/modal.tsx:49](https://github.com/Tahul/zidane/blob/main/src/tui/modal.tsx#L49)

### `ModelOptionsPickerModal`

Function.

Modal for the active model's reasoning effort and feature toggles.

```ts
function ModelOptionsPickerModal(
  props: { cacheImpact?: CacheImpactFetcher; currentEffort: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive' | undefined; enabled: Record<string, boolean> | undefined; onPickEffort: (effort: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive') => void; onToggleOption: (optionId: string) => void; options: readonly ModelOption[]; supportsAdaptive: boolean; supportsReasoning: boolean },
): ReactNode
```

**Parameters**

- `props` (`{ cacheImpact?: CacheImpactFetcher; currentEffort: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive' | undefined; enabled: Record<string, boolean> | undefined; onPickEffort: (effort: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive') => void; onToggleOption: (optionId: string) => void; options: readonly ModelOption[]; supportsAdaptive: boolean; supportsReasoning: boolean }`): Props: what the model supports, the current effort, the options and which are on, and the effort and toggle callbacks.

**Returns** `ReactNode`

Source: [src/tui/model-options-picker.tsx:41](https://github.com/Tahul/zidane/blob/main/src/tui/model-options-picker.tsx#L41)

### `ModelPickerModal`

Function.

Modal for choosing a model across the authenticated providers, with search. With `onApplyChain` it becomes an ordered multi-select for the primary model and its fallbacks.

```ts
function ModelPickerModal(
  props: { cacheImpact?: CacheImpactFetcher; chain?: readonly ModelChainEntry[]; current: PickedModel; modelsFor: (key: string, account?: string) => readonly ModelInfo[]; onApplyChain?: (chain: ModelChainEntry[]) => void; onPick?: (picked: PickedModel) => void; providers: readonly ProviderAuth[] },
): ReactNode
```

**Parameters**

- `props` (`{ cacheImpact?: CacheImpactFetcher; chain?: readonly ModelChainEntry[]; current: PickedModel; modelsFor: (key: string, account?: string) => readonly ModelInfo[]; onApplyChain?: (chain: ModelChainEntry[]) => void; onPick?: (picked: PickedModel) => void; providers: readonly ProviderAuth[] }`): Props: the providers, the model resolver, the current pick, the pick or chain callbacks, and the host's cache estimate.

**Returns** `ReactNode`

Source: [src/tui/model-picker.tsx:134](https://github.com/Tahul/zidane/blob/main/src/tui/model-picker.tsx#L134)

### `onInputSubmit`

Function.

`@opentui/react` extends `React.JSX.IntrinsicElements`, so `onSubmit` on `<input>`
gets intersected with the DOM `SubmitEvent` shape and demands an unhelpful overload.
The OpenTUI input runtime fires `(value: string) => void`; this helper isolates
the cast so each call site stays readable.

```ts
function onInputSubmit(handler: (value: string) => void): never
```

**Parameters**

- `handler` (`(value: string) => void`): Called with the input value on submit.

**Returns** `never`

Source: [src/tui/components.tsx:30](https://github.com/Tahul/zidane/blob/main/src/tui/components.tsx#L30)

### `PlanReviewPanel`

Function.

The plan BODY half of plan review — pairs with [`InteractionBlock`](https://zidane.si/docs/reference/tui.md#InteractionBlock)
(which renders only the title + decision picker for `plan` requests).
The chat screen hides the Transcript (same `visible: false` pattern as
the file-edit approval modal) and mounts this panel in its place, so
the plan body gets the full conversation area: always expanded, one
scrollbox, no preview/collapse state. Hosts embedding these standalone
must mount BOTH — the picker alone shows a plan the user cannot read.

```ts
function PlanReviewPanel(props: { request: PlanRequest }): ReactNode
```

**Parameters**

- `props` (`{ request: PlanRequest }`): Props: the plan request to show.

**Returns** `ReactNode`

Source: [src/tui/interaction-block.tsx:194](https://github.com/Tahul/zidane/blob/main/src/tui/interaction-block.tsx#L194)

### `registerTuiHostModules`

Function.

Map `react` / `@opentui/*` bare imports to THIS module's live instances
for everything imported afterwards — extensions included. Exported (and
idempotent) because ordering is the whole game: any host that imports
extension modules BEFORE `runTui()` (the dev CLI builds its inline
dev-extension list up front) must call this first, or those modules
resolve their own react copy and cache it forever — hooks then throw
against the reconciler's copy ("Invalid hook call" → failed renderer).
And it must live HERE, not in the caller: the caller's own `react`
import may itself resolve a different copy (the dev CLI's does — the
tui workspace carries its own node_modules/react), while this module's
static imports are by definition the ones the reconciler runs.

```ts
function registerTuiHostModules(): void
```

Source: [src/tui/index.tsx:120](https://github.com/Tahul/zidane/blob/main/src/tui/index.tsx#L120)

### `renderHintSpans`

Function.

Pure renderer for a list of [`Hint`](https://zidane.si/docs/reference/chat.md#Hint)s as colored spans — the same
keys and labels as `HintRow`, `HINT_GAP` blank columns apart,
without the click targets.

Returns spans only (no enclosing `<text>`, no leading / trailing
whitespace) so the caller can wrap it in whichever container fits
their surface: the bottom-bar footer uses a single-row `<text>`, the
prompt-box overlay wraps it with leading + trailing spaces so the
outermost cells punch through the border like a native title would.

Pure function over an opaque `ThemeColors`, no internal hook calls so it
composes inside any `<text>` regardless of where the parent grabbed its
palette.

```ts
function renderHintSpans(hints: readonly Hint[], COLOR: ThemeColors): ReactNode
```

**Parameters**

- `hints` (`readonly Hint[]`): Hints to render, in order.
- `COLOR` (`ThemeColors`): Theme colors to paint them with.

**Returns** `ReactNode`

Source: [src/tui/components.tsx:444](https://github.com/Tahul/zidane/blob/main/src/tui/components.tsx#L444)

### `runTui`

Function.

Boots the full chat TUI and runs it until the user quits.
Resolves the config, loads inline and disk extensions, and starts the renderer. It never returns: it ends the process with `process.exit(0)` once the renderer is dismissed. Throws when called a second time in one process. To keep control after the renderer closes, mount [`App`](https://zidane.si/docs/reference/tui.md#App) on your own renderer instead.

It exits the process because, after the renderer's `destroy()` restores the
terminal, React's reconciler and OpenTUI's internal listeners can keep the
event loop open. The script then hangs in `bun run`, and `bun --watch run`
waits forever for the child to exit.

Environment overrides:

- `ZIDANE_STORAGE_DIR` sets `storageDir`.
- `ZIDANE_PREFIX` sets `prefix`.
- `ZIDANE_DEBUG` enables `gatherStats` on the renderer and writes the final
  fps and frame-time stats to stderr on exit.

Renderer fps comes from the `targetFps` setting (the settings modal offers
30, 60 and 120). The stored value seeds the renderer at boot and `AppShell`
mirrors later changes onto the live renderer.

```ts
function runTui(options?: RunTuiOptions): Promise<never>
```

**Parameters**

- `options?` (`RunTuiOptions`, default `{}`): Chat options plus inline `extensions` and a `remoteHost` to run every session on a remote zidane server.

**Returns** `Promise<never>`

**Example**

```ts
import { BUILTIN_AGENTS, BUILTIN_PROVIDERS, defineExtension } from 'zidane/chat'
import { runTui } from 'zidane/tui'

await runTui({ prefix: '.myapp' })
await runTui({ storageDir: '/data', prefix: 'myapp' })
await runTui({ providers: { ...BUILTIN_PROVIDERS, mine: myDescriptor } })
await runTui({ agents: { ...BUILTIN_AGENTS, debug: myDebugProfile } })

// Inline extension bundled with a custom CLI binary. Disk discovery
// (.zidane/extensions/<name>/) still runs alongside.
await runTui({
  extensions: [
    defineExtension({
      apiVersion: 1,
      id: 'my-host-ext',
      setup(ctx) { ctx.hooks.on('tool:after', e => ctx.logger.info('ran', e)) },
    }),
  ],
})
```

Source: [src/tui/index.tsx:183](https://github.com/Tahul/zidane/blob/main/src/tui/index.tsx#L183)

### `SessionDetailsModal`

Function.

Modal with the details of one session: usage, environment and space, with actions to delete, favorite and move it.

```ts
function SessionDetailsModal(
  props: { actions: SessionDetailsModalActions; isCurrent: boolean; keybindings: KeyBindings; session: SessionData; title?: string },
): ReactNode
```

**Parameters**

- `props` (`{ actions: SessionDetailsModalActions; isCurrent: boolean; keybindings: KeyBindings; session: SessionData; title?: string }`): Props: the session data, an optional title, whether it is the open session, the actions and the key bindings.

**Returns** `ReactNode`

Source: [src/tui/session-details-modal.tsx:197](https://github.com/Tahul/zidane/blob/main/src/tui/session-details-modal.tsx#L197)

### `SessionsScreen`

Function.

Screen that lists sessions with search and lets the user open or create one.

```ts
function SessionsScreen(props: SessionsScreenProps): ReactNode
```

**Parameters**

- `props` (`SessionsScreenProps`): Props: the sessions, the current and focused ids, the pick, create and focus callbacks, and the search and scope settings.

**Returns** `ReactNode`

Source: [src/tui/sessions/sessions-screen.tsx:183](https://github.com/Tahul/zidane/blob/main/src/tui/sessions/sessions-screen.tsx#L183)

### `SettingsModal`

Function.

The settings modal. It shows the settings categories, then the optional tabs the host wires: keybindings, authentication, skills, MCP servers, extensions and logs.

```ts
function SettingsModal(props?: SettingsModalProps): ReactNode
```

**Parameters**

- `props?` (`SettingsModalProps`, default `{}`): Optional props: model catalog, logs, authentication, actions and the app version.

**Returns** `ReactNode`

Source: [src/tui/settings-modal.tsx:108](https://github.com/Tahul/zidane/blob/main/src/tui/settings-modal.tsx#L108)

### `SkillsSettingsModal`

Function.

List + toggle modal for discovered skills. State machine + keyboard +
row geometry live in `<ToggleListModal>`; this file just supplies the
skill-specific column (description) and the empty-state hint.

Pass `onRefresh` to expose `ctrl+r` (force re-scan) on the list. The
standalone modal is for embedders — the in-app flow goes through
`<SettingsModal>` which exposes the same affordance via its own
`SettingsActions.onRefreshSkills` plumbing.

```ts
function SkillsSettingsModal(
  props: { catalog: readonly SkillConfig[]; onRefresh?: () => void | Promise<void> },
): ReactNode
```

**Parameters**

- `props` (`{ catalog: readonly SkillConfig[]; onRefresh?: () => void | Promise<void> }`): Props: the skills to list, and an optional refresh callback bound to `ctrl+r`.

**Returns** `ReactNode`

Source: [src/tui/skills-settings.tsx:17](https://github.com/Tahul/zidane/blob/main/src/tui/skills-settings.tsx#L17)

### `Spinner`

Function.

Bar spinner in the warning color with an optional dim label.

```ts
function Spinner(props: { label?: string }): ReactNode
```

**Parameters**

- `props` (`{ label?: string }`): Props: an optional label shown after the glyph.

**Returns** `ReactNode`

Source: [src/tui/status-spinner.tsx:17](https://github.com/Tahul/zidane/blob/main/src/tui/status-spinner.tsx#L17)

### `StatusSpinner`

Function.

Bar spinner painted in a caller-chosen color: the session-title status
icon and the folded-exchange marker. Five cells wide.

```ts
function StatusSpinner(props: { color: string }): ReactNode
```

**Parameters**

- `props` (`{ color: string }`): Props: the color of the glyph.

**Returns** `ReactNode`

Source: [src/tui/status-spinner.tsx:33](https://github.com/Tahul/zidane/blob/main/src/tui/status-spinner.tsx#L33)

### `ToggleListModal`

Function.

Generic list-with-checkboxes modal. Powers both the Skills and MCP
server pickers — same state machine, same keyboard map, same row
geometry. Per-feature variance flows in through props:

- `keyOf` — extract the persisted identity (skill name, server name).
- `settingKey` — `'enabledSkills'` | `'enabledMcps'`.
- `renderDetail` — appended to each row in mute color (descriptions,
  transports, …). Optional.
- `emptyState` — replacement content when `catalog` is empty.

Renderer-agnostic state machine lives in `useEnabledToggleSet`
(chat layer) — a GUI shell can build its own toggle list against the
same hook without pulling OpenTUI.

```ts
function ToggleListModal<T>(
  props: { catalog: readonly T[]; emptyState: ReactNode; gcKeys?: readonly string[]; keyOf: (entry: T) => string; onRefresh?: () => void | Promise<void>; preamble?: ReactNode; renderDetail?: (entry: T) => ReactNode; settingKey: EnabledAllowlistKey; title: string },
): ReactNode
```

**Type parameters**

- `T`

**Parameters**

- `props` (`{ catalog: readonly T[]; emptyState: ReactNode; gcKeys?: readonly string[]; keyOf: (entry: T) => string; onRefresh?: () => void | Promise<void>; preamble?: ReactNode; renderDetail?: (entry: T) => ReactNode; settingKey: EnabledAllowlistKey; title: string }`): Props: the catalog, how to key and describe rows, the setting key, the title and the empty state.

**Returns** `ReactNode`

Source: [src/tui/toggle-list-modal.tsx:28](https://github.com/Tahul/zidane/blob/main/src/tui/toggle-list-modal.tsx#L28)

### `Transcript`

Function.

Transcript rows are keyed by array index on purpose.

Identity keys look more correct and are worse here: the streaming tail event
is a *new object* on every \~16ms drain tick (coalescing copies the accumulated
text into fresh state), so keying by identity hands the hot row a new key
every tick and remounts its whole `<markdown>` subtree — measured as the
markdown renderable id incrementing once per tick, versus staying put with
index keys.

The cost of index keys is that `revealEarlierTranscriptEvents` prepends a
page, shifting every index and remounting the list once. That is a rare,
user-initiated action; the streaming tail is 60 times a second. Fixing both
needs a stable id on `StreamEvent` itself, which is a data-model change
rather than a rendering one.

```ts
function Transcript(
  props: { afterTurn?: { render: (turnId: string) => ReactNode; turnIds: ReadonlySet<string> }; authStops?: AuthStopActions; busy?: boolean; events: StreamEvent[]; interactionPending?: boolean; loading?: boolean; onLoadEarlier?: LoadEarlier; onOpenSessionEvent?: (sessionId: string) => void; onOpenTurn?: (turnId: string) => boolean | void; onReachBottom?: () => void; onResume?: (text: string) => void; onSelectTurn?: (turnId: string) => boolean | void; pinSeq?: number; retry?: RunRetry | null; searchPending?: boolean; searchQuery?: string; selectedTurnId?: string | null; settings: Settings },
): ReactNode
```

**Parameters**

- `props` (`{ afterTurn?: { render: (turnId: string) => ReactNode; turnIds: ReadonlySet<string> }; authStops?: AuthStopActions; busy?: boolean; events: StreamEvent[]; interactionPending?: boolean; loading?: boolean; onLoadEarlier?: LoadEarlier; onOpenSessionEvent?: (sessionId: string) => void; onOpenTurn?: (turnId: string) => boolean | void; onReachBottom?: () => void; onResume?: (text: string) => void; onSelectTurn?: (turnId: string) => boolean | void; pinSeq?: number; retry?: RunRetry | null; searchPending?: boolean; searchQuery?: string; selectedTurnId?: string | null; settings: Settings }`): Props: the stream events, settings, the selected turn, the busy flag and the search state.

**Returns** `ReactNode`

Source: [src/tui/transcript/transcript.tsx:44](https://github.com/Tahul/zidane/blob/main/src/tui/transcript/transcript.tsx#L44)

### `TuiExtensionHostProvider`

Function.

Provides `host` to an extension's components.

```ts
function TuiExtensionHostProvider(props: TuiExtensionHostProviderProps): ReactNode
```

**Parameters**

- `props` (`TuiExtensionHostProviderProps`): See [`TuiExtensionHostProviderProps`](https://zidane.si/docs/reference/tui.md#TuiExtensionHostProviderProps).

**Returns** `ReactNode`

Source: [src/tui/kit/host.tsx:70](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L70)

### `TurnDetailsModal`

Function.

Modal with the full content of one turn, with actions to fork, delete or edit it.

```ts
function TurnDetailsModal(
  props: { actions: TurnDetailsModalActions; index: number; keybindings: KeyBindings; searchQuery?: string; toolResultTurns?: SessionTurn[]; total: number; turn: SessionTurn },
): ReactNode
```

**Parameters**

- `props` (`{ actions: TurnDetailsModalActions; index: number; keybindings: KeyBindings; searchQuery?: string; toolResultTurns?: SessionTurn[]; total: number; turn: SessionTurn }`): Props: the turn, its tool-result turns, the search query, its position, the actions and the key bindings.

**Returns** `ReactNode`

Source: [src/tui/turn-details-modal.tsx:68](https://github.com/Tahul/zidane/blob/main/src/tui/turn-details-modal.tsx#L68)

### `useExtension`

Function.

The extension the component belongs to. Throws outside a [`TuiExtensionHostProvider`](https://zidane.si/docs/reference/tui.md#TuiExtensionHostProvider).

```ts
function useExtension(): TuiExtensionInfo
```

**Returns** `TuiExtensionInfo`

Source: [src/tui/kit/host.tsx:82](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L82)

### `useExtensionRenderers`

Function.

Live renderer tables for the transcript. Empty (all-fallback) when unmounted.

```ts
function useExtensionRenderers(): ExtensionRenderers
```

**Returns** `ExtensionRenderers`

Source: [src/tui/extension-renderers.tsx:71](https://github.com/Tahul/zidane/blob/main/src/tui/extension-renderers.tsx#L71)

### `useMdStyle`

Function.

Active markdown / syntax-highlighting style. Returns a single shared
`SyntaxStyle` instance for the active theme — built once at provider
mount, re-built on theme switch. A `Settings.theme` flip re-paints every
`<markdown>` that reads this hook.

Pass `{ selected: true }` to get the on-selection variant where inline
code chips' background matches the row's selection surface (so the
chips blend into the highlight rather than reading as punched-out
rectangles).

Throws if used outside `<MdStyleProvider>` so a missing wiring shows up
loudly in development rather than silently rendering plain text.

```ts
function useMdStyle(opts?: { dimmed?: boolean; selected?: boolean }): SyntaxStyle
```

**Parameters**

- `opts?` (`{ dimmed?: boolean; selected?: boolean }`, default `{}`): Variant to get: `selected` for rows with a selection surface, `dimmed` for muted text such as reasoning.

**Returns** `SyntaxStyle`

Source: [src/tui/theme.ts:180](https://github.com/Tahul/zidane/blob/main/src/tui/theme.ts#L180)

### `useModal`

Function.

Reads the modal API of the nearest [`ModalRoot`](https://zidane.si/docs/reference/tui.md#ModalRoot). Throws when used outside one.

```ts
function useModal(): ModalApi
```

**Returns** `ModalApi`: The API to open and close a modal.

Source: [src/tui/modal.tsx:141](https://github.com/Tahul/zidane/blob/main/src/tui/modal.tsx#L141)

### `useModalAwareFocus`

Function.

Focus computed against the modal layer.

Pass a component's preferred focus state and this returns `false` whenever a
modal is open — so focused inputs (textarea, selects) release their focus and
stop intercepting keys behind the overlay. Pair with `focusable={false}` on
"passive" focusables (scrollbox) so the renderer doesn't cycle focus into
them when the primary input blurs.

```ts
function useModalAwareFocus(preferred?: boolean): boolean
```

**Parameters**

- `preferred?` (`boolean`, default `true`): Whether the component wants focus when no modal is open.

**Returns** `boolean`

Source: [src/tui/modal.tsx:189](https://github.com/Tahul/zidane/blob/main/src/tui/modal.tsx#L189)

### `useRowVisible`

Function.

`true` once the row has been scrolled into the viewport (± `margin`
rows of warm-up) — LATCHED: it never flips back to `false`, because
its job is gating a row's INITIAL async work, not re-triggering on
every scroll flicker (re-run-on-revisit belongs to the author's own
state keys). Reports `true` immediately when no visibility tracker is
mounted. Throws outside an extension-rendered row.

```ts
function useRowVisible(margin?: number): boolean
```

**Parameters**

- `margin?` (`number`, default `5`): Rows of warm-up around the viewport that count as visible.

**Returns** `boolean`

Source: [src/tui/extension-renderers.tsx:214](https://github.com/Tahul/zidane/blob/main/src/tui/extension-renderers.tsx#L214)

### `useRpc`

Function.

A typed RPC client for `contract`, stable across renders.

```ts
function useRpc<C extends RpcContract<Readonly<Record<string, RpcMethodSpec>>, Readonly<Record<string, RpcEventSpec>>>>(
  contract: C,
): RpcClient<C>
```

**Type parameters**

- `C` (`extends RpcContract<Readonly<Record<string, RpcMethodSpec>>, Readonly<Record<string, RpcEventSpec>>>`)

**Parameters**

- `contract` (`C`): A contract made by `Rpc.define`.

**Returns** `RpcClient<C>`: The client; its methods reject with `RpcCallError`.

Source: [src/tui/kit/host.tsx:92](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L92)

### `useSessionState`

Function.

Durable per-row state for renderer components — the cache that lets an
async row fetch ONCE and rehydrate on reload instead of refetching:

const \[data, setData] = useSessionState\<Pr | null>('pr', null)
useEffect(() => { if (visible && !data) fetchPr().then(setData) }, \[visible, data])

Persisted in the session's extension-state bag under the OWNING
extension's namespace, keyed `renderer:<localId>:<turnId>:<id>` — the
same value the extension can inspect via `ctx.session.get`. Values must
be JSON-safe (enforced by the type). Rows without a turn id (shouldn't
exist — live rows pre-mint theirs) degrade to volatile memory state.
Throws when called outside an extension-rendered row.

```ts
function useSessionState<T extends JsonValue>(
  id: string,
  initial: T,
): [T, (next: T) => void]
```

**Type parameters**

- `T` (`extends JsonValue`)

**Parameters**

- `id` (`string`): Key of the value, unique within the renderer.
- `initial` (`T`): Value used until one has been stored.

**Returns** `[T, (next: T) => void]`

Source: [src/tui/extension-renderers.tsx:189](https://github.com/Tahul/zidane/blob/main/src/tui/extension-renderers.tsx#L189)

### `useSettings`

Function.

The extension's effective settings values by key, live.

```ts
function useSettings(): Readonly<Record<string, unknown>>
```

**Returns** `Readonly<Record<string, unknown>>`

Source: [src/tui/kit/host.tsx:127](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L127)

### `useStore`

Function.

A store's value and setter. The component draws again when the value
changes, from this window or any other client.

```ts
function useStore<T = unknown>(
  name: string,
  location?: TuiStoreLocation,
): readonly [T | undefined, (value: T) => Promise<void>]
```

**Type parameters**

- `T` (default `unknown`)

**Parameters**

- `name` (`string`): The store name the extension declared.
- `location?` (`TuiStoreLocation`): Scope and session.

**Returns** `readonly [T | undefined, (value: T) => Promise<void>]`: `[value, set]`; `value` is `undefined` while the extension declares no such store, and `set` rejects then.

Source: [src/tui/kit/host.tsx:107](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L107)

## Constants

### `Footer`

Constant.

Footer status bar. Renders as a single row when the terminal is wide
enough, otherwise stacks the context indicator beneath the hint row.
Width tiering is driven by plain-text length estimates — close enough
since the segments are ASCII-heavy.

Memoized: AppShell re-renders on every stream drain tick, and the footer's
props settle between ticks (its clocks run on its own 1s tickers).

```ts
const Footer: MemoExoticComponent<(__namedParameters: { compactionStartedAt?: number | null; compactionTokens?: number | null; context: ContextUsage | null; contextKey?: string | null; cost?: number | null; costEstimated?: boolean; daemonHealth?: DaemonHealth | null; hints: Hint[]; hostLabel?: string | null; retry?: { attempt: number; max: number; source?: string } | null; run?: { settledOutputTokens: number; startedAt: number } | null; status?: 'retrying' | 'busy' | 'compacting' | 'asking' | 'searching' | null; streaming?: { elapsedMs: number; estimated: boolean; outputTokens: number; turnId?: string } | null; usage?: FooterUsage | null; usageKey?: string | null }) => ReactNode>
```

Source: [src/tui/components.tsx:72](https://github.com/Tahul/zidane/blob/main/src/tui/components.tsx#L72)

### `TitleOverlay`

Constant.

Colored title for a full-screen bordered surface. The `title` slot
rides `titleColor` (defaults to a per-character gradient across the
theme's throbber pair, fallback brand→accent) on the LEFT of the top
border; the optional `meta` slot rides on the RIGHT.

`meta` accepts either:

- a plain `string` → rendered entirely in `COLOR.dim` (the simple
  "single dim label" case, e.g. `"5 sessions"`).
- a [`MetaSegment`](https://zidane.si/docs/reference/tui.md#MetaSegment) array → rendered concatenated with each
  segment's own color, or as badges (build split values with
  `metaBadges`).

Every span carries an explicit background (`bg`, defaulting to
`SURFACE.background`). OpenTUI leaves the existing cell char in place
when it paints a space with no background, so a transparent overlay
lets the underlying border `─` bleed through every gap in the title
(reads as a strikethrough) and lets a native `title=` on the same row
interleave its characters with the overlay's. Opaque spans overpaint
the whole run — which also means the `bg` has to MATCH the surface
under the decorated box (pass `SURFACE.modal` inside a modal panel, as
`McpAuthorizingPanel` does), or the overlay stamps a mismatched run
along the border row. Both directions are pinned in
`test/tui/title-overlay-collision.test.tsx`.

Responsive behavior (driven by `useTerminalDimensions`):

- Both fit → render both with a 2-cell visual gap between them.
- Meta doesn't fit → drop meta; title takes the full budget.
- Title doesn't fit → truncate with a trailing `…`.
- Terminal is degenerately narrow → render nothing.

The width math assumes the immediate parent fills the screen's
content area (the standard `flexGrow: 1` flex column). Hosts running
the bordered box inside a narrower container should pass `parentWidth`
to override the terminal-width assumption.

```ts
const TitleOverlay: MemoExoticComponent<(__namedParameters: { action?: TitleOverlayAction | null; bg?: string; clickKey?: string | null; meta?: string | readonly MetaSegment[] | null; parentWidth?: number; statusIcon?: ReactNode; statusIconCells?: 1 | 2; title: string; titleColor?: string }) => ReactNode>
```

**Example**

```ts
<box style={{ flexDirection: 'column', flexGrow: 1 }}>
  <box style={{ border: true, flexGrow: 1 }}>...</box>
  <TitleOverlay
    title="my session"
    meta={metaBadges(['#abcd', '5 turns'])}
  />
</box>
```

Source: [src/tui/components.tsx:640](https://github.com/Tahul/zidane/blob/main/src/tui/components.tsx#L640)

## Interfaces

### `BadgeProps`

Interface.

Props of [`Badge`](https://zidane.si/docs/reference/tui.md#Badge).

```ts
interface BadgeProps
```

**Properties**

- `children` (`string`): The badge text.
- `tone?` (`BadgeTone`): Default `'neutral'`.

Source: [src/tui/kit/basics.tsx:15](https://github.com/Tahul/zidane/blob/main/src/tui/kit/basics.tsx#L15)

### `ContextUsage`

Interface.

Context window usage shown in the footer.

```ts
interface ContextUsage
```

**Properties**

- `max` (`number`): Size of the context window in tokens.
- `used` (`number`): Tokens in use.

Source: [src/tui/components.tsx:43](https://github.com/Tahul/zidane/blob/main/src/tui/components.tsx#L43)

### `EmptyStateProps`

Interface.

Props of [`EmptyState`](https://zidane.si/docs/reference/tui.md#EmptyState).

```ts
interface EmptyStateProps
```

**Properties**

- `children?` (`ReactNode`): Extra content under the text (a hint row).
- `description?` (`string`): How to get some.
- `title` (`string`): What is missing.

Source: [src/tui/kit/basics.tsx:55](https://github.com/Tahul/zidane/blob/main/src/tui/kit/basics.tsx#L55)

### `ExtensionRenderers`

Interface.

Renderer lookups the transcript reads.

```ts
interface ExtensionRenderers
```

**Properties**

- `custom` (`(id: string) => ExtensionCustomRenderer | null`): Resolve a namespaced custom-renderer id, `null` when unavailable.
- `tool` (`(tool: string) => readonly ExtensionToolRendererEntry[]`): Tool renderers to consult for a tool's result row, in order. The row
  takes the first non-null render; an empty array means default
  rendering. Entries carry `owner` so hooks inside the winning renderer
  can persist under the right extension namespace.

Source: [src/tui/extension-renderers.tsx:48](https://github.com/Tahul/zidane/blob/main/src/tui/extension-renderers.tsx#L48)

### `FooterUsage`

Interface.

Subscription-usage summary for the footer: the most-constrained window of
the active provider (see `mostConstrainedWindow` in
`chat/provider-usage.ts`). A view type, like [`ContextUsage`](https://zidane.si/docs/reference/tui.md#ContextUsage). The footer
knows a label and a percent, not which endpoint they came from.

```ts
interface FooterUsage
```

**Properties**

- `label` (`string`): Short window label, e.g. `5h`, `weekly`, `credits`.
- `usedPercent` (`number`): 0-100.

Source: [src/tui/components.tsx:56](https://github.com/Tahul/zidane/blob/main/src/tui/components.tsx#L56)

### `FormProps`

Interface.

Props of [`Form`](https://zidane.si/docs/reference/tui.md#Form).

```ts
interface FormProps
```

**Properties**

- `active?` (`boolean`): Whether the form takes keys. Default `true`.
- `extensionId?` (`string`): The extension the schema belongs to.
- `onAction?` (`(key: string) => void`): Runs an action row. Without it, action rows are not drawn.
- `onCancel?` (`() => void`): Called on `esc`.
- `onSubmit` (`(values: FormValues) => void`): Called with every value on `ctrl+s`.
- `schema?` (`SettingsSchema`): The schema to draw. Needs `extensionId`. Ignored when `wire` is given.
- `values?` (`Readonly<Record<string, unknown>>`): Initial values by key; else the wire's values, else the defaults.
- `wire?` (`WireSettings`): The wire form of the settings (`ctx.ui.form` and settings pages send it).

Source: [src/tui/kit/form.tsx:30](https://github.com/Tahul/zidane/blob/main/src/tui/kit/form.tsx#L30)

### `KeyHintProps`

Interface.

Props of [`KeyHint`](https://zidane.si/docs/reference/tui.md#KeyHint).

```ts
interface KeyHintProps
```

**Properties**

- `hints` (`readonly (false | Hint | null | undefined)[]`): The hints, left to right. Falsy entries are skipped.

Source: [src/tui/kit/basics.tsx:35](https://github.com/Tahul/zidane/blob/main/src/tui/kit/basics.tsx#L35)

### `ListItem`

Interface.

One row of a [`List`](https://zidane.si/docs/reference/tui.md#List).

```ts
interface ListItem
```

**Properties**

- `badges?` (`readonly BadgeEntry[]`): Badges at the end of the row.
- `description?` (`string`): Muted text after the title; also searched.
- `disabled?` (`boolean`): Drawn quieter and never picked.
- `id` (`string`): Unique in the list.
- `label` (`string`): The row's title.

Source: [src/tui/kit/list.tsx:19](https://github.com/Tahul/zidane/blob/main/src/tui/kit/list.tsx#L19)

### `ListProps`

Interface.

Props of [`List`](https://zidane.si/docs/reference/tui.md#List).

```ts
interface ListProps<T extends ListItem = ListItem>
```

**Type parameters**

- `T` (`extends ListItem`, default `ListItem`)

**Properties**

- `active?` (`boolean`): Whether the list takes keys. Default `true`; pass `false` while another part of the screen has focus.
- `emptyText?` (`string`): Shown when no row matches. Default `Nothing here` / `No match`.
- `initialId?` (`string`): The row the cursor starts on.
- `items` (`readonly T[]`): The rows to draw, in order (`searchable` re-ranks them by the filter).
- `maxVisible?` (`number`): Rows drawn at once. Default 10.
- `onHighlight?` (`(item: T | undefined) => void`): Called when the cursor lands on another row.
- `onSelect` (`(item: T) => void`): Called with the row under the cursor on `↵` or a click.
- `placeholder?` (`string`): Placeholder of the filter line.
- `searchable?` (`boolean`): Shows a filter line; typing narrows the rows.

Source: [src/tui/kit/list.tsx:33](https://github.com/Tahul/zidane/blob/main/src/tui/kit/list.tsx#L33)

### `MarkdownProps`

Interface.

Props of [`Markdown`](https://zidane.si/docs/reference/tui.md#Markdown).

```ts
interface MarkdownProps
```

**Properties**

- `children` (`string`): The markdown source.
- `dim?` (`boolean`): Muted text, like the agent's reasoning.
- `streaming?` (`boolean`): Still receiving text: an open fence stays open. Default `false`.

Source: [src/tui/kit/markdown.tsx:12](https://github.com/Tahul/zidane/blob/main/src/tui/kit/markdown.tsx#L12)

### `MetaSegment`

Interface.

One run of styled text in a [`TitleOverlay`](https://zidane.si/docs/reference/tui.md#TitleOverlay) meta segment array.
`color` defaults to `COLOR.dim` (the standard secondary tone for
meta info); supply an explicit color to accent a value — typical use
is a `warn` tint on a primary stat (turn count, session count) while
surrounding separators stay `mute`.

```ts
interface MetaSegment
```

**Properties**

- `badge?` (`BadgeTone`): Drawn as a badge in this tone (`color` ignored).
- `border?` (`boolean`): Drawn as the border line (`color` ignored): see `META_CHIP_GAP`.
- `clickKey?` (`string`): Keybinding spec a click on the segment synthesizes (click == keypress,
  `clickable.tsx`); hover paints it in the accent. Unparseable: inert.
- `color?` (`string`): Foreground color. Defaults to `COLOR.dim`.
- `text` (`string`): Text of the segment.

Source: [src/tui/components.tsx:519](https://github.com/Tahul/zidane/blob/main/src/tui/components.tsx#L519)

### `ModalProps`

Interface.

Props of [`Modal`](https://zidane.si/docs/reference/tui.md#Modal).

```ts
interface ModalProps
```

**Properties**

- `bottomTitle?` (`string`): Secondary label rendered on the bottom border, right-aligned. A plain
  string, so one short phrase (e.g. "3 before, 6 after" on the
  turn-details modal); several values belong in `rightTitle` as badges.
  Kept short: long strings collide with the bottom-right corner glyph.
- `children` (`ReactNode`): Content of the modal.
- `disableEscape?` (`boolean`): When true, Modal's built-in Esc-to-close handler is suppressed for
  this render. The child stays free to register its own `useKeyboard`
  and handle Esc however it wants (cancel a pending confirmation,
  block dismissal mid-async, …). `@opentui/react`'s `useKeyboard`
  registers independent listeners with no propagation, so a child's
  `return` inside its own handler can't stop this Modal's default
  handler from firing on the same keystroke — `disableEscape` is the
  declarative escape hatch that closes that race.
- `fullscreen?` (`boolean`): Fill the terminal edge-to-edge (minus margins), tracking the live
  terminal size exactly. Overrides the `maxWidth` / `minWidth` /
  `maxHeight` tiers — for workbench-style surfaces (settings) where
  more room is strictly better and a centered floating panel wastes
  the margins. Content must still bring its own `scrollbox` for
  overflow, same as `maxHeight`.
- `horizontalMargin?` (`number`): Columns of breathing room kept on each side of the modal.
- `maxHeight?` (`number`): Hard cap on the modal's height in rows. When set, the modal grows
  to this size when the terminal allows and shrinks down to fit on
  smaller screens. Without it, the modal grows to fit its children
  (uncapped) — fine for picker-style modals, problematic for
  unbounded content (long turn previews, etc.) where the modal could
  exceed the viewport and bury its esc-hint footer.
- `maxWidth?` (`number`): Preferred width in columns. Modal grows to this when the terminal allows.
- `minWidth?` (`number`): Floor on the width, so content stays minimally legible.
- `onClose?` (`() => void`): Called when the user presses esc. Defaults to `useModal().close()` if available.
- `rightTitle?` (`ReactNode`): Right-aligned overlay rendered on the **top** border, next to the
  title slot. Use it for live counters / status badges, as
  `<TitleBadges items={[{ text: '3 in progress', tone: 'warn' }, '2 completed']} />`
  on the todos modal. Same scissor-rect
  trick as `TitleOverlay` / `CompletionPopup` — painted as an
  absolutely-positioned sibling so OpenTUI's scissor doesn't clip
  the border row.
- `title?` (`string`): Title shown on the top border.
- `verticalMargin?` (`number`): Rows of breathing room kept above + below the modal when `maxHeight` is set.

Source: [src/tui/modal.tsx:204](https://github.com/Tahul/zidane/blob/main/src/tui/modal.tsx#L204)

### `PickedModel`

Interface.

A model choice: provider and model id.

```ts
interface PickedModel
```

**Properties**

- `account?` (`string`): The stored account of the provider (credential id), when it has several; absent = its active one.
- `modelId` (`string`): Id of the model.
- `providerKey` (`string`): Provider the model belongs to.

Source: [src/tui/model-picker.tsx:70](https://github.com/Tahul/zidane/blob/main/src/tui/model-picker.tsx#L70)

### `QueuedPreview`

Interface.

One entry in the queued-messages preview list shown above the prompt
input. Only the displayable text + the user-provided ref spans are
surfaced — the `references` carry-through happens entirely in the
App layer (see `messageQueueRef`); this view doesn't need to know
about them.

```ts
interface QueuedPreview
```

**Properties**

- `attachments?` (`number`): How many attachments were captured with the prompt. A COUNT rather than the
  attachments themselves: the row only needs to say they exist, and holding
  the payloads in a render prop would keep every queued file's bytes alive
  for as long as the queue is on screen.

  Absent or `0` renders no badge.
- `refs?` (`readonly { end: number; providerId: string; start: number }[]`): Optional chip spans from the original prompt (`@files`, `/skills`)
  — passed through so the queued entry highlights skill / file names
  the same way the echoed user-prompt event does once it lands in
  the transcript.
- `text` (`string`): Raw prompt text the user typed (no `❯` prefix).

Source: [src/tui/queue/queued-messages-block.tsx:18](https://github.com/Tahul/zidane/blob/main/src/tui/queue/queued-messages-block.tsx#L18)

### `QueueShortcuts`

Interface.

Resolved keybinding specs the queue UI surfaces to the user. Each
field is the literal string from `keybindings.json` ("ctrl+return",
"backspace" — or whatever the user has configured) formatted for
display via [`formatBindingForDisplay`](https://zidane.si/docs/reference/chat.md#formatBindingForDisplay) at render time so any
customization (e.g. `"cmd+up"`, `"delete"`) shows the actual key.

```ts
interface QueueShortcuts
```

**Properties**

- `drop` (`string`): Binding that drops the selected queued message.
- `enter` (`string`): Binding that enters selection mode on the queue. When unbound, `↑` does.
- `push` (`string`): Binding that pushes the selected queued message.

Source: [src/tui/queue/queued-messages-block.tsx:46](https://github.com/Tahul/zidane/blob/main/src/tui/queue/queued-messages-block.tsx#L46)

### `RowViewportWindow`

Interface.

The scrollbox viewport's absolute rect.

```ts
interface RowViewportWindow
```

**Properties**

- `height` (`number`): Height of the viewport in rows.
- `top` (`number`): Top edge of the viewport in absolute rows.

Source: [src/tui/extension-renderers.tsx:105](https://github.com/Tahul/zidane/blob/main/src/tui/extension-renderers.tsx#L105)

### `RunTuiOptions`

Interface.

Options accepted by [`runTui`](https://zidane.si/docs/reference/tui.md#runTui). Extends [`ChatOptions`](https://zidane.si/docs/reference/chat.md#ChatOptions) with the
launcher-only fields that hosts can't carry through the synchronous
`resolveConfig` surface (extension loading is async because user
`setup()` can be async).

```ts
interface RunTuiOptions extends Partial<ChatOptions>
```

**Properties**

- `agents?` (`Readonly<Record<string, AgentProfile>>`, from `Partial`): Agent profile registry — keyed by `profile.id`. Each profile bundles a
  preset with display metadata (label, description, accent). The TUI
  offers a picker and the `cycleAgent` keybinding (shift+tab by default)
  when more than one profile is registered.

  - Omitted (and `preset` omitted) → [`BUILTIN_AGENTS`](https://zidane.si/docs/reference/chat.md#BUILTIN_AGENTS) (Build, Codemode and Plan).
  - Omitted but `preset` provided → single-entry registry around the preset.
  - Provided → host owns the registry; pass exactly what you want. Profiles
    contributed by extensions are added under it (the host wins on id collision).

  Hosts mixing built-ins with their own typically spread:
  `agents: { ...BUILTIN_AGENTS, debug: myDebugProfile }`.
- `autoUpdate?` (`AutoUpdateConfig | null`, from `Partial`): Auto-update wiring. When set, the App quietly checks the npm registry
  for a newer release of `packageName` (matching the host's package),
  writes the result to the cache under `paths.cacheDir` (see
  `AutoUpdateConfig.cacheTtlMs` for how long reads trust it), and
  surfaces a passive `↑ vX.Y.Z` chip in the footer when one is available.

  Consumers wrapping `runTui` for their own CLI inline their own
  `package.json` version at build time and pass it here. The bundled
  `zidane-tui` binary wires this automatically in `tui/src/cli.ts`.

  Defaults to `null` — no auto-update plumbing, no network calls,
  no chip.
- `cwd?` (`string`, from `Partial`): Override for the project root. Defaults to `process.cwd()`. Hosts
  embedding the TUI from a non-CLI context (tests, daemons) can pin
  the cwd here so the git-root walk produces a stable result. Has no
  effect when `projectDb` resolves to `false`.
- `defaultAgent?` (`string`, from `Partial`): Id of the profile every fresh session (and the initial launch) starts
  in. Resolved against `agents`; unknown ids fall through to
  `DEFAULT_AGENT_ID`, then the registry's first key. Sessions remember
  the profile they were created with; this is never overridden by a
  previously picked profile.
- `extensionRegistry?` (`ExtensionRegistry`, from `Partial`): Pre-built [`ExtensionRegistry`](https://zidane.si/docs/reference/extensions.md#ExtensionRegistry). The `resolveConfig` surface is
  synchronous; extension `setup()` can be async, so the host must
  build the registry ahead of time and pass it here verbatim.

  Typical wiring (matches `runTui`):

  ```ts
  import {
    buildExtensionRegistry,
    discoverExtensions,
    inlineLoadedExtension,
    resolveConfig,
  } from 'zidane'

  const registry = await buildExtensionRegistry([
    // Project-scoped extensions run arbitrary local code — they stay
    // off unless the host establishes trust explicitly (or the user
    // sets ZIDANE_TRUST_PROJECT_EXTENSIONS=1).
    ...await discoverExtensions({ cwd, prefix, trustProjectExtensions: hostTrustsProject }),
    ...inlineDefinitions.map(inlineLoadedExtension),
  ])
  const config = resolveConfig({ ...opts, extensionRegistry: registry })
  ```

  Omitted → empty registry (no contributions).
- `extensions?` (`readonly ExtensionDefinition<SettingsSchema>[]`): Inline extensions the host wants `runTui` to load alongside any
  disk-discovered ones. The launcher runs `setup()` on every entry,
  folds the result into a single `ExtensionRegistry`, and
  threads it through `resolveConfig` for you.

  Use this for extensions bundled with a custom CLI binary; for
  user-installed extensions, drop them in
  `.zidane/extensions/<name>/index.ts` and let auto-discovery pick
  them up.

  Inline definitions honor the same user-toggle allowlist
  (`Settings.enabledExtensions`) as disk extensions: a toggled-off
  inline extension's `setup()` is skipped at boot and its
  contributions never reach `ResolvedConfig`. The only difference is
  load mechanics — inline modules are already imported by the host,
  while disabled disk extensions are never imported at all.
- `extensionScanPaths?` (`readonly string[]`, from `Partial`): Extra extension scan roots the host used (or wants runtime re-discovery
  to use). Each path is a root containing extension dirs/packages:
  `<root>/<name>/index.ts`. `runTui` wires this from
  `ZIDANE_EXTENSION_DIRS` when omitted.
- `launch?` (`StartLocalOptions`, from `Partial`): Optional one-shot launch request used by CLI shells such as `zidane run`.
  The TUI opens/creates the requested session and submits the prompt after
  the first provider/session activation, while keeping the normal renderer
  and persistence behavior.
- `onboarding?` (`boolean`): Show the setup wizard on the first launch of a fresh install. Default
  `true`. `ZIDANE_NO_ONBOARDING=1` also turns it off. A launch with a
  prompt, a remote host or CI never shows it. Without a provider, the app
  then opens on the provider picker, as before.
- `pluginScanPaths?` (`readonly string[]`, from `Partial`): Extra Agent Plugin scan roots. Each path is a root containing plugin
  directories: `<root>/<name>/plugin.json`. `runTui` wires this from
  `ZIDANE_PLUGIN_DIRS` when omitted.

  Appended after the four default roots, so a project or user plugin still
  wins a name collision against a dev root.
- `prefix?` (`string`, from `Partial`): Directory name used under `storageDir` for sessions + state.
  Default: `'.zidane'` → `~/.zidane/sessions.db` + `~/.zidane/state.json`.
  Pass e.g. `'.myapp'` for `~/.myapp/...`, or `'myapp'` for a visible dir.
- `preset?` (`Preset`, from `Partial`): Legacy single-agent preset (tools, system prompt, behavior). When set
  **without** `ChatOptions.agents`, the resolver wraps this preset
  into a one-entry [`AgentRegistry`](https://zidane.si/docs/reference/chat.md#AgentRegistry) keyed `default` so existing
  callers keep working and the picker is effectively hidden.

  Prefer `agents` for any multi-profile setup. Passing both throws.
- `projectDb?` (`boolean`, from `Partial`): Project-scoped session storage. Off by default; when it resolves
  to `true` and the launch happens inside a git repo, zidane
  auto-creates `<git-root>/.{prefix}/` and stores `sessions.db` +
  `state.json` there — so sessions, mcps.json, and skills all live
  alongside the project they belong to. Credentials and the
  cross-project safelist stay at `~/.{prefix}/` regardless.

  Precedence (most → least specific): `options.projectDb` >
  `ZIDANE_PROJECT_DB` env var (`0`/`false`/`off`/`no` → off,
  `1`/`true`/`on`/`yes` → on) > `~/.{prefix}/config.json`
  `projectDb` > `false`. Note the host option beats the env var —
  setting `ZIDANE_PROJECT_DB` has no effect when the host passes an
  explicit `projectDb`.

  When it resolves to `false`, OR when not inside a git repo,
  everything lives at `~/.{prefix}/` — the classic single-store
  layout.
- `providers?` (`Readonly<Record<string, ProviderDescriptor>>`, from `Partial`): Provider registry. Each value is a [`ProviderDescriptor`](https://zidane.si/docs/reference/chat.md#ProviderDescriptor) carrying
  label + factory + credential metadata. **No automatic merge** — hosts
  pass exactly what they want.

  - Omitted → [`BUILTIN_PROVIDERS`](https://zidane.si/docs/reference/chat.md#BUILTIN_PROVIDERS) (anthropic, openai, openrouter, cerebras and the other built-ins).
  - `providers: BUILTIN_PROVIDERS` → same as omitted, but explicit.
  - `providers: { anthropic: anthropicDescriptor }` → only Anthropic.
  - `providers: { ...BUILTIN_PROVIDERS, mine: customDescriptor }` → built-ins + custom.
  - `providers: { mine: customDescriptor }` → custom-only, **no built-ins**.
- `remoteHost?` (`RemoteHost`): Run every session on this remote zidane-server instead of a local host
  (`zidane --host <wss-url> --token <t>`): the whole window, multi-process
  semantics. Ignored under `ZIDANE_NO_REMOTE=1` (see `./host-target`).
- `storageDir?` (`string`, from `Partial`): Storage root. Defaults to the user's home directory. Combined with
  `prefix` to form the data directory.
- `store?` (`SessionStore | ((paths: ResolvedPaths) => SessionStore)`, from `Partial`): Session store — either an instance or a factory called with the
  resolved storage paths. **Required** at the chat layer so this module
  stays platform-agnostic (no `bun:sqlite` baked in). Terminal hosts use
  `createTuiStore` from `zidane/session/sqlite`; a future GUI host can
  supply a different adapter (IndexedDB, remote API, in-memory) without
  touching this layer.

  ```ts
  import { createTuiStore } from 'zidane/session/sqlite'

  // Either pass a factory…
  resolveConfig({ store: paths => createTuiStore(paths.db) })

  // …or an instance built ahead of time.
  const store = createTuiStore('/path/to/sessions.db')
  resolveConfig({ store })
  ```
- `trustProjectExtensions?` (`boolean`, from `Partial`): Whether project-scoped extensions are trusted to run their (arbitrary,
  local) code. Off by default unless the embedding host has established
  project trust; falls back to `ZIDANE_TRUST_PROJECT_EXTENSIONS=1`.

  Surfaced on `ResolvedConfig.trustProjectExtensions` so a host
  that re-discovers extensions at runtime (e.g. the TUI on a working-
  directory change) reuses the same trust decision as boot.

Source: [src/tui/index.tsx:49](https://github.com/Tahul/zidane/blob/main/src/tui/index.tsx#L49)

### `SessionCompactResult`

Interface.

What `onCompact` returns. Drives the modal's success banner.

```ts
interface SessionCompactResult
```

**Properties**

- `droppedCount` (`number`): Number of turns the PTL-retry path had to drop without summarizing.
  Zero on first-try success. When non-zero, the modal warns the user
  that some context was lost — those turns stay in the conversation
  history but the new summary doesn't describe them.
- `effectiveTokens` (`number`): Estimated wire-level context size the NEXT turn will start from.
  Sum of the summary's tokens plus the restoration content's
  byte-based estimate — captures what the model will actually see on
  its next call, modulo the system prompt and any preserved tail
  turns whose precise token count we don't know without re-asking
  the provider.

  Drives both the post-compact success banner ("→ N effective") and
  the chat footer's `ctx` indicator — `onCompactSession` writes this
  value into `lastInputTokens` so the user sees the drop immediately
  instead of waiting for the next real turn's `usage` update.
- `inputTokens` (`number`): Total input tokens billed (including cache reads + creations).
- `model` (`string`): Model used for the summary call.
- `outputTokens` (`number`): Output tokens of the summary itself.
- `replacedCount` (`number`): Number of turns absorbed into the summary.
- `restoredFiles` (`number`): Number of files re-injected as synthetic `read_file` results after
  the compaction marker. Zero when the session had no active reads or
  the host opted out of restoration.
- `restoredSkills` (`number`): Number of skills re-injected as synthetic `skills_use` results
  after the compaction marker.

Source: [src/tui/session-details-modal.tsx:150](https://github.com/Tahul/zidane/blob/main/src/tui/session-details-modal.tsx#L150)

### `SessionDetailsModalActions`

Interface.

Actions a host supplies to [`SessionDetailsModal`](https://zidane.si/docs/reference/tui.md#SessionDetailsModal).

```ts
interface SessionDetailsModalActions
```

**Properties**

- `environment?` (`SessionEnvironmentActions`): Environment commands, run with `1`–`9`. Omit when the host can't run them.
- `favorite?` (`(sessionId: string, favorite: boolean) => Promise<void>`): Add to favorites or remove (`sessionFavorite`). Omit when the host can't.
- `move?` (`(sessionId: string) => void`): Move to another environment or machine (`sessionMoveEnvironment`): the
  parent closes this modal and walks the steps (where, folder, confirm).
  Omit when the host can't.
- `onCompact?` (`(sessionId: string, signal: AbortSignal, onProgress?: (estOutputTokens: number) => void) => Promise<SessionCompactResult>`): Compact older turns via an LLM-generated summary. The host calls
  `compactConversation` and appends the synthetic `compact-summary`
  marker turn so older turns stop reaching the provider. The model
  sees everything from the marker forward verbatim; the user can
  still scroll back to see the original turns.

  Resolves to a result envelope used by the modal to flash the
  "compacted N turns" success banner. Reject with an Error to
  surface a failure message.

  `signal` is provided so a parent unmount (modal closed, session
  switched) can abort the in-flight summary call.

  `onProgress` (optional for hosts) receives a rough cumulative
  output-token estimate while the summary streams — the modal renders
  it next to the running spinner so a multi-minute compaction shows
  visible progress. Hosts should throttle before invoking.

  Omit when compaction isn't wired (no provider picked yet, or the
  host opted out) — the modal hides the `k` shortcut.
- `onDelete` (`(sessionId: string) => void | Promise<void>`): Delete the entire session from the store. Parent handles teardown / navigation.
- `onExport?` (`(sessionId: string, format: SessionExportFormat) => Promise<SessionExportResult>`): Export the session to disk in the requested format. The host
  decides where the file lands (typically `.{prefix}/sessions/`) and
  returns the absolute path so the modal can show a confirmation
  footer. Reject with an Error to surface a failure message.

  Omit when export isn't wired — the modal hides the `e` / `j`
  shortcuts.
- `onGenerateTitle?` (`(sessionId: string, signal: AbortSignal) => Promise<string>`): Generate a title for the session via the active provider/model.
  Resolves to the new title (already persisted by the parent); reject
  with an Error to surface a failure message in the modal footer.

  Omit when title generation isn't wired (no provider picked yet, or
  the host opted out) — the modal hides the `g` shortcut.

  `signal` is provided so a parent unmount (modal closed, session
  switched) can abort the in-flight provider stream. Implementations
  should forward it to whatever transport they use; an aborted
  generation should reject (e.g. with the signal's reason).
- `spaces?` (`{ list: readonly Space[]; move: (sessionId: string, spaceId: string) => Promise<void> }`): Spaces on: the spaces (switcher order) and the move. The modal shows the
  session's space and moves it to the next one on `sessionMoveSpace`.
  Omit while Spaces is off.

Source: [src/tui/session-details-modal.tsx:75](https://github.com/Tahul/zidane/blob/main/src/tui/session-details-modal.tsx#L75)

### `SessionExportResult`

Interface.

Result handed back by the host's export handler — drives the success footer.

```ts
interface SessionExportResult
```

**Properties**

- `filepath` (`string`): Absolute path of the written file.
- `format` (`SessionExportFormat`): Format that was written — used to label the success banner.

Source: [src/tui/session-details-modal.tsx:32](https://github.com/Tahul/zidane/blob/main/src/tui/session-details-modal.tsx#L32)

### `SettingsActions`

Interface.

Callbacks the settings modal calls to act on the host, such as configuring a provider, picking models, and refreshing skills and MCP servers. Each is optional.

```ts
interface SettingsActions
```

**Properties**

- `getContextSegments?` (`() => Promise<{ canOverrideSystem: boolean; live: boolean; overrideBlockedBy?: 'scope' | 'profile'; segments: readonly ContextSegment[]; shadowedSystemPath?: string }>`): Read the session's live default-context segments — every chunk of text in
  the prompt, with provenance (`agent.getContextSegments()`). When wired, an
  "Instructions" tab appears; the modal calls this on every activation of
  that tab (context changes as a session runs) and owns the loading / error
  state. Cheap by contract: no provider round-trip.
- `getExtensionsSnapshot?` (`() => { catalog: readonly LoadedExtension[]; configCategories: Readonly<Record<string, ExtensionConfigCategory>>; configItems: Readonly<Record<string, Readonly<Record<string, StoredExtensionConfigItem>>>>; inactive: readonly LoadedExtension[]; plugins: readonly LoadedPluginRecord[]; skills: readonly SkillConfig[] }`): Live read of the extension registry — the modal is opened with
  SNAPSHOT props (a one-shot element), so after an op that rebuilds the
  registry (update / remove / install) the rows would keep showing the
  pre-op state (stale versions, missing config items) until the modal
  was reopened. Ops call this on success and swap the snapshot in.
- `onAddMcp?` (`(entry: McpServerConfig, scope: 'user' | 'project') => Promise<string>`): Persist a new MCP server (`ctrl+g` on the MCPs tab — inline form).
  The host upserts into the scope's `mcps.json` and refreshes the
  catalog. Resolves to a success message; rejects with the failure.
- `onBrowseExtensions?` (`(input: string) => Promise<ExtensionSourceListing>`): List the extensions of a source (`ctrl+g` on the Extensions tab):
  `owner/repo`, a link, an npm package or a folder. Never rejects for a
  source it cannot read: the listing carries `error`.
- `onBrowseSkills?` (`(input: string) => Promise<SkillSourceListing>`): List the skills of a source (`ctrl+g` on the Skills tab): what
  `browseSkillSource` takes, such as `owner/repo`, a GitHub or
  skills.sh link, or an npm package. Rejects with a plain message.
- `onCancelLoginMcp?` (`(name: string) => void`): Abort an in-flight MCP OAuth flow.
- `onChangeExtensionOrigin?` (`(ext: LoadedExtension, source: string, onProgress?: (message: string) => void) => Promise<string>`): Install the focused extension again from another source, in place
  (`ctrl+o`): the source must hold an extension of the same name, and
  later updates follow it. Same resolve/reject contract as
  `onUpdateExtension`; `onProgress` streams the install's stages.
- `onConfigureProvider?` (`(provider: ProviderAuth) => void`): Open the setup wizard for a specific provider — `↵` on any row of the
  Authentication tab. The tab is config-only (switching the *active*
  provider is handled by the cross-provider model picker), so every row,
  configured or not, lands in that provider's credential editor. Closes
  the modal as a side effect.
- `onCreateExtension?` (`() => void`): Open the "create extension" flow (`ctrl+a` on the Extensions tab):
  prompt for a name + scope, scaffold `index.ts`, then route into the
  same edit-session path as `onEditExtension`.
- `onCreateSystemPrompt?` (`() => void | Promise<void>`): Create a user-scope `SYSTEM.md` (seeded with the doctrine currently in
  effect) and open it in `$EDITOR` — `↵` on the System prompt row when no
  override exists yet. The only Instructions action that WRITES, hence its own
  hint label rather than the generic "open file".
- `onEditExtension?` (`(dir: string) => void`): Open an "edit extension" session rooted at `dir` (`ctrl+e` on a
  focused extension row). The host chdir's into the dir, opens a fresh
  session marked as an extension-edit, and auto-activates the docs
  skill. Works on errored extensions too — they still carry a `dir`.
  Inline / builtin extensions have no `dir`, so the row's `ctrl+e` is
  a no-op for them.
- `onEditSkill?` (`(skill: SkillConfig) => void`): Open an "edit skill" session rooted at the skill's directory (`ctrl+e`
  on a focused Skills-tab row) — mirrors `onEditExtension`: the host
  chdir's into the dir, opens a fresh session marked as a skill-edit, and
  auto-activates the skill-edit preset skill. Inline / builtin skills have
  no directory, so the modal surfaces a notice instead of calling this.
- `onImportClaudeCode?` (`() => void`): Open the "Import from Claude Code" flow (sessions / MCP servers /
  skills — see `claude-import.ts`). Hosts wire it only when a
  `~/.claude` setup is actually detected, so the row never shows on
  machines without one. Closes the modal as a side effect — the
  import modal owns the modal slot.
- `onInstallExtension?` (`(req: { extension?: string; onProgress?: (message: string) => void; scope: 'user' | 'project'; source: string }) => Promise<string>`): Install an extension from GitHub / npm (`ctrl+g` on the Extensions
  tab — the modal owns the two-step source → scope flow). Same
  resolve/reject contract as the other extension actions; `onProgress`
  streams fetch/validate/install stages into the busy line.
- `onInstallSkill?` (`(req: { onProgress?: (message: string) => void; scope: 'user' | 'project'; skills?: readonly string[]; source: string }) => Promise<string>`): Install skills from GitHub / npm / skills.sh (`ctrl+g` on the
  Skills tab, after `onBrowseSkills` listed the source). `skills` are
  the picked selectors of one source. Resolves to a success message;
  rejects with the failure (surfaced on the tab's op line). The host
  decides the engine (skills.sh CLI when on PATH, else the native fetch).
- `onLoginMcp?` (`(name: string) => void | Promise<void>`): Start an MCP OAuth flow. Resolves once tokens land or the flow aborts.
- `onLogoutMcp?` (`(name: string) => void`): Wipe stored MCP tokens.
- `onOpenEnvironments?` (`() => void`): Open Settings → Environments (`environments-settings.tsx`): the default
  environment for new sessions and each kind's options. Replaces the
  settings modal in the single modal slot.
- `onOpenInstructionsFile?` (`(path: string) => void | Promise<void>`): Open the file backing an Instructions-tab row in `$EDITOR` (`↵` / click).
  Only ever called with a `ContextSegment.path`, i.e. for `origin: 'file'`
  rows — the built-in / generated / MCP segments have no file, and the tab
  surfaces a notice for those instead of calling this.
- `onOpenKeybindings?` (`() => void`): Open `~/.zidane/keybindings.json` in `$EDITOR`.
- `onPickCompactionModel?` (`() => void`): Open the cross-provider model picker to choose the model the
  compaction summary call runs on (`Settings.compactionModel`).
  Closes the modal as a side effect — the picker owns the modal slot.
- `onPickDecisionModel?` (`() => void`): Open the model picker over the decision models (`Settings.decisionModel`).
  Closes the modal as a side effect — the picker owns the modal slot.
- `onReauth?` (`() => void`): Generic "open the auth flow" — only used as a FALLBACK action row on the
  General tab for embedders that don't wire the dedicated Authentication
  tab (`authentication` prop unset). The standard TUI shows the tab instead.
- `onRefreshInstructions?` (`() => Promise<'no-session' | 'queued' | 'rebuilt'>`): Re-read the instruction files from disk and rebuild the live agent so a
  running session picks them up — `ctrl+r` on the Instructions tab, matching
  the Skills / MCPs refresh key.

  Resolves with what actually happened, because the outcomes differ enough to
  report: `'rebuilt'` (applied now), `'queued'` (a run is in flight; applied
  when it ends), `'no-session'` (nothing to rebuild — the re-read alone
  refreshed the preview).
- `onRefreshMcps?` (`() => void | Promise<unknown>`): Re-parse the project's `mcps.json` files. Fired by `ctrl+r` on
  the MCPs tab. Sync-shaped under the hood but typed as async so
  future discovery paths (registry lookups, etc.) can stay
  non-blocking without changing the contract.
- `onRefreshMcpTools?` (`(name?: string) => void | Promise<void>`): Refresh the per-server tool catalog by opening transient MCP
  connections, capturing `tools/list` payloads via the agent's
  `mcp:tools:list` hook, and writing the result to the on-disk
  cache. Fired automatically when the MCPs tab activates so the
  panel always shows the freshest tool list. Hosts that don't
  want the auto-refresh (no live agent yet, no MCP credentials
  wired) can omit the action — the panel then degrades to
  whatever the on-disk cache last captured. With `name`, tests that
  one server only (`ctrl+t` on a server row).
- `onRefreshPlugins?` (`() => void | Promise<void>`): Re-scan the Agent Plugin roots. Fired by `ctrl+r` on the Extensions &
  Plugins tab. Without it a plugin dropped into `.agents/plugins` while
  the TUI is open stays invisible until a restart or a project change.
- `onRefreshSkills?` (`() => void | Promise<void>`): Force a fresh skills rescan, bypassing the background SWR throttle.
  Fired by `ctrl+r` on the Skills tab. When wired the host should
  call `discoverProjectSkills` directly and replace the catalog —
  an in-flight refresh promise resolves into the modal automatically
  via the catalog prop.
- `onRemoveCredential?` (`(provider: ProviderAuth, key: string) => void | readonly ProviderAuth[]`): Remove ONE stored credential (`ctrl+x` on a source row). Removing the
  active one promotes a survivor host-side. Returns fresh detection, like
  `onSetPreferredCredential`, so the row disappears in place.
- `onRemoveExtension?` (`(ext: LoadedExtension) => Promise<string>`): Delete the focused extension's directory and hot-reload (`ctrl+x`,
  pressed twice — the modal owns the arm/confirm step). Same
  resolve/reject contract as `onUpdateExtension`.
- `onRemoveMcp?` (`(entry: DiscoveredMcp) => Promise<string>`): Remove the focused server from the config file that declared it
  (`ctrl+x` on a server row of the MCPs tab, two-press confirm). The
  host guards ownership (`.agents/` and foreign files stay read-only)
  and refreshes the catalog. Resolves to a success message; rejects
  with the failure.
- `onRemoveSkill?` (`(skill: SkillConfig) => Promise<string>`): Delete the focused skill's directory from disk (`ctrl+x` on the
  Skills tab, two-press confirm). The host guards what's deletable
  (inline / builtin skills have no directory) and refreshes the
  catalog. Resolves to a success message; rejects with the failure.
- `onRenameCredential?` (`(provider: ProviderAuth, key: string, label: string) => void | readonly ProviderAuth[]`): Rename a stored OAuth account (`ctrl+e` on its row). An empty label
  reverts to the default name. Returns fresh detection, like the two above.
- `onRunSetup?` (`() => void`): Open the setup wizard again, with the current values ("Run setup
  again", the last row of the Agent tab). Closes the modal as a side
  effect: the wizard takes the whole screen.
- `onSetPreferredCredential?` (`(provider: ProviderAuth, target: PreferredCredentialTarget) => void | readonly ProviderAuth[]`): Promote one of a provider's STORED credentials to preferred — fired by `↵`
  on a source row when a provider holds more than one (an API key plus any
  number of OAuth accounts). Addressed by file key (`AuthMethod.key`) so two
  OAuth accounts are distinguishable; the kind form is the fallback for a
  host-built `ProviderAuth` with no keys. Never fired for `env`, which is
  observed rather than stored.
- `onTestDecisionModel?` (`() => void`): Send one tiny decision to the picked decision model and show the result.
  Replaces the settings modal in the single modal slot.
- `onUpdateExtension?` (`(ext: LoadedExtension) => Promise<string>`): Update the focused extension from its `.install.json` provenance
  (`ctrl+u`): re-fetch the recorded source, replace the install in
  place, hot-reload the registry. Resolves to the feedback line the
  modal shows; rejects with the reason (no provenance, fetch failure,
  …) which renders as an error notice.

Source: [src/tui/settings/settings-actions.ts:12](https://github.com/Tahul/zidane/blob/main/src/tui/settings/settings-actions.ts#L12)

### `TuiExtensionHost`

Interface.

The host side of the kit hooks, one per extension. The TUI host
integration implements it against the extension runtime.

```ts
interface TuiExtensionHost
```

**Properties**

- `extension` (`TuiExtensionInfo`, readonly): The extension whose components draw.
- `rpc` (`<C extends RpcContract<Readonly<Record<string, RpcMethodSpec>>, Readonly<Record<string, RpcEventSpec>>>>(contract: C) => RpcClient<C>`): A typed client for `contract`, calling as the extension's UI (`caller.kind: 'ui'`).
- `settings` (`Readable<Readonly<Record<string, unknown>>>`): The extension's effective settings values by key, live.
- `store` (`(name: string, location?: TuiStoreLocation) => TuiHostStore<unknown> | undefined`): The store the extension declared under `name`, or `undefined` when it declared none there.

Source: [src/tui/kit/host.tsx:44](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L44)

### `TuiExtensionHostProviderProps`

Interface.

Props of [`TuiExtensionHostProvider`](https://zidane.si/docs/reference/tui.md#TuiExtensionHostProvider).

```ts
interface TuiExtensionHostProviderProps
```

**Properties**

- `children` (`ReactNode`): Its components.
- `host` (`TuiExtensionHost`): The extension's host.

Source: [src/tui/kit/host.tsx:56](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L56)

### `TuiExtensionInfo`

Interface.

The extension a component belongs to.

```ts
interface TuiExtensionInfo
```

**Properties**

- `displayName?` (`string`, readonly): Name shown in Settings.
- `id` (`string`, readonly): The extension id.
- `version?` (`string`, readonly): Semver of the extension.

Source: [src/tui/kit/host.tsx:17](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L17)

### `TuiHostStore`

Interface.

A store as a component sees it: its value, live, and a setter.

```ts
interface TuiHostStore<T = unknown> extends Readable<T>
```

**Type parameters**

- `T` (default `unknown`)

**Properties**

- `get` (`() => T`, from `Readable`): The current value.
- `set` (`(value: T) => Promise<void>`): Writes the value; rejects when it does not fit the store's schema.
- `subscribe` (`(listener: (value: T) => void) => () => void`, from `Readable`): Calls `listener` after each change. Returns the unsubscribe.

Source: [src/tui/kit/host.tsx:35](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L35)

### `TuiStoreLocation`

Interface.

Where a store lives: the scope and, for `'session'`, the session.

```ts
interface TuiStoreLocation
```

**Properties**

- `scope?` (`StorageScope`): Default: what the host picks (`'user'` in the TUI host).
- `sessionId?` (`string`): For `'session'` scope. Default: the session on screen.

Source: [src/tui/kit/host.tsx:27](https://github.com/Tahul/zidane/blob/main/src/tui/kit/host.tsx#L27)

### `TurnDetailsModalActions`

Interface.

Actions a host supplies to [`TurnDetailsModal`](https://zidane.si/docs/reference/tui.md#TurnDetailsModal).

```ts
interface TurnDetailsModalActions
```

**Properties**

- `onDelete` (`(turnId: string) => void`): Delete this turn (with cleanup of orphan tool blocks in neighbors).
- `onEdit` (`(turnId: string, newText: string) => void`): Edit the text content of this turn.
- `onFork` (`(turnId: string) => void`): Fork the session at this turn — new session keeps history up to here.

Source: [src/tui/turn-details-modal.tsx:53](https://github.com/Tahul/zidane/blob/main/src/tui/turn-details-modal.tsx#L53)

## Types

### `BadgeTone`

Type.

How a badge reads: the theme color of its text. `neutral` (the default)
is a plain value; `mute` recedes (ids, folders, ages); the rest carry a
role (`success` is the accent, `info` the model color, `money` the cost
color).

```ts
type BadgeTone =
  | 'neutral'
  | 'mute'
  | 'brand'
  | 'success'
  | 'warn'
  | 'error'
  | 'info'
  | 'money'
```

Source: [src/tui/ui.tsx:162](https://github.com/Tahul/zidane/blob/main/src/tui/ui.tsx#L162)

### `FormValues`

Type.

Values a form submits, by setting key. Model lists are comma-joined, as Settings stores them.

```ts
type FormValues = Record<string, ExtensionConfigValue>
```

Source: [src/tui/kit/form.tsx:27](https://github.com/Tahul/zidane/blob/main/src/tui/kit/form.tsx#L27)

## Re-exports

These names are documented on the entry point that owns them.

- [zidane/chat/pure](https://zidane.si/docs/reference/chat-pure.md): [`accentColor`](https://zidane.si/docs/reference/chat-pure.md#accentColor), [`displayNameFor`](https://zidane.si/docs/reference/chat-pure.md#displayNameFor), [`formatToolCall`](https://zidane.si/docs/reference/chat-pure.md#formatToolCall), [`isEditErrorResult`](https://zidane.si/docs/reference/chat-pure.md#isEditErrorResult), [`isTurnHighlighted`](https://zidane.si/docs/reference/chat-pure.md#isTurnHighlighted), [`isVisible`](https://zidane.si/docs/reference/chat-pure.md#isVisible), [`MarkdownSegment`](https://zidane.si/docs/reference/chat-pure.md#MarkdownSegment), [`PromptSegment`](https://zidane.si/docs/reference/chat-pure.md#PromptSegment), [`PromptSegmentRef`](https://zidane.si/docs/reference/chat-pure.md#PromptSegmentRef), [`selectableTurnIds`](https://zidane.si/docs/reference/chat-pure.md#selectableTurnIds), [`splitMarkdownCodeBlocks`](https://zidane.si/docs/reference/chat-pure.md#splitMarkdownCodeBlocks), [`splitPromptSegments`](https://zidane.si/docs/reference/chat-pure.md#splitPromptSegments), [`TOOL_DISPLAY`](https://zidane.si/docs/reference/chat-pure.md#TOOL_DISPLAY), [`ToolDisplayMeta`](https://zidane.si/docs/reference/chat-pure.md#ToolDisplayMeta), [`ToolFormatLine`](https://zidane.si/docs/reference/chat-pure.md#ToolFormatLine), [`turnSelectionOwnership`](https://zidane.si/docs/reference/chat-pure.md#turnSelectionOwnership)
- [zidane/chat](https://zidane.si/docs/reference/chat.md): [`clipHintsToWidth`](https://zidane.si/docs/reference/chat.md#clipHintsToWidth), [`computeTurnAnchors`](https://zidane.si/docs/reference/chat.md#computeTurnAnchors), [`Hint`](https://zidane.si/docs/reference/chat.md#Hint), [`hintsLength`](https://zidane.si/docs/reference/chat.md#hintsLength), [`marginTopFor`](https://zidane.si/docs/reference/chat.md#marginTopFor), [`ThemeProvider`](https://zidane.si/docs/reference/chat.md#ThemeProvider), [`TranscriptItem`](https://zidane.si/docs/reference/chat.md#TranscriptItem), [`truncateTrailing`](https://zidane.si/docs/reference/chat.md#truncateTrailing), [`useColors`](https://zidane.si/docs/reference/chat.md#useColors), [`useSelectStyle`](https://zidane.si/docs/reference/chat.md#useSelectStyle), [`useSurfaces`](https://zidane.si/docs/reference/chat.md#useSurfaces), [`useTheme`](https://zidane.si/docs/reference/chat.md#useTheme)
