From 47bb90839fc710cb852164bbb7cb3717f41277fb Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:40:49 -0700 Subject: [PATCH] minor change --- src/main/ipc/pty/ipc/spawn-commit-persist.ts | 2 +- src/renderer/src/components/Terminal.tsx | 1 - .../dashboard-worktree-launch-options.ts | 2 +- .../folder-workspace-composer-submit.test.ts | 691 ++++-------------- .../folder-workspace-composer-submit.ts | 11 +- .../src/lib/launch-work-item-direct.test.ts | 214 ++---- .../src/lib/launch-work-item-direct.ts | 11 + .../src/lib/pending-worktree-creation.ts | 5 + .../agent-node-entrypoint-identities.ts | 4 +- 9 files changed, 249 insertions(+), 692 deletions(-) diff --git a/src/main/ipc/pty/ipc/spawn-commit-persist.ts b/src/main/ipc/pty/ipc/spawn-commit-persist.ts index be9cb31394a..be17e3c331a 100644 --- a/src/main/ipc/pty/ipc/spawn-commit-persist.ts +++ b/src/main/ipc/pty/ipc/spawn-commit-persist.ts @@ -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, diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index cebf7de2fc5..ba286d798ab 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -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 { diff --git a/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts b/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts index 7c5f10068a3..3f8e7f9eaf6 100644 --- a/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts +++ b/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts @@ -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 } diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts index a96564b6652..7e194663e28 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts @@ -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>() return { ...actual, activateAndRevealFolderWorkspace: mocks.activateAndRevealFolderWorkspace } }) -vi.mock('@/lib/new-workspace', async (importOriginal) => { - const actual = await importOriginal() - 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 = {}): 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') + } + ) }) diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts index 1ac42cb3cb3..e8a1f48c396 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts @@ -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. diff --git a/src/renderer/src/lib/launch-work-item-direct.test.ts b/src/renderer/src/lib/launch-work-item-direct.test.ts index e70526aa3a7..abd5b05d40f 100644 --- a/src/renderer/src/lib/launch-work-item-direct.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct.test.ts @@ -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') }) }) diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index e3f0aaeb6ff..68401497a11 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -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) diff --git a/src/renderer/src/lib/pending-worktree-creation.ts b/src/renderer/src/lib/pending-worktree-creation.ts index fd8e0deb308..f63cb4c7d61 100644 --- a/src/renderer/src/lib/pending-worktree-creation.ts +++ b/src/renderer/src/lib/pending-worktree-creation.ts @@ -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 diff --git a/src/shared/agent-node-entrypoint-identities.ts b/src/shared/agent-node-entrypoint-identities.ts index e71118312d0..17939c15239 100644 --- a/src/shared/agent-node-entrypoint-identities.ts +++ b/src/shared/agent-node-entrypoint-identities.ts @@ -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,