mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
feat(agents): add guarded Auto permission presets and per-agent controls
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Agent permission presets
|
||||
|
||||
Orca's permission presets map one product choice onto agent-specific CLI arguments or
|
||||
environment variables. The word **Auto** does not describe one uniform security policy:
|
||||
depending on the agent, it can mean classifier-reviewed actions, edits-only approval,
|
||||
risk-threshold approval, or a sandboxed approval reviewer. Keep the exact behavior visible in
|
||||
the UI and do not imply that every agent provides the same safety boundary.
|
||||
|
||||
This matrix records vendor-documented presets checked on September 6, 2026. A local help
|
||||
check identifies the syntax accepted by the installed CLI; the linked vendor source defines
|
||||
its semantics.
|
||||
|
||||
| Agent | Auto preset | Verified behavior | Evidence |
|
||||
| --- | --- | --- | --- |
|
||||
| Claude Code | `--permission-mode auto` | Uses Claude's auto-mode permission classifier. The installed Claude Code 2.1.263 help lists `auto` separately from `acceptEdits`, `manual`, and `bypassPermissions`. | [Claude Code permissions](https://code.claude.com/docs/en/permissions) and `claude --help` |
|
||||
| Codex | `--approve-for-me` | Routes approval requests through automatic review while retaining the workspace-write sandbox. The installed Codex CLI 0.153.4 help documents this behavior. | [Codex security](https://developers.openai.com/codex/security/) and [Codex CLI argument tests](https://github.com/openai/codex/blob/main/codex-rs/cli/src/main.rs) |
|
||||
| Gemini CLI | `--approval-mode auto_edit` | Auto-approves edit tools while continuing to prompt for other tools. The installed Gemini CLI 0.58.0 help lists `default`, `auto_edit`, `yolo`, and `plan`. | [Gemini CLI repository](https://github.com/google-gemini/gemini-cli) and `gemini --help` |
|
||||
| goose | `GOOSE_MODE=smart_approve` | Automatically approves low-risk actions and asks for approval on higher-risk actions. | [goose permission modes](https://github.com/block/goose/blob/main/documentation/docs/guides/managing-tools/goose-permissions.md) and [mode values](https://github.com/block/goose/blob/main/crates/goose-provider-types/src/goose_mode.rs) |
|
||||
| Qwen Code | `--approval-mode auto` | Uses an LLM classifier for shell commands, network calls, and out-of-workspace edits; risky or uncertain actions still prompt. | [Qwen Code approval modes](https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/approval-mode.md) |
|
||||
| Devin CLI | `--permission-mode smart` | Auto-approves workspace edits and uses a fast model to judge other actions, falling back to a prompt when unsafe, uncertain, or unavailable. Smart is rolling out gradually and may be unavailable for some accounts. | [Devin permissions](https://docs.devin.ai/cli/reference/permissions.md) and [commands and flags](https://docs.devin.ai/cli/reference/commands.md) |
|
||||
| Droid | `--auto medium` | Auto-approves edits, low-risk tools, and reversible workspace changes such as installs, builds, local commits, moves, and copies. Higher-risk actions still prompt. | [Droid autonomy levels](https://docs.factory.ai/autonomy-and-safety/auto-run.md) and `droid --help` from Droid 0.205.0 |
|
||||
|
||||
Gemini's `auto_edit` is deliberately narrower than classifier-based Auto modes. Droid also
|
||||
offers `--auto low` and `--auto high`; Medium is the balanced candidate because Low covers
|
||||
edits and low-risk tools, while High permits high-risk actions unless a separate safety check
|
||||
intervenes. Devin's `accept-edits` is a narrower fallback if Smart is unavailable, but it is
|
||||
not behaviorally equivalent to Smart.
|
||||
|
||||
Claude Agent Teams forwards its arguments to the Claude binary through `orca claude-teams`
|
||||
and uses the same `--permission-mode auto` preset (see `src/cli/handlers/core.ts`).
|
||||
|
||||
The global Auto preset uses Manual for harnesses without a verified mapping. Per-agent
|
||||
controls omit Auto for those harnesses. Custom argument and environment profiles are preserved.
|
||||
Settings apply to future launches, including folder workspaces and SSH/WSL execution hosts.
|
||||
The installed CLI and account must support the selected mode; Orca never retries with bypass
|
||||
when a guarded mode is rejected. Existing default permission preferences are unchanged.
|
||||
|
||||
## No verified Auto preset yet
|
||||
|
||||
The remaining agents in `YOLO_TUI_AGENT_ARGS` have no intermediate preset verified for this
|
||||
work: OpenClaude, Antigravity, Aider, Amp, Kiro, Crush, Autohand, Cline,
|
||||
Command Code, Continue, Cursor, Kimi, Mistral Vibe, Rovo, Hermes, Copilot, Grok, Ante, and Trae.
|
||||
This means only that this review did not find sufficiently clear vendor evidence for a preset;
|
||||
it is not a claim that the agent lacks intermediate permission controls. Amp's `smart` agent
|
||||
mode, for example, controls its model, system prompt, and tool selection rather than command
|
||||
approval policy, so it must not be treated as an Auto permission preset.
|
||||
|
||||
Re-check vendor documentation and current `--help` output before adding any of these agents.
|
||||
CLI flags and rollout availability change independently of Orca releases.
|
||||
@@ -247,3 +247,17 @@ predicate. It is unobservable today — the host publishes neither field for a c
|
||||
all, so a mirror has nothing to take either way. If the capability-gated publish this section
|
||||
anticipates ever lands, narrow them the same way rather than by placement kind: a mirror should
|
||||
take a failure it cannot otherwise see, and only the hosting client should refuse it.
|
||||
|
||||
## Auto permission presets on paired settings
|
||||
|
||||
`settings.agent-permission-auto.v1` identifies clients that understand Auto launch
|
||||
argument and environment presets. New hosts project those presets as manual in every
|
||||
settings response to older clients: their onboarding otherwise treats Auto as a custom
|
||||
mixed profile and can apply bypass to the remaining harnesses. When the host contains
|
||||
an Auto preset, unchanged legacy permission-map roundtrips are ignored, and changed
|
||||
permission maps are rejected with a client-upgrade message. Unrelated settings remain
|
||||
editable. Capable clients receive and edit the actual presets.
|
||||
|
||||
New clients can store Auto presets on older hosts because their existing argument and
|
||||
environment normalizers preserve these strings. Such hosts cannot protect the profile
|
||||
from a second, older client changing settings; that protection requires the host upgrade.
|
||||
|
||||
@@ -18,6 +18,7 @@ import { launchOrcaApp } from './launch'
|
||||
import { addEnvironmentFromPairingCode } from './environments'
|
||||
import { RuntimeClientError } from './types'
|
||||
import {
|
||||
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
|
||||
AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY,
|
||||
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
|
||||
@@ -72,6 +73,7 @@ describe('CLI remote WebSocket transport', () => {
|
||||
clientCapabilities: [
|
||||
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
|
||||
SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY,
|
||||
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
|
||||
SKILL_INSTALL_RESULT_V2_CAPABILITY,
|
||||
WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
|
||||
import {
|
||||
applyAgentPermissionMode,
|
||||
AUTO_TUI_AGENT_ARGS,
|
||||
AUTO_TUI_AGENT_ENV
|
||||
} from '../../../../shared/tui-agent-permissions'
|
||||
import { remoteRuntimeClientCapabilities } from '../../../../shared/remote-runtime-client-capabilities'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import type { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { RpcDispatcher } from '../dispatcher'
|
||||
import { CLIENT_UI_METHODS } from './client-ui'
|
||||
|
||||
function setup(mode: 'auto' | 'manual' = 'auto') {
|
||||
const settings = { ...applyAgentPermissionMode({ mode }), compactWorktreeCards: false }
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
getClientSettings: vi.fn(() => settings),
|
||||
updateClientSettings: vi.fn(async (updates) => Object.assign(settings, updates)),
|
||||
updateClientPRBotAuthorOverride: vi.fn(() => settings)
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS })
|
||||
const request = (method: string, params?: unknown, capabilities: string[] = []) =>
|
||||
dispatcher.dispatch(
|
||||
{ id: '1', authToken: 'token', method, params },
|
||||
{ clientKind: 'runtime', clientCapabilities: capabilities }
|
||||
)
|
||||
return { settings, runtime, request }
|
||||
}
|
||||
|
||||
function resultSettings(response: Awaited<ReturnType<ReturnType<typeof setup>['request']>>) {
|
||||
expect(response.ok).toBe(true)
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error.message)
|
||||
}
|
||||
return (response.result as { settings: ReturnType<typeof setup>['settings'] }).settings
|
||||
}
|
||||
|
||||
describe('Auto permissions across remote versions', () => {
|
||||
it('advertises Auto support on remote request transports', () => {
|
||||
expect(remoteRuntimeClientCapabilities()).toContain(AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY)
|
||||
})
|
||||
|
||||
it('projects Auto as manual to old clients without changing host settings', async () => {
|
||||
const { settings, request } = setup()
|
||||
const before = structuredClone(settings)
|
||||
const projected = resultSettings(await request('settings.get'))
|
||||
for (const agent of Object.keys(AUTO_TUI_AGENT_ARGS)) {
|
||||
expect(projected.agentDefaultArgs[agent as TuiAgent]).toBe('')
|
||||
}
|
||||
for (const agent of Object.keys(AUTO_TUI_AGENT_ENV)) {
|
||||
expect(projected.agentDefaultEnv[agent as TuiAgent]).toEqual({})
|
||||
}
|
||||
expect(settings).toEqual(before)
|
||||
})
|
||||
|
||||
it('preserves Auto when an old client echoes its projection with an unrelated edit', async () => {
|
||||
const { settings, runtime, request } = setup()
|
||||
const before = structuredClone(settings)
|
||||
const projected = resultSettings(await request('settings.get'))
|
||||
const updated = resultSettings(
|
||||
await request('settings.update', { ...projected, compactWorktreeCards: true })
|
||||
)
|
||||
expect(runtime.updateClientSettings).toHaveBeenCalledWith({ compactWorktreeCards: true })
|
||||
expect(settings.agentDefaultArgs).toEqual(before.agentDefaultArgs)
|
||||
expect(settings.agentDefaultEnv).toEqual(before.agentDefaultEnv)
|
||||
expect(updated).toEqual({ ...projected, compactWorktreeCards: true })
|
||||
})
|
||||
|
||||
it('rejects old-client permission widening, including unsupported Auto fallback agents', async () => {
|
||||
const { settings, runtime, request } = setup()
|
||||
const before = structuredClone(settings)
|
||||
const projected = resultSettings(await request('settings.get'))
|
||||
const response = await request('settings.update', {
|
||||
...applyAgentPermissionMode({ ...projected, mode: 'yolo' })
|
||||
})
|
||||
expect(response).toMatchObject({
|
||||
ok: false,
|
||||
error: { message: expect.stringContaining('Update this Orca client') }
|
||||
})
|
||||
expect(runtime.updateClientSettings).not.toHaveBeenCalled()
|
||||
expect(settings).toEqual(before)
|
||||
})
|
||||
|
||||
it('allows a capable client to view and change Auto', async () => {
|
||||
const { settings, request } = setup()
|
||||
const capabilities = [AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY]
|
||||
expect(resultSettings(await request('settings.get', undefined, capabilities))).toEqual(settings)
|
||||
const manual = applyAgentPermissionMode({ mode: 'manual' })
|
||||
expect(resultSettings(await request('settings.update', manual, capabilities))).toMatchObject(
|
||||
manual
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps permission edits available to legacy clients on hosts without Auto', async () => {
|
||||
const { request } = setup('manual')
|
||||
const bypass = applyAgentPermissionMode({ mode: 'yolo' })
|
||||
expect(resultSettings(await request('settings.update', bypass))).toMatchObject(bypass)
|
||||
})
|
||||
|
||||
it('projects settings published by unrelated bot-author updates too', async () => {
|
||||
const { request } = setup()
|
||||
const projected = resultSettings(await request('settings.get'))
|
||||
expect(
|
||||
resultSettings(
|
||||
await request('settings.updatePRBotAuthorOverride', { author: 'bot', isBot: true })
|
||||
)
|
||||
).toEqual(projected)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import { AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
|
||||
import { AUTO_TUI_AGENT_ARGS, AUTO_TUI_AGENT_ENV } from '../../../../shared/tui-agent-permissions'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import type {
|
||||
RuntimeClientSettings,
|
||||
RuntimeClientSettingsUpdate
|
||||
} from '../../runtime-client-settings'
|
||||
import type { RpcContext } from '../core'
|
||||
|
||||
type ClientContext = Pick<RpcContext, 'clientCapabilities' | 'clientKind'>
|
||||
|
||||
export function projectClientPermissionSettings(
|
||||
settings: RuntimeClientSettings,
|
||||
context: ClientContext
|
||||
): RuntimeClientSettings {
|
||||
if (
|
||||
context.clientKind === undefined ||
|
||||
context.clientCapabilities?.includes(AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY)
|
||||
) {
|
||||
return settings
|
||||
}
|
||||
let projected = settings
|
||||
for (const [agent, preset] of Object.entries(AUTO_TUI_AGENT_ARGS)) {
|
||||
if (settings.agentDefaultArgs?.[agent as TuiAgent]?.trim() === preset) {
|
||||
projected = {
|
||||
...projected,
|
||||
agentDefaultArgs: { ...projected.agentDefaultArgs, [agent]: '' }
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [agent, preset] of Object.entries(AUTO_TUI_AGENT_ENV)) {
|
||||
if (isDeepStrictEqual(settings.agentDefaultEnv?.[agent as TuiAgent], preset)) {
|
||||
projected = {
|
||||
...projected,
|
||||
agentDefaultEnv: { ...projected.agentDefaultEnv, [agent]: {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
return projected
|
||||
}
|
||||
|
||||
export function protectClientPermissionUpdate(
|
||||
updates: RuntimeClientSettingsUpdate,
|
||||
context: RpcContext
|
||||
): RuntimeClientSettingsUpdate {
|
||||
if (
|
||||
context.clientKind === undefined ||
|
||||
context.clientCapabilities?.includes(AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY) ||
|
||||
(updates.agentDefaultArgs === undefined && updates.agentDefaultEnv === undefined)
|
||||
) {
|
||||
return updates
|
||||
}
|
||||
const settings = context.runtime.getClientSettings()
|
||||
const projected = projectClientPermissionSettings(settings, context)
|
||||
if (projected === settings) {
|
||||
return updates
|
||||
}
|
||||
|
||||
// Legacy onboarding turns an unrecognized permission profile into bypass.
|
||||
for (const field of ['agentDefaultArgs', 'agentDefaultEnv'] as const) {
|
||||
if (updates[field] !== undefined && !isDeepStrictEqual(updates[field], projected[field])) {
|
||||
throw new Error('Update this Orca client to change permissions while Auto mode is enabled.')
|
||||
}
|
||||
}
|
||||
const { agentDefaultArgs: _args, agentDefaultEnv: _env, ...remaining } = updates
|
||||
return remaining
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
PRBotAuthorOverrideUpdate,
|
||||
SettingsUpdate
|
||||
} from './client-settings-schemas'
|
||||
import {
|
||||
projectClientPermissionSettings,
|
||||
protectClientPermissionUpdate
|
||||
} from './client-permission-projection'
|
||||
|
||||
import { FeatureInteractionIdParam, UiUpdate } from './client-ui-schemas'
|
||||
// Type-only side effect: keeps the schema/PersistedUIState parity assertions in
|
||||
// the typecheck graph so drift fails the build instead of a paired client.
|
||||
@@ -16,13 +21,18 @@ export const CLIENT_UI_METHODS: RpcMethod[] = [
|
||||
defineMethod({
|
||||
name: 'settings.get',
|
||||
params: null,
|
||||
handler: (_params, { runtime }) => ({ settings: runtime.getClientSettings() })
|
||||
handler: (_params, context) => ({
|
||||
settings: projectClientPermissionSettings(context.runtime.getClientSettings(), context)
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'settings.update',
|
||||
params: SettingsUpdate,
|
||||
handler: async (params, { runtime }) => ({
|
||||
settings: await runtime.updateClientSettings(params)
|
||||
handler: async (params, context) => ({
|
||||
settings: projectClientPermissionSettings(
|
||||
await context.runtime.updateClientSettings(protectClientPermissionUpdate(params, context)),
|
||||
context
|
||||
)
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
@@ -44,8 +54,11 @@ export const CLIENT_UI_METHODS: RpcMethod[] = [
|
||||
defineMethod({
|
||||
name: 'settings.updatePRBotAuthorOverride',
|
||||
params: PRBotAuthorOverrideUpdate,
|
||||
handler: (params, { runtime }) => ({
|
||||
settings: runtime.updateClientPRBotAuthorOverride(params)
|
||||
handler: (params, context) => ({
|
||||
settings: projectClientPermissionSettings(
|
||||
context.runtime.updateClientPRBotAuthorOverride(params),
|
||||
context
|
||||
)
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
|
||||
@@ -13,17 +13,17 @@ describe('AgentStep', () => {
|
||||
onSelect={vi.fn()}
|
||||
detectedSet={new Set([AGENT_CATALOG[0].id])}
|
||||
isDetecting={false}
|
||||
yoloPermissions
|
||||
onYoloPermissionsChange={vi.fn()}
|
||||
permissionMode="auto"
|
||||
onPermissionModeChange={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
expect(html).toContain(`Show ${AGENT_CATALOG.length - 1} more agents→`)
|
||||
expect(html).toContain('data-agent-grid-scroll')
|
||||
expect(html).toContain('data-slot="checkbox"')
|
||||
expect(html).toContain('Yolo / Dangerously skip permissions')
|
||||
expect(html).not.toContain('role="radiogroup"')
|
||||
expect(html).toContain('>Auto</button>')
|
||||
expect(html).toContain('agents without Auto use Manual')
|
||||
expect(html).toContain('role="radiogroup"')
|
||||
})
|
||||
|
||||
it('labels the fallback agents summary as hide when expanded', () => {
|
||||
@@ -34,8 +34,8 @@ describe('AgentStep', () => {
|
||||
onSelect={vi.fn()}
|
||||
detectedSet={new Set([AGENT_CATALOG[0].id])}
|
||||
isDetecting={false}
|
||||
yoloPermissions
|
||||
onYoloPermissionsChange={vi.fn()}
|
||||
permissionMode="auto"
|
||||
onPermissionModeChange={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useLayoutEffect, useRef, useState } from 'react'
|
||||
import { Check, ExternalLink, Info } from 'lucide-react'
|
||||
import { Check, ExternalLink } from 'lucide-react'
|
||||
import { getAgentCatalog, AgentIcon, type AgentCatalogEntry } from '@/lib/agent-catalog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { AgentPermissionsSetting } from '../settings/AgentPermissionsSetting'
|
||||
import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissions'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
@@ -18,8 +18,8 @@ type AgentStepProps = {
|
||||
onSelect: (agent: TuiAgent, fromCollapsedSection: boolean) => void
|
||||
detectedSet: Set<TuiAgent>
|
||||
isDetecting: boolean
|
||||
yoloPermissions?: boolean
|
||||
onYoloPermissionsChange?: (enabled: boolean) => void
|
||||
permissionMode?: AgentPermissionMode
|
||||
onPermissionModeChange?: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
|
||||
}
|
||||
|
||||
function useAgentGridScrollMaxHeight(
|
||||
@@ -64,8 +64,8 @@ export function AgentStep({
|
||||
onSelect,
|
||||
detectedSet,
|
||||
isDetecting,
|
||||
yoloPermissions = true,
|
||||
onYoloPermissionsChange
|
||||
permissionMode,
|
||||
onPermissionModeChange
|
||||
}: AgentStepProps) {
|
||||
const agentCatalog = getAgentCatalog()
|
||||
const detected = agentCatalog.filter((agent) => detectedSet.has(agent.id))
|
||||
@@ -190,65 +190,15 @@ export function AgentStep({
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<YoloPermissionsControl
|
||||
yoloPermissions={yoloPermissions}
|
||||
onYoloPermissionsChange={onYoloPermissionsChange}
|
||||
/>
|
||||
{permissionMode && onPermissionModeChange && (
|
||||
<div className="mt-auto shrink-0 rounded-lg border border-border bg-muted/25 px-4 py-3">
|
||||
<AgentPermissionsSetting mode={permissionMode} onChange={onPermissionModeChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function YoloPermissionsControl({
|
||||
yoloPermissions,
|
||||
onYoloPermissionsChange
|
||||
}: {
|
||||
yoloPermissions: boolean
|
||||
onYoloPermissionsChange?: (enabled: boolean) => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<label className="mt-auto flex shrink-0 cursor-pointer items-center justify-between gap-4 rounded-lg border border-border bg-muted/25 px-4 py-3 transition-colors hover:bg-muted/40">
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<Checkbox
|
||||
checked={yoloPermissions}
|
||||
onCheckedChange={(checked) => onYoloPermissionsChange?.(checked === true)}
|
||||
className="border-border bg-card data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
||||
aria-label={translate(
|
||||
'auto.components.onboarding.AgentStep.yoloPermissionsLabel',
|
||||
'Yolo / Dangerously skip permissions'
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 text-sm font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.onboarding.AgentStep.yoloPermissionsLabel',
|
||||
'Yolo / Dangerously skip permissions'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={translate(
|
||||
'auto.components.onboarding.AgentStep.yoloPermissionsInfo',
|
||||
'Agent permission info'
|
||||
)}
|
||||
onPointerDown={(event) => event.preventDefault()}
|
||||
className="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
>
|
||||
<Info className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6} style={{ zIndex: 120 }}>
|
||||
{translate(
|
||||
'auto.components.onboarding.AgentStep.yoloPermissionsTooltip',
|
||||
'Skip permission checks for agents for less interruptions'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionHeader({
|
||||
label,
|
||||
count,
|
||||
|
||||
@@ -314,8 +314,8 @@ export default function OnboardingFlow({
|
||||
onSelect={flow.setSelectedAgent}
|
||||
detectedSet={flow.detectedSet}
|
||||
isDetecting={flow.isDetectingAgents}
|
||||
yoloPermissions={flow.yoloPermissions}
|
||||
onYoloPermissionsChange={flow.setYoloPermissions}
|
||||
permissionMode={flow.permissionMode}
|
||||
onPermissionModeChange={flow.setPermissionMode}
|
||||
/>
|
||||
)}
|
||||
{currentStep.id === 'theme' && (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { createElement, useEffect, act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultOnboardingState } from '../../../../shared/constants'
|
||||
import { getDefaultOnboardingState, getDefaultSettings } from '../../../../shared/constants'
|
||||
import type { OnboardingState } from '../../../../shared/onboarding-state-types'
|
||||
|
||||
const trackMock = vi.hoisted(() => vi.fn())
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
buildCompletedOnboardingNotificationSettings,
|
||||
buildOnboardingDismissedPayload,
|
||||
useCloseWith,
|
||||
usePersistCurrentStep,
|
||||
type DismissedExtras,
|
||||
trackOnboardingDismissed
|
||||
} from './use-onboarding-flow-persistence'
|
||||
@@ -90,6 +91,48 @@ describe('onboarding flow persistence', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it.each([null, 'auto'] as const)(
|
||||
'preserves permissions unless a preset is explicitly selected (%s)',
|
||||
async (selection) => {
|
||||
const settings = getDefaultSettings('/tmp')
|
||||
settings.agentDefaultArgs = {
|
||||
...settings.agentDefaultArgs,
|
||||
claude: '',
|
||||
codex: '--model custom'
|
||||
}
|
||||
const updateSettings = vi.fn()
|
||||
let persist: (() => Promise<{ ok: boolean }>) | undefined
|
||||
function Probe(): null {
|
||||
persist = usePersistCurrentStep({
|
||||
currentStepId: 'agent',
|
||||
selectedAgent: 'claude',
|
||||
permissionModeSelection: selection,
|
||||
theme: settings.theme,
|
||||
settings,
|
||||
updateSettings,
|
||||
onboardingChecklist: getDefaultOnboardingState().checklist,
|
||||
onOnboardingChange: vi.fn(),
|
||||
setError: vi.fn()
|
||||
})
|
||||
return null
|
||||
}
|
||||
container = document.createElement('div')
|
||||
root = createRoot(container)
|
||||
act(() => root?.render(createElement(Probe)))
|
||||
await act(async () => {
|
||||
await persist?.()
|
||||
})
|
||||
const update = updateSettings.mock.calls[0][0]
|
||||
if (selection === null) {
|
||||
expect(update).toEqual({ defaultTuiAgent: 'claude' })
|
||||
} else {
|
||||
expect(update.agentDefaultArgs.claude).toBe('--permission-mode auto')
|
||||
expect(update.agentDefaultArgs.codex).toBe('--model custom')
|
||||
expect(update.agentDefaultArgs.aider).toBe('')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('builds dismissed telemetry with the triggering advance path', () => {
|
||||
expect(
|
||||
buildOnboardingDismissedPayload(3, {
|
||||
|
||||
@@ -6,7 +6,10 @@ import type { EventProps } from '../../../../shared/telemetry-events'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import type { OnboardingState } from '../../../../shared/onboarding-state-types'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { applyAgentPermissionMode } from '../../../../shared/tui-agent-permissions'
|
||||
import {
|
||||
applyAgentPermissionMode,
|
||||
type AgentPermissionMode
|
||||
} from '../../../../shared/tui-agent-permissions'
|
||||
import type { StepId, StepNumber } from './use-onboarding-flow-types'
|
||||
|
||||
export async function persistStep(
|
||||
@@ -132,7 +135,7 @@ export function useCloseWith({ onOnboardingChange, startTimeRef, setError }: Clo
|
||||
type PersistCurrentStepDeps = {
|
||||
currentStepId: StepId
|
||||
selectedAgent: TuiAgent | null
|
||||
yoloPermissions: boolean
|
||||
permissionModeSelection: Exclude<AgentPermissionMode, 'mixed'> | null
|
||||
theme: GlobalSettings['theme']
|
||||
settings: GlobalSettings | null
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void> | void
|
||||
@@ -148,7 +151,7 @@ export type PersistCurrentStepResult = {
|
||||
export function usePersistCurrentStep({
|
||||
currentStepId,
|
||||
selectedAgent,
|
||||
yoloPermissions,
|
||||
permissionModeSelection,
|
||||
theme,
|
||||
settings,
|
||||
updateSettings,
|
||||
@@ -165,11 +168,13 @@ export function usePersistCurrentStep({
|
||||
const defaultTuiAgent = selectedAgentOrBlank(selectedAgent)
|
||||
await updateSettings({
|
||||
defaultTuiAgent,
|
||||
...applyAgentPermissionMode({
|
||||
mode: yoloPermissions ? 'yolo' : 'manual',
|
||||
agentDefaultArgs: settings.agentDefaultArgs,
|
||||
agentDefaultEnv: settings.agentDefaultEnv
|
||||
})
|
||||
...(permissionModeSelection === null
|
||||
? {}
|
||||
: applyAgentPermissionMode({
|
||||
mode: permissionModeSelection,
|
||||
agentDefaultArgs: settings.agentDefaultArgs,
|
||||
agentDefaultEnv: settings.agentDefaultEnv
|
||||
}))
|
||||
})
|
||||
const choseAgent = defaultTuiAgent !== 'blank'
|
||||
const wasAlreadyChosen = onboardingChecklist.choseAgent
|
||||
@@ -226,7 +231,7 @@ export function usePersistCurrentStep({
|
||||
settings,
|
||||
theme,
|
||||
updateSettings,
|
||||
yoloPermissions,
|
||||
permissionModeSelection,
|
||||
setError
|
||||
])
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import { STEPS } from './use-onboarding-flow-types'
|
||||
import { persistStep, useCloseWith, usePersistCurrentStep } from './use-onboarding-flow-persistence'
|
||||
import { resolveOnboardingSettingsHydration } from './onboarding-settings-hydration'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { resolveAgentPermissionModeSummary } from '../../../../shared/tui-agent-permissions'
|
||||
import {
|
||||
resolveAgentPermissionModeSummary,
|
||||
type AgentPermissionMode
|
||||
} from '../../../../shared/tui-agent-permissions'
|
||||
import { isWindowsUserAgent } from '@/components/terminal-pane/pane-helpers'
|
||||
import {
|
||||
isSkippedStepIndex,
|
||||
@@ -66,12 +69,16 @@ export function useOnboardingFlow(
|
||||
? settings.defaultTuiAgent
|
||||
: null
|
||||
)
|
||||
const [yoloPermissions, setYoloPermissions] = useState(
|
||||
const [permissionModeSelection, setPermissionModeSelection] = useState<Exclude<
|
||||
AgentPermissionMode,
|
||||
'mixed'
|
||||
> | null>(null)
|
||||
const permissionMode =
|
||||
permissionModeSelection ??
|
||||
resolveAgentPermissionModeSummary({
|
||||
agentDefaultArgs: settings?.agentDefaultArgs,
|
||||
agentDefaultEnv: settings?.agentDefaultEnv
|
||||
}) !== 'manual'
|
||||
)
|
||||
})
|
||||
// Why: hydrate theme from saved settings so users who already chose one see it preselected.
|
||||
const [theme, setTheme] = useState<GlobalSettings['theme']>(settings?.theme ?? 'dark')
|
||||
const [busyLabel, setBusyLabel] = useState<string | null>(null)
|
||||
@@ -80,7 +87,6 @@ export function useOnboardingFlow(
|
||||
// Why: settings hydrate async after the lazy initializers run; re-sync once before commit unless the user edited the field.
|
||||
const themeInteractedRef = useRef(false)
|
||||
const agentInteractedRef = useRef(false)
|
||||
const yoloPermissionsInteractedRef = useRef(false)
|
||||
const [settingsHydrated, setSettingsHydrated] = useState(settings != null)
|
||||
const settingsHydration = resolveOnboardingSettingsHydration({
|
||||
settings,
|
||||
@@ -99,17 +105,6 @@ export function useOnboardingFlow(
|
||||
setSelectedAgent(settingsHydration.selectedAgent)
|
||||
}
|
||||
}
|
||||
if (settings && !yoloPermissionsInteractedRef.current) {
|
||||
const nextYoloPermissions =
|
||||
resolveAgentPermissionModeSummary({
|
||||
agentDefaultArgs: settings.agentDefaultArgs,
|
||||
agentDefaultEnv: settings.agentDefaultEnv
|
||||
}) !== 'manual'
|
||||
if (nextYoloPermissions !== yoloPermissions) {
|
||||
setYoloPermissions(nextYoloPermissions)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: track interaction so async settings hydration doesn't overwrite a value the user chose.
|
||||
const setThemeInteractive = useCallback((value: GlobalSettings['theme']) => {
|
||||
themeInteractedRef.current = true
|
||||
@@ -152,11 +147,6 @@ export function useOnboardingFlow(
|
||||
},
|
||||
[]
|
||||
)
|
||||
const setYoloPermissionsInteractive = useCallback((enabled: boolean) => {
|
||||
yoloPermissionsInteractedRef.current = true
|
||||
setYoloPermissions(enabled)
|
||||
}, [])
|
||||
|
||||
const detectedSet = useMemo(() => new Set(detectedAgentIds ?? []), [detectedAgentIds])
|
||||
const currentStep = STEPS[stepIndex]
|
||||
// Why: the stepper shows only steps the user will land on; skipped optional steps are dropped, not rendered as dead dots.
|
||||
@@ -278,7 +268,7 @@ export function useOnboardingFlow(
|
||||
const persistCurrentStep = usePersistCurrentStep({
|
||||
currentStepId: currentStep.id,
|
||||
selectedAgent,
|
||||
yoloPermissions,
|
||||
permissionModeSelection,
|
||||
theme,
|
||||
settings,
|
||||
updateSettings,
|
||||
@@ -318,8 +308,8 @@ export function useOnboardingFlow(
|
||||
currentStep,
|
||||
selectedAgent,
|
||||
setSelectedAgent: setSelectedAgentInteractive,
|
||||
yoloPermissions,
|
||||
setYoloPermissions: setYoloPermissionsInteractive,
|
||||
permissionMode,
|
||||
setPermissionMode: setPermissionModeSelection,
|
||||
theme,
|
||||
setTheme: setThemeInteractive,
|
||||
busyLabel,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink } from 'lucide-react'
|
||||
import { AgentPermissionModeControl } from './AgentPermissionsSetting'
|
||||
import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissions'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { AgentIcon } from '@/lib/agent-catalog'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -68,6 +70,8 @@ export type AgentCatalogRowProps = {
|
||||
cmdOverride: string | undefined
|
||||
argsOverride: string
|
||||
envOverride: Record<string, string>
|
||||
permissionMode?: AgentPermissionMode
|
||||
onSetPermissionMode?: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
|
||||
onSetDefault: () => void
|
||||
onSetEnabled: (enabled: boolean) => void
|
||||
onSaveOverride: (value: string) => void
|
||||
@@ -89,6 +93,8 @@ export function AgentCatalogRow({
|
||||
cmdOverride,
|
||||
argsOverride,
|
||||
envOverride,
|
||||
permissionMode,
|
||||
onSetPermissionMode,
|
||||
onSetDefault,
|
||||
onSetEnabled,
|
||||
onSaveOverride,
|
||||
@@ -98,9 +104,7 @@ export function AgentCatalogRow({
|
||||
}: AgentCatalogRowProps): React.JSX.Element {
|
||||
const envSummary = stringifyAgentDefaultEnvDraft(envOverride)
|
||||
const defaultEnvSummary = stringifyAgentDefaultEnvDraft(defaultEnv)
|
||||
const [cmdOpen, setCmdOpen] = useState(
|
||||
Boolean(cmdOverride) || argsOverride !== defaultArgs || envSummary !== defaultEnvSummary
|
||||
)
|
||||
const [cmdOpen, setCmdOpen] = useState(Boolean(cmdOverride) || permissionMode === 'mixed')
|
||||
|
||||
return (
|
||||
<div className={cn('py-3', !isDetected && 'opacity-70')}>
|
||||
@@ -181,12 +185,12 @@ export function AgentCatalogRow({
|
||||
aria-label={
|
||||
cmdOpen
|
||||
? translate(
|
||||
'auto.components.settings.AgentsPane.cea7d97be1',
|
||||
'Collapse command override'
|
||||
'auto.components.settings.AgentsPane.collapseLaunchSettings',
|
||||
'Collapse launch settings'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.AgentsPane.dc4a2ffdc0',
|
||||
'Expand command override'
|
||||
'auto.components.settings.AgentsPane.expandLaunchSettings',
|
||||
'Expand launch settings'
|
||||
)
|
||||
}
|
||||
className="size-7 text-muted-foreground hover:text-foreground"
|
||||
@@ -202,6 +206,31 @@ export function AgentCatalogRow({
|
||||
|
||||
{isDetected && cmdOpen && (
|
||||
<div className="mt-3 pl-10">
|
||||
{permissionMode && onSetPermissionMode && (
|
||||
<div className="mb-3 space-y-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-xs font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissions',
|
||||
'Agent Permissions'
|
||||
)}
|
||||
</span>
|
||||
<AgentPermissionModeControl
|
||||
mode={permissionMode}
|
||||
onChange={onSetPermissionMode}
|
||||
agent={agentId}
|
||||
/>
|
||||
</div>
|
||||
{permissionMode === 'mixed' && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsEditCustom',
|
||||
'Custom configuration. Edit or reset the launch arguments and environment below to use a permission preset.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<AgentCommandOverrideInput
|
||||
key={cmdOverride ?? defaultCmd}
|
||||
defaultCmd={defaultCmd}
|
||||
|
||||
@@ -101,10 +101,10 @@ export function AgentDefaultArgsInput({
|
||||
event.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
defaultArgs ||
|
||||
translate('auto.components.settings.AgentsPane.6f99bf5dd0', 'No default arguments')
|
||||
}
|
||||
placeholder={translate(
|
||||
'auto.components.settings.AgentsPane.6f99bf5dd0',
|
||||
'No default arguments'
|
||||
)}
|
||||
spellCheck={false}
|
||||
className="h-7 flex-1 font-mono text-xs"
|
||||
/>
|
||||
@@ -175,10 +175,10 @@ export function AgentDefaultEnvInput({
|
||||
event.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
defaultEnvText ||
|
||||
translate('auto.components.settings.AgentsPane.2d133152fa', 'No default environment')
|
||||
}
|
||||
placeholder={translate(
|
||||
'auto.components.settings.AgentsPane.2d133152fa',
|
||||
'No default environment'
|
||||
)}
|
||||
spellCheck={false}
|
||||
aria-invalid={envDraftTooLarge || undefined}
|
||||
aria-describedby={envDraftTooLarge ? envDraftErrorId : undefined}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { AgentPermissionModeControl } from './AgentPermissionsSetting'
|
||||
|
||||
describe('agent permission choices', () => {
|
||||
it('offers Auto only to supported agents', () => {
|
||||
const supported = renderToStaticMarkup(
|
||||
<AgentPermissionModeControl agent="claude" mode="auto" onChange={vi.fn()} />
|
||||
)
|
||||
const unsupported = renderToStaticMarkup(
|
||||
<AgentPermissionModeControl agent="aider" mode="manual" onChange={vi.fn()} />
|
||||
)
|
||||
expect(supported).toContain('>Auto</button>')
|
||||
expect(unsupported).not.toContain('>Auto</button>')
|
||||
expect(unsupported).toContain('>Manual</button>')
|
||||
expect(unsupported).toContain('>Yolo</button>')
|
||||
})
|
||||
|
||||
it('disables presets while custom launch settings need editing', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AgentPermissionModeControl agent="claude" mode="mixed" onChange={vi.fn()} />
|
||||
)
|
||||
expect(html.match(/aria-disabled="true"/g)).toHaveLength(3)
|
||||
expect(html).not.toContain('aria-checked="true"')
|
||||
})
|
||||
|
||||
it('passes an explicitly selected global preset through', () => {
|
||||
const onChange = vi.fn()
|
||||
const element = AgentPermissionModeControl({ mode: 'mixed', onChange })
|
||||
element.props.onChange('auto')
|
||||
expect(onChange).toHaveBeenCalledWith('auto')
|
||||
expect(element.props.options.every((option: { disabled: boolean }) => !option.disabled)).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import {
|
||||
supportsTuiAgentAutoPermissionMode,
|
||||
type AgentPermissionMode
|
||||
} from '../../../../shared/tui-agent-permissions'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { SettingsSegmentedControl, SettingsSubsectionHeader } from './SettingsFormControls'
|
||||
|
||||
export function AgentPermissionModeControl({
|
||||
mode,
|
||||
onChange,
|
||||
agent
|
||||
}: {
|
||||
mode: AgentPermissionMode
|
||||
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
|
||||
agent?: TuiAgent
|
||||
}): React.JSX.Element {
|
||||
const disabled = agent !== undefined && mode === 'mixed'
|
||||
return (
|
||||
<SettingsSegmentedControl<AgentPermissionMode>
|
||||
value={mode}
|
||||
onChange={(next) => {
|
||||
if (next !== 'mixed') {
|
||||
onChange(next)
|
||||
}
|
||||
}}
|
||||
ariaLabel={translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissions',
|
||||
'Agent Permissions'
|
||||
)}
|
||||
size="sm"
|
||||
options={[
|
||||
{
|
||||
value: 'manual',
|
||||
label: translate('auto.components.settings.AgentsPane.agentPermissionsManual', 'Manual'),
|
||||
disabled
|
||||
},
|
||||
...(!agent || supportsTuiAgentAutoPermissionMode(agent)
|
||||
? [
|
||||
{
|
||||
value: 'auto' as const,
|
||||
label: translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsAuto',
|
||||
'Auto'
|
||||
),
|
||||
disabled
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
value: 'yolo',
|
||||
label: translate('auto.components.settings.AgentsPane.agentPermissionsYolo', 'Yolo'),
|
||||
disabled
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentPermissionsSetting({
|
||||
mode,
|
||||
onChange
|
||||
}: {
|
||||
mode: AgentPermissionMode
|
||||
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<SettingsSubsectionHeader
|
||||
title={translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissions',
|
||||
'Agent Permissions'
|
||||
)}
|
||||
className="flex-wrap"
|
||||
action={<AgentPermissionModeControl mode={mode} onChange={onChange} />}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsModesDescription',
|
||||
'Manual asks for approval. Auto uses the agent’s guarded approval mode; agents without Auto use Manual. Yolo skips permission checks.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{mode === 'mixed'
|
||||
? translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsCustom',
|
||||
'Custom configuration. Choose a preset to update standard modes; custom launch arguments and environment values are preserved.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsPreserveCustom',
|
||||
'Applies to all agents. Custom launch arguments and environment values are preserved.'
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -414,22 +414,11 @@ describe('AgentsPane', () => {
|
||||
expect(matchesSettingsSearch('manual', getAgentsPaneSearchEntries())).toBe(true)
|
||||
})
|
||||
|
||||
it('applies the selected agent permission mode from settings without a mixed segment', () => {
|
||||
const onChange = vi.fn()
|
||||
const element = AgentPermissionsSetting({ mode: 'mixed', onChange })
|
||||
const props = element.props.children.props.action.props as {
|
||||
value: 'yolo'
|
||||
onChange: (value: 'yolo' | 'manual' | 'mixed') => void
|
||||
options: { value: string }[]
|
||||
}
|
||||
|
||||
expect(props.value).toBe('yolo')
|
||||
expect(props.options.map((option) => option.value)).toEqual(['yolo', 'manual'])
|
||||
props.onChange('mixed')
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
|
||||
props.onChange('manual')
|
||||
expect(onChange).toHaveBeenCalledWith('manual')
|
||||
it('shows mixed permission settings without selecting a preset', () => {
|
||||
const html = renderToStaticMarkup(<AgentPermissionsSetting mode="mixed" onChange={vi.fn()} />)
|
||||
expect(html).toContain('Custom configuration')
|
||||
expect(html).toContain('>Auto</button>')
|
||||
expect(html).not.toContain('aria-checked="true"')
|
||||
})
|
||||
|
||||
it('keeps catalog agent ids, labels, and commands discoverable in settings search', () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Info } from 'lucide-react'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { getAgentCatalog } from '@/lib/agent-catalog'
|
||||
@@ -14,11 +13,7 @@ import {
|
||||
getAgentGeneratedTabTitlesTitle
|
||||
} from './agent-generated-tab-title-copy'
|
||||
import { getAgentStatusHooksDescription, getAgentStatusHooksTitle } from './agent-status-hooks-copy'
|
||||
import {
|
||||
SettingsSegmentedControl,
|
||||
SettingsSubsectionHeader,
|
||||
SettingsSwitchRow
|
||||
} from './SettingsFormControls'
|
||||
import { SettingsSwitchRow } from './SettingsFormControls'
|
||||
import {
|
||||
isTuiAgentEnabled,
|
||||
normalizeDisabledTuiAgents
|
||||
@@ -31,12 +26,11 @@ import {
|
||||
} from '../../../../shared/tui-agent-launch-defaults'
|
||||
import {
|
||||
applyAgentPermissionMode,
|
||||
applyTuiAgentPermissionMode,
|
||||
resolveAgentPermissionModeSummary,
|
||||
type AgentPermissionMode
|
||||
resolveTuiAgentPermissionMode
|
||||
} from '../../../../shared/tui-agent-permissions'
|
||||
import { getSettingOwnershipSummary } from './setting-ownership'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
|
||||
import { isPairedWebClientWindow } from '@/lib/desktop-window-chrome'
|
||||
import { getAgentsPaneSearchEntries } from './agents-search'
|
||||
import {
|
||||
@@ -44,6 +38,8 @@ import {
|
||||
createAgentAvailabilityUpdateQueue
|
||||
} from './agent-availability-settings'
|
||||
import { AgentAvailabilityControl, type AgentCatalogRowProps } from './AgentCatalogRow'
|
||||
import { AgentPermissionsSetting } from './AgentPermissionsSetting'
|
||||
export { AgentPermissionsSetting } from './AgentPermissionsSetting'
|
||||
import { AgentDefaultSetting } from './AgentDefaultSetting'
|
||||
import { AgentDetectionCatalog } from './AgentDetectionCatalog'
|
||||
|
||||
@@ -65,79 +61,6 @@ type AgentsPaneProps = {
|
||||
|
||||
const enqueueAgentAvailabilityUpdate = createAgentAvailabilityUpdateQueue()
|
||||
|
||||
export function AgentPermissionsSetting({
|
||||
mode,
|
||||
onChange
|
||||
}: {
|
||||
mode: AgentPermissionMode
|
||||
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
|
||||
}): React.JSX.Element {
|
||||
const visibleMode: Exclude<AgentPermissionMode, 'mixed'> = mode === 'manual' ? 'manual' : 'yolo'
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
{translate('auto.components.settings.AgentsPane.agentPermissions', 'Agent Permissions')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsInfo',
|
||||
'Agent permissions info'
|
||||
)}
|
||||
className="grid size-5 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
>
|
||||
<Info className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>
|
||||
{translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsTooltip',
|
||||
"Doesn't apply to agents where you've overridden launch arguments."
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
description={translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsDescription',
|
||||
'Choose whether Orca launches agents with fewer permission prompts or with manual checks.'
|
||||
)}
|
||||
action={
|
||||
<SettingsSegmentedControl<AgentPermissionMode>
|
||||
value={visibleMode}
|
||||
onChange={(nextMode) => {
|
||||
if (nextMode !== 'mixed') {
|
||||
onChange(nextMode)
|
||||
}
|
||||
}}
|
||||
ariaLabel={translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissions',
|
||||
'Agent Permissions'
|
||||
)}
|
||||
size="sm"
|
||||
options={[
|
||||
{
|
||||
value: 'yolo',
|
||||
label: translate('auto.components.settings.AgentsPane.agentPermissionsYolo', 'Yolo')
|
||||
},
|
||||
{
|
||||
value: 'manual',
|
||||
label: translate(
|
||||
'auto.components.settings.AgentsPane.agentPermissionsManual',
|
||||
'Manual'
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentsPane({
|
||||
settings,
|
||||
updateSettings,
|
||||
@@ -212,6 +135,23 @@ export function AgentsPane({
|
||||
cmdOverride: isDetected ? cmdOverrides[agent.id] : undefined,
|
||||
argsOverride: resolveTuiAgentLaunchArgs(agent.id, agentDefaultArgs),
|
||||
envOverride: resolveTuiAgentLaunchEnv(agent.id, agentDefaultEnv),
|
||||
permissionMode: resolveTuiAgentPermissionMode({
|
||||
agent: agent.id,
|
||||
agentArgs: agentDefaultArgs[agent.id],
|
||||
agentEnv: agentDefaultEnv[agent.id]
|
||||
}),
|
||||
onSetPermissionMode: (mode) => {
|
||||
const next = applyTuiAgentPermissionMode({
|
||||
agent: agent.id,
|
||||
mode,
|
||||
agentArgs: resolveTuiAgentLaunchArgs(agent.id, agentDefaultArgs),
|
||||
agentEnv: resolveTuiAgentLaunchEnv(agent.id, agentDefaultEnv)
|
||||
})
|
||||
updateSettings({
|
||||
agentDefaultArgs: { ...agentDefaultArgs, [agent.id]: next.agentArgs },
|
||||
agentDefaultEnv: { ...agentDefaultEnv, [agent.id]: next.agentEnv }
|
||||
})
|
||||
},
|
||||
onSetDefault: isDetected ? () => updateSettings({ defaultTuiAgent: agent.id }) : () => {},
|
||||
onSetEnabled: (enabled) => setAgentEnabled(agent.id, enabled),
|
||||
onSaveOverride: isDetected
|
||||
|
||||
@@ -34,6 +34,7 @@ function buildAgentSettingsKeywords(): string[] {
|
||||
{ key: 'auto.components.settings.agents.search.permission', fallback: 'permission' },
|
||||
{ key: 'auto.components.settings.agents.search.permissions', fallback: 'permissions' },
|
||||
{ key: 'auto.components.settings.agents.search.yolo', fallback: 'yolo', englishOnly: true },
|
||||
{ key: 'auto.components.settings.agents.search.auto', fallback: 'auto' },
|
||||
{ key: 'auto.components.settings.agents.search.manual', fallback: 'manual' },
|
||||
{
|
||||
key: 'auto.components.settings.agents.search.e2b7c0dcd7',
|
||||
@@ -126,7 +127,7 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.agents.search.agentPermissionsDescription',
|
||||
'Switch agent permission defaults between Yolo and Manual.'
|
||||
'Switch agent permission defaults between Manual, Auto, and Yolo.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.agents.search.permission', 'permission'),
|
||||
@@ -135,6 +136,7 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
|
||||
'permissions'
|
||||
),
|
||||
...translateSearchKeyword('auto.components.settings.agents.search.yolo', 'yolo'),
|
||||
...translateSearchKeyword('auto.components.settings.agents.search.auto', 'auto'),
|
||||
...translateSearchKeyword('auto.components.settings.agents.search.manual', 'manual'),
|
||||
...translateSearchKeyword('auto.components.settings.agents.search.skip', 'skip'),
|
||||
...translateSearchKeyword('auto.components.settings.agents.search.checks', 'checks')
|
||||
|
||||
+23
-1
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { YOLO_TUI_AGENT_ARGS } from '../../../../shared/tui-agent-permissions'
|
||||
import { AUTO_TUI_AGENT_ARGS, YOLO_TUI_AGENT_ARGS } from '../../../../shared/tui-agent-permissions'
|
||||
import { createTestStore, makeTab } from '../../store/slices/store-test-helpers'
|
||||
import type { AppState } from '../../store/types'
|
||||
import {
|
||||
@@ -102,6 +102,28 @@ describe('Codex auto-approval status suppression', () => {
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['waiting', 'blocked'] as const)(
|
||||
'preserves Auto %s attention, including user questions',
|
||||
(state) => {
|
||||
registerCodexLaunchConfig({ agentArgs: AUTO_TUI_AGENT_ARGS.codex ?? '', launchToken })
|
||||
for (const toolName of [undefined, 'request_user_input']) {
|
||||
expect(
|
||||
shouldSuppressCodexAutoApprovalStatus(
|
||||
{ state, prompt: 'needs attention', agentType: 'codex', toolName },
|
||||
{ paneKey, tabId: 'tab-1', launchToken }
|
||||
)
|
||||
).toBe(false)
|
||||
}
|
||||
expect(
|
||||
shouldSuppressCodexAutoApprovalSyntheticTitle('Codex - action required', {
|
||||
paneKey,
|
||||
tabId: 'tab-1',
|
||||
launchToken
|
||||
})
|
||||
).toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
it('preserves manual Codex permission attention', () => {
|
||||
registerCodexLaunchConfig({ agentArgs: '', launchToken })
|
||||
|
||||
|
||||
+11
-1
@@ -675,6 +675,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AgentStep": {
|
||||
"yoloPermissionsInfo": "Agent permission info",
|
||||
"yoloPermissionsLabel": "Yolo / Dangerously skip permissions",
|
||||
"yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
@@ -1087,7 +1092,12 @@
|
||||
"9b175d0f5e": "Pre-selected agent when opening a new workspace.",
|
||||
"c8794e622e": "Detected",
|
||||
"db9e9e5887": "Customize command",
|
||||
"df123171d1": "Not installed"
|
||||
"df123171d1": "Not installed",
|
||||
"agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.",
|
||||
"agentPermissionsInfo": "Agent permissions info",
|
||||
"agentPermissionsTooltip": "Doesn't apply to agents where you've overridden launch arguments.",
|
||||
"cea7d97be1": "Collapse command override",
|
||||
"dc4a2ffdc0": "Expand command override"
|
||||
},
|
||||
"AppearancePane": {
|
||||
"0f28e7b30c": "Choose how Orca looks in the app window.",
|
||||
|
||||
@@ -6583,7 +6583,14 @@
|
||||
"codexSessionSourceInfo": "About importing Codex history",
|
||||
"codexSessionSourceTooltip": "Orca runs Codex in an isolated home. Point this at your existing Codex home to import that session history. Empty uses ~/.codex.",
|
||||
"storedDefaultUndetected": "Saved as your default, but not detected right now",
|
||||
"noAgentsDetected": "No agents detected. If one is installed, the probe may have timed out."
|
||||
"noAgentsDetected": "No agents detected. If one is installed, the probe may have timed out.",
|
||||
"agentPermissionsAuto": "Auto",
|
||||
"agentPermissionsModesDescription": "Manual asks for approval. Auto uses the agent’s guarded approval mode; agents without Auto use Manual. Yolo skips permission checks.",
|
||||
"agentPermissionsCustom": "Custom configuration. Choose a preset to update standard modes; custom launch arguments and environment values are preserved.",
|
||||
"agentPermissionsPreserveCustom": "Applies to all agents. Custom launch arguments and environment values are preserved.",
|
||||
"agentPermissionsEditCustom": "Custom configuration. Edit or reset the launch arguments and environment below to use a permission preset.",
|
||||
"collapseLaunchSettings": "Collapse launch settings",
|
||||
"expandLaunchSettings": "Expand launch settings"
|
||||
},
|
||||
"AppIconSelector": {
|
||||
"d5a112dc9b": "Next icon",
|
||||
@@ -8944,7 +8951,7 @@
|
||||
"a79d266f71": "session",
|
||||
"afbf35be68": "stable session",
|
||||
"agentPermissions": "Agent Permissions",
|
||||
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual.",
|
||||
"agentPermissionsDescription": "Switch agent permission defaults between Manual, Auto, and Yolo.",
|
||||
"agentRuntime": "Agent Runtime",
|
||||
"agentRuntimeDescription": "Choose whether agents are detected and launched on Windows or in WSL by default.",
|
||||
"runtime": "runtime",
|
||||
@@ -8955,7 +8962,8 @@
|
||||
"yolo": "yolo",
|
||||
"manual": "manual",
|
||||
"skip": "skip",
|
||||
"checks": "checks"
|
||||
"checks": "checks",
|
||||
"auto": "auto"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
|
||||
@@ -5556,7 +5556,14 @@
|
||||
"03e1a5081a": "on {{value0}}",
|
||||
"25a41a9aad": "Re-detect agents installed on the active server",
|
||||
"remoteDetectionFailed": "Couldn’t detect installed agents. Check the host connection and try again.",
|
||||
"retryDetection": "Retry"
|
||||
"retryDetection": "Retry",
|
||||
"agentPermissionsAuto": "Auto",
|
||||
"agentPermissionsModesDescription": "Manual pide aprobación. Auto usa el modo de aprobación protegido del agente; los agentes sin Auto usan Manual. Yolo omite las comprobaciones de permisos.",
|
||||
"agentPermissionsCustom": "Configuración personalizada. Elige un ajuste para actualizar los modos estándar; se conservan los argumentos de inicio y las variables de entorno personalizados.",
|
||||
"agentPermissionsPreserveCustom": "Se aplica a todos los agentes. Se conservan los argumentos de inicio y las variables de entorno personalizados.",
|
||||
"agentPermissionsEditCustom": "Configuración personalizada. Edita o restablece los argumentos de inicio y el entorno de abajo para usar un ajuste de permisos.",
|
||||
"collapseLaunchSettings": "Contraer ajustes de inicio",
|
||||
"expandLaunchSettings": "Expandir ajustes de inicio"
|
||||
},
|
||||
"AppIconSelector": {
|
||||
"d5a112dc9b": "Icono siguiente",
|
||||
@@ -7788,9 +7795,10 @@
|
||||
"a79d266f71": "sesión",
|
||||
"afbf35be68": "sesión estable",
|
||||
"agentPermissions": "Permisos de agentes",
|
||||
"agentPermissionsDescription": "Cambia los permisos predeterminados de los agentes entre Yolo y Manual.",
|
||||
"agentPermissionsDescription": "Cambia los permisos predeterminados de los agentes entre Manual, Auto y Yolo.",
|
||||
"agentRuntime": "Runtime del agente",
|
||||
"agentRuntimeDescription": "Elige si los agentes se detectan y ejecutan en Windows o en WSL de forma predeterminada."
|
||||
"agentRuntimeDescription": "Elige si los agentes se detectan y ejecutan en Windows o en WSL de forma predeterminada.",
|
||||
"auto": "automático"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
|
||||
@@ -6298,7 +6298,14 @@
|
||||
"codexSessionSourceInfo": "À propos de l'import de l'historique Codex",
|
||||
"codexSessionSourceTooltip": "Orca exécute Codex dans un home isolé. Pointez ceci vers votre home Codex existant pour en importer l'historique de sessions. Si vide, ~/.codex est utilisé.",
|
||||
"storedDefaultUndetected": "Enregistré par défaut, mais non détecté actuellement",
|
||||
"noAgentsDetected": "Aucun agent détecté. Si un agent est installé, la détection a peut-être expiré."
|
||||
"noAgentsDetected": "Aucun agent détecté. Si un agent est installé, la détection a peut-être expiré.",
|
||||
"agentPermissionsAuto": "Auto",
|
||||
"agentPermissionsModesDescription": "Manual demande une approbation. Auto utilise le mode d’approbation protégé de l’agent ; les agents sans Auto utilisent Manual. Yolo ignore les vérifications de permissions.",
|
||||
"agentPermissionsCustom": "Configuration personnalisée. Choisissez un préréglage pour modifier les modes standard ; les arguments de lancement et les valeurs d’environnement personnalisés sont conservés.",
|
||||
"agentPermissionsPreserveCustom": "S’applique à tous les agents. Les arguments de lancement et les valeurs d’environnement personnalisés sont conservés.",
|
||||
"agentPermissionsEditCustom": "Configuration personnalisée. Modifiez ou réinitialisez les arguments de lancement et l’environnement ci-dessous pour utiliser un préréglage de permissions.",
|
||||
"collapseLaunchSettings": "Réduire les paramètres de lancement",
|
||||
"expandLaunchSettings": "Développer les paramètres de lancement"
|
||||
},
|
||||
"AppIconSelector": {
|
||||
"d5a112dc9b": "Icône suivante",
|
||||
@@ -8629,9 +8636,10 @@
|
||||
"a79d266f71": "session",
|
||||
"afbf35be68": "session stable",
|
||||
"agentPermissions": "Permissions des agents",
|
||||
"agentPermissionsDescription": "Basculer les autorisations d'agent par défaut entre Yolo et Manuelle.",
|
||||
"agentPermissionsDescription": "Choisissez les permissions par défaut des agents : Manual, Auto ou Yolo.",
|
||||
"agentRuntime": "Runtime des agents",
|
||||
"agentRuntimeDescription": "Choisissez si les agents sont détectés et lancés par défaut sous Windows ou dans WSL."
|
||||
"agentRuntimeDescription": "Choisissez si les agents sont détectés et lancés par défaut sous Windows ou dans WSL.",
|
||||
"auto": "automatique"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
|
||||
@@ -5541,7 +5541,14 @@
|
||||
"03e1a5081a": "{{value0}} 上",
|
||||
"25a41a9aad": "アクティブなサーバーにインストールされている Agent を再検出",
|
||||
"remoteDetectionFailed": "インストールされている Agent を検出できませんでした。ホストへの接続を確認して、もう一度お試しください。",
|
||||
"retryDetection": "再試行"
|
||||
"retryDetection": "再試行",
|
||||
"agentPermissionsAuto": "Auto",
|
||||
"agentPermissionsModesDescription": "Manual は承認を求めます。Auto はAgentの保護付き承認モードを使用します。Auto 非対応のAgentは Manual を使用します。Yolo は権限チェックをスキップします。",
|
||||
"agentPermissionsCustom": "カスタム設定です。プリセットを選択すると標準モードを更新します。カスタムの起動引数と環境変数の値は保持されます。",
|
||||
"agentPermissionsPreserveCustom": "すべてのAgentに適用します。カスタムの起動引数と環境変数の値は保持されます。",
|
||||
"agentPermissionsEditCustom": "カスタム設定です。権限プリセットを使用するには、以下の起動引数と環境変数を編集またはリセットしてください。",
|
||||
"collapseLaunchSettings": "起動設定を折りたたむ",
|
||||
"expandLaunchSettings": "起動設定を展開"
|
||||
},
|
||||
"AppIconSelector": {
|
||||
"d5a112dc9b": "次へのアイコン",
|
||||
@@ -7810,9 +7817,10 @@
|
||||
"a79d266f71": "セッション",
|
||||
"afbf35be68": "安定したセッション",
|
||||
"agentPermissions": "Agent の権限",
|
||||
"agentPermissionsDescription": "Agent の権限の既定値を Yolo と Manual で切り替えます。",
|
||||
"agentPermissionsDescription": "Agentの既定の権限を Manual、Auto、Yolo から選択します。",
|
||||
"agentRuntime": "Agent のランタイム",
|
||||
"agentRuntimeDescription": "デフォルトで Agent を Windows または WSL のどちらで検出および起動するかを選択します。"
|
||||
"agentRuntimeDescription": "デフォルトで Agent を Windows または WSL のどちらで検出および起動するかを選択します。",
|
||||
"auto": "自動"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
|
||||
@@ -5546,7 +5546,14 @@
|
||||
"03e1a5081a": "on {{value0}}",
|
||||
"25a41a9aad": "Re-detect agents installed on the active server",
|
||||
"remoteDetectionFailed": "Couldn’t detect installed agents. Check the host connection and try again.",
|
||||
"retryDetection": "Retry"
|
||||
"retryDetection": "Retry",
|
||||
"agentPermissionsAuto": "Auto",
|
||||
"agentPermissionsModesDescription": "Manual은 승인을 요청합니다. Auto는 에이전트의 보호된 승인 모드를 사용하며, Auto를 지원하지 않는 에이전트는 Manual을 사용합니다. Yolo는 권한 검사를 건너뜁니다.",
|
||||
"agentPermissionsCustom": "사용자 지정 설정입니다. 프리셋을 선택하면 표준 모드가 변경되며, 사용자 지정 실행 인수와 환경 변수 값은 유지됩니다.",
|
||||
"agentPermissionsPreserveCustom": "모든 에이전트에 적용됩니다. 사용자 지정 실행 인수와 환경 변수 값은 유지됩니다.",
|
||||
"agentPermissionsEditCustom": "사용자 지정 설정입니다. 권한 프리셋을 사용하려면 아래 실행 인수와 환경 변수를 편집하거나 초기화하세요.",
|
||||
"collapseLaunchSettings": "실행 설정 접기",
|
||||
"expandLaunchSettings": "실행 설정 펼치기"
|
||||
},
|
||||
"AppIconSelector": {
|
||||
"d5a112dc9b": "다음 아이콘",
|
||||
@@ -7778,9 +7785,10 @@
|
||||
"a79d266f71": "세션",
|
||||
"afbf35be68": "안정적인 세션",
|
||||
"agentPermissions": "Agent 권한",
|
||||
"agentPermissionsDescription": "agent 권한 기본값을 Yolo와 수동 사이에서 전환합니다.",
|
||||
"agentPermissionsDescription": "agent의 기본 권한을 Manual, Auto, Yolo 중에서 선택합니다.",
|
||||
"agentRuntime": "Agent 런타임",
|
||||
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다."
|
||||
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다.",
|
||||
"auto": "자동"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
|
||||
@@ -5607,7 +5607,14 @@
|
||||
"03e1a5081a": "on {{value0}}",
|
||||
"25a41a9aad": "重新检测活动服务器上安装的智能体",
|
||||
"remoteDetectionFailed": "无法检测已安装的智能体。请检查主机连接后重试。",
|
||||
"retryDetection": "Retry"
|
||||
"retryDetection": "Retry",
|
||||
"agentPermissionsAuto": "Auto",
|
||||
"agentPermissionsModesDescription": "Manual 会请求批准。Auto 使用智能体带保护机制的审批模式;不支持 Auto 的智能体使用 Manual。Yolo 会跳过权限检查。",
|
||||
"agentPermissionsCustom": "自定义配置。选择预设以更新标准模式;自定义启动参数和环境变量值会保留。",
|
||||
"agentPermissionsPreserveCustom": "应用于所有智能体。自定义启动参数和环境变量值会保留。",
|
||||
"agentPermissionsEditCustom": "自定义配置。编辑或重置下方的启动参数和环境变量以使用权限预设。",
|
||||
"collapseLaunchSettings": "收起启动设置",
|
||||
"expandLaunchSettings": "展开启动设置"
|
||||
},
|
||||
"AppIconSelector": {
|
||||
"d5a112dc9b": "下一个图标",
|
||||
@@ -7843,9 +7850,10 @@
|
||||
"a79d266f71": "会话",
|
||||
"afbf35be68": "稳定会话",
|
||||
"agentPermissions": "智能体权限",
|
||||
"agentPermissionsDescription": "在 Yolo 和手动之间切换智能体权限默认值。",
|
||||
"agentPermissionsDescription": "在 Manual、Auto 和 Yolo 之间切换智能体的默认权限。",
|
||||
"agentRuntime": "智能体运行时",
|
||||
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。"
|
||||
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。",
|
||||
"auto": "自动"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
|
||||
@@ -173,6 +173,8 @@ export const GITLAB_READY_FOR_REVIEW_RUNTIME_CAPABILITY =
|
||||
'gitlab.updateMR.readyForReview.v1' as const
|
||||
export const GITLAB_READY_FOR_REVIEW_UPDATE_REQUIRED_MESSAGE =
|
||||
'Marking a merge request ready requires a newer Orca server. Update the server and try again.'
|
||||
// Older clients cannot round-trip Auto permission presets safely.
|
||||
export const AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY = 'settings.agent-permission-auto.v1' as const
|
||||
export const WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY =
|
||||
'worktree.visibility-defaults.v1' as const
|
||||
export const WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY =
|
||||
@@ -194,6 +196,7 @@ export const AUTOMATION_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY =
|
||||
export const NATIVE_REMOTE_RUNTIME_CLIENT_CAPABILITIES = [
|
||||
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
|
||||
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
|
||||
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
|
||||
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
|
||||
WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY,
|
||||
@@ -266,6 +269,7 @@ export const RUNTIME_CAPABILITIES = [
|
||||
FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY,
|
||||
GITHUB_MARK_PR_READY_RUNTIME_CAPABILITY,
|
||||
GITLAB_READY_FOR_REVIEW_RUNTIME_CAPABILITY,
|
||||
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
|
||||
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
|
||||
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
|
||||
ACCOUNT_IMPORT_RUNTIME_CAPABILITY,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
|
||||
AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY,
|
||||
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
|
||||
@@ -18,6 +19,7 @@ export function remoteRuntimeClientCapabilities(
|
||||
new Set([
|
||||
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
|
||||
SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY,
|
||||
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
|
||||
SKILL_INSTALL_RESULT_V2_CAPABILITY,
|
||||
WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { applyAgentPermissionMode, AUTO_TUI_AGENT_ARGS } from './tui-agent-permissions'
|
||||
import {
|
||||
normalizeTuiAgentArgsRecord,
|
||||
normalizeTuiAgentEnvRecord,
|
||||
resolveTuiAgentLaunchArgs,
|
||||
resolveTuiAgentLaunchEnv
|
||||
} from './tui-agent-launch-defaults'
|
||||
import { buildAgentStartupPlan } from './tui-agent-startup'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
import { tokenizeStartupCommand } from './tui-agent-startup-shell'
|
||||
|
||||
describe('Auto permission launch settings', () => {
|
||||
it.each(['darwin', 'linux', 'win32'] as const)(
|
||||
'retains guarded presets through normalization and local/remote %s launch planning',
|
||||
(platform) => {
|
||||
const profile = applyAgentPermissionMode({ mode: 'auto' })
|
||||
const args = normalizeTuiAgentArgsRecord(profile.agentDefaultArgs)
|
||||
const env = normalizeTuiAgentEnvRecord(profile.agentDefaultEnv)
|
||||
for (const isRemote of [false, true]) {
|
||||
for (const agent of [...Object.keys(AUTO_TUI_AGENT_ARGS), 'goose', 'amp'] as TuiAgent[]) {
|
||||
const agentArgs = resolveTuiAgentLaunchArgs(agent, args)
|
||||
const agentEnv = resolveTuiAgentLaunchEnv(agent, env)
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent,
|
||||
agentArgs,
|
||||
agentEnv,
|
||||
platform,
|
||||
isRemote,
|
||||
cmdOverrides: {},
|
||||
prompt: '',
|
||||
allowEmptyPromptLaunch: true
|
||||
})
|
||||
expect(plan).not.toBeNull()
|
||||
if (agent in AUTO_TUI_AGENT_ARGS) {
|
||||
const parsed = tokenizeStartupCommand(
|
||||
plan?.launchCommand ?? '',
|
||||
platform === 'win32' ? 'powershell' : 'posix'
|
||||
)
|
||||
expect(parsed.ok).toBe(true)
|
||||
if (parsed.ok) {
|
||||
const flags = AUTO_TUI_AGENT_ARGS[agent]?.split(' ') ?? []
|
||||
expect(parsed.tokens.slice(-flags.length)).toEqual(flags)
|
||||
}
|
||||
}
|
||||
if (agent === 'goose') {
|
||||
expect(plan?.env).toEqual({ GOOSE_MODE: 'smart_approve' })
|
||||
}
|
||||
if (agent === 'amp') {
|
||||
expect(agentArgs).toBe('')
|
||||
expect(plan?.launchCommand).not.toContain('--dangerously-allow-all')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
applyAgentPermissionMode,
|
||||
applyTuiAgentPermissionMode,
|
||||
AUTO_TUI_AGENT_ARGS,
|
||||
AUTO_TUI_AGENT_ENV,
|
||||
supportsTuiAgentAutoPermissionMode,
|
||||
resolveAgentPermissionModeSummary,
|
||||
resolveTuiAgentPermissionMode,
|
||||
YOLO_TUI_AGENT_ARGS,
|
||||
@@ -86,3 +90,85 @@ describe('tui agent permissions', () => {
|
||||
).toBe('yolo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('intermediate permission presets', () => {
|
||||
it('round trips all presets including manual fallbacks from an Auto profile', () => {
|
||||
let profile = applyAgentPermissionMode({ mode: 'yolo' })
|
||||
for (const mode of ['auto', 'manual', 'yolo', 'auto'] as const) {
|
||||
profile = applyAgentPermissionMode({ ...profile, mode })
|
||||
expect(resolveAgentPermissionModeSummary(profile)).toBe(mode)
|
||||
for (const agent of Object.keys(
|
||||
YOLO_TUI_AGENT_ARGS
|
||||
) as (keyof typeof YOLO_TUI_AGENT_ARGS)[]) {
|
||||
expect(
|
||||
resolveTuiAgentPermissionMode({ agent, agentArgs: profile.agentDefaultArgs[agent] })
|
||||
).toBe(mode === 'auto' && !supportsTuiAgentAutoPermissionMode(agent) ? 'manual' : mode)
|
||||
}
|
||||
expect(
|
||||
resolveTuiAgentPermissionMode({ agent: 'goose', agentEnv: profile.agentDefaultEnv.goose })
|
||||
).toBe(mode)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps unknown and custom settings when applying a global preset', () => {
|
||||
const profile = applyAgentPermissionMode({
|
||||
mode: 'auto',
|
||||
agentDefaultArgs: {
|
||||
claude: '--model custom',
|
||||
opencode: '--port 1234',
|
||||
codex: ' --approve-for-me '
|
||||
},
|
||||
agentDefaultEnv: {
|
||||
goose: { GOOSE_MODE: 'approve', CUSTOM: 'value' },
|
||||
claude: { CUSTOM: 'value' }
|
||||
}
|
||||
})
|
||||
expect(profile.agentDefaultArgs.claude).toBe('--model custom')
|
||||
expect(profile.agentDefaultArgs.codex).toBe(' --approve-for-me ')
|
||||
expect(profile.agentDefaultArgs.opencode).toBe('--port 1234')
|
||||
expect(profile.agentDefaultEnv.goose).toEqual({ GOOSE_MODE: 'approve', CUSTOM: 'value' })
|
||||
expect(profile.agentDefaultEnv.claude).toEqual({ CUSTOM: 'value' })
|
||||
expect(resolveAgentPermissionModeSummary(profile)).toBe('mixed')
|
||||
})
|
||||
|
||||
it('reports custom per-agent choices as mixed rather than misleadingly showing Auto or Yolo', () => {
|
||||
const profile = applyAgentPermissionMode({ mode: 'auto' })
|
||||
expect(
|
||||
resolveAgentPermissionModeSummary({
|
||||
...profile,
|
||||
agentDefaultArgs: { ...profile.agentDefaultArgs, codex: '' }
|
||||
})
|
||||
).toBe('mixed')
|
||||
expect(
|
||||
resolveAgentPermissionModeSummary({
|
||||
...profile,
|
||||
agentDefaultArgs: { ...profile.agentDefaultArgs, amp: YOLO_TUI_AGENT_ARGS.amp }
|
||||
})
|
||||
).toBe('mixed')
|
||||
})
|
||||
|
||||
it('uses the existing args and env launch fields for per-agent choices', () => {
|
||||
expect(
|
||||
applyTuiAgentPermissionMode({
|
||||
agent: 'claude',
|
||||
mode: 'auto',
|
||||
agentArgs: YOLO_TUI_AGENT_ARGS.claude,
|
||||
agentEnv: { CUSTOM: 'value' }
|
||||
})
|
||||
).toEqual({ agentArgs: AUTO_TUI_AGENT_ARGS.claude, agentEnv: { CUSTOM: 'value' } })
|
||||
expect(
|
||||
applyTuiAgentPermissionMode({
|
||||
agent: 'goose',
|
||||
mode: 'auto',
|
||||
agentEnv: YOLO_TUI_AGENT_ENV.goose
|
||||
})
|
||||
).toEqual({ agentArgs: '', agentEnv: AUTO_TUI_AGENT_ENV.goose })
|
||||
expect(
|
||||
applyTuiAgentPermissionMode({
|
||||
agent: 'amp',
|
||||
mode: 'auto',
|
||||
agentArgs: YOLO_TUI_AGENT_ARGS.amp
|
||||
})
|
||||
).toEqual({ agentArgs: '', agentEnv: {} })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
|
||||
export type AgentPermissionMode = 'yolo' | 'manual' | 'mixed'
|
||||
export type AgentPermissionMode = 'yolo' | 'auto' | 'manual' | 'mixed'
|
||||
|
||||
export const YOLO_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
|
||||
claude: '--dangerously-skip-permissions',
|
||||
@@ -36,12 +36,27 @@ export const YOLO_TUI_AGENT_ENV: Partial<Record<TuiAgent, Record<string, string>
|
||||
goose: { GOOSE_MODE: 'auto' }
|
||||
}
|
||||
|
||||
// Only documented intermediate CLI presets belong here; unsupported agents use Manual.
|
||||
export const AUTO_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
|
||||
claude: '--permission-mode auto',
|
||||
'claude-agent-teams': '--permission-mode auto',
|
||||
codex: '--approve-for-me',
|
||||
gemini: '--approval-mode auto_edit',
|
||||
'qwen-code': '--approval-mode auto',
|
||||
devin: '--permission-mode smart',
|
||||
droid: '--auto medium'
|
||||
}
|
||||
|
||||
export const AUTO_TUI_AGENT_ENV: Partial<Record<TuiAgent, Record<string, string>>> = {
|
||||
goose: { GOOSE_MODE: 'smart_approve' }
|
||||
}
|
||||
|
||||
const PERMISSION_AGENT_IDS = Object.keys(TUI_AGENT_CONFIG).filter(
|
||||
(agent): agent is TuiAgent => agent in YOLO_TUI_AGENT_ARGS || agent in YOLO_TUI_AGENT_ENV
|
||||
)
|
||||
|
||||
function normalizeArgs(value: string | null | undefined): string {
|
||||
return value?.trim() ?? ''
|
||||
export function supportsTuiAgentAutoPermissionMode(agent: TuiAgent): boolean {
|
||||
return agent in AUTO_TUI_AGENT_ARGS || agent in AUTO_TUI_AGENT_ENV
|
||||
}
|
||||
|
||||
function sameEnv(
|
||||
@@ -49,49 +64,15 @@ function sameEnv(
|
||||
right: Record<string, string> | null | undefined
|
||||
): boolean {
|
||||
const leftEntries = Object.entries(left ?? {})
|
||||
const rightEntries = Object.entries(right ?? {})
|
||||
if (leftEntries.length !== rightEntries.length) {
|
||||
return false
|
||||
}
|
||||
return leftEntries.every(([name, value]) => right?.[name] === value)
|
||||
}
|
||||
|
||||
function resolveAgentPermissionMode(args: string, yoloArgs: string): AgentPermissionMode {
|
||||
if (!args) {
|
||||
return 'manual'
|
||||
}
|
||||
return args === yoloArgs ? 'yolo' : 'mixed'
|
||||
}
|
||||
|
||||
function resolveAgentEnvPermissionMode(
|
||||
env: Record<string, string> | null | undefined,
|
||||
yoloEnv: Record<string, string> | undefined
|
||||
): AgentPermissionMode {
|
||||
if (sameEnv(env, {})) {
|
||||
return 'manual'
|
||||
}
|
||||
return sameEnv(env, yoloEnv) ? 'yolo' : 'mixed'
|
||||
return (
|
||||
leftEntries.length === Object.keys(right ?? {}).length &&
|
||||
leftEntries.every(([name, value]) => right?.[name] === value)
|
||||
)
|
||||
}
|
||||
|
||||
function combinePermissionModes(modes: AgentPermissionMode[]): AgentPermissionMode {
|
||||
let sawYolo = false
|
||||
let sawManual = false
|
||||
let sawMixed = false
|
||||
|
||||
for (const mode of modes) {
|
||||
if (mode === 'yolo') {
|
||||
sawYolo = true
|
||||
} else if (mode === 'manual') {
|
||||
sawManual = true
|
||||
} else {
|
||||
sawMixed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (sawMixed || (sawYolo && sawManual)) {
|
||||
return 'mixed'
|
||||
}
|
||||
return sawYolo ? 'yolo' : 'manual'
|
||||
const distinct = new Set(modes)
|
||||
return distinct.size > 1 ? 'mixed' : (modes[0] ?? 'manual')
|
||||
}
|
||||
|
||||
export function resolveTuiAgentPermissionMode(args: {
|
||||
@@ -101,17 +82,28 @@ export function resolveTuiAgentPermissionMode(args: {
|
||||
}): AgentPermissionMode {
|
||||
const modes: AgentPermissionMode[] = []
|
||||
if (args.agent in YOLO_TUI_AGENT_ARGS) {
|
||||
const value = args.agentArgs ?? ''
|
||||
modes.push(
|
||||
resolveAgentPermissionMode(
|
||||
normalizeArgs(args.agentArgs),
|
||||
YOLO_TUI_AGENT_ARGS[args.agent] ?? ''
|
||||
)
|
||||
!value
|
||||
? 'manual'
|
||||
: value === YOLO_TUI_AGENT_ARGS[args.agent]
|
||||
? 'yolo'
|
||||
: value === AUTO_TUI_AGENT_ARGS[args.agent]
|
||||
? 'auto'
|
||||
: 'mixed'
|
||||
)
|
||||
}
|
||||
if (args.agent in YOLO_TUI_AGENT_ENV) {
|
||||
modes.push(resolveAgentEnvPermissionMode(args.agentEnv, YOLO_TUI_AGENT_ENV[args.agent]))
|
||||
modes.push(
|
||||
sameEnv(args.agentEnv, {})
|
||||
? 'manual'
|
||||
: sameEnv(args.agentEnv, YOLO_TUI_AGENT_ENV[args.agent])
|
||||
? 'yolo'
|
||||
: AUTO_TUI_AGENT_ENV[args.agent] && sameEnv(args.agentEnv, AUTO_TUI_AGENT_ENV[args.agent])
|
||||
? 'auto'
|
||||
: 'mixed'
|
||||
)
|
||||
}
|
||||
|
||||
return combinePermissionModes(modes)
|
||||
}
|
||||
|
||||
@@ -119,19 +111,62 @@ export function resolveAgentPermissionModeSummary(args: {
|
||||
agentDefaultArgs?: Partial<Record<TuiAgent, string>> | null
|
||||
agentDefaultEnv?: Partial<Record<TuiAgent, Record<string, string>>> | null
|
||||
}): AgentPermissionMode {
|
||||
const modes: AgentPermissionMode[] = []
|
||||
|
||||
for (const agent of PERMISSION_AGENT_IDS) {
|
||||
modes.push(
|
||||
resolveTuiAgentPermissionMode({
|
||||
agent,
|
||||
agentArgs: args.agentDefaultArgs?.[agent],
|
||||
agentEnv: args.agentDefaultEnv?.[agent]
|
||||
})
|
||||
const modes = PERMISSION_AGENT_IDS.map((agent) => ({
|
||||
agent,
|
||||
mode: resolveTuiAgentPermissionMode({
|
||||
agent,
|
||||
agentArgs: args.agentDefaultArgs?.[agent],
|
||||
agentEnv: args.agentDefaultEnv?.[agent]
|
||||
})
|
||||
}))
|
||||
if (
|
||||
modes.some(({ mode }) => mode === 'auto') &&
|
||||
modes.every(
|
||||
({ agent, mode }) =>
|
||||
mode === 'auto' || (mode === 'manual' && !supportsTuiAgentAutoPermissionMode(agent))
|
||||
)
|
||||
) {
|
||||
return 'auto'
|
||||
}
|
||||
return combinePermissionModes(modes.map(({ mode }) => mode))
|
||||
}
|
||||
|
||||
return combinePermissionModes(modes)
|
||||
export function applyTuiAgentPermissionMode(args: {
|
||||
agent: TuiAgent
|
||||
mode: Exclude<AgentPermissionMode, 'mixed'>
|
||||
agentArgs?: string | null
|
||||
agentEnv?: Record<string, string> | null
|
||||
}): { agentArgs: string; agentEnv: Record<string, string> } {
|
||||
let agentArgs = args.agentArgs ?? ''
|
||||
let agentEnv = { ...args.agentEnv }
|
||||
if (
|
||||
args.agent in YOLO_TUI_AGENT_ARGS &&
|
||||
(!agentArgs ||
|
||||
agentArgs === YOLO_TUI_AGENT_ARGS[args.agent] ||
|
||||
agentArgs === AUTO_TUI_AGENT_ARGS[args.agent])
|
||||
) {
|
||||
agentArgs =
|
||||
(args.mode === 'yolo'
|
||||
? YOLO_TUI_AGENT_ARGS[args.agent]
|
||||
: args.mode === 'auto'
|
||||
? AUTO_TUI_AGENT_ARGS[args.agent]
|
||||
: '') ?? ''
|
||||
}
|
||||
if (
|
||||
args.agent in YOLO_TUI_AGENT_ENV &&
|
||||
(sameEnv(agentEnv, {}) ||
|
||||
sameEnv(agentEnv, YOLO_TUI_AGENT_ENV[args.agent]) ||
|
||||
(AUTO_TUI_AGENT_ENV[args.agent] && sameEnv(agentEnv, AUTO_TUI_AGENT_ENV[args.agent])))
|
||||
) {
|
||||
agentEnv = {
|
||||
...(args.mode === 'yolo'
|
||||
? YOLO_TUI_AGENT_ENV[args.agent]
|
||||
: args.mode === 'auto'
|
||||
? AUTO_TUI_AGENT_ENV[args.agent]
|
||||
: {})
|
||||
}
|
||||
}
|
||||
return { agentArgs, agentEnv }
|
||||
}
|
||||
|
||||
export function applyAgentPermissionMode(args: {
|
||||
@@ -144,24 +179,19 @@ export function applyAgentPermissionMode(args: {
|
||||
} {
|
||||
const nextArgs = { ...args.agentDefaultArgs }
|
||||
const nextEnv = { ...args.agentDefaultEnv }
|
||||
|
||||
for (const agent of PERMISSION_AGENT_IDS) {
|
||||
const result = applyTuiAgentPermissionMode({
|
||||
agent,
|
||||
mode: args.mode,
|
||||
agentArgs: nextArgs[agent],
|
||||
agentEnv: nextEnv[agent]
|
||||
})
|
||||
if (agent in YOLO_TUI_AGENT_ARGS) {
|
||||
const yoloArgs = YOLO_TUI_AGENT_ARGS[agent] ?? ''
|
||||
const currentArgs = normalizeArgs(nextArgs[agent])
|
||||
if (!currentArgs || currentArgs === yoloArgs) {
|
||||
nextArgs[agent] = args.mode === 'yolo' ? yoloArgs : ''
|
||||
}
|
||||
nextArgs[agent] = result.agentArgs
|
||||
}
|
||||
|
||||
if (agent in YOLO_TUI_AGENT_ENV) {
|
||||
const yoloEnv = YOLO_TUI_AGENT_ENV[agent]
|
||||
const currentEnv = nextEnv[agent]
|
||||
if (sameEnv(currentEnv, {}) || sameEnv(currentEnv, yoloEnv)) {
|
||||
nextEnv[agent] = args.mode === 'yolo' ? { ...yoloEnv } : {}
|
||||
}
|
||||
nextEnv[agent] = result.agentEnv
|
||||
}
|
||||
}
|
||||
|
||||
return { agentDefaultArgs: nextArgs, agentDefaultEnv: nextEnv }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user