import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js" import type { OpencodeClient } from "@opencode-ai/sdk/client" interface FileItem { path: string added?: number removed?: number isGitFile: boolean } interface FilePickerProps { open: boolean onSelect: (path: string) => void onNavigate: (direction: "up" | "down") => void onClose: () => void instanceClient: OpencodeClient searchQuery: string textareaRef?: HTMLTextAreaElement workspaceFolder: string } const FilePicker: Component = (props) => { const [files, setFiles] = createSignal([]) const [selectedIndex, setSelectedIndex] = createSignal(0) const [loading, setLoading] = createSignal(false) const [allFiles, setAllFiles] = createSignal([]) const [isInitialized, setIsInitialized] = createSignal(false) let containerRef: HTMLDivElement | undefined let scrollContainerRef: HTMLDivElement | undefined async function fetchFiles(searchQuery: string) { console.log(`[FilePicker] Fetching files for query: "${searchQuery}"`) setLoading(true) try { if (allFiles().length === 0) { console.log(`[FilePicker] Scanning workspace: ${props.workspaceFolder}`) const scannedPaths = await window.electronAPI.scanDirectory(props.workspaceFolder) const scannedFiles: FileItem[] = scannedPaths.map((path) => ({ path, isGitFile: false, })) setAllFiles(scannedFiles) console.log(`[FilePicker] Found ${scannedFiles.length} files`) } const filteredFiles = searchQuery.trim() ? allFiles().filter((f) => f.path.toLowerCase().includes(searchQuery.toLowerCase())) : allFiles() console.log(`[FilePicker] Showing ${filteredFiles.length} files`) setFiles(filteredFiles) setSelectedIndex(0) setTimeout(() => { if (scrollContainerRef) { scrollContainerRef.scrollTop = 0 } }, 0) } catch (error) { console.error(`[FilePicker] Failed to fetch files:`, error) setFiles([]) } finally { setLoading(false) } } let lastQuery = "" createEffect(() => { console.log( `[FilePicker] Effect triggered - open: ${props.open}, query: "${props.searchQuery}", isInitialized: ${isInitialized()}`, ) if (props.open && !isInitialized()) { setIsInitialized(true) console.log("[FilePicker] First open - fetching files") fetchFiles(props.searchQuery) lastQuery = props.searchQuery return } if (props.open && props.searchQuery !== lastQuery) { console.log(`[FilePicker] Query changed from "${lastQuery}" to "${props.searchQuery}"`) lastQuery = props.searchQuery fetchFiles(props.searchQuery) } }) function scrollToSelected() { setTimeout(() => { const selectedElement = containerRef?.querySelector('[data-file-selected="true"]') if (selectedElement) { selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" }) } }, 0) } function handleSelect(path: string) { props.onSelect(path) } function handleNavigateUp() { setSelectedIndex((prev) => { const next = Math.max(prev - 1, 0) scrollToSelected() return next }) } function handleNavigateDown() { setSelectedIndex((prev) => { const next = Math.min(prev + 1, files().length - 1) scrollToSelected() return next }) } createEffect(() => { if (!props.open) return const listener = (e: KeyboardEvent) => { if (!props.open) return const fileList = files() if (e.key === "Escape") { e.preventDefault() e.stopPropagation() props.onClose() return } if (fileList.length === 0) return if (e.key === "ArrowDown") { e.preventDefault() e.stopPropagation() handleNavigateDown() props.onNavigate("down") } else if (e.key === "ArrowUp") { e.preventDefault() e.stopPropagation() handleNavigateUp() props.onNavigate("up") } else if (e.key === "Enter") { e.preventDefault() e.stopPropagation() if (fileList[selectedIndex()]) { handleSelect(fileList[selectedIndex()].path) } } } document.addEventListener("keydown", listener, true) onCleanup(() => document.removeEventListener("keydown", listener, true)) }) return ( ) } export default FilePicker