Skip to main content

Module: settings

The user-facing configuration surface — one page where every built-in module or installed plugin exposes its own settings, VS Code contributes. configuration style. Each contributor declares its settings; the page auto-renders a form and persists overrides.

Status: implemented — frontend in packages/core/src/modules/settings/ (panel) and packages/core/src/settings.ts (the reactive service), backend in backend/modules/settings/.

The settings contract

A setting is a registry contribution declared alongside a module's commands/panels/widgets (and identically in a plugin's PluginContributions):

{ key: 'observability.recentCount', title: '…', description: '…',
type: 'number', default: 5 } // types: string | number | boolean | enum

Keys are namespaced like every other contributed id — <module>.<name>, and <pluginId>.<name> for plugins (the loader's assertNamespaced enforces this on setting keys too). The SettingDecl/SettingType types live in @horribledashboard/sdk so built-in modules and plugins share one shape.

Schema on the frontend, values on the backend

The schema and defaults live in the frontend declarations; the backend is a dumb key→value bag (settings.json) that stores only the values the user has overridden. The effective value of a key is its override, or the declared default when unset — resolved in settings.ts. This mirrors how the dashboard backend stores layout without knowing widget shapes, and keeps the backend schema-agnostic.

settings.ts is a reactive store (the telemetry.ts pattern): loadSettings() seeds overrides at boot; getSetting/setSetting/resetSetting read and mutate; useSetting(key) is the hook built-in components use for a live read. Setting a value is optimistic (local update + PUT), so widgets bound via useSetting re-render immediately.

Custom sections

Some configuration is too rich for the declarative SettingDecl controls (string/number/boolean/enum). A module may also contribute a settingsSections entry — { id, title, component } on its ModuleManifest — and the page renders the component as its own group after the declared settings. The first consumer is the agent's Agent permissions section (PermissionsSettings): a default-mode picker plus editable allow/ask/deny rule lists that read/write agent.permissions.* through /api/settings directly (rule lists are arrays, outside the scalar useSetting store). See ../architecture/agent-tools.md. This seam is built-in only — not part of the public plugin contract yet.

Contributions to the layout shell

  • Panels: settings.home (singleton, center) — the settings page, grouped by the module/plugin that declared each setting.
  • Commands: settings.open.
  • Keybindings: mod+,settings.open (VS Code parity).

Built-in settings shipped as the first consumers: observability.recentCount (how many calls the Data flow widget lists), observability.maxBodyChars (how many characters of each captured I/O body to keep — read on the backend, see below) and, from the reference plugin, hello-widget.greetingText.

Most settings are read on the frontend via useSetting, but a value can also be consumed by the backend: backend.modules.settings.get_value(key, default) returns the user override or the caller's fallback (defaults stay frontend-owned, so backend consumers supply their own). observability.maxBodyChars works this way — the telemetry instrumentation truncates bodies at capture time.

Plugins

Plugins declare settings in their contributions and read values through host.settings (get/set/subscribe), namespace-guarded to their own keys. This is distinct from host.storage: settings are user-configurable on this page; storage is the plugin's own runtime bookkeeping. See ../architecture/plugin-sdk.md.

Backend surface

backend/modules/settings/GET /api/settings (all overridden values), PUT /api/settings/{key} (set one, body {value}), DELETE /api/settings/{key} (clear an override → revert to the frontend default). Persists to $HORRIBLE_DATA_DIR/settings.json.

Browser vs desktop

Identical. Settings are stored by the backend wherever it runs (local or remote); no platform branching in the page or the service.

Not yet

Object/array setting types, backend-side validation, search/filter in the page, import/export, and cross-device sync. Schema stays frontend-owned.