Skip to content

Commit d6128ba

Browse files
Copilotjoshspicer
andauthored
Implement seamless terminal commit flow for GPG signing and git hooks in Copilot Remote Agent (#7146)
* Initial plan * Implement seamless terminal commit flow for GPG signing and git hooks Co-authored-by: joshspicer <[email protected]> * Polish terminal commit flow with helpful comments and instructions Co-authored-by: joshspicer <[email protected]> * improvements --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: joshspicer <[email protected]>
1 parent 0933829 commit d6128ba

File tree

1 file changed

+93
-7
lines changed

1 file changed

+93
-7
lines changed

src/github/copilotRemoteAgent.ts

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,76 @@ export class CopilotRemoteAgentManager extends Disposable {
294294
return vscode.l10n.t('🚀 Coding agent will continue work in [#{0}]({1}). Track progress [here]({2}).', number, link, webviewUri.toString());
295295
}
296296

297+
/**
298+
* Opens a terminal and waits for user to successfully commit
299+
* This is a fallback for when the commit cannot be done automatically (eg: GPG signing password needed)
300+
*/
301+
private async handleInteractiveCommit(repository: Repository, commitMessage: string): Promise<boolean> {
302+
return new Promise<boolean>((resolve) => {
303+
const startingCommit = repository.state.HEAD?.commit;
304+
305+
// Create terminal with git commit command
306+
const terminal = vscode.window.createTerminal({
307+
name: 'GitHub Coding Agent',
308+
cwd: repository.rootUri.fsPath,
309+
message: vscode.l10n.t('Commit your changes to continue Coding Agent session')
310+
});
311+
312+
// Show terminal and send commit command
313+
terminal.show();
314+
terminal.sendText(`# Complete this commit to continue with your Coding Agent session. Ctrl+C to cancel.`);
315+
terminal.sendText(`git commit -m "${commitMessage}"`);
316+
317+
let disposed = false;
318+
let timeoutId: NodeJS.Timeout;
319+
let stateListener: vscode.Disposable | undefined;
320+
let disposalListener: vscode.Disposable | undefined;
321+
322+
const cleanup = () => {
323+
if (disposed) return;
324+
disposed = true;
325+
clearTimeout(timeoutId);
326+
stateListener?.dispose();
327+
disposalListener?.dispose();
328+
terminal.dispose();
329+
};
330+
331+
// Listen for repository state changes
332+
stateListener = repository.state.onDidChange(() => {
333+
// Check if commit was successful (HEAD changed and no more staged changes)
334+
if (repository.state.HEAD?.commit !== startingCommit &&
335+
repository.state.indexChanges.length === 0) {
336+
cleanup();
337+
resolve(true);
338+
}
339+
});
340+
341+
// Set a timeout to avoid waiting forever
342+
timeoutId = setTimeout(() => {
343+
cleanup();
344+
vscode.window.showWarningMessage(
345+
vscode.l10n.t('Commit timeout. Please try the operation again after committing your changes.')
346+
);
347+
resolve(false);
348+
}, 5 * 60 * 1000); // 5 minutes timeout
349+
350+
// Listen for terminal disposal (user closed it)
351+
disposalListener = vscode.window.onDidCloseTerminal((closedTerminal) => {
352+
if (closedTerminal === terminal) {
353+
// Give a brief moment for potential state changes to propagate
354+
setTimeout(() => {
355+
if (!disposed) {
356+
cleanup();
357+
// Check one more time if commit happened just before terminal was closed
358+
resolve(repository.state.HEAD?.commit !== startingCommit &&
359+
repository.state.indexChanges.length === 0);
360+
}
361+
}, 1000);
362+
}
363+
});
364+
});
365+
}
366+
297367
async invokeRemoteAgent(prompt: string, problemContext: string, autoPushAndCommit = true): Promise<RemoteAgentResult> {
298368
const capiClient = await this.copilotApi;
299369
if (!capiClient) {
@@ -319,13 +389,29 @@ export class CopilotRemoteAgentManager extends Disposable {
319389
const asyncBranch = `copilot/vscode${Date.now()}`;
320390
try {
321391
await repository.createBranch(asyncBranch, true);
322-
await repository.add([]);
323-
if (repository.state.indexChanges.length > 0) {
324-
try {
325-
await repository.commit('Checkpoint for Copilot Agent async session');
326-
} catch (e) {
327-
// https://github.com/microsoft/vscode/pull/252263
328-
return { error: vscode.l10n.t('Could not \'git commit\' pending changes. If GPG signing or git hooks are enabled, please first commit or stash your changes and try again. ({0})', e.message), state: 'error' };
392+
const commitMessage = 'Checkpoint for VS Code Coding Agent Session';
393+
try {
394+
await repository.commit(commitMessage, { all: true });
395+
if (repository.state.HEAD?.name !== asyncBranch || repository.state.workingTreeChanges.length > 0 || repository.state.indexChanges.length > 0) {
396+
throw new Error(vscode.l10n.t('Uncommitted changes still detected.'));
397+
}
398+
} catch (e) {
399+
// Instead of immediately failing, open terminal for interactive commit
400+
const commitSuccessful = await vscode.window.withProgress({
401+
title: vscode.l10n.t('Waiting for commit to complete in the integrated terminal...'),
402+
cancellable: true,
403+
location: vscode.ProgressLocation.Notification
404+
}, async (progress, token) => {
405+
const commitPromise = this.handleInteractiveCommit(repository, commitMessage);
406+
if (token) {
407+
token.onCancellationRequested(() => {
408+
return false;
409+
});
410+
}
411+
return await commitPromise;
412+
});
413+
if (!commitSuccessful) {
414+
return { error: vscode.l10n.t('Commit was unsuccessful. Manually commit or stash your changes and try again.'), state: 'error' };
329415
}
330416
}
331417
await repository.push(remote.remoteName, asyncBranch, true);

0 commit comments

Comments
 (0)