mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 06:30:25 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user