Skip to content

zidane-ui/onboarding/headless

The setup wizard without components: the OnboardingHost contract, the run controller and the strings of the wizard.

import { … } from 'zidane-ui/onboarding/headless'ui/src/onboarding/headless/index.ts

Use this entry point to implement a host for the setup wizard, or to run the wizard without React. OnboardingHost holds what the wizard reads and the actions that it asks for. It reads what zidane found, the providers, the themes and the app icons. It asks to sign in, save a key, import, add a connector, install an extension and write the result. If you leave out a member, the wizard hides its step or its control. createOnboardingController connects a host to the core's run (createOnboardingRun in zidane/chat/pure). When the user passes a step, it starts the imports and installs that the user picked there. When the user passes the last step, it saves the result through host.commit. createFixtureOnboardingHost (zidane-ui/onboarding/testing) answers every member from demo data, for tests, galleries and docs.

TypeScript
import { createOnboardingController } from 'zidane-ui/onboarding/headless'
import { createFixtureOnboardingHost } from 'zidane-ui/onboarding/testing'

const host = createFixtureOnboardingHost({ delayMs: 0, onCommit: result => console.log(result.settings) })
const run = createOnboardingController(host, host.context.get()!, { start: 'features' })
run.dispatch({ type: 'set-settings', settings: { spaces: true } })
run.dispatch({ type: 'next' }) // Ready
run.dispatch({ type: 'next' }) // saves { set: { spaces: true }, reset: [] }

Functions#

connectorsFromImportFunction#

The connectors that an import brings over: the MCP servers that it copies and that have the id of a connector. The Connectors step marks them, and the run does not add them twice.

TypeScript
function connectorsFromImport(
  preview: OnboardingImportPreviewState,
  selection: OnboardingImportSelection | undefined,
): readonly string[]
Parameters
NameTypeDescription
previewOnboardingImportPreviewState

What the import brings over.

selectionOnboardingImportSelection | undefined

What the user picked to import.

readonly string[]
Example
import { connectorsFromImport } from 'zidane/chat/pure'

const none = { copy: [], skip: [] }
const preview = { sessions: { project: 0, all: 0 }, mcps: { copy: ['github', 'my-db'], skip: [] }, skills: none, commands: none, agents: none, notes: [] }
const selection = { sessions: false, allProjects: false, mcps: true, skills: false, commands: false, agents: false }
connectorsFromImport({ kind: 'ready', preview }, selection) // ['github']

dist/media-type-BzRZ4FqT.d.ts:10891

createOnboardingControllerFunction#

Starts a run over a host.

TypeScript
function createOnboardingController(
  host: OnboardingHost,
  setup: OnboardingSetup,
  options?: OnboardingRunOptions,
): OnboardingRun
Parameters
NameTypeDefaultDescription
hostOnboardingHost

The app's host.

setupOnboardingSetup

What the run starts from: host.context's value once detection finished.

options?OnboardingRunOptions{}

The step to open at, and the clock.

OnboardingRun

The controller. Call dispose when the wizard closes.

ui/src/onboarding/headless/controller.ts:95

onboardingPlanContextFunction#

What decides the steps of a run on this host: the setup's mode and surface, and the steps whose host member is missing left out.

TypeScript
function onboardingPlanContext(
  host: OnboardingHost,
  setup: OnboardingSetup,
): OnboardingPlanContext
Parameters
NameTypeDescription
hostOnboardingHost

The host.

setupOnboardingSetup

What the run starts from.

OnboardingPlanContext

ui/src/onboarding/headless/controller.ts:36

Constants#

DEFAULT_ONBOARDING_LABELSConstant#

The default English strings of the setup wizard, merged over the design system's labels.

