Skip to content

Commit 9982a4d

Browse files
committed
initial git ai integration
1 parent 932681b commit 9982a4d

File tree

4 files changed

+250
-0
lines changed

4 files changed

+250
-0
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { exec, spawn } from "child_process";
2+
3+
import { getCurrentSession, getSessionFilePath } from "../session.js";
4+
import { logger } from "../util/logger.js";
5+
import { BaseService } from "./BaseService.js";
6+
import { services } from "./index.js";
7+
8+
interface GitAiHookInput {
9+
session_id: string;
10+
transcript_path: string;
11+
cwd: string;
12+
model?: string;
13+
hook_event_name: "PreToolUse" | "PostToolUse";
14+
tool_input: {
15+
file_path: string;
16+
};
17+
}
18+
19+
export interface GitAiIntegrationServiceState {
20+
isEnabled: boolean;
21+
isGitAiAvailable: boolean | null; // null = not checked yet
22+
}
23+
24+
export class GitAiIntegrationService extends BaseService<GitAiIntegrationServiceState> {
25+
constructor() {
26+
super("GitAiIntegrationService", {
27+
isEnabled: true,
28+
isGitAiAvailable: null,
29+
});
30+
}
31+
32+
async doInitialize(): Promise<GitAiIntegrationServiceState> {
33+
// Check if git-ai is available on first initialization
34+
const isAvailable = await this.checkGitAiAvailable();
35+
return {
36+
isEnabled: true,
37+
isGitAiAvailable: isAvailable,
38+
};
39+
}
40+
41+
private async checkGitAiAvailable(): Promise<boolean> {
42+
return new Promise((resolve) => {
43+
try {
44+
exec("git-ai --version", (error) => {
45+
if (error) {
46+
resolve(false);
47+
return;
48+
}
49+
resolve(true);
50+
});
51+
} catch {
52+
// Handle edge case where exec throws synchronously
53+
resolve(false);
54+
}
55+
});
56+
}
57+
58+
/**
59+
* Helper function to call git-ai checkpoint with the given hook input
60+
*/
61+
private async callGitAiCheckpoint(
62+
hookInput: GitAiHookInput,
63+
workspaceDirectory: string,
64+
): Promise<void> {
65+
const hookInputJson = JSON.stringify(hookInput);
66+
67+
logger.debug("Calling git-ai checkpoint", {
68+
hookInput,
69+
workspaceDirectory,
70+
});
71+
72+
await new Promise<void>((resolve, reject) => {
73+
const gitAiProcess = spawn(
74+
"git-ai",
75+
["checkpoint", "continue-cli", "--hook-input", "stdin"],
76+
{ cwd: workspaceDirectory },
77+
);
78+
79+
let stdout = "";
80+
let stderr = "";
81+
82+
gitAiProcess.stdout?.on("data", (data: Buffer) => {
83+
stdout += data.toString();
84+
});
85+
86+
gitAiProcess.stderr?.on("data", (data: Buffer) => {
87+
stderr += data.toString();
88+
});
89+
90+
gitAiProcess.on("error", (error: Error) => {
91+
reject(error);
92+
});
93+
94+
gitAiProcess.on("close", (code: number | null) => {
95+
if (code === 0) {
96+
logger.debug("git-ai checkpoint completed", { stdout, stderr });
97+
resolve();
98+
} else {
99+
reject(
100+
new Error(`git-ai checkpoint exited with code ${code}: ${stderr}`),
101+
);
102+
}
103+
});
104+
105+
// Write JSON to stdin and close
106+
gitAiProcess.stdin?.write(hookInputJson);
107+
gitAiProcess.stdin?.end();
108+
});
109+
}
110+
111+
async beforeFileEdit(filePath: string): Promise<void> {
112+
if (!this.currentState.isEnabled) {
113+
return;
114+
}
115+
116+
// Skip if git-ai is not available
117+
if (this.currentState.isGitAiAvailable === false) {
118+
return;
119+
}
120+
121+
try {
122+
const session = getCurrentSession();
123+
const sessionFilePath = getSessionFilePath();
124+
125+
// Get current model from ModelService
126+
const modelState = services.model.getState();
127+
console.log("modelState", modelState);
128+
const modelName = modelState?.model?.model;
129+
130+
const hookInput: GitAiHookInput = {
131+
session_id: session.sessionId,
132+
transcript_path: sessionFilePath,
133+
cwd: session.workspaceDirectory,
134+
hook_event_name: "PreToolUse",
135+
tool_input: {
136+
file_path: filePath,
137+
},
138+
};
139+
140+
// Only include model if it's available
141+
if (modelName) {
142+
hookInput.model = modelName;
143+
}
144+
145+
await this.callGitAiCheckpoint(hookInput, session.workspaceDirectory);
146+
} catch (error) {
147+
logger.warn("git-ai checkpoint (pre-edit) failed", { error, filePath });
148+
// Mark as unavailable if command fails
149+
this.setState({ isGitAiAvailable: false });
150+
// Don't throw - allow file edit to proceed
151+
}
152+
}
153+
154+
async afterFileEdit(filePath: string): Promise<void> {
155+
if (!this.currentState.isEnabled) {
156+
return;
157+
}
158+
159+
// Skip if git-ai is not available
160+
if (this.currentState.isGitAiAvailable === false) {
161+
return;
162+
}
163+
164+
try {
165+
const session = getCurrentSession();
166+
const sessionFilePath = getSessionFilePath();
167+
168+
// Get current model from ModelService
169+
const modelState = services.model.getState();
170+
console.log("modelState", modelState);
171+
const modelName = modelState?.model?.model;
172+
173+
const hookInput: GitAiHookInput = {
174+
session_id: session.sessionId,
175+
transcript_path: sessionFilePath,
176+
cwd: session.workspaceDirectory,
177+
hook_event_name: "PostToolUse",
178+
tool_input: {
179+
file_path: filePath,
180+
},
181+
};
182+
183+
// Only include model if it's available
184+
if (modelName) {
185+
hookInput.model = modelName;
186+
}
187+
188+
await this.callGitAiCheckpoint(hookInput, session.workspaceDirectory);
189+
} catch (error) {
190+
logger.warn("git-ai checkpoint (post-edit) failed", { error, filePath });
191+
// Mark as unavailable if command fails
192+
this.setState({ isGitAiAvailable: false });
193+
// Don't throw - file edit already completed
194+
}
195+
}
196+
197+
setEnabled(enabled: boolean): void {
198+
this.setState({ isEnabled: enabled });
199+
}
200+
}

