Run the repo's Custom GitHub Issue Command when starting a workspace from an issue (#6827)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-07-02 18:09:14 -07:00 committed by GitHub
parent d6fb8e04f9
commit 11ca4955ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 545 additions and 5 deletions

View File

@ -97,6 +97,13 @@ function makeDeps(store = makeStore()) {
resolveSetupDecision: vi.fn().mockResolvedValue({ kind: 'decided', decision: 'inherit' }),
resolvePrStartPoint: vi.fn(),
confirmHooks: vi.fn().mockResolvedValue('run'),
readIssueCommand: vi.fn().mockResolvedValue({
effectiveContent: null,
localContent: null,
sharedContent: null,
localFilePath: '',
source: 'none'
}),
beginBackgroundCreate: vi.fn(() => 'creation-1'),
continueBackgroundCreate: vi.fn(() => true),
activatePendingCreate: vi.fn(),
@ -607,4 +614,206 @@ describe('createGitHubWorkItemWorkspaceInBackground', () => {
expect(request.startup?.command).toBe('codex --prompt-file')
expect(buildAgentStartupPlan).not.toHaveBeenCalled()
})
it('attaches the rendered issue command when the repo configured one and trust runs', async () => {
const deps = makeDeps()
deps.readIssueCommand.mockResolvedValueOnce({
effectiveContent: 'gh issue view {{issue}} --repo {{artifact_url}}',
localContent: null,
sharedContent: 'gh issue view {{issue}} --repo {{artifact_url}}',
localFilePath: '',
source: 'shared'
})
await createGitHubWorkItemWorkspaceInBackground(
{
item: makeIssue(),
repoId: 'repo-1',
openModalFallback: vi.fn()
},
deps
)
expect(deps.confirmHooks).toHaveBeenCalledWith(expect.anything(), 'repo-1', 'setup')
expect(deps.confirmHooks).toHaveBeenCalledWith(expect.anything(), 'repo-1', 'issueCommand')
const continueCall = deps.continueBackgroundCreate.mock.calls[0] as unknown[] | undefined
expect(continueCall).toBeDefined()
const request = continueCall?.[1] as WorktreeCreationRequest
expect(request.issueCommand?.command).toBe(
'gh issue view 42 --repo https://github.com/stablyai/orca/issues/42'
)
})
it('omits the issue command when the repo configured none', async () => {
const deps = makeDeps()
await createGitHubWorkItemWorkspaceInBackground(
{
item: makeIssue(),
repoId: 'repo-1',
openModalFallback: vi.fn()
},
deps
)
const continueCall = deps.continueBackgroundCreate.mock.calls[0] as unknown[] | undefined
expect(continueCall).toBeDefined()
const request = continueCall?.[1] as WorktreeCreationRequest
expect(request.issueCommand).toBeUndefined()
expect(deps.confirmHooks).not.toHaveBeenCalledWith(expect.anything(), 'repo-1', 'issueCommand')
})
it('skips the issue command when setup trust was declined', async () => {
const deps = makeDeps()
deps.readIssueCommand.mockResolvedValueOnce({
effectiveContent: 'echo {{issue}}',
localContent: 'echo {{issue}}',
sharedContent: null,
localFilePath: '/repo/.orca/issue-command',
source: 'local'
})
// Why: the setup confirmHooks call resolves 'skip', which mirrors the
// composer suppressing the issue command when setup trust is declined.
deps.confirmHooks.mockResolvedValue('skip')
await createGitHubWorkItemWorkspaceInBackground(
{
item: makeIssue(),
repoId: 'repo-1',
openModalFallback: vi.fn()
},
deps
)
const continueCall = deps.continueBackgroundCreate.mock.calls[0] as unknown[] | undefined
expect(continueCall).toBeDefined()
const request = continueCall?.[1] as WorktreeCreationRequest
expect(request.issueCommand).toBeUndefined()
expect(deps.confirmHooks).not.toHaveBeenCalledWith(expect.anything(), 'repo-1', 'issueCommand')
// Why: a declined setup trust short-circuits before the (up-to-15s) read,
// so workspace creation is never stalled to fetch a command we will drop.
expect(deps.readIssueCommand).not.toHaveBeenCalled()
})
it('does not run the issue command for PR items', async () => {
const deps = makeDeps()
deps.resolvePrStartPoint.mockResolvedValueOnce({
baseBranch: 'feature/from-pr',
pushTarget: { remote: 'origin', branch: 'feature/from-pr' },
branchNameOverride: 'feature/from-pr',
compareBaseRef: 'main'
})
deps.readIssueCommand.mockResolvedValueOnce({
effectiveContent: 'echo {{issue}}',
localContent: 'echo {{issue}}',
sharedContent: null,
localFilePath: '/repo/.orca/issue-command',
source: 'local'
})
await createGitHubWorkItemWorkspaceInBackground(
{
item: makeIssue({ type: 'pr', number: 7, url: 'https://github.com/stablyai/orca/pull/7' }),
repoId: 'repo-1',
openModalFallback: vi.fn()
},
deps
)
const continueCall = deps.continueBackgroundCreate.mock.calls[0] as unknown[] | undefined
expect(continueCall).toBeDefined()
const request = continueCall?.[1] as WorktreeCreationRequest
expect(request.issueCommand).toBeUndefined()
expect(deps.confirmHooks).not.toHaveBeenCalledWith(expect.anything(), 'repo-1', 'issueCommand')
})
it('fails closed when reading the issue command rejects', async () => {
const deps = makeDeps()
deps.readIssueCommand.mockRejectedValueOnce(new Error('runtime offline'))
const result = await createGitHubWorkItemWorkspaceInBackground(
{
item: makeIssue(),
repoId: 'repo-1',
openModalFallback: vi.fn()
},
deps
)
expect(result).toEqual({ kind: 'background-started' })
const continueCall = deps.continueBackgroundCreate.mock.calls[0] as unknown[] | undefined
expect(continueCall).toBeDefined()
const request = continueCall?.[1] as WorktreeCreationRequest
expect(request.issueCommand).toBeUndefined()
})
it('re-reads the store for each trust check so mid-flow trust updates are honored', async () => {
const snapshots = [makeStore(), makeStore(), makeStore()]
let getStoreCall = 0
const deps = makeDeps(snapshots[0])
deps.getStore = vi.fn(
() => snapshots[Math.min(getStoreCall++, snapshots.length - 1)] ?? snapshots[0]
)
deps.readIssueCommand.mockResolvedValueOnce({
effectiveContent: 'echo {{issue}}',
localContent: null,
sharedContent: 'echo {{issue}}',
localFilePath: '',
source: 'shared'
})
await createGitHubWorkItemWorkspaceInBackground(
{
item: makeIssue(),
repoId: 'repo-1',
openModalFallback: vi.fn()
},
deps
)
// Why: an "Always trust" stamped by the setup prompt only exists in a fresh
// snapshot. Each trust check must read the store at call time instead of
// reusing the snapshot captured when the flow began.
const setupCall = deps.confirmHooks.mock.calls.find((call) => call[2] === 'setup')
const issueCall = deps.confirmHooks.mock.calls.find((call) => call[2] === 'issueCommand')
expect(setupCall?.[0]).toBeDefined()
expect(issueCall?.[0]).toBeDefined()
expect(setupCall?.[0]).not.toBe(snapshots[0])
expect(issueCall?.[0]).not.toBe(snapshots[0])
expect(issueCall?.[0]).not.toBe(setupCall?.[0])
})
it('keeps the seeded agent prompt as the bare issue URL (not Complete <url>)', async () => {
const store = makeStore({
ensureDetectedAgents: vi.fn().mockResolvedValue(['codex'])
})
const deps = makeDeps(store)
deps.readIssueCommand.mockResolvedValueOnce({
effectiveContent: 'echo {{issue}}',
localContent: 'echo {{issue}}',
sharedContent: null,
localFilePath: '/repo/.orca/issue-command',
source: 'local'
})
await createGitHubWorkItemWorkspaceInBackground(
{
item: makeIssue(),
repoId: 'repo-1',
openModalFallback: vi.fn()
},
deps
)
const continueCall = deps.continueBackgroundCreate.mock.calls[0] as unknown[] | undefined
expect(continueCall).toBeDefined()
const request = continueCall?.[1] as WorktreeCreationRequest
// Why: Brennan's confirmed scope keeps the quick-start prompt the bare link;
// the issue command runs as a side-pane split, not as the agent prompt.
expect(request.startupPlan?.draftPrompt).toBe('https://github.com/stablyai/orca/issues/42')
expect(request.startupPlan?.draftPrompt ?? '').not.toContain('Complete')
expect(request.quickPrompt).not.toContain('Complete')
// The issue command itself still threads through as a side-pane split.
expect(request.issueCommand?.command).toBe('echo 42')
})
})

