From a244dd9eee185954a95a9d364d0a6b5c8dc7da79 Mon Sep 17 00:00:00 2001 From: Owen <33508519+lxxonx@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:04:43 +0900 Subject: [PATCH] fix: Open project dialog shows directories properly in web UI - Make query parameter optional in /find/file endpoint (default to empty string) - Add directory parameter support to search from specified path - Fix useFilteredList to properly handle async items functions with null safety - Add null-safety for filter parameter in dialog-select-directory Fixes #8325 --- .../components/dialog-select-directory.tsx | 2 +- packages/opencode/src/server/server.ts | 44 +++++++++++++++++-- packages/ui/src/hooks/use-filtered-list.tsx | 9 +++- 3 files changed, 49 insertions(+), 6 deletions(-) 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 26215e93cb4..13a9842c58e 100644 --- a/packages/ui/src/hooks/use-filtered-list.tsx +++ b/packages/ui/src/hooks/use-filtered-list.tsx @@ -33,7 +33,14 @@ export function useFilteredList(props: FilteredListProps) { }), 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 = Array.isArray(items) ? items : [] + } else if (typeof props.items === "function") { + all = (await props.items(needle ?? "")) || [] + } else { + all = [] + } const result = pipe( all, (x) => {