mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-03 20:41:07 +02:00
feat(code-mode): approval-policy selector in Settings
Surface the approval policy (Ask every time / Auto-approve reads / YOLO) in Settings -> Code Mode, instead of being config-file only. The broker already reads CodeModeConfig.approvalPolicy; this plumbs it through the codeMode:getConfig / setConfig IPC + main handlers and adds the picker UI (with a one-line explanation of each level). Defaults to "ask".
This commit is contained in:
parent
f0079b4db8
commit
2fef77416f
3 changed files with 57 additions and 6 deletions
|
|
@ -643,11 +643,11 @@ export function setupIpcHandlers() {
|
||||||
'codeMode:getConfig': async () => {
|
'codeMode:getConfig': async () => {
|
||||||
const repo = container.resolve<ICodeModeConfigRepo>('codeModeConfigRepo');
|
const repo = container.resolve<ICodeModeConfigRepo>('codeModeConfigRepo');
|
||||||
const config = await repo.getConfig();
|
const config = await repo.getConfig();
|
||||||
return { enabled: config.enabled };
|
return { enabled: config.enabled, approvalPolicy: config.approvalPolicy };
|
||||||
},
|
},
|
||||||
'codeMode:setConfig': async (_event, args) => {
|
'codeMode:setConfig': async (_event, args) => {
|
||||||
const repo = container.resolve<ICodeModeConfigRepo>('codeModeConfigRepo');
|
const repo = container.resolve<ICodeModeConfigRepo>('codeModeConfigRepo');
|
||||||
await repo.setConfig({ enabled: args.enabled });
|
await repo.setConfig({ enabled: args.enabled, approvalPolicy: args.approvalPolicy });
|
||||||
invalidateCopilotInstructionsCache();
|
invalidateCopilotInstructionsCache();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { useTheme } from "@/contexts/theme-context"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { AccountSettings } from "@/components/settings/account-settings"
|
import { AccountSettings } from "@/components/settings/account-settings"
|
||||||
import { ConnectedAccountsSettings } from "@/components/settings/connected-accounts-settings"
|
import { ConnectedAccountsSettings } from "@/components/settings/connected-accounts-settings"
|
||||||
|
import type { ApprovalPolicy } from "@x/shared/src/code-mode.js"
|
||||||
|
|
||||||
type ConfigTab = "account" | "connections" | "models" | "mcp" | "security" | "code-mode" | "appearance" | "note-tagging" | "help"
|
type ConfigTab = "account" | "connections" | "models" | "mcp" | "security" | "code-mode" | "appearance" | "note-tagging" | "help"
|
||||||
|
|
||||||
|
|
@ -1712,6 +1713,7 @@ function AgentStatusRow({
|
||||||
|
|
||||||
function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
const [enabled, setEnabled] = useState(false)
|
const [enabled, setEnabled] = useState(false)
|
||||||
|
const [approvalPolicy, setApprovalPolicy] = useState<ApprovalPolicy>('ask')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [status, setStatus] = useState<CodeModeAgentStatus | null>(null)
|
const [status, setStatus] = useState<CodeModeAgentStatus | null>(null)
|
||||||
|
|
@ -1736,7 +1738,10 @@ function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const result = await window.ipc.invoke("codeMode:getConfig", null)
|
const result = await window.ipc.invoke("codeMode:getConfig", null)
|
||||||
if (!cancelled) setEnabled(result.enabled)
|
if (!cancelled) {
|
||||||
|
setEnabled(result.enabled)
|
||||||
|
setApprovalPolicy(result.approvalPolicy ?? 'ask')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setEnabled(false)
|
if (!cancelled) setEnabled(false)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -1752,7 +1757,7 @@ function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setEnabled(next)
|
setEnabled(next)
|
||||||
try {
|
try {
|
||||||
await window.ipc.invoke("codeMode:setConfig", { enabled: next })
|
await window.ipc.invoke("codeMode:setConfig", { enabled: next, approvalPolicy })
|
||||||
window.dispatchEvent(new Event("code-mode-config-changed"))
|
window.dispatchEvent(new Event("code-mode-config-changed"))
|
||||||
toast.success(next ? "Code mode enabled" : "Code mode disabled")
|
toast.success(next ? "Code mode enabled" : "Code mode disabled")
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -1761,7 +1766,22 @@ function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [approvalPolicy])
|
||||||
|
|
||||||
|
const handlePolicyChange = useCallback(async (next: ApprovalPolicy) => {
|
||||||
|
const prev = approvalPolicy
|
||||||
|
setSaving(true)
|
||||||
|
setApprovalPolicy(next)
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke("codeMode:setConfig", { enabled, approvalPolicy: next })
|
||||||
|
window.dispatchEvent(new Event("code-mode-config-changed"))
|
||||||
|
} catch {
|
||||||
|
setApprovalPolicy(prev)
|
||||||
|
toast.error("Failed to update approval policy")
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}, [enabled, approvalPolicy])
|
||||||
|
|
||||||
const anyReady = status?.claude.installed && status?.claude.signedIn
|
const anyReady = status?.claude.installed && status?.claude.signedIn
|
||||||
|| status?.codex.installed && status?.codex.signedIn
|
|| status?.codex.installed && status?.codex.signedIn
|
||||||
|
|
@ -1832,6 +1852,35 @@ function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{enabled && (
|
||||||
|
<div className="rounded-md border px-3 py-3 space-y-2">
|
||||||
|
<div className="text-sm font-medium">Approvals</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
How the coding agent checks in before changing files or running commands. You always see
|
||||||
|
everything it does in the timeline — this only controls the prompts.
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={approvalPolicy}
|
||||||
|
onValueChange={(v) => handlePolicyChange(v as ApprovalPolicy)}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ask">Ask every time</SelectItem>
|
||||||
|
<SelectItem value="auto-approve-reads">Auto-approve reads</SelectItem>
|
||||||
|
<SelectItem value="yolo">Auto-approve everything (YOLO)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{approvalPolicy === 'ask' && 'You approve every file change and command the agent wants to run.'}
|
||||||
|
{approvalPolicy === 'auto-approve-reads' && 'Reading and searching run automatically; you still approve writes, edits, and commands.'}
|
||||||
|
{approvalPolicy === 'yolo' && 'The agent runs everything — writes, edits, and commands — without asking. Use only in folders you trust.'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{enabled && status && !anyReady && (
|
{enabled && status && !anyReady && (
|
||||||
<div className="rounded-md border border-amber-500/40 bg-amber-50/60 dark:bg-amber-950/20 px-3 py-2.5 flex items-start gap-2 text-xs">
|
<div className="rounded-md border border-amber-500/40 bg-amber-50/60 dark:bg-amber-950/20 px-3 py-2.5 flex items-start gap-2 text-xs">
|
||||||
<AlertTriangle className="size-4 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
|
<AlertTriangle className="size-4 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { ZListToolkitsResponse } from './composio.js';
|
||||||
import { BrowserStateSchema } from './browser-control.js';
|
import { BrowserStateSchema } from './browser-control.js';
|
||||||
import { BillingInfoSchema } from './billing.js';
|
import { BillingInfoSchema } from './billing.js';
|
||||||
import { EmailBlockSchema, GmailThreadSchema } from './blocks.js';
|
import { EmailBlockSchema, GmailThreadSchema } from './blocks.js';
|
||||||
import { PermissionDecision } from './code-mode.js';
|
import { PermissionDecision, ApprovalPolicy } from './code-mode.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Runtime Validation Schemas (Single Source of Truth)
|
// Runtime Validation Schemas (Single Source of Truth)
|
||||||
|
|
@ -431,11 +431,13 @@ const ipcSchemas = {
|
||||||
req: z.null(),
|
req: z.null(),
|
||||||
res: z.object({
|
res: z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
approvalPolicy: ApprovalPolicy.optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'codeMode:setConfig': {
|
'codeMode:setConfig': {
|
||||||
req: z.object({
|
req: z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
approvalPolicy: ApprovalPolicy.optional(),
|
||||||
}),
|
}),
|
||||||
res: z.object({
|
res: z.object({
|
||||||
success: z.literal(true),
|
success: z.literal(true),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue