# Settings screen

> The settings screen (nav, search, deep links) over the sections an app lists, zidane's settings rows read and written through a settings host, and the parts to build an app's own sections.

`zidane-ui/settings` is the settings screen. `SettingsShell` draws a nav, a search and the open section of whatever sections you hand it. `SettingsRowsPage` draws a page of zidane's settings (General, Agent, Interface, Advanced) from the catalog, read and written through a `SettingsHost`. The previews run on a fixture host: a toggle writes it, and search lands on the row it finds.

## Mount it

```tsx
import { createServerClient, createServerSettingsHost } from 'zidane-ui/adapters'
import { SettingsProvider, SettingsRowsPage, SettingsShell, type SettingsSection } from 'zidane-ui/settings'
import { layoutSearchEntries, SETTINGS_PAGES, type SettingsSectionsPage } from 'zidane-ui/settings/headless'

const client = createServerClient({ url: 'wss://box:4280', token })
const settings = createServerSettingsHost({ client })

const sections: SettingsSection[] = SETTINGS_PAGES.filter((page): page is SettingsSectionsPage => 'sections' in page).map(page => ({
  id: page.id,
  title: page.label,
  icon: page.icon,
  description: page.description,
  search: layoutSearchEntries(page, { server: true, actions: [] }),
  render: () => <SettingsRowsPage page={page} server />,
}))

export function Settings({ onClose }: { onClose: () => void }) {
  return (
    <SettingsProvider host={settings}>
      <SettingsShell sections={sections} onClose={onClose} />
    </SettingsProvider>
  )
}
```

`AppHostProvider` (`zidane-ui/app`) puts `host.settings` in a `SettingsProvider` for you.

## Sections

