askQuestionsTool.ts ×25

Frontier kind: Code frontier

unlabeled · c_2e32573edea2

2 tests · 27496 LOC · 140 files · introduces 0 tests · 118 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
25 ranges118 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2848 ranges27496 lines · 140 files · Browse complete extent
All tests (intent)
2 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 118 introduced LOC across 25 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts 118 introduced LOC · 25 ranges

Open complete file

52 } as const;
53
54 > function truncateToLimit(value: string | undefined, limit: number): string | undefined { askQuestionsTool.ts
55 > if (value === undefined) {
56 return undefined;
57 }
58 > if (value.length > limit) { askQuestionsTool.ts
59 return value.slice(0, limit - 3) + '...';
60 }
61 > return value; askQuestionsTool.ts
62 > }
63
64 export interface IQuestionOption {
184
185 async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
186 > const stopWatch = StopWatch.create(true); askQuestionsTool.ts
187 > const parameters = invocation.parameters as IAskQuestionsParams;
188 > const { questions } = parameters;
189 > this.logService.trace(`[AskQuestionsTool] Invoking with ${questions?.length ?? 0} question(s)`);
190 >
191 > if (!questions || questions.length === 0) {
192 throw new Error(localize('askQuestionsTool.noQuestions', 'No questions provided. The questions array must contain at least one question.'));
193 }
195 > const chatSessionResource = invocation.context?.sessionResource;
196 > const chatRequestId = invocation.chatRequestId;
197 > const { request, sessionResource } = this.getRequest(chatSessionResource, chatRequestId);
198 >
199 > if (!sessionResource || !request) {
200 this.logService.warn('[AskQuestionsTool] Missing chat context; marking all questions as skipped.');
201 return this.createSkippedResult(questions);
202 }
204 > // In autopilot mode or when auto-reply is enabled, the user is not available —
205 > // auto-respond instead of blocking. Still append a completed carousel so the
206 > // user can see what was skipped.
207 > const resolveId = invocation.chatStreamToolCallId ?? invocation.callId;
208 > if (request.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot || this.configService.getValue<boolean>(ChatConfiguration.AutoReply)) {
209 const reason = request.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot ? 'Autopilot mode' : 'Auto-reply enabled';
210 this.logService.info(`[AskQuestionsTool] ${reason}: auto-responding to questions`);
216 return this.createAutopilotResult(questions);
217 }
219 > const { carousel, idToHeaderMap } = this.toQuestionCarousel(questions, resolveId);
220 > carousel.terminalId = this.extractTerminalId(request);
221 > this.logService.trace(`[AskQuestionsTool] request=${request.id} terminalExecutionId=${request.terminalExecutionId ?? 'undefined'} carousel.terminalId=${carousel.terminalId ?? 'undefined'}`);
222 > this.chatService.appendProgress(request, carousel);
223 > const externalAnswerListener = this.chatService.onDidReceiveQuestionCarouselAnswer(event => {
224 if (event.resolveId !== carousel.resolveId || carousel.isUsed) {
225 return;
226 }
227 carousel.dismiss(event.answers);
229 >
230 > let answerResult: { answers: IChatQuestionAnswers | undefined } | undefined;
231 > try {
232 > answerResult = await raceCancellation(carousel.completion.p, token);
233 > } catch (error) {
234 if (error instanceof CancellationError) {
235 carousel.dismiss(undefined);
236 }
237 throw error;
238 > } finally { askQuestionsTool.ts
239 > externalAnswerListener.dispose();
240 > }
241 > if (!answerResult) {
242 carousel.dismiss(undefined);
243 throw new CancellationError();
249 // When the user typed directly in the terminal (bypassing the carousel),
250 // tell the agent to stop asking questions and wait for the command to finish.
251 > if (carousel.dismissedByTerminalInput && carousel.terminalId) { askQuestionsTool.ts
252 this.logService.info(`[AskQuestionsTool] Carousel dismissed because user typed directly in terminal ${carousel.terminalId}`);
253 return {
261 progress.report({ message: localize('askQuestionsTool.progress', 'Analyzing your answers...') });
262
263 > const converted = this.convertCarouselAnswers(questions, answerResult?.answers, idToHeaderMap); askQuestionsTool.ts
264 > const { answeredCount, skippedCount, freeTextCount, recommendedAvailableCount, recommendedSelectedCount } = this.collectMetrics(questions, converted);
265 >
266 > this.sendTelemetry(invocation.chatRequestId, questions.length, answeredCount, skippedCount, freeTextCount, recommendedAvailableCount, recommendedSelectedCount, stopWatch.elapsed());
267 >
268 > const toolResultJson = JSON.stringify(converted);
269 > this.logService.trace(`[AskQuestionsTool] Returning tool result with metrics: questions=${questions.length}, answered=${answeredCount}, skipped=${skippedCount}, freeText=${freeTextCount}, recommendedAvailable=${recommendedAvailableCount}, recommendedSelected=${recommendedSelectedCount}`);
270 > return {
271 > content: [{ kind: 'text', value: toolResultJson }]
272 > };
273 > }
274
275 async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
303
304 private getRequest(chatSessionResource: URI | undefined, chatRequestId: string | undefined): { request: IChatRequestModel | undefined; sessionResource: URI | undefined } {
305 > if (!chatSessionResource) { askQuestionsTool.ts
306 return { request: undefined, sessionResource: undefined };
307 }
309 > const model = this.chatService.getSession(chatSessionResource);
310 > let request: IChatRequestModel | undefined;
311 > if (model) {
312 > // Prefer an exact match on chatRequestId when possible
313 > if (chatRequestId) {
314 > request = model.getRequests().find(r => r.id === chatRequestId);
315 > }
316 > // Fall back to the most recent request in the session if we can't find a match
317 > if (!request) {
318 request = model.getRequests().at(-1);
319 }
321 >
322 > if (!request) {
323 return { request: undefined, sessionResource: chatSessionResource };
324 }
326 > return { request, sessionResource: chatSessionResource };
327 > }
328
329 /**
338 */
339 private extractTerminalId(request: IChatRequestModel): string | undefined {
340 > if (request.terminalExecutionId) { askQuestionsTool.ts
341 return request.terminalExecutionId;
342 }
344 > const match = request.message.text.match(/\[Terminal (?<termId>\S+) notification:/);
345 > if (match?.groups?.termId) {
346 return match.groups.termId;
347 }
349 > // Search completed runInTerminal tool invocations in the response
350 > // for the terminal execution ID (covers foreground/timeout path).
351 > // Only match output that explicitly indicates the terminal is still
352 > // running and waiting for input; otherwise the question is unrelated
353 > // to the prior terminal command.
354 > const response = request.response;
355 > if (response) {
356 const parts = response.response.value;
357 for (let i = parts.length - 1; i >= 0; i--) {
372 }
373 }
375 > return undefined;
376 > }
377
378 private toQuestionCarousel(questions: IQuestion[], resolveId?: string): { carousel: ChatQuestionCarouselData; idToHeaderMap: Map<string, string> } {
379 > const idToHeaderMap = new Map<string, string>(); askQuestionsTool.ts
380 > const carouselResolveId = resolveId ?? generateUuid();
381 > const mappedQuestions = questions.map((question, index) => this.toChatQuestion(question, idToHeaderMap, carouselResolveId, index));
382 > return {
383 > carousel: new ChatQuestionCarouselData(mappedQuestions, true, carouselResolveId),
384 > idToHeaderMap
385 > };
386 > }
387
388 private toChatQuestion(question: IQuestion, idToHeaderMap: Map<string, string>, resolveId: string, index: number): IChatQuestion {
389 > let type: IChatQuestion['type']; askQuestionsTool.ts
390 > if (!question.options || question.options.length === 0) {
391 type = 'text';
392 > } else if (question.multiSelect) { askQuestionsTool.ts
393 type = 'multiSelect';
394 } else {
395 type = 'singleSelect';
396 }
398 > let defaultValue: string | string[] | undefined;
399 > if (question.options) {
400 const recommendedOptions = question.options.filter(opt => opt.recommended);
401 if (recommendedOptions.length > 0) {
403 }
404 }
406 > // Use a stable UUID as the internal ID to avoid collisions when truncating headers
407 > // The original header is preserved in idToHeaderMap for answer correlation
408 > const internalId = `${resolveId}:${index}`;
409 > idToHeaderMap.set(internalId, question.header);
410 >
411 > // Truncate header for display only
412 > const displayTitle = truncateToLimit(question.header, HardLimits.header) ?? question.header;
413 >
414 > return {
415 > id: internalId,
416 > type,
417 > title: displayTitle,
418 > message: question.question,
419 > detailedMessage: question.message,
420 > options: question.options?.map(opt => ({
421 id: opt.label,
422 label: opt.description ? `${opt.label} - ${opt.description}` : opt.label,
423 value: opt.label
424 > })), askQuestionsTool.ts
425 > defaultValue,
426 > allowFreeformInput: question.allowFreeformInput ?? true
427 > };
428 > }
429
430 protected convertCarouselAnswers(questions: IQuestion[], carouselAnswers: IChatQuestionAnswers | undefined, idToHeaderMap: Map<string, string>): IAnswerResult {