TypeScript
const DEFAULT_ONBOARDING_LABELS: { accent: string; accentTheme: string; addDiagnosticToChat: (message: string) => string; added: string; address: string; addressPlaceholder: string; addsConnector: (connector: string) => string; addToChat: string; agentName: (agent: OnboardingAgentId) => string; allowOnce: string; allSources: string; alwaysAllow: string; apiKeyFor: (provider: string) => string; appIcon: string; approvalAsk: (command: string) => string; approvedByModel: string; askedWhenUnsure: string; back: string; background: string; browse: string; browseFailed: (message: string) => string; browsing: (source: string) => string; cancel: string; change: string; changeLater: string; changeStep: (step: string) => string; clearPicks: string; close: string; closeTab: (name: string) => string; code: string; completionSuggestions: string; configure: string; confirm: string; connected: string; connectFirst: string; connectProviderAction: string; connectProviderBody: string; connectProviderTitle: string; continue: string; copied: string; copy: string; copyCode: string; copyImage: string; copyImageBase64: string; copyImageDataUrl: string; createToken: string; credentialsAt: (path: string) => string; current: string; custom: string; darkTheme: string; demoAddress: string; demoAllowed: string; demoApproved: string; demoAsksYou: string; demoBlocked: string; demoBranch: string; demoCalls: string; demoCommitCommand: string; demoCommitMessage: string; demoEstimateSession: string; demoEstimateTask: string; demoFlowCalls: readonly ['read_file src/app.ts', 'edit src/limits.ts', 'bun test', 'git commit']; demoFolders: readonly ['my-app', '.zidane', 'extensions', 'deploy.ts']; demoGenericTitle: string; demoGhost: string; demoGoal: string; demoIssue: string; demoIssuePrompt: string; demoMessage: string; demoMessageCalls: readonly ['read app.ts', 'edit api.ts']; demoNotifyApp: string; demoNotifyBody: string; demoPageButton: string; demoPersonalSessions: readonly ['Plan the trip to Lisbon', 'Sort the tax receipts']; demoProjectPath: string; demoPromptGhost: string; demoPromptStart: string; demoQueued: string; demoReminderFrom: string; demoReminderIn: string; demoReminderNow: string; demoReminderText: string; demoReminderTyped: string; demoRiskyCall: string; demoRunEnded: string; demoScript: readonly ['const files = await glob(\'src/**/*.ts\')', 'for (const file of files) await read(file)']; demoSearchQuery: string; demoSearchResults: readonly ['vite.dev/guide/migration', 'github.com/vitejs/vite/releases', 'vite.dev/blog/announcing-vite6']; demoSecretDots: string; demoSecretPath: string; demoSecretPrompt: string; demoSecretSave: string; demoSecretSaved: string; demoSpacePersonal: string; demoSpaceWork: string; demoSteeredCall: string; demoStep: string; demoTab: string; demoTerminalError: string; demoTerminalLines: readonly ['$ bun dev', 'ready on localhost:5173', 'GET /chat 200 in 12 ms']; demoTitle: string; demoTyped: string; demoUpdateReady: string; demoUsageLabel: string; demoUsagePercent: string; demoVersion: string; demoWorkSessions: readonly ['Fix the login redirect', 'Review the rate limit']; deny: string; detecting: string; diagnosticsCheckedBy: (source: string) => string; diagnosticsChecking: string; diagnosticsNone: string; done: string; editorConflict: string; editorConflictDetail: string; editorEmpty: string; editorOverwrite: string; editorReadFailed: (message: string) => string; editorReload: string; effort: string; effortHint: string; effortLabel: (level: NonNullable<'off' | 'max' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'adaptive' | undefined>) => string; errorFailed: (region: string) => string; errorStack: string; errorTryAgain: string; experimental: string; filePath: (path: string) => string; fileReadOnly: (path: string) => string; fileTree: string; fileUnsaved: string; fileUnsavedTitle: (path: string) => string; filterSkills: string; findClose: string; findCloseHint: string; findInFile: string; findMatchCase: string; findNext: string; findNextHint: string; findNoResults: string; findPrevious: string; findPreviousHint: string; findReferences: string; found: string; foundAgent: (agent: OnboardingAgentId) => string; foundEnvKey: (envKey: string) => string; foundGithub: (login: string | undefined, source: string) => string; foundLocalServer: (server: OnboardingLocalServer) => string; foundSaved: (provider: string) => string; foundSignedIn: (provider: string) => string; fromImport: (agent: string) => string; fromRepo: (repo: string) => string; fullSetup: string; fullSetupDetail: string; fullSetupTime: string; goToDefinition: string; goToStep: (step: string) => string; importAgents: string; importAllProjects: string; importCommands: string; importFrom: (agent: string) => string; importMcps: string; importNames: (list: readonly string[]) => string; importNothing: string; importSessions: string; importSessionsDetail: (project: number, all: number) => string; importSkills: string; importSkipped: (count: number) => string; installed: string; installing: string; installProvider: (provider: string) => string; isDefault: (name: string) => string; lightTheme: string; listingTruncated: string; loading: string; localFound: (server: OnboardingLocalServer) => string; matchesPreset: (preset: string) => string; messagesDuringRun: string; messagesDuringRunHint: string; modalBack: string; modalFailed: string; mode: string; modeDark: string; model: string; modeLight: string; modeSystem: string; moreOptions: string; moreProviders: string; needs: (list: readonly string[]) => string; needsAccount: (account: string) => string; needsCommand: (name: string, optional: boolean) => string; needsConnector: (connector: string) => string; needsDecisions: string; next: string; noAccount: string; noGhLogin: string; noReferences: string; noSkillMatch: string; noSkills: string; noSkillsHint: string; notNow: string; of: (index: number, count: number) => string; openFiles: string; openInNewTab: string; pack: string; packHint: string; paletteBack: string; paletteBackHint: string; paletteChoose: string; paletteEmpty: string; paletteFilter: string; paletteLabel: string; paletteLoading: string; paletteNoMatch: (query: string) => string; paletteOpen: string; paletteRun: string; paletteScopeHint: string; paletteTurnOff: string; paletteTurnOn: string; presetInUse: string; presetYours: string; prevents: string; preview: string; previewPresets: string; previous: string; quickStart: string; quickStartDetail: string; quickStartTime: string; readInPlaceDetail: string; readInPlaceLabel: string; readInPlaceTitle: (agents: readonly string[]) => string; readyConfigured: string; readyEnv: (envKey: string) => string; readyKey: string; readyOAuth: string; recapAccent: (accent: string) => string; recapIcon: (icon: string) => string; recapLookSystem: (dark: string, light: string) => string; recapNone: string; recapProvider: (provider: string, model?: string) => string; recapSwitched: (on: readonly string[], off: readonly string[]) => string; recommended: string; recommendedSources: string; reload: string; rename: string; retry: string; runsWithoutAsking: string; sampleCall: string; sampleCommand: string; sampleShortCommand: string; sandboxed: string; save: string; saveFailed: (message: string) => string; saving: string; sceneAddServer: string; sceneAgent: string; sceneApiKey: string; sceneBranches: readonly ['main', 'fix-login']; sceneCall: string; sceneClick: string; sceneCmdClick: string; sceneCode: string; sceneCodeTypo: string; sceneCommand: string; sceneCredentials: string; sceneDesktop: string; sceneDiagnostic: string; sceneEditor: string; sceneEnvKey: string; sceneExtensionAdds: readonly ['/standup', 'deploy', 'Panel', 'Provider']; sceneExtensionCode: readonly ['export default defineExtension({', '  name: \'hello\',', '  setup(ctx) {', '    ctx.registerCommand(\'standup\')']; sceneExtensionFile: string; sceneExtensionName: string; sceneFileName: string; sceneFilesPanel: string; sceneHost: string; sceneListening: string; sceneLocal: string; sceneMcpFile: string; sceneNeedsTrust: string; sceneNewRun: string; sceneOnlyYou: string; sceneOr: string; scenePath: string; sceneProjectExtension: string; scenePromptParts: readonly ['zidane rules', 'AGENTS.md', 'MCP servers', 'Skills']; scenePromptTokens: readonly ['2.1k', '640', '320', '410']; sceneProvider: string; sceneResult: string; sceneSandbox: string; sceneSandboxes: readonly ['Docker', 'E2B', 'Daytona']; sceneSaved: string; sceneSecretCommand: string; sceneSecretDots: string; sceneSecretName: string; sceneSecretOutput: string; sceneSecretPrompt: string; sceneSecretSave: string; sceneSecretsFolder: string; sceneSecretTtl: string; sceneServer: string; sceneServerStart: string; sceneService: string; sceneSession: string; sceneSignedIn: string; sceneSkillAbouts: readonly ['Test first', 'Read PDFs']; sceneSkillNames: readonly ['tdd', 'pdf', 'docx']; sceneSkillPrompt: string; sceneSkillsFolder: string; sceneSkillUsing: string; sceneSystemFile: string; sceneSystemPrompt: string; sceneTerminal: string; sceneToken: string; sceneTokenCreate: string; sceneWorktree: string; sceneWss: string; sceneZidaneFolder: string; screenBack: (screen: string) => string; screenBackKey: string; selectAll: string; setup: string; signIn: string; signInCopyCode: string; signInDone: (account?: string) => string; signInEnterCode: (host: string) => string; signInFailed: string; signInInBrowser: (service: string) => string; signInNotOpened: string; signInOpenPage: string; signInPaste: string; signInStarting: string; signInSubmit: string; signInWith: (account: string) => string; skillCount: (count: number) => string; skillsPicked: (count: number) => string; skip: string; skipSetup: string; sourceChipName: (label: string, picked: number) => string; sourceField: string; sourceName: (label: string, repo: string) => string; sourcePlaceholder: string; spendCap: string; spendCapDetail: string; stars: (count: number) => string; start: string; stepLabel: (step: OnboardingStepId) => string; stepLede: (step: OnboardingStepId) => string; stepOf: (index: number, count: number) => string; steps: string; stepTitle: (step: OnboardingStepId) => string; taskDone: string; taskFailed: string; taskNeedsSignIn: string; taskQueued: string; taskRunning: string; terminal: string; terminalExited: (code: number) => string; terminalLoadFailed: (message: string) => string; testAgain: string; testFailed: string; testing: string; testOk: (model: string, seconds: string) => string; toAdd: (count: number) => string; toInstall: (count: number) => string; tokenFor: (connector: string) => string; tokenOptional: string; tokenOptionalAuth: string; tokenOrGhLogin: string; tokenSignIn: string; tutorialOnServer: (topic: TutorialTopic, host: string) => TutorialWords; tutorials: Readonly<Record<TutorialTopic, TutorialWords>>; unlocks: string; useApiKey: string; usedIn: (step: string) => string; useGhLogin: string; usePreset: (preset: string) => string; usesGhLogin: (login?: string) => string; useSignIn: string; usesTokens: string; useToken: string; viaExtension: string; viewOnGithub: string; viewOnNpm: string; whatYoullSee: string; working: string; wrapLines: string; zoomFit: string; zoomPercent: (percent: number) => string }

