Skip to content

Commit 8464add

Browse files
authored
Display Copilot CLI edits when resuming a session (#1754)
* Display Copilot CLI edits when resuming a session * Fix tests * Fix test * Fixes * Fixes * Fix issues
1 parent d137a2f commit 8464add

File tree

4 files changed

+54
-27
lines changed

4 files changed

+54
-27
lines changed

src/extension/agents/copilotcli/node/copilotcliSession.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IToolsService } from '../../../tools/common/toolsService';
1515
import { ExternalEditTracker } from '../../common/externalEditTracker';
1616
import { getAffectedUrisForEditTool } from '../common/copilotcliTools';
1717
import { ICopilotCLISDK } from './copilotCli';
18-
import { buildChatHistoryFromEvents, processToolExecutionComplete, processToolExecutionStart } from './copilotcliToolInvocationFormatter';
18+
import { buildChatHistoryFromEvents, isCopilotCliEditToolCall, processToolExecutionComplete, processToolExecutionStart } from './copilotcliToolInvocationFormatter';
1919
import { getCopilotLogger } from './logger';
2020
import { getConfirmationToolParams, PermissionRequest } from './permissionHelpers';
2121

@@ -193,12 +193,15 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
193193
}
194194

195195
case 'tool.execution_start': {
196-
const responsePart = processToolExecutionStart(event, this._toolNames, this._pendingToolInvocations);
197-
const toolName = this._toolNames.get(event.data.toolCallId);
196+
this._toolNames.set(event.data.toolCallId, event.data.toolName);
197+
const responsePart = processToolExecutionStart(event, this._pendingToolInvocations);
198+
if (isCopilotCliEditToolCall(event.data.toolName, event.data.arguments)) {
199+
this._pendingToolInvocations.delete(event.data.toolCallId);
200+
}
198201
if (responsePart instanceof ChatResponseThinkingProgressPart) {
199202
stream.push(responsePart);
200203
}
201-
this.logService.trace(`Start Tool ${toolName || '<unknown>'}`);
204+
this.logService.trace(`Start Tool ${event.data.toolName || '<unknown>'}`);
202205
break;
203206
}
204207

src/extension/agents/copilotcli/node/copilotcliSessionService.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { ModelProvider, Session, SessionManager } from '@github/copilot/sdk';
6+
import type { ModelProvider, Session, SessionManager, internal } from '@github/copilot/sdk';
77
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
88
import type { CancellationToken, ChatRequest } from 'vscode';
99
import { ILogService } from '../../../../platform/log/common/logService';
1010
import { createServiceIdentifier } from '../../../../util/common/services';
1111
import { coalesce } from '../../../../util/vs/base/common/arrays';
1212
import { raceCancellationError } from '../../../../util/vs/base/common/async';
1313
import { Emitter, Event } from '../../../../util/vs/base/common/event';
14+
import { Lazy } from '../../../../util/vs/base/common/lazy';
1415
import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
1516
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
1617
import { ChatSessionStatus } from '../../../../vscodeTypes';
@@ -51,7 +52,7 @@ export const ICopilotCLISessionService = createServiceIdentifier<ICopilotCLISess
5152
export class CopilotCLISessionService extends Disposable implements ICopilotCLISessionService {
5253
declare _serviceBrand: undefined;
5354

54-
private _sessionManager: SessionManager | undefined;
55+
private _sessionManager: Lazy<Promise<internal.CLISessionManager>>;
5556
private _sessionWrappers = new DisposableMap<string, CopilotCLISession>();
5657
private _newActiveSessions = new Map<string, ICopilotCLISessionItem>();
5758

@@ -65,16 +66,17 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
6566
@IInstantiationService private readonly instantiationService: IInstantiationService
6667
) {
6768
super();
68-
}
6969

