Skip to content

Commit 2f462e4

Browse files
feat(tools): consolidate view_range into read_file and remove view_range
`read_file` now supports: - whole-file reads: `{ path }` - ranged reads: `{ path, startLine, endLine }` - first N lines: `{ path, maxLines }` - N lines from a start: `{ path, startLine, maxLines }` - start to EOF: `{ path, startLine }` Partial reads return line-numbered output. `endLine` and `maxLines` cannot be combined. Updated: - tool constants/read tool set - tool definitions - dispatcher validation/execution - prompt text - filesystem and dispatcher tests - registry tests
1 parent c150bfb commit 2f462e4

8 files changed

Lines changed: 206 additions & 178 deletions

File tree

src/constants/prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Follow these rules:
1616
When tools return results, incorporate them into your response naturally`;
1717

1818
export const TOOL_INSTRUCTIONS = `Available tools:
19-
- read_file: Read file contents at a path
19+
- read_file: Read file contents at a path; supports startLine, endLine, and maxLines options
2020
- write_file: Write content to a file (requires approval)
2121
- edit_file: Replace one exact text match in a file (requires approval)
2222
- create_directory: Create a directory and missing parent directories (requires approval)

src/constants/tool.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export const RUN_SHELL = 'run_shell';
88
export const LIST_DIR = 'list_dir';
99
export const FIND_FILES = 'find_files';
1010
export const GREP_SEARCH = 'grep_search';
11-
export const VIEW_RANGE = 'view_range';
1211
export const WEB_SEARCH = 'web_search';
1312
export const WEB_FETCH = 'web_fetch';
1413

@@ -17,7 +16,6 @@ export const READ_TOOL_NAMES = [
1716
LIST_DIR,
1817
FIND_FILES,
1918
GREP_SEARCH,
20-
VIEW_RANGE,
2119
WEB_SEARCH,
2220
WEB_FETCH,
2321
] as const;

