Compare commits

..

2 Commits

Author SHA1 Message Date
Richard Feldman
f91603dc82 Go back to SDK version 1.0.100 for #37976 for now 2025-09-11 10:50:47 -04:00
Richard Feldman
7d511f6bbe Pin npm dependencies 2025-09-11 10:49:47 -04:00
14 changed files with 378 additions and 855 deletions

6
.eslintignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
dist/
coverage/
*.min.js
*.config.js
.github/

33
.eslintrc.json Normal file
View File

@@ -0,0 +1,33 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"env": {
"node": true,
"es2020": true
},
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
"no-console": "off",
"no-constant-condition": "off",
"default-case": "error",
"prefer-const": "error",
"no-var": "error",
"eqeqeq": ["error", "always"],
"curly": ["error", "all"]
},
"ignorePatterns": ["dist", "node_modules", "*.js"]
}

View File

@@ -6,7 +6,6 @@ on:
jobs:
build:
runs-on: ubuntu-latest
environment: release # Optional: for enhanced security
permissions:
contents: read
id-token: write
@@ -17,8 +16,8 @@ jobs:
with:
node-version: "lts/*"
registry-url: "https://registry.npmjs.org"
- name: Update npm
run: npm install -g npm@latest
- run: npm ci
- run: npm run build
- run: npm publish
# Add back provenance once repo is public
- run: npm publish # --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,20 +0,0 @@
# Changelog
## v0.4.3
- Move ACP tools over MCP from an "http" MCP server to an "sdk" one so more tool calls can stay in-memory.
- Update to @anthropic-ai/claude-code@v1.0.119
- Update to @zed-industries/agent-client-protocol@v0.4.0
## v0.4.2
- Fix missing package.json metadata
## v0.4.1
- Add support for /compact command [ecfd36a](https://github.com/zed-industries/claude-code-acp/commit/ecfd36afa6c4e31f12e1daf9b8a2bdc12dda1794)
- Add default limits to read tool [7bd1638](https://github.com/zed-industries/claude-code-acp/commit/7bd163818bb959b11fd2c933eff73ad83c57abb8)
- Better rendering of Tool errors [491efe3](https://github.com/zed-industries/claude-code-acp/commit/491efe32e8547075842e448d873fc01b2ffabf3a)
- Load managed-settings.json [f691024](https://github.com/zed-industries/claude-code-acp/commit/f691024350362858e00b97248ac68e356d2331c2)
- Update to @anthropic-ai/claude-code@v1.0.113
- Update to @zed-industries/agent-client-protocol@v0.3.1

View File

@@ -33,7 +33,6 @@ Read the docs on [External Agent](https://zed.dev/docs/ai/external-agents) suppo
### Other clients
- Neovim via [CodeCompanion.nvim](https://codecompanion.olimorris.dev/configuration/adapters#setup-claude-code-via-acp)
- [marimo notebook](https://github.com/marimo-team/marimo)
[Submit a PR](https://github.com/zed-industries/claude-code-acp/pulls) to add yours!

View File

@@ -1,48 +0,0 @@
import js from "@eslint/js";
import tseslint from "@typescript-eslint/eslint-plugin";
import tsparser from "@typescript-eslint/parser";
import prettierConfig from "eslint-config-prettier";
import globals from "globals";
export default [
{
ignores: ["node_modules/", "dist/", "coverage/", "*.min.js", "*.config.js", ".github/"],
},
js.configs.recommended,
{
files: ["src/**/*.ts"],
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
parser: tsparser,
parserOptions: {
project: "./tsconfig.json",
},
globals: {
...globals.node,
},
},
plugins: {
"@typescript-eslint": tseslint,
},
rules: {
...tseslint.configs.recommended.rules,
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
},
],
"no-console": "off",
"no-constant-condition": "off",
"default-case": "error",
"prefer-const": "error",
"no-var": "error",
eqeqeq: ["error", "always"],
curly: ["error", "all"],
},
},
prettierConfig,
];

584
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"bin": {
"claude-code-acp": "./dist/index.js"
},
"version": "0.4.3",
"version": "0.4.1",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/index.js",
"type": "module",
@@ -40,20 +40,12 @@
"sdk",
"code"
],
"homepage": "https://github.com/zed-industries/claude-code-acp#readme",
"bugs": {
"url": "https://github.com/zed-industries/claude-code-acp/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/zed-industries/claude-code-acp.git"
},
"author": "Zed Industries",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/claude-code": "1.0.119",
"@modelcontextprotocol/sdk": "1.18.1",
"@zed-industries/agent-client-protocol": "0.4.0",
"@anthropic-ai/claude-code": "1.0.100",
"@modelcontextprotocol/sdk": "1.17.5",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.8",
"diff": "8.0.2",
"express": "5.1.0",
"minimist": "1.2.8",
@@ -62,14 +54,13 @@
"devDependencies": {
"@types/express": "5.0.3",
"@types/minimist": "1.2.5",
"@types/node": "24.5.2",
"@types/node": "24.3.1",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.44.0",
"@typescript-eslint/parser": "8.44.0",
"@typescript-eslint/eslint-plugin": "8.43.0",
"@typescript-eslint/parser": "8.43.0",
"@vitest/ui": "3.2.4",
"eslint": "9.35.0",
"eslint-config-prettier": "10.1.8",
"globals": "16.4.0",
"prettier": "3.6.2",
"ts-node": "10.9.2",
"typescript": "5.9.2",

View File

@@ -7,7 +7,6 @@ import {
ClientCapabilities,
InitializeRequest,
InitializeResponse,
ndJsonStream,
NewSessionRequest,
NewSessionResponse,
PromptRequest,
@@ -37,12 +36,7 @@ import * as os from "node:os";
import { v7 as uuidv7 } from "uuid";
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
import { SessionNotification } from "@zed-industries/agent-client-protocol";
import {
createMcpServer,
createPermissionMcpServer,
PERMISSION_TOOL_NAME,
toolNames,
} from "./mcp-server.js";
import { createMcpServer, toolNames } from "./mcp-server.js";
import { AddressInfo } from "node:net";
import { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult } from "./tools.js";
@@ -145,17 +139,9 @@ export class ClaudeAcpAgent implements Agent {
}
}
const server = createMcpServer(this, sessionId, this.clientCapabilities);
const server = await createMcpServer(this, sessionId, this.clientCapabilities);
const address = server.address() as AddressInfo;
mcpServers["acp"] = {
type: "sdk",
name: "acp",
instance: server,
};
// Ideally replace with `canUseTool`
const permissionServer = await createPermissionMcpServer(this, sessionId);
const address = permissionServer.address() as AddressInfo;
mcpServers["acpPermission"] = {
type: "http",
url: "http://127.0.0.1:" + address.port + "/mcp",
headers: {
@@ -166,7 +152,7 @@ export class ClaudeAcpAgent implements Agent {
const options: Options = {
cwd: params.cwd,
mcpServers,
permissionPromptToolName: PERMISSION_TOOL_NAME,
permissionPromptToolName: toolNames.permission,
stderr: (err) => console.error(err),
// note: although not documented by the types, passing an absolute path
// here works to find zed's managed node version.
@@ -298,24 +284,6 @@ export class ClaudeAcpAgent implements Agent {
continue;
}
// Slash commands like /compact can generate invalid output... doesn't match
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
if (
typeof message.message.content === "string" &&
message.message.content.includes("<local-command-stdout>")
) {
console.log(message.message.content);
break;
}
if (
typeof message.message.content === "string" &&
message.message.content.includes("<local-command-stderr>")
) {
console.error(message.message.content);
break;
}
if (
message.message.model === "<synthetic>" &&
message.message.content.length === 1 &&
@@ -334,7 +302,7 @@ export class ClaudeAcpAgent implements Agent {
break;
}
default:
unreachable(message as never);
unreachable(message);
}
}
throw new Error("Session did not end in result");
@@ -393,6 +361,7 @@ async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand
"bashes", // Modal
"bug", // Modal
"clear", // Escape Codes
"compact", // Not supported via SDK?
"config", // Modal
"context", // Escape Codes
"cost", // Escape Codes
@@ -421,14 +390,14 @@ async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand
"todos", // Escape Codes
"vim", // Not needed
];
const commands = await query.supportedCommands();
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
const commands = await (query as any).supportedCommands();
return commands
.map((command) => {
.map((command: { name: string; description: string; argumentHint: string }) => {
const input = command.argumentHint ? { hint: command.argumentHint } : null;
return {
name: command.name,
// @ts-expect-error type in the ts interface
description: command.description || "",
input,
};
@@ -622,11 +591,11 @@ export function toAcpNotifications(
}
export function runAcp() {
const input = nodeToWebWritable(process.stdout);
const output = nodeToWebReadable(process.stdin);
const stream = ndJsonStream(input, output);
new AgentSideConnection((client) => new ClaudeAcpAgent(client), stream);
new AgentSideConnection(
(client) => new ClaudeAcpAgent(client),
nodeToWebWritable(process.stdout),
nodeToWebReadable(process.stdin),
);
}
type ContentChunk =

View File

@@ -8,7 +8,7 @@ import { ClaudeAcpAgent } from "./acp-agent.js";
import { ClientCapabilities, TerminalOutputResponse } from "@zed-industries/agent-client-protocol";
import * as diff from "diff";
import { sleep, unreachable, extractLinesWithByteLimit } from "./utils.js";
import { sleep, unreachable } from "./utils.js";
import { PermissionResult } from "@anthropic-ai/claude-code";
export const SYSTEM_REMINDER = `
@@ -17,8 +17,6 @@ export const SYSTEM_REMINDER = `
Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.
</system-reminder>`;
const defaults = { maxFileSize: 50000, linesToRead: 1000 };
const unqualifiedToolNames = {
read: "read",
edit: "edit",
@@ -27,6 +25,7 @@ const unqualifiedToolNames = {
bash: "Bash",
killBash: "KillBash",
bashOutput: "BashOutput",
permission: "permission",
};
const SERVER_PREFIX = "mcp__acp__";
@@ -38,6 +37,7 @@ export const toolNames = {
bash: SERVER_PREFIX + unqualifiedToolNames.bash,
killBash: SERVER_PREFIX + unqualifiedToolNames.killBash,
bashOutput: SERVER_PREFIX + unqualifiedToolNames.bashOutput,
permission: SERVER_PREFIX + unqualifiedToolNames.permission,
};
const editToolNames = [toolNames.edit, toolNames.multiEdit, toolNames.write];
@@ -46,9 +46,12 @@ export function createMcpServer(
agent: ClaudeAcpAgent,
sessionId: string,
clientCapabilities: ClientCapabilities | undefined,
): McpServer {
): Promise<Server> {
// Create MCP server
const server = new McpServer({ name: "acp", version: "1.0.0" }, { capabilities: { tools: {} } });
const server = new McpServer({
name: "acp-mcp-server",
version: "1.0.0",
});
if (clientCapabilities?.fs?.readTextFile) {
server.registerTool(
@@ -65,17 +68,8 @@ In sessions with ${toolNames.read} always use it instead of Read as it contains
offset: z
.number()
.optional()
.default(0)
.describe(
"Which line to start reading from (0-based). Omit to start from the beginning.",
),
linesToRead: z
.number()
.optional()
.default(defaults.linesToRead)
.describe(
`How many lines to read. Omit for ${defaults.linesToRead}. (Reading files that are too large can fill up the context window unnecessarily, so be thoughtful about how much you read at a time.)`,
),
.describe("Which line to start reading from. Omit to start from the beginning."),
limit: z.number().optional().describe("How many lines to read. Omit for the whole file."),
},
annotations: {
title: "Read file",
@@ -98,43 +92,18 @@ In sessions with ${toolNames.read} always use it instead of Read as it contains
],
};
}
const content = await agent.readTextFile({
sessionId,
path: input.abs_path,
limit: input.limit,
line: input.offset,
});
// Extract lines with byte limit enforcement
const result = extractLinesWithByteLimit(
content.content,
input.offset ?? 0,
input.linesToRead,
defaults.maxFileSize,
);
// Construct informative message about what was read
let readInfo = "";
if (input.offset > 0 || result.wasLimited) {
readInfo = "\n\n<file-read-info>";
if (result.wasLimited) {
readInfo += `Read ${result.linesRead} lines (hit 50KB limit). `;
} else {
readInfo += `Read lines ${(input.offset ?? 0) + 1}-${result.actualEndLine}. `;
}
if (result.wasLimited) {
readInfo += `Continue with offset=${result.actualEndLine}.`;
}
readInfo += "</file-read-info>";
}
return {
content: [
{
type: "text",
text: result.content + readInfo + SYSTEM_REMINDER,
text: content.content + SYSTEM_REMINDER,
},
],
};
@@ -620,30 +589,9 @@ File editing instructions:
);
}
return server;
}
const UNQUALIFIED_PERMISSION_TOOL_NAME = "permission";
const PERMISSION_SERVER_PREFIX = "mcp__acpPermission__";
export const PERMISSION_TOOL_NAME = PERMISSION_SERVER_PREFIX + UNQUALIFIED_PERMISSION_TOOL_NAME;
/**
* Separate permission tool. Needs to be an HTTP server for now, because Claude Code doesn't
* support permission tools via SDK servers.
* Ideally it moves to the `canUseTool` callback in the future.
*/
export function createPermissionMcpServer(
agent: ClaudeAcpAgent,
sessionId: string,
): Promise<Server> {
const server = new McpServer(
{ name: "acpPermission", version: "1.0.0" },
{ capabilities: { tools: {} } },
);
const alwaysAllowedTools: { [key: string]: boolean } = {};
server.registerTool(
UNQUALIFIED_PERMISSION_TOOL_NAME,
unqualifiedToolNames.permission,
{
title: "Permission Tool",
description: "Used to request tool permissions",

View File

@@ -5,7 +5,6 @@ import {
AvailableCommand,
Client,
ClientSideConnection,
ndJsonStream,
NewSessionResponse,
ReadTextFileRequest,
ReadTextFileResponse,
@@ -92,7 +91,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
async writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
this.files.set(params.path, params.content);
return {};
return null;
}
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
@@ -109,13 +108,14 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
newSessionResponse: NewSessionResponse;
}> {
let client;
const input = nodeToWebWritable(child.stdin!);
const output = nodeToWebReadable(child.stdout!);
const stream = ndJsonStream(input, output);
const connection = new ClientSideConnection((agent) => {
client = new TestClient(agent);
return client;
}, stream);
const connection = new ClientSideConnection(
(agent) => {
client = new TestClient(agent);
return client;
},
nodeToWebWritable(child.stdin!),
nodeToWebReadable(child.stdout!),
);
await connection.initialize({
protocolVersion: 1,
@@ -191,50 +191,6 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration"
expect(client.takeReceivedText()).toContain("Hello GPT-5");
}, 30000);
it("/compact works", async () => {
const { client, connection, newSessionResponse } = await setupTestSession(__dirname);
const commands = await client.availableCommandsPromise;
expect(commands).toContainEqual({
description:
"Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]",
input: {
hint: "<optional custom summarization instructions>",
},
name: "compact",
});
// Error case (no previous message)
await connection.prompt({
prompt: [{ type: "text", text: "/compact" }],
sessionId: newSessionResponse.sessionId,
});
expect(client.takeReceivedText()).toBe("");
// Send something
await connection.prompt({
prompt: [{ type: "text", text: "Hi" }],
sessionId: newSessionResponse.sessionId,
});
// Clear response
client.takeReceivedText();
// Test with instruction
await connection.prompt({
prompt: [
{
type: "text",
text: "/compact greeting",
},
],
sessionId: newSessionResponse.sessionId,
});
expect(client.takeReceivedText()).toContain("");
}, 30000);
});
describe("tool conversions", () => {
@@ -822,10 +778,7 @@ describe("tool conversions", () => {
// Should return empty object when parsing fails
expect(update).toEqual({
content: [
{
content: { type: "text", text: "```\nFailed to find `old_string`\n```" },
type: "content",
},
{ content: { type: "text", text: "Failed to find `old_string`" }, type: "content" },
],
});
});

View File

@@ -1,174 +0,0 @@
import { describe, it, expect } from "vitest";
import { extractLinesWithByteLimit } from "../utils.js";
describe("extractLinesWithByteLimit", () => {
const simpleContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
it("should extract all lines when under byte limit", () => {
const result = extractLinesWithByteLimit(simpleContent, 0, 5, 1000);
expect(result.content).toBe(simpleContent);
expect(result.actualEndLine).toBe(4);
expect(result.wasLimited).toBe(false);
expect(result.linesRead).toBe(5);
});
it("should extract partial lines with offset", () => {
const result = extractLinesWithByteLimit(simpleContent, 2, 2, 1000);
expect(result.content).toBe("Line 3\nLine 4");
expect(result.actualEndLine).toBe(3);
expect(result.wasLimited).toBe(false);
expect(result.linesRead).toBe(2);
});
it("should limit output when exceeding byte limit", () => {
// Create content that will exceed byte limit
const longLine = "x".repeat(100);
const manyLines = Array(10).fill(longLine).join("\n");
const result = extractLinesWithByteLimit(manyLines, 0, 10, 250);
expect(result.wasLimited).toBe(true);
expect(result.actualEndLine).toBe(1); // Should only get 2 lines
expect(result.linesRead).toBe(2);
});
it("should handle offset beyond file length", () => {
const result = extractLinesWithByteLimit(simpleContent, 10, 5, 1000);
expect(result.content).toBe("");
expect(result.actualEndLine).toBe(10);
expect(result.wasLimited).toBe(false);
expect(result.linesRead).toBe(0);
});
it("should handle empty content", () => {
const result = extractLinesWithByteLimit("", 0, 10, 1000);
expect(result.content).toBe("");
expect(result.actualEndLine).toBe(0); // Empty string splits to [""] and we process that one empty line
expect(result.wasLimited).toBe(false);
expect(result.linesRead).toBe(1); // We read the one empty line
});
it("should handle single line file", () => {
const singleLine = "This is a single line without newline";
const result = extractLinesWithByteLimit(singleLine, 0, 10, 1000);
expect(result.content).toBe(singleLine);
expect(result.actualEndLine).toBe(0);
expect(result.wasLimited).toBe(false);
expect(result.linesRead).toBe(1);
});
it("should correctly count bytes with multi-byte characters", () => {
const unicodeContent = "Hello 世界\n你好 World\nEmoji: 🌍\nNormal line";
const result = extractLinesWithByteLimit(unicodeContent, 0, 10, 1000);
expect(result.content).toBe(unicodeContent);
expect(result.linesRead).toBe(4);
});
it("should stop at byte limit even with one more line available", () => {
// Create lines where adding one more would exceed limit
const line1 = "a".repeat(40);
const line2 = "b".repeat(40);
const line3 = "c".repeat(40);
const content = `${line1}\n${line2}\n${line3}`;
const result = extractLinesWithByteLimit(content, 0, 3, 85);
expect(result.content).toBe(`${line1}\n${line2}`);
expect(result.actualEndLine).toBe(1);
expect(result.wasLimited).toBe(true);
expect(result.linesRead).toBe(2);
});
it("should read exactly to limit when possible", () => {
const exactContent = "12345\n67890\n12345"; // 17 bytes total
const result = extractLinesWithByteLimit(exactContent, 0, 3, 17);
expect(result.content).toBe(exactContent);
expect(result.actualEndLine).toBe(2);
expect(result.wasLimited).toBe(false);
expect(result.linesRead).toBe(3);
});
it("should handle reading from middle to end", () => {
const result = extractLinesWithByteLimit(simpleContent, 3, 100, 1000);
expect(result.content).toBe("Line 4\nLine 5");
expect(result.actualEndLine).toBe(4);
expect(result.wasLimited).toBe(false);
expect(result.linesRead).toBe(2);
});
it("should handle large content with typical parameters", () => {
const longContent = Array(2000).fill("Line content here").join("\n");
const result = extractLinesWithByteLimit(longContent, 0, 1000, 50000);
expect(result.actualEndLine).toBeLessThanOrEqual(1000); // Should be limited by line count
});
it("should provide correct data for partial read at start", () => {
const result = extractLinesWithByteLimit(simpleContent, 0, 2, 1000);
expect(result.actualEndLine).toBe(1);
expect(result.linesRead).toBe(2);
});
it("should provide correct data for partial read with offset", () => {
const result = extractLinesWithByteLimit(simpleContent, 1, 2, 1000);
expect(result.actualEndLine).toBe(2);
expect(result.linesRead).toBe(2);
});
it("should not add newline after last line", () => {
const result = extractLinesWithByteLimit("Line 1\nLine 2\nLine 3", 2, 1, 1000);
expect(result.content).toBe("Line 3"); // No trailing newline
});
it("should handle Windows-style line endings", () => {
const windowsContent = "Line 1\r\nLine 2\r\nLine 3";
const result = extractLinesWithByteLimit(windowsContent, 0, 3, 1000);
// Note: split("\n") will keep the \r characters
expect(result.content).toBe("Line 1\r\nLine 2\r\nLine 3");
expect(result.linesRead).toBe(3);
});
it("should handle very large files efficiently", () => {
// Create a 100KB file
const largeLine = "x".repeat(1000);
const largeContent = Array(110).fill(largeLine).join("\n");
const result = extractLinesWithByteLimit(largeContent, 0, 200, 50000);
expect(result.wasLimited).toBe(true);
expect(result.actualEndLine).toBeLessThan(50); // Should stop well before 200 lines
});
it("should handle line limit of 0", () => {
const result = extractLinesWithByteLimit(simpleContent, 0, 0, 1000);
expect(result.content).toBe("");
expect(result.actualEndLine).toBe(0);
expect(result.linesRead).toBe(0);
expect(result.wasLimited).toBe(false);
});
it("should allow at least one line even if it exceeds byte limit", () => {
const veryLongLine = "x".repeat(100000); // 100KB line
const result = extractLinesWithByteLimit(veryLongLine, 0, 1, 50000);
// Should return the line even though it exceeds the byte limit
// because we always allow at least one line if no lines have been added yet
expect(result.content).toBe(veryLongLine);
expect(result.actualEndLine).toBe(0);
expect(result.linesRead).toBe(1);
expect(result.wasLimited).toBe(false);
});
});

View File

@@ -487,7 +487,7 @@ export function toolUpdateFromToolResult(
case "Write": {
if (toolResult.is_error && toolResult.content?.length > 0) {
// Only return errors
return toAcpContentUpdate(toolResult.content, true);
return toAcpContentUpdate(toolResult.content);
}
return {};
}
@@ -511,26 +511,17 @@ export function toolUpdateFromToolResult(
case "WebSearch":
case "Other":
default: {
return toAcpContentUpdate(toolResult.content, toolResult.is_error);
return toAcpContentUpdate(toolResult.content);
}
}
}
function toAcpContentUpdate(
content: any,
isError: boolean = false,
): { content?: ToolCallContent[] } {
function toAcpContentUpdate(content: any): { content?: ToolCallContent[] } {
if (Array.isArray(content) && content.length > 0) {
return {
content: content.map((content: any) => ({
type: "content",
content:
isError && content.type === "text"
? {
...content,
text: `\`\`\`\n${content.text}\n\`\`\``,
}
: content,
content,
})),
};
} else if (typeof content === "string" && content.length > 0) {
@@ -540,7 +531,7 @@ function toAcpContentUpdate(
type: "content",
content: {
type: "text",
text: isError ? `\`\`\`\n${content}\n\`\`\`` : content,
text: content,
},
},
],

View File

@@ -122,113 +122,3 @@ export function applyEnvironmentSettings(settings: ManagedSettings): void {
}
}
}
export interface ExtractLinesResult {
content: string;
actualEndLine: number;
wasLimited: boolean;
linesRead: number;
}
/**
* Extracts lines from file content with byte limit enforcement.
*
* @param fullContent - The complete file content
* @param linesToSkip - Starting line number (0-based)
* @param linesToRead - Maximum number of lines to read
* @param maxContentLength - Maximum number of UTF-16 Code Units to return
* @returns Object containing extracted content and metadata
*/
export function extractLinesWithByteLimit(
fullContent: string,
linesToSkip: number,
linesToRead: number,
maxContentLength: number,
): ExtractLinesResult {
if (fullContent === "" || linesToRead === 0) {
if (linesToSkip === 0 && linesToRead > 0) {
return {
content: "",
actualEndLine: 0,
wasLimited: false,
linesRead: 1,
};
} else {
return {
content: "",
actualEndLine: linesToSkip,
wasLimited: false,
linesRead: 0,
};
}
}
let linesSeen = 0;
let index = 0;
while (linesSeen < linesToSkip) {
const nextIndex = fullContent.indexOf("\n", index);
// There were not enough lines to skip.
if (nextIndex < 0) {
return {
content: "",
actualEndLine: linesToSkip,
wasLimited: false,
linesRead: 0,
};
}
linesSeen += 1;
index = nextIndex + 1;
}
// We've successfully skipped over all the lines we were supposed to.
// Now we can actually start reading!
const startIndex = index;
linesSeen = 0;
let contentLength = 0;
let wasLimited = false;
while (linesSeen < linesToRead) {
const nextIndex = fullContent.indexOf("\n", index);
if (nextIndex < 0) {
// Last line in file (no trailing newline)
const newContentLength = fullContent.length - startIndex;
if (linesSeen > 0 && newContentLength > maxContentLength) {
wasLimited = true;
break;
}
linesSeen += 1;
contentLength = newContentLength;
break;
} else {
// Line with newline - include up to the newline
const newContentLength = nextIndex + 1 - startIndex;
if (linesSeen > 0 && newContentLength > maxContentLength) {
wasLimited = true;
break;
}
linesSeen += 1;
contentLength = newContentLength;
index = nextIndex + 1;
}
}
// If we ended with a newline and we stopped due to byte limit or line limit (not end of file), remove the trailing newline
if (contentLength > 0 && fullContent[startIndex + contentLength - 1] === "\n") {
// Check if there's more content after our current position that we didn't read
if (startIndex + contentLength < fullContent.length) {
contentLength -= 1;
}
}
return {
content: fullContent.slice(startIndex, startIndex + contentLength),
actualEndLine: linesSeen > 0 ? linesToSkip + linesSeen - 1 : linesToSkip,
wasLimited,
linesRead: linesSeen,
};
}