From 76e9174ef907525debab56ea27c26d662756ad7e Mon Sep 17 00:00:00 2001 From: Devika Verma Date: Sat, 22 Aug 2026 07:24:47 +0530 Subject: [PATCH] Explain Auto-Approve once per session --- surfaces/gui/src/App.tsx | 25 +++++++++++++++++ .../gui/src/components/Transcript.test.tsx | 28 +++++++++++++++++++ surfaces/gui/src/components/Transcript.tsx | 15 ++++++++++ surfaces/gui/src/styles.css | 9 ++++++ surfaces/gui/src/types.ts | 5 +++- 5 files changed, 81 insertions(+), 1 deletion(-) diff --git a/surfaces/gui/src/App.tsx b/surfaces/gui/src/App.tsx index fc38533e..0c56417b 100644 --- a/surfaces/gui/src/App.tsx +++ b/surfaces/gui/src/App.tsx @@ -117,6 +117,18 @@ function readLastSessions(): Record { } } +// Shown once the first time Auto-Approve is active in a session (spec §1.5). Says what the +// mode buys, what it can never override, and — deliberately unhedged — where it can be +// wrong. It reduces interruptions, not risk, and the last line has to leave that clear. +const AUTO_APPROVE_NOTICE = [ + "Your session model lets routine actions through without asking; anything it isn't sure " + + "about still comes to you — whatever the rules settle outright, it never sees.", + "It reduces interruptions from lower-risk actions, not the risk from what it can't tell: " + + "what a script will do, or whether an instruction came from you or from a web page or " + + "email. Shell commands aren't sandboxed — they reach anything you can. These are model " + + "judgments, not guarantees — a false allow executes unchecked.", +].join("\n\n"); + function rememberLastSession(agent: string, sessionId: string, workspace: string | null) { if (!agent || !sessionId) return; try { @@ -187,6 +199,10 @@ export function App() { const [streaming, setStreamingState] = useState(""); // Ref mirror of `streaming`: the WS handler closure is built once per socket and can't read // fresh state — the interrupted/error flush below needs the live buffer at event time. + // Auto-Approve banner: once per session, however the mode was entered (picked in the + // composer, applied by a plan approval, or restored with the session). Keyed on the + // session id, so toggling out and back stays quiet but a new session explains itself. + const autoApproveNoticeFor = useRef(""); const streamingRef = useRef(""); const setStreaming = (value: string | ((s: string) => string)) => { streamingRef.current = typeof value === "function" ? value(streamingRef.current) : value; @@ -870,6 +886,15 @@ export function App() { atBottomRef.current = true; setFollowing(true); }, [sessionId]); + + useEffect(() => { + if (mode !== "auto-approve" || autoApproveNoticeFor.current === sessionId) return; + autoApproveNoticeFor.current = sessionId; + setItems((p) => [ + ...p, + { kind: "notice", tone: "info", title: "Auto-Approve is on.", text: AUTO_APPROVE_NOTICE }, + ]); + }, [mode, sessionId]); useEffect(() => { if (atBottomRef.current) scrollToBottom(); }, [items, streaming]); diff --git a/surfaces/gui/src/components/Transcript.test.tsx b/surfaces/gui/src/components/Transcript.test.tsx index 66342e6d..758e4233 100644 --- a/surfaces/gui/src/components/Transcript.test.tsx +++ b/surfaces/gui/src/components/Transcript.test.tsx @@ -318,3 +318,31 @@ describe("reviewer deny card (§8.4)", () => { expect(screen.queryByTestId("reviewer-allow-anyway")).toBeNull(); }); }); + +// The Auto-Approve banner (spec §1.5): a titled notice is prose, not a status line, so it +// renders as a heading plus paragraphs rather than one centred grey row. +describe("mode notice", () => { + const BANNER: Item[] = [ + { + kind: "notice", + tone: "info", + title: "Auto-Approve is on.", + text: "First paragraph about what it does.\n\nSecond paragraph about what it can't tell.", + }, + ]; + + it("renders the title and one paragraph per blank-line break", () => { + render(); + const block = screen.getByTestId("mode-notice"); + expect(block.textContent).toContain("Auto-Approve is on."); + expect(block.querySelectorAll("p")).toHaveLength(2); + // Prose layout, not the centred one-liner used for "Context compacted". + expect(block.className).toContain("notice-block"); + }); + + it("leaves untitled status notices as plain one-liners", () => { + render(); + expect(screen.queryByTestId("mode-notice")).toBeNull(); + expect(screen.getByText("Context compacted").className).not.toContain("notice-block"); + }); +}); diff --git a/surfaces/gui/src/components/Transcript.tsx b/surfaces/gui/src/components/Transcript.tsx index 9b74518e..5e0ad322 100644 --- a/surfaces/gui/src/components/Transcript.tsx +++ b/surfaces/gui/src/components/Transcript.tsx @@ -524,6 +524,21 @@ export function Transcript({ items, running, streamingText, onRetry, onUndoMemor ); case "notice": + // A titled notice is prose (the Auto-Approve banner), not a status line. + if (item.title) { + return ( +
+
{item.title}
+ {item.text.split("\n\n").map((para, i) => ( +

{para}

+ ))} +
+ ); + } return (
{item.text} diff --git a/surfaces/gui/src/styles.css b/surfaces/gui/src/styles.css index 19fa2b11..f9d2211b 100644 --- a/surfaces/gui/src/styles.css +++ b/surfaces/gui/src/styles.css @@ -367,6 +367,15 @@ body { .notice { font-size: 13px; color: var(--muted); text-align: center; } .notice.warn { color: var(--accent); } +/* Mode banner: prose, so it reads left-aligned in a card rather than as a centred + status line. Same `notice` item, switched on by a title. */ +.notice-block { + text-align: left; max-width: 62ch; margin: 12px auto; padding: 11px 13px; + border: 1px solid var(--line); border-radius: 9px; background: var(--paper); + display: flex; flex-direction: column; gap: 7px; line-height: 1.5; +} +.notice-block .notice-title { font-weight: 600; color: var(--ink); } +.notice-block p { margin: 0; } /* approval */ .approval { diff --git a/surfaces/gui/src/types.ts b/surfaces/gui/src/types.ts index 2c014ca6..6e5eb784 100644 --- a/surfaces/gui/src/types.ts +++ b/surfaces/gui/src/types.ts @@ -152,7 +152,10 @@ export type Item = questions?: GroupedQuestion[]; resolved?: string; } - | { kind: "notice"; tone: "info" | "warn"; text: string; retriable?: boolean } + // `title` switches the one-line status notice to a block: a heading plus + // blank-line-separated paragraphs, left-aligned. Used for the Auto-Approve banner, + // which is prose rather than a status line. + | { kind: "notice"; tone: "info" | "warn"; text: string; retriable?: boolean; title?: string } // MEMORY-SPEC §5.1: the save notice, inline in the conversation where the user is // already looking (a corner toast vanished before it could be read or undone — // owner-hit 2026-07-28). Stays put. `previous` is set when an existing memory was