Update background handling of update_todo tool (#3845)

This commit is contained in:
Don Jayamanne
2026-02-19 16:51:20 +11:00
committed by GitHub
parent f3e6859c16
commit 242fffd027
9 changed files with 199 additions and 72 deletions

View File

@@ -5,15 +5,21 @@
import type { SessionEvent, ToolExecutionCompleteEvent, ToolExecutionStartEvent } from '@github/copilot/sdk';
import * as l10n from '@vscode/l10n';
import type { ChatPromptReference, ChatTerminalToolInvocationData, ChatTodoStatus, ChatTodoToolInvocationData, ExtendedChatResponsePart } from 'vscode';
import type { CancellationToken, ChatParticipantToolToken, ChatPromptReference, ChatSimpleToolResultData, ChatTerminalToolInvocationData, ExtendedChatResponsePart, LanguageModelToolDefinition, LanguageModelToolInformation, LanguageModelToolInvocationOptions, LanguageModelToolResult2 } from 'vscode';
import { ILogger } from '../../../../platform/log/common/logService';
import { IChatEndpoint } from '../../../../platform/networking/common/networking';
import { isLocation } from '../../../../util/common/types';
import { decodeBase64 } from '../../../../util/vs/base/common/buffer';
import { Emitter } from '../../../../util/vs/base/common/event';
import { ResourceMap } from '../../../../util/vs/base/common/map';
import { constObservable, IObservable } from '../../../../util/vs/base/common/observable';
import { isAbsolutePath } from '../../../../util/vs/base/common/resources';
import { URI } from '../../../../util/vs/base/common/uri';
import { ChatMcpToolInvocationData, ChatRequestTurn2, ChatResponseCodeblockUriPart, ChatResponseMarkdownPart, ChatResponsePullRequestPart, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatToolInvocationPart, Location, MarkdownString, McpToolInvocationContentData, Range, Uri } from '../../../../vscodeTypes';
import { ChatMcpToolInvocationData, ChatRequestTurn2, ChatResponseCodeblockUriPart, ChatResponseMarkdownPart, ChatResponsePullRequestPart, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatToolInvocationPart, LanguageModelTextPart, Location, MarkdownString, McpToolInvocationContentData, Range, Uri } from '../../../../vscodeTypes';
import type { MCP } from '../../../common/modelContextProtocol';
import { ToolName } from '../../../tools/common/toolNames';
import { ICopilotTool } from '../../../tools/common/toolsRegistry';
import { IOnWillInvokeToolEvent, IToolsService, IToolValidationResult } from '../../../tools/common/toolsService';
import { formatUriForFileWidget } from '../../../tools/common/toolUtils';
import { extractChatPromptReferences, getFolderAttachmentPath } from './copilotCLIPrompt';
import { IChatDelegationSummaryService } from './delegationSummaryService';
@@ -895,17 +901,12 @@ function formatReplyToCommentInvocation(invocation: ChatToolInvocationPart, tool
}
/**
* Parse markdown todo list into structured ChatTodoToolInvocationData.
* Extracts title from first non-empty line (strips leading #), parses checklist items,
* and generates sequential numeric IDs.
*/
function parseTodoMarkdown(markdown: string): { title: string; todoList: Array<{ id: number; title: string; status: ChatTodoStatus }> } {
export function parseTodoMarkdown(markdown: string): { title: string; todoList: Array<{ id: number; title: string; status: 'not-started' | 'in-progress' | 'completed' }> } {
const lines = markdown.split('\n');
const todoList: Array<{ id: number; title: string; status: ChatTodoStatus }> = [];
const todoList: Array<{ id: number; title: string; status: 'not-started' | 'in-progress' | 'completed' }> = [];
let title = 'Updated todo list';
let inCodeBlock = false;
let currentItem: { title: string; status: ChatTodoStatus } | null = null;
let currentItem: { title: string; status: 'not-started' | 'in-progress' | 'completed' } | null = null;
for (const line of lines) {
// Track code fences
@@ -948,13 +949,13 @@ function parseTodoMarkdown(markdown: string): { title: string; todoList: Array<{
const itemTitle = match[2];
// Map checkbox character to status
let status: ChatTodoStatus;
let status: 'not-started' | 'in-progress' | 'completed';
if (checkboxChar === 'x' || checkboxChar === 'X') {
status = 3; // ChatTodoStatus.Completed
status = 'completed';
} else if (checkboxChar === '>' || checkboxChar === '~') {
status = 2; // ChatTodoStatus.InProgress
status = 'in-progress';
} else {
status = 1; // ChatTodoStatus.NotStarted
status = 'not-started';
}
currentItem = { title: itemTitle, status };
@@ -987,20 +988,59 @@ function formatUpdateTodoInvocation(invocation: ChatToolInvocationPart, toolCall
invocation.invocationMessage = parsed.title;
invocation.toolSpecificData = {
todoList: parsed.todoList
} as ChatTodoToolInvocationData;
output: '',
input: [`# ${parsed.title}`, ...parsed.todoList.map(item => `- [${item.status === 'completed' ? 'x' : item.status === 'in-progress' ? '>' : ' '}] ${item.title}`)].join('\n')
};
}
function formatUpdateTodoInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: UpdateTodoTool, result: ToolCallResult): void {
const parsed = toolCall.arguments.todos ? parseTodoMarkdown(toolCall.arguments.todos) : { title: '', todoList: [] };
// Re-parse todo markdown on completion to ensure UI has final state
if (parsed.todoList.length > 0) {
invocation.invocationMessage = parsed.title;
invocation.toolSpecificData = {
todoList: parsed.todoList
} as ChatTodoToolInvocationData;
const input = (invocation.toolSpecificData ? (invocation.toolSpecificData as ChatSimpleToolResultData).input : '') || '';
invocation.toolSpecificData = {
output: typeof result.result?.content === 'string' ? result.result.content : JSON.stringify(result.result?.content || '', null, 2),
input
};
}
export async function updateTodoList(
event: ToolExecutionStartEvent,
toolsService: IToolsService,
toolInvocationToken: ChatParticipantToolToken,
token: CancellationToken
) {
const toolData = event.data as ToolCall;
if (toolData.toolName !== 'update_todo' || !toolData.arguments.todos) {
return;
}
const { todoList } = parseTodoMarkdown(toolData.arguments.todos);
if (!todoList.length) {
return;
}
await toolsService.invokeTool(ToolName.CoreManageTodoList, {
input: {
operation: 'write',
todoList: todoList.map((item, i) => ({
id: i,
title: item.title,
description: '',
status: item.status
} satisfies IManageTodoListToolInputParams['todoList'][number])),
} satisfies IManageTodoListToolInputParams,
toolInvocationToken,
}, token);
}
interface IManageTodoListToolInputParams {
readonly operation?: 'write' | 'read'; // Optional in write-only mode
readonly todoList: readonly {
readonly id: number;
readonly title: string;
readonly description: string;
readonly status: 'not-started' | 'in-progress' | 'completed';
}[];
}
/**
@@ -1011,6 +1051,7 @@ function emptyInvocation(_invocation: ChatToolInvocationPart, _toolCall: Unknown
// No custom formatting needed
}
function genericToolInvocationCompleted(invocation: ChatToolInvocationPart, toolCall: UnknownToolCall, result: ToolCallResult): void {
if (result.success && result.result?.content) {
invocation.toolSpecificData = {
@@ -1020,3 +1061,77 @@ function genericToolInvocationCompleted(invocation: ChatToolInvocationPart, tool
}
}
/**
* Mock tools service that can be configured for different test scenarios
*/
export class FakeToolsService implements IToolsService {
readonly _serviceBrand: undefined;
private readonly _onWillInvokeTool = new Emitter<IOnWillInvokeToolEvent>();
readonly onWillInvokeTool = this._onWillInvokeTool.event;
readonly tools: ReadonlyArray<LanguageModelToolInformation> = [];
readonly copilotTools = new Map<ToolName, ICopilotTool<unknown>>();
private _confirmationResult: 'yes' | 'no' = 'yes';
private _invokeToolCalls: Array<{ name: string; input: unknown }> = [];
setConfirmationResult(result: 'yes' | 'no'): void {
this._confirmationResult = result;
}
get invokeToolCalls(): ReadonlyArray<{ name: string; input: unknown }> {
return this._invokeToolCalls;
}
clearCalls(): void {
this._invokeToolCalls = [];
}
invokeToolWithEndpoint(name: string, options: LanguageModelToolInvocationOptions<unknown>, endpoint: IChatEndpoint | undefined, token: CancellationToken): Thenable<LanguageModelToolResult2> {
return this.invokeTool(name, options);
}
modelSpecificTools: IObservable<{ definition: LanguageModelToolDefinition; tool: ICopilotTool<unknown> }[]> = constObservable([]);
async invokeTool(
name: string,
options: LanguageModelToolInvocationOptions<unknown>
): Promise<LanguageModelToolResult2> {
this._invokeToolCalls.push({ name, input: options.input });
if (name === ToolName.CoreConfirmationTool || name === ToolName.CoreTerminalConfirmationTool) {
return {
content: [new LanguageModelTextPart(this._confirmationResult)]
};
}
return { content: [] };
}
getCopilotTool(): ICopilotTool<unknown> | undefined {
return undefined;
}
getTool(): LanguageModelToolInformation | undefined {
return undefined;
}
getToolByToolReferenceName(): LanguageModelToolInformation | undefined {
return undefined;
}
validateToolInput(): IToolValidationResult {
return { inputObj: {} };
}
validateToolName(): string | undefined {
return undefined;
}
getEnabledTools(): LanguageModelToolInformation[] {
return [];
}
}

View File

@@ -6,6 +6,7 @@
import type { Attachment, Session, SessionOptions } from '@github/copilot/sdk';
import * as l10n from '@vscode/l10n';
import type * as vscode from 'vscode';
import type { ChatParticipantToolToken } from 'vscode';
import { ILogService } from '../../../../platform/log/common/logService';
import { CapturingToken } from '../../../../platform/requestLogger/common/capturingToken';
import { IRequestLogger, LoggedRequestKind } from '../../../../platform/requestLogger/node/requestLogger';
@@ -20,8 +21,9 @@ import { extUriBiasedIgnorePathCase } from '../../../../util/vs/base/common/reso
import { ThemeIcon } from '../../../../util/vs/base/common/themables';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { ChatQuestion, ChatQuestionType, ChatRequestTurn2, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatSessionStatus, ChatToolInvocationPart, EventEmitter, Uri } from '../../../../vscodeTypes';
import { IToolsService } from '../../../tools/common/toolsService';
import { ExternalEditTracker } from '../../common/externalEditTracker';
import { buildChatHistoryFromEvents, getAffectedUrisForEditTool, isCopilotCliEditToolCall, processToolExecutionComplete, processToolExecutionStart, ToolCall, UnknownToolCall } from '../common/copilotCLITools';
import { buildChatHistoryFromEvents, getAffectedUrisForEditTool, isCopilotCliEditToolCall, processToolExecutionComplete, processToolExecutionStart, ToolCall, UnknownToolCall, updateTodoList } from '../common/copilotCLITools';
import { IChatDelegationSummaryService } from '../common/delegationSummaryService';
import { CopilotCLISessionOptions, ICopilotCLISDK } from './copilotCli';
import { ICopilotCLIImageSupport } from './copilotCLIImageSupport';
@@ -74,7 +76,7 @@ export interface ICopilotCLISession extends IDisposable {
attachPermissionHandler(handler: PermissionHandler): IDisposable;
attachStream(stream: vscode.ChatResponseStream): IDisposable;
handleRequest(
requestId: string,
request: { id: string; toolInvocationToken: ChatParticipantToolToken },
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
@@ -139,6 +141,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
@IChatDelegationSummaryService private readonly _delegationSummaryService: IChatDelegationSummaryService,
@IRequestLogger private readonly _requestLogger: IRequestLogger,
@ICopilotCLIImageSupport private readonly _imageSupport: ICopilotCLIImageSupport,
@IToolsService private readonly _toolsService: IToolsService,
) {
super();
this.sessionId = _sdkSession.sessionId;
@@ -174,7 +177,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
}
public async handleRequest(
requestId: string,
request: { id: string; toolInvocationToken: ChatParticipantToolToken },
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
@@ -184,11 +187,11 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
const label = 'prompt' in input ? input.prompt : `/${input.command}`;
const promptLabel = label.length > 50 ? label.substring(0, 47) + '...' : label;
const capturingToken = new CapturingToken(`Background Agent | ${promptLabel}`, 'worktree', false, true);
return this._requestLogger.captureInvocation(capturingToken, () => this._handleRequestImpl(requestId, input, attachments, modelId, authInfo, token));
return this._requestLogger.captureInvocation(capturingToken, () => this._handleRequestImpl(request, input, attachments, modelId, authInfo, token));
}
private async _handleRequestImpl(
requestId: string,
request: { id: string; toolInvocationToken: ChatParticipantToolToken },
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
@@ -335,6 +338,13 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
} else if (responsePart instanceof ChatToolInvocationPart) {
responsePart.enablePartialUpdate = true;
this._stream?.push(responsePart);
if ((event.data as ToolCall).toolName === 'update_todo') {
updateTodoList(event, this._toolsService, request.toolInvocationToken, token).catch(error => {
this.logService.error(`[CopilotCLISession] Failed to invoke todo tool for toolCallId ${event.data.toolCallId}`, error);
});
}
}
}
this.logService.trace(`[CopilotCLISession] Start Tool ${event.data.toolName || '<unknown>'}`);
@@ -405,7 +415,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
}
this.logService.trace(`[CopilotCLISession] Invoking session (completed) ${this.sessionId}`);
const requestDetails: { requestId: string; toolIdEditMap: Record<string, string> } = { requestId, toolIdEditMap: {} };
const requestDetails: { requestId: string; toolIdEditMap: Record<string, string> } = { requestId: request.id, toolIdEditMap: {} };
await Promise.all(Array.from(toolIdEditMap.entries()).map(async ([toolId, editFilePromise]) => {
const editId = await editFilePromise.catch(() => undefined);
if (editId) {

View File

@@ -23,6 +23,7 @@ import { DisposableStore, IReference, toDisposable } from '../../../../../util/v
import { URI } from '../../../../../util/vs/base/common/uri';
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
import { createExtensionUnitTestingServices } from '../../../../test/node/services';
import { FakeToolsService } from '../../common/copilotCLITools';
import { IChatDelegationSummaryService } from '../../common/delegationSummaryService';
import { COPILOT_CLI_DEFAULT_AGENT_ID, ICopilotCLIAgents, ICopilotCLISDK } from '../copilotCli';
import { ICopilotCLIImageSupport } from '../copilotCLIImageSupport';
@@ -147,7 +148,7 @@ describe('CopilotCLISessionService', () => {
}
}();
}
return disposables.add(new CopilotCLISession(options, sdkSession, logService, workspaceService, sdk, instantiationService, delegationService, new NullRequestLogger(), new NullICopilotCLIImageSupport()));
return disposables.add(new CopilotCLISession(options, sdkSession, logService, workspaceService, sdk, instantiationService, delegationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService()));
}
} as unknown as IInstantiationService;
const configurationService = accessor.get(IConfigurationService);

View File

@@ -21,7 +21,7 @@ import { ChatSessionStatus, Uri } from '../../../../../vscodeTypes';
import { createExtensionUnitTestingServices } from '../../../../test/node/services';
import { MockChatResponseStream } from '../../../../test/node/testHelpers';
import { ExternalEditTracker } from '../../../common/externalEditTracker';
import { ToolCall } from '../../common/copilotCLITools';
import { FakeToolsService, ToolCall } from '../../common/copilotCLITools';
import { IChatDelegationSummaryService } from '../../common/delegationSummaryService';
import { CopilotCLISessionOptions, ICopilotCLISDK } from '../copilotCli';
import { CopilotCLISession } from '../copilotcliSession';
@@ -131,6 +131,7 @@ describe('CopilotCLISession', () => {
delegationService,
requestLogger,
new NullICopilotCLIImageSupport(),
new FakeToolsService()
));
}
@@ -140,7 +141,7 @@ describe('CopilotCLISession', () => {
// Attach stream first, then invoke with new signature (no stream param)
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Hello' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Hello' }, [], undefined, authInfo, CancellationToken.None);
expect(session.status).toBe(ChatSessionStatus.Completed);
expect(stream.output.join('\n')).toContain('Echo: Hello');
@@ -151,7 +152,7 @@ describe('CopilotCLISession', () => {
const session = await createSession();
const stream = new MockChatResponseStream();
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Hi' }, [], 'modelB', authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Hi' }, [], 'modelB', authInfo, CancellationToken.None);
expect(sdkSession._selectedModel).toBe('modelB');
});
@@ -162,7 +163,7 @@ describe('CopilotCLISession', () => {
const session = await createSession();
const stream = new MockChatResponseStream();
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Boom' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Boom' }, [], undefined, authInfo, CancellationToken.None);
expect(session.status).toBe(ChatSessionStatus.Failed);
expect(stream.output.join('\n')).toContain('Error: network');
@@ -174,7 +175,7 @@ describe('CopilotCLISession', () => {
const listener = disposables.add(session.onDidChangeStatus(s => statuses.push(s)));
const stream = new MockChatResponseStream();
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Status OK' }, [], 'modelA', authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Status OK' }, [], 'modelA', authInfo, CancellationToken.None);
listener.dispose?.();
expect(statuses).toEqual([ChatSessionStatus.InProgress, ChatSessionStatus.Completed]);
@@ -189,7 +190,7 @@ describe('CopilotCLISession', () => {
const listener = disposables.add(session.onDidChangeStatus(s => statuses.push(s)));
const stream = new MockChatResponseStream();
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Will Fail' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Will Fail' }, [], undefined, authInfo, CancellationToken.None);
listener.dispose?.();
expect(stream.output.join('\n')).toContain('Error: boom');
});
@@ -208,7 +209,7 @@ describe('CopilotCLISession', () => {
session.attachStream(stream);
// Path must be absolute within workspace, should auto-approve
await session.handleRequest('', { prompt: 'Test' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Test' }, [], undefined, authInfo, CancellationToken.None);
expect(result).toEqual({ kind: 'approved' });
});
@@ -227,7 +228,7 @@ describe('CopilotCLISession', () => {
session.attachStream(stream);
// Path must be absolute within workspace, should auto-approve
await session.handleRequest('', { prompt: 'Test' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Test' }, [], undefined, authInfo, CancellationToken.None);
expect(result).toEqual({ kind: 'approved' });
});
@@ -252,7 +253,7 @@ describe('CopilotCLISession', () => {
}));
// Path must be absolute within workspace, should auto-approve
await session.handleRequest('', { prompt: 'Test' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Test' }, [], undefined, authInfo, CancellationToken.None);
const file = path.join('/workingDirectory', 'file.ts');
expect(result).toEqual({ kind: 'denied-interactively-by-user' });
expect(askedForPermission).not.toBeUndefined();
@@ -275,7 +276,7 @@ describe('CopilotCLISession', () => {
const stream = new MockChatResponseStream();
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Write' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Write' }, [], undefined, authInfo, CancellationToken.None);
expect(result).toEqual({ kind: 'approved' });
});
@@ -293,7 +294,7 @@ describe('CopilotCLISession', () => {
};
const stream = new MockChatResponseStream();
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Write' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Write' }, [], undefined, authInfo, CancellationToken.None);
expect(result).toEqual({ kind: 'denied-interactively-by-user' });
});
@@ -311,7 +312,7 @@ describe('CopilotCLISession', () => {
};
const stream = new MockChatResponseStream();
session.attachStream(stream);
await session.handleRequest('', { prompt: 'Write' }, [], undefined, authInfo, CancellationToken.None);
await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Write' }, [], undefined, authInfo, CancellationToken.None);
expect(result).toEqual({ kind: 'denied-interactively-by-user' });
});
@@ -333,7 +334,7 @@ describe('CopilotCLISession', () => {
});
// Act: start handling request (do not await yet)
const requestPromise = session.handleRequest('', { prompt: 'Edits' }, [], undefined, authInfo, CancellationToken.None);
const requestPromise = session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'Edits' }, [], undefined, authInfo, CancellationToken.None);
// Wait a tick to ensure event listeners are registered inside handleRequest
await new Promise(r => setTimeout(r, 0));

