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.
This commit is contained in:
Tony Box
2026-09-15 06:08:34 +00:00
committed by GitHub
parent dc61db8e03
commit 8031b5e217
2 changed files with 58 additions and 2 deletions
+14 -1
View File
@@ -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<string, string | undefined> = 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 {
@@ -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 {