diff options
Diffstat (limited to 'packages/cli/src/acp')
| -rw-r--r-- | packages/cli/src/acp/acp.ts | 464 | ||||
| -rw-r--r-- | packages/cli/src/acp/acpPeer.ts | 677 |
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}`); - } - } -} |
