summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-10 15:38:52 -0400
committerGitHub <[email protected]>2026-04-10 15:38:52 -0400
commitf63bdc8e08a179960fcfd1fe982354dfdf84b8fb (patch)
tree69b795db6b01db41e42ddc3fdcb55804593e1e31 /packages
parentce2612020564ace2bfe95a36f139a07ba237f563 (diff)
downloadopencode-f63bdc8e08a179960fcfd1fe982354dfdf84b8fb.tar.gz
opencode-f63bdc8e08a179960fcfd1fe982354dfdf84b8fb.zip
convert list tool to Tool.defineEffect (#21899)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/tool/ls.ts176
1 files changed, 94 insertions, 82 deletions
diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts
index b848e969b..659315790 100644
--- a/packages/opencode/src/tool/ls.ts
+++ b/packages/opencode/src/tool/ls.ts
@@ -1,10 +1,12 @@
import z from "zod"
+import { Effect } from "effect"
+import * as Stream from "effect/Stream"
import { Tool } from "./tool"
import * as path from "path"
import DESCRIPTION from "./ls.txt"
import { Instance } from "../project/instance"
import { Ripgrep } from "../file/ripgrep"
-import { assertExternalDirectory } from "./external-directory"
+import { assertExternalDirectoryEffect } from "./external-directory"
export const IGNORE_PATTERNS = [
"node_modules/",
@@ -35,87 +37,97 @@ export const IGNORE_PATTERNS = [
const LIMIT = 100
-export const ListTool = Tool.define("list", {
- description: DESCRIPTION,
- parameters: z.object({
- path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
- ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
- }),
- async execute(params, ctx) {
- const searchPath = path.resolve(Instance.directory, params.path || ".")
- await assertExternalDirectory(ctx, searchPath, { kind: "directory" })
-
- await ctx.ask({
- permission: "list",
- patterns: [searchPath],
- always: ["*"],
- metadata: {
- path: searchPath,
- },
- })
-
- const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
- const files = []
- for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs, signal: ctx.abort })) {
- files.push(file)
- if (files.length >= LIMIT) break
- }
-
- // Build directory structure
- const dirs = new Set<string>()
- const filesByDir = new Map<string, string[]>()
-
- for (const file of files) {
- const dir = path.dirname(file)
- const parts = dir === "." ? [] : dir.split("/")
-
- // Add all parent directories
- for (let i = 0; i <= parts.length; i++) {
- const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
- dirs.add(dirPath)
- }
-
- // Add file to its directory
- if (!filesByDir.has(dir)) filesByDir.set(dir, [])
- filesByDir.get(dir)!.push(path.basename(file))
- }
-
- function renderDir(dirPath: string, depth: number): string {
- const indent = " ".repeat(depth)
- let output = ""
-
- if (depth > 0) {
- output += `${indent}${path.basename(dirPath)}/\n`
- }
-
- const childIndent = " ".repeat(depth + 1)
- const children = Array.from(dirs)
- .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
- .sort()
-
- // Render subdirectories first
- for (const child of children) {
- output += renderDir(child, depth + 1)
- }
-
- // Render files
- const files = filesByDir.get(dirPath) || []
- for (const file of files.sort()) {
- output += `${childIndent}${file}\n`
- }
-
- return output
- }
-
- const output = `${searchPath}/\n` + renderDir(".", 0)
+export const ListTool = Tool.defineEffect(
+ "list",
+ Effect.gen(function* () {
+ const rg = yield* Ripgrep.Service
return {
- title: path.relative(Instance.worktree, searchPath),
- metadata: {
- count: files.length,
- truncated: files.length >= LIMIT,
- },
- output,
+ description: DESCRIPTION,
+ parameters: z.object({
+ path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
+ ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
+ }),
+ execute: (params: { path?: string; ignore?: string[] }, ctx: Tool.Context) =>
+ Effect.gen(function* () {
+ const searchPath = path.resolve(Instance.directory, params.path || ".")
+ yield* assertExternalDirectoryEffect(ctx, searchPath, { kind: "directory" })
+
+ yield* Effect.promise(() =>
+ ctx.ask({
+ permission: "list",
+ patterns: [searchPath],
+ always: ["*"],
+ metadata: {
+ path: searchPath,
+ },
+ }),
+ )
+
+ const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
+ const files = yield* rg.files({ cwd: searchPath, glob: ignoreGlobs }).pipe(
+ Stream.take(LIMIT),
+ Stream.runCollect,
+ Effect.map((chunk) => [...chunk]),
+ )
+
+ // Build directory structure
+ const dirs = new Set<string>()
+ const filesByDir = new Map<string, string[]>()
+
+ for (const file of files) {
+ const dir = path.dirname(file)
+ const parts = dir === "." ? [] : dir.split("/")
+
+ // Add all parent directories
+ for (let i = 0; i <= parts.length; i++) {
+ const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
+ dirs.add(dirPath)
+ }
+
+ // Add file to its directory
+ if (!filesByDir.has(dir)) filesByDir.set(dir, [])
+ filesByDir.get(dir)!.push(path.basename(file))
+ }
+
+ function renderDir(dirPath: string, depth: number): string {
+ const indent = " ".repeat(depth)
+ let output = ""
+
+ if (depth > 0) {
+ output += `${indent}${path.basename(dirPath)}/\n`
+ }
+
+ const childIndent = " ".repeat(depth + 1)
+ const children = Array.from(dirs)
+ .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
+ .sort()
+
+ // Render subdirectories first
+ for (const child of children) {
+ output += renderDir(child, depth + 1)
+ }
+
+ // Render files
+ const files = filesByDir.get(dirPath) || []
+ for (const file of files.sort()) {
+ output += `${childIndent}${file}\n`
+ }
+
+ return output
+ }
+
+ const output = `${searchPath}/\n` + renderDir(".", 0)
+
+ return {
+ title: path.relative(Instance.worktree, searchPath),
+ metadata: {
+ count: files.length,
+ truncated: files.length >= LIMIT,
+ },
+ output,
+ }
+ }).pipe(Effect.orDie, Effect.runPromise),
}
- },
-})
+ }),
+)