Skip to content

Commit 8b61442

Browse files
committed
computer: Paginate ls results
Return file size and modification time from ls, and expose limit, offset, and nextOffset so large directories can be read in stable pages.
1 parent 6c1672c commit 8b61442

2 files changed

Lines changed: 88 additions & 11 deletions

File tree

packages/computer/src/tools/ai.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,17 @@ describe("createAITools filesystem tools", () => {
289289
);
290290
await expect(executeTool(tools.ls, { path: "/workspace/notes" })).resolves.toEqual({
291291
path: "/workspace/notes",
292-
entries: [{ name: "todo.txt", isFile: true, isDirectory: false }],
292+
count: 1,
293+
entries: [
294+
{
295+
name: "todo.txt",
296+
size: 8,
297+
mtime: 1_700_000_000_000,
298+
isFile: true,
299+
isDirectory: false,
300+
isSymbolicLink: false,
301+
},
302+
],
293303
});
294304
await expect(
295305
executeTool(tools.read, { path: "/workspace/notes/todo.txt", limit: 1 }),
@@ -313,6 +323,32 @@ describe("createAITools filesystem tools", () => {
313323
);
314324
});
315325

326+
it("paginates ls results and reports a continuation offset", async () => {
327+
const workspace = makeWorkspace();
328+
await workspace.fs.mkdir("/workspace", { recursive: true });
329+
for (const name of ["a", "b", "c"]) {
330+
await workspace.fs.writeFile(`/workspace/${name}`, name);
331+
}
332+
const tools = createAITools({ workspace });
333+
334+
await expect(
335+
executeTool(tools.ls, { path: "/workspace", limit: 2, offset: 0 }),
336+
).resolves.toMatchObject({
337+
count: 2,
338+
entries: [
339+
{ name: "a", size: 1 },
340+
{ name: "b", size: 1 },
341+
],
342+
nextOffset: 2,
343+
});
344+
await expect(
345+
executeTool(tools.ls, { path: "/workspace", limit: 2, offset: 2 }),
346+
).resolves.toMatchObject({
347+
count: 1,
348+
entries: [{ name: "c", size: 1 }],
349+
});
350+
});
351+
316352
it("preserves file mode when write overwrites an existing file", async () => {
317353
const writes: Array<{ path: string; content: string; mode?: number }> = [];
318354
const tool = createWriteTool({

packages/computer/src/tools/fs/list.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,75 @@ import { z } from "zod";
33

44
export interface ListWorkspaceLike {
55
fs: {
6-
readdir(path: string): Promise<Array<{ name: string; isFile: boolean; isDirectory: boolean }>>;
6+
readdir(
7+
path: string,
8+
options?: { limit?: number; offset?: number },
9+
): Promise<
10+
Array<{
11+
name: string;
12+
size: number;
13+
mtime: number;
14+
isFile: boolean;
15+
isDirectory: boolean;
16+
isSymbolicLink: boolean;
17+
}>
18+
>;
719
};
820
}
921

1022
export interface ListToolOptions {
1123
workspace: ListWorkspaceLike;
1224
}
1325

26+
const DEFAULT_LIMIT = 200;
27+
const MAX_LIMIT = 1000;
28+
1429
const inputSchema = z.object({
1530
path: z.string().describe("Absolute directory path to list, e.g. /workspace/src."),
31+
limit: z
32+
.number()
33+
.int()
34+
.min(1)
35+
.max(MAX_LIMIT)
36+
.optional()
37+
.describe(`Maximum entries to return. Defaults to ${DEFAULT_LIMIT}.`),
38+
offset: z.number().int().min(0).optional().describe("Number of entries to skip in name order."),
1639
});
1740

1841
export function createListTool(options: ListToolOptions): Tool<z.infer<typeof inputSchema>> {
1942
return tool({
2043
description:
21-
"List entries in a workspace directory. Returns each entry name and whether it is a file or directory.",
44+
"List entries in a workspace directory with file sizes and modification times. Use limit and offset to page through large directories.",
2245
inputSchema,
23-
execute: async ({ path }) => {
46+
execute: async ({ path, limit, offset }) => {
2447
try {
25-
const entries = await options.workspace.fs.readdir(path);
26-
return {
48+
const pageSize = limit ?? DEFAULT_LIMIT;
49+
const pageOffset = offset ?? 0;
50+
const entries = await options.workspace.fs.readdir(path, {
51+
limit: pageSize + 1,
52+
offset: pageOffset,
53+
});
54+
const truncated = entries.length > pageSize;
55+
const page = (truncated ? entries.slice(0, pageSize) : entries).map((entry) => ({
56+
name: entry.name,
57+
size: entry.size,
58+
mtime: entry.mtime,
59+
isFile: entry.isFile,
60+
isDirectory: entry.isDirectory,
61+
isSymbolicLink: entry.isSymbolicLink,
62+
}));
63+
const result: {
64+
path: string;
65+
count: number;
66+
entries: typeof page;
67+
nextOffset?: number;
68+
} = {
2769
path,
28-
entries: entries.map((entry) => ({
29-
name: entry.name,
30-
isFile: entry.isFile,
31-
isDirectory: entry.isDirectory,
32-
})),
70+
count: page.length,
71+
entries: page,
3372
};
73+
if (truncated) result.nextOffset = pageOffset + pageSize;
74+
return result;
3475
} catch (err) {
3576
return { error: err instanceof Error ? err.message : String(err) };
3677
}

0 commit comments

Comments
 (0)