diff --git a/components/settings/agent-section.tsx b/components/settings/agent-section.tsx index a253fb4..a4a440b 100644 --- a/components/settings/agent-section.tsx +++ b/components/settings/agent-section.tsx @@ -170,11 +170,13 @@ export function composeSearchEngine( return { ...agent, search: { engine } }; } -/** The indicator under a row that HAS an override file (owner ruling - * 2026-09-26: the empty state carries NO description line; the - * indicator exists only when the user's version is on disk). Exported - * for the pin. */ -export const PROMPT_SAVED_HINT = "Your version is saved"; +/** The indicator under a row whose override IS the version in use (owner + * ruling, refined 2026-09-26: the small text states WHICH VERSION IS + * IN USE — "Your version is in use" when the switch is on, and NOTHING + * when the default composes, even if a saved override sits on disk + * unused. The switch's meaning becomes legible: it visibly controls + * this line). Exported for the pin. */ +export const PROMPT_IN_USE_HINT = "Your version is in use"; /** One section of the prompt view — the owner's ruled order: Teacher, * Prober, then Shared last. */ @@ -411,7 +413,8 @@ export function AgentSection({ }; }, [openMenu]); - async function saveSegment(path: string, text: string) { setSegmentStatus(null); + async function saveSegment(path: string, text: string) { + setSegmentStatus(null); const res = await fetch(`/api/settings/prompts/${path}`, { method: "PUT", headers: { "content-type": "application/json" }, @@ -421,9 +424,18 @@ export function AgentSection({ setSegmentStatus(`Save failed: ${(await res.json().catch(() => ({}))).error ?? res.status}`); return; } - // Refetch: file presence gates the switch, so the view must track it. readPrompts(); - setSegmentStatus(`Saved ${path}`); + setSegmentStatus("Saved"); + // Auto-on (owner ruling, refined 2026-09-26): writing an override and + // putting it to work are ONE action — a save with the switch off was + // the "did something, changed nothing" trap (the composed prompt + // stayed the default). The file exists now, so the ON switch the + // agent-section validation requires is legal; the order (file, then + // switch) is the safe one. Saving an edit while already on rides + // through — the switch write is idempotent at the record level. + if (agent?.prompts?.[path] !== true) { + onSectionChange(composePromptSwitch(agent, path, true)); + } } async function deleteSegment(path: string) { @@ -433,7 +445,16 @@ export function AgentSection({ setSegmentStatus(`Delete failed: ${res.status}`); return; } + // Auto-off — the mirror of save's auto-on: deleting the file while + // the switch is on would orphan an ON record switch (the agent-section + // validation 400s every later write while the UI's toggle can no + // longer reach the OFF escape hatch — the trap found in live verify + // 2026-09-26). Composing the OFF here keeps the record clean. + if (agent?.prompts?.[path] === true) { + onSectionChange(composePromptSwitch(agent, path, false)); + } readPrompts(); + setSegmentStatus("Deleted"); } /** The search-engine menu row — label + the quiet live-key mark. */ @@ -463,15 +484,24 @@ export function AgentSection({ const segmentRow = (info: SegmentInfo) => { const fileExists = info.overrideText !== null; const open = openSegment === info.path; + // The Save affordance (owner ruling, refined 2026-09-26): clickable + // only when there's something to save — the draft differs from + // what's on disk. An unchanged editor shows a grayed, inert Save + // (the always-active button was the "doesn't feel like it does + // anything" half of the report). + const draft = segmentDraft[info.path] ?? info.overrideText ?? ""; + const dirty = draft !== (info.overrideText ?? ""); return (
{/* Graying ruling: ONLY the toggle dims without an @@ -520,8 +550,13 @@ export function AgentSection({
@@ -534,6 +569,17 @@ export function AgentSection({ Delete )} + {/* The quiet inline confirmation — next to the + action, not the old path-echoing line at the + section's bottom (production-copy: no file + paths in user-facing status). One editor is + open at a time (openSegment), so one shared + status reads unambiguously. */} + {segmentStatus && ( +

+ {segmentStatus} +

+ )}
)} @@ -770,7 +816,6 @@ export function AgentSection({ ))} - {segmentStatus &&

{segmentStatus}

} )} diff --git a/tests/settings-pure.test.ts b/tests/settings-pure.test.ts index b87db43..19952fe 100644 --- a/tests/settings-pure.test.ts +++ b/tests/settings-pure.test.ts @@ -9,7 +9,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { appearanceVars, derivedPalette, resolveAppearance, staleVarNames } from "../components/settings/appearance.tsx"; import { - PROMPT_SAVED_HINT, + PROMPT_IN_USE_HINT, composePromptSwitch, composeSearchEngine, composeSlotModel, @@ -384,7 +384,7 @@ describe("sectionedSegments — the prompts overhaul (2026-09-26)", () => { assert.equal(teacher.hint, undefined); }); - it("the indicator copy is pinned (override present = the row earns its line)", () => { - assert.equal(PROMPT_SAVED_HINT, "Your version is saved"); + it("the indicator copy is pinned — it states WHICH VERSION IS IN USE (refined 2026-09-26)", () => { + assert.equal(PROMPT_IN_USE_HINT, "Your version is in use"); }); });