Skip to content

Commit c7de3c1

Browse files
committed
fix(tools): read_file uses arrow separator, not tab
The cat -n style `{num}\t{content}` rendered fine in plain pagers but every TUI / Ink / chat-bubble renderer expanded the tab to the next 8-column tab stop, producing an ugly wide gap between the gutter and the line content (visible in the in-CLI tool output panel). Use the literal arrow `→` like Claude Code does so the rendering is tight in every terminal and the model can still cleanly distinguish gutter from content when quoting into edit_file.
1 parent 01b5e37 commit c7de3c1

2 files changed

Lines changed: 12 additions & 6 deletions

File tree

src/tools/read-file.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ describe("read_file", () => {
3535
const result = await run(ctx, { path: "hello.txt" });
3636
const text = (result.content[0] as { type: "text"; text: string }).text;
3737

38-
expect(text).toContain(" 1\tfirst line");
39-
expect(text).toContain(" 2\tsecond line");
38+
expect(text).toContain(" 1→first line");
39+
expect(text).toContain(" 2→second line");
4040
expect(result.details.totalLines).toBe(2);
4141
expect(result.details.isPartialView).toBe(false);
4242

@@ -74,8 +74,8 @@ describe("read_file", () => {
7474
const result = await run(ctx, { path: "long.txt", offset: 10, limit: 5 });
7575
const text = (result.content[0] as { type: "text"; text: string }).text;
7676

77-
expect(text).toContain(" 10\tline 10");
78-
expect(text).toContain(" 14\tline 14");
77+
expect(text).toContain(" 10→line 10");
78+
expect(text).toContain(" 14→line 14");
7979
expect(text).not.toContain("line 15");
8080
expect(text).toContain("showing lines 10-14 of 50");
8181

src/tools/read-file.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Arguments:
5757
- limit (optional): max lines to return. Default 2000, max 2000.
5858
5959
Behavior:
60-
- Text files: returned line-numbered (six-column gutter, tab, content), suitable for the model to quote back into edit_file.
60+
- Text files: returned line-numbered as \` N→content\` (six-column right-aligned line number, an arrow separator, then the line). When you quote a line into edit_file's old_string, quote only the part after the arrow.
6161
- Image files (.png, .jpg, .jpeg, .gif, .webp, .bmp): returned as a vision payload — works only with image-capable models.
6262
- Files > 5 MB are rejected; use shell head/tail/sed for huge files.
6363
- Binary text files are rejected.
@@ -130,7 +130,13 @@ export function createReadFile(ctx: ToolContext): AgentTool<typeof Params, ReadF
130130
const isPartialView = startIdx > 0 || endIdx < totalLines;
131131

132132
const slice = allLines.slice(startIdx, endIdx);
133-
const formatted = slice.map((line, i) => `${pad6(startIdx + i + 1)}\t${line}`).join("\n");
133+
// Use a literal arrow instead of \t so the gutter renders tight in
134+
// every terminal (TUI / pager / web echo). cat -n's tab gets
135+
// expanded to the next tab stop by most renderers, producing the
136+
// ugly 8-column gap users complained about. Matches Claude Code's
137+
// `${num}→${content}` format, which models are already familiar
138+
// with from training data.
139+
const formatted = slice.map((line, i) => `${pad6(startIdx + i + 1)}${line}`).join("\n");
134140
const tail = isPartialView ? `\n... (showing lines ${startIdx + 1}-${endIdx} of ${totalLines})` : "";
135141

136142
ctx.fileStateCache.record({

0 commit comments

Comments
 (0)