diff --git a/agent-support/vscode/package.json b/agent-support/vscode/package.json index b144c4f773..108d346c39 100644 --- a/agent-support/vscode/package.json +++ b/agent-support/vscode/package.json @@ -29,6 +29,12 @@ "default": false, "description": "Enable notifications for AI and human edit detection." }, + "gitai.binaryPath": { + "type": "string", + "default": "", + "scope": "machine", + "description": "Path to the git-ai binary. Leave empty to use git-ai from PATH." + }, "gitai.experiments.aiTabTracking": { "type": "boolean", "default": false, diff --git a/agent-support/vscode/src/ai-edit-manager.ts b/agent-support/vscode/src/ai-edit-manager.ts index d5ea05cc7a..5c6964bc36 100644 --- a/agent-support/vscode/src/ai-edit-manager.ts +++ b/agent-support/vscode/src/ai-edit-manager.ts @@ -1,9 +1,9 @@ import * as vscode from "vscode"; import * as path from "path"; import * as fs from "fs"; -import { exec, spawn } from "child_process"; +import { execFile, spawn } from "child_process"; import { isVersionSatisfied } from "./utils/semver"; -import { getGitAiBinary } from "./utils/binary-path"; +import { getGitAiBinary, resolveGitAiBinary } from "./utils/binary-path"; import { MIN_GIT_AI_VERSION, GIT_AI_INSTALL_DOCS_URL } from "./consts"; import { getGitRepoRoot } from "./utils/git-api"; import { shouldSkipLegacyCopilotHooks } from "./utils/vscode-hooks"; @@ -65,6 +65,11 @@ export class AIEditManager { return this.legacyCopilotHooksEnabled; } + public resetGitAiCheckCache(): void { + this.gitAiVersion = null; + this.hasShownGitAiErrorMessage = false; + } + private cleanupOldCheckpointEntries(): void { const now = Date.now(); const entriesToDelete: string[] = []; @@ -493,8 +498,9 @@ export class AIEditManager { return true; } // TODO Consider only re-checking every X attempts + await resolveGitAiBinary(); return new Promise((resolve) => { - exec("git-ai --version", (error, stdout, stderr) => { + execFile(getGitAiBinary(), ["--version"], (error, stdout, stderr) => { if (error) { if (!this.hasShownGitAiErrorMessage) { // Show startup notification diff --git a/agent-support/vscode/src/blame-lens-manager.ts b/agent-support/vscode/src/blame-lens-manager.ts index e9a3d809cd..3ebf03f57c 100644 --- a/agent-support/vscode/src/blame-lens-manager.ts +++ b/agent-support/vscode/src/blame-lens-manager.ts @@ -195,6 +195,21 @@ export class BlameLensManager { if (event.affectsConfiguration('gitai.blameMode')) { this.handleBlameModeChange(); } + if (event.affectsConfiguration('gitai.binaryPath')) { + this.blameService.resetGitAiAvailability(); + // Discard any cached results from the old binary so the next request + // uses the new path rather than returning stale attributions. + this.currentBlameResult = null; + this.pendingBlameRequest = null; + this.casFetchInProgress.clear(); + const editor = vscode.window.activeTextEditor; + if (editor && this.blameMode !== 'off') { + if (this.blameMode === 'all') { + this.requestBlameForFullFile(editor); + } + this.updateStatusBar(editor); + } + } // Rebuild color decorations if workbench color customizations change if (event.affectsConfiguration('workbench.colorCustomizations')) { console.log('[git-ai] Color customizations changed, rebuilding color decorations'); diff --git a/agent-support/vscode/src/blame-service.ts b/agent-support/vscode/src/blame-service.ts index c31800a57b..ff955b850f 100644 --- a/agent-support/vscode/src/blame-service.ts +++ b/agent-support/vscode/src/blame-service.ts @@ -207,6 +207,12 @@ export class BlameService { public clearCache(): void { this.contentCache.clear(); } + + public resetGitAiAvailability(): void { + this.gitAiAvailable = null; + this.hasShownInstallMessage = false; + this.clearCache(); + } /** * Cancel all pending operations and clear cache. @@ -473,4 +479,3 @@ export class BlameService { } } - diff --git a/agent-support/vscode/src/extension.ts b/agent-support/vscode/src/extension.ts index a338225c0f..0322c95eb9 100644 --- a/agent-support/vscode/src/extension.ts +++ b/agent-support/vscode/src/extension.ts @@ -8,7 +8,7 @@ import { detectIDEHost, IDEHostKindVSCode } from "./utils/host-kind"; import { AITabEditManager } from "./ai-tab-edit-manager"; import { Config } from "./utils/config"; import { BlameLensManager, registerBlameLensCommands } from "./blame-lens-manager"; -import { initBinaryResolver } from "./utils/binary-path"; +import { initBinaryResolver, resetGitAiBinaryCache } from "./utils/binary-path"; import { KnownHumanCheckpointManager } from "./known-human-checkpoint-manager"; function getDistinctId(): string { @@ -57,6 +57,16 @@ export function activate(context: vscode.ExtensionContext) { } const aiEditManager = new AIEditManager(context); + // Resets the resolved binary path and AI-edit version check when binaryPath changes. + // BlameLensManager has its own listener that resets blame service availability + refreshes UI. + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("gitai.binaryPath")) { + resetGitAiBinaryCache(); + aiEditManager.resetGitAiCheckCache(); + } + }) + ); const knownHumanManager = new KnownHumanCheckpointManager( vscode.version, diff --git a/agent-support/vscode/src/utils/binary-path.ts b/agent-support/vscode/src/utils/binary-path.ts index e3e04c1f13..e75fd4bc69 100644 --- a/agent-support/vscode/src/utils/binary-path.ts +++ b/agent-support/vscode/src/utils/binary-path.ts @@ -1,10 +1,13 @@ import { execFile } from "child_process"; +import * as fs from "fs"; import * as os from "os"; import * as vscode from "vscode"; +import { Config } from "./config"; let resolvedPath: string | null = null; let resolvePromise: Promise | null = null; let extensionMode: vscode.ExtensionMode | null = null; +let hasShownBinaryPathWarning = false; /** * Call once at activation to pass in the extension context's mode. @@ -13,14 +16,32 @@ export function initBinaryResolver(mode: vscode.ExtensionMode): void { extensionMode = mode; } +export function resetGitAiBinaryCache(): void { + resolvedPath = null; + resolvePromise = null; + hasShownBinaryPathWarning = false; +} + /** * Resolve the full path to the `git-ai` binary using a login shell. - * Only runs in development mode — in production the plain "git-ai" name - * is used directly (relies on the process PATH). + * A configured binary path always wins. Otherwise this only runs in + * development mode; in production the plain "git-ai" name is used directly. * * The result is cached after the first successful resolution. */ export function resolveGitAiBinary(): Promise { + const configuredPath = Config.getBinaryPath(); + if (configuredPath) { + if (!fs.existsSync(configuredPath) && !hasShownBinaryPathWarning) { + hasShownBinaryPathWarning = true; + vscode.window.showWarningMessage( + `git-ai: configured binary path does not exist: "${configuredPath}". Check the gitai.binaryPath setting.` + ); + } + resolvePromise = null; + return Promise.resolve(configuredPath); + } + // Skip shell resolution in production — just use "git-ai" if (extensionMode !== vscode.ExtensionMode.Development) { return Promise.resolve(null); @@ -73,5 +94,5 @@ export function resolveGitAiBinary(): Promise { * (which relies on the current process PATH). */ export function getGitAiBinary(): string { - return resolvedPath || "git-ai"; + return Config.getBinaryPath() || resolvedPath || "git-ai"; } diff --git a/agent-support/vscode/src/utils/config.ts b/agent-support/vscode/src/utils/config.ts index 42e6389c22..6182409d2f 100644 --- a/agent-support/vscode/src/utils/config.ts +++ b/agent-support/vscode/src/utils/config.ts @@ -15,6 +15,11 @@ export class Config { return !!this.getRoot().get("experiments.aiTabTracking"); } + static getBinaryPath(): string | null { + const binaryPath = this.getRoot().get("binaryPath")?.trim(); + return binaryPath || null; + } + static getBlameMode(): BlameMode { const mode = this.getRoot().get("blameMode"); if (mode === 'off' || mode === 'line' || mode === 'all') { @@ -28,4 +33,3 @@ export class Config { } } -