Skip to content

Commit f63bdc8

Browse files
authored
convert list tool to Tool.defineEffect (#21899)
1 parent ce26120 commit f63bdc8

1 file changed

Lines changed: 94 additions & 82 deletions

File tree

  • packages/opencode/src/tool

packages/opencode/src/tool/ls.ts

Lines changed: 94 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import z from "zod"
2+
import { Effect } from "effect"
3+
import * as Stream from "effect/Stream"
24
import { Tool } from "./tool"
35
import * as path from "path"
46
import DESCRIPTION from "./ls.txt"
57
import { Instance } from "../project/instance"
68
import { Ripgrep } from "../file/ripgrep"
7-
import { assertExternalDirectory } from "./external-directory"
9+
import { assertExternalDirectoryEffect } from "./external-directory"
810

911
export const IGNORE_PATTERNS = [
1012
"node_modules/",
@@ -35,87 +37,97 @@ export const IGNORE_PATTERNS = [
3537

3638
const LIMIT = 100
3739

38-
export const ListTool = Tool.define("list", {
39-
description: DESCRIPTION,
40-
parameters: z.object({
41-
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
42-
ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
43-
}),
44-
async execute(params, ctx) {
45-
const searchPath = path.resolve(Instance.directory, params.path || ".")
46-
await assertExternalDirectory(ctx, searchPath, { kind: "directory" })
47-
48-
await ctx.ask({
49-
permission: "list",
50-
patterns: [searchPath],
51-
always: ["*"],
52-
metadata: {
53-
path: searchPath,
54-
},
55-
})
56-
57-
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
58-
const files = []
59-
for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs, signal: ctx.abort })) {
60-
files.push(file)
61-
if (files.length >= LIMIT) break
62-
}
63-
64-
// Build directory structure
65-
const dirs = new Set<string>()
66-
const filesByDir = new Map<string, string[]>()
67-
68-
for (const file of files) {
69-
const dir = path.dirname(file)
70-
const parts = dir === "." ? [] : dir.split("/")
71-
72-
// Add all parent directories
73-
for (let i = 0; i <= parts.length; i++) {
74-
const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
75-
dirs.add(dirPath)
76-
}
77-
78-
// Add file to its directory
79-
if (!filesByDir.has(dir)) filesByDir.set(dir, [])
80-
filesByDir.get(dir)!.push(path.basename(file))
81-
}
82-
83-
function renderDir(dirPath: string, depth: number): string {
84-
const indent = " ".repeat(depth)
85-
let output = ""
86-
87-
if (depth > 0) {
88-
output += `${indent}${path.basename(dirPath)}/\n`
89-
}
90-
91-
const childIndent = " ".repeat(depth + 1)
92-
const children = Array.from(dirs)
93-
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
94-
.sort()
95-
96-
// Render subdirectories first
97-
for (const child of children) {
98-
output += renderDir(child, depth + 1)
99-
}
100-
101-
// Render files
102-
const files = filesByDir.get(dirPath) || []
103-
for (const file of files.sort()) {
104-
output += `${childIndent}${file}\n`
105-
}
106-
107-
return output
108-
}
109-
110-
const output = `${searchPath}/\n` + renderDir(".", 0)
40+
export const ListTool = Tool.defineEffect(
41+
"list",
42+
Effect.gen(function* () {
43+
const rg = yield* Ripgrep.Service
11144

11245
return {
113-
title: path.relative(Instance.worktree, searchPath),
114-
metadata: {
115-
count: files.length,
116-
truncated: files.length >= LIMIT,
117-
},
118-
output,
46+
description: DESCRIPTION,
47+
parameters: z.object({
48+
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
49+
ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
50+
}),
51+
execute: (params: { path?: string; ignore?: string[] }, ctx: Tool.Context) =>
52+
Effect.gen(function* () {
53+
const searchPath = path.resolve(Instance.directory, params.path || ".")
54+
yield* assertExternalDirectoryEffect(ctx, searchPath, { kind: "directory" })
55+
56+
yield* Effect.promise(() =>
57+
ctx.ask({
58+
permission: "list",
59+
patterns: [searchPath],
60+
always: ["*"],
61+
metadata: {
62+
path: searchPath,
63+
},
64+
}),
65+
)
66+
67+
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
68+
const files = yield* rg.files({ cwd: searchPath, glob: ignoreGlobs }).pipe(
69+
Stream.take(LIMIT),
70+
Stream.runCollect,
71+
Effect.map((chunk) => [...chunk]),
72+
)
73+
74+
// Build directory structure
75+
const dirs = new Set<string>()
76+
const filesByDir = new Map<string, string[]>()
77+
78+
for (const file of files) {
79+
const dir = path.dirname(file)
80+
const parts = dir === "." ? [] : dir.split("/")
81+
82+
// Add all parent directories
83+
for (let i = 0; i <= parts.length; i++) {
84+
const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
85+
dirs.add(dirPath)
86+
}
87+
88+
// Add file to its directory
89+
if (!filesByDir.has(dir)) filesByDir.set(dir, [])
90+
filesByDir.get(dir)!.push(path.basename(file))
91+
}
92+
93+
function renderDir(dirPath: string, depth: number): string {
94+
const indent = " ".repeat(depth)
95+
let output = ""
96+
97+
if (depth > 0) {
98+
output += `${indent}${path.basename(dirPath)}/\n`
99+
}
100+
101+
const childIndent = " ".repeat(depth + 1)
102+
const children = Array.from(dirs)
103+
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
104+
.sort()
105+
106+
// Render subdirectories first
107+
for (const child of children) {
108+
output += renderDir(child, depth + 1)
109+
}
110+
111+
// Render files
112+
const files = filesByDir.get(dirPath) || []
113+
for (const file of files.sort()) {
114+
output += `${childIndent}${file}\n`
115+
}
116+
117+
return output
118+
}
119+
120+
const output = `${searchPath}/\n` + renderDir(".", 0)
121+
122+
return {
123+
title: path.relative(Instance.worktree, searchPath),
124+
metadata: {
125+
count: files.length,
126+
truncated: files.length >= LIMIT,
127+
},
128+
output,
129+
}
130+
}).pipe(Effect.orDie, Effect.runPromise),
119131
}
120-
},
121-
})
132+
}),
133+
)

0 commit comments

Comments
 (0)