ui/src/onboarding/headless/labels.ts:38

Interfaces#

OnboardingAppIconInterface#

An app icon that Look offers.

TypeScript
interface OnboardingAppIcon
OnboardingAppIcon properties
NameTypeDescription
idstring

Stable id, stored in OnboardingAppearance.icon.

labelstring

Display name.

srcstring

URL of a square image of the icon. A data URL works.

ui/src/onboarding/headless/host.ts:78

OnboardingControllerOptionsInterface#

Options of createOnboardingRun.

TypeScript
interface OnboardingControllerOptions
OnboardingControllerOptions properties
NameTypeDescription
now?() => Date

The clock of the records. It defaults to the current time.

start?OnboardingStepId

Opens the run at this step instead of its first one, when the run has it.

dist/media-type-BzRZ4FqT.d.ts:10941

OnboardingHostInterface#

What an app implements to run the setup wizard.

TypeScript
interface OnboardingHost
OnboardingHost properties
NameTypeDescription
appIcons?{ list: Readable<readonly OnboardingAppIcon[]>; note?: string }

App icons for Look, for an app that has several. Absent: Look offers no icon.

connectors?{ installed: Readable<readonly string[]>; add(id: string, input?: ConnectorInput): Promise<void>; githubToken?(): Promise<string | null>; signIn?(id: string): SignInFlow }

