mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 12:10:22 -05:00
Update background handling of update_todo tool (#3845)
This commit is contained in:
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user