Use ChatWidget.input.selectedToolsModel as source of truth for what tools can be selected for a request (#246590)

re https://github.com/microsoft/vscode/issues/246589
This commit is contained in:
Johannes Rieken
2025-04-15 10:53:13 +02:00
committed by GitHub
parent a0ce4461e8
commit cd71f72e54
8 changed files with 42 additions and 21 deletions
@@ -55,7 +55,6 @@ import { IChatEditingService } from '../../common/chatEditingService.js';
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, OmittedState } from '../../common/chatModel.js';
import { ChatRequestAgentPart } from '../../common/chatParserTypes.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js';
import { imageToHash, isImage } from '../chatPasteProviders.js';
import { isQuickChat } from '../chatWidget.js';
@@ -682,7 +681,6 @@ export class AttachContextAction extends Action2 {
const chatAgentService = accessor.get(IChatAgentService);
const commandService = accessor.get(ICommandService);
const widgetService = accessor.get(IChatWidgetService);
const languageModelToolsService = accessor.get(ILanguageModelToolsService);
const quickChatService = accessor.get(IQuickChatService);
const clipboardService = accessor.get(IClipboardService);
const editorService = accessor.get(IEditorService);
@@ -749,7 +747,7 @@ export class AttachContextAction extends Action2 {
}
}
for (const tool of languageModelToolsService.getTools()) {
for (const tool of widget.input.selectedToolsModel.tools.get()) {
if (tool.canBeReferencedInPrompt) {
const item: IToolQuickPickItem = {
kind: 'tool',
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IChatVariablesService, IDynamicVariable } from '../common/chatVariables.js';
import { IToolData } from '../common/languageModelToolsService.js';
import { IChatWidgetService } from './chat.js';
import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
@@ -32,4 +33,12 @@ export class ChatVariablesService implements IChatVariablesService {
return model.variables;
}
getSelectedTools(sessionId: string): ReadonlyArray<IToolData> {
const widget = this.chatWidgetService.getWidgetBySessionId(sessionId);
if (!widget) {
return [];
}
return widget.input.selectedToolsModel.tools.get();
}
}
@@ -985,6 +985,10 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.renderWelcomeViewContentIfNeeded();
this.refreshParsedInput();
}));
this._register(autorun(r => {
this.input.selectedToolsModel.tools.read(r); // SIGNAL
this.refreshParsedInput();
}));
}
private onDidStyleChange(): void {
@@ -42,7 +42,6 @@ import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPa
import { IChatSlashCommandService } from '../../common/chatSlashCommands.js';
import { IDynamicVariable } from '../../common/chatVariables.js';
import { ChatAgentLocation, ChatMode } from '../../common/constants.js';
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { ChatSubmitAction } from '../actions/chatExecuteActions.js';
import { IChatWidget, IChatWidgetService } from '../chat.js';
import { ChatInputPart } from '../chatInputPart.js';
@@ -887,7 +886,6 @@ class ToolCompletions extends Disposable {
constructor(
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@ILanguageModelToolsService toolsService: ILanguageModelToolsService
) {
super();
@@ -908,7 +906,7 @@ class ToolCompletions extends Disposable {
const usedTools = widget.parsedInput.parts.filter((p): p is ChatRequestToolPart => p instanceof ChatRequestToolPart);
const usedToolNames = new Set(usedTools.map(v => v.toolName));
const toolItems: CompletionItem[] = [];
toolItems.push(...Array.from(toolsService.getTools())
toolItems.push(...widget.input.selectedToolsModel.tools.get()
.filter(t => t.canBeReferencedInPrompt)
.filter(t => !usedToolNames.has(t.toolReferenceName ?? ''))
.map((t): CompletionItem => {
@@ -11,7 +11,7 @@ import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynami
import { IChatSlashCommandService } from './chatSlashCommands.js';
import { IChatVariablesService, IDynamicVariable } from './chatVariables.js';
import { ChatAgentLocation, ChatMode } from './constants.js';
import { ILanguageModelToolsService } from './languageModelToolsService.js';
import { IToolData } from './languageModelToolsService.js';
const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent
const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)
@@ -28,12 +28,12 @@ export class ChatRequestParser {
@IChatAgentService private readonly agentService: IChatAgentService,
@IChatVariablesService private readonly variableService: IChatVariablesService,
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,
@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService,
) { }
parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest {
const parts: IParsedChatRequestPart[] = [];
const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls
const toolsByName = new Map<string, IToolData>((this.variableService.getSelectedTools(sessionId)).filter(t => t.toolReferenceName).map(t => [t.toolReferenceName!, t]));
let lineNumber = 1;
let column = 1;
@@ -43,7 +43,7 @@ export class ChatRequestParser {
let newPart: IParsedChatRequestPart | undefined;
if (previousChar.match(/\s/) || i === 0) {
if (char === chatVariableLeader) {
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts);
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName);
} else if (char === chatAgentLeader) {
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);
} else if (char === chatSubcommandLeader) {
@@ -141,7 +141,7 @@ export class ChatRequestParser {
return new ChatRequestAgentPart(agentRange, agentEditorRange, agent);
}
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestToolPart | undefined {
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, toolsByName: ReadonlyMap<string, IToolData>): ChatRequestAgentPart | ChatRequestToolPart | undefined {
const nextVariableMatch = message.match(variableReg);
if (!nextVariableMatch) {
return;
@@ -151,7 +151,7 @@ export class ChatRequestParser {
const varRange = new OffsetRange(offset, offset + full.length);
const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
const tool = this.toolsService.getToolByName(name);
const tool = toolsByName.get(name);
if (tool && tool.canBeReferencedInPrompt) {
return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon);
}
@@ -11,6 +11,7 @@ import { Location } from '../../../../editor/common/languages.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IChatModel, IDiagnosticVariableEntryFilterData } from './chatModel.js';
import { IChatContentReference, IChatProgressMessage } from './chatService.js';
import { IToolData } from './languageModelToolsService.js';
export interface IChatVariableData {
id: string;
@@ -45,6 +46,7 @@ export const IChatVariablesService = createDecorator<IChatVariablesService>('ICh
export interface IChatVariablesService {
_serviceBrand: undefined;
getDynamicVariables(sessionId: string): ReadonlyArray<IDynamicVariable>;
getSelectedTools(sessionId: string): ReadonlyArray<IToolData>;
}
export interface IDynamicVariable {
@@ -19,9 +19,8 @@ import { IChatService } from '../../common/chatService.js';
import { IChatSlashCommandService } from '../../common/chatSlashCommands.js';
import { IChatVariablesService } from '../../common/chatVariables.js';
import { ChatMode, ChatAgentLocation } from '../../common/constants.js';
import { ILanguageModelToolsService, IToolData } from '../../common/languageModelToolsService.js';
import { IToolData } from '../../common/languageModelToolsService.js';
import { MockChatService } from './mockChatService.js';
import { MockChatVariablesService } from './mockChatVariables.js';
suite('ChatRequestParser', () => {
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
@@ -29,7 +28,7 @@ suite('ChatRequestParser', () => {
let instantiationService: TestInstantiationService;
let parser: ChatRequestParser;
let toolsService: MockObject<ILanguageModelToolsService>;
let variableService: MockObject<IChatVariablesService>;
setup(async () => {
instantiationService = testDisposables.add(new TestInstantiationService());
instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));
@@ -37,11 +36,13 @@ suite('ChatRequestParser', () => {
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IContextKeyService, new MockContextKeyService());
instantiationService.stub(IChatVariablesService, new MockChatVariablesService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
toolsService = mockObject<ILanguageModelToolsService>()({});
instantiationService.stub(ILanguageModelToolsService, toolsService as any);
variableService = mockObject<IChatVariablesService>()();
variableService.getDynamicVariables.returns([]);
variableService.getSelectedTools.returns([]);
instantiationService.stub(IChatVariablesService, variableService as any);
});
test('plain text', async () => {
@@ -198,8 +199,10 @@ suite('ChatRequestParser', () => {
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
toolsService.getToolByName.onCall(0).returns({ id: 'get_selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData);
toolsService.getToolByName.onCall(1).returns({ id: 'get_debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData);
variableService.getSelectedTools.returns([
{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } },
{ id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }
] satisfies IToolData[]);
parser = instantiationService.createInstance(ChatRequestParser);
const result = parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole');
@@ -211,8 +214,10 @@ suite('ChatRequestParser', () => {
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
toolsService.getToolByName.onCall(0).returns({ id: 'get_selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData);
toolsService.getToolByName.onCall(1).returns({ id: 'get_debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } } satisfies IToolData);
variableService.getSelectedTools.returns([
{ id: 'get_selection', toolReferenceName: 'selection', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } },
{ id: 'get_debugConsole', toolReferenceName: 'debugConsole', canBeReferencedInPrompt: true, displayName: '', modelDescription: '', source: { type: 'internal' } }
] satisfies IToolData[]);
parser = instantiationService.createInstance(ChatRequestParser);
const result = parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole');
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js';
import { IToolData } from '../../common/languageModelToolsService.js';
export class MockChatVariablesService implements IChatVariablesService {
_serviceBrand: undefined;
@@ -11,4 +12,8 @@ export class MockChatVariablesService implements IChatVariablesService {
getDynamicVariables(sessionId: string): readonly IDynamicVariable[] {
return [];
}
getSelectedTools(sessionId: string): readonly IToolData[] {
return [];
}
}