summaryrefslogtreecommitdiff
path: root/packages/cli/src/acp
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/acp')
-rw-r--r--packages/cli/src/acp/acp.ts464
-rw-r--r--packages/cli/src/acp/acpPeer.ts677
2 files changed, 0 insertions, 1141 deletions
diff --git a/packages/cli/src/acp/acp.ts b/packages/cli/src/acp/acp.ts
deleted file mode 100644
index 1fbdf7a8..00000000
--- a/packages/cli/src/acp/acp.ts
+++ /dev/null
@@ -1,464 +0,0 @@
-/**
- * @license
- * Copyright 2025 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-/* ACP defines a schema for a simple (experimental) JSON-RPC protocol that allows GUI applications to interact with agents. */
-
-import { Icon } from '@google/gemini-cli-core';
-import { WritableStream, ReadableStream } from 'node:stream/web';
-
-export class ClientConnection implements Client {
- #connection: Connection<Agent>;
-
- constructor(
- agent: (client: Client) => Agent,
- input: WritableStream<Uint8Array>,
- output: ReadableStream<Uint8Array>,
- ) {
- this.#connection = new Connection(agent(this), input, output);
- }
-
- /**
- * Streams part of an assistant response to the client
- */
- async streamAssistantMessageChunk(
- params: StreamAssistantMessageChunkParams,
- ): Promise<void> {
- await this.#connection.sendRequest('streamAssistantMessageChunk', params);
- }
-
- /**
- * Request confirmation before running a tool
- *
- * When allowed, the client returns a [`ToolCallId`] which can be used
- * to update the tool call's `status` and `content` as it runs.
- */
- requestToolCallConfirmation(
- params: RequestToolCallConfirmationParams,
- ): Promise<RequestToolCallConfirmationResponse> {
- return this.#connection.sendRequest('requestToolCallConfirmation', params);
- }
-
- /**
- * pushToolCall allows the agent to start a tool call
- * when it does not need to request permission to do so.
- *
- * The returned id can be used to update the UI for the tool
- * call as needed.
- */
- pushToolCall(params: PushToolCallParams): Promise<PushToolCallResponse> {
- return this.#connection.sendRequest('pushToolCall', params);
- }
-
- /**
- * updateToolCall allows the agent to update the content and status of the tool call.
- *
- * The new content replaces what is currently displayed in the UI.
- *
- * The [`ToolCallId`] is included in the response of
- * `pushToolCall` or `requestToolCallConfirmation` respectively.
- */
- async updateToolCall(params: UpdateToolCallParams): Promise<void> {
- await this.#connection.sendRequest('updateToolCall', params);
- }
-}
-
-type AnyMessage = AnyRequest | AnyResponse;
-
-type AnyRequest = {
- id: number;
- method: string;
- params?: unknown;
-};
-
-type AnyResponse = { jsonrpc: '2.0'; id: number } & Result<unknown>;
-
-type Result<T> =
- | {
- result: T;
- }
- | {
- error: ErrorResponse;
- };
-
-type ErrorResponse = {
- code: number;
- message: string;
- data?: { details?: string };
-};
-
-type PendingResponse = {
- resolve: (response: unknown) => void;
- reject: (error: ErrorResponse) => void;
-};
-
-class Connection<D> {
- #pendingResponses: Map<number, PendingResponse> = new Map();
- #nextRequestId: number = 0;
- #delegate: D;
- #peerInput: WritableStream<Uint8Array>;
- #writeQueue: Promise<void> = Promise.resolve();
- #textEncoder: TextEncoder;
-
- constructor(
- delegate: D,
- peerInput: WritableStream<Uint8Array>,
- peerOutput: ReadableStream<Uint8Array>,
- ) {
- this.#peerInput = peerInput;
- this.#textEncoder = new TextEncoder();
-
- this.#delegate = delegate;
- this.#receive(peerOutput);
- }
-
- async #receive(output: ReadableStream<Uint8Array>) {
- let content = '';
- const decoder = new TextDecoder();
- for await (const chunk of output) {
- content += decoder.decode(chunk, { stream: true });
- const lines = content.split('\n');
- content = lines.pop() || '';
-
- for (const line of lines) {
- const trimmedLine = line.trim();
-
- if (trimmedLine) {
- const message = JSON.parse(trimmedLine);
- this.#processMessage(message);
- }
- }
- }
- }
-
- async #processMessage(message: AnyMessage) {
- if ('method' in message) {
- const response = await this.#tryCallDelegateMethod(
- message.method,
- message.params,
- );
-
- await this.#sendMessage({
- jsonrpc: '2.0',
- id: message.id,
- ...response,
- });
- } else {
- this.#handleResponse(message);
- }
- }
-
- async #tryCallDelegateMethod(
- method: string,
- params?: unknown,
- ): Promise<Result<unknown>> {
- const methodName = method as keyof D;
- if (typeof this.#delegate[methodName] !== 'function') {
- return RequestError.methodNotFound(method).toResult();
- }
-
- try {
- const result = await this.#delegate[methodName](params);
- return { result: result ?? null };
- } catch (error: unknown) {
- if (error instanceof RequestError) {
- return error.toResult();
- }
-
- let details;
-
- if (error instanceof Error) {
- details = error.message;
- } else if (
- typeof error === 'object' &&
- error != null &&
- 'message' in error &&
- typeof error.message === 'string'
- ) {
- details = error.message;
- }
-
- return RequestError.internalError(details).toResult();
- }
- }
-
- #handleResponse(response: AnyResponse) {
- const pendingResponse = this.#pendingResponses.get(response.id);
- if (pendingResponse) {
- if ('result' in response) {
- pendingResponse.resolve(response.result);
- } else if ('error' in response) {
- pendingResponse.reject(response.error);
- }
- this.#pendingResponses.delete(response.id);
- }
- }
-
- async sendRequest<Req, Resp>(method: string, params?: Req): Promise<Resp> {
- const id = this.#nextRequestId++;
- const responsePromise = new Promise((resolve, reject) => {
- this.#pendingResponses.set(id, { resolve, reject });
- });
- await this.#sendMessage({ jsonrpc: '2.0', id, method, params });
- return responsePromise as Promise<Resp>;
- }
-
- async #sendMessage(json: AnyMessage) {
- const content = JSON.stringify(json) + '\n';
- this.#writeQueue = this.#writeQueue
- .then(async () => {
- const writer = this.#peerInput.getWriter();
- try {
- await writer.write(this.#textEncoder.encode(content));
- } finally {
- writer.releaseLock();
- }
- })
- .catch((error) => {
- // Continue processing writes on error
- console.error('ACP write error:', error);
- });
- return this.#writeQueue;
- }
-}
-
-export class RequestError extends Error {
- data?: { details?: string };
-
- constructor(
- public code: number,
- message: string,
- details?: string,
- ) {
- super(message);
- this.name = 'RequestError';
- if (details) {
- this.data = { details };
- }
- }
-
- static parseError(details?: string): RequestError {
- return new RequestError(-32700, 'Parse error', details);
- }
-
- static invalidRequest(details?: string): RequestError {
- return new RequestError(-32600, 'Invalid request', details);
- }
-
- static methodNotFound(details?: string): RequestError {
- return new RequestError(-32601, 'Method not found', details);
- }
-
- static invalidParams(details?: string): RequestError {
- return new RequestError(-32602, 'Invalid params', details);
- }
-
- static internalError(details?: string): RequestError {
- return new RequestError(-32603, 'Internal error', details);
- }
-
- toResult<T>(): Result<T> {
- return {
- error: {
- code: this.code,
- message: this.message,
- data: this.data,
- },
- };
- }
-}
-
-// Protocol types
-
-export const LATEST_PROTOCOL_VERSION = '0.0.9';
-
-export type AssistantMessageChunk =
- | {
- text: string;
- }
- | {
- thought: string;
- };
-
-export type ToolCallConfirmation =
- | {
- description?: string | null;
- type: 'edit';
- }
- | {
- description?: string | null;
- type: 'execute';
- command: string;
- rootCommand: string;
- }
- | {
- description?: string | null;
- type: 'mcp';
- serverName: string;
- toolDisplayName: string;
- toolName: string;
- }
- | {
- description?: string | null;
- type: 'fetch';
- urls: string[];
- }
- | {
- description: string;
- type: 'other';
- };
-
-export type ToolCallContent =
- | {
- type: 'markdown';
- markdown: string;
- }
- | {
- type: 'diff';
- newText: string;
- oldText: string | null;
- path: string;
- };
-
-export type ToolCallStatus = 'running' | 'finished' | 'error';
-
-export type ToolCallId = number;
-
-export type ToolCallConfirmationOutcome =
- | 'allow'
- | 'alwaysAllow'
- | 'alwaysAllowMcpServer'
- | 'alwaysAllowTool'
- | 'reject'
- | 'cancel';
-
-/**
- * A part in a user message
- */
-export type UserMessageChunk =
- | {
- text: string;
- }
- | {
- path: string;
- };
-
-export interface StreamAssistantMessageChunkParams {
- chunk: AssistantMessageChunk;
-}
-
-export interface RequestToolCallConfirmationParams {
- confirmation: ToolCallConfirmation;
- content?: ToolCallContent | null;
- icon: Icon;
- label: string;
- locations?: ToolCallLocation[];
-}
-
-export interface ToolCallLocation {
- line?: number | null;
- path: string;
-}
-
-export interface PushToolCallParams {
- content?: ToolCallContent | null;
- icon: Icon;
- label: string;
- locations?: ToolCallLocation[];
-}
-
-export interface UpdateToolCallParams {
- content: ToolCallContent | null;
- status: ToolCallStatus;
- toolCallId: ToolCallId;
-}
-
-export interface RequestToolCallConfirmationResponse {
- id: ToolCallId;
- outcome: ToolCallConfirmationOutcome;
-}
-
-export interface PushToolCallResponse {
- id: ToolCallId;
-}
-
-export interface InitializeParams {
- /**
- * The version of the protocol that the client supports.
- * This should be the latest version supported by the client.
- */
- protocolVersion: string;
-}
-
-export interface SendUserMessageParams {
- chunks: UserMessageChunk[];
-}
-
-export interface InitializeResponse {
- /**
- * Indicates whether the agent is authenticated and
- * ready to handle requests.
- */
- isAuthenticated: boolean;
- /**
- * The version of the protocol that the agent supports.
- * If the agent supports the requested version, it should respond with the same version.
- * Otherwise, the agent should respond with the latest version it supports.
- */
- protocolVersion: string;
-}
-
-export interface Error {
- code: number;
- data?: unknown;
- message: string;
-}
-
-export interface Client {
- streamAssistantMessageChunk(
- params: StreamAssistantMessageChunkParams,
- ): Promise<void>;
-
- requestToolCallConfirmation(
- params: RequestToolCallConfirmationParams,
- ): Promise<RequestToolCallConfirmationResponse>;
-
- pushToolCall(params: PushToolCallParams): Promise<PushToolCallResponse>;
-
- updateToolCall(params: UpdateToolCallParams): Promise<void>;
-}
-
-export interface Agent {
- /**
- * Initializes the agent's state. It should be called before any other method,
- * and no other methods should be called until it has completed.
- *
- * If the agent is not authenticated, then the client should prompt the user to authenticate,
- * and then call the `authenticate` method.
- * Otherwise the client can send other messages to the agent.
- */
- initialize(params: InitializeParams): Promise<InitializeResponse>;
-
- /**
- * Begins the authentication process.
- *
- * This method should only be called if `initialize` indicates the user isn't already authenticated.
- * The Promise MUST not resolve until authentication is complete.
- */
- authenticate(): Promise<void>;
-
- /**
- * Allows the user to send a message to the agent.
- * This method should complete after the agent is finished, during
- * which time the agent may update the client by calling
- * streamAssistantMessageChunk and other methods.
- */
- sendUserMessage(params: SendUserMessageParams): Promise<void>;
-
- /**
- * Cancels the current generation.
- */
- cancelSendMessage(): Promise<void>;
-}
diff --git a/packages/cli/src/acp/acpPeer.ts b/packages/cli/src/acp/acpPeer.ts
deleted file mode 100644
index 40d8753f..00000000
--- a/packages/cli/src/acp/acpPeer.ts
+++ /dev/null
@@ -1,677 +0,0 @@
-/**
- * @license
- * Copyright 2025 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { WritableStream, ReadableStream } from 'node:stream/web';
-
-import {
- AuthType,
- Config,
- GeminiChat,
- ToolRegistry,
- logToolCall,
- ToolResult,
- convertToFunctionResponse,
- ToolCallConfirmationDetails,
- ToolConfirmationOutcome,
- clearCachedCredentialFile,
- isNodeError,
- getErrorMessage,
- isWithinRoot,
- getErrorStatus,
-} from '@google/gemini-cli-core';
-import * as acp from './acp.js';
-import { Agent } from './acp.js';
-import { Readable, Writable } from 'node:stream';
-import { Content, Part, FunctionCall, PartListUnion } from '@google/genai';
-import { LoadedSettings, SettingScope } from '../config/settings.js';
-import * as fs from 'fs/promises';
-import * as path from 'path';
-
-export async function runAcpPeer(config: Config, settings: LoadedSettings) {
- const stdout = Writable.toWeb(process.stdout) as WritableStream;
- const stdin = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
-
- // Stdout is used to send messages to the client, so console.log/console.info
- // messages to stderr so that they don't interfere with ACP.
- console.log = console.error;
- console.info = console.error;
- console.debug = console.error;
-
- new acp.ClientConnection(
- (client: acp.Client) => new GeminiAgent(config, settings, client),
- stdout,
- stdin,
- );
-}
-
-class GeminiAgent implements Agent {
- chat?: GeminiChat;
- pendingSend?: AbortController;
-
- constructor(
- private config: Config,
- private settings: LoadedSettings,
- private client: acp.Client,
- ) {}
-
- async initialize(_: acp.InitializeParams): Promise<acp.InitializeResponse> {
- let isAuthenticated = false;
- if (this.settings.merged.selectedAuthType) {
- try {
- await this.config.refreshAuth(this.settings.merged.selectedAuthType);
- isAuthenticated = true;
- } catch (error) {
- console.error('Failed to refresh auth:', error);
- }
- }
- return { protocolVersion: acp.LATEST_PROTOCOL_VERSION, isAuthenticated };
- }
-
- async authenticate(): Promise<void> {
- await clearCachedCredentialFile();
- await this.config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE);
- this.settings.setValue(
- SettingScope.User,
- 'selectedAuthType',
- AuthType.LOGIN_WITH_GOOGLE,
- );
- }
-
- async cancelSendMessage(): Promise<void> {
- if (!this.pendingSend) {
- throw new Error('Not currently generating');
- }
-
- this.pendingSend.abort();
- delete this.pendingSend;
- }
-
- async sendUserMessage(params: acp.SendUserMessageParams): Promise<void> {
- this.pendingSend?.abort();
- const pendingSend = new AbortController();
- this.pendingSend = pendingSend;
-
- if (!this.chat) {
- const geminiClient = this.config.getGeminiClient();
- this.chat = await geminiClient.startChat();
- }
-
- const promptId = Math.random().toString(16).slice(2);
- const chat = this.chat!;
- const toolRegistry: ToolRegistry = await this.config.getToolRegistry();
- const parts = await this.#resolveUserMessage(params, pendingSend.signal);
-
- let nextMessage: Content | null = { role: 'user', parts };
-
- while (nextMessage !== null) {
- if (pendingSend.signal.aborted) {
- chat.addHistory(nextMessage);
- return;
- }
-
- const functionCalls: FunctionCall[] = [];
-
- try {
- const responseStream = await chat.sendMessageStream(
- {
- message: nextMessage?.parts ?? [],
- config: {
- abortSignal: pendingSend.signal,
- tools: [
- {
- functionDeclarations: toolRegistry.getFunctionDeclarations(),
- },
- ],
- },
- },
- promptId,
- );
- nextMessage = null;
-
- for await (const resp of responseStream) {
- if (pendingSend.signal.aborted) {
- return;
- }
-
- if (resp.candidates && resp.candidates.length > 0) {
- const candidate = resp.candidates[0];
- for (const part of candidate.content?.parts ?? []) {
- if (!part.text) {
- continue;
- }
-
- this.client.streamAssistantMessageChunk({
- chunk: part.thought
- ? { thought: part.text }
- : { text: part.text },
- });
- }
- }
-
- if (resp.functionCalls) {
- functionCalls.push(...resp.functionCalls);
- }
- }
- } catch (error) {
- if (getErrorStatus(error) === 429) {
- throw new acp.RequestError(
- 429,
- 'Rate limit exceeded. Try again later.',
- );
- }
-
- throw error;
- }
-
- if (functionCalls.length > 0) {
- const toolResponseParts: Part[] = [];
-
- for (const fc of functionCalls) {
- const response = await this.#runTool(
- pendingSend.signal,
- promptId,
- fc,
- );
-
- const parts = Array.isArray(response) ? response : [response];
-
- for (const part of parts) {
- if (typeof part === 'string') {
- toolResponseParts.push({ text: part });
- } else if (part) {
- toolResponseParts.push(part);
- }
- }
- }
-
- nextMessage = { role: 'user', parts: toolResponseParts };
- }
- }
- }
-
- async #runTool(
- abortSignal: AbortSignal,
- promptId: string,
- fc: FunctionCall,
- ): Promise<PartListUnion> {
- const callId = fc.id ?? `${fc.name}-${Date.now()}`;
- const args = (fc.args ?? {}) as Record<string, unknown>;
-
- const startTime = Date.now();
-
- const errorResponse = (error: Error) => {
- const durationMs = Date.now() - startTime;
- logToolCall(this.config, {
- 'event.name': 'tool_call',
- 'event.timestamp': new Date().toISOString(),
- prompt_id: promptId,
- function_name: fc.name ?? '',
- function_args: args,
- duration_ms: durationMs,
- success: false,
- error: error.message,
- });
-
- return [
- {
- functionResponse: {
- id: callId,
- name: fc.name ?? '',
- response: { error: error.message },
- },
- },
- ];
- };
-
- if (!fc.name) {
- return errorResponse(new Error('Missing function name'));
- }
-
- const toolRegistry: ToolRegistry = await this.config.getToolRegistry();
- const tool = toolRegistry.getTool(fc.name as string);
-
- if (!tool) {
- return errorResponse(
- new Error(`Tool "${fc.name}" not found in registry.`),
- );
- }
-
- let toolCallId: number | undefined = undefined;
- try {
- const invocation = tool.build(args);
- const confirmationDetails =
- await invocation.shouldConfirmExecute(abortSignal);
- if (confirmationDetails) {
- let content: acp.ToolCallContent | null = null;
- if (confirmationDetails.type === 'edit') {
- content = {
- type: 'diff',
- path: confirmationDetails.fileName,
- oldText: confirmationDetails.originalContent,
- newText: confirmationDetails.newContent,
- };
- }
-
- const result = await this.client.requestToolCallConfirmation({
- label: invocation.getDescription(),
- icon: tool.icon,
- content,
- confirmation: toAcpToolCallConfirmation(confirmationDetails),
- locations: invocation.toolLocations(),
- });
-
- await confirmationDetails.onConfirm(toToolCallOutcome(result.outcome));
- switch (result.outcome) {
- case 'reject':
- return errorResponse(
- new Error(`Tool "${fc.name}" not allowed to run by the user.`),
- );
-
- case 'cancel':
- return errorResponse(
- new Error(`Tool "${fc.name}" was canceled by the user.`),
- );
- case 'allow':
- case 'alwaysAllow':
- case 'alwaysAllowMcpServer':
- case 'alwaysAllowTool':
- break;
- default: {
- const resultOutcome: never = result.outcome;
- throw new Error(`Unexpected: ${resultOutcome}`);
- }
- }
- toolCallId = result.id;
- } else {
- const result = await this.client.pushToolCall({
- icon: tool.icon,
- label: invocation.getDescription(),
- locations: invocation.toolLocations(),
- });
- toolCallId = result.id;
- }
-
- const toolResult: ToolResult = await invocation.execute(abortSignal);
- const toolCallContent = toToolCallContent(toolResult);
-
- await this.client.updateToolCall({
- toolCallId,
- status: 'finished',
- content: toolCallContent,
- });
-
- const durationMs = Date.now() - startTime;
- logToolCall(this.config, {
- 'event.name': 'tool_call',
- 'event.timestamp': new Date().toISOString(),
- function_name: fc.name,
- function_args: args,
- duration_ms: durationMs,
- success: true,
- prompt_id: promptId,
- });
-
- return convertToFunctionResponse(fc.name, callId, toolResult.llmContent);
- } catch (e) {
- const error = e instanceof Error ? e : new Error(String(e));
- if (toolCallId) {
- await this.client.updateToolCall({
- toolCallId,
- status: 'error',
- content: { type: 'markdown', markdown: error.message },
- });
- }
- return errorResponse(error);
- }
- }
-
- async #resolveUserMessage(
- message: acp.SendUserMessageParams,
- abortSignal: AbortSignal,
- ): Promise<Part[]> {
- const atPathCommandParts = message.chunks.filter((part) => 'path' in part);
-
- if (atPathCommandParts.length === 0) {
- return message.chunks.map((chunk) => {
- if ('text' in chunk) {
- return { text: chunk.text };
- } else {
- throw new Error('Unexpected chunk type');
- }
- });
- }
-
- // Get centralized file discovery service
- const fileDiscovery = this.config.getFileService();
- const respectGitIgnore = this.config.getFileFilteringRespectGitIgnore();
-
- const pathSpecsToRead: string[] = [];
- const atPathToResolvedSpecMap = new Map<string, string>();
- const contentLabelsForDisplay: string[] = [];
- const ignoredPaths: string[] = [];
-
- const toolRegistry = await this.config.getToolRegistry();
- const readManyFilesTool = toolRegistry.getTool('read_many_files');
- const globTool = toolRegistry.getTool('glob');
-
- if (!readManyFilesTool) {
- throw new Error('Error: read_many_files tool not found.');
- }
-
- for (const atPathPart of atPathCommandParts) {
- const pathName = atPathPart.path;
-
- // Check if path should be ignored by git
- if (fileDiscovery.shouldGitIgnoreFile(pathName)) {
- ignoredPaths.push(pathName);
- const reason = respectGitIgnore
- ? 'git-ignored and will be skipped'
- : 'ignored by custom patterns';
- console.warn(`Path ${pathName} is ${reason}.`);
- continue;
- }
-
- let currentPathSpec = pathName;
- let resolvedSuccessfully = false;
-
- try {
- const absolutePath = path.resolve(this.config.getTargetDir(), pathName);
- if (isWithinRoot(absolutePath, this.config.getTargetDir())) {
- const stats = await fs.stat(absolutePath);
- if (stats.isDirectory()) {
- currentPathSpec = pathName.endsWith('/')
- ? `${pathName}**`
- : `${pathName}/**`;
- this.#debug(
- `Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
- );
- } else {
- this.#debug(
- `Path ${pathName} resolved to file: ${currentPathSpec}`,
- );
- }
- resolvedSuccessfully = true;
- } else {
- this.#debug(
- `Path ${pathName} is outside the project directory. Skipping.`,
- );
- }
- } catch (error) {
- if (isNodeError(error) && error.code === 'ENOENT') {
- if (this.config.getEnableRecursiveFileSearch() && globTool) {
- this.#debug(
- `Path ${pathName} not found directly, attempting glob search.`,
- );
- try {
- const globResult = await globTool.buildAndExecute(
- {
- pattern: `**/*${pathName}*`,
- path: this.config.getTargetDir(),
- },
- abortSignal,
- );
- if (
- globResult.llmContent &&
- typeof globResult.llmContent === 'string' &&
- !globResult.llmContent.startsWith('No files found') &&
- !globResult.llmContent.startsWith('Error:')
- ) {
- const lines = globResult.llmContent.split('\n');
- if (lines.length > 1 && lines[1]) {
- const firstMatchAbsolute = lines[1].trim();
- currentPathSpec = path.relative(
- this.config.getTargetDir(),
- firstMatchAbsolute,
- );
- this.#debug(
- `Glob search for ${pathName} found ${firstMatchAbsolute}, using relative path: ${currentPathSpec}`,
- );
- resolvedSuccessfully = true;
- } else {
- this.#debug(
- `Glob search for '**/*${pathName}*' did not return a usable path. Path ${pathName} will be skipped.`,
- );
- }
- } else {
- this.#debug(
- `Glob search for '**/*${pathName}*' found no files or an error. Path ${pathName} will be skipped.`,
- );
- }
- } catch (globError) {
- console.error(
- `Error during glob search for ${pathName}: ${getErrorMessage(globError)}`,
- );
- }
- } else {
- this.#debug(
- `Glob tool not found. Path ${pathName} will be skipped.`,
- );
- }
- } else {
- console.error(
- `Error stating path ${pathName}. Path ${pathName} will be skipped.`,
- );
- }
- }
-
- if (resolvedSuccessfully) {
- pathSpecsToRead.push(currentPathSpec);
- atPathToResolvedSpecMap.set(pathName, currentPathSpec);
- contentLabelsForDisplay.push(pathName);
- }
- }
-
- // Construct the initial part of the query for the LLM
- let initialQueryText = '';
- for (let i = 0; i < message.chunks.length; i++) {
- const chunk = message.chunks[i];
- if ('text' in chunk) {
- initialQueryText += chunk.text;
- } else {
- // type === 'atPath'
- const resolvedSpec = atPathToResolvedSpecMap.get(chunk.path);
- if (
- i > 0 &&
- initialQueryText.length > 0 &&
- !initialQueryText.endsWith(' ') &&
- resolvedSpec
- ) {
- // Add space if previous part was text and didn't end with space, or if previous was @path
- const prevPart = message.chunks[i - 1];
- if (
- 'text' in prevPart ||
- ('path' in prevPart && atPathToResolvedSpecMap.has(prevPart.path))
- ) {
- initialQueryText += ' ';
- }
- }
- if (resolvedSpec) {
- initialQueryText += `@${resolvedSpec}`;
- } else {
- // If not resolved for reading (e.g. lone @ or invalid path that was skipped),
- // add the original @-string back, ensuring spacing if it's not the first element.
- if (
- i > 0 &&
- initialQueryText.length > 0 &&
- !initialQueryText.endsWith(' ') &&
- !chunk.path.startsWith(' ')
- ) {
- initialQueryText += ' ';
- }
- initialQueryText += `@${chunk.path}`;
- }
- }
- }
- initialQueryText = initialQueryText.trim();
-
- // Inform user about ignored paths
- if (ignoredPaths.length > 0) {
- const ignoreType = respectGitIgnore ? 'git-ignored' : 'custom-ignored';
- this.#debug(
- `Ignored ${ignoredPaths.length} ${ignoreType} files: ${ignoredPaths.join(', ')}`,
- );
- }
-
- // Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
- if (pathSpecsToRead.length === 0) {
- console.warn('No valid file paths found in @ commands to read.');
- return [{ text: initialQueryText }];
- }
-
- const processedQueryParts: Part[] = [{ text: initialQueryText }];
-
- const toolArgs = {
- paths: pathSpecsToRead,
- respectGitIgnore, // Use configuration setting
- };
-
- let toolCallId: number | undefined = undefined;
- try {
- const invocation = readManyFilesTool.build(toolArgs);
- const toolCall = await this.client.pushToolCall({
- icon: readManyFilesTool.icon,
- label: invocation.getDescription(),
- });
- toolCallId = toolCall.id;
- const result = await invocation.execute(abortSignal);
- const content = toToolCallContent(result) || {
- type: 'markdown',
- markdown: `Successfully read: ${contentLabelsForDisplay.join(', ')}`,
- };
- await this.client.updateToolCall({
- toolCallId: toolCall.id,
- status: 'finished',
- content,
- });
-
- if (Array.isArray(result.llmContent)) {
- const fileContentRegex = /^--- (.*?) ---\n\n([\s\S]*?)\n\n$/;
- processedQueryParts.push({
- text: '\n--- Content from referenced files ---',
- });
- for (const part of result.llmContent) {
- if (typeof part === 'string') {
- const match = fileContentRegex.exec(part);
- if (match) {
- const filePathSpecInContent = match[1]; // This is a resolved pathSpec
- const fileActualContent = match[2].trim();
- processedQueryParts.push({
- text: `\nContent from @${filePathSpecInContent}:\n`,
- });
- processedQueryParts.push({ text: fileActualContent });
- } else {
- processedQueryParts.push({ text: part });
- }
- } else {
- // part is a Part object.
- processedQueryParts.push(part);
- }
- }
- processedQueryParts.push({ text: '\n--- End of content ---' });
- } else {
- console.warn(
- 'read_many_files tool returned no content or empty content.',
- );
- }
-
- return processedQueryParts;
- } catch (error: unknown) {
- if (toolCallId) {
- await this.client.updateToolCall({
- toolCallId,
- status: 'error',
- content: {
- type: 'markdown',
- markdown: `Error reading files (${contentLabelsForDisplay.join(', ')}): ${getErrorMessage(error)}`,
- },
- });
- }
- throw error;
- }
- }
-
- #debug(msg: string) {
- if (this.config.getDebugMode()) {
- console.warn(msg);
- }
- }
-}
-
-function toToolCallContent(toolResult: ToolResult): acp.ToolCallContent | null {
- if (toolResult.returnDisplay) {
- if (typeof toolResult.returnDisplay === 'string') {
- return {
- type: 'markdown',
- markdown: toolResult.returnDisplay,
- };
- } else {
- return {
- type: 'diff',
- path: toolResult.returnDisplay.fileName,
- oldText: toolResult.returnDisplay.originalContent,
- newText: toolResult.returnDisplay.newContent,
- };
- }
- } else {
- return null;
- }
-}
-
-function toAcpToolCallConfirmation(
- confirmationDetails: ToolCallConfirmationDetails,
-): acp.ToolCallConfirmation {
- switch (confirmationDetails.type) {
- case 'edit':
- return { type: 'edit' };
- case 'exec':
- return {
- type: 'execute',
- rootCommand: confirmationDetails.rootCommand,
- command: confirmationDetails.command,
- };
- case 'mcp':
- return {
- type: 'mcp',
- serverName: confirmationDetails.serverName,
- toolName: confirmationDetails.toolName,
- toolDisplayName: confirmationDetails.toolDisplayName,
- };
- case 'info':
- return {
- type: 'fetch',
- urls: confirmationDetails.urls || [],
- description: confirmationDetails.urls?.length
- ? null
- : confirmationDetails.prompt,
- };
- default: {
- const unreachable: never = confirmationDetails;
- throw new Error(`Unexpected: ${unreachable}`);
- }
- }
-}
-
-function toToolCallOutcome(
- outcome: acp.ToolCallConfirmationOutcome,
-): ToolConfirmationOutcome {
- switch (outcome) {
- case 'allow':
- return ToolConfirmationOutcome.ProceedOnce;
- case 'alwaysAllow':
- return ToolConfirmationOutcome.ProceedAlways;
- case 'alwaysAllowMcpServer':
- return ToolConfirmationOutcome.ProceedAlwaysServer;
- case 'alwaysAllowTool':
- return ToolConfirmationOutcome.ProceedAlwaysTool;
- case 'reject':
- case 'cancel':
- return ToolConfirmationOutcome.Cancel;
- default: {
- const unreachable: never = outcome;
- throw new Error(`Unexpected: ${unreachable}`);
- }
- }
-}