70-
public async getSessionManager(): Promise<SessionManager> {
71-
if (!this._sessionManager) {
70+
this._sessionManager = new Lazy<Promise<internal.CLISessionManager>>(async () => {
7271
const { internal } = await this.copilotCLISDK.getPackage();
73-
this._sessionManager = new internal.CLISessionManager({
72+
return new internal.CLISessionManager({
7473
logger: getCopilotLogger(this.logService)
7574
});
76-
}
77-
return this._sessionManager;
75+
});
76+
}
77+
78+
async getSessionManager() {
79+
return this._sessionManager.value;
7880
}
7981

8082
private _getAllSessionsProgress: Promise<readonly ICopilotCLISessionItem[]> | undefined;

src/extension/agents/copilotcli/node/copilotcliToolInvocationFormatter.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ export function buildChatHistoryFromEvents(events: readonly SessionEvent[]): (Ch
133133
const turns: (ChatRequestTurn2 | ChatResponseTurn2)[] = [];
134134
let currentResponseParts: ExtendedChatResponsePart[] = [];
135135
const pendingToolInvocations = new Map<string, ChatToolInvocationPart>();
136-
const toolNames = new Map<string, string>();
137136

138137
for (const event of events) {
139138
switch (event.type) {
@@ -172,7 +171,7 @@ export function buildChatHistoryFromEvents(events: readonly SessionEvent[]): (Ch
172171
break;
173172
}
174173
case 'tool.execution_start': {
175-
const responsePart = processToolExecutionStart(event, toolNames, pendingToolInvocations);
174+
const responsePart = processToolExecutionStart(event, pendingToolInvocations);
176175
if (responsePart instanceof ChatResponseThinkingProgressPart) {
177176
currentResponseParts.push(responsePart);
178177
}
@@ -196,13 +195,12 @@ export function buildChatHistoryFromEvents(events: readonly SessionEvent[]): (Ch
196195
return turns;
197196
}
198197

199-
export function processToolExecutionStart(event: ToolExecutionStartEvent, toolNames: Map<string, string>, pendingToolInvocations: Map<string, ChatToolInvocationPart | ChatResponseThinkingProgressPart>): ChatToolInvocationPart | ChatResponseThinkingProgressPart | undefined {
198+
export function processToolExecutionStart(event: ToolExecutionStartEvent, pendingToolInvocations: Map<string, ChatToolInvocationPart | ChatResponseThinkingProgressPart>): ChatToolInvocationPart | ChatResponseThinkingProgressPart | undefined {
200199
const toolInvocation = createCopilotCLIToolInvocation(
201200
event.data.toolName,
202201
event.data.toolCallId,
203202
event.data.arguments
204203
);
205-
toolNames.set(event.data.toolCallId, event.data.toolName);
206204
if (toolInvocation) {
207205
// Store pending invocation to update with result later
208206
pendingToolInvocations.set(event.data.toolCallId, toolInvocation);
@@ -239,9 +237,6 @@ export function createCopilotCLIToolInvocation(
239237
if (toolName === CopilotCLIToolNames.ReportIntent) {
240238
return undefined; // Ignore these for now
241239
}
242-
if (isCopilotCliEditToolCall(toolName, args)) {
243-
return undefined;
244-
}
245240
if (toolName === CopilotCLIToolNames.Think) {
246241
const thought = (args as { thought?: string })?.thought;
247242
if (thought && typeof thought === 'string') {
@@ -255,8 +250,8 @@ export function createCopilotCLIToolInvocation(
255250
invocation.isComplete = false;
256251

257252
// Format based on tool name
258-
if (toolName === CopilotCLIToolNames.StrReplaceEditor && (args as StrReplaceEditorArgs)?.command === 'view') {
259-
formatViewToolInvocation(invocation, args as StrReplaceEditorArgs);
253+
if (toolName === CopilotCLIToolNames.StrReplaceEditor) {
254+
formatStrReplaceEditorInvocation(invocation, args as StrReplaceEditorArgs);
260255
} else if (toolName === CopilotCLIToolNames.Bash) {
261256
formatBashInvocation(invocation, args as BashArgs);
262257
} else if (toolName === CopilotCLIToolNames.View) {
@@ -272,9 +267,38 @@ function formatViewToolInvocation(invocation: ChatToolInvocationPart, args: StrR
272267
const path = args.path ?? '';
273268
const display = path ? formatUriForMessage(path) : '';
274269

275-
invocation.invocationMessage = new MarkdownString(l10n.t("Read {0}", display));
270+
invocation.invocationMessage = new MarkdownString(l10n.t("Viewed {0}", display));
276271
}
277272

273+
function formatStrReplaceEditorInvocation(invocation: ChatToolInvocationPart, args: StrReplaceEditorArgs): void {
274+
const command = args.command;
275+
const path = args.path ?? '';
276+
const display = path ? formatUriForMessage(path) : '';
277+
278+
switch (command) {
279+
case 'view':
280+
if (args.view_range) {
281+
invocation.invocationMessage = new MarkdownString(l10n.t("Viewed {0} (lines {1}-{2})", display, args.view_range[0], args.view_range[1]));
282+
} else {
283+
invocation.invocationMessage = new MarkdownString(l10n.t("Viewed {0}", display));
284+
}
285+
break;
286+
case 'str_replace':
287+
invocation.invocationMessage = new MarkdownString(l10n.t("Edited {0}", display));
288+
break;
289+
case 'insert':
290+
invocation.invocationMessage = new MarkdownString(l10n.t("Inserted text in {0}", display));
291+
break;
292+
case 'create':
293+
invocation.invocationMessage = new MarkdownString(l10n.t("Created {0}", display));
294+
break;
295+
case 'undo_edit':
296+
invocation.invocationMessage = new MarkdownString(l10n.t("Undid edit in {0}", display));
297+
break;
298+
default:
299+
invocation.invocationMessage = new MarkdownString(l10n.t("Modified {0}", display));
300+
}
301+
}
278302

279303
function formatBashInvocation(invocation: ChatToolInvocationPart, args: BashArgs): void {
280304
const command = args.command ?? '';

src/extension/agents/copilotcli/node/test/copilotcliToolInvocationFormatter.spec.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('copilotcliToolInvocationFormatter', () => {
5050
const invocation = createCopilotCLIToolInvocation(CopilotCLIToolNames.StrReplaceEditor, 'id3', { command: 'view', path: '/tmp/file.ts', view_range: [1, 5] }) as ChatToolInvocationPart;
5151
expect(invocation).toBeInstanceOf(ChatToolInvocationPart);
5252
const msg = typeof invocation.invocationMessage === 'string' ? invocation.invocationMessage : invocation.invocationMessage?.value;
53-
expect(msg).toMatch(/Read/);
53+
expect(msg).toMatch(/Viewed/);
5454
expect(msg).toMatch(/file.ts/);
5555
});
5656

@@ -73,9 +73,8 @@ describe('copilotcliToolInvocationFormatter', () => {
7373

7474
it('processToolExecutionStart stores invocation and processToolExecutionComplete updates status on success', () => {
7575
const pending = new Map<string, ChatToolInvocationPart | ChatResponseThinkingProgressPart>();
76-
const names = new Map<string, string>();
7776
const startEvt: ToolExecutionStart = { type: 'tool.execution_start', data: { toolName: CopilotCLIToolNames.View, toolCallId: 'call-1', arguments: { command: 'view', path: '/x.ts' } } };
78-
const part = processToolExecutionStart(startEvt as any, names, pending);
77+
const part = processToolExecutionStart(startEvt as any, pending);
7978
expect(part).toBeInstanceOf(ChatToolInvocationPart);
8079
const completeEvt: ToolExecutionComplete = { type: 'tool.execution_complete', data: { toolCallId: 'call-1', success: true } };
8180
const completed = processToolExecutionComplete(completeEvt as any, pending) as ChatToolInvocationPart;
@@ -86,9 +85,8 @@ describe('copilotcliToolInvocationFormatter', () => {
8685

8786
it('processToolExecutionComplete marks rejected error invocation', () => {
8887
const pending = new Map<string, ChatToolInvocationPart | ChatResponseThinkingProgressPart>();
89-
const names = new Map<string, string>();
9088
const startEvt: ToolExecutionStart = { type: 'tool.execution_start', data: { toolName: CopilotCLIToolNames.View, toolCallId: 'call-err', arguments: { command: 'view', path: '/y.ts' } } };
91-
const part = processToolExecutionStart(startEvt as any, names, pending) as ChatToolInvocationPart;
89+
const part = processToolExecutionStart(startEvt as any, pending) as ChatToolInvocationPart;
9290
expect(part).toBeInstanceOf(ChatToolInvocationPart);
9391
const completeEvt: ToolExecutionComplete = { type: 'tool.execution_complete', data: { toolCallId: 'call-err', success: false, error: { code: 'rejected', message: 'Denied' } } };
9492
const completed = processToolExecutionComplete(completeEvt as any, pending) as ChatToolInvocationPart;

0 commit comments

Comments
 (0)