Thin working settings UI (owner-approved plan): /settings + live appearance + keyboard rebinding
The validation surface for the settings foundation — deliberately thin,
real read/write against the delta record, polish is a later wave.
Mount (owner decisions): dedicated page, single scroll; entry = sidebar
bottom row + ⌘/Ctrl+, (open-settings in the registry); chat defaults NOT
shown (they live on /chat/new); mode dark-only (light palette = design
wave); final tool list = research/demo/visual/pdf (master-adjudicated).
Sections (PUT /api/settings/[section], replace-whole per section):
- General: auto-update toggle.
- Agent: six background-role model+thinking selects (Default = config
pin; a stored id stays VISIBLE even when the catalog hasn't loaded or
dropped it — a select showing Default over a live override would lie),
the four tool toggles, prompt override switches (file-gated) with
source/override text editors over the segment routes.
- Appearance: applied LIVE as runtime CSS variables by an
AppearanceProvider at the root layout — proseFontSize → the one
reading-size var, fontFamily → --font-prose, background → --background
+ the derived raised/accent/edge palette (dark curve, today's exact
spacing: +8/+22/+47 per channel, clamped), sessionNameSerif →
--font-title (titles flip with ZERO chat.tsx edits), headingWeight →
new --lh-heading-weight token, bold/italic colors → the lh-ink vars.
Mode renders honestly DISABLED.
- Graph: the ladder grays a rung when any ANCESTOR is effectively off
(stored values persist, resolve inert — switch shows stored, graying
shows effective).
- Keyboard: view/rebind/reset over the registry; overrides persist in
lh:keymap and apply INSTANTLY (dispatcher reads per keydown); bare
modifier presses are ignored while armed; displayOnly actions list but
never rebind.
First-boot fix: {settings: null} is no-record, not fetch-failure.
CROSS-LANE (flagged for master): src/core/keyboard.ts gained the
open-settings action/binding and exports saveOverrides/loadOverrides
(one-liners; no existing pins); app/layout.tsx mounts the provider.
Live-verified: structure, record writes (general/tools/graph/appearance),
prose-size applying live (16→20px), rebind + chord navigation from
another page, ladder graying, no console errors. Gates: typecheck,
lint, 689/689 (28 new), build.
This commit is contained in:
+17
-2
@@ -39,7 +39,8 @@ export type KeyAction =
|
||||
| "question-next-row"
|
||||
| "question-prev-row"
|
||||
| "question-pick-row"
|
||||
| "question-submit";
|
||||
| "question-submit"
|
||||
| "open-settings";
|
||||
|
||||
/** Where the cheat sheet groups an action (display only — dispatch is
|
||||
* global; a seam that isn't mounted simply no-ops). */
|
||||
@@ -59,6 +60,7 @@ export interface KeyActionDef {
|
||||
|
||||
export const KEYBOARD_ACTIONS: readonly KeyActionDef[] = [
|
||||
{ id: "toggle-dictation", label: "Toggle dictation", scope: "global", binding: "mod+d" },
|
||||
{ id: "open-settings", label: "Open settings", scope: "global", binding: "mod+," },
|
||||
{ id: "open-model-picker", label: "Model selector", scope: "global", binding: "mod+k" },
|
||||
{ id: "cycle-thinking", label: "Cycle thinking level", scope: "global", binding: "shift+tab" },
|
||||
{ id: "toggle-sidebar", label: "Toggle sessions sidebar", scope: "global", binding: "mod+b" },
|
||||
@@ -91,7 +93,7 @@ const STORAGE_KEY = "lh:keymap";
|
||||
/** localStorage overrides ({actionId: binding}) — read once per module
|
||||
* instance; the settings editor will write this key. Guarded: SSR and a
|
||||
* corrupt/absent store both fall back to defaults. */
|
||||
function loadOverrides(): Partial<Record<KeyAction, string>> {
|
||||
export function loadOverrides(): Partial<Record<KeyAction, string>> {
|
||||
if (typeof window === "undefined") return {};
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
@@ -104,6 +106,19 @@ function loadOverrides(): Partial<Record<KeyAction, string>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Write the full overrides map ({actionId: binding}) — the settings
|
||||
* editor's rebind/reset persistence. Guarded: SSR no-ops. An empty
|
||||
* map clears the store entirely (all-defaults). */
|
||||
export function saveOverrides(overrides: Partial<Record<KeyAction, string>>): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
if (Object.keys(overrides).length) window.localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides));
|
||||
else window.localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* storage unavailable = overrides don't persist, defaults keep working */
|
||||
}
|
||||
}
|
||||
|
||||
/** The live key map: defaults with user overrides applied. */
|
||||
export function effectiveKeymap(): Map<string, KeyAction> {
|
||||
const overrides = loadOverrides();
|
||||
|
||||
Reference in New Issue
Block a user