Feature 2: settings pass - flag exposure, gated picker entry, toggles

The auto_approve flag (and its shadow sibling) become first-class settings
instead of hand-edited TOML, and the Auto-Approve mode entry appears in the
picker only when the flag is on.

Server:
- manager: auto_approve()/auto_approve_shadow() read prefs.json first,
  falling back to the config.toml value a power user may have set; both
  writers persist to prefs. Both stores are user-global, so a cloned repo
  still can't enable either (the 1.5 invariant, unchanged).
- get_settings() exposes both; POST /v1/settings/auto-approve and
  /auto-approve-shadow write them (same shape as context-bar).
- Session builds pass the prefs-backed values into build_engine via new
  optional auto_approve/auto_approve_shadow overrides (None = config value),
  so a Settings flip takes effect on the next session build with no restart.
  Scheduled runs keep reading config only - they are unattended, so the
  live reviewer can never fire there regardless.

GUI:
- Mode picker: the Auto-Approve entry is `gated` - shown when
  getSettings().auto_approve is true, fetched on menu open. A session
  already IN auto-approve always shows its own entry so the current mode
  stays legible even if the flag was later turned off. This replaces the
  TEST-ONLY unconditional entry.
- Settings: AutoApproveCard with the feature toggle and the nested shadow-
  evaluation toggle ("records what it would have decided next to your own
  choice - without changing anything").
- api.ts: ModelSettings.auto_approve/auto_approve_shadow + setters.

Verified live against the running sidecar: flag off hides the entry on an
interactive session, flag on shows it, the Settings toggles round-trip and
persist. tests/test_auto_approve_settings.py (6): defaults, REST round-
trip, restart persistence, config fallback, prefs-beats-config, and the
build_engine override. tsc clean; 111 GUI unit tests pass.
This commit is contained in:
Devika Verma
2026-08-12 17:25:12 -07:00
parent 42a1fa1fb7
commit 71c786ab45
7 changed files with 290 additions and 8 deletions
+32
View File
@@ -697,6 +697,11 @@ export interface ModelSettings {
// Composer: show the context-window fill bar (default FALSE; absent → the chip shows
// the session total). The usage popover keeps both numbers regardless.
context_bar?: boolean;
// Auto-Approve mode (spec §1.5): the feature flag that offers the reviewer mode, and its
// shadow-eval sibling. Both default FALSE and are absent on older backends — the composer
// hides the Auto-Approve mode entry unless auto_approve is explicitly true.
auto_approve?: boolean;
auto_approve_shadow?: boolean;
// Curated-matrix display names ({full id → "GLM-5.2 · via Together"}); custom models absent.
model_labels?: Record<string, string>;
// {full id → context window in tokens}, verified matrix entries only — drives the
@@ -775,6 +780,33 @@ export async function setContextBar(
return res.json();
}
type AutoApproveResult = {
ok: boolean;
auto_approve?: boolean;
auto_approve_shadow?: boolean;
error?: string;
};
/** Toggle the Auto-Approve feature flag (spec §1.5); applies to the next session build. */
export async function setAutoApprove(on: boolean): Promise<AutoApproveResult> {
const res = await fetch(`${httpBase()}/v1/settings/auto-approve`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ auto_approve: on }),
});
return res.json();
}
/** Toggle shadow evaluation (Part 6 step 3): the reviewer records but never decides. */
export async function setAutoApproveShadow(on: boolean): Promise<AutoApproveResult> {
const res = await fetch(`${httpBase()}/v1/settings/auto-approve-shadow`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ auto_approve_shadow: on }),
});
return res.json();
}
/** Persist how many sessions a sidebar group shows before "Show more". */
export async function setSessionsPeek(
n: number,
+28 -4
View File
@@ -24,13 +24,24 @@ import {
// kept so saved sessions and configs keep working. Auto-Approve ("auto-approve") is the
// reviewer mode (spec: reviewed-auto-mode.md); it appears only when the server says the
// feature flag is on, wired in the settings pass — until then the picker omits it.
// `caution` prefixes the label with a warning triangle — a picker-local extension of
// Dropdown's Option.
type ModeOption = Option & { caution?: boolean };
// `caution` prefixes the label with a warning triangle; `gated` hides the entry unless the
// server's auto_approve flag is on. Picker-local extensions of Dropdown's Option.
type ModeOption = Option & { caution?: boolean; gated?: boolean };
// "auto" is the legacy wire value for Bypass approvals (server: Mode.BYPASS_APPROVALS).
// Auto-Approve is `gated`: shown only when getSettings().auto_approve is true (the feature
// flag, off by default). Copy (owner, 2026-08-12): two lines like every other entry —
// "your session model" carries the who-judges fact inline; per-check cost surfaces in the
// metering badge, not as picker text.
const PERMISSION_OPTIONS: ModeOption[] = [
{ value: "discuss", label: "Discuss", description: "Chat and explore — no edits or commands" },
{ value: "interactive", label: "Ask for approval", description: "Ask before edits and commands" },
{
value: "auto-approve",
label: "Auto-Approve",
description: "Your session model clears routine actions; doubtful ones still ask",
gated: true,
},
{
value: "auto",
label: "Bypass approvals",
@@ -847,6 +858,19 @@ function ModeMenu({
onUnattendedChange?: (on: boolean) => void;
}) {
const [open, setOpen] = useState(false);
// The Auto-Approve entry is gated on the server flag. Fetch once on first open; a session
// already IN auto-approve mode always shows its own entry so the current mode is legible
// even if the flag was later turned off.
const [autoApproveEnabled, setAutoApproveEnabled] = useState(false);
useEffect(() => {
if (!open) return;
getSettings()
.then((s) => setAutoApproveEnabled(s.auto_approve === true))
.catch(() => {});
}, [open]);
const options = PERMISSION_OPTIONS.filter(
(o) => !o.gated || autoApproveEnabled || o.value === mode,
);
const current = PERMISSION_OPTIONS.find((o) => o.value === mode);
return (
<div className="relative">
@@ -875,7 +899,7 @@ function ModeMenu({
role="menu"
data-testid="mode-menu"
>
{PERMISSION_OPTIONS.map((o) => (
{options.map((o) => (
<button
key={o.value}
className="w-full flex flex-col items-start px-2.5 py-1.5 rounded-lg text-left hover:bg-paper"
@@ -2,6 +2,8 @@ import { useEffect, useState } from "react";
import {
getSettings,
getTrustedWorkspaces,
setAutoApprove,
setAutoApproveShadow,
setCompactionSettings,
setContextBar,
setOnboarded,
@@ -447,6 +449,8 @@ function AppearanceSection() {
<ContextBarCard />
<AutoApproveCard />
<FilesCard />
<TrustedWorkspacesCard />
@@ -851,6 +855,77 @@ function ContextBarCard() {
);
}
// Auto-Approve (spec §1.5): the experimental feature flag that adds the "Auto-Approve" mode
// to the composer's mode picker, plus its shadow-evaluation sibling. Both default off and are
// user-global (a cloned repo can't turn either on). Shadow is nested under the main flag — it
// only makes sense to measure the reviewer once you know what it is.
function AutoApproveCard() {
const [on, setOn] = useState<boolean | null>(null);
const [shadow, setShadow] = useState(false);
useEffect(() => {
getSettings()
.then((s) => {
setOn(s.auto_approve === true);
setShadow(s.auto_approve_shadow === true);
})
.catch(() => setOn(false));
}, []);
const saveOn = async (next: boolean) => {
setOn(next);
await setAutoApprove(next);
};
const saveShadow = async (next: boolean) => {
setShadow(next);
await setAutoApproveShadow(next);
};
if (on === null) return null;
return (
<div className={CARD + " p-4 mb-4"} data-testid="auto-approve-card">
<div className={FIELD_LABEL}>Auto-Approve (experimental)</div>
<label className="flex items-start gap-3 py-2">
<input
type="checkbox"
className="mt-0.5"
data-testid="auto-approve-toggle"
checked={on}
onChange={(e) => saveOn(e.target.checked)}
/>
<span>
<span className="block text-[13px] text-ink">Enable Auto-Approve mode</span>
<span className="block text-[12px] text-muted">
Adds an <em>Auto-Approve</em> option to the mode picker. In that mode, your session
model reviews each action that would normally need approval and clears the routine
ones; anything doubtful still asks you. It can never allow something the rules
block. One extra model call per check, billed to your usage.
</span>
</span>
</label>
<label className="flex items-start gap-3 py-2 pl-7">
<input
type="checkbox"
className="mt-0.5"
data-testid="auto-approve-shadow-toggle"
checked={shadow}
onChange={(e) => saveShadow(e.target.checked)}
/>
<span>
<span className="block text-[13px] text-ink">
Shadow evaluation <span className="text-faint">(for measuring)</span>
</span>
<span className="block text-[12px] text-muted">
On any mode, the reviewer records what it <em>would</em> have decided next to your
own choice without changing anything. Lets you see how it would behave before
trusting it. Also costs one model call per approval.
</span>
</span>
</label>
</div>
);
}
function SidebarCard() {
const [peek, setPeek] = useState<number | null>(null);