Resolve mobile new-worktree agent before commit (#3238)
This commit is contained in:
parent
f2fa3ee904
commit
7366c3d32e
|
|
@ -17,7 +17,6 @@ import { BottomDrawer } from './BottomDrawer'
|
|||
import { MobileAgentIcon } from './MobileAgentIcon'
|
||||
import { getSuggestedCreatureName } from './worktree-name-suggestion'
|
||||
import { deriveWorkspaceSshGate, workspaceSshStatusLabel } from '../tasks/workspace-ssh-gate'
|
||||
import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog'
|
||||
import { WORKTREE_CREATE_TIMEOUT_MS } from '../tasks/workspace-create-timeout'
|
||||
import {
|
||||
isSetupHookTrusted,
|
||||
|
|
@ -27,13 +26,19 @@ import {
|
|||
type SetupHookTrust
|
||||
} from '../tasks/setup-hook-trust'
|
||||
import {
|
||||
filterEnabledMobileTuiAgents,
|
||||
isMobileTuiAgent,
|
||||
isMobileTuiAgentEnabled,
|
||||
MOBILE_TUI_AGENT_LAUNCH_COMMANDS
|
||||
} from '../tasks/mobile-tui-agents'
|
||||
import type { PersistedTrustedOrcaHooks, TuiAgent } from '../../../src/shared/types'
|
||||
import type { SshConnectionState } from '../../../src/shared/ssh-types'
|
||||
import {
|
||||
NEW_WORKTREE_AGENT_OPTIONS as AGENT_OPTIONS,
|
||||
NEW_WORKTREE_BLANK_AGENT as BLANK_TERMINAL,
|
||||
pickPreferredNewWorktreeAgent,
|
||||
resolveNewWorktreeAgentSelection,
|
||||
type NewWorktreeAgentOption as AgentOption
|
||||
} from './new-worktree-agent-selection'
|
||||
|
||||
type Repo = {
|
||||
id: string
|
||||
|
|
@ -71,51 +76,6 @@ type SetupTrustPrompt = {
|
|||
previouslyApproved: boolean
|
||||
}
|
||||
|
||||
type AgentOption = {
|
||||
id: TuiAgent | '__blank__'
|
||||
label: string
|
||||
faviconDomain?: string
|
||||
}
|
||||
|
||||
const AGENT_OPTIONS: AgentOption[] = MOBILE_AGENT_CATALOG
|
||||
|
||||
const BLANK_TERMINAL: AgentOption = { id: '__blank__', label: 'Blank Terminal' }
|
||||
|
||||
function agentOptionFor(id: string | null | undefined): AgentOption | null {
|
||||
if (!id) return null
|
||||
if (id === 'blank' || id === '__blank__') return BLANK_TERMINAL
|
||||
return AGENT_OPTIONS.find((agent) => agent.id === id) ?? null
|
||||
}
|
||||
|
||||
function pickPreferredAgent(
|
||||
settings: RuntimeSettings | null,
|
||||
detectedAgentIds: Set<string> | null
|
||||
): AgentOption {
|
||||
const preferred = agentOptionFor(settings?.defaultTuiAgent)
|
||||
if (preferred?.id === '__blank__') {
|
||||
return preferred
|
||||
}
|
||||
if (
|
||||
preferred &&
|
||||
isMobileTuiAgent(preferred.id) &&
|
||||
isMobileTuiAgentEnabled(preferred.id, settings?.disabledTuiAgents) &&
|
||||
(detectedAgentIds === null || detectedAgentIds.has(preferred.id))
|
||||
) {
|
||||
return preferred
|
||||
}
|
||||
const enabledAgents = filterEnabledMobileTuiAgents(
|
||||
MOBILE_AGENT_CATALOG.map((agent) => agent.id),
|
||||
settings?.disabledTuiAgents
|
||||
)
|
||||
const detectedOption = AGENT_OPTIONS.find(
|
||||
(agent) =>
|
||||
agent.id !== '__blank__' &&
|
||||
enabledAgents.includes(agent.id) &&
|
||||
(detectedAgentIds === null || detectedAgentIds.has(agent.id))
|
||||
)
|
||||
return detectedOption ?? BLANK_TERMINAL
|
||||
}
|
||||
|
||||
function repoColor(name: string): string {
|
||||
const palette = ['#f97316', '#8b5cf6', '#06b6d4', '#ec4899', '#84cc16', '#f59e0b', '#6366f1']
|
||||
let hash = 0
|
||||
|
|
@ -209,10 +169,10 @@ export function NewWorktreeModal({
|
|||
const [repos, setRepos] = useState<Repo[]>([])
|
||||
const [selectedRepo, setSelectedRepo] = useState<Repo | null>(null)
|
||||
const [showRepoPicker, setShowRepoPicker] = useState(false)
|
||||
const [selectedAgent, setSelectedAgent] = useState<AgentOption>(AGENT_OPTIONS[0]!)
|
||||
const [selectedAgentState, setSelectedAgent] = useState<AgentOption>(AGENT_OPTIONS[0]!)
|
||||
const [runtimeSettings, setRuntimeSettings] = useState<RuntimeSettings | null>(null)
|
||||
const [detectedAgentIds, setDetectedAgentIds] = useState<Set<string> | null>(null)
|
||||
const [agentOverridden, setAgentOverridden] = useState(false)
|
||||
const [agentOverriddenState, setAgentOverridden] = useState(false)
|
||||
const [showAgentPicker, setShowAgentPicker] = useState(false)
|
||||
const [sshState, setSshState] = useState<SshConnectionState | null>(null)
|
||||
const [sshConnecting, setSshConnecting] = useState(false)
|
||||
|
|
@ -246,6 +206,23 @@ export function NewWorktreeModal({
|
|||
state: sshState,
|
||||
connecting: sshConnecting
|
||||
})
|
||||
const selectedAgentResolution = resolveNewWorktreeAgentSelection({
|
||||
visible,
|
||||
selectedAgent: selectedAgentState,
|
||||
agentOverridden: agentOverriddenState,
|
||||
runtimeSettings,
|
||||
detectedAgentIds
|
||||
})
|
||||
// Why: agent preference repair is pure render dataflow; doing it here
|
||||
// avoids a stale selected-agent commit while preserving user overrides.
|
||||
if (
|
||||
selectedAgentState.id !== selectedAgentResolution.selectedAgent.id ||
|
||||
agentOverriddenState !== selectedAgentResolution.agentOverridden
|
||||
) {
|
||||
setSelectedAgent(selectedAgentResolution.selectedAgent)
|
||||
setAgentOverridden(selectedAgentResolution.agentOverridden)
|
||||
}
|
||||
const selectedAgent = selectedAgentResolution.selectedAgent
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
|
|
@ -383,23 +360,6 @@ export function NewWorktreeModal({
|
|||
}
|
||||
}, [client, selectedRepoConnectionId, sshGate.status, visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || agentOverridden) return
|
||||
setSelectedAgent(pickPreferredAgent(runtimeSettings, detectedAgentIds))
|
||||
}, [agentOverridden, detectedAgentIds, runtimeSettings, visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || detectedAgentIds === null || selectedAgent.id === '__blank__') return
|
||||
if (
|
||||
detectedAgentIds.has(selectedAgent.id) &&
|
||||
isMobileTuiAgentEnabled(selectedAgent.id, runtimeSettings?.disabledTuiAgents)
|
||||
) {
|
||||
return
|
||||
}
|
||||
setSelectedAgent(pickPreferredAgent(runtimeSettings, detectedAgentIds))
|
||||
setAgentOverridden(false)
|
||||
}, [detectedAgentIds, runtimeSettings, selectedAgent.id, visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!client || !selectedRepo) {
|
||||
setSetupCommand(null)
|
||||
|
|
@ -526,7 +486,7 @@ export function NewWorktreeModal({
|
|||
selectedAgent.id !== '__blank__' &&
|
||||
!isMobileTuiAgentEnabled(selectedAgent.id, latestRuntimeSettings?.disabledTuiAgents)
|
||||
) {
|
||||
setSelectedAgent(pickPreferredAgent(latestRuntimeSettings, detectedAgentIds))
|
||||
setSelectedAgent(pickPreferredNewWorktreeAgent(latestRuntimeSettings, detectedAgentIds))
|
||||
setAgentOverridden(false)
|
||||
setError('Selected agent is disabled. Choose an enabled agent before creating.')
|
||||
return
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
NEW_WORKTREE_BLANK_AGENT,
|
||||
newWorktreeAgentOptionFor,
|
||||
pickPreferredNewWorktreeAgent,
|
||||
resolveNewWorktreeAgentSelection
|
||||
} from './new-worktree-agent-selection'
|
||||
|
||||
describe('new worktree agent selection', () => {
|
||||
it('picks the preferred detected agent when there is no user override', () => {
|
||||
const selected = newWorktreeAgentOptionFor('claude')
|
||||
const resolved = resolveNewWorktreeAgentSelection({
|
||||
visible: true,
|
||||
selectedAgent: selected,
|
||||
agentOverridden: false,
|
||||
runtimeSettings: { defaultTuiAgent: 'codex' },
|
||||
detectedAgentIds: new Set(['claude', 'codex'])
|
||||
})
|
||||
|
||||
expect(resolved).toEqual({
|
||||
selectedAgent: newWorktreeAgentOptionFor('codex'),
|
||||
agentOverridden: false
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps an available user override', () => {
|
||||
const selected = newWorktreeAgentOptionFor('codex')
|
||||
const resolved = resolveNewWorktreeAgentSelection({
|
||||
visible: true,
|
||||
selectedAgent: selected,
|
||||
agentOverridden: true,
|
||||
runtimeSettings: { defaultTuiAgent: 'claude' },
|
||||
detectedAgentIds: new Set(['claude', 'codex'])
|
||||
})
|
||||
|
||||
expect(resolved).toEqual({ selectedAgent: selected, agentOverridden: true })
|
||||
})
|
||||
|
||||
it('clears an unavailable user override after detection completes', () => {
|
||||
const resolved = resolveNewWorktreeAgentSelection({
|
||||
visible: true,
|
||||
selectedAgent: newWorktreeAgentOptionFor('codex'),
|
||||
agentOverridden: true,
|
||||
runtimeSettings: { defaultTuiAgent: 'claude' },
|
||||
detectedAgentIds: new Set(['claude'])
|
||||
})
|
||||
|
||||
expect(resolved).toEqual({
|
||||
selectedAgent: newWorktreeAgentOptionFor('claude'),
|
||||
agentOverridden: false
|
||||
})
|
||||
})
|
||||
|
||||
it('clears a disabled user override after detection completes', () => {
|
||||
const resolved = resolveNewWorktreeAgentSelection({
|
||||
visible: true,
|
||||
selectedAgent: newWorktreeAgentOptionFor('codex'),
|
||||
agentOverridden: true,
|
||||
runtimeSettings: { defaultTuiAgent: 'claude', disabledTuiAgents: ['codex'] },
|
||||
detectedAgentIds: new Set(['claude', 'codex'])
|
||||
})
|
||||
|
||||
expect(resolved).toEqual({
|
||||
selectedAgent: newWorktreeAgentOptionFor('claude'),
|
||||
agentOverridden: false
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps blank terminal as an explicit override', () => {
|
||||
const resolved = resolveNewWorktreeAgentSelection({
|
||||
visible: true,
|
||||
selectedAgent: NEW_WORKTREE_BLANK_AGENT,
|
||||
agentOverridden: true,
|
||||
runtimeSettings: { defaultTuiAgent: 'claude' },
|
||||
detectedAgentIds: new Set(['claude'])
|
||||
})
|
||||
|
||||
expect(resolved).toEqual({
|
||||
selectedAgent: NEW_WORKTREE_BLANK_AGENT,
|
||||
agentOverridden: true
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves closed modal state untouched', () => {
|
||||
const selected = newWorktreeAgentOptionFor('codex')
|
||||
const resolved = resolveNewWorktreeAgentSelection({
|
||||
visible: false,
|
||||
selectedAgent: selected,
|
||||
agentOverridden: true,
|
||||
runtimeSettings: { defaultTuiAgent: 'claude' },
|
||||
detectedAgentIds: new Set(['claude'])
|
||||
})
|
||||
|
||||
expect(resolved).toEqual({ selectedAgent: selected, agentOverridden: true })
|
||||
})
|
||||
|
||||
it('uses blank when no detected agent is known', () => {
|
||||
expect(pickPreferredNewWorktreeAgent({ defaultTuiAgent: null }, new Set()).id).toBe('__blank__')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import type { TuiAgent } from '../../../src/shared/types'
|
||||
import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog'
|
||||
import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents'
|
||||
import { pickWorkspaceAgent } from '../tasks/workspace-agent-selection'
|
||||
|
||||
export type NewWorktreeRuntimeSettings = {
|
||||
defaultTuiAgent?: TuiAgent | 'blank' | null
|
||||
disabledTuiAgents?: TuiAgent[]
|
||||
}
|
||||
|
||||
export type NewWorktreeAgentOption = {
|
||||
id: TuiAgent | '__blank__'
|
||||
label: string
|
||||
faviconDomain?: string
|
||||
}
|
||||
|
||||
export const NEW_WORKTREE_AGENT_OPTIONS: NewWorktreeAgentOption[] = MOBILE_AGENT_CATALOG
|
||||
|
||||
export const NEW_WORKTREE_BLANK_AGENT: NewWorktreeAgentOption = {
|
||||
id: '__blank__',
|
||||
label: 'Blank Terminal'
|
||||
}
|
||||
|
||||
export function newWorktreeAgentOptionFor(id: string | null | undefined): NewWorktreeAgentOption {
|
||||
if (id === 'blank' || id === '__blank__') {
|
||||
return NEW_WORKTREE_BLANK_AGENT
|
||||
}
|
||||
return NEW_WORKTREE_AGENT_OPTIONS.find((agent) => agent.id === id) ?? NEW_WORKTREE_BLANK_AGENT
|
||||
}
|
||||
|
||||
export function pickPreferredNewWorktreeAgent(
|
||||
settings: NewWorktreeRuntimeSettings | null,
|
||||
detectedAgentIds: Set<string> | null
|
||||
): NewWorktreeAgentOption {
|
||||
return newWorktreeAgentOptionFor(
|
||||
pickWorkspaceAgent(
|
||||
{
|
||||
defaultTuiAgent: settings?.defaultTuiAgent,
|
||||
disabledTuiAgents: settings?.disabledTuiAgents
|
||||
},
|
||||
detectedAgentIds
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function isSelectableAgent(
|
||||
agent: NewWorktreeAgentOption,
|
||||
settings: NewWorktreeRuntimeSettings | null,
|
||||
detectedAgentIds: Set<string> | null
|
||||
): boolean {
|
||||
if (agent.id === '__blank__') {
|
||||
return true
|
||||
}
|
||||
if (!isMobileTuiAgentEnabled(agent.id, settings?.disabledTuiAgents)) {
|
||||
return false
|
||||
}
|
||||
return detectedAgentIds === null || detectedAgentIds.has(agent.id)
|
||||
}
|
||||
|
||||
export function resolveNewWorktreeAgentSelection({
|
||||
visible,
|
||||
selectedAgent,
|
||||
agentOverridden,
|
||||
runtimeSettings,
|
||||
detectedAgentIds
|
||||
}: {
|
||||
visible: boolean
|
||||
selectedAgent: NewWorktreeAgentOption
|
||||
agentOverridden: boolean
|
||||
runtimeSettings: NewWorktreeRuntimeSettings | null
|
||||
detectedAgentIds: Set<string> | null
|
||||
}): { selectedAgent: NewWorktreeAgentOption; agentOverridden: boolean } {
|
||||
if (!visible) {
|
||||
return { selectedAgent, agentOverridden }
|
||||
}
|
||||
|
||||
const preferred = pickPreferredNewWorktreeAgent(runtimeSettings, detectedAgentIds)
|
||||
if (!agentOverridden) {
|
||||
return { selectedAgent: preferred, agentOverridden: false }
|
||||
}
|
||||
|
||||
if (
|
||||
detectedAgentIds !== null &&
|
||||
!isSelectableAgent(selectedAgent, runtimeSettings, detectedAgentIds)
|
||||
) {
|
||||
return { selectedAgent: preferred, agentOverridden: false }
|
||||
}
|
||||
|
||||
return { selectedAgent, agentOverridden: true }
|
||||
}
|
||||
Loading…
Reference in New Issue