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:
@@ -11,7 +11,7 @@ import type { ChatSettings } from "@/src/core";
|
||||
* by construction (agents are stateless per turn).
|
||||
*
|
||||
* The body parsing itself is shared with the draft route
|
||||
* (src/server/settings-body.ts, draft-settings wave 2026-09-18).
|
||||
* (src/server/settings-body.ts, default-settings wave 2026-09-18).
|
||||
*/
|
||||
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
/**
|
||||
* GET/PUT /api/settings/draft — server persistence for the DRAFT (pre-
|
||||
* session) settings: the model + thinking-level + knowledge-context
|
||||
* selections made on the new-session surfaces (the home graph view's topic
|
||||
* dock and /chat/new). Both surfaces share ONE record (owner, 2026-09-18):
|
||||
* whatever the learner last left the controls on is what the next visit
|
||||
* shows. The file lives at data/draft-settings.json
|
||||
* (config.DRAFT_SETTINGS_FILE).
|
||||
* GET/PUT /api/settings/defaults — server persistence for the DEFAULT
|
||||
* settings: THE default model + thinking level + knowledge-context
|
||||
* budget. The new-session surfaces' controls (the home graph view's
|
||||
* topic dock and /chat/new) ARE the settings page for the default model
|
||||
* (owner, 2026-09-18): 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. Both surfaces share ONE record; the file
|
||||
* lives at data/default-settings.json (config.DEFAULT_SETTINGS_FILE).
|
||||
*
|
||||
* GET → 200 {settings} once saved, 200 null on first boot (the surfaces
|
||||
* keep config-pinned defaults until the learner chooses something).
|
||||
* GET → 200 {settings} once saved, 200 null on first boot ("no model").
|
||||
* PUT → 200 {settings} — the VALIDATED copy persisted verbatim. The exact
|
||||
* per-chat contract (parseSettingsBody + the catalog gate): unknown
|
||||
* provider/model or an unsupported thinking level is a 400 carrying
|
||||
* the supported list, never a silent clamp.
|
||||
*
|
||||
* Per-session settings (PUT /api/chats/:id/settings) are a separate record
|
||||
* untouched by this — the draft choice still rides along on session
|
||||
* creation exactly as before.
|
||||
* untouched by this — the default still rides along on session creation
|
||||
* exactly as before.
|
||||
*/
|
||||
import { readDraftSettings, writeDraftSettings, parseSettingsBody, validateChatSettings } from "@/src/server";
|
||||
import { readDefaultSettings, writeDefaultSettings, parseSettingsBody, validateChatSettings } from "@/src/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(readDraftSettings());
|
||||
return Response.json(readDefaultSettings());
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
@@ -34,6 +34,6 @@ export async function PUT(request: Request) {
|
||||
if (!verdict.ok) {
|
||||
return Response.json({ error: verdict.error, supported: verdict.supported }, { status: 400 });
|
||||
}
|
||||
writeDraftSettings(parsed.settings);
|
||||
writeDefaultSettings(parsed.settings);
|
||||
return Response.json({ settings: parsed.settings });
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NewSession } from "@/components/new-session";
|
||||
import { readDefaultSettings } from "@/src/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -8,7 +9,12 @@ export const dynamic = "force-dynamic";
|
||||
* chat; on send it creates the chat (title = that message) and redirects to
|
||||
* the real page, whose initialPrompt path delivers the message as turn 1.
|
||||
* Static segment beats the dynamic [id] route — "new" is never a chat id.
|
||||
*
|
||||
* The default settings record is server-rendered in (readDefaultSettings →
|
||||
* initialSettings): the controls show truth on the first frame, no
|
||||
* default-model flash (the default IS the user's last choice — "no model"
|
||||
* only before the first-ever pick).
|
||||
*/
|
||||
export default function NewSessionPage() {
|
||||
return <NewSession />;
|
||||
}
|
||||
return <NewSession initialSettings={readDefaultSettings()} />;
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,5 +1,12 @@
|
||||
import { Home } from "@/components/home";
|
||||
import { readDefaultSettings } from "@/src/server";
|
||||
|
||||
// Dynamic: the default settings record is server-rendered into the page —
|
||||
// the model + thinking + context controls show truth on the FIRST frame
|
||||
// (no default-model flash), and every navigation re-reads the record
|
||||
// (dynamic routes aren't cached client-side).
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default function HomePage() {
|
||||
return <Home />;
|
||||
return <Home initialSettings={readDefaultSettings()} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user