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,8 +1,8 @@
|
||||
/**
|
||||
* Tests for the draft-settings wave (2026-09-18): the shared settings-body
|
||||
* parser (the one seam behind BOTH settings routes — per-chat and draft —
|
||||
* so their parse behavior is identical) and the draft-settings store
|
||||
* (data/draft-settings.json read/write, first-boot null semantics).
|
||||
* Tests for the default-settings wave (2026-09-18): the shared settings-body
|
||||
* parser (the one seam behind BOTH settings routes — per-chat and defaults —
|
||||
* so their parse behavior is identical) and the default-settings store
|
||||
* (data/default-settings.json read/write, first-boot null semantics).
|
||||
*
|
||||
* The store tests write the real repo data file (npm test's shared data/,
|
||||
* disposable scratch by doctrine) and clean up after themselves.
|
||||
@@ -13,8 +13,8 @@ import fs from "node:fs";
|
||||
import { afterEach, describe, it } from "node:test";
|
||||
import { GRAPH_CONTEXT_TOKENS_MAX, type ChatSettings } from "../src/core";
|
||||
import { parseSettingsBody } from "../src/server/settings-body.ts";
|
||||
import { DRAFT_SETTINGS_FILE } from "../config.ts";
|
||||
import { readDraftSettings, writeDraftSettings } from "../src/store/draft-settings.ts";
|
||||
import { DEFAULT_SETTINGS_FILE } from "../config.ts";
|
||||
import { readDefaultSettings, writeDefaultSettings } from "../src/store/default-settings.ts";
|
||||
|
||||
describe("parseSettingsBody (shared by both settings routes)", () => {
|
||||
it("an empty object parses to {} — the clear-overrides write", () => {
|
||||
@@ -67,10 +67,10 @@ describe("parseSettingsBody (shared by both settings routes)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the draft-settings store", () => {
|
||||
describe("the default-settings store", () => {
|
||||
afterEach(() => {
|
||||
try {
|
||||
fs.rmSync(DRAFT_SETTINGS_FILE);
|
||||
fs.rmSync(DEFAULT_SETTINGS_FILE);
|
||||
} catch {
|
||||
/* nothing to clean */
|
||||
}
|
||||
@@ -78,11 +78,11 @@ describe("the draft-settings store", () => {
|
||||
|
||||
it("reads null on first boot (no file) — the surfaces use config defaults", () => {
|
||||
try {
|
||||
fs.rmSync(DRAFT_SETTINGS_FILE);
|
||||
fs.rmSync(DEFAULT_SETTINGS_FILE);
|
||||
} catch {
|
||||
/* absent already */
|
||||
}
|
||||
assert.equal(readDraftSettings(), null);
|
||||
assert.equal(readDefaultSettings(), null);
|
||||
});
|
||||
|
||||
it("write then read roundtrips verbatim — validated shape, no sanitizer games", () => {
|
||||
@@ -91,18 +91,18 @@ describe("the draft-settings store", () => {
|
||||
thinkingLevel: "high",
|
||||
graphContextTokens: 1024,
|
||||
};
|
||||
writeDraftSettings(settings);
|
||||
assert.deepEqual(readDraftSettings(), settings);
|
||||
writeDefaultSettings(settings);
|
||||
assert.deepEqual(readDefaultSettings(), settings);
|
||||
});
|
||||
|
||||
it("an empty {} record roundtrips (all overrides cleared)", () => {
|
||||
writeDraftSettings({});
|
||||
assert.deepEqual(readDraftSettings(), {});
|
||||
writeDefaultSettings({});
|
||||
assert.deepEqual(readDefaultSettings(), {});
|
||||
});
|
||||
|
||||
it("a corrupt file reads as null — defaults, never a crash", () => {
|
||||
fs.rmSync(DRAFT_SETTINGS_FILE, { force: true }); // a directory from a broken earlier run also blocks
|
||||
fs.writeFileSync(DRAFT_SETTINGS_FILE, "{ not json", "utf-8");
|
||||
assert.equal(readDraftSettings(), null);
|
||||
fs.rmSync(DEFAULT_SETTINGS_FILE, { force: true }); // a directory from a broken earlier run also blocks
|
||||
fs.writeFileSync(DEFAULT_SETTINGS_FILE, "{ not json", "utf-8");
|
||||
assert.equal(readDefaultSettings(), null);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user