From 3dfc331d7b4ad430e8850ad4f12de8e43fa65d5d Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Fri, 25 Sep 2026 19:18:57 +0300 Subject: [PATCH] Prompt editor affordances: version-in-use hint, dirty-gated Save, auto-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner-confirmed semantics (verified in composeInstructions first: switch ON = the user's override text is what the agent reads; OFF = the default composes, the saved file stays on disk unused). Four refinements: 1. The small text under a file name states WHICH VERSION IS IN USE: 'Your version is in use' (PROMPT_IN_USE_HINT, pinned) when the switch is on; NOTHING when the default composes — even with a saved override sitting on disk. The old presence-based 'Your version is saved' (shown whenever a file existed, regardless of the switch) dies; the switch's meaning becomes legible through the line it visibly controls. 2. Save is clickable only when there's something to save: grayed and inert while the draft equals what's on disk (including the untouched editor just opened), full-contrast the moment the text differs. The always-active same-color button was the 'doesn't feel like it does anything' half of the report. 3. Saving auto-turns the switch ON — writing an override and putting it to work collapse into one action (a save with the switch off was the did-something-changed-nothing trap). Fires whenever the record genuinely has the path off, so a re-save after toggling off re-on's too. Order is the safe one (file first, then the switch the validation requires it). 4. A quiet inline 'Saved' / 'Deleted' confirmation rides next to the buttons (the old status line echoed the file path at the section's bottom — production-copy violation and easy to miss). Plus the mirror-symmetry the auto-on needs: deleting an override while its switch is on now composes the switch OFF in the same action — not just redundant with Platform's 643a693 server-side clear: the dialog's record mirror is what composes the next section write, so a stale ON would silently defeat the auto-on after a save-following-delete (the mirror would think the switch is already on and skip the write the server record needs). Live-verified on :3001: untouched editor grays Save; typing activates it; reverting re-grays it; save-with-switch-off lands toggle ON + hint + 'Saved' + Save re-grayed in one click; toggle-off drops the hint with the file retained on disk; re-save while off re-on's; delete while on clears switch + file + hint with record left clean and a subsequent agent-section write landing 200. --- components/settings/agent-section.tsx | 75 +++++++++++++++++++++------ tests/settings-pure.test.ts | 6 +-- 2 files changed, 63 insertions(+), 18 deletions(-) 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"); }); });