mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
perf(worktree): prepare agent checkouts before composer submission
This commit is contained in:
@@ -185,11 +185,11 @@ function QuickTabBody({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || quickAgent !== null || createDisabled) {
|
||||
if (!active || createDisabled) {
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
void prepareQuickWorkspace(() => cancelled)
|
||||
void prepareQuickWorkspace(quickAgent, () => cancelled)
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export type ComposerSubmitModel = {
|
||||
) => QuickSubmitSource | null
|
||||
resetForNextCreate: () => void
|
||||
submit: () => Promise<void>
|
||||
prepareQuickWorkspace: (isCancelled: () => boolean) => Promise<void>
|
||||
prepareQuickWorkspace: (agent: TuiAgent | null, isCancelled: () => boolean) => Promise<void>
|
||||
submitQuick: (agent: TuiAgent | null) => Promise<void>
|
||||
submitFolderTarget: (requestedAgent: TuiAgent | null) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -81,9 +81,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) {
|
||||
if (
|
||||
!isCancelled ||
|
||||
(preparation &&
|
||||
(!selectedRepoIsGit ||
|
||||
requestedAgent !== null ||
|
||||
(ephemeralVmsEnabled && selectedEphemeralVmRecipeId)))
|
||||
(!selectedRepoIsGit || (ephemeralVmsEnabled && selectedEphemeralVmRecipeId)))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export function useQuickSubmitAction(input: QuickSubmitActionInput) {
|
||||
)
|
||||
|
||||
const prepareQuickWorkspace = useCallback(
|
||||
(isCancelled: () => boolean) => submitQuick(null, { isCancelled }),
|
||||
(agent: TuiAgent | null, isCancelled: () => boolean) => submitQuick(agent, { isCancelled }),
|
||||
[submitQuick]
|
||||
)
|
||||
return { submitQuick, prepareQuickWorkspace }
|
||||
|
||||
@@ -59,7 +59,7 @@ export type UseComposerStateResult = {
|
||||
promptTextareaRef: RefObject<HTMLTextAreaElement | null>
|
||||
nameInputRef: RefObject<HTMLInputElement | null>
|
||||
submit: () => Promise<void>
|
||||
prepareQuickWorkspace: (isCancelled: () => boolean) => Promise<void>
|
||||
prepareQuickWorkspace: (agent: TuiAgent | null, isCancelled: () => boolean) => Promise<void>
|
||||
submitQuick: (agent: TuiAgent | null) => Promise<void>
|
||||
createDisabled: boolean
|
||||
selectAddedProjectRepo: (repoId: string) => void
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createRequestedWorktree } from './create-requested-worktree'
|
||||
import { makeRequest } from './worktree-creation-request.test-fixture'
|
||||
|
||||
const { createWorktree } = vi.hoisted(() => ({ createWorktree: vi.fn() }))
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: { getState: () => ({ createWorktree }) }
|
||||
}))
|
||||
vi.mock('@/lib/worktree-draft-startup-view-mode', () => ({
|
||||
resolveBackendDraftStartup: (request: { startup?: unknown }) => request.startup
|
||||
}))
|
||||
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
describe('durable composer creation launch boundary', () => {
|
||||
it.each(['command', 'draft'] as const)(
|
||||
'withholds agent %s execution until Create while retaining workspace metadata',
|
||||
async (delivery) => {
|
||||
const request = makeRequest({
|
||||
agent: 'codex',
|
||||
startup: delivery === 'command' ? { command: 'codex', launchAgent: 'codex' } : undefined,
|
||||
launchDraftPrompt: 'Investigate this task',
|
||||
startupPlan: {
|
||||
agent: 'codex',
|
||||
launchCommand: 'codex',
|
||||
expectedProcess: 'codex',
|
||||
followupPrompt: null,
|
||||
launchConfig: { agentArgs: '', agentEnv: {} }
|
||||
}
|
||||
})
|
||||
const snapshot = structuredClone(request)
|
||||
await createRequestedWorktree('reservation', request, true)
|
||||
const args = createWorktree.mock.calls[0]
|
||||
expect(args[10]).toBe('codex')
|
||||
expect(args[16]).toBeUndefined()
|
||||
expect(args[25]).not.toHaveProperty('startupDraft')
|
||||
expect(request).toEqual(snapshot)
|
||||
}
|
||||
)
|
||||
|
||||
it('preserves ordinary backend agent launch', async () => {
|
||||
const startup = { command: 'codex', launchAgent: 'codex' as const }
|
||||
await createRequestedWorktree('submit', makeRequest({ agent: 'codex', startup }))
|
||||
expect(createWorktree.mock.calls[0][16]).toEqual(startup)
|
||||
})
|
||||
|
||||
it('preserves ordinary host-owned draft launch', async () => {
|
||||
await createRequestedWorktree(
|
||||
'submit',
|
||||
makeRequest({ agent: 'codex', launchDraftPrompt: 'Unsent task' })
|
||||
)
|
||||
expect(createWorktree.mock.calls[0][25]).toMatchObject({ startupDraft: 'Unsent task' })
|
||||
})
|
||||
|
||||
it('continues warming blank shells without selecting them', async () => {
|
||||
await createRequestedWorktree(
|
||||
'reservation',
|
||||
makeRequest({ startup: { command: '', env: { PROJECT: 'fixture' } } }),
|
||||
true
|
||||
)
|
||||
expect(createWorktree.mock.calls[0][16]).toEqual({
|
||||
command: '',
|
||||
env: { PROJECT: 'fixture' },
|
||||
activate: false
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -12,8 +12,11 @@ export function createRequestedWorktree(
|
||||
): Promise<CreateWorktreeResult> {
|
||||
const provisionedRoot = getProvisionedRootCreateOptions(preparedRequest)
|
||||
const structuredLaunch = preparedRequest.agentLaunchRoute === 'structured-native-chat'
|
||||
const deferAgentLaunch = background && preparedRequest.agent !== null
|
||||
const backendStartup =
|
||||
provisionedRoot || structuredLaunch ? undefined : resolveBackendDraftStartup(preparedRequest)
|
||||
provisionedRoot || structuredLaunch || deferAgentLaunch
|
||||
? undefined
|
||||
: resolveBackendDraftStartup(preparedRequest)
|
||||
return useAppStore
|
||||
.getState()
|
||||
.createWorktree(
|
||||
@@ -54,7 +57,8 @@ export function createRequestedWorktree(
|
||||
? { linkedTaskSourceContext: preparedRequest.linkedTaskSourceContext }
|
||||
: {}),
|
||||
// Why: the remote host must own task-draft startup so its initial terminal is the agent, not an idle fallback shell.
|
||||
...(!structuredLaunch &&
|
||||
...(!deferAgentLaunch &&
|
||||
!structuredLaunch &&
|
||||
!backendStartup &&
|
||||
preparedRequest.agent &&
|
||||
preparedRequest.launchDraftPrompt
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { CreateWorktreeResult } from '../../../shared/worktree/create-types'
|
||||
import { makeRequest } from './worktree-creation-request.test-fixture'
|
||||
import { executeWorktreeCreation } from './worktree-creation-flow-execute'
|
||||
import { activateAndRevealWorktree } from './worktree-activation'
|
||||
|
||||
const { store } = vi.hoisted(() => ({
|
||||
store: {
|
||||
createWorktree: vi.fn(),
|
||||
pendingWorktreeCreations: { 'creation-1': {} },
|
||||
activeView: 'terminal',
|
||||
activePendingCreationId: 'creation-1'
|
||||
}
|
||||
}))
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: () => store } }))
|
||||
vi.mock('@/lib/browser-uuid', () => ({ createBrowserUuid: () => 'creation-1' }))
|
||||
vi.mock('@/lib/worktree-activation', () => ({ activateAndRevealWorktree: vi.fn() }))
|
||||
vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({
|
||||
ensureWorktreeHasInitialTerminal: vi.fn()
|
||||
}))
|
||||
vi.mock('@/lib/agent-trust-preflight', () => ({ preflightAgentTrust: vi.fn() }))
|
||||
vi.mock('@/lib/ephemeral-vm-worktree-creation', () => ({
|
||||
attachEphemeralVmRuntimeToWorkspace: vi.fn(),
|
||||
cleanupEphemeralVmRuntimeForFailedCreate: vi.fn(),
|
||||
prepareRequestForCreate: vi.fn()
|
||||
}))
|
||||
vi.mock('@/lib/worktree-creation-structured-session', () => ({
|
||||
launchStructuredWorktreeSession: vi.fn()
|
||||
}))
|
||||
vi.mock('@/lib/worktree-creation-structured-recovery', () => ({
|
||||
markStructuredWorktreeLaunchUnconfirmed: vi.fn()
|
||||
}))
|
||||
vi.mock('@/lib/worktree-creation-completion', () => ({ completeWorktreeCreation: vi.fn() }))
|
||||
|
||||
describe('retained agent activation', () => {
|
||||
it('launches the selected agent from a retained checkout without creating again', async () => {
|
||||
vi.mocked(activateAndRevealWorktree).mockReturnValueOnce({ primaryTabId: 'agent-tab' })
|
||||
const launchConfig = { agentArgs: '--model selected', agentEnv: { PROJECT: 'fixture' } }
|
||||
const request = makeRequest({
|
||||
agent: 'codex',
|
||||
launchDraftPrompt: 'Unsent task',
|
||||
startupPlan: {
|
||||
agent: 'codex',
|
||||
launchCommand: 'codex --model selected',
|
||||
expectedProcess: 'codex',
|
||||
followupPrompt: null,
|
||||
draftPrompt: 'Unsent task',
|
||||
env: { PROJECT: 'fixture' },
|
||||
launchConfig
|
||||
}
|
||||
})
|
||||
await executeWorktreeCreation('creation-1', request, {
|
||||
worktree: { id: 'wt-agent', repoId: 'repo-1' }
|
||||
} as CreateWorktreeResult)
|
||||
expect(store.createWorktree).not.toHaveBeenCalled()
|
||||
expect(activateAndRevealWorktree).toHaveBeenCalledExactlyOnceWith('wt-agent', {
|
||||
sidebarRevealBehavior: 'auto',
|
||||
startup: {
|
||||
command: 'codex --model selected',
|
||||
launchAgent: 'codex',
|
||||
env: { PROJECT: 'fixture' },
|
||||
launchConfig,
|
||||
launchToken: 'creation-1',
|
||||
draftPrompt: 'Unsent task',
|
||||
launchDraftText: 'Unsent task'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -26,6 +26,28 @@ const result = {
|
||||
} as CreateWorktreeResult
|
||||
|
||||
describe('retained composer worktree creation', () => {
|
||||
it('retains an agent checkout and preserves the exact launch plan for Create', async () => {
|
||||
const checkout = { worktree: result.worktree }
|
||||
const create = vi.fn(async () => checkout)
|
||||
const controller = createRetainedWorktreeCreation(create)
|
||||
const agentRequest = request({
|
||||
agent: 'codex',
|
||||
startup: { command: 'codex', launchAgent: 'codex' },
|
||||
startupPlan: {
|
||||
agent: 'codex',
|
||||
launchCommand: 'codex',
|
||||
expectedProcess: 'codex',
|
||||
followupPrompt: null,
|
||||
launchConfig: { agentArgs: '', agentEnv: {} }
|
||||
},
|
||||
launchDraftPrompt: 'Keep this unsent'
|
||||
})
|
||||
expect(controller.start(agentRequest, 'owner')).toBe(true)
|
||||
expect(await controller.take(agentRequest, 'owner')).toBe(checkout)
|
||||
expect(create).toHaveBeenCalledExactlyOnceWith(agentRequest)
|
||||
expect(controller.take(agentRequest, 'owner')).toBeNull()
|
||||
})
|
||||
|
||||
it('retains a remote blank checkout whose terminal starts on activation', async () => {
|
||||
const create = vi.fn(async () => ({ worktree: result.worktree }))
|
||||
const controller = createRetainedWorktreeCreation(create)
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { WorktreeCreationRequest } from './pending-worktree-creation'
|
||||
|
||||
export function canRetainComposerWorktree(request: WorktreeCreationRequest): boolean {
|
||||
return (
|
||||
request.agent === null &&
|
||||
(request.startup === undefined || request.startup.command === '') &&
|
||||
!request.startup?.launchAgent &&
|
||||
!request.startup?.launchConfig &&
|
||||
!request.startupPlan &&
|
||||
!request.launchDraftPrompt &&
|
||||
(request.agent !== null
|
||||
? Boolean(request.startupPlan)
|
||||
: (request.startup === undefined || request.startup.command === '') &&
|
||||
!request.startup?.launchAgent &&
|
||||
!request.startup?.launchConfig &&
|
||||
!request.startupPlan &&
|
||||
!request.launchDraftPrompt) &&
|
||||
!request.issueCommand &&
|
||||
!request.ephemeralVmRecipe &&
|
||||
!request.ephemeralVmRuntimeId &&
|
||||
|
||||
@@ -114,9 +114,7 @@ export type Worktree = {
|
||||
* grant newly-created worktrees a short grace window at the top of Recent,
|
||||
* immune to ambient PTY-bump reordering in other worktrees. */
|
||||
createdAt?: number
|
||||
/** Agent selected when Orca originally created the worktree. Used only to
|
||||
* seed a replacement terminal if the user later reopens the worktree after
|
||||
* closing every visible surface. */
|
||||
/** Agent selected at creation; metadata alone never requests an agent launch. */
|
||||
createdWithAgent?: TuiAgent
|
||||
/** True while an auto-named workspace is waiting for the first agent message
|
||||
* to drive the branch/title rename. */
|
||||
|
||||
Reference in New Issue
Block a user