feat: no baked-in default model — the new-session controls ARE the default settings page
Owner reframe (2026-09-18): the model + thinking + context controls on
the home graph view and /chat/new are not a draft overlay on a baked-in
default — they ARE the settings page for the default model, and the
user's last choice IS the default. BYOK first boot has no keys and
possibly no OpenRouter, so there is NO default to show: the controls
render 'no model' ('—') until the first pick.
The flash was the deeper symptom: the surfaces rendered from client
state that started empty and fetched truth afterward, so every visit
flashed the config pin (GLM/low — a dev artifact) for a few frames.
Fix at the root: both pages are now force-dynamic server components
that read the record (readDefaultSettings → initialSettings) and hand
it to the client — the first frame shows truth. Dynamic routes aren't
cached client-side (staleTimes.dynamic = 0), so every navigation
re-reads the record; the useDefaultSettings hook keeps only save-side
state (optimistic set + PUT). The GET-on-mount and its touch-guard are
gone.
ModelControl gains noDefaultModel (the default surfaces only): unset
settings render no model/level, never the catalog pin; the thinking
menu no-ops until a model exists. Chat surfaces keep the catalog
fallback — the engine runs the config pin when a session record has no
override, so the control keeps showing what will actually run.
Renamed draft→default throughout: data/default-settings.json,
GET/PUT /api/settings/defaults, useDefaultSettings. New sessions
still inherit the default on creation exactly as before.
Live-verified: first-boot 'no model' with zero flash, picker +
Shift+Tab cycle, values held across session→/chat/new→home roundtrips
and a reload (10×100ms samples, never the old default), zero console
errors.
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@ export * from "./commit-positions.ts";
|
||||
export * from "./jobs.ts";
|
||||
export * from "./stores.ts";
|
||||
export { readRenderSettings, writeRenderSettings } from "../store/render-settings.ts";
|
||||
export { readDraftSettings, writeDraftSettings } from "../store/draft-settings.ts";
|
||||
export { readDefaultSettings, writeDefaultSettings } from "../store/default-settings.ts";
|
||||
export * from "./settings-body.ts";
|
||||
export * from "./turn-liveness.ts";
|
||||
export * from "./observability.ts";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* The shared settings-PUT body parser — the one seam both settings routes
|
||||
* use (per-chat: app/api/chats/[id]/settings; pre-session draft:
|
||||
* app/api/settings/draft). Extracted verbatim from the per-chat route
|
||||
* (draft-settings wave, 2026-09-18): identical parse, identical errors.
|
||||
* (default-settings wave, 2026-09-18): identical parse, identical errors.
|
||||
*
|
||||
* Replace-whole semantics: the client sends the complete desired object;
|
||||
* absent fields are absent overrides ({} clears everything). No merges, so
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Server-side persistence for the DEFAULT settings — THE default model +
|
||||
* thinking level + knowledge-context budget (owner, 2026-09-18): the
|
||||
* new-session surfaces' controls (the home graph view's topic dock and
|
||||
* /chat/new) ARE the settings page for the default model. There is NO
|
||||
* baked-in default — BYOK first boot renders "no model" until the user
|
||||
* picks one, and the last choice IS the default from then on. The pages
|
||||
* server-render from this record (initialSettings), so the controls show
|
||||
* truth on the first frame — no default-model flash ever. Per-session
|
||||
* settings live in the chat record (PUT /api/chats/:id/settings) and are
|
||||
* untouched by this; new sessions inherit the default on creation.
|
||||
*
|
||||
* This module only moves bytes: the file is written exclusively through the
|
||||
* validated route (app/api/settings/defaults — parseSettingsBody +
|
||||
* validateChatSettings gate every write), so no sanitizer runs here. On
|
||||
* read, anything unreadable/corrupt is null = first boot = "no model".
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_SETTINGS_FILE } from "../../config.ts";
|
||||
import type { ChatSettings } from "../core";
|
||||
|
||||
/** The persisted default settings, or null when never saved / unreadable. */
|
||||
export function readDefaultSettings(): ChatSettings | null {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = fs.readFileSync(DEFAULT_SETTINGS_FILE, "utf-8");
|
||||
} catch {
|
||||
return null; // never saved / unreadable → the surfaces render "no model"
|
||||
}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||
return parsed as ChatSettings;
|
||||
} catch {
|
||||
return null; // corrupt → "no model", never crash the surfaces
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist the validated settings verbatim. */
|
||||
export function writeDefaultSettings(settings: ChatSettings): void {
|
||||
fs.mkdirSync(path.dirname(DEFAULT_SETTINGS_FILE), { recursive: true });
|
||||
fs.writeFileSync(DEFAULT_SETTINGS_FILE, JSON.stringify(settings, null, "\t"), "utf-8");
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* Server-side persistence for the DRAFT (pre-session) settings — the
|
||||
* model + thinking-level + knowledge-context choices made on the
|
||||
* new-session surfaces (the home graph view's topic dock and /chat/new).
|
||||
* One small JSON blob: data/draft-settings.json, read-modify-write.
|
||||
*
|
||||
* Both surfaces share ONE record (owner, draft-settings wave 2026-09-18):
|
||||
* whatever the learner last left the controls on is what the next visit
|
||||
* shows — navigating away and back must not reset the feel. Per-session
|
||||
* settings live in the chat record (PUT /api/chats/:id/settings) and are
|
||||
* untouched by this.
|
||||
*
|
||||
* This module only moves bytes: the file is written exclusively through the
|
||||
* validated route (app/api/settings/draft — parseSettingsBody +
|
||||
* validateChatSettings gate every write), so no sanitizer runs here. On
|
||||
* read, anything unreadable/corrupt is null = first boot = config defaults.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DRAFT_SETTINGS_FILE } from "../../config.ts";
|
||||
import type { ChatSettings } from "../core";
|
||||
|
||||
/** The persisted draft settings, or null when never saved / unreadable. */
|
||||
export function readDraftSettings(): ChatSettings | null {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = fs.readFileSync(DRAFT_SETTINGS_FILE, "utf-8");
|
||||
} catch {
|
||||
return null; // never saved / unreadable → the surfaces use config defaults
|
||||
}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||
return parsed as ChatSettings;
|
||||
} catch {
|
||||
return null; // corrupt → defaults, never crash the surfaces
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist the validated settings verbatim. */
|
||||
export function writeDraftSettings(settings: ChatSettings): void {
|
||||
fs.mkdirSync(path.dirname(DRAFT_SETTINGS_FILE), { recursive: true });
|
||||
fs.writeFileSync(DRAFT_SETTINGS_FILE, JSON.stringify(settings, null, "\t"), "utf-8");
|
||||
}
|
||||
Reference in New Issue
Block a user