"use client"; /** * THE SETTINGS PAGE (thin settings wave, 2026-09-19): one column, section * blocks in fixed order — General, Agent, Appearance, Graph, Keyboard — * over the versioned delta record at GET /api/settings, written per * section through PUT /api/settings/[section] (replace-whole). * * Thin by owner decision: simple controls, real read/write, the point is * a WORKING validation surface that exercises the whole settings schema — * not the designed settings app. Polish lives in a later wave. * * The chat section (new-session defaults) is deliberately NOT here * (owner decision): its controls live on the /chat/new surface; two * editors for one value is a drift bug. The auth/provider-keys page is * deliberately absent (needs auth.json plumbing that doesn't exist). */ import { useCallback, useEffect, useState } from "react"; import { CURRENT_SETTINGS_VERSION, type AgentSettings, type AppearanceSettings, type GraphSettings, type UserSettings } from "@/src/core"; import { SidebarToggle } from "@/components/sidebar.tsx"; import { useAppearance } from "./appearance.tsx"; import { SettingsSection, SettingsRow, Toggle } from "./controls.tsx"; import { AgentSection } from "./agent-section.tsx"; import { AppearanceSection, GraphSection } from "./appearance-section.tsx"; import { KeyboardSection } from "./keyboard-section.tsx"; export function SettingsPage() { const { onAppearanceChange } = useAppearance(); const [record, setRecord] = useState(undefined); const [error, setError] = useState(null); useEffect(() => { // 200 {settings: null} is FIRST BOOT (no record = factory everything), // not a failure — the error state is a non-200 or a network throw only. fetch("/api/settings") .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`status ${r.status}`)))) .then((body: { settings: UserSettings | null }) => setRecord(body.settings ?? { version: CURRENT_SETTINGS_VERSION }), ) .catch(() => setRecord(null)); }, []); /** ONE writer per page: PUT the whole section, keep the record mirror * in sync, surface route errors inline (a 400 is this wave's point — * the validation surface catching schema mistakes). */ const write = useCallback( async (section: "general" | "agent" | "appearance" | "graph", body: object) => { setError(null); const res = await fetch(`/api/settings/${section}`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { const detail = (await res.json().catch(() => ({}))) as { error?: string }; setError(`${section}: ${detail.error ?? res.status}`); return; } setRecord((prev) => ({ ...(prev ?? {}), [section]: body }) as UserSettings); if (section === "appearance") onAppearanceChange(body as AppearanceSettings); }, [onAppearanceChange], ); return (
{/* The app header — same 60px band, same border, same CMU title (the alignment constant from header round 6). */}
Settings
{error && (

{error}

)} {record === undefined ? (

Loading settings…

) : record === null ? (

Settings could not be loaded — refresh to retry.

) : ( <> void write("general", { autoUpdate: on })} /> void write("agent", next)} /> void write("appearance", next)} /> void write("graph", next)} /> )}
); }