Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions agent-support/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions agent-support/vscode/src/ai-edit-manager.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions agent-support/vscode/src/blame-lens-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Comment thread
clarete marked this conversation as resolved.
// Rebuild color decorations if workbench color customizations change
if (event.affectsConfiguration('workbench.colorCustomizations')) {
console.log('[git-ai] Color customizations changed, rebuilding color decorations');
Expand Down
7 changes: 6 additions & 1 deletion agent-support/vscode/src/blame-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -473,4 +479,3 @@ export class BlameService {
}
}


12 changes: 11 additions & 1 deletion agent-support/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 24 additions & 3 deletions agent-support/vscode/src/utils/binary-path.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> | null = null;
let extensionMode: vscode.ExtensionMode | null = null;
let hasShownBinaryPathWarning = false;

/**
* Call once at activation to pass in the extension context's mode.
Expand All @@ -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<string | null> {
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);
Expand Down Expand Up @@ -73,5 +94,5 @@ export function resolveGitAiBinary(): Promise<string | null> {
* (which relies on the current process PATH).
*/
export function getGitAiBinary(): string {
return resolvedPath || "git-ai";
return Config.getBinaryPath() || resolvedPath || "git-ai";
}
6 changes: 5 additions & 1 deletion agent-support/vscode/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export class Config {
return !!this.getRoot().get<boolean>("experiments.aiTabTracking");
}

static getBinaryPath(): string | null {
const binaryPath = this.getRoot().get<string>("binaryPath")?.trim();
return binaryPath || null;
}

static getBlameMode(): BlameMode {
const mode = this.getRoot().get<string>("blameMode");
if (mode === 'off' || mode === 'line' || mode === 'all') {
Expand All @@ -28,4 +33,3 @@ export class Config {
}
}