View File

@ -19,7 +19,10 @@ import {
} from '@/lib/launch-work-item-direct-preflight'
import { agentLaunchCommandErrorMessage } from '@/lib/launch-work-item-direct-messages'
import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed'
import { renderIssueCommandTemplate } from '@/lib/new-workspace'
import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner'
import { readRuntimeIssueCommand } from '@/runtime/runtime-hooks-client'
import { isGitRepoKind } from '../../../shared/repo-kind'
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../shared/execution-host'
import { evaluateRuntimeCompat } from '../../../shared/protocol-compat'
import {
@ -50,8 +53,9 @@ type BackgroundGitHubWorkItemCreateDeps = {
confirmHooks: (
store: GitHubWorkItemBackgroundStoreSnapshot,
repoId: string,
scope: 'setup'
scope: 'setup' | 'issueCommand'
) => ReturnType<typeof ensureHooksConfirmed>
readIssueCommand: typeof readRuntimeIssueCommand
beginBackgroundCreate: typeof beginBackgroundWorktreePreparation
continueBackgroundCreate: typeof continueBackgroundWorktreeCreation
activatePendingCreate: (creationId: string) => void
@ -78,8 +82,9 @@ const DEFAULT_DEPS: BackgroundGitHubWorkItemCreateDeps = {
useAppStore.getState().activePendingCreationId === creationId,
resolveSetupDecision: resolveDirectSetupDecision,
resolvePrStartPoint: resolveDirectPrStartPoint,
confirmHooks: (store, repoId, scope) =>
confirmHooks: (store, repoId, scope: 'setup' | 'issueCommand') =>
ensureHooksConfirmed(store as ReturnType<typeof useAppStore.getState>, repoId, scope),
readIssueCommand: readRuntimeIssueCommand,
beginBackgroundCreate: beginBackgroundWorktreePreparation,
continueBackgroundCreate: continueBackgroundWorktreeCreation,
activatePendingCreate: (creationId) => {
@ -222,7 +227,10 @@ export async function createGitHubWorkItemWorkspaceInBackground(
}
}
const trustDecision = await deps.confirmHooks(store, args.repoId, 'setup')
// Why: trust prompts are serialized app-wide, so read the store fresh at
// each check — an "Always trust" stamped by an earlier prompt (including
// this flow's own setup prompt) must short-circuit instead of re-prompting.
const trustDecision = await deps.confirmHooks(deps.getStore(), args.repoId, 'setup')
if (!deps.hasPendingCreate(creationId)) {
return { kind: 'background-started' }
}
@ -246,6 +254,49 @@ export async function createGitHubWorkItemWorkspaceInBackground(
}
const backendStartup = buildGitHubWorkItemBackendStartup(agent, startupPlan, quickTelemetry)
// Why: mirror the composer's trust-gated issue-command split. Only GitHub
// issues (numeric issue number) on git repos run it; PRs/Linear/folders never
// do. Reuse the setup trust decision: a 'skip' there also skips the command.
let issueCommand: WorktreeCreationRequest['issueCommand']
// Why: a declined setup trust also skips the issue command, so short-circuit
// before the (up-to-15s) read rather than reading just to drop the result.
if (
trustDecision !== 'skip' &&
isGitRepoKind(repo) &&
args.item.type === 'issue' &&
typeof args.item.number === 'number'
) {
// Why: read failures fail closed (no command), so create still proceeds.
let effectiveContent = ''
try {
const issueCommandRead = await deps.readIssueCommand(repoOwnerSettings, args.repoId)
effectiveContent = (issueCommandRead.effectiveContent ?? '').trim()
} catch {
effectiveContent = ''
}
if (!deps.hasPendingCreate(creationId)) {
return { kind: 'background-started' }
}
if (effectiveContent.length > 0) {
const issueCommandTrust = await deps.confirmHooks(
deps.getStore(),
args.repoId,
'issueCommand'
)
if (!deps.hasPendingCreate(creationId)) {
return { kind: 'background-started' }
}
if (issueCommandTrust === 'run') {
issueCommand = {
command: renderIssueCommandTemplate(effectiveContent, {
issueNumber: args.item.number,
artifactUrl: args.item.url ?? null
})
}
}
}
}
const request: WorktreeCreationRequest = {
...initialRequest,
...(baseBranch ? { baseBranch } : {}),
@ -255,6 +306,7 @@ export async function createGitHubWorkItemWorkspaceInBackground(
agent,
...(branchNameOverride ? { branchNameOverride } : {}),
...(backendStartup ? { startup: backendStartup } : {}),
...(issueCommand ? { issueCommand } : {}),
startupPlan,
quickPrompt,
quickTelemetry

View File

@ -0,0 +1,77 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import {
queueHookCommandsForFirstWorktreeTab,
resetHookCommandDelayedDeliveryForTests
} from './hook-command-delayed-delivery'
type AppState = ReturnType<typeof useAppStore.getState>
const initialTabsByWorktree = useAppStore.getState().tabsByWorktree
const initialGetKnownWorktreeById = useAppStore.getState().getKnownWorktreeById
function setStorePartial(partial: Record<string, unknown>): void {
useAppStore.setState(partial as Partial<AppState>)
}
function markWorktreeKnown(worktreeId: string): void {
setStorePartial({
getKnownWorktreeById: ((id: string) =>
id === worktreeId ? { id } : undefined) as unknown as AppState['getKnownWorktreeById']
})
}
afterEach(() => {
resetHookCommandDelayedDeliveryForTests()
setStorePartial({
tabsByWorktree: initialTabsByWorktree,
getKnownWorktreeById: initialGetKnownWorktreeById
})
})
describe('queueHookCommandsForFirstWorktreeTab', () => {
it('holds the delivery until the first worktree tab lands, then delivers exactly once', () => {
markWorktreeKnown('wt-1')
setStorePartial({ tabsByWorktree: {} })
const deliver = vi.fn()
queueHookCommandsForFirstWorktreeTab({ worktreeId: 'wt-1', deliver })
expect(deliver).not.toHaveBeenCalled()
setStorePartial({ tabsByWorktree: { 'wt-1': [{ id: 'mirror-tab-1' }] } })
expect(deliver).toHaveBeenCalledTimes(1)
expect(deliver).toHaveBeenCalledWith(expect.anything(), 'mirror-tab-1')
// Later tab churn must not re-deliver the consumed entry.
setStorePartial({ tabsByWorktree: { 'wt-1': [{ id: 'mirror-tab-1' }, { id: 'tab-2' }] } })
expect(deliver).toHaveBeenCalledTimes(1)
})
it('delivers immediately when the worktree already has a tab at queue time', () => {
markWorktreeKnown('wt-1')
setStorePartial({ tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] } })
const deliver = vi.fn()
queueHookCommandsForFirstWorktreeTab({ worktreeId: 'wt-1', deliver })
expect(deliver).toHaveBeenCalledTimes(1)
expect(deliver).toHaveBeenCalledWith(expect.anything(), 'tab-1')
})
it('drops the pending delivery when the worktree is no longer known', () => {
setStorePartial({
tabsByWorktree: {},
getKnownWorktreeById: (() => undefined) as unknown as AppState['getKnownWorktreeById']
})
const deliver = vi.fn()
queueHookCommandsForFirstWorktreeTab({ worktreeId: 'wt-gone', deliver })
// A tab appearing later (e.g. an id reused by mirror churn) must not
// deliver commands for a worktree that was dropped while unknown.
markWorktreeKnown('wt-gone')
setStorePartial({ tabsByWorktree: { 'wt-gone': [{ id: 'tab-1' }] } })
expect(deliver).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,67 @@
import { useAppStore } from '@/store'
type AppStoreSnapshot = ReturnType<typeof useAppStore.getState>
type PendingWorktreeHookCommandDelivery = {
worktreeId: string
deliver: (state: AppStoreSnapshot, firstTerminalTabId: string) => void
}
// Why: runtime-owned worktrees mirror their session tabs asynchronously, so a
// fresh create usually has no tab to queue setup/issue commands on yet. Hold
// the delivery until the first mirrored terminal tab lands instead of
// dropping it. Mirrors agent-startup-delayed-delivery's lazy-subscription
// shape: subscribed only while something is pending.
const pendingHookCommandDeliveries = new Map<string, PendingWorktreeHookCommandDelivery>()
let unsubscribePendingHookCommandDeliveries: (() => void) | null = null
export function queueHookCommandsForFirstWorktreeTab(
delivery: PendingWorktreeHookCommandDelivery
): void {
pendingHookCommandDeliveries.set(delivery.worktreeId, delivery)
ensurePendingHookCommandSubscription()
flushPendingHookCommandDeliveries()
}
export function resetHookCommandDelayedDeliveryForTests(): void {
pendingHookCommandDeliveries.clear()
unsubscribePendingHookCommandDeliveries?.()
unsubscribePendingHookCommandDeliveries = null
}
function ensurePendingHookCommandSubscription(): void {
if (unsubscribePendingHookCommandDeliveries) {
return
}
unsubscribePendingHookCommandDeliveries = useAppStore.subscribe(() => {
flushPendingHookCommandDeliveries()
})
}
function stopPendingHookCommandSubscriptionIfIdle(): void {
if (pendingHookCommandDeliveries.size > 0 || !unsubscribePendingHookCommandDeliveries) {
return
}
unsubscribePendingHookCommandDeliveries()
unsubscribePendingHookCommandDeliveries = null
}
function flushPendingHookCommandDeliveries(): void {
const state = useAppStore.getState()
for (const [worktreeId, delivery] of pendingHookCommandDeliveries) {
const firstTerminalTabId = state.tabsByWorktree[worktreeId]?.[0]?.id
if (!firstTerminalTabId) {
// Why: a worktree can be removed before its tabs ever mirror; drop the
// entry so the subscription does not stay armed forever.
if (!state.getKnownWorktreeById(worktreeId)) {
pendingHookCommandDeliveries.delete(worktreeId)
}
continue
}
// Delete before delivering so store writes inside deliver cannot re-enter
// this entry through the subscription.
pendingHookCommandDeliveries.delete(worktreeId)
delivery.deliver(state, firstTerminalTabId)
}
stopPendingHookCommandSubscriptionIfIdle()
}

View File

@ -77,6 +77,9 @@ export type WorktreeCreationRequest = {
* agent launch is self-contained; otherwise the renderer drives startup via
* `startupPlan`. */
startup?: WorktreeStartupLaunch
/** Repo Custom GitHub Issue Command to run in a side-pane split after the
* workspace's first terminal is created. Mirrors the composer's trust-gated issueCommand. */
issueCommand?: { command: string; env?: Record<string, string> }
pendingFirstAgentMessageRename: boolean
/** Post-create note persisted as the worktree comment. */
note: string

View File

@ -2,8 +2,16 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SetupScriptLaunchMode } from '../../../shared/types'
import { activateAndRevealWorktree, ensureWorktreeHasInitialTerminal } from './worktree-activation'
import { resetHookCommandDelayedDeliveryForTests } from './hook-command-delayed-delivery'
import { useAppStore } from '@/store'
type AppStoreState = ReturnType<typeof useAppStore.getState>
const initialTabsByWorktree = useAppStore.getState().tabsByWorktree
const initialGetKnownWorktreeById = useAppStore.getState().getKnownWorktreeById
const initialPendingIssueCommandSplitByTabId =
useAppStore.getState().pendingIssueCommandSplitByTabId
function setSetupScriptLaunchMode(mode: SetupScriptLaunchMode | null): void {
useAppStore.setState((state) => ({
settings: state.settings
@ -22,6 +30,12 @@ afterEach(() => {
: ({ activeRuntimeEnvironmentId: null } as unknown as typeof state.settings)
}))
setSetupScriptLaunchMode('new-tab')
resetHookCommandDelayedDeliveryForTests()
useAppStore.setState({
tabsByWorktree: initialTabsByWorktree,
getKnownWorktreeById: initialGetKnownWorktreeById,
pendingIssueCommandSplitByTabId: initialPendingIssueCommandSplitByTabId
} as Partial<AppStoreState>)
})
function createMockStore(overrides: Record<string, unknown> = {}) {
@ -266,6 +280,40 @@ describe('ensureWorktreeHasInitialTerminal', () => {
)
})
it('holds the issue command for the first mirrored web runtime tab when none exists yet', () => {
;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true
useAppStore.setState((state) => ({
settings: state.settings
? { ...state.settings, activeRuntimeEnvironmentId: 'web-runtime-1' }
: ({ activeRuntimeEnvironmentId: 'web-runtime-1' } as unknown as typeof state.settings)
}))
useAppStore.setState({
tabsByWorktree: {},
getKnownWorktreeById: ((id: string) =>
id === 'wt-1'
? { id: 'wt-1' }
: undefined) as unknown as AppStoreState['getKnownWorktreeById']
} as Partial<AppStoreState>)
const store = createMockStore()
const result = ensureWorktreeHasInitialTerminal(store, 'wt-1', undefined, undefined, {
command: 'gh issue view 42'
})
// Why: runtime session tabs mirror in asynchronously — the command must be
// held for the first mirrored tab rather than silently dropped.
expect(result).toBeNull()
expect(useAppStore.getState().pendingIssueCommandSplitByTabId).toEqual({})
useAppStore.setState({
tabsByWorktree: { 'wt-1': [{ id: 'mirror-tab-1' }] }
} as unknown as Partial<AppStoreState>)
expect(useAppStore.getState().pendingIssueCommandSplitByTabId['mirror-tab-1']).toEqual({
command: 'gh issue view 42'
})
})
it('creates a local initial terminal for explicitly local worktrees while a runtime is focused', () => {
useAppStore.setState((state) => ({
settings: state.settings

View File

@ -49,6 +49,7 @@ import {
import { isTuiAgent } from '../../../shared/tui-agent-config'
import { repoIsRemote } from '../../../shared/agent-launch-remote'
import { resumeSleepingAgentSessionsForWorktree } from '@/lib/resume-sleeping-agent-session'
import { queueHookCommandsForFirstWorktreeTab } from '@/lib/hook-command-delayed-delivery'
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
import {
getRuntimeEnvironmentIdForWorktree,
@ -506,6 +507,24 @@ export function ensureWorktreeHasInitialTerminal(
)
return existingTerminalTabId
}
if (setup || issueCommand) {
// Why: runtime-owned worktrees mirror their session tabs asynchronously,
// so right after create there is usually no tab yet. Hold the commands
// for the first mirrored tab instead of silently dropping them.
queueHookCommandsForFirstWorktreeTab({
worktreeId,
deliver: (state, firstTerminalTabId) =>
queueSetupAndIssueCommands(
state,
worktreeId,
firstTerminalTabId,
setup,
issueCommand,
wrappedSetupCommandStr,
opts
)
})
}
return null
}

View File

@ -504,6 +504,70 @@ describe('staged background worktree creation', () => {
})
})
// Why: one-click "Start workspace from issue" commonly backgrounds, so the
// user-moved-on path is the common delivery for the repo's issue command; it
// must thread through as the 5th positional arg, not be dropped to undefined.
it('threads the request issue command into the background terminal seed', async () => {
store.activeView = 'tasks'
store.createWorktree.mockResolvedValueOnce({
worktree: {
id: 'wt-1',
repoId: 'repo-1'
}
})
const started = continueBackgroundWorktreeCreation(
'creation-1',
makeRequest({ issueCommand: { command: 'gh issue view 42' } }),
{ revealCreationSurface: false }
)
expect(started).toBe(true)
// Why: vi.waitFor instead of a fixed microtask flush — the await count in
// executeWorktreeCreation grows over time (e.g. VM preflight), and a fixed
// flush silently starves this assertion in merged builds.
await vi.waitFor(() =>
expect(ensureWorktreeHasInitialTerminal).toHaveBeenCalledWith(
store,
'wt-1',
undefined,
undefined,
{ command: 'gh issue view 42' },
undefined,
{ activateCreatedTabs: false }
)
)
})
// Why: the still-watching path activates the worktree directly, so the issue
// command must reach activateAndRevealWorktree too — both branches carry it.
it('threads the request issue command into the active reveal', async () => {
store.activeView = 'terminal'
store.activePendingCreationId = 'creation-1'
store.createWorktree.mockResolvedValueOnce({
worktree: {
id: 'wt-1',
repoId: 'repo-1',
path: '/repo/wt-1'
}
})
vi.mocked(activateAndRevealWorktree).mockReturnValueOnce({ primaryTabId: 'tab-1' })
const started = continueBackgroundWorktreeCreation(
'creation-1',
makeRequest({ issueCommand: { command: 'gh issue view 42' } })
)
expect(started).toBe(true)
await vi.waitFor(() =>
expect(activateAndRevealWorktree).toHaveBeenCalledWith(
'wt-1',
expect.objectContaining({ issueCommand: { command: 'gh issue view 42' } })
)
)
expect(ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled()
})
it('toasts a staged create error after the user leaves the creation surface', async () => {
store.activeView = 'tasks'
store.createWorktree.mockRejectedValueOnce(new Error('create failed'))

View File

@ -225,7 +225,8 @@ async function executeWorktreeCreation(
sidebarRevealBehavior: 'auto',
...(result.setup ? { setup: result.setup } : {}),
...(result.defaultTabs ? { defaultTabs: result.defaultTabs } : {}),
...(startupOpt ? { startup: startupOpt } : {})
...(startupOpt ? { startup: startupOpt } : {}),
...(preparedRequest.issueCommand ? { issueCommand: preparedRequest.issueCommand } : {})
})
primaryTabId = activation === false ? null : activation.primaryTabId
} else {
@ -237,7 +238,7 @@ async function executeWorktreeCreation(
worktree.id,
startupOpt,
result.setup,
undefined,
preparedRequest.issueCommand,
result.defaultTabs,
{ activateCreatedTabs: false }
)