Skip to content

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.

Settings

General

Sessions, updates, notifications and spaces.

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.

Read-only (a token without the admin scope)
Failed read

Preference sections#

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

SectionReadsThe app adds
AppearanceSectionThe nearest AppearanceProvider: mode, themes, code theme, accent, contrast, fonts, sizes, motionappIcon (an App icon group after Accent), onReset (runs after Reset appearance)
EditorSectionThe files host's editorPrefs (editing, saving, diagnostics, language servers) and the appearance host's tree sidecwd, openIn (where clicked files open), intro, diagnosticsTools(cwd) and languageServers(cwd) (the per-folder lists)
KeybindingsSectionThe nearest KeybindingsProvider: every row of KEYBINDING_SECTIONS with the keys in effectactions (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.

Appearance (live: themes this frame)
Keybindings with two overrides
  • Send messageReturn
  • New lineShift Return
  • Previous history entryUp Arrow
  • Next history entryDown Arrow
  • Mention a file@
  • Use a skill/
  • New sessionControl NorCommand N
  • Back to the main sessionControl Shift M
  • Browse messagesControl S
  • Session detailsControl Shift X
  • Switch agentShift Command I
  • Switch modelControl M
  • Reasoning effortControl L
  • Todo listControl T
  • ArtifactsControl Shift A
  • Stop the current workControl K
  • Generate titleControl Shift T
  • Compact conversationControl Shift K
  • Export as MarkdownControl Shift E
  • Export as JSONControl Shift J
  • Copy session idControl Shift C
  • Add to or remove from favorites
  • Delete sessionControl Shift D
  • Next panelControl Shift ]
  • Previous panelControl Shift [
  • Next tab, file or terminal (else next space)Command Option Right Arrow
  • Previous tab, file or terminal (else previous space)Command Option Left Arrow
  • Close tab, file or terminalControl WorCommand W
  • Split the panel tab rightCommand \
  • Split the panel tab downOption Command \
  • Find in the file or diffs (in Files and Diffs)Command ForControl F
  • Search across files (in Files and Diffs)Command Shift ForControl Shift F
  • Show or hide the terminalControl `orCommand J
  • New terminalControl Shift `
  • Annotate the page (in the Browser tab)Command .
  • Command paletteCommand K
  • Show or hide the sidebarCommand B
  • Back to the previous conversationCommand Left ArroworCommand [
  • Forward to the next conversationCommand Right ArroworCommand ]
  • UsageControl P
  • SettingsCommand ,
  • KeybindingsControl Yor?
  • Zoom inCommand Plus
  • Zoom outCommand Minus
  • Reset zoomCommand 0
Files & editor, language servers on

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.

SectionReadsThe app adds
SkillsSectionskills: 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
ExtensionsSectionextensions: list(cwd), install, update, remove. The checkboxes write enabledExtensions.cwd, onEdit, onCreateExtension, children under the list
PluginsSectionplugins: catalog(cwd). The checkboxes write enabledPlugins (a consent gate).cwd
InstructionsSectioninstructions: the session's prompt (segments), createSystemOverride, openFilesessionId, localFiles, renderMarkdown
EnvironmentsSectionenvironments: list(cwd), update(change), verify(kind)cwd
LogsSectionlogs: list(), follow(onRecord)
PerformanceSectionperformance: 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.
HostCatalogSectionhostCatalog: 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 SettingsSections 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.

Skills (live: toggle, search, delete)
No skills in .claude/skills or ~/.claude/skills.
Extensions, one failed to load
No extensions in .zidane/extensions or ~/.zidane/extensions.
Plugins, one rejected
Looking for plugins…
Instructions of a session
Environments

Loading environments…

Logs
level ≥
follow0 lines
Nothing logged yet. Extension warnings, MCP diagnostics and host debug lines appear here.
Performance (live: figures drift every 2 s)
Measuring…
Performance overlay
Measuring…
A server's skills

box doesn't take installs from this app. Install on the server.

Reading box's skills…

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.

SectionReadsThe app adds
ProvidersSectionproviders: 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)
ServerProvidersSectionThe same providers group, drawn as a server's cards: Set API key, Sign in (ServerSignIn: the page here, the tokens there), Remove.hostName
McpSectionmcp: 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
EnabledListSectionThe settings document only: a host's enabled* list, for a host that can't list its catalog (HostCatalogSection falls back to it).settingKey, hostName
ServerSignInA 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 SettingsSections. 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.