Connectors. Absent: the step is left out.

contextReadable<OnboardingSetup | null>

What the wizard starts from. It is null while the app still detects.

extensions?{ installed: Readable<readonly string[]>; install(name: string, onProgress?: (message: string) => void): Promise<void> }

Native extensions. Absent: the step is left out.

imports?{ preview(): Promise<OnboardingImportPreview>; run(selection: OnboardingImportSelection): Promise<void> }

Bring your setup. Absent: the step is left out.

providers{ list: Readable<readonly OnboardingProvider[]>; models?(key: string): Promise<readonly OnboardingModelInfo[]>; saveKey?(key: string, input: { fields?: Record<string, string>; value?: string }): Promise<void>; signIn?(key: string): SignInFlow; test?(key: string, model?: string): Promise<ProviderTestResult> }

The providers Connect lists, and how to set them up.

skills?{ images?: SkillSourceImages; installed: Readable<readonly string[]>; browse(input: string): Promise<SkillSourceListing>; install(source: string, rels: readonly string[], onProgress?: (message: string) => void): Promise<void> }

Skills. Absent: the step is left out.

themes?{ list: Readable<readonly Theme[]>; preview(appearance: OnboardingAppearance | null): void }

Themes for Look. Absent: the step is left out.

commit(result: OnboardingResult) => Promise<void>