extensions/cli/src/services/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AuthService } from "./AuthService.js";
88
import { ChatHistoryService } from "./ChatHistoryService.js";
99
import { ConfigService } from "./ConfigService.js";
1010
import { FileIndexService } from "./FileIndexService.js";
11+
import { GitAiIntegrationService } from "./GitAiIntegrationService.js";
1112
import { MCPService } from "./MCPService.js";
1213
import { ModelService } from "./ModelService.js";
1314
import { ResourceMonitoringService } from "./ResourceMonitoringService.js";
@@ -43,6 +44,7 @@ const storageSyncService = new StorageSyncService();
4344
const agentFileService = new AgentFileService();
4445
const toolPermissionService = new ToolPermissionService();
4546
const systemMessageService = new SystemMessageService();
47+
const gitAiIntegrationService = new GitAiIntegrationService();
4648

4749
/**
4850
* Initialize all services and register them with the service container
@@ -296,6 +298,12 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) {
296298
[], // No dependencies for now, but could depend on SESSION in future
297299
);
298300

301+
serviceContainer.register(
302+
SERVICE_NAMES.GIT_AI_INTEGRATION,
303+
() => gitAiIntegrationService.initialize(),
304+
[], // No dependencies
305+
);
306+
299307
// Eagerly initialize all services to ensure they're ready when needed
300308
// This avoids race conditions and "service not ready" errors
301309
await serviceContainer.initializeAll();
@@ -357,6 +365,7 @@ export const services = {
357365
storageSync: storageSyncService,
358366
agentFile: agentFileService,
359367
toolPermissions: toolPermissionService,
368+
gitAiIntegration: gitAiIntegrationService,
360369
} as const;
361370

362371
// Export the service container for advanced usage

extensions/cli/src/services/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export interface AgentFileServiceState {
127127

128128
export type { ChatHistoryState } from "./ChatHistoryService.js";
129129
export type { FileIndexServiceState } from "./FileIndexService.js";
130+
export type { GitAiIntegrationServiceState } from "./GitAiIntegrationService.js";
130131

131132
/**
132133
* Service names as constants to prevent typos
@@ -145,6 +146,7 @@ export const SERVICE_NAMES = {
145146
UPDATE: "update",
146147
STORAGE_SYNC: "storageSync",
147148
AGENT_FILE: "agentFile",
149+
GIT_AI_INTEGRATION: "gitAiIntegration",
148150
} as const;
149151

150152
/**

extensions/cli/src/tools/index.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,24 +189,63 @@ export function convertMcpToolToContinueTool(mcpTool: MCPTool): Tool {
189189
};
190190
}
191191

192+
function extractFilePathFromToolCall(
193+
toolCall: PreprocessedToolCall,
194+
): string | null {
195+
const preprocessed = toolCall.preprocessResult;
196+
if (!preprocessed?.args) return null;
197+
198+
const args = preprocessed.args;
199+
200+
// Extract file path based on tool type
201+
if (toolCall.name === "Edit" && args.resolvedPath) {
202+
return args.resolvedPath;
203+
} else if (toolCall.name === "MultiEdit" && args.file_path) {
204+
return args.file_path;
205+
} else if (toolCall.name === "Write" && args.filepath) {
206+
return args.filepath;
207+
}
208+
209+
return null;
210+
}
211+
192212
export async function executeToolCall(
193213
toolCall: PreprocessedToolCall,
194214
): Promise<string> {
195215
const startTime = Date.now();
216+
const FILE_EDIT_TOOLS = ["Edit", "MultiEdit", "Write"];
217+
const isFileEdit = FILE_EDIT_TOOLS.includes(toolCall.name);
196218

197219
try {
198220
logger.debug("Executing tool", {
199221
toolName: toolCall.name,
200222
arguments: toolCall.arguments,
201223
});
202224

225+
// GIT-AI CHECKPOINT: Call before file editing (BLOCKING)
226+
if (isFileEdit) {
227+
const filePath = extractFilePathFromToolCall(toolCall);
228+
if (filePath) {
229+
await services.gitAiIntegration.beforeFileEdit(filePath);
230+
}
231+
}
232+
203233
// IMPORTANT: if preprocessed args are present, uses preprocessed args instead of original args
204234
// Preprocessed arg names may be different
205235
const result = await toolCall.tool.run(
206236
toolCall.preprocessResult?.args ?? toolCall.arguments,
207237
);
208238
const duration = Date.now() - startTime;
209239

240+
// GIT-AI CHECKPOINT: Call after file editing (NON-BLOCKING)
241+
if (isFileEdit) {
242+
const filePath = extractFilePathFromToolCall(toolCall);
243+
if (filePath) {
244+
// Don't await - run in background to avoid blocking
245+
void services.gitAiIntegration.afterFileEdit(filePath);
246+
}
247+
}
248+
210249
telemetryService.logToolResult({
211250
toolName: toolCall.name,
212251
success: true,

0 commit comments

Comments
 (0)