Accounts: keys, sign-ins, several credentials
  • Anthropic
    API keyOAuth
    Claude models. Sign in with your Claude plan, or use an API key.
    • workin 12 daysada@example.com(in use)
    • apikeycredentials.json
  • OpenAI
    GPT models. Sign in with your ChatGPT plan, or use an API key.
  • OpenRouter
    Hundreds of models behind one key.
  • Google Vertex AI
    Gemini on your Google Cloud project.
A server's Accounts
Anthropicconfigured
in 12 dayscredentials.json
  • workin 12 daysada@example.com(in use)
  • apikeycredentials.json
OpenAInot configured
OpenRouterconfigured
OPENROUTER_API_KEY
Google Vertex AInot configured
MCP servers of a folder
  • filesystemstdioshared · 23/3 tools
    npx -y @modelcontextprotocol/server-filesystem ~/work
    Connected (2h ago)
  • linearhttpNeeds sign-in
    https://mcp.linear.app/mcp
  • searchhttpKey refused
    https://search.example.com/mcp
    Check the headers in mcps.json. (1h ago)
A server's enabled skills

Can't list this server's catalog from here. Install and add skills on the server.

Pick a host#

HostFromKeeps
createServerSettingsHost({ client })zidane-ui/adapterszidane-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/testingMemory, for demos and tests
Your ownzidane-ui/settings/headlessAnything
MemberGives
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.
writableWhether this connection may write now
skills, extensions, plugins, instructions, environments, logs, hostCatalog, pathsThe catalog sections' data and actions (above)
providers, mcpAccounts' 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.modelsFeedsLeft out
decisionModels(cwd)DecisionModelRow, and whether ApprovalModeRow offers Model decisionno decision row; Model decision stays offered
test(cwd, model)the Test button of ModelPickField, TextModelRow, CompactionModelRow, the default modelno Test
testDecision(cwd, model?)the decision row's Testno Test
lastUsed()what the default model's "last used" chip teststhe 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.

Defaults for new sessions (live: pick, Test)
Helper models and tool approvals
Extension options and actions (live: toggle, Run)

No extension options. Enabled extensions add theirs here.

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} />) }]
  : []
Create extension

Create extension

Create a Zidane extension and open a session to write it.

Scope
~/.zidane/extensions/<name>
Import from Claude Code (live: Import)

Import from Claude Code

Copy conversations, MCP servers, skills, commands and subagents from ~/.claude. Zidane never changes your Claude Code setup and skips what it already imported.

Looking for Claude Code…

Parts#

PartDoes
SettingsShell, SettingsFrame, SettingsNavThe screen, its default frame, the nav (a vertical tablist, ↑/↓ wrap)
SettingsRowsPageA layout page of rows over the host
ToggleRow, ChoiceRow, SelectRow, ValueChipA 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, SettingFocusA section body's column, its scrolling region, its header actions, a row search can land on
SETTINGS_TOGGLES, SETTINGS_CHOICES, SETTINGS_PAGESThe catalog and zidane's layout
layoutSearchEntries, sectionSearchHits, searchSettingsSearch entries of a layout page, the shell's index, the match
settingsPageCounts, listLengthThe nav's counts: a list page's length, a page of rows its settings
SettingsHeadingText, SettingsTutorialA 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#

TypeScript
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 pointHolds
zidane-ui/settingsSettingsProvider, SettingsShell, SettingsRowsPage, the rows and section parts
zidane-ui/settings/headlessSettingsHost, SettingsSection, the catalog, the layout, layoutSections, search, the labels
zidane-ui/settings/testingcreateFixtureSettingsHost (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/contractdescribeSettingsHostContract, describeSettingsSectionsContract, describeSettingsAccountsContract, describeSettingsModelsContract, describeSettingsExtensionOptionsContract, describeSettingsClaudeImportContract