A section is `{ id, title, icon?, monogram?, group?, description?, keywords?, search?, keepMounted?, render }`. The shell lists them in order. A section without an `icon` but with a `monogram` (an extension's name) shows that word's first letter, uppercase, in a small square the size of an icon.

- `group` names the section it sits under in the nav, indented. Without one it is a top-level entry.
- `search` lists the rows inside it. A result opens the section and highlights the row with `data-setting-focus` equal to its `key`. Without `search` the section is found by its title, description and keywords. `extraSearch` adds entries the section can't list up front.
- `keepMounted` keeps the body mounted after its first visit, so its async state survives switching away. Its header actions portal into the screen's header with `SettingsPanelActions`.
- `render` takes no arguments. The body reads `useSettingsSection()`: whether it is open, and `select(id, focus)` to open another section on a row.

`layoutSections(pages, { server, actions, renderRows, renderPanel })` (`zidane-ui/settings/headless`) turns layout pages (`SETTINGS_PAGES`, `visiblePages`) into sections: nav entry, group, search entries, panels kept mounted, your body for each.

`initialSection` and `initialFocus` open the screen on a section and a row. `renderFrame` draws your own frame around the nav, search and body: the desktop app puts them in its full-window screen. The default `SettingsFrame` stacks its two columns below 640px.

## The rows

`SettingsRowsPage` draws each catalog key as a toggle or a choice. A toggle that needs another setting, a toggle another setting holds on, and a toggle a kill switch turns off (`killSwitches`) are inert and say why. What the kit can't draw, you supply: `customRows` (model pickers, agent defaults), `renderRow` (your own row for a key), `rowExtra` (after each row), `spaces`, `notes` and `actions`. A custom row you leave out is skipped, and a section left empty is not drawn. With `server`, the layout's `localOnly` parts are left out; add `spacesOnServer` when your app's spaces are the server's (the web app's are), so Spaces stays (`layoutSearchEntries` takes it too).

Without `update`, or while `writable` is false, every row is read-only and the page says so. A failed first read shows an error with a retry.

## Preference sections

Three sections hold what one device prefers, read from the areas that own it rather than from the settings host:

| Section | Reads | The app adds |
| - | - | - |
| `AppearanceSection` | The nearest `AppearanceProvider`: mode, themes, code theme, accent, contrast, fonts, sizes, motion | `appIcon` (an App icon group after Accent), `onReset` (runs after Reset appearance) |
| `EditorSection` | The files host's `editorPrefs` (editing, saving, diagnostics, language servers) and the appearance host's tree side | `cwd`, `openIn` (where clicked files open), `intro`, `diagnosticsTools(cwd)` and `languageServers(cwd)` (the per-folder lists) |
| `KeybindingsSection` | The nearest `KeybindingsProvider`: every row of `KEYBINDING_SECTIONS` with the keys in effect | `actions` (more header buttons). Edit in file needs the host's `openInEditor`, Reload a host. |

What a host or prop leaves out is hidden: no `editorPrefs`, no Editing or Diagnostics groups; no `openIn`, no Open in row. `appearanceSettingsSection(props)`, `editorSettingsSection(props)` and `keybindingsSettingsSection(props)` wrap each one as a kept-mounted `SettingsSection`, with its title, icon, group and search entries from `SETTINGS_PAGES`.

```tsx
import { appearanceSettingsSection, keybindingsSettingsSection, SettingsShell } from 'zidane-ui/settings'

<SettingsShell sections={[...sections, appearanceSettingsSection(), keybindingsSettingsSection()]} />
```

Their strings are their own labels (`DEFAULT_APPEARANCE_SECTION_LABELS`, `DEFAULT_EDITOR_SECTION_LABELS`, `DEFAULT_KEYBINDINGS_SECTION_LABELS`), overridden with `LabelsProvider`. The previews run on fixture hosts: what you change writes them.

## Catalog sections

These sections read the settings host's optional groups. A section whose group the host leaves out shows nothing to do, and an action the group leaves out hides its button.

| Section | Reads | The app adds |
| - | - | - |
| `SkillsSection` | `skills`: the folder's skills (`catalog(cwd)`), the source browser (`browse`, `install`), `remove`, the SKILL.md editor (`file`, `write`). The checkboxes write `enabledSkills`. | `cwd`, `onEdit(skill)` (an authoring session), header `actions` |
| `ExtensionsSection` | `extensions`: `list(cwd)`, `install`, `update`, `remove`. The checkboxes write `enabledExtensions`. | `cwd`, `onEdit`, `onCreateExtension`, `children` under the list |
| `PluginsSection` | `plugins`: `catalog(cwd)`. The checkboxes write `enabledPlugins` (a consent gate). | `cwd` |
| `InstructionsSection` | `instructions`: the session's prompt (`segments`), `createSystemOverride`, `openFile` | `sessionId`, `localFiles`, `renderMarkdown` |
| `EnvironmentsSection` | `environments`: `list(cwd)`, `update(change)`, `verify(kind)` | `cwd` |
| `LogsSection` | `logs`: `list()`, `follow(onRecord)` | |
| `PerformanceSection` | `performance`: `history()`, `watch(onSample)` while the section is open, and `threads(pid)`, `resetCounters`, `exportReport`, `profile(target, seconds)` when offered. `PerformanceHud` is the same figures as a corner card, watched while mounted. | |
| `HostCatalogSection` | `hostCatalog`: a host's own skills, extensions or plugins (`list(kind, cwd)`), `onChanged`, and when `installs()` says so `install`, `remove`, `update`. The rows are the local sections' own. A skill without its body shows no weights and does not open. A skill that a plugin or an extension brings follows the switch of that plugin or extension. A host that cannot list them gets its enabled list. A host's MCP servers are `McpSection` over its `mcp` group. | `kind` (`skills`, `extensions`, `plugins`), `hostName`, `cwd` |

`paths` (`reveal`, `open`) adds the Finder and editor buttons on rows. A list the section keeps on screen comes as a `SettingsResource` (`get`, `subscribe`, `refresh`): `createSettingsResource(read)` builds one over an async read. `skillsSettingsSection(props)`, `extensionsSettingsSection`, `pluginsSettingsSection`, `instructionsSettingsSection`, `environmentsSettingsSection`, `logsSettingsSection`, `performanceSettingsSection` and `hostCatalogSettingsSection` return them as `SettingsSection`s with the layout's title and icon.

`createServerSettingsHost` offers `environments`, `instructions` and `hostCatalog` (installs need the server's `allowRemoteInstalls` and the `admin` scope), and `extensions` over the server's catalog with the options and actions its folder declares (`extensions.configSchema`, `extensions.actions`, `extensions.actionStatuses`, `extensions.runAction`; `create` scaffolds in the server user's folder, or in a project inside the server's workspace roots, the only kind a session can then open in). With an `admin` token it offers `logs` over `logs.list`, followed with `logs.watch` while the section is open and released with `logs.unwatch` when it closes. zidane-server has no method for this device's skills and plugins on disk, installs by folder or opening a path, so a web app shows the server's catalogs (plugins too) with `HostCatalogSection`.

`SettingsProvider` takes the server's name as `hostName` when its host is a server's. Then the page descriptions, catalog rows and tutorials name the server instead of this computer. A layout page or row carries a `serverDescription`, where `{host}` stands for the name. `describedOn(part, host)` reads it, and `layoutSections({ host })` and the descriptors that take a `hostName` use that function. `SettingsTutorial` plays `tutorialOnServer(topic, host)`. A text that is true on both hosts reads the same.

## Accounts and MCP servers

Two more groups feed Settings › Accounts and Settings › MCP servers. As above, a section whose group the host leaves out shows nothing to do, an action the group leaves out hides its control, and a connection that may not write (`writable`) shows no action.

| Section | Reads | The app adds |
| - | - | - |
| `ProvidersSection` | `providers`: the providers and their credentials (`list(cwd)`), `setApiKey`, `configure` (fields a provider takes instead of a key), `clear`, and on a provider holding several credentials `setPreferred`, `rename`, `remove`. `signIn(key, options)` runs a browser sign-in per provider (a second account asks its name first); leaving the screen cancels the ones still running. | `cwd`, an `intro`, `confirm` (asked before Clear and Remove) |
| `ServerProvidersSection` | The same `providers` group, drawn as a server's cards: Set API key, Sign in (`ServerSignIn`: the page here, the tokens there), Remove. | `hostName` |
| `McpSection` | `mcp`: the folder's servers (`servers(cwd)`), `test` (and Test all, three at a time), `signIn` / `signOut` for HTTP servers, `add` (validated by zidane's `validateMcpForm`; `projectScope` offers the project's file), `remove` with an inline confirm, and while a server's process runs, its badge (shared by N sessions, or per session; `instances(cwd)`) and Restart (`restart`). The switches write `enabledMcps` and `disabledMcpTools`, and a local `stdio` server's Shared switch writes `mcpSharing`; a new server is turned on, an edit keeps its switch. | `cwd`, an `intro`, header `actions` |
| `EnabledListSection` | The settings document only: a host's `enabled*` list, for a host that can't list its catalog (`HostCatalogSection` falls back to it). | `settingKey`, `hostName` |
| `ServerSignIn` | A `SignInFlow` from `begin()`, started on mount and cancelled on unmount: the address, a device code, a paste field when the flow takes input, Cancel then Close. | `begin`, `hostName`, `label`, `onClose` |

`providersSettingsSection(props)`, `serverProvidersSettingsSection`, `mcpSettingsSection` and `enabledListSettingsSection` return them as `SettingsSection`s. A sign-in is the onboarding area's `SignInFlow` (`state`, `submit`, `cancel`), so an app that already signs in for its setup wizard reuses it.

`createServerSettingsHost` offers `providers` over `auth.status` (with each provider's configuration fields), `auth.setApiKey`, `auth.configure`, `auth.remove` (one credential or all: the sign-out), `auth.setPreferred`, `auth.rename` and `auth.oauth.*`. It offers `mcp` over `catalog.mcps` (each server's sign-in state, tools and last Test), `mcp.test` and `mcp.upsert`. `mcp.upsert` writes the user's or the folder's project config, with an Advanced OAuth secret. The `mcp` group also has `mcp.remove`, `mcp.signOut`, and the shared processes `mcp.instances` and `mcp.restart`. Writes need the `admin` scope. `settingsMcpServerOf` maps a `catalog.mcps` row to the section's server, as the desktop app's server host does. An MCP server's sign-in needs `oauthCallbackUrl`. It is the server's `/zidane/oauth/callback` at the origin through which the page reaches the server, where the browser comes back. A page cannot listen on a loopback port. zidane-server has no method for extension providers of a folder or removing a project's MCP server, so those controls stay hidden there.

## Pick a host

| Host | From | Keeps |
| - | - | - |
| `createServerSettingsHost({ client })` | `zidane-ui/adapters` | zidane-server's settings: `settings.get` (`chat` scope), `settings.set` (`admin` scope, so a `chat` token reads only), kept current by `settings:changed` |
| `createFixtureSettingsHost(options)` | `zidane-ui/settings/testing` | Memory, for demos and tests |
| Your own | `zidane-ui/settings/headless` | Anything |

| Member | Gives |
| - | - |
| `document`, `refresh()` | The settings: `loading`, `ready` with every key over `DEFAULT_SETTINGS`, or `error` with a typed `SettingsError` |
| `update(patch)` | Write a patch. The document shows it at once. A refused write rejects with a `SettingsError` and the document goes back. |
| `writable` | Whether this connection may write now |
| `skills`, `extensions`, `plugins`, `instructions`, `environments`, `logs`, `hostCatalog`, `paths` | The catalog sections' data and actions (above) |
| `providers`, `mcp` | Accounts' and MCP servers' data and actions (above) |

## Read settings anywhere

Put the host at the root and read it with `useSettings()`: `{ settings, setSetting, toggle }`, the defaults until the host has read. The provider subscribes to `document` once and hands it down through context, so every reader re-renders off that one subscription. `setSetting` writes through `update` and drops a refused write; the document goes back to what the host holds.

```tsx
import { SettingsProvider, useSettings } from 'zidane-ui/settings'

function ThinkingToggle() {
  const { settings, toggle } = useSettings()
  return <button type="button" aria-pressed={settings.showThinking} onClick={() => toggle('showThinking')}>Thinking</button>
}

<SettingsProvider host={settings}><App /></SettingsProvider>
```

## Model rows

Settings › Agent's rows that pick a model. The chat models come from the composer's catalog (`ComposerHost.models`, so put a `ComposerHostProvider` above), the rest from the host's optional `models` group:

| `SettingsHost.models` | Feeds | Left out |
| - | - | - |
| `decisionModels(cwd)` | `DecisionModelRow`, and whether `ApprovalModeRow` offers Model decision | no decision row; Model decision stays offered |
| `test(cwd, model)` | the Test button of `ModelPickField`, `TextModelRow`, `CompactionModelRow`, the default model | no Test |
| `testDecision(cwd, model?)` | the decision row's Test | no Test |
| `lastUsed()` | what the default model's "last used" chip tests | the default pick |

`AgentDefaultsRows` edits the agent, model and effort new sessions start on (`agentDefaultsOf(settings)` reads them, `agentDefaultsPatch(value)` writes them in one `update`); with `inherited` it is a space's rows, falling back to those. `SpacesDefaultsSection` is Settings › General › Spaces with each space's rows. `ConversationPresetsRow` previews the transcript presets. `ModelListPicker` is the bare-id list the decision row and extension `model` options open.

## Extension options, new extensions, Claude Code import

`ExtensionOptionsSection` lists the options and action rows the enabled extensions declare, as an `ExtensionsSection`'s children: values live on `Settings.extensionConfig`, an action runs on the host and its config patch is written in one `update`. `useExtensionOptionsSearch(cwd)` makes them search hits (`SettingsShell`'s `extraSearch`). They read optional members of `SettingsHost.extensions`: `configSchema`, `actions`, `actionStatuses`, `runAction`.

`CreateExtensionDialog` scaffolds an extension (`extensions.create`) and hands the result to your `onOpen` (open an authoring session); a project extension stops on the trust notice first. `ClaudeImportDialog` imports `~/.claude` (`SettingsHost.claudeImport`: `detect`, `run`). Both open on the modal layer (`useModal`) and render nothing when the host leaves their member out. `createServerSettingsHost` offers both over the server's own files (`extensions.create`, `claudeImport.detect` / `claudeImport.run`) to an `admin` token when the server takes installs (`allowRemoteInstalls`): a server scaffolds in its user's folder only, and the import lands on the server.

`useClaudeImportAvailable()` says whether to offer the import at all: `true` once `claudeImport.detect` finds a Claude Code setup on the host, `false` before and without `claudeImport` (the desktop app and the web app both hide the action otherwise, as the TUI does).

```tsx
import { ClaudeImportDialog, useClaudeImportAvailable } from 'zidane-ui/settings'

const available = useClaudeImportAvailable()
const actions = available
  ? [{ id: 'claude-import', label: 'Import from Claude Code', icon: 'i-ph-download-simple', onSelect: () => modal.open(<ClaudeImportDialog cwd={cwd} onClose={modal.close} />) }]
  : []
```

## Parts

| Part | Does |
| - | - |
| `SettingsShell`, `SettingsFrame`, `SettingsNav` | The screen, its default frame, the nav (a vertical tablist, ↑/↓ wrap) |
| `SettingsRowsPage` | A layout page of rows over the host |
| `ToggleRow`, `ChoiceRow`, `SelectRow`, `ValueChip` | A catalog toggle, a catalog choice, a row that picks one of its options from a menu, the value chip of a row that picks one |
| `SettingsPanel`, `SettingsPanelBody`, `SettingsPanelActions`, `SettingFocus` | A section body's column, its scrolling region, its header actions, a row search can land on |
| `SETTINGS_TOGGLES`, `SETTINGS_CHOICES`, `SETTINGS_PAGES` | The catalog and zidane's layout |
| `layoutSearchEntries`, `sectionSearchHits`, `searchSettings` | Search entries of a layout page, the shell's index, the match |
| `settingsPageCounts`, `listLength` | The nav's counts: a list page's length, a page of rows its settings |
| `SettingsHeadingText`, `SettingsTutorial` | A frame heading that crossfades between sections, a page's tutorial |

Every string comes from the labels (`DEFAULT_SETTINGS_LABELS`). The catalog's row copy is data: reword it by handing the shell your own sections.

## Test your host

```ts
import {
  describeSettingsAccountsContract,
  describeSettingsClaudeImportContract,
  describeSettingsExtensionOptionsContract,
  describeSettingsHostContract,
  describeSettingsModelsContract,
  describeSettingsSectionsContract,
} from 'zidane-ui/settings/testing/contract'

describeSettingsHostContract('my host', backend => makeHarness(backend), ['update', 'writable'])
// The catalog groups: the host is built over the arrays the suite hands it.
describeSettingsSectionsContract('my host', backend => makeSectionsHarness(backend), ['environments', 'hostCatalog'])
// Accounts and MCP servers: the host holds the providers and servers the suite hands it.
describeSettingsAccountsContract('my host', backend => makeAccountsHarness(backend), ['providers', 'providers.setApiKey', 'mcp'])
// The model rows, the extension options, the Claude Code import: each lists the members the host offers.
describeSettingsModelsContract('my host', backend => makeModelsHarness(backend), ['decisionModels', 'test', 'testDecision'])
describeSettingsExtensionOptionsContract('my host', backend => makeOptionsHarness(backend), [])
describeSettingsClaudeImportContract('my host', backend => makeImportHarness(backend), false)
```

| Entry point | Holds |
| - | - |
| `zidane-ui/settings` | `SettingsProvider`, `SettingsShell`, `SettingsRowsPage`, the rows and section parts |
| `zidane-ui/settings/headless` | `SettingsHost`, `SettingsSection`, the catalog, the layout, `layoutSections`, search, the labels |
| `zidane-ui/settings/testing` | `createFixtureSettingsHost` (its `sections` option fills the catalog groups and the extension options, `accounts` the providers and MCP servers, `models` the model rows' group, `claudeImport` the import), `DEMO_PROVIDERS`, `DEMO_MCP_SERVERS` |
| `zidane-ui/settings/testing/contract` | `describeSettingsHostContract`, `describeSettingsSectionsContract`, `describeSettingsAccountsContract`, `describeSettingsModelsContract`, `describeSettingsExtensionOptionsContract`, `describeSettingsClaudeImportContract` |
