src/vs/platform/agentHost/node/claude/claudeInteractiveTools.ts

160 LOC · 160 covered · 0 uncovered · 27 ranges · 376 concepts · 17 introducers · 211 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

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 related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- claudeInteractiveTools.ts ×6
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { localize } from '../../../../nls.js';
7 > import { ConfirmationOptionKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ToolCallStatus, type ChatInputOption, type ChatInputQuestion, type ToolCallPendingConfirmationState } from '../../common/state/protocol/state.js';
8 > import type { ChatInputAnswer } from '../../common/state/sessionState.js';
9 > import { getClaudeToolDisplayName } from './claudeToolDisplay.js';
10 >
11 > /**
12 > * Pure projections between the Claude SDK's interactive built-in tool
13 > * inputs/outputs and the agentHost workbench protocol.
14 > *
15 > * Phase 7 S3.5. The two interactive tools (`ExitPlanMode`,
16 > * `AskUserQuestion`) are exempt from the SDK's `permissionMode` auto-
17 > * approval and always reach `canUseTool`. The agent's job for each is
18 > * to render a workbench prompt and translate the user's answer back
19 > * into the SDK's `PermissionResult` shape — this module owns those
20 > * projections so they can be tested without standing up an agent.
21 > */
22 >
23 > // #region ExitPlanMode
24 >
25 > /**
26 > * Build the {@link ToolCallPendingConfirmationState} card body for the
27 > * `ExitPlanMode` confirmation. Custom Approve / Deny buttons (no "Allow
28 > * in this Session") so the approval is never remembered — each plan
29 > * must be approved on its own merit. Mirrors the production extension's
30 > * `exitPlanModeHandler.ts`.
31 > */
32 > export function buildExitPlanModeConfirmationState(input: Record<string, unknown>, toolUseID: string): ToolCallPendingConfirmationState {
33 > const plan = typeof input.plan === 'string' ? input.plan : ''; claudeInteractiveTools.ts ×1
34 > return {
35 > status: ToolCallStatus.PendingConfirmation,
36 > toolCallId: toolUseID,
37 > toolName: 'ExitPlanMode',
38 > displayName: getClaudeToolDisplayName('ExitPlanMode'),
39 > invocationMessage: { markdown: plan },
40 > toolInput: JSON.stringify(input),
41 > confirmationTitle: localize('claude.exitPlanMode.title', "Ready to code?"),
42 > options: [
43 > { id: 'approve', label: localize('claude.exitPlanMode.approve', "Approve"), kind: ConfirmationOptionKind.Approve },
44 > { id: 'deny', label: localize('claude.exitPlanMode.deny', "Deny"), kind: ConfirmationOptionKind.Deny },
45 > ],
46 > };
47 > }
49 > // #endregion
50 >
51 > // #region AskUserQuestion
52 >
53 > /**
54 > * Narrowed view of the `AskUserQuestion` SDK input. The SDK delivers
55 > * questions as `Record<string, unknown>`; we cast (no schema validation
56 > * — the SDK is the upstream authority) and surface the subset we use.
57 > */
58 > export interface ParsedAskUserQuestionInput {
59 > readonly questions: ReadonlyArray<{
60 > readonly question: string;
61 > readonly header: string;
62 > readonly options: ReadonlyArray<{ label: string; description?: string }>;
63 > readonly multiSelect?: boolean;
64 > readonly allowFreeformInput?: boolean;
65 > }>;
66 > }
67 >
68 > /**
69 > * Cast the `AskUserQuestion` SDK input into the typed shape. Returns
70 > * `undefined` when there are no questions — the agent translates that
71 > * to a `deny` `PermissionResult`.
72 > */
73 > export function parseAskUserQuestionInput(input: Record<string, unknown>): ParsedAskUserQuestionInput | undefined {
74 > const askInput = input as Partial<ParsedAskUserQuestionInput>; claudeInteractiveTools.ts ×1
75 > if (!askInput.questions?.length) {
76 > return undefined; claudeInteractiveTools.ts ×1
77 > }
78 > return { questions: askInput.questions }; claudeInteractiveTools.ts ×1
79 > }
81 > /**
82 > * Derive the workbench question id for the `idx`-th SDK question.
83 > * Both {@link buildAskUserSessionInputQuestions} and
84 > * {@link flattenAskUserAnswers} key into the answers map by this id, so
85 > * keep the two callers in sync via this helper. Empty-header questions
86 > * fall back to a positional id so they round-trip; they would
87 > * otherwise collide on `''`.
88 > */
89 > function askUserQuestionId(header: string, idx: number): string { claudeInteractiveTools.ts ×1
90 > return header || `q-${idx}`;
91 > }
93 > /**
94 > * Project the parsed SDK questions into the workbench's
95 > * {@link ChatInputQuestion} shape. `multiSelect` flips the question
96 > * kind; the rest of the fields map 1:1.
97 > */
98 > export function buildAskUserSessionInputQuestions(askInput: ParsedAskUserQuestionInput): ChatInputQuestion[] {
99 > return askInput.questions.map((q, idx) => { claudeInteractiveTools.ts ×3
100 > const opts: ChatInputOption[] = q.options.map(opt => ({
101 > id: opt.label, claudeInteractiveTools.ts ×1
102 > label: opt.label,
103 > ...(opt.description !== undefined ? { description: opt.description } : {}),
105 > const id = askUserQuestionId(q.header, idx);
106 > return q.multiSelect
108 > id,
109 > kind: ChatInputQuestionKind.MultiSelect,
110 > title: q.header,
111 > message: q.question,
112 > options: opts,
113 > allowFreeformInput: q.allowFreeformInput ?? false,
114 > }
116 > id,
117 > kind: ChatInputQuestionKind.SingleSelect,
118 > title: q.header,
119 > message: q.question,
120 > options: opts,
121 > allowFreeformInput: q.allowFreeformInput ?? false,
122 > };
124 > }
126 > /**
127 > * Re-key the workbench answers from `{questionHeader → ChatInputAnswer}`
128 > * into the production extension's `Record<questionText, valueString>`
129 > * contract. Skipped questions and empty answers are dropped; the result
130 > * is `{}` when nothing was answered. Single-select / multi-select /
131 > * text answer shapes flatten to a comma-joined string (matching the
132 > * production extension's wire format).
133 > */
134 > export function flattenAskUserAnswers(askInput: ParsedAskUserQuestionInput, answers: Record<string, ChatInputAnswer>): Record<string, string> {
135 > const result: Record<string, string> = {}; claudeInteractiveTools.ts ×2
136 > for (let idx = 0; idx < askInput.questions.length; idx++) {
137 > const q = askInput.questions[idx];
138 > const a = answers[askUserQuestionId(q.header, idx)];
139 > if (!a || a.state === ChatInputAnswerState.Skipped) {
141 > }
142 > const parts: string[] = []; claudeInteractiveTools.ts ×3
143 > const value = a.value;
144 > if (value.kind === ChatInputAnswerValueKind.Selected) {
145 > if (value.value) { parts.push(value.value); } claudeInteractiveTools.ts ×1
146 > if (value.freeformValues) { parts.push(...value.freeformValues); }
147 > } else if (value.kind === ChatInputAnswerValueKind.SelectedMany) { claudeInteractiveTools.ts ×3
148 > parts.push(...value.value); claudeInteractiveTools.ts ×1
149 > if (value.freeformValues) { parts.push(...value.freeformValues); }
150 > } else if (value.kind === ChatInputAnswerValueKind.Text) { claudeInteractiveTools.ts ×1
151 > parts.push(value.value);
152 > }
153 > if (parts.length > 0) { claudeInteractiveTools.ts ×3
154 > result[q.question] = parts.join(', '); claudeInteractiveTools.ts ×1
155 > }
157 > return result;
158 > }
160 > // #endregion