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.
General
Sessions, updates, notifications and spaces.
Mount it#
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.
groupnames the section it sits under in the nav, indented. Without one it is a top-level entry.searchlists the rows inside it. A result opens the section and highlights the row withdata-setting-focusequal to itskey. Withoutsearchthe section is found by its title, description and keywords.extraSearchadds entries the section can't list up front.keepMountedkeeps the body mounted after its first visit, so its async state survives switching away. Its header actions portal into the screen's header withSettingsPanelActions.rendertakes no arguments. The body readsuseSettingsSection(): whether it is open, andselect(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.
Couldn't reach the settings.
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.
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.
- Send messageReturn
- New lineShift Return
- Previous history entryUp Arrow
- Next history entryDown Arrow
- Mention a file@@
- Use a skill//
- New sessionControl NNCommand NN
- Back to the main sessionControl Shift MM
- Browse messagesControl SS
- Session detailsControl Shift XX
- Switch agentShift Command II
- Switch modelControl MM
- Reasoning effortControl LL
- Todo listControl TT
- ArtifactsControl Shift AA
- Stop the current workControl KK
- Generate titleControl Shift TT
- Compact conversationControl Shift KK
- Export as MarkdownControl Shift EE
- Export as JSONControl Shift JJ
- Copy session idControl Shift CC
- Add to or remove from favorites
- Delete sessionControl Shift DD
- 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 WWCommand WW
- Split the panel tab rightCommand \\
- Split the panel tab downOption Command \\
- Find in the file or diffs (in Files and Diffs)Command FFControl FF
- Search across files (in Files and Diffs)Command Shift FFControl Shift FF
- Show or hide the terminalControl ``Command JJ
- New terminalControl Shift ``
- Annotate the page (in the Browser tab)Command ..
- Command paletteCommand KK
- Show or hide the sidebarCommand BB
- Back to the previous conversationCommand Left ArrowCommand [[
- Forward to the next conversationCommand Right ArrowCommand ]]
- UsageControl PP
- SettingsCommand ,,
- KeybindingsControl YY??
- Zoom inCommand PlusPlus
- Zoom outCommand MinusMinus
- Reset zoomCommand 00
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 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.
How skills work
Recommended sources
How extensions work
How instructions work
Loading environments…
How skills work
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.
| 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 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.
- AnthropicClaude models. Sign in with your Claude plan, or use an API key.
- work
- apikey
- OpenAIGPT models. Sign in with your ChatGPT plan, or use an API key.
- OpenRouterHundreds of models behind one key.
- Google Vertex AIGemini on your Google Cloud project.
- work
- apikey
- filesystem
- linear
- search
Can't list this server's catalog from here. Install and add skills on the server.
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.
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.
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).
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 a Zidane extension and open a session to write it.
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.
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#
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 |