mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
minor change
This commit is contained in:
@@ -45,7 +45,7 @@ export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise<{
|
||||
ptyId: ctx.result.id,
|
||||
isDaemonHostSpawn: ctx.isDaemonHostSpawn,
|
||||
isReattach: ctx.result.isReattach === true,
|
||||
pinnedByResume: ctx.codexResumeHomeSelected,
|
||||
pinnedByResume: Boolean(ctx.codexResumeLaunch.codexResumeHome),
|
||||
launchCodexHomePath: ctx.selectedCodexHomePath,
|
||||
launchEnv: ctx.baseEnv,
|
||||
target: ctx.codexSelectionTarget,
|
||||
|
||||
@@ -454,7 +454,6 @@ import {
|
||||
import { isMainTerminalSideEffectAuthorityForPty } from './terminal-pane/terminal-side-effect-facts-handler'
|
||||
import { appendUniqueOpenFileIds } from './terminal/unsaved-close-queue'
|
||||
import { setWindowCloseRequestHandler } from './window-close-request-coordinator'
|
||||
import CodexRestartChip from './CodexRestartChip'
|
||||
import { AgentLaunchNotices } from './terminal-pane/AgentLaunchNotices'
|
||||
import { AgentLaunchRecoveryCardContainer } from './terminal-pane/AgentLaunchRecoveryCardContainer'
|
||||
import {
|
||||
|
||||
@@ -119,7 +119,7 @@ export function buildDashboardWorktreeLaunchOptions(
|
||||
)
|
||||
const preferred = state.settings?.defaultTuiAgent
|
||||
result[worktreeId] =
|
||||
preferred && preferred !== 'blank' && isBuiltInTuiAgent(preferred) && enabled.includes(preferred)
|
||||
isTuiAgent(preferred) && enabled.some((agent) => agent === preferred)
|
||||
? [preferred, ...enabled.filter((agent) => agent !== preferred)]
|
||||
: enabled
|
||||
}
|
||||
|
||||
@@ -1,38 +1,21 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
|
||||
import type { ProjectGroup } from '../../../../shared/project-group-types'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import type * as NewWorkspaceModule from '@/lib/new-workspace'
|
||||
import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
activateAndRevealFolderWorkspace: vi.fn(),
|
||||
ensureAgentStartupInTerminal: vi.fn()
|
||||
activateAndRevealFolderWorkspace: vi.fn()
|
||||
}))
|
||||
|
||||
// Why: importOriginal keeps the real resolveStartupLaunchDraftText, so the
|
||||
// invariant test below exercises the shipped gate instead of a copy of it.
|
||||
vi.mock('@/lib/worktree-activation', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>()
|
||||
return { ...actual, activateAndRevealFolderWorkspace: mocks.activateAndRevealFolderWorkspace }
|
||||
})
|
||||
|
||||
vi.mock('@/lib/new-workspace', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof NewWorkspaceModule>()
|
||||
return {
|
||||
...actual,
|
||||
ensureAgentStartupInTerminal: mocks.ensureAgentStartupInTerminal
|
||||
}
|
||||
})
|
||||
|
||||
import { useAppStore } from '@/store'
|
||||
import { decideInitialAgentTabViewMode } from '@/lib/native-chat-initial-view-mode'
|
||||
import { resolveStartupLaunchDraftText } from '@/lib/worktree-startup-payload'
|
||||
import {
|
||||
getFolderWorkspaceAgentLaunchPlatform,
|
||||
submitFolderWorkspaceCreate
|
||||
} from './folder-workspace-composer-submit'
|
||||
import { useAppStore } from '@/store'
|
||||
import { submitFolderWorkspaceCreate } from './folder-workspace-composer-submit'
|
||||
|
||||
function makeProjectGroup(): ProjectGroup {
|
||||
return {
|
||||
@@ -70,7 +53,9 @@ function makeFolderWorkspace(overrides: Partial<FolderWorkspace> = {}): FolderWo
|
||||
|
||||
describe('submitFolderWorkspaceCreate', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.activateAndRevealFolderWorkspace.mockReturnValue({ primaryTabId: 'tab-1' })
|
||||
useAppStore.setState({ nativeChatLaunchDraftByTabId: {} })
|
||||
Object.assign(window, {
|
||||
api: {
|
||||
agentTrust: {
|
||||
@@ -81,8 +66,7 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mocks.activateAndRevealFolderWorkspace.mockReset()
|
||||
mocks.ensureAgentStartupInTerminal.mockReset()
|
||||
useAppStore.setState({ nativeChatLaunchDraftByTabId: {} })
|
||||
Reflect.deleteProperty(window, 'api')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
@@ -103,7 +87,6 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
note: '',
|
||||
quickAgent: null,
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange
|
||||
})
|
||||
@@ -115,18 +98,14 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
linkedTask: null
|
||||
})
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
|
||||
runtimeEnvironmentId: null
|
||||
})
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
'Failed to activate folder workspace after create:',
|
||||
expect.any(Error)
|
||||
)
|
||||
})
|
||||
|
||||
it('marks a blank folder workspace for first-input rename when launching an agent with a note', async () => {
|
||||
it('uses an identity-only host launch and marks a submitted first prompt for rename', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
@@ -136,13 +115,10 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
note: 'Fix the flaky checkout flow',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
agentArgs: '--model gpt-5.4',
|
||||
agentEnv: { ORCA_AGENT_PROFILE: 'review' },
|
||||
launchSource: 'new_workspace_composer',
|
||||
runtimeEnvironmentId: 'env-1',
|
||||
createFolderWorkspace,
|
||||
onOpenChange
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
@@ -153,81 +129,74 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
createdWithAgent: 'codex',
|
||||
pendingFirstAgentMessageRename: true
|
||||
})
|
||||
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith(
|
||||
'folder-workspace-1',
|
||||
expect.objectContaining({
|
||||
runtimeEnvironmentId: 'env-1',
|
||||
startup: expect.objectContaining({
|
||||
command: expect.stringContaining('codex'),
|
||||
env: { ORCA_AGENT_PROFILE: 'review' },
|
||||
telemetry: expect.objectContaining({
|
||||
launch_source: 'new_workspace_composer'
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
expect(startup?.command).toContain('--model')
|
||||
expect(startup?.command).toContain('gpt-5.4')
|
||||
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
|
||||
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
|
||||
runtimeEnvironmentId: 'env-1',
|
||||
startup: {
|
||||
command: '',
|
||||
launchAgent: 'codex',
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'codex' },
|
||||
prompt: 'Fix the flaky checkout flow',
|
||||
allowEmptyPromptLaunch: true
|
||||
},
|
||||
telemetry: {
|
||||
launch_source: 'new_workspace_composer',
|
||||
request_kind: 'new'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('does not mark first-input rename when the folder workspace has an explicit name', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
it.each([
|
||||
{
|
||||
label: 'an explicit workspace name',
|
||||
name: 'Checkout polish',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: null,
|
||||
note: 'Fix the flaky checkout flow',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Checkout polish',
|
||||
connectionId: null,
|
||||
linkedTask: null,
|
||||
createdWithAgent: 'codex'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not mark first-input rename when a linked work item owns the folder workspace name', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
const linkedWorkItem = {
|
||||
provider: 'github' as const,
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Restore checkout polish',
|
||||
url: 'https://github.com/stablyai/orca/issues/42',
|
||||
repoId: 'repo-1'
|
||||
expectedName: 'Checkout polish'
|
||||
},
|
||||
{
|
||||
label: 'linked work-item naming',
|
||||
name: '',
|
||||
linkedWorkItem: {
|
||||
provider: 'github' as const,
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Restore checkout polish',
|
||||
url: 'https://github.com/stablyai/orca/issues/42',
|
||||
repoId: 'repo-1'
|
||||
},
|
||||
note: 'Use the issue context',
|
||||
expectedName: 'Restore checkout polish'
|
||||
},
|
||||
{
|
||||
label: 'an empty submitted prompt',
|
||||
name: '',
|
||||
linkedWorkItem: null,
|
||||
note: ' ',
|
||||
expectedName: 'Platform workspace'
|
||||
}
|
||||
])('does not mark first-input rename for $label', async (input) => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
name: input.name,
|
||||
lastAutoName: '',
|
||||
linkedWorkItem,
|
||||
note: 'Use the issue context',
|
||||
linkedWorkItem: input.linkedWorkItem,
|
||||
note: input.note,
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Restore checkout polish',
|
||||
connectionId: null,
|
||||
linkedTask: linkedWorkItem,
|
||||
createdWithAgent: 'codex'
|
||||
})
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ pendingFirstAgentMessageRename: true })
|
||||
)
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: input.expectedName, createdWithAgent: 'codex' })
|
||||
)
|
||||
})
|
||||
|
||||
it('creates a Jira folder workspace with its bound source context', async () => {
|
||||
@@ -262,7 +231,6 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
note: '',
|
||||
quickAgent: null,
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
@@ -276,8 +244,7 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps linked Codex context out of submitted startup and pastes it as a draft', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
it('launches linked context as a host-owned draft and mirrors it into native chat', async () => {
|
||||
const linkedWorkItem = {
|
||||
provider: 'github' as const,
|
||||
type: 'pr' as const,
|
||||
@@ -286,6 +253,7 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
url: 'https://github.com/stablyai/orca/pull/91',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
const draft = `Review this before starting\n\n${linkedWorkItem.url}`
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
@@ -295,54 +263,30 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
note: 'Review this before starting',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
launchSource: 'new_workspace_composer',
|
||||
createFolderWorkspace,
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Restore linked quick-create',
|
||||
connectionId: null,
|
||||
linkedTask: linkedWorkItem,
|
||||
createdWithAgent: 'codex'
|
||||
})
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
expect(startup?.command).toBe('codex')
|
||||
expect(startup?.command).not.toContain(linkedWorkItem.url)
|
||||
expect(startup?.command).not.toContain('Review this before starting')
|
||||
expect(startup).toMatchObject({
|
||||
command: '',
|
||||
launchAgent: 'codex',
|
||||
launchDraftText: draft,
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'codex' },
|
||||
prompt: draft,
|
||||
promptDelivery: 'draft'
|
||||
}
|
||||
})
|
||||
expect(startup).not.toHaveProperty('draftPrompt')
|
||||
expect(window.api.agentTrust?.markTrusted).toHaveBeenCalledWith({
|
||||
preset: 'codex',
|
||||
workspacePath: '/repo/platform/hi'
|
||||
})
|
||||
expect(mocks.ensureAgentStartupInTerminal).toHaveBeenCalledWith({
|
||||
worktreeId: folderWorkspaceKey('folder-workspace-1'),
|
||||
primaryTabId: 'tab-1',
|
||||
startup: expect.objectContaining({
|
||||
agent: 'codex',
|
||||
launchCommand: 'codex',
|
||||
followupPrompt: null,
|
||||
draftPrompt: `Review this before starting\n\n${linkedWorkItem.url}`
|
||||
})
|
||||
})
|
||||
expect(useAppStore.getState().nativeChatLaunchDraftByTabId['tab-1']?.text).toBe(draft)
|
||||
})
|
||||
|
||||
it('pre-marks remote linked Codex folder workspaces trusted before draft paste', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () =>
|
||||
makeFolderWorkspace({
|
||||
connectionId: 'ssh-1',
|
||||
folderPath: '/home/alice/platform/Trust remote folder draft'
|
||||
})
|
||||
)
|
||||
const linkedWorkItem = {
|
||||
provider: 'github' as const,
|
||||
type: 'pr' as const,
|
||||
number: 92,
|
||||
title: 'Trust remote folder draft',
|
||||
url: 'https://github.com/stablyai/orca/pull/92',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
it('pre-marks remote folder agents trusted on the owning SSH host', async () => {
|
||||
const projectGroup = {
|
||||
...makeProjectGroup(),
|
||||
connectionId: 'ssh-1',
|
||||
@@ -351,158 +295,57 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup,
|
||||
name: '',
|
||||
name: 'Remote folder',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem,
|
||||
linkedWorkItem: null,
|
||||
note: '',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
isRemote: true,
|
||||
createFolderWorkspace,
|
||||
createFolderWorkspace: vi.fn(async () =>
|
||||
makeFolderWorkspace({
|
||||
connectionId: 'ssh-1',
|
||||
folderPath: '/home/alice/platform/Remote folder'
|
||||
})
|
||||
),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(window.api.agentTrust?.markTrusted).toHaveBeenCalledWith({
|
||||
preset: 'codex',
|
||||
workspacePath: '/home/alice/platform/Trust remote folder draft',
|
||||
workspacePath: '/home/alice/platform/Remote folder',
|
||||
connectionId: 'ssh-1'
|
||||
})
|
||||
expect(mocks.ensureAgentStartupInTerminal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
worktreeId: folderWorkspaceKey('folder-workspace-1'),
|
||||
startup: expect.objectContaining({
|
||||
agent: 'codex',
|
||||
draftPrompt: linkedWorkItem.url
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('delivers non-linked follow-up prompts for agents that need stdin after launch', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
|
||||
it.each([
|
||||
['a local WSL path', { parentPath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\platform' }],
|
||||
[
|
||||
'a remote Windows path',
|
||||
{ connectionId: 'ssh-windows', parentPath: 'C:\\Users\\alice\\platform' }
|
||||
]
|
||||
])('leaves command and platform resolution to the host for %s', async (_label, overrides) => {
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: 'Aider followup',
|
||||
projectGroup: { ...makeProjectGroup(), ...overrides },
|
||||
name: 'Host-owned launch',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: null,
|
||||
note: 'Fix the failing folder prompt flow',
|
||||
quickAgent: 'aider',
|
||||
note: "Use Bob's startup",
|
||||
quickAgent: 'claude',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
expect(startup?.command).toBe('aider')
|
||||
expect(mocks.ensureAgentStartupInTerminal).toHaveBeenCalledWith({
|
||||
worktreeId: folderWorkspaceKey('folder-workspace-1'),
|
||||
primaryTabId: 'tab-1',
|
||||
startup: expect.objectContaining({
|
||||
agent: 'aider',
|
||||
launchCommand: 'aider',
|
||||
followupPrompt: 'Fix the failing folder prompt flow'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('uses native draft launch for linked agents with prefill support', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
const linkedWorkItem = {
|
||||
provider: 'gitlab' as const,
|
||||
type: 'mr' as const,
|
||||
number: 17,
|
||||
title: 'Review folder workspace draft',
|
||||
url: 'https://gitlab.example.com/group/project/-/merge_requests/17',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem,
|
||||
note: 'Check the migration path',
|
||||
quickAgent: 'claude',
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
expect(startup?.command).toContain('claude --prefill')
|
||||
expect(startup?.command).toContain('Check the migration path')
|
||||
expect(startup?.command).toContain(linkedWorkItem.url)
|
||||
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses native prefill for link-only Linear folder workspace drafts', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
const linkedWorkItem = {
|
||||
provider: 'linear' as const,
|
||||
type: 'issue' as const,
|
||||
number: 0,
|
||||
title: 'Ship Linear source drafts',
|
||||
url: 'https://linear.app/acme/issue/ENG-77/ship-linear-source-drafts',
|
||||
linearIdentifier: 'ENG-77',
|
||||
linkedContext: {
|
||||
provider: 'linear' as const,
|
||||
version: 1 as const,
|
||||
renderedText: [
|
||||
'Linear issue context snapshot',
|
||||
'Identifier: ENG-77',
|
||||
'Title: Ship Linear source drafts',
|
||||
'Description:',
|
||||
'Distinctive folder Linear body.'
|
||||
].join('\n')
|
||||
expect(mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup).toMatchObject({
|
||||
command: '',
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'claude' },
|
||||
prompt: "Use Bob's startup"
|
||||
}
|
||||
}
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem,
|
||||
note: 'User note stays above source',
|
||||
quickAgent: 'claude',
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
projectGroupId: 'group-1',
|
||||
name: 'ENG-77 Ship Linear source drafts',
|
||||
connectionId: null,
|
||||
linkedTask: {
|
||||
provider: 'linear',
|
||||
type: 'issue',
|
||||
number: 0,
|
||||
title: 'Ship Linear source drafts',
|
||||
url: 'https://linear.app/acme/issue/ENG-77/ship-linear-source-drafts',
|
||||
linearIdentifier: 'ENG-77'
|
||||
},
|
||||
createdWithAgent: 'claude'
|
||||
})
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
expect(startup?.command).toContain('claude --prefill')
|
||||
expect(startup?.command).toContain('User note stays above source')
|
||||
expect(startup?.command).toContain('Linked Linear issue: ENG-77')
|
||||
expect(startup?.command).toContain(
|
||||
'https://linear.app/acme/issue/ENG-77/ship-linear-source-drafts'
|
||||
)
|
||||
expect(startup?.command).not.toContain('Distinctive folder Linear body.')
|
||||
expect(startup?.command).not.toContain('--- BEGIN LINKED WORK ITEM CONTEXT ---')
|
||||
expect(startup?.command).not.toContain('orca linear')
|
||||
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps explicit blank linked folder creates free of agent startup and draft paste', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
it('keeps explicit blank linked creates free of agent startup', async () => {
|
||||
const linkedWorkItem = {
|
||||
provider: 'github' as const,
|
||||
type: 'issue' as const,
|
||||
@@ -520,111 +363,13 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
note: 'Keep this as metadata only',
|
||||
quickAgent: null,
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Restore checkout polish',
|
||||
connectionId: null,
|
||||
linkedTask: linkedWorkItem
|
||||
})
|
||||
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
|
||||
runtimeEnvironmentId: null
|
||||
})
|
||||
expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not mark first-input rename without submitted first input', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: null,
|
||||
note: ' ',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: true,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Platform workspace',
|
||||
connectionId: null,
|
||||
linkedTask: null,
|
||||
createdWithAgent: 'codex'
|
||||
})
|
||||
})
|
||||
|
||||
it('quotes quick-agent startup for POSIX when the folder group is a local WSL UNC path', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
const projectGroup = {
|
||||
...makeProjectGroup(),
|
||||
parentPath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\platform'
|
||||
}
|
||||
|
||||
expect(getFolderWorkspaceAgentLaunchPlatform(projectGroup)).toBe('linux')
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup,
|
||||
name: 'WSL folder',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: null,
|
||||
note: "Use Bob's POSIX startup",
|
||||
quickAgent: 'claude',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith(
|
||||
'folder-workspace-1',
|
||||
expect.objectContaining({
|
||||
startup: expect.objectContaining({
|
||||
command: `claude 'Use Bob'"'"'s POSIX startup'`
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('quotes quick-agent startup for Windows when the remote folder group uses a Windows path', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
|
||||
const projectGroup = {
|
||||
...makeProjectGroup(),
|
||||
connectionId: 'ssh-windows',
|
||||
parentPath: 'C:\\Users\\alice\\platform'
|
||||
}
|
||||
|
||||
expect(getFolderWorkspaceAgentLaunchPlatform(projectGroup)).toBe('win32')
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup,
|
||||
name: 'Remote Windows folder',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: null,
|
||||
note: "Use Bob's Windows startup",
|
||||
quickAgent: 'claude',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith(
|
||||
'folder-workspace-1',
|
||||
expect.objectContaining({
|
||||
startup: expect.objectContaining({
|
||||
command: "claude 'Use Bob''s Windows startup'"
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves SSH group ownership when creating and activating a folder workspace', async () => {
|
||||
@@ -634,7 +379,6 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
executionHostId: 'ssh:ssh-1'
|
||||
}
|
||||
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace({ connectionId: 'ssh-1' }))
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup,
|
||||
@@ -644,11 +388,9 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
note: '',
|
||||
quickAgent: null,
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
isRemote: true,
|
||||
runtimeEnvironmentId: null,
|
||||
createFolderWorkspace,
|
||||
onOpenChange
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(createFolderWorkspace).toHaveBeenCalledWith({
|
||||
@@ -657,14 +399,12 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
connectionId: 'ssh-1',
|
||||
linkedTask: null
|
||||
})
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
|
||||
runtimeEnvironmentId: null
|
||||
})
|
||||
})
|
||||
|
||||
it('returns false when folder workspace creation fails without returning a workspace', async () => {
|
||||
const createFolderWorkspace = vi.fn(async () => null)
|
||||
it('returns false when folder workspace creation fails', async () => {
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
await expect(
|
||||
@@ -676,8 +416,7 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
note: '',
|
||||
quickAgent: null,
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace,
|
||||
createFolderWorkspace: vi.fn(async () => null),
|
||||
onOpenChange
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
@@ -685,185 +424,49 @@ describe('submitFolderWorkspaceCreate', () => {
|
||||
expect(onOpenChange).not.toHaveBeenCalled()
|
||||
expect(mocks.activateAndRevealFolderWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('submitFolderWorkspaceCreate native-chat launch draft', () => {
|
||||
const ISSUE_URL = 'https://github.com/stablyai/orca/issues/42'
|
||||
const linkedIssue = {
|
||||
provider: 'github' as const,
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Restore linked quick-create',
|
||||
url: ISSUE_URL,
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
function seededDraftFor(tabId: string): { text: string } | undefined {
|
||||
return useAppStore.getState().nativeChatLaunchDraftByTabId[tabId]
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.activateAndRevealFolderWorkspace.mockReturnValue({ primaryTabId: 'tab-1' })
|
||||
useAppStore.setState({ nativeChatLaunchDraftByTabId: {} })
|
||||
Object.assign(window, {
|
||||
api: { agentTrust: { markTrusted: vi.fn().mockResolvedValue(undefined) } }
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mocks.activateAndRevealFolderWorkspace.mockReset()
|
||||
mocks.ensureAgentStartupInTerminal.mockReset()
|
||||
useAppStore.setState({ nativeChatLaunchDraftByTabId: {} })
|
||||
Reflect.deleteProperty(window, 'api')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('mirrors a startup-paste draft into the chat composer', async () => {
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: linkedIssue,
|
||||
note: '',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(seededDraftFor('tab-1')?.text).toBe(ISSUE_URL)
|
||||
})
|
||||
|
||||
it('mirrors an argv-prefill draft, which never lands in startupPlan.draftPrompt', async () => {
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: linkedIssue,
|
||||
note: '',
|
||||
quickAgent: 'claude',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
// The draft rides in on `--prefill`, so the plan carries no draftPrompt at
|
||||
// all — keying the mirror off it would silently drop this whole branch.
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
expect(startup?.draftPrompt).toBeUndefined()
|
||||
expect(startup?.command).toContain(ISSUE_URL)
|
||||
expect(seededDraftFor('tab-1')?.text).toBe(ISSUE_URL)
|
||||
})
|
||||
|
||||
it('mirrors a multi-line draft into chat', async () => {
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: linkedIssue,
|
||||
note: 'Reproduce on Windows first',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(mocks.ensureAgentStartupInTerminal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
startup: expect.objectContaining({
|
||||
draftPrompt: `Reproduce on Windows first\n\n${ISSUE_URL}`
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(seededDraftFor('tab-1')?.text).toBe(`Reproduce on Windows first\n\n${ISSUE_URL}`)
|
||||
})
|
||||
|
||||
it('does not mirror an unlinked note, which is submitted rather than drafted', async () => {
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: null,
|
||||
note: 'Fix the flaky checkout flow',
|
||||
quickAgent: 'codex',
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
expect(seededDraftFor('tab-1')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('folder-workspace draft: seeded set == chat-opening set', () => {
|
||||
const ISSUE_URL = 'https://github.com/stablyai/orca/issues/42'
|
||||
const linkedIssue = {
|
||||
provider: 'github' as const,
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Restore linked quick-create',
|
||||
url: ISSUE_URL,
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.activateAndRevealFolderWorkspace.mockReturnValue({ primaryTabId: 'tab-1' })
|
||||
useAppStore.setState({ nativeChatLaunchDraftByTabId: {} })
|
||||
Object.assign(window, {
|
||||
api: { agentTrust: { markTrusted: vi.fn().mockResolvedValue(undefined) } }
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mocks.activateAndRevealFolderWorkspace.mockReset()
|
||||
mocks.ensureAgentStartupInTerminal.mockReset()
|
||||
useAppStore.setState({ nativeChatLaunchDraftByTabId: {} })
|
||||
Reflect.deleteProperty(window, 'api')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
// Why: `claude` takes its draft on argv, so `startupPlan.draftPrompt` stays
|
||||
// undefined; `codex` gets a startup paste and sets it. Both must reach the
|
||||
// view-mode gate, and both must agree with what the composer actually holds.
|
||||
it.each([
|
||||
['argv-prefill', 'claude' as const, '', true],
|
||||
['argv-prefill multi-line', 'claude' as const, 'Reproduce on Windows first', true],
|
||||
['startup-paste', 'codex' as const, '', true],
|
||||
['startup-paste multi-line', 'codex' as const, 'Reproduce on Windows first', true]
|
||||
])('%s', async (_label, quickAgent, note, expectMirrored) => {
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem: linkedIssue,
|
||||
note,
|
||||
quickAgent,
|
||||
autoRenameBranchFromWork: false,
|
||||
agentCmdOverrides: {},
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
['claude', ''],
|
||||
['claude', 'Reproduce on Windows first'],
|
||||
['codex', ''],
|
||||
['codex', 'Reproduce on Windows first']
|
||||
] as const)(
|
||||
'keeps the %s linked draft mirror and initial view decision aligned',
|
||||
async (agent, note) => {
|
||||
const linkedWorkItem = {
|
||||
provider: 'github' as const,
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Restore linked quick-create',
|
||||
url: 'https://github.com/stablyai/orca/issues/42',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
const seeded = useAppStore.getState().nativeChatLaunchDraftByTabId['tab-1'] != null
|
||||
const draftText = resolveStartupLaunchDraftText(startup)
|
||||
const opensInChat =
|
||||
decideInitialAgentTabViewMode({
|
||||
await submitFolderWorkspaceCreate({
|
||||
projectGroup: makeProjectGroup(),
|
||||
name: '',
|
||||
lastAutoName: '',
|
||||
linkedWorkItem,
|
||||
note,
|
||||
quickAgent: agent,
|
||||
autoRenameBranchFromWork: false,
|
||||
createFolderWorkspace: vi.fn(async () => makeFolderWorkspace()),
|
||||
onOpenChange: vi.fn()
|
||||
})
|
||||
|
||||
const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup
|
||||
const draftText = resolveStartupLaunchDraftText(startup)
|
||||
const seeded = useAppStore.getState().nativeChatLaunchDraftByTabId['tab-1']?.text
|
||||
const viewMode = decideInitialAgentTabViewMode({
|
||||
experimentalNativeChat: true,
|
||||
openAgentTabsInChatByDefault: true,
|
||||
agent: quickAgent,
|
||||
...(draftText != null
|
||||
? { promptDelivery: 'draft' as const, launchDraftText: draftText }
|
||||
: {})
|
||||
}) === 'chat'
|
||||
agent,
|
||||
...(draftText ? { promptDelivery: 'draft' as const, launchDraftText: draftText } : {})
|
||||
})
|
||||
|
||||
// The draft always reaches the TUI, whichever way it is delivered.
|
||||
expect(`${startup?.command ?? ''}${startup?.draftPrompt ?? ''}`).toContain(ISSUE_URL)
|
||||
expect(seeded).toBe(expectMirrored)
|
||||
expect(opensInChat).toBe(expectMirrored)
|
||||
})
|
||||
expect(draftText).toContain(linkedWorkItem.url)
|
||||
expect(seeded).toBe(draftText)
|
||||
expect(viewMode).toBe('chat')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
|
||||
import { resolveQuickCreateLinkedWorkItemPrompt } from '@/lib/linked-work-item-context'
|
||||
import { seedNativeChatLaunchDraftForAgentTab } from '@/lib/agent-launch-prompt-delivery'
|
||||
import {
|
||||
activateAndRevealFolderWorkspace,
|
||||
type WorktreeStartupPayload
|
||||
@@ -73,6 +74,7 @@ function buildFolderWorkspaceStartup(args: {
|
||||
command: '',
|
||||
launchAgent: agent,
|
||||
agentLaunch,
|
||||
...(linkedDraft ? { launchDraftText: linkedDraft } : {}),
|
||||
// Host overwrites agent_kind from the resolved receipt before the emit, so
|
||||
// this host-resolved launch threads only the surface-owned fields.
|
||||
telemetry: {
|
||||
@@ -168,10 +170,17 @@ export async function submitFolderWorkspaceCreate({
|
||||
: undefined
|
||||
onOpenChange(false)
|
||||
try {
|
||||
activateAndRevealFolderWorkspace(workspace.id, {
|
||||
const activation = activateAndRevealFolderWorkspace(workspace.id, {
|
||||
...(startup ? { startup } : {}),
|
||||
runtimeEnvironmentId
|
||||
})
|
||||
if (activation && activation.primaryTabId && startup?.launchDraftText && quickAgent) {
|
||||
seedNativeChatLaunchDraftForAgentTab({
|
||||
tabId: activation.primaryTabId,
|
||||
agent: quickAgent,
|
||||
text: startup.launchDraftText
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// Why: creation already succeeded. Do not leave the completed create modal
|
||||
// open if the follow-up reveal/startup path hits a transient issue.
|
||||
|
||||
@@ -201,31 +201,6 @@ describe('launchWorkItemDirect', () => {
|
||||
mockApi.agentTrust.markTrusted.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
it('rejects invalid per-launch CLI arguments before creating a workspace', async () => {
|
||||
const { launchWorkItemDirect } = await import('./launch-work-item-direct')
|
||||
|
||||
await expect(
|
||||
launchWorkItemDirect({
|
||||
repoId: 'repo-1',
|
||||
launchSource: 'task_page',
|
||||
openModalFallback: vi.fn(),
|
||||
agentArgs: '--model "unterminated',
|
||||
item: {
|
||||
type: 'issue',
|
||||
number: 42,
|
||||
title: 'Fix invalid saved launch args',
|
||||
url: 'https://github.com/acme/repo/issues/42'
|
||||
}
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
|
||||
expect(mocks.createWorktree).not.toHaveBeenCalled()
|
||||
expect(mocks.ensureDetectedAgents).not.toHaveBeenCalled()
|
||||
expect(mocks.toastError).toHaveBeenCalledWith(
|
||||
'CLI arguments are invalid: Unclosed quote in command template.'
|
||||
)
|
||||
})
|
||||
|
||||
it('passes a resolved PR branch override while using a short PR identity for workspace names', async () => {
|
||||
mocks.ensureDetectedAgents.mockResolvedValue([])
|
||||
mocks.store.settings = {}
|
||||
@@ -279,7 +254,8 @@ describe('launchWorkItemDirect', () => {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'refs/remotes/origin/main'
|
||||
'refs/remotes/origin/main',
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
@@ -335,33 +311,21 @@ describe('launchWorkItemDirect', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(mocks.createWorktree).toHaveBeenCalledWith(
|
||||
'repo-1',
|
||||
'eng-42-ship-linear-parity',
|
||||
undefined,
|
||||
'inherit',
|
||||
undefined,
|
||||
'sidebar',
|
||||
'Ship Linear parity',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'ENG-42',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
const createArgs = mocks.createWorktree.mock.calls[0]
|
||||
expect(createArgs?.[1]).toBe('eng-42-ship-linear-parity')
|
||||
expect(createArgs?.[6]).toBe('Ship Linear parity')
|
||||
expect(createArgs?.[10]).toBe('codex')
|
||||
expect(createArgs?.[11]).toBe('ENG-42')
|
||||
expect(createArgs?.[25]).toEqual({
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'codex' },
|
||||
prompt:
|
||||
'Linked Linear issue: ENG-42\nhttps://linear.app/acme/issue/ENG-42/ship-linear-parity\n',
|
||||
promptDelivery: 'draft',
|
||||
allowEmptyPromptLaunch: true
|
||||
},
|
||||
agentLaunchTelemetry: { launch_source: 'task_page', request_kind: 'new' }
|
||||
})
|
||||
})
|
||||
|
||||
it('prefills a link-only Linear reference without source context', async () => {
|
||||
@@ -399,35 +363,23 @@ describe('launchWorkItemDirect', () => {
|
||||
'Linked Linear issue: ENG-42',
|
||||
'https://linear.app/acme/issue/ENG-42/ship-linear-parity'
|
||||
].join('\n')
|
||||
expect(buildAgentDraftLaunchPlan).toHaveBeenCalledWith({
|
||||
agent: 'claude',
|
||||
draft: `${expectedDraft}\n`,
|
||||
cmdOverrides: {},
|
||||
agentArgs: '--dangerously-skip-permissions',
|
||||
agentEnv: {},
|
||||
sessionOptions: undefined,
|
||||
platform: 'win32',
|
||||
isRemote: false
|
||||
})
|
||||
expect(buildAgentStartupPlan).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agent: 'claude',
|
||||
prompt: '',
|
||||
expect(mocks.createWorktree.mock.calls[0]?.[25]).toEqual({
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'claude' },
|
||||
prompt: `${expectedDraft}\n`,
|
||||
promptDelivery: 'draft',
|
||||
allowEmptyPromptLaunch: true
|
||||
})
|
||||
)
|
||||
},
|
||||
agentLaunchTelemetry: { launch_source: 'task_page', request_kind: 'new' }
|
||||
})
|
||||
expect(buildAgentDraftLaunchPlan).not.toHaveBeenCalled()
|
||||
expect(buildAgentStartupPlan).not.toHaveBeenCalled()
|
||||
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith(
|
||||
'repo-1::/repo/worktree',
|
||||
expect.objectContaining({
|
||||
startup: expect.objectContaining({
|
||||
command: expect.stringContaining('Linked Linear issue: ENG-42')
|
||||
})
|
||||
backendStartupTerminalSpawned: true
|
||||
})
|
||||
)
|
||||
const startupCommand = mocks.activateAndRevealWorktree.mock.calls[0]?.[1]?.startup?.command
|
||||
expect(startupCommand).toContain('https://linear.app/acme/issue/ENG-42/ship-linear-parity')
|
||||
expect(startupCommand).not.toContain('The distinctive Linear body text is here.')
|
||||
expect(startupCommand).not.toContain('--- BEGIN LINKED WORK ITEM CONTEXT ---')
|
||||
expect(pasteDraftWhenAgentReady).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -459,12 +411,14 @@ describe('launchWorkItemDirect', () => {
|
||||
createdAt: expect.any(Number)
|
||||
})
|
||||
expect(mocks.seedNativeChatLaunchPrompt).not.toHaveBeenCalled()
|
||||
// Why: the draft is inside `--prefill`, so the plan sets no draftPrompt.
|
||||
// launchDraftText is the only thing that lets the view-mode gate see a
|
||||
// draft here — without it this tab opens in chat unconditionally.
|
||||
const startup = mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]?.startup
|
||||
expect(startup?.draftPrompt).toBeUndefined()
|
||||
expect(startup?.launchDraftText).toBe('https://github.com/acme/repo/issues/12')
|
||||
expect(mocks.createWorktree.mock.calls[0]?.[25]).toMatchObject({
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'claude' },
|
||||
prompt: 'https://github.com/acme/repo/issues/12',
|
||||
promptDelivery: 'draft'
|
||||
}
|
||||
})
|
||||
expect(mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]).not.toHaveProperty('startup')
|
||||
})
|
||||
|
||||
it('seeds the chat-composer launch draft for a multi-line Linear draft launch', async () => {
|
||||
@@ -524,23 +478,17 @@ describe('launchWorkItemDirect', () => {
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(buildAgentDraftLaunchPlan).not.toHaveBeenCalled()
|
||||
expect(pasteDraftWhenAgentReady).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tabId: 'tab-1',
|
||||
content: 'Use this explicit user prompt.',
|
||||
agent: 'claude',
|
||||
submit: true,
|
||||
forcePaste: true,
|
||||
onTimeout: expect.any(Function)
|
||||
})
|
||||
)
|
||||
expect(mocks.seedNativeChatLaunchPrompt).toHaveBeenCalledWith({
|
||||
tabId: 'tab-1',
|
||||
agent: 'claude',
|
||||
text: 'Use this explicit user prompt.',
|
||||
createdAt: expect.any(Number)
|
||||
expect(mocks.createWorktree.mock.calls[0]?.[25]).toEqual({
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'claude' },
|
||||
prompt: 'Use this explicit user prompt.',
|
||||
allowEmptyPromptLaunch: true
|
||||
},
|
||||
agentLaunchTelemetry: { launch_source: 'task_page', request_kind: 'new' }
|
||||
})
|
||||
expect(buildAgentDraftLaunchPlan).not.toHaveBeenCalled()
|
||||
expect(pasteDraftWhenAgentReady).not.toHaveBeenCalled()
|
||||
expect(mocks.seedNativeChatLaunchPrompt).not.toHaveBeenCalled()
|
||||
expect(mocks.seedNativeChatLaunchDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -585,38 +533,20 @@ describe('launchWorkItemDirect', () => {
|
||||
|
||||
expect(mocks.store.ensureDetectedAgents).not.toHaveBeenCalled()
|
||||
expect(mocks.store.ensureRemoteDetectedAgents).toHaveBeenCalledWith('ssh-1')
|
||||
expect(mockApi.agentTrust.markTrusted).toHaveBeenCalledWith({
|
||||
preset: 'cursor',
|
||||
workspacePath: '/home/orca/repo-worktrees/issue-77',
|
||||
connectionId: 'ssh-1'
|
||||
})
|
||||
expect(buildAgentDraftLaunchPlan).toHaveBeenCalledWith({
|
||||
agent: 'cursor',
|
||||
draft: 'https://github.com/acme/repo/issues/77',
|
||||
cmdOverrides: {},
|
||||
agentArgs: '--yolo',
|
||||
agentEnv: {},
|
||||
sessionOptions: undefined,
|
||||
platform: 'linux',
|
||||
isRemote: true
|
||||
})
|
||||
expect(buildAgentStartupPlan).toHaveBeenCalledWith({
|
||||
agent: 'cursor',
|
||||
prompt: '',
|
||||
cmdOverrides: {},
|
||||
agentArgs: '--yolo',
|
||||
agentEnv: {},
|
||||
sessionOptions: undefined,
|
||||
platform: 'linux',
|
||||
isRemote: true,
|
||||
allowEmptyPromptLaunch: true
|
||||
expect(mockApi.agentTrust.markTrusted).not.toHaveBeenCalled()
|
||||
expect(buildAgentDraftLaunchPlan).not.toHaveBeenCalled()
|
||||
expect(buildAgentStartupPlan).not.toHaveBeenCalled()
|
||||
expect(mocks.createWorktree.mock.calls[0]?.[25]).toMatchObject({
|
||||
agentLaunch: {
|
||||
selection: { kind: 'agent', agent: 'cursor' },
|
||||
prompt: 'https://github.com/acme/repo/issues/77',
|
||||
promptDelivery: 'draft'
|
||||
}
|
||||
})
|
||||
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith(
|
||||
'wt-ssh',
|
||||
expect.objectContaining({
|
||||
startup: expect.objectContaining({
|
||||
draftPrompt: 'https://github.com/acme/repo/issues/77'
|
||||
})
|
||||
backendStartupTerminalSpawned: true
|
||||
})
|
||||
)
|
||||
expect(pasteDraftWhenAgentReady).not.toHaveBeenCalled()
|
||||
@@ -686,12 +616,13 @@ describe('launchWorkItemDirect', () => {
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(mocks.activateAndRevealWorktree).toHaveBeenCalled()
|
||||
const activationOptions = mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]
|
||||
expect(activationOptions.startup.command).toContain(
|
||||
`command test -n "$fish_pid" && set --erase -g ORCA_PI_PREFILL; command test -z "$fish_pid" && unset ORCA_PI_PREFILL; true`
|
||||
)
|
||||
expect(activationOptions.startup.command).not.toContain('Remove-Item Env:ORCA_PI_PREFILL')
|
||||
expect(mocks.createWorktree.mock.calls[0]?.[25]).toMatchObject({
|
||||
agentLaunch: { selection: { kind: 'agent', agent: 'pi' } }
|
||||
})
|
||||
expect(mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]).toMatchObject({
|
||||
backendStartupTerminalSpawned: true
|
||||
})
|
||||
expect(mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]).not.toHaveProperty('startup')
|
||||
})
|
||||
|
||||
it('uses the repo SSH connection when the created worktree is not hydrated yet', async () => {
|
||||
@@ -730,10 +661,10 @@ describe('launchWorkItemDirect', () => {
|
||||
|
||||
expect(mocks.ensureRemoteDetectedAgents).toHaveBeenCalledWith('ssh-1')
|
||||
expect(mocks.ensureDetectedAgents).not.toHaveBeenCalled()
|
||||
const activationOptions = mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]
|
||||
expect(activationOptions.startup.command).toContain(
|
||||
`command test -n "$fish_pid" && set --erase -g ORCA_PI_PREFILL; command test -z "$fish_pid" && unset ORCA_PI_PREFILL; true`
|
||||
)
|
||||
expect(mocks.createWorktree.mock.calls[0]?.[25]).toMatchObject({
|
||||
agentLaunch: { selection: { kind: 'agent', agent: 'pi' } }
|
||||
})
|
||||
expect(mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]).not.toHaveProperty('startup')
|
||||
})
|
||||
|
||||
it('plans direct local Windows-path launches with POSIX startup for WSL project runtime', async () => {
|
||||
@@ -781,11 +712,10 @@ describe('launchWorkItemDirect', () => {
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(buildAgentStartupPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agent: 'codex',
|
||||
platform: 'linux'
|
||||
})
|
||||
)
|
||||
expect(buildAgentStartupPlan).not.toHaveBeenCalled()
|
||||
expect(mocks.createWorktree.mock.calls[0]?.[25]).toMatchObject({
|
||||
agentLaunch: { selection: { kind: 'agent', agent: 'codex' } }
|
||||
})
|
||||
expect(mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]).not.toHaveProperty('startup')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { GitPushTarget, SetupDecision, TuiAgent } from '../../../shared/typ
|
||||
import { getLinearIssueWorkspaceName } from '../../../shared/workspace-name'
|
||||
import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity'
|
||||
import { getDirectWorkItemDraftContent } from '@/lib/launch-work-item-direct-draft'
|
||||
import { seedNativeChatLaunchDraftForAgentTab } from '@/lib/agent-launch-prompt-delivery'
|
||||
import {
|
||||
resolveDirectPrStartPoint,
|
||||
resolveDirectSetupDecision
|
||||
@@ -238,6 +239,16 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom
|
||||
toast.error(workspaceActivationErrorMessage())
|
||||
return false
|
||||
}
|
||||
if (activation.primaryTabId && requestedAgent && promptDelivery === 'draft') {
|
||||
const draft = draftContent.trim()
|
||||
if (draft) {
|
||||
seedNativeChatLaunchDraftForAgentTab({
|
||||
tabId: activation.primaryTabId,
|
||||
agent: requestedAgent,
|
||||
text: draftContent
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create workspace.'
|
||||
toast.error(message)
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
WorkspaceLinkedItem
|
||||
} from '../../../shared/types'
|
||||
import type { AgentLaunchSpawnRequest } from '../../../shared/agent-launch-spawn-request'
|
||||
import type { AgentStartupPlan } from '@/lib/tui-agent-startup'
|
||||
import type { AgentStartedTelemetry } from '@/lib/worktree-activation'
|
||||
import type { TaskSourceContext, WorkspaceRunContext } from '../../../shared/task-source-context'
|
||||
import type { AgentStartupPlan } from '../../../shared/tui-agent-startup'
|
||||
@@ -97,6 +98,10 @@ export type WorktreeCreationRequest = {
|
||||
pendingFirstAgentMessageRename: boolean
|
||||
/** Post-create note persisted as the worktree comment. */
|
||||
note: string
|
||||
/** Legacy renderer-owned startup retained for queued pre-host-launch requests. */
|
||||
startupPlan?: AgentStartupPlan | null
|
||||
quickPrompt?: string
|
||||
launchDraftPrompt?: string
|
||||
/** Telemetry emitted renderer-side off the host's `launched` receipt (the host
|
||||
* create-spawn threads no telemetry). Null for blank-shell creates. */
|
||||
quickTelemetry: AgentStartedTelemetry | null
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
import type { BuiltInTuiAgent } from './types'
|
||||
|
||||
// Node CLIs whose shims launch a generic script (cli.js, versioned index.js), so
|
||||
// only the exact install path is an authoritative identity signal.
|
||||
export const EXACT_NODE_ENTRYPOINT_IDENTITIES: readonly {
|
||||
pattern: RegExp
|
||||
agent: TuiAgent
|
||||
agent: BuiltInTuiAgent
|
||||
processName: string
|
||||
}[] = [
|
||||
// Why: Cursor's native Windows launcher runs a generic versioned index.js,
|
||||
|
||||
Reference in New Issue
Block a user