Writes what a finished run decided.

ui/src/onboarding/headless/host.ts:108

OnboardingModelInfoInterface#

A model Connect offers as the default.

TypeScript
interface OnboardingModelInfo
OnboardingModelInfo properties
NameTypeDescription
defaultEffort?OnboardingEffort

The effort it runs at when none is picked.

efforts?readonly OnboardingEffort[]

The efforts the model takes, lowest first. Absent: no effort pick.

idstring

Model id.

labelstring

Display name.

recommended?boolean

The model the wizard picks first.

ui/src/onboarding/headless/host.ts:64

OnboardingSetupInterface#

What the wizard starts from.

TypeScript
interface OnboardingSetup
OnboardingSetup properties
NameTypeDescription
appearance?OnboardingAppearance

The look in effect now, for a desktop app.

credentialsPath?string

Where credentials are stored, for Connect's note (~/.zidane/credentials.json).

detectionOnboardingDetection

What was found on this machine.

modeOnboardingMode

How much of it runs.

provider?OnboardingProviderPick

The provider, model and effort in use now, for a re-run.

settingsPartial<Settings>

The stored settings: explicit choices only, without the defaults.

surfaceOnboardingSurface

The app that runs it.

ui/src/onboarding/headless/host.ts:33

ProviderTestResultInterface#

The outcome of Connect's live test.

TypeScript
interface ProviderTestResult
ProviderTestResult properties
NameTypeDescription
durationMsnumber

How long it took, in milliseconds.

message?string

Why it failed.

model?string

The model the test ran on.

okboolean

The model answered.

ui/src/onboarding/headless/host.ts:88

SignInFlowInterface#

A browser sign-in in progress: its state, and the ways to answer it or stop it.

TypeScript
interface SignInFlow
SignInFlow properties
NameTypeDescription
stateReadable<OnboardingSignInState>

Where the sign-in is. done and error end it.

cancel() => void

Stops the sign-in.

submit?(text: string) => void

Answers a prompt, or passes a pasted redirect URL or code. Absent: the flow takes no input.

ui/src/onboarding/headless/host.ts:51

SkillSourceImagesInterface#

Where the skill source browser finds the pictures of a source.

TypeScript
interface SkillSourceImages
SkillSourceImages properties
NameTypeDescription
avatar(owner: string) => string | undefined

The avatar of a GitHub owner. Absent: the owner's letter.

preview(owner: string, repo: string) => string | undefined

The preview image of a repository, 2:1. Absent: no preview.

ui/src/onboarding/headless/host.ts:100

Types#

OnboardingControllerType#

A run and the work it starts. Its stores are Readables, so useReadable reads them.

TypeScript
type OnboardingController = OnboardingRun

ui/src/onboarding/headless/controller.ts:27

OnboardingEffortType#

A reasoning effort level, as the model picker names it.

TypeScript
type OnboardingEffort = NonNullable<OnboardingProviderPick['effort']>

ui/src/onboarding/headless/host.ts:61

OnboardingImportPreviewStateType#

What an import from Claude Code brings over.

  • none: the run offers no import, because there is no Claude Code setup or no import.
  • loading: the host is reading it.
  • ready: the preview.
  • failed: the host failed to read it.
TypeScript
type OnboardingImportPreviewState =
  | { kind: 'none' }
  | { kind: 'loading' }
  | { kind: 'ready'; preview: OnboardingImportPreview }
  | { kind: 'failed'; message: string }

dist/media-type-BzRZ4FqT.d.ts:10863

OnboardingLabelsType#

The setup wizard's strings: DEFAULT_ONBOARDING_LABELS' keys.

TypeScript
type OnboardingLabels = typeof DEFAULT_ONBOARDING_LABELS

ui/src/onboarding/headless/labels.ts:307

OnboardingPhaseType#

Where a run is in its life.

  • running: the user is going through the steps.
  • saving: the last step was passed, and the host is writing the result.
  • saved: the result is written. The app closes the wizard.
  • failed: writing failed. The run is back on its step, and moving on again tries again.
TypeScript
type OnboardingPhase =
  | { kind: 'running' }
  | { kind: 'saving' }
  | { kind: 'saved'; result: OnboardingResult }
  | { kind: 'failed'; message: string }

dist/media-type-BzRZ4FqT.d.ts:10844