src/utils/tools/definitions.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { READ_TOOLS, TOOLS, WRITE_TOOLS } from './definitions';
33
describe('definitions', () => {
44
describe('TOOLS', () => {
55
it('exports tool definitions', () => {
6-
expect(TOOLS).toHaveLength(13);
6+
expect(TOOLS).toHaveLength(12);
77
expect(TOOLS.map((t) => t.function.name)).toContain('read_file');
88
expect(TOOLS.map((t) => t.function.name)).toContain('write_file');
99
expect(TOOLS.map((t) => t.function.name)).toContain('edit_file');
@@ -14,7 +14,6 @@ describe('definitions', () => {
1414
expect(TOOLS.map((t) => t.function.name)).toContain('list_dir');
1515
expect(TOOLS.map((t) => t.function.name)).toContain('find_files');
1616
expect(TOOLS.map((t) => t.function.name)).toContain('grep_search');
17-
expect(TOOLS.map((t) => t.function.name)).toContain('view_range');
1817
expect(TOOLS.map((t) => t.function.name)).toContain('web_search');
1918
expect(TOOLS.map((t) => t.function.name)).toContain('web_fetch');
2019
});

src/utils/tools/definitions.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,22 @@ function defineTool(
3737
export const TOOLS = [
3838
defineTool(
3939
TOOL.READ_FILE,
40-
'Read the contents of a file at the specified path',
40+
'Read the contents of a file at the specified path, optionally limited by line range',
4141
{
4242
path: { type: 'string', description: 'The path to the file to read' },
43+
startLine: {
44+
type: 'number',
45+
description: 'Optional starting line number to read from (1-indexed)',
46+
},
47+
endLine: {
48+
type: 'number',
49+
description: 'Optional ending line number to read through (inclusive)',
50+
},
51+
maxLines: {
52+
type: 'number',
53+
description:
54+
'Optional maximum number of lines to read; cannot be combined with endLine',
55+
},
4356
},
4457
['path'],
4558
),
@@ -184,23 +197,6 @@ export const TOOLS = [
184197
['pattern', 'path'],
185198
),
186199

187-
defineTool(
188-
TOOL.VIEW_RANGE,
189-
'View a specific range of lines from a file',
190-
{
191-
path: { type: 'string', description: 'The path to the file' },
192-
start: {
193-
type: 'number',
194-
description: 'The starting line number (1-indexed)',
195-
},
196-
end: {
197-
type: 'number',
198-
description: 'The ending line number (inclusive)',
199-
},
200-
},
201-
['path', 'start', 'end'],
202-
),
203-
204200
defineTool(
205201
TOOL.WEB_SEARCH,
206202
'Search the web for external or current information',

src/utils/tools/dispatcher.test.ts

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,52 @@ describe('dispatcher', () => {
282282
);
283283
});
284284

285+
it('returns error when read_file range args are not numbers', async () => {
286+
const result = await executeTool('read_file', {
287+
path: '/test.txt',
288+
startLine: '2',
289+
});
290+
291+
expect(result.error).toContain(
292+
'Invalid optional numeric argument: startLine',
293+
);
294+
});
295+
296+
it('returns error when read_file range args are below one', async () => {
297+
const result = await executeTool('read_file', {
298+
path: '/test.txt',
299+
maxLines: 0,
300+
});
301+
302+
expect(result.error).toContain(
303+
'Invalid read range: startLine, endLine, and maxLines must be >= 1',
304+
);
305+
});
306+
307+
it('returns error when read_file combines endLine and maxLines', async () => {
308+
const result = await executeTool('read_file', {
309+
path: '/test.txt',
310+
endLine: 3,
311+
maxLines: 2,
312+
});
313+
314+
expect(result.error).toContain(
315+
'Invalid read range: endLine cannot be combined with maxLines',
316+
);
317+
});
318+
319+
it('returns error when read_file endLine is less than startLine', async () => {
320+
const result = await executeTool('read_file', {
321+
path: '/test.txt',
322+
endLine: 2,
323+
startLine: 5,
324+
});
325+
326+
expect(result.error).toContain(
327+
'Invalid read range: endLine must be >= startLine',
328+
);
329+
});
330+
285331
it('executes read_file tool', async () => {
286332
vi.mocked(existsSync).mockReturnValue(true);
287333
vi.mocked(readFileSync).mockReturnValue('file content');
@@ -291,6 +337,20 @@ describe('dispatcher', () => {
291337
expect(result.error).toBeUndefined();
292338
});
293339

340+
it('executes read_file tool with line range', async () => {
341+
vi.mocked(existsSync).mockReturnValue(true);
342+
vi.mocked(readFileSync).mockReturnValue('line1\nline2\nline3');
343+
344+
const result = await executeTool('read_file', {
345+
path: '/test.txt',
346+
endLine: 3,
347+
startLine: 2,
348+
});
349+
350+
expect(result.content).toBe('2: line2\n3: line3');
351+
expect(result.error).toBeUndefined();
352+
});
353+
294354
it('executes write_file tool', async () => {
295355
const result = await executeTool('write_file', {
296356
path: '/test.txt',
@@ -499,43 +559,6 @@ describe('dispatcher', () => {
499559
expect(result.error).toBeUndefined();
500560
});
501561

502-
it('executes view_range tool', async () => {
503-
vi.mocked(existsSync).mockReturnValue(true);
504-
vi.mocked(readFileSync).mockReturnValue(
505-
'line1\nline2\nline3\nline4\nline5',
506-
);
507-
508-
const result = await executeTool('view_range', {
509-
path: '/test.txt',
510-
start: 2,
511-
end: 4,
512-
});
513-
expect(result.content).toContain('line2');
514-
expect(result.content).toContain('line3');
515-
expect(result.content).toContain('line4');
516-
expect(result.error).toBeUndefined();
517-
});
518-
519-
it('returns error for invalid view_range numeric arguments', async () => {
520-
const result = await executeTool('view_range', {
521-
path: '/test.txt',
522-
start: '2',
523-
end: 4,
524-
});
525-
526-
expect(result.error).toContain('Missing required numeric arguments');
527-
});
528-
529-
it('returns error when view_range end is less than start', async () => {
530-
const result = await executeTool('view_range', {
531-
path: '/test.txt',
532-
start: 5,
533-
end: 2,
534-
});
535-
536-
expect(result.error).toContain('Invalid line range');
537-
});
538-
539562
it('executes web_search tool', async () => {
540563
mockFetch.mockResolvedValue({
541564
ok: true,

src/utils/tools/dispatcher.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
listDir,
1313
readFile,
1414
renamePath,
15-
viewRange,
1615
writeFile,
1716
} from './filesystem';
1817
import { runShell } from './shell';
@@ -39,7 +38,6 @@ const REQUIRED_STRING_ARGS: Record<ToolName, string[]> = {
3938
[TOOL.LIST_DIR]: ['path'],
4039
[TOOL.FIND_FILES]: ['path'],
4140
[TOOL.GREP_SEARCH]: ['pattern', 'path'],
42-
[TOOL.VIEW_RANGE]: ['path'],
4341
[TOOL.WEB_SEARCH]: ['query'],
4442
[TOOL.WEB_FETCH]: ['url'],
4543
} as const;
@@ -68,22 +66,45 @@ function validateArgs(
6866
}
6967
}
7068

71-
if (name === TOOL.VIEW_RANGE) {
72-
if (!Number.isInteger(args.start) || !Number.isInteger(args.end)) {
69+
if (name === TOOL.READ_FILE) {
70+
const numericArgs = ['startLine', 'endLine', 'maxLines'] as const;
71+
72+
for (const key of numericArgs) {
73+
if (args[key] !== undefined && !Number.isInteger(args[key])) {
74+
return {
75+
content: '',
76+
error: `Invalid optional numeric argument: ${key} (received keys: ${received})`,
77+
};
78+
}
79+
}
80+
81+
if (
82+
(typeof args.startLine === 'number' && args.startLine < 1) ||
83+
(typeof args.endLine === 'number' && args.endLine < 1) ||
84+
(typeof args.maxLines === 'number' && args.maxLines < 1)
85+
) {
7386
return {
7487
content: '',
75-
error: `Missing required numeric arguments: start, end (received keys: ${received})`,
88+
error:
89+
'Invalid read range: startLine, endLine, and maxLines must be >= 1',
90+
};
91+
}
92+
93+
if (args.endLine !== undefined && args.maxLines !== undefined) {
94+
return {
95+
content: '',
96+
error: 'Invalid read range: endLine cannot be combined with maxLines',
7697
};
7798
}
7899

79100
if (
80-
(args.start as number) < 1 ||
81-
(args.end as number) < (args.start as number)
101+
typeof args.startLine === 'number' &&
102+
typeof args.endLine === 'number' &&
103+
args.endLine < args.startLine
82104
) {
83105
return {
84106
content: '',
85-
error:
86-
'Invalid line range: start must be >= 1 and end must be >= start',
107+
error: 'Invalid read range: endLine must be >= startLine',
87108
};
88109
}
89110
}
@@ -248,7 +269,11 @@ export async function executeTool(
248269

249270
switch (name) {
250271
case TOOL.READ_FILE:
251-
return readFile(stringArgs.path);
272+
return readFile(stringArgs.path, {
273+
endLine: args.endLine as number | undefined,
274+
maxLines: args.maxLines as number | undefined,
275+
startLine: args.startLine as number | undefined,
276+
});
252277

253278
case TOOL.WRITE_FILE:
254279
return writeFile(stringArgs.path, stringArgs.content);
@@ -281,13 +306,6 @@ export async function executeTool(
281306
case TOOL.GREP_SEARCH:
282307
return await grepSearch(stringArgs.pattern, stringArgs.path);
283308

284-
case TOOL.VIEW_RANGE:
285-
return viewRange(
286-
stringArgs.path,
287-
args.start as number,
288-
args.end as number,
289-
);
290-
291309
case TOOL.WEB_SEARCH:
292310
return await webSearch(stringArgs.query);
293311

0 commit comments

Comments
 (0)