From 8031b5e21780bc02762522f14d7715a7e59abdce Mon Sep 17 00:00:00 2001 From: Tony Box Date: Tue, 15 Sep 2026 06:08:34 +0000 Subject: [PATCH] fix(daemon): prefer direct Copilot CLI (#7974) Avoid VS Code's interactive Copilot bootstrapper when a direct CLI install is available, while retaining the bootstrapper as a fallback. --- apps/daemon/src/runtimes/executables.ts | 15 ++++++- .../tests/runtimes/env-and-detection.test.ts | 45 ++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/apps/daemon/src/runtimes/executables.ts b/apps/daemon/src/runtimes/executables.ts index effba3c395..ebde184d23 100644 --- a/apps/daemon/src/runtimes/executables.ts +++ b/apps/daemon/src/runtimes/executables.ts @@ -201,6 +201,18 @@ function configuredExecutableOverride( return executableFilePath(configuredEnv?.[envKey] ?? process.env[envKey]); } +function orderPathCandidatesForAgent(agentId: string, candidates: string[]): string[] { + if (agentId !== 'copilot') return candidates; + const isVsCodeBootstrap = (candidate: string) => + candidate.replace(/\\/g, '/').toLowerCase().includes( + '/code/user/globalstorage/github.copilot-chat/copilotcli/', + ); + return [ + ...candidates.filter((candidate) => !isVsCodeBootstrap(candidate)), + ...candidates.filter(isVsCodeBootstrap), + ]; +} + export function resolveAmrOpenCodeExecutable( env: Record = process.env, ): string | null { @@ -421,10 +433,11 @@ export function inspectAgentExecutableResolution( pathCandidates.push(resolved); } } + const orderedPathCandidates = orderPathCandidatesForAgent(def.id, pathCandidates); // First hit among what is left. Plain order is not enough on its own — the // first file that merely *exists* can be a shim detection already proved // dead, which is why those are filtered out above rather than ranked below. - const pathResolvedPath: string | null = pathCandidates[0] ?? null; + const pathResolvedPath: string | null = orderedPathCandidates[0] ?? null; const builtInPath = packagedBuiltInExecutable(def, configuredEnv); const appBundlePath = codexAppBundleExecutable(def); return { diff --git a/apps/daemon/tests/runtimes/env-and-detection.test.ts b/apps/daemon/tests/runtimes/env-and-detection.test.ts index 1ea53cb75c..2a10f80724 100644 --- a/apps/daemon/tests/runtimes/env-and-detection.test.ts +++ b/apps/daemon/tests/runtimes/env-and-detection.test.ts @@ -1,7 +1,7 @@ import { symlinkSync } from 'node:fs'; import { test, vi } from 'vitest'; import { homedir } from 'node:os'; -import { dirname, relative, resolve } from 'node:path'; +import { delimiter, dirname, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import * as platform from '@open-design/platform'; import { @@ -515,6 +515,49 @@ test('resolveAgentExecutable supports configured binary overrides for non-Codex } }); +test('resolveAgentExecutable prefers a direct Copilot CLI over the VS Code bootstrapper', () => { + const dir = mkdtempSync(join(tmpdir(), 'od-copilot-bin-order-')); + try { + return withEnvSnapshot(['PATH', 'PATHEXT', 'OD_AGENT_HOME'], () => { + const vscodeDir = join( + dir, + 'Code', + 'User', + 'globalStorage', + 'github.copilot-chat', + 'copilotCli', + ); + const npmDir = join(dir, 'npm'); + mkdirSync(vscodeDir, { recursive: true }); + mkdirSync(npmDir, { recursive: true }); + const windows = process.platform === 'win32'; + const vscodeBootstrap = join(vscodeDir, windows ? 'copilot.BAT' : 'copilot'); + const directCli = join(npmDir, windows ? 'copilot.CMD' : 'copilot'); + writeFileSync(vscodeBootstrap, windows ? '@exit /b 0\n' : '#!/bin/sh\nexit 0\n'); + writeFileSync(directCli, windows ? '@exit /b 0\n' : '#!/bin/sh\nexit 0\n'); + if (!windows) { + chmodSync(vscodeBootstrap, 0o755); + chmodSync(directCli, 0o755); + } + process.env.PATH = [vscodeDir, npmDir].join(delimiter); + process.env.PATHEXT = '.EXE;.BAT;.CMD'; + process.env.OD_AGENT_HOME = dir; + + assert.equal( + resolveAgentExecutable(minimalAgentDef({ id: 'copilot', bin: 'copilot' })), + directCli, + ); + rmSync(directCli); + assert.equal( + resolveAgentExecutable(minimalAgentDef({ id: 'copilot', bin: 'copilot' })), + vscodeBootstrap, + ); + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test('resolveAgentExecutable prefers opencode-cli before desktop opencode fallback', () => { const dir = mkdtempSync(join(tmpdir(), 'od-opencode-cli-')); try {