Skip to content

Commit 600a2db

Browse files
authored
More efficient file reads (#73)
Allows the client to handle line offsets, rather than loading the entire file and then slicing it on the agent side. Signed-off-by: Ben Brandt <benjamin.j.brandt@gmail.com> Signed-off-by: Ben Brandt <benjamin.j.brandt@gmail.com>
1 parent f2a4b3d commit 600a2db

File tree

6 files changed

+35
-151
lines changed

6 files changed

+35
-151
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## v0.4.7
4+
5+
- More efficient file reads from the client.
6+
37
## v0.4.6
48

59
- Update to @anthropic-ai/claude-code@v1.0.128

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"bin": {
77
"claude-code-acp": "./dist/index.js"
88
},
9-
"version": "0.4.6",
9+
"version": "0.4.7",
1010
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
1111
"main": "dist/index.js",
1212
"type": "module",

src/mcp-server.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,12 @@ In sessions with ${toolNames.read} always use it instead of Read as it contains
102102
const content = await agent.readTextFile({
103103
sessionId,
104104
path: input.abs_path,
105+
line: input.offset + 1,
106+
limit: input.linesToRead,
105107
});
106108

107109
// Extract lines with byte limit enforcement
108-
const result = extractLinesWithByteLimit(
109-
content.content,
110-
input.offset ?? 0,
111-
input.linesToRead,
112-
defaults.maxFileSize,
113-
);
110+
const result = extractLinesWithByteLimit(content.content, defaults.maxFileSize);
114111

115112
// Construct informative message about what was read
116113
let readInfo = "";
@@ -120,11 +117,11 @@ In sessions with ${toolNames.read} always use it instead of Read as it contains
120117
if (result.wasLimited) {
121118
readInfo += `Read ${result.linesRead} lines (hit 50KB limit). `;
122119
} else {
123-
readInfo += `Read lines ${(input.offset ?? 0) + 1}-${result.actualEndLine}. `;
120+
readInfo += `Read lines ${(input.offset ?? 0) + 1}-${result.linesRead}. `;
124121
}
125122

126123
if (result.wasLimited) {
127-
readInfo += `Continue with offset=${result.actualEndLine}.`;
124+
readInfo += `Continue with offset=${result.linesRead}.`;
128125
}
129126

130127
readInfo += "</file-read-info>";

src/tests/extract-lines.test.ts

Lines changed: 11 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,66 +5,44 @@ describe("extractLinesWithByteLimit", () => {
55
const simpleContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
66

77
it("should extract all lines when under byte limit", () => {
8-
const result = extractLinesWithByteLimit(simpleContent, 0, 5, 1000);
8+
const result = extractLinesWithByteLimit(simpleContent, 1000);
99

1010
expect(result.content).toBe(simpleContent);
11-
expect(result.actualEndLine).toBe(4);
1211
expect(result.wasLimited).toBe(false);
1312
expect(result.linesRead).toBe(5);
1413
});
1514

16-
it("should extract partial lines with offset", () => {
17-
const result = extractLinesWithByteLimit(simpleContent, 2, 2, 1000);
18-
19-
expect(result.content).toBe("Line 3\nLine 4");
20-
expect(result.actualEndLine).toBe(3);
21-
expect(result.wasLimited).toBe(false);
22-
expect(result.linesRead).toBe(2);
23-
});
24-
2515
it("should limit output when exceeding byte limit", () => {
2616
// Create content that will exceed byte limit
2717
const longLine = "x".repeat(100);
2818
const manyLines = Array(10).fill(longLine).join("\n");
2919

30-
const result = extractLinesWithByteLimit(manyLines, 0, 10, 250);
20+
const result = extractLinesWithByteLimit(manyLines, 250);
3121

3222
expect(result.wasLimited).toBe(true);
33-
expect(result.actualEndLine).toBe(1); // Should only get 2 lines
3423
expect(result.linesRead).toBe(2);
3524
});
3625

37-
it("should handle offset beyond file length", () => {
38-
const result = extractLinesWithByteLimit(simpleContent, 10, 5, 1000);
39-
40-
expect(result.content).toBe("");
41-
expect(result.actualEndLine).toBe(10);
42-
expect(result.wasLimited).toBe(false);
43-
expect(result.linesRead).toBe(0);
44-
});
45-
4626
it("should handle empty content", () => {
47-
const result = extractLinesWithByteLimit("", 0, 10, 1000);
27+
const result = extractLinesWithByteLimit("", 1000);
4828

4929
expect(result.content).toBe("");
50-
expect(result.actualEndLine).toBe(0); // Empty string splits to [""] and we process that one empty line
5130
expect(result.wasLimited).toBe(false);
5231
expect(result.linesRead).toBe(1); // We read the one empty line
5332
});
5433

5534
it("should handle single line file", () => {
5635
const singleLine = "This is a single line without newline";
57-
const result = extractLinesWithByteLimit(singleLine, 0, 10, 1000);
36+
const result = extractLinesWithByteLimit(singleLine, 1000);
5837

5938
expect(result.content).toBe(singleLine);
60-
expect(result.actualEndLine).toBe(0);
6139
expect(result.wasLimited).toBe(false);
6240
expect(result.linesRead).toBe(1);
6341
});
6442

6543
it("should correctly count bytes with multi-byte characters", () => {
6644
const unicodeContent = "Hello 世界\n你好 World\nEmoji: 🌍\nNormal line";
67-
const result = extractLinesWithByteLimit(unicodeContent, 0, 10, 1000);
45+
const result = extractLinesWithByteLimit(unicodeContent, 1000);
6846

6947
expect(result.content).toBe(unicodeContent);
7048
expect(result.linesRead).toBe(4);
@@ -77,63 +55,25 @@ describe("extractLinesWithByteLimit", () => {
7755
const line3 = "c".repeat(40);
7856
const content = `${line1}\n${line2}\n${line3}`;
7957

80-
const result = extractLinesWithByteLimit(content, 0, 3, 85);
58+
const result = extractLinesWithByteLimit(content, 85);
8159

82-
expect(result.content).toBe(`${line1}\n${line2}`);
83-
expect(result.actualEndLine).toBe(1);
60+
expect(result.content).toBe(`${line1}\n${line2}\n`);
8461
expect(result.wasLimited).toBe(true);
8562
expect(result.linesRead).toBe(2);
8663
});
8764

8865
it("should read exactly to limit when possible", () => {
8966
const exactContent = "12345\n67890\n12345"; // 17 bytes total
90-
const result = extractLinesWithByteLimit(exactContent, 0, 3, 17);
67+
const result = extractLinesWithByteLimit(exactContent, 17);
9168

9269
expect(result.content).toBe(exactContent);
93-
expect(result.actualEndLine).toBe(2);
9470
expect(result.wasLimited).toBe(false);
9571
expect(result.linesRead).toBe(3);
9672
});
9773

98-
it("should handle reading from middle to end", () => {
99-
const result = extractLinesWithByteLimit(simpleContent, 3, 100, 1000);
100-
101-
expect(result.content).toBe("Line 4\nLine 5");
102-
expect(result.actualEndLine).toBe(4);
103-
expect(result.wasLimited).toBe(false);
104-
expect(result.linesRead).toBe(2);
105-
});
106-
107-
it("should handle large content with typical parameters", () => {
108-
const longContent = Array(2000).fill("Line content here").join("\n");
109-
const result = extractLinesWithByteLimit(longContent, 0, 1000, 50000);
110-
111-
expect(result.actualEndLine).toBeLessThanOrEqual(1000); // Should be limited by line count
112-
});
113-
114-
it("should provide correct data for partial read at start", () => {
115-
const result = extractLinesWithByteLimit(simpleContent, 0, 2, 1000);
116-
117-
expect(result.actualEndLine).toBe(1);
118-
expect(result.linesRead).toBe(2);
119-
});
120-
121-
it("should provide correct data for partial read with offset", () => {
122-
const result = extractLinesWithByteLimit(simpleContent, 1, 2, 1000);
123-
124-
expect(result.actualEndLine).toBe(2);
125-
expect(result.linesRead).toBe(2);
126-
});
127-
128-
it("should not add newline after last line", () => {
129-
const result = extractLinesWithByteLimit("Line 1\nLine 2\nLine 3", 2, 1, 1000);
130-
131-
expect(result.content).toBe("Line 3"); // No trailing newline
132-
});
133-
13474
it("should handle Windows-style line endings", () => {
13575
const windowsContent = "Line 1\r\nLine 2\r\nLine 3";
136-
const result = extractLinesWithByteLimit(windowsContent, 0, 3, 1000);
76+
const result = extractLinesWithByteLimit(windowsContent, 1000);
13777

13878
// Note: split("\n") will keep the \r characters
13979
expect(result.content).toBe("Line 1\r\nLine 2\r\nLine 3");
@@ -145,29 +85,18 @@ describe("extractLinesWithByteLimit", () => {
14585
const largeLine = "x".repeat(1000);
14686
const largeContent = Array(110).fill(largeLine).join("\n");
14787

148-
const result = extractLinesWithByteLimit(largeContent, 0, 200, 50000);
88+
const result = extractLinesWithByteLimit(largeContent, 50000);
14989

15090
expect(result.wasLimited).toBe(true);
151-
expect(result.actualEndLine).toBeLessThan(50); // Should stop well before 200 lines
152-
});
153-
154-
it("should handle line limit of 0", () => {
155-
const result = extractLinesWithByteLimit(simpleContent, 0, 0, 1000);
156-
157-
expect(result.content).toBe("");
158-
expect(result.actualEndLine).toBe(0);
159-
expect(result.linesRead).toBe(0);
160-
expect(result.wasLimited).toBe(false);
16191
});
16292

16393
it("should allow at least one line even if it exceeds byte limit", () => {
16494
const veryLongLine = "x".repeat(100000); // 100KB line
165-
const result = extractLinesWithByteLimit(veryLongLine, 0, 1, 50000);
95+
const result = extractLinesWithByteLimit(veryLongLine, 50000);
16696

16797
// Should return the line even though it exceeds the byte limit
16898
// because we always allow at least one line if no lines have been added yet
16999
expect(result.content).toBe(veryLongLine);
170-
expect(result.actualEndLine).toBe(0);
171100
expect(result.linesRead).toBe(1);
172101
expect(result.wasLimited).toBe(false);
173102
});

src/utils.ts

Lines changed: 12 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export function applyEnvironmentSettings(settings: ManagedSettings): void {
125125

126126
export interface ExtractLinesResult {
127127
content: string;
128-
actualEndLine: number;
129128
wasLimited: boolean;
130129
linesRead: number;
131130
}
@@ -134,100 +133,55 @@ export interface ExtractLinesResult {
134133
* Extracts lines from file content with byte limit enforcement.
135134
*
136135
* @param fullContent - The complete file content
137-
* @param linesToSkip - Starting line number (0-based)
138-
* @param linesToRead - Maximum number of lines to read
139136
* @param maxContentLength - Maximum number of UTF-16 Code Units to return
140137
* @returns Object containing extracted content and metadata
141138
*/
142139
export function extractLinesWithByteLimit(
143140
fullContent: string,
144-
linesToSkip: number,
145-
linesToRead: number,
146141
maxContentLength: number,
147142
): ExtractLinesResult {
148-
if (fullContent === "" || linesToRead === 0) {
149-
if (linesToSkip === 0 && linesToRead > 0) {
150-
return {
151-
content: "",
152-
actualEndLine: 0,
153-
wasLimited: false,
154-
linesRead: 1,
155-
};
156-
} else {
157-
return {
158-
content: "",
159-
actualEndLine: linesToSkip,
160-
wasLimited: false,
161-
linesRead: 0,
162-
};
163-
}
143+
if (fullContent === "") {
144+
return {
145+
content: "",
146+
wasLimited: false,
147+
linesRead: 1,
148+
};
164149
}
165150

166151
let linesSeen = 0;
167152
let index = 0;
168-
169-
while (linesSeen < linesToSkip) {
170-
const nextIndex = fullContent.indexOf("\n", index);
171-
172-
// There were not enough lines to skip.
173-
if (nextIndex < 0) {
174-
return {
175-
content: "",
176-
actualEndLine: linesToSkip,
177-
wasLimited: false,
178-
linesRead: 0,
179-
};
180-
}
181-
182-
linesSeen += 1;
183-
index = nextIndex + 1;
184-
}
185-
186-
// We've successfully skipped over all the lines we were supposed to.
187-
// Now we can actually start reading!
188-
const startIndex = index;
189153
linesSeen = 0;
190154

191155
let contentLength = 0;
192156
let wasLimited = false;
193157

194-
while (linesSeen < linesToRead) {
158+
while (true) {
195159
const nextIndex = fullContent.indexOf("\n", index);
196160

197161
if (nextIndex < 0) {
198162
// Last line in file (no trailing newline)
199-
const newContentLength = fullContent.length - startIndex;
200-
if (linesSeen > 0 && newContentLength > maxContentLength) {
163+
if (linesSeen > 0 && fullContent.length > maxContentLength) {
201164
wasLimited = true;
202165
break;
203166
}
204167
linesSeen += 1;
205-
contentLength = newContentLength;
168+
contentLength = fullContent.length;
206169
break;
207170
} else {
208171
// Line with newline - include up to the newline
209-
const newContentLength = nextIndex + 1 - startIndex;
172+
const newContentLength = nextIndex + 1;
210173
if (linesSeen > 0 && newContentLength > maxContentLength) {
211174
wasLimited = true;
212175
break;
213176
}
214177
linesSeen += 1;
215178
contentLength = newContentLength;
216-
index = nextIndex + 1;
217-
}
218-
}
219-
220-
// If we ended with a newline and we stopped due to byte limit or line limit (not end of file), remove the trailing newline
221-
if (contentLength > 0 && fullContent[startIndex + contentLength - 1] === "\n") {
222-
// Check if there's more content after our current position that we didn't read
223-
if (startIndex + contentLength < fullContent.length) {
224-
contentLength -= 1;
179+
index = newContentLength;
225180
}
226181
}
227182

228183
return {
229-
content: fullContent.slice(startIndex, startIndex + contentLength),
230-
actualEndLine: linesSeen > 0 ? linesToSkip + linesSeen - 1 : linesToSkip,
184+
content: fullContent.slice(0, contentLength),
231185
wasLimited,
232186
linesRead: linesSeen,
233187
};

0 commit comments

Comments
 (0)