mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 13:00:37 +00:00
Explain Auto-Approve once per session
This commit is contained in:
@@ -117,6 +117,18 @@ function readLastSessions(): Record<string, LastSession> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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]);
|
||||
|
||||
@@ -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(<Transcript items={BANNER} running={false} />);
|
||||
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(<Transcript items={[{ kind: "notice", tone: "info", text: "Context compacted" }]} running={false} />);
|
||||
expect(screen.queryByTestId("mode-notice")).toBeNull();
|
||||
expect(screen.getByText("Context compacted").className).not.toContain("notice-block");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -524,6 +524,21 @@ export function Transcript({ items, running, streamingText, onRetry, onUndoMemor
|
||||
</div>
|
||||
);
|
||||
case "notice":
|
||||
// A titled notice is prose (the Auto-Approve banner), not a status line.
|
||||
if (item.title) {
|
||||
return (
|
||||
<div
|
||||
className={"notice notice-block " + (item.tone === "warn" ? "warn" : "")}
|
||||
key={bi}
|
||||
data-testid="mode-notice"
|
||||
>
|
||||
<div className="notice-title">{item.title}</div>
|
||||
{item.text.split("\n\n").map((para, i) => (
|
||||
<p key={i}>{para}</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={"notice " + (item.tone === "warn" ? "warn" : "")} key={bi}>
|
||||
{item.text}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user