diff --git a/src/renderer/src/lib/agent-launch-platform.ts b/src/renderer/src/lib/agent-launch-platform.ts new file mode 100644 index 000000000..a16f2c0fa --- /dev/null +++ b/src/renderer/src/lib/agent-launch-platform.ts @@ -0,0 +1,12 @@ +import { isWindowsAbsolutePathLike } from '../../../shared/cross-platform-path' +import { CLIENT_PLATFORM } from '@/lib/new-workspace' +import type { AppState } from '@/store' + +export function getAgentLaunchPlatformForRepo( + repo: Pick +): NodeJS.Platform { + if (!repo.connectionId) { + return CLIENT_PLATFORM + } + return isWindowsAbsolutePathLike(repo.path) ? 'win32' : 'linux' +} 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 1304151bf..d71242530 100644 --- a/src/renderer/src/lib/launch-work-item-direct.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct.test.ts @@ -4,6 +4,7 @@ import type { AppState } from '@/store' const storeState = vi.hoisted(() => ({ value: {} as Partial & { ensureDetectedAgents: ReturnType + ensureRemoteDetectedAgents: ReturnType createWorktree: ReturnType updateWorktreeMeta: ReturnType setSidebarOpen: ReturnType @@ -81,10 +82,16 @@ vi.mock('@/lib/telemetry', () => ({ })) import { launchWorkItemDirect } from './launch-work-item-direct' +import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft' +import { buildAgentDraftLaunchPlan, buildAgentStartupPlan } from '@/lib/tui-agent-startup' +import { pickTuiAgent } from '../../../shared/tui-agent-selection' const mockApi = { worktrees: { resolvePrBase: vi.fn() + }, + agentTrust: { + markTrusted: vi.fn() } } @@ -109,6 +116,7 @@ describe('launchWorkItemDirect', () => { ], settings: {}, ensureDetectedAgents: vi.fn(async () => []), + ensureRemoteDetectedAgents: vi.fn(async () => []), createWorktree: vi.fn(async () => ({ worktree: { id: 'wt-1', path: '/repo/../worktrees/fix' } })), @@ -117,6 +125,7 @@ describe('launchWorkItemDirect', () => { } as typeof storeState.value // @ts-expect-error -- test shim globalThis.window = { api: mockApi } + mockApi.agentTrust.markTrusted.mockResolvedValue(undefined) }) it('passes a resolved PR branch override while using a short PR identity for workspace names', async () => { @@ -187,4 +196,70 @@ describe('launchWorkItemDirect', () => { undefined ) }) + + it('uses remote cursor-agent detection, trust preflight, and paste launch for SSH repos', async () => { + storeState.value.repos = [ + { + id: 'repo-ssh', + path: '/home/orca/repo', + displayName: 'Remote Repo', + badgeColor: '#000', + addedAt: 0, + connectionId: 'ssh-1' + } + ] as AppState['repos'] + storeState.value.settings = { defaultTuiAgent: 'cursor' } as AppState['settings'] + storeState.value.ensureRemoteDetectedAgents.mockResolvedValue(['cursor']) + vi.mocked(pickTuiAgent).mockReturnValue('cursor') + vi.mocked(buildAgentDraftLaunchPlan).mockReturnValue(null) + vi.mocked(buildAgentStartupPlan).mockReturnValue({ + agent: 'cursor', + launchCommand: 'cursor-agent', + expectedProcess: 'cursor-agent', + followupPrompt: null + }) + storeState.value.createWorktree.mockResolvedValue({ + worktree: { id: 'wt-ssh', path: '/home/orca/repo-worktrees/issue-77' } + }) + + await launchWorkItemDirect({ + repoId: 'repo-ssh', + launchSource: 'task_page', + telemetrySource: 'sidebar', + openModalFallback: vi.fn(), + item: { + type: 'issue', + number: 77, + title: 'Fix cursor direct launch', + url: 'https://github.com/acme/repo/issues/77' + } + }) + + expect(storeState.value.ensureDetectedAgents).not.toHaveBeenCalled() + expect(storeState.value.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: {}, + platform: 'linux' + }) + expect(buildAgentStartupPlan).toHaveBeenCalledWith({ + agent: 'cursor', + prompt: '', + cmdOverrides: {}, + platform: 'linux', + allowEmptyPromptLaunch: true + }) + expect(pasteDraftWhenAgentReady).toHaveBeenCalledWith({ + tabId: 'tab-1', + content: 'https://github.com/acme/repo/issues/77', + agent: 'cursor', + onTimeout: expect.any(Function) + }) + }) }) diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index 176cc5ba3..b287b67d7 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -7,7 +7,6 @@ import { pickTuiAgent } from '../../../shared/tui-agent-selection' import { activateAndRevealWorktree, type AgentStartedTelemetry } from '@/lib/worktree-activation' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { - CLIENT_PLATFORM, getWorkspaceIntentName, getSetupConfig, getWorkspaceSeedName, @@ -20,6 +19,7 @@ import { import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed' import { checkRuntimeHooks } from '@/runtime/runtime-hooks-client' import { track, tuiAgentToAgentKind } from '@/lib/telemetry' +import { getAgentLaunchPlatformForRepo } from '@/lib/agent-launch-platform' import type { GitPushTarget, GitHubPrStartPoint, @@ -202,7 +202,9 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom const settings = store.settings // Why: agent detection shells out and can be cold/slow. Start it now, but // don't let it serialize setup-policy resolution or git worktree creation. - const detectedAgentsPromise = store.ensureDetectedAgents() + const detectedAgentsPromise = repo.connectionId + ? store.ensureRemoteDetectedAgents(repo.connectionId) + : store.ensureDetectedAgents() const setupResolution = await resolveSetupDecision(repoId, repo) if (setupResolution.kind === 'needs-modal') { @@ -274,6 +276,7 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom ) worktreeId = result.worktree.id const worktreePath = result.worktree.path + const agentLaunchPlatform = getAgentLaunchPlatformForRepo(repo) const detectedIds = new Set(await detectedAgentsPromise) effectiveAgent = pickTuiAgent( @@ -302,7 +305,8 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom try { await window.api.agentTrust.markTrusted({ preset: preflight, - workspacePath: worktreePath + workspacePath: worktreePath, + ...(repo.connectionId ? { connectionId: repo.connectionId } : {}) }) } catch { // Best-effort: continue with launch even if the trust write @@ -324,7 +328,7 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom agent: effectiveAgent, draft: draftContent, cmdOverrides: settings?.agentCmdOverrides ?? {}, - platform: CLIENT_PLATFORM + platform: agentLaunchPlatform }) if (draftLaunchPlan) { startupPlan = { @@ -340,7 +344,7 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom agent: effectiveAgent, prompt: '', cmdOverrides: settings?.agentCmdOverrides ?? {}, - platform: CLIENT_PLATFORM, + platform: agentLaunchPlatform, allowEmptyPromptLaunch: true }) }