import { createStore, produce, reconcile } from "solid-js/store" import type { FileNode } from "@opencode-ai/sdk/v2" type DirectoryState = { expanded: boolean loaded?: boolean loading?: boolean error?: string children?: string[] } type TreeStoreOptions = { scope: () => string normalizeDir: (input: string) => string list: (input: string) => Promise onError: (message: string) => void } export function createFileTreeStore(options: TreeStoreOptions) { const [tree, setTree] = createStore<{ node: Record dir: Record }>({ node: {}, dir: { "": { expanded: true } }, }) const inflight = new Map>() const reset = () => { inflight.clear() setTree("node", reconcile({})) setTree("dir", reconcile({})) setTree("dir", "", { expanded: true }) } const ensureDir = (path: string) => { if (tree.dir[path]) return setTree("dir", path, { expanded: false }) } const listDir = (input: string, opts?: { force?: boolean }) => { const dir = options.normalizeDir(input) ensureDir(dir) const current = tree.dir[dir] if (!opts?.force && current?.loaded) return Promise.resolve() const pending = inflight.get(dir) if (pending) return pending setTree( "dir", dir, produce((draft) => { draft.loading = true draft.error = undefined }), ) const directory = options.scope() const promise = options .list(dir) .then((nodes) => { if (options.scope() !== directory) return const prevChildren = tree.dir[dir]?.children ?? [] const nextChildren = nodes.map((node) => node.path) const nextSet = new Set(nextChildren) setTree( "node", produce((draft) => { const removedDirs: string[] = [] for (const child of prevChildren) { if (nextSet.has(child)) continue const existing = draft[child] if (existing?.type === "directory") removedDirs.push(child) delete draft[child] } if (removedDirs.length > 0) { const keys = Object.keys(draft) for (const key of keys) { for (const removed of removedDirs) { if (!key.startsWith(removed + "/")) continue delete draft[key] break } } } for (const node of nodes) { draft[node.path] = node } }), ) setTree( "dir", dir, produce((draft) => { draft.loaded = true draft.loading = false draft.children = nextChildren }), ) }) .catch((e) => { if (options.scope() !== directory) return setTree( "dir", dir, produce((draft) => { draft.loading = false draft.error = e.message }), ) options.onError(e.message) }) .finally(() => { inflight.delete(dir) }) inflight.set(dir, promise) return promise } const expandDir = (input: string) => { const dir = options.normalizeDir(input) ensureDir(dir) setTree("dir", dir, "expanded", true) void listDir(dir) } const collapseDir = (input: string) => { const dir = options.normalizeDir(input) ensureDir(dir) setTree("dir", dir, "expanded", false) } const dirState = (input: string) => { const dir = options.normalizeDir(input) return tree.dir[dir] } const children = (input: string) => { const dir = options.normalizeDir(input) const ids = tree.dir[dir]?.children if (!ids) return [] const out: FileNode[] = [] for (const id of ids) { const node = tree.node[id] if (node) out.push(node) } return out } return { listDir, expandDir, collapseDir, dirState, children, node: (path: string) => tree.node[path], isLoaded: (path: string) => Boolean(tree.dir[path]?.loaded), reset, } }