View File

@@ -983,18 +983,18 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
// This is a request that was created in createCLISessionAndSubmitRequest with attachments already resolved.
const { prompt, attachments } = contextForRequest;
this.contextForRequest.delete(session.object.sessionId);
await session.object.handleRequest(request.id, { prompt }, attachments, modelId, authInfo, token);
await session.object.handleRequest(request, { prompt }, attachments, modelId, authInfo, token);
await this.commitWorktreeChangesIfNeeded(session.object, token);
} else if (request.command && !request.prompt && !isUntitled) {
const input = (copilotCLICommands as readonly string[]).includes(request.command)
? { command: request.command as CopilotCLICommand }
: { prompt: `/${request.command}` };
await session.object.handleRequest(request.id, input, [], modelId, authInfo, token);
await session.object.handleRequest(request, input, [], modelId, authInfo, token);
await this.commitWorktreeChangesIfNeeded(session.object, token);
} else {
// Construct the full prompt with references to be sent to CLI.
const { prompt, attachments } = await this.promptResolver.resolvePrompt(request, undefined, [], session.object.options.isolationEnabled, session.object.options.workingDirectory, token);
await session.object.handleRequest(request.id, { prompt }, attachments, modelId, authInfo, token);
await session.object.handleRequest(request, { prompt }, attachments, modelId, authInfo, token);
await this.commitWorktreeChangesIfNeeded(session.object, token);
}
@@ -1331,7 +1331,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
// The caller is most likely a chat editor or the like.
// Now that we've delegated it to a session, we can get out of here.
// Else if the request takes say 10 minutes, the caller would be blocked for that long.
session.object.handleRequest(request.id, { prompt }, attachments, model, authInfo, token)
session.object.handleRequest(request, { prompt }, attachments, model, authInfo, token)
.then(() => this.commitWorktreeChangesIfNeeded(session.object, token))
.catch(error => {
this.logService.error(`Failed to handle CLI session request: ${error}`);

View File

@@ -20,7 +20,7 @@ import { NullTelemetryService } from '../../../../platform/telemetry/common/null
import type { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
import { MockExtensionContext } from '../../../../platform/test/node/extensionContext';
import { IWorkspaceService, NullWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
import { LanguageModelTextPart, LanguageModelToolResult2 } from '../../../../util/common/test/shims/chatTypes';
import { LanguageModelTextPart, LanguageModelToolResult2 } from '../../../../vscodeTypes';
import { mock } from '../../../../util/common/test/simpleMock';
import { CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
@@ -38,7 +38,7 @@ import { MockCliSdkSession, MockCliSdkSessionManager, NullCopilotCLIAgents, Null
import { ChatSummarizerProvider } from '../../../prompt/node/summarizer';
import { createExtensionUnitTestingServices } from '../../../test/node/services';
import { MockChatResponseStream, TestChatRequest } from '../../../test/node/testHelpers';
import type { IToolsService } from '../../../tools/common/toolsService';
import { type IToolsService } from '../../../tools/common/toolsService';
import { mockLanguageModelChat } from '../../../tools/node/test/searchToolTestUtils';
import { IChatSessionWorkspaceFolderService } from '../../common/chatSessionWorkspaceFolderService';
import { IChatSessionWorktreeService, type ChatSessionWorktreeProperties } from '../../common/chatSessionWorktreeService';
@@ -187,7 +187,7 @@ function createChatContext(sessionId: string, isUntitled: boolean): vscode.ChatC
class TestCopilotCLISession extends CopilotCLISession {
public requests: Array<{ input: CopilotCLISessionInput; attachments: Attachment[]; modelId: string | undefined; authInfo: NonNullable<SessionOptions['authInfo']>; token: vscode.CancellationToken }> = [];
override handleRequest(requestId: string, input: CopilotCLISessionInput, attachments: Attachment[], modelId: string | undefined, authInfo: NonNullable<SessionOptions['authInfo']>, token: vscode.CancellationToken): Promise<void> {
override handleRequest(request: { id: string; toolInvocationToken: vscode.ChatParticipantToolToken }, input: CopilotCLISessionInput, attachments: Attachment[], modelId: string | undefined, authInfo: NonNullable<SessionOptions['authInfo']>, token: vscode.CancellationToken): Promise<void> {
this.requests.push({ input, attachments, modelId, authInfo, token });
return Promise.resolve();
}
@@ -293,7 +293,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
}
}();
}
const session = new TestCopilotCLISession(options, sdkSession, logService, workspaceService, sdk, instantiationService, delegationService, new NullRequestLogger(), new NullICopilotCLIImageSupport());
const session = new TestCopilotCLISession(options, sdkSession, logService, workspaceService, sdk, instantiationService, delegationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService());
cliSessions.push(session);
return disposables.add(session);
}

View File

@@ -9,7 +9,7 @@ import { MockFileSystemService } from '../../../../platform/filesystem/node/test
import { IGitService, RepoContext } from '../../../../platform/git/common/gitService';
import { ILogService } from '../../../../platform/log/common/logService';
import { NullWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
import { LanguageModelTextPart, LanguageModelToolResult2 } from '../../../../util/common/test/shims/chatTypes';
import { LanguageModelTextPart, LanguageModelToolResult2 } from '../../../../vscodeTypes';
import { mock } from '../../../../util/common/test/simpleMock';
import { CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';

View File

@@ -12,7 +12,7 @@ import { IIgnoreService } from '../../../../../platform/ignore/common/ignoreServ
import { ILogService } from '../../../../../platform/log/common/logService';
import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';
import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
import { ChatReferenceDiagnostic } from '../../../../../util/common/test/shims/chatTypes';
import { ChatReferenceDiagnostic } from '../../../../../vscodeTypes';
import { DiagnosticSeverity } from '../../../../../util/common/test/shims/enums';
import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';
import { mock } from '../../../../../util/common/test/simpleMock';

View File

@@ -411,7 +411,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt: 'What is 1+8?' }, [], undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'What is 1+8?' }, [], undefined, authInfo, CancellationToken.None);
// Verify we have a response of 9.
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
@@ -419,7 +419,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
assertStreamContains(stream, '9');
// Can send a subsequent request.
await session.object.handleRequest('', { prompt: 'What is 11+25?' }, [], undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'What is 11+25?' }, [], undefined, authInfo, CancellationToken.None);
// Verify we have a response of 36.
assertStreamContains(stream, '36');
})
@@ -436,7 +436,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
const session = await sessionService.createSession({ workingDirectory }, CancellationToken.None);
sessionId = session.object.sessionId;
await session.object.handleRequest('', { prompt: 'What is 1+8?' }, [], undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'What is 1+8?' }, [], undefined, authInfo, CancellationToken.None);
session.dispose();
}
@@ -456,7 +456,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt: 'What was my previous question?' }, [], undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt: 'What was my previous question?' }, [], undefined, authInfo, CancellationToken.None);
// Verify we have a response of 9.
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
@@ -475,7 +475,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, [], undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, [], undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -506,7 +506,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
}
}));
await session.object.handleRequest('', { prompt }, [], undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, [], undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -529,7 +529,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -551,7 +551,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -565,7 +565,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
[],
promptResolver
));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -591,7 +591,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertStreamContains(stream, 'throw');
@@ -611,7 +611,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -632,7 +632,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -659,7 +659,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -687,7 +687,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
const tsContents = await fs.readFile(tsFile, 'utf-8');
@@ -721,7 +721,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
}
}));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assertNoErrorsInStream(stream);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
@@ -743,7 +743,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -771,7 +771,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -800,7 +800,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);
@@ -829,7 +829,7 @@ ssuite.skip({ title: '@cli', location: 'external' }, async (_) => {
disposables.add(session);
disposables.add(session.object.attachStream(stream));
await session.object.handleRequest('', { prompt }, attachments, undefined, authInfo, CancellationToken.None);
await session.object.handleRequest({ id: '', toolInvocationToken: undefined as never }, { prompt }, attachments, undefined, authInfo, CancellationToken.None);
assert.strictEqual(session.object.status, ChatSessionStatus.Completed);
assertNoErrorsInStream(stream);