diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index a3fc0c43..013caa11 100644 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -1,6 +1,9 @@ import { ipcMain, BrowserWindow } from "electron" import { processManager } from "./process-manager" import { randomBytes } from "crypto" +import * as fs from "fs" +import * as path from "path" +import ignore from "ignore" interface Instance { id: string @@ -80,4 +83,49 @@ export function setupInstanceIPC(mainWindow: BrowserWindow) { ipcMain.handle("instance:list", async () => { return Array.from(instances.values()) }) + + ipcMain.handle("fs:scanDirectory", async (event, workspaceFolder: string) => { + const ig = ignore() + ig.add([".git", "node_modules"]) + + const gitignorePath = path.join(workspaceFolder, ".gitignore") + if (fs.existsSync(gitignorePath)) { + const content = fs.readFileSync(gitignorePath, "utf-8") + ig.add(content) + } + + function scanDir(dirPath: string, baseDir: string): string[] { + const results: string[] = [] + + try { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name) + const relativePath = path.relative(baseDir, fullPath) + + if (ig.ignores(relativePath)) { + continue + } + + if (entry.isDirectory()) { + const dirWithSlash = relativePath + "/" + if (!ig.ignores(dirWithSlash)) { + results.push(dirWithSlash) + const subFiles = scanDir(fullPath, baseDir) + results.push(...subFiles) + } + } else { + results.push(relativePath) + } + } + } catch (error) { + console.warn(`Error scanning ${dirPath}:`, error) + } + + return results + } + + return scanDir(workspaceFolder, workspaceFolder) + }) } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ab9dc856..589ea9f9 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -14,6 +14,7 @@ export interface ElectronAPI { }) => void, ) => void onNewInstance: (callback: () => void) => void + scanDirectory: (workspaceFolder: string) => Promise } const electronAPI: ElectronAPI = { @@ -35,6 +36,7 @@ const electronAPI: ElectronAPI = { onNewInstance: (callback) => { ipcRenderer.on("menu:newInstance", () => callback()) }, + scanDirectory: (workspaceFolder: string) => ipcRenderer.invoke("fs:scanDirectory", workspaceFolder), } contextBridge.exposeInMainWorld("electronAPI", electronAPI) diff --git a/package.json b/package.json index 4db9e142..488e1a39 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@opencode-ai/sdk": "0.15.13", "@solidjs/router": "^0.13.0", "electron": "38.4.0", + "ignore": "7.0.5", "lucide-solid": "^0.300.0", "marked": "^12.0.0", "shiki": "^3.13.0", diff --git a/src/components/file-picker.tsx b/src/components/file-picker.tsx index 4f9b6273..5a6db0f1 100644 --- a/src/components/file-picker.tsx +++ b/src/components/file-picker.tsx @@ -1,6 +1,4 @@ import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js" -import * as fs from "fs" -import * as path from "path" interface FileItem { path: string @@ -26,86 +24,9 @@ const FilePicker: Component = (props) => { const [loading, setLoading] = createSignal(false) const [allFiles, setAllFiles] = createSignal([]) const [isInitialized, setIsInitialized] = createSignal(false) - const [gitignorePatterns, setGitignorePatterns] = createSignal>(new Set()) let containerRef: HTMLDivElement | undefined - - async function loadGitignore() { - try { - const gitignorePath = path.join(props.workspaceFolder, ".gitignore") - if (fs.existsSync(gitignorePath)) { - const content = fs.readFileSync(gitignorePath, "utf-8") - const patterns = new Set( - content - .split("\n") - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")), - ) - setGitignorePatterns(patterns) - console.log(`[FilePicker] Loaded ${patterns.size} gitignore patterns`) - } - } catch (error) { - console.warn("[FilePicker] Could not load .gitignore:", error) - } - } - - function isIgnored(relativePath: string): boolean { - const patterns = gitignorePatterns() - for (const pattern of patterns) { - if (pattern.endsWith("/") && relativePath.startsWith(pattern)) { - return true - } - if (relativePath === pattern || relativePath.startsWith(pattern + "/")) { - return true - } - if (pattern.includes("*")) { - const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$") - if (regex.test(relativePath)) { - return true - } - } - } - return false - } - - async function scanDirectory(dirPath: string, baseDir: string): Promise { - const results: FileItem[] = [] - - try { - const entries = fs.readdirSync(dirPath, { withFileTypes: true }) - - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name) - const relativePath = path.relative(baseDir, fullPath) - - if (entry.name === ".git" || entry.name === "node_modules") { - continue - } - - if (isIgnored(relativePath)) { - continue - } - - if (entry.isDirectory()) { - results.push({ - path: relativePath + "/", - isGitFile: false, - }) - const subFiles = await scanDirectory(fullPath, baseDir) - results.push(...subFiles) - } else { - results.push({ - path: relativePath, - isGitFile: false, - }) - } - } - } catch (error) { - console.warn(`[FilePicker] Error scanning ${dirPath}:`, error) - } - - return results - } + let scrollContainerRef: HTMLDivElement | undefined async function fetchFiles(searchQuery: string) { console.log(`[FilePicker] Fetching files for query: "${searchQuery}"`) @@ -113,9 +34,12 @@ const FilePicker: Component = (props) => { try { if (allFiles().length === 0) { - await loadGitignore() console.log(`[FilePicker] Scanning workspace: ${props.workspaceFolder}`) - const scannedFiles = await scanDirectory(props.workspaceFolder, 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`) } @@ -127,6 +51,12 @@ const FilePicker: Component = (props) => { 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([]) @@ -231,7 +161,7 @@ const FilePicker: Component = (props) => { class="absolute bottom-full left-0 mb-2 w-full max-w-2xl rounded-lg border border-gray-300 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-900" style={{ "z-index": 100 }} > -
+