Skip to content

Settings Panel & Capability Pages

dazeb edited this page Sep 17, 2026 · 2 revisions

Settings Panel & Capability Pages

This page covers the settings UI shell: how the cog opens it, how the sidebar/group navigation is composed, how a page turns into a list of sections, and how the per-feature capability pages (Skills / Hooks / Commands / Plugins / Subagents / MCP / Usage) plug into that shell. The internals of those capability pages are documented separately under Capability Settings: MCP, Plugins, Skills, Subagents & Usage and Hook & Slash Command Settings; here we focus on the shell they render inside.

Responsibilities

  • Entry point. CogMenu.tsx is a single presentational button — no menu, no dropdown. The old source-control/settings dropdown was removed: source control now lives in the sidebar, so the gear is a one-click path to the app settings sheet (src/renderer/src/components/CogMenu.tsx#L1-L27).
  • Panel shell. AppSettingsPanel.tsx renders a full-width sheet with a head row (title + close), a left sidebar nav, and a scrollable content column grouped by domain. It renders its controls with the shared UI kit in components/ui/kit.tsx (Button, Card, CardNote, FieldRow, Hint, PrefRow, Row, Section, Select, Status, TextArea, TextInput, Toggle) and is styled by Tailwind v4 utilities with tokens in src/renderer/src/settings.css (neutral palette — no lime) (src/renderer/src/components/AGENTS.md#L13-L30).
  • Capability page bodies. CapabilityPages.tsx exports one component per capability page — SkillsPage, HooksPage, CommandsPage, PluginsPage, SubagentsPage, McpServersPage, UsagePage — which the panel imports and mounts as sections (src/renderer/src/components/AppSettingsPanel.tsx#L12-L20).

Call chain: toolbar → panel → live settings

App.tsx owns the mount state and the settings mirror. The toolbar always renders CogMenu; the panel is conditionally mounted, and any settings write is echoed back into the app root so live-settings (e.g. invert-wheel zoom) take effect without reopening the panel (src/renderer/src/App.tsx#L88-L104).

flowchart TD
  Toolbar["App toolbar"] --> Cog["CogMenu — one-click gear"]
  Cog -->|onOpenSettings| Open["App: settingsOpen = true"]
  Open --> Panel["AppSettingsPanel (full-width sheet)"]
  Panel -->|onClose / Escape / backdrop| Close["App: settingsOpen = false"]
  Panel -->|onSettingsChange| Sync["App: setSettings"]
  Sync --> Theme["applyTheme(settings.theme)"]
  Sync --> Home["useBrowserHome.setHomeUrl(browserHomeUrl)"]
  Sync --> Canvas["Canvas: invertWheelZoom"]
Loading

Key nodes:

  • CogMenu is stateless; it only signals onOpenSettings. There is no menu to keep in sync.
  • AppSettingsPanelProps declares onClose (required) and onSettingsChange (optional), with the doc comment spelling out the live-settings contract (src/renderer/src/components/AppSettingsPanel.tsx#L22-L27).
  • On app boot, App loads settings once via window.termsprawl.settings.get() and calls applyTheme(...); the same settings object is handed to the panel as the initial SectionCtx.settings (src/renderer/src/App.tsx#L40-L47).

Panel composition and page gating

The sidebar is data-driven. NAV_GROUPS is an ordered list of { label, pages }, each page a SettingsPage with id, title, optional description, an icon, and optional editions. The full PageId union is the page inventory: general, appearance, models, browser, skills, hooks, commands, plugins, subagents, mcp, accounts, a2a, usage, cloud, connections, updates (src/renderer/src/components/AppSettingsPanel.tsx#L146-L165, #L123-L139).

flowchart TD
  Groups["NAV_GROUPS"] --> Pages["SettingsPage: id, title, description?, icon, editions?"]
  Pages --> Gate1{"editions omits current EditionKind?"}
  Gate1 -->|yes| DropPage["page omitted from sidebar"]
  Gate1 -->|no| Nav["sidebar nav item — selects active PageId"]
  Nav --> Body["scrollable content column"]
  Body --> Sections["SettingsSection[] for this page"]
  Sections --> Gate2{"section.editions omits current edition?"}
  Gate2 -->|yes| DropSec["section not rendered"]
  Gate2 -->|no| Bare{"section.bare?"}
  Bare -->|yes| Own["render(ctx) supplies its own cards — CapabilityPages exports"]
  Bare -->|no| Wrapped["render(ctx) inside the panel's card wrapper"]
  Own --> Ctx["SectionCtx"]
  Wrapped --> Ctx
Loading

Key nodes:

  • EditionKind is 'desktop' | 'server', read from the bridge's runtime hint. A page like browser is marked editions: ['desktop'] and is dropped from the Server Edition panel — the canvas shows only what the server actually implements (src/renderer/src/components/AppSettingsPanel.tsx#L140-L155, #L213-L225).
  • bare exists for pages that already lay out their own titled groups (the code names Skills, Hooks, Commands, MCP, Usage) — those sections skip the panel's card wrapper (src/renderer/src/components/AppSettingsPanel.tsx#L67-L77).
  • The first two groups are visible in the evidence: Basics (general, models, browser) and Agent capabilities (skills, hooks, commands, mcp, subagents, plugins). The remaining union members (appearance, accounts, a2a, usage, cloud, connections, updates) are declared in PageId and described in components/AGENTS.md as General / User / Agents / Connections / Updates at a higher level (src/renderer/src/components/AppSettingsPanel.tsx#L167-L300, src/renderer/src/components/AGENTS.md#L13-L25).

Section model and the shared section context

A SettingsSection is { id, title?, bare?, editions?, render(ctx) }. The in-code comment calls this out explicitly: adding one to a page's render array is the extension point for future settings, and editions is the mechanism that drops a section from one edition (src/renderer/src/components/AppSettingsPanel.tsx#L63-L77).

Everything a section can read or do arrives through SectionCtx — the single plumbing door:

Group Fields
Core settings settings: AppSettings, update(patch): Promise<AppSettings>, permissionSupported
Accounts addAccount, deleteAccount, setActive, setPermissionMode, loginInto, newLabel/setNewLabel, confirmDelete/setConfirmDelete
Cloud / space cloudUser, cloudBusy, device, lastBackup, space, spaceBusy, spaceError, spaceNote, spaceLoaded, cloudSignIn, cloudSignOut, cloudBackupNow, cloudOpenSpace, cloudOpenSnapshot, cloudSyncProject
GitHub ghConnected, ghBusy, ghNote, ghConnect, ghDisconnect
Workspace bundle workspaceExportBundle, workspaceImportBundle

(src/renderer/src/components/AppSettingsPanel.tsx#L79-L121.)

The save path is a round-trip through the panel's update(patch) contract and back out through onSettingsChange:

sequenceDiagram
  participant UI as PrefRow / capability page
  participant Panel as AppSettingsPanel
  participant Bridge as preload settings channel
  participant App as App (root)
  UI->>Panel: ctx.update(patch)
  Panel->>Bridge: persist patch
  Bridge-->>Panel: new AppSettings
  Panel->>App: onSettingsChange(settings)
  App->>App: setSettings + applyTheme
Loading

Note on the diagram: the persistence hop is shaped by the declared update(patch) => Promise<AppSettings> signature plus the sibling write pattern used elsewhere in App (window.termsprawl.settings.set(...).then(setSettings)); the exact internal call inside AppSettingsPanel was not part of the read excerpt.

Capability pages

CapabilityPages.tsx is the body library for the agent-capability pages. Each export corresponds to exactly one PageId and is mounted as a bare section: the page owns its own titled cards, its own lists, and its own per-item actions, while the surrounding nav, page title/description, close behavior, and edit/close affordances all stay in AppSettingsPanel (src/renderer/src/components/AppSettingsPanel.tsx#L12-L20, #L63-L77).

Related panel-adjacent helpers that the shell reaches for directly:

  • HelpBadge.tsx — a portaled ? explanation next to titles, so node overflow cannot clip it (src/renderer/src/components/AGENTS.md#L29-L30).
  • relay-trust.ts — trustState / TrustState, the trust-decision helper consumed by the Connections/Relay surface (paired with relay-trust.test.ts) (src/renderer/src/components/AppSettingsPanel.tsx#L10).
  • discoverAgentCard from core/a2a/client — A2A peer discovery invoked from the A2A section.
  • parseRelayTermFrame / RelayTermFrame from core/relay-term — relay terminal-frame handling used in the Relay surface.
  • useProjects and useCanvasRequests — the panel reaches into the project store and canvas request bus for cloud/space and bundle actions (src/renderer/src/components/AppSettingsPanel.tsx#L6-L9).

Agent ordering is a panel-level constant: PRIMARY_AGENTS = ['codex', 'grok'], with Claude registered but shown as optional/secondary (src/renderer/src/components/AppSettingsPanel.tsx#L29-L31).

Key state

  • App root: settings: AppSettings | null, settingsOpen: boolean (plus version/error). Settings can be null during boot; the panel is only mounted on user action, at which point the panel expects a concrete AppSettings (src/renderer/src/App.tsx#L22-L25, #L102-L104).
  • Panel: the active PageId, plus local draft/async state folded into SectionCtx — newLabel, confirmDelete, and the per-integration status fields (cloudBusy, spaceBusy, spaceError, spaceNote, spaceLoaded, ghBusy, ghNote).
  • Persisted and applied outside the panel: theme choices go through state/theme.ts (applyTheme); accent resolution happens in App via resolveAccent and is also the never-purple guard (src/renderer/src/App.tsx#L34-L37).

Boundary conditions

  • Edition split. EditionKind gating is the only mechanism that removes entire pages/sections on Server Edition; browser is explicitly desktop-only (src/renderer/src/components/AppSettingsPanel.tsx#L140-L155, #L213-L225).
  • Runtime capability flags. permissionSupported and spaceLoaded are boolean gates a section must respect: permissionSupported reflects whether the active CLI supports permission-mode control, and spaceLoaded is false until the panel has learned whether the user has a cloud space — sections must not render "no space" prematurely (src/renderer/src/components/AppSettingsPanel.tsx#L79-L103).
  • Close behavior. The sheet closes on Escape, backdrop click, or the header close button (src/renderer/src/components/AGENTS.md#L24-L25).
  • Confirmation UI. Any destructive action inside settings must use the in-app .confirm-overlay pattern (as TabBar does); window.confirm is forbidden because Electron silently no-ops it (src/renderer/src/components/AGENTS.md#L7-L9, #L32-L35).
  • Null-safety at the seam. App reads settings?.theme, settings?.invertWheelZoom, and settings?.browserHomeUrl with optional chaining before the panel ever opens; the panel itself can safely assume a concrete object (src/renderer/src/App.tsx#L37-L52, #L128).
  • Onboarding is adjacent but separate. ShouldShowOnboarding(settings, activeProjectCount) is evaluated in App, not the panel; dismissing it writes onboardedAt through the same settings channel and then setSettings (src/renderer/src/App.tsx#L107-L113).

Extension points

  1. Add a top-level page: add a PageId variant, add a SettingsPage entry to the right NavGroup, then add the render branch for that id in the content column. Mark editions if the page is desktop- or server-only (src/renderer/src/components/AppSettingsPanel.tsx#L146-L165).
  2. Add an in-page section: push a SettingsSection onto that page's render array. This is the documented extension point for future settings (src/renderer/src/components/AppSettingsPanel.tsx#L63-L77).
  3. Mark a section bare when it owns its card layout — this is how the capability pages mount (src/renderer/src/components/AppSettingsPanel.tsx#L67-L77).
  4. Extend SectionCtx when new shared state or handlers are needed; the interface is deliberately the aggregate so sections don't reach into stores individually.
  5. Add a capability page body in CapabilityPages.tsx and export it; the shell import list at the top of AppSettingsPanel.tsx is the wire-up point (src/renderer/src/components/AppSettingsPanel.tsx#L12-L20).

Sources: src/renderer/src/components/AppSettingsPanel.tsx, src/renderer/src/components/CapabilityPages.tsx, src/renderer/src/components/CogMenu.tsx, src/renderer/src/App.tsx, src/renderer/src/components/AGENTS.md.

termsprawl

App Shell & Platform Foundations

Canvas, Nodes & Renderer State

Terminals & Session Continuity

Persistence, Projects & Files

Agent Runtime & Tooling

Chat Nodes & Model Providers

Git & Source Control

Embedded Browser Nodes

Server Edition

Relay & Remote Access

Integrations & Secondary Surfaces

Settings, Updates & Maintenance

Clone this wiki locally