Add per-tool and per-project YOLO mode#28
Conversation
- Migration 145: new `tool_yolo` table + `projects.yolo_mode` column - ToolYoloAPI: CRUD hooks for per-tool YOLO entries - ProjectAPI: yoloMode field, fetchProjectYoloMode, useSetProjectYoloMode - ToolsetsManager: resolveYoloMode() precedence chain (per-project → per-tool → global) - MessageAPI: thread projectId through to executeToolCall - PermissionsTab: per-tool YOLO toggles section (shown when global YOLO is off) - ProjectView: YOLO Mode selector (Inherit Global / Enabled / Disabled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
Adds finer-grained YOLO (auto-execute) controls by introducing per-project overrides and per-tool allowlisting, and threads project context through tool execution so the correct precedence can be applied.
Changes:
- Add
tool_yolotable andprojects.yolo_modecolumn to persist per-tool and per-project YOLO settings. - Introduce ToolYolo API helpers/hooks and expose per-project YOLO mode fetch/mutation in ProjectAPI.
- Update tool execution flow to resolve YOLO mode via precedence (project → tool → global) and add UI controls for per-project/per-tool settings.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src-tauri/src/migrations.rs | Adds migration for tool_yolo table and projects.yolo_mode column |
| src/core/chorus/api/ToolYoloAPI.ts | New API module for CRUD/checking per-tool YOLO entries + React Query hooks |
| src/core/chorus/api/ProjectAPI.ts | Adds yoloMode to Project type, DB mapping, fetch helper, and mutation |
| src/core/chorus/ToolsetsManager.ts | Adds YOLO resolution precedence logic and threads projectId into tool execution |
| src/core/chorus/api/MessageAPI.ts | Passes projectId through to tool execution during streaming |
| src/ui/components/PermissionsTab.tsx | Adds per-tool auto-accept UI when global YOLO is off |
| src/ui/components/ProjectView.tsx | Adds per-project YOLO mode selector (inherit/enabled/disabled) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Migration { | ||
| version: 145, | ||
| description: "add tool_yolo table and projects.yolo_mode column", | ||
| kind: MigrationKind::Up, | ||
| sql: r#" | ||
| CREATE TABLE IF NOT EXISTS tool_yolo ( | ||
| toolset_name TEXT NOT NULL, | ||
| tool_name TEXT NOT NULL, | ||
| created_at DATETIME DEFAULT CURRENT_TIMESTAMP, | ||
| PRIMARY KEY (toolset_name, tool_name) | ||
| ); | ||
|
|
||
| ALTER TABLE projects ADD COLUMN yolo_mode INTEGER DEFAULT NULL; | ||
| "#, | ||
| }, |
There was a problem hiding this comment.
Migration list is out of version order (144, 145, then 143). tauri_plugin_sql migrations are typically applied in the order provided, and out-of-order versions can cause startup migration failures or skipped migrations. Reorder these migration blocks so versions are strictly increasing (…142, 143, 144, 145) and keep new migrations appended in the correct position.
| // 2. Per-tool YOLO | ||
| const isToolYolo = await checkToolYolo(toolsetName, toolName); | ||
| if (isToolYolo) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
resolveYoloMode always queries tool_yolo before checking global YOLO. Since the effective result is (projectOverride ?? (globalYolo || perToolYolo)), you can fetch global YOLO before checking per-tool and skip the tool_yolo query entirely when global YOLO is enabled. This avoids an extra DB roundtrip on every tool call.
| {!yoloMode && allTools.length > 0 && ( | ||
| <div className="space-y-2"> | ||
| <div className="space-y-1"> | ||
| <h3 className="text-base font-semibold"> | ||
| Auto-accept specific tools | ||
| </h3> |
There was a problem hiding this comment.
useYoloMode() can return undefined while loading / when metadata is absent, but the UI treats falsy as "global YOLO off" (!yoloMode). This can briefly show the per-tool auto-accept section (or hide the note) even if global YOLO is actually enabled. Consider handling the loading/undefined state explicitly (e.g., yoloMode === false / yoloMode === true, or gate on the underlying app-metadata query readiness).
| <Label className="font-mono text-sm cursor-pointer"> | ||
| {toolsetName}_{toolName} | ||
| </Label> | ||
| </div> | ||
| <Switch |
There was a problem hiding this comment.
The per-tool Label isn’t associated with its Switch (no htmlFor/id), so clicking the label won’t toggle the control and screen readers won’t get a clear relationship. Add a stable id per tool switch and set Label htmlFor (or add an aria-label to the Switch).
| const { data: toolYoloEntries } = ToolYoloAPI.useAllToolYolo(); | ||
| const setToolYolo = ToolYoloAPI.useSetToolYolo(); | ||
| const deleteToolYolo = ToolYoloAPI.useDeleteToolYolo(); |
There was a problem hiding this comment.
useAllToolYolo() runs unconditionally, even when global YOLO is enabled and the per-tool section is hidden. Consider setting the query’s enabled option based on yoloMode === false (and possibly allTools.length > 0) to avoid an unnecessary DB query.
| const allTools = React.useMemo(() => { | ||
| return ToolsetsManager.instance | ||
| .listToolsets() | ||
| .flatMap((toolset) => | ||
| toolset.listTools().map((tool) => ({ | ||
| toolsetName: tool.toolsetName, | ||
| toolName: tool.displayNameSuffix, | ||
| })), | ||
| ); | ||
| }, []); |
There was a problem hiding this comment.
allTools is memoized with an empty dependency array, so the list will never update if toolsets are started/stopped or refreshed while this tab is mounted. If tool availability can change at runtime, consider deriving this list from reactive state (or recomputing when toolset status/config changes).
| const allTools = React.useMemo(() => { | |
| return ToolsetsManager.instance | |
| .listToolsets() | |
| .flatMap((toolset) => | |
| toolset.listTools().map((tool) => ({ | |
| toolsetName: tool.toolsetName, | |
| toolName: tool.displayNameSuffix, | |
| })), | |
| ); | |
| }, []); | |
| const allTools = ToolsetsManager.instance | |
| .listToolsets() | |
| .flatMap((toolset) => | |
| toolset.listTools().map((tool) => ({ | |
| toolsetName: tool.toolsetName, | |
| toolName: tool.displayNameSuffix, | |
| })), | |
| ); |
| const rows = await db.select<ToolYoloDBRow[]>( | ||
| "SELECT 1 FROM tool_yolo WHERE toolset_name = ? AND tool_name = ?", | ||
| [toolsetName, toolName], | ||
| ); | ||
| return rows.length > 0; |
There was a problem hiding this comment.
checkToolYolo runs SELECT 1 ... but types the result as ToolYoloDBRow[]. This is misleading and makes it easy to accidentally rely on fields that won’t exist. Either select the actual columns (or SELECT 1 AS exists) and update the row type to match.
- Fix migration ordering (142→143→144→145 instead of 142→144→145→143)
- Check global YOLO before per-tool to skip unnecessary DB query
- Fix checkToolYolo SELECT 1 typed as ToolYoloDBRow (now { exists })
- Add htmlFor/id to per-tool Label/Switch for accessibility
- Make useAllToolYolo conditional (skip query when global YOLO is on)
- Use yoloMode === false instead of !yoloMode to handle loading state
- Remove stale useMemo with empty deps for allTools
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes #(relevant issue if exists)
Adds finer-grained YOLO control beyond the existing all-or-nothing global toggle.
Changes
tool_yolotable (per-tool auto-execute list) +projects.yolo_modecolumn (NULL = inherit, 0 = force off, 1 = force on)tool_yoloentriesyoloModefield onProjecttype;fetchProjectYoloMode()(non-hook for ToolsetsManager);useSetProjectYoloMode()mutationresolveYoloMode()precedence chain: per-project override → per-tool YOLO → global YOLO;projectId?param added toexecuteToolCall()projectIdthrough toexecuteToolCall()Test plan