fix(desktop,dashboard): single-key config writers send a sparse patch, not the cached snapshot

Applying a reasoning/speed default in Desktop Settings -> Model reset an
auxiliary slot a user had pinned via CLI back to provider "auto" / model ""
while leaving reasoning_effort intact (#95460). POST /api/model/set was
never the writer; writeAgentDefault was: it round-tripped the whole
default-expanded config record (loaded when Settings opened) through
PUT /api/config, so every key another surface changed since the snapshot was
echoed back with its stale, default-filled value. The pinned slot's
provider/model existed only as defaults in the snapshot; reasoning_effort was
already in it, hence the asymmetry the report observed.

PUT /api/config deep-merges onto disk, so a writer only needs to send the key
it changed. Every desktop single-key writer now does exactly that (the
config-settings page already diffed against a baseline): Model defaults
(agent.reasoning_effort / service_tier), Appearance resume_last_session,
terminal font, session auto-archive, the two browser.use_real_profile
toggles, and the Capabilities voice fields (diffConfig against a baseline).
The optimistic shared-cache write keeps the full merged record so sibling
surfaces repaint without a refetch. The dashboard's ReasoningPicker had the
same read-modify-write shape and now sends the sparse patch too.

Tests pin the wire contract: only the edited key is sent, a sibling pin that
is not in the snapshot cannot be echoed back.
This commit is contained in:
teknium1
2026-09-13 20:47:09 -07:00
committed by Teknium
parent 7d368c7d2c
commit 55babca783
12 changed files with 69 additions and 58 deletions
+5 -13
View File
@@ -77,20 +77,12 @@ export function ReasoningPicker({
const prev = effort;
setEffort(next); // optimistic
setSaving(true);
// Read-modify-write the whole config — the dashboard's single-key save
// pattern — so we never clobber sibling keys. `saveConfig` PUTs the full
// object the agent boots from.
// Sparse patch: PUT /api/config deep-merges onto disk, so sending only
// the edited key never clobbers sibling keys — and never echoes a
// default-expanded snapshot back over values another surface changed
// meanwhile (a CLI-pinned auxiliary slot would come back as "auto").
void api
.getConfig(profile)
.then((cfg) => {
const base = (cfg ?? {}) as Record<string, unknown>;
const agent =
base.agent && typeof base.agent === "object"
? { ...(base.agent as Record<string, unknown>) }
: {};
agent.reasoning_effort = next;
return api.saveConfig({ ...base, agent }, profile);
})
.saveConfig({ agent: { reasoning_effort: next } }, profile)
.then(() => {
onChanged?.(next);
})