diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index bf4a1f9edd4..9f12d2fef98 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -70,7 +70,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { } const directories = async (filter: string) => { - const query = normalizeQuery(filter.trim()) + const query = normalizeQuery((filter ?? "").trim()) return fetchDirs(query) } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 52457515b8e..a628a67fe4b 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1962,20 +1962,56 @@ export namespace Server { validator( "query", z.object({ - query: z.string(), + directory: z.string().optional(), + query: z.string().optional().default(""), dirs: z.enum(["true", "false"]).optional(), type: z.enum(["file", "directory"]).optional(), limit: z.coerce.number().int().min(1).max(200).optional(), }), ), async (c) => { - const query = c.req.valid("query").query + const directory = c.req.valid("query").directory + const query = c.req.valid("query").query ?? "" const dirs = c.req.valid("query").dirs const type = c.req.valid("query").type - const limit = c.req.valid("query").limit + const limit = c.req.valid("query").limit ?? 10 + + if (directory && type === "directory") { + const fs = await import("fs") + const path = await import("path") + const fuzzysort = await import("fuzzysort") + + const scanDirs = async (base: string, depth: number = 0): Promise => { + if (depth > 2) return [] + const results: string[] = [] + try { + const entries = await fs.promises.readdir(base, { withFileTypes: true }) + for (const entry of entries) { + if (!entry.isDirectory()) continue + if (entry.name.startsWith(".")) continue + if (["node_modules", "dist", "build", ".git"].includes(entry.name)) continue + const rel = path.relative(directory, path.join(base, entry.name)) + results.push(rel + "/") + if (depth < 1) { + const children = await scanDirs(path.join(base, entry.name), depth + 1) + results.push(...children) + } + } + } catch {} + return results + } + + const allDirs = await scanDirs(directory) + if (!query) { + return c.json(allDirs.slice(0, limit)) + } + const sorted = fuzzysort.go(query, allDirs, { limit }).map((r) => r.target) + return c.json(sorted) + } + const results = await File.search({ query, - limit: limit ?? 10, + limit, dirs: dirs !== "false", type, }) diff --git a/packages/ui/src/hooks/use-filtered-list.tsx b/packages/ui/src/hooks/use-filtered-list.tsx index b6bd7d5c6c0..9391a683856 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -23,20 +23,21 @@ export function useFilteredList(props: FilteredListProps) { const [grouped, { refetch }] = createResource( () => { - // When items is a function (not async filter function), call it to track changes - const itemsValue = - typeof props.items === "function" - ? (props.items as () => T[])() // Call synchronous function to track it - : props.items - return { filter: store.filter, - items: itemsValue, + items: Array.isArray(props.items) ? props.items : undefined, } }, async ({ filter, items }) => { const needle = filter?.toLowerCase() - const all = (items ?? (await (props.items as (filter: string) => T[] | Promise)(needle))) || [] + let all: T[] + if (items !== undefined) { + all = items + } else if (typeof props.items === "function") { + all = (await props.items(needle ?? "")) || [] + } else { + all = [] + } const result = pipe( all, (x) => {