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.
/*---------------------------------------------------------------------------------------------
claudeInteractiveTools.ts ×6
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../nls.js';
import { ConfirmationOptionKind, ChatInputAnswerState, ChatInputAnswerValueKind, ChatInputQuestionKind, ToolCallStatus, type ChatInputOption, type ChatInputQuestion, type ToolCallPendingConfirmationState } from '../../common/state/protocol/state.js';
import type { ChatInputAnswer } from '../../common/state/sessionState.js';
import { getClaudeToolDisplayName } from './claudeToolDisplay.js';
/**
* Pure projections between the Claude SDK's interactive built-in tool
* inputs/outputs and the agentHost workbench protocol.
*
* Phase 7 S3.5. The two interactive tools (`ExitPlanMode`,
* `AskUserQuestion`) are exempt from the SDK's `permissionMode` auto-
* approval and always reach `canUseTool`. The agent's job for each is
* to render a workbench prompt and translate the user's answer back
* into the SDK's `PermissionResult` shape — this module owns those
* projections so they can be tested without standing up an agent.
*/
// #region ExitPlanMode
/**
* Build the {@link ToolCallPendingConfirmationState} card body for the
* `ExitPlanMode` confirmation. Custom Approve / Deny buttons (no "Allow
* in this Session") so the approval is never remembered — each plan
* must be approved on its own merit. Mirrors the production extension's
* `exitPlanModeHandler.ts`.
*/
export function buildExitPlanModeConfirmationState(input: Record<string, unknown>, toolUseID: string): ToolCallPendingConfirmationState {
return {
status: ToolCallStatus.PendingConfirmation,
toolCallId: toolUseID,
toolName: 'ExitPlanMode',
displayName: getClaudeToolDisplayName('ExitPlanMode'),
invocationMessage: { markdown: plan },
toolInput: JSON.stringify(input),
confirmationTitle: localize('claude.exitPlanMode.title', "Ready to code?"),
options: [
{ id: 'approve', label: localize('claude.exitPlanMode.approve', "Approve"), kind: ConfirmationOptionKind.Approve },
{ id: 'deny', label: localize('claude.exitPlanMode.deny', "Deny"), kind: ConfirmationOptionKind.Deny },
],
};
}
// #endregion
// #region AskUserQuestion
/**
* Narrowed view of the `AskUserQuestion` SDK input. The SDK delivers
* questions as `Record<string, unknown>`; we cast (no schema validation
* — the SDK is the upstream authority) and surface the subset we use.
*/
export interface ParsedAskUserQuestionInput {
readonly questions: ReadonlyArray<{
readonly question: string;
readonly header: string;
readonly options: ReadonlyArray<{ label: string; description?: string }>;
readonly multiSelect?: boolean;
readonly allowFreeformInput?: boolean;
}>;
}
/**
* Cast the `AskUserQuestion` SDK input into the typed shape. Returns
* `undefined` when there are no questions — the agent translates that
* to a `deny` `PermissionResult`.
*/
export function parseAskUserQuestionInput(input: Record<string, unknown>): ParsedAskUserQuestionInput | undefined {
if (!askInput.questions?.length) {
}
}
/**
* Derive the workbench question id for the `idx`-th SDK question.
* Both {@link buildAskUserSessionInputQuestions} and
* {@link flattenAskUserAnswers} key into the answers map by this id, so
* keep the two callers in sync via this helper. Empty-header questions
* fall back to a positional id so they round-trip; they would
* otherwise collide on `''`.
*/
return header || `q-${idx}`;
}
/**
* Project the parsed SDK questions into the workbench's
* {@link ChatInputQuestion} shape. `multiSelect` flips the question
* kind; the rest of the fields map 1:1.
*/
export function buildAskUserSessionInputQuestions(askInput: ParsedAskUserQuestionInput): ChatInputQuestion[] {
const opts: ChatInputOption[] = q.options.map(opt => ({
label: opt.label,
...(opt.description !== undefined ? { description: opt.description } : {}),
const id = askUserQuestionId(q.header, idx);
return q.multiSelect
id,
kind: ChatInputQuestionKind.MultiSelect,
title: q.header,
message: q.question,
options: opts,
allowFreeformInput: q.allowFreeformInput ?? false,
}
id,
kind: ChatInputQuestionKind.SingleSelect,
title: q.header,
message: q.question,
options: opts,
allowFreeformInput: q.allowFreeformInput ?? false,
};
}
/**
* Re-key the workbench answers from `{questionHeader → ChatInputAnswer}`
* into the production extension's `Record<questionText, valueString>`
* contract. Skipped questions and empty answers are dropped; the result
* is `{}` when nothing was answered. Single-select / multi-select /
* text answer shapes flatten to a comma-joined string (matching the
* production extension's wire format).
*/
export function flattenAskUserAnswers(askInput: ParsedAskUserQuestionInput, answers: Record<string, ChatInputAnswer>): Record<string, string> {
for (let idx = 0; idx < askInput.questions.length; idx++) {
const q = askInput.questions[idx];
const a = answers[askUserQuestionId(q.header, idx)];
if (!a || a.state === ChatInputAnswerState.Skipped) {
}
const value = a.value;
if (value.kind === ChatInputAnswerValueKind.Selected) {
if (value.freeformValues) { parts.push(...value.freeformValues); }
} else if (value.kind === ChatInputAnswerValueKind.SelectedMany) {
claudeInteractiveTools.ts ×3
if (value.freeformValues) { parts.push(...value.freeformValues); }
parts.push(value.value);
}
}
return result;
}
// #endregion