diff --git a/AGENTS.md b/AGENTS.md index 081c3481..dd7eb586 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,7 +145,7 @@ Before commit/push/PR: use the **`pre-push-review`** skill (`~/.cursor/skills/pr - **RPC**: CLI registers handlers (`rpc-register`), hub routes requests via `rpcGateway.ts` - **Versioned updates**: CLI sends `update-metadata`/`update-state` with version; hub rejects stale - **Session modes**: `local` (terminal) vs `remote` (web-controlled); switchable mid-session -- **Permission modes**: `default`, `acceptEdits`, `bypassPermissions`, `plan` +- **Permission modes**: `default`, `acceptEdits`, `auto`, `bypassPermissions`, `plan` - **Namespaces**: Multi-user isolation via `CLI_API_TOKEN:` suffix ## Adding new web features — consider an FUE diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index 44fd4baa..4472aee9 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -153,8 +153,11 @@ export async function runClaude(options: StartOptions = {}): Promise { setControlledByUser(session, startingMode); // Import MessageQueue2 and create message queue + // 'plan' and 'auto' are enforced inside Claude itself (not emulated via + // canCallTool like acceptEdits/bypassPermissions), so switching to/from + // them must start a new process with the matching --permission-mode flag. const messageQueue = new MessageQueue2(mode => hashObject({ - isPlan: mode.permissionMode === 'plan', + agentEnforcedMode: mode.permissionMode === 'plan' || mode.permissionMode === 'auto' ? mode.permissionMode : null, model: mode.model, effort: mode.effort, fallbackModel: mode.fallbackModel, diff --git a/cli/src/claude/utils/permissionHandler.ts b/cli/src/claude/utils/permissionHandler.ts index 275760ec..d154b718 100644 --- a/cli/src/claude/utils/permissionHandler.ts +++ b/cli/src/claude/utils/permissionHandler.ts @@ -32,7 +32,7 @@ interface PermissionResponse { receivedAt?: number; } -const PLAN_EXIT_MODES: PermissionMode[] = ['default', 'acceptEdits', 'bypassPermissions']; +const PLAN_EXIT_MODES: PermissionMode[] = ['default', 'acceptEdits', 'auto', 'bypassPermissions']; function isAskUserQuestionToolName(toolName: string): boolean { return toolName === 'AskUserQuestion' || toolName === 'ask_user_question'; diff --git a/cli/src/runner/buildCliArgs.test.ts b/cli/src/runner/buildCliArgs.test.ts index 46fe6622..6d809b11 100644 --- a/cli/src/runner/buildCliArgs.test.ts +++ b/cli/src/runner/buildCliArgs.test.ts @@ -72,7 +72,7 @@ describe('buildCliArgs', () => { }) it('validates all known permission modes', () => { - for (const mode of ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'ask', 'read-only', 'safe-yolo', 'yolo']) { + for (const mode of ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan', 'ask', 'read-only', 'safe-yolo', 'yolo']) { const args = buildCliArgs('claude', { directory: '/tmp', permissionMode: mode, diff --git a/shared/src/modes.test.ts b/shared/src/modes.test.ts new file mode 100644 index 00000000..0b0a50c1 --- /dev/null +++ b/shared/src/modes.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'bun:test' +import { + getPermissionModeLabel, + getPermissionModeTone, + isPermissionModeAllowedForFlavor +} from './modes' + +describe('claude auto permission mode', () => { + it('is allowed for claude only', () => { + expect(isPermissionModeAllowedForFlavor('auto', 'claude')).toBe(true) + expect(isPermissionModeAllowedForFlavor('auto', 'codex')).toBe(false) + expect(isPermissionModeAllowedForFlavor('auto', 'gemini')).toBe(false) + expect(isPermissionModeAllowedForFlavor('auto', 'cursor')).toBe(false) + expect(isPermissionModeAllowedForFlavor('auto', 'opencode')).toBe(false) + expect(isPermissionModeAllowedForFlavor('auto', 'kimi')).toBe(false) + }) + + it('has a label and tone', () => { + expect(getPermissionModeLabel('auto')).toBe('Auto') + expect(getPermissionModeTone('auto')).toBe('warning') + }) +}) diff --git a/shared/src/modes.ts b/shared/src/modes.ts index c8eb7389..06007c5f 100644 --- a/shared/src/modes.ts +++ b/shared/src/modes.ts @@ -11,7 +11,7 @@ export const AGENT_FLAVORS = ['claude', 'codex', 'cursor', 'gemini', 'kimi', 'op export type AgentFlavor = typeof AGENT_FLAVORS[number] export const AgentFlavorSchema = z.enum(AGENT_FLAVORS) -export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'bypassPermissions', 'plan'] as const +export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan'] as const export type ClaudePermissionMode = typeof CLAUDE_PERMISSION_MODES[number] export const CODEX_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const @@ -35,6 +35,7 @@ export type CursorPermissionMode = typeof CURSOR_PERMISSION_MODES[number] export const PERMISSION_MODES = [ 'default', 'acceptEdits', + 'auto', 'bypassPermissions', 'plan', 'ask', @@ -49,6 +50,7 @@ export type PermissionMode = typeof PERMISSION_MODES[number] export const PERMISSION_MODE_LABELS: Record = { default: 'Default', acceptEdits: 'Accept Edits', + auto: 'Auto', plan: 'Plan Mode', ask: 'Ask Mode', debug: 'Debug Mode', @@ -63,6 +65,7 @@ export type PermissionModeTone = 'neutral' | 'info' | 'warning' | 'danger' export const PERMISSION_MODE_TONES: Record = { default: 'neutral', acceptEdits: 'warning', + auto: 'warning', plan: 'info', ask: 'info', debug: 'info', diff --git a/web/README.md b/web/README.md index 8e384a20..2f6ee5ea 100644 --- a/web/README.md +++ b/web/README.md @@ -47,7 +47,7 @@ See `src/router.tsx` for route definitions. - Message thread with infinite scroll. - Composer for sending messages. -- Permission mode toggle (default/acceptEdits/bypassPermissions/plan). +- Permission mode toggle (default/acceptEdits/auto/bypassPermissions/plan). - Model selection (default/sonnet/sonnet[1m]/opus/opus[1m]). - Session abort and mode switch controls. - Context size display. diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 8867d1da..488e7ea2 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -521,8 +521,8 @@ export class ApiClient { async approvePermission( sessionId: string, requestId: string, - modeOrOptions?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | { - mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' + modeOrOptions?: 'default' | 'acceptEdits' | 'auto' | 'bypassPermissions' | 'plan' | { + mode?: 'default' | 'acceptEdits' | 'auto' | 'bypassPermissions' | 'plan' allowTools?: string[] decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort' answers?: Record | Record