mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
fix(ssh): support Cmd+P file search for non-git folders without ripgrep
When SSHed into a remote machine that lacks ripgrep, quick-open for non-git directories returned zero results because the only fallback (git ls-files) silently fails outside a git repo. - Add readdir-based file listing fallback in the relay for when neither rg nor git is applicable - Return [] instead of throwing in worktrees:list when SSH provider is unavailable (matches worktrees:listAll behavior, preserves cached data via the renderer's empty-array guard) - Return [] from fs:listFiles when SSH filesystem provider is missing so quick-open shows "No matching files" instead of an error banner
This commit is contained in:
@@ -440,8 +440,12 @@ export function registerFilesystemHandlers(store: Store): void {
|
||||
): Promise<string[]> => {
|
||||
if (args.connectionId) {
|
||||
const provider = getSshFilesystemProvider(args.connectionId)
|
||||
// Why: when the SSH connection is not yet established (cold start) or
|
||||
// temporarily disconnected, return [] so quick-open shows "No matching
|
||||
// files" instead of an error banner. The file list will repopulate when
|
||||
// the user re-opens quick-open after the connection is restored.
|
||||
if (!provider) {
|
||||
throw new Error(`No filesystem provider for connection "${args.connectionId}"`)
|
||||
return []
|
||||
}
|
||||
return provider.listFiles(args.rootPath)
|
||||
}
|
||||
|
||||
@@ -99,11 +99,14 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store
|
||||
gitWorktrees = [createFolderWorktree(repo)]
|
||||
} else if (repo.connectionId) {
|
||||
const provider = getSshGitProvider(repo.connectionId)
|
||||
// Why: when SSH is disconnected the provider is null. Throwing here
|
||||
// makes the renderer's fetchWorktrees catch block preserve its cached
|
||||
// worktree list instead of replacing it with an empty array.
|
||||
// Why: when SSH is disconnected the provider is null. Return [] so the
|
||||
// renderer's fetchWorktrees guard (`worktrees.length === 0 && current.length > 0`)
|
||||
// preserves its cached worktree list. This avoids a console error on every
|
||||
// fetchAllWorktrees cycle while the connection is being (re-)established —
|
||||
// worktrees will be properly populated when the SSH `connected` event fires
|
||||
// and triggers a re-fetch.
|
||||
if (!provider) {
|
||||
throw new Error(`SSH connection "${repo.connectionId}" is not active`)
|
||||
return []
|
||||
}
|
||||
gitWorktrees = await provider.listWorktrees(repo.path)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Plain readdir-based file listing fallback.
|
||||
*
|
||||
* Why: when neither ripgrep nor git is available (e.g. a non-git folder on a
|
||||
* remote machine without rg), we still need to list files for quick-open.
|
||||
* This walks the directory tree using Node's fs.readdir, respecting the same
|
||||
* blocklist and timeout constraints as the git-based fallback.
|
||||
*/
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join, relative } from 'path'
|
||||
|
||||
const MAX_FILES = 10_000
|
||||
const TIMEOUT_MS = 10_000
|
||||
|
||||
const HIDDEN_DIR_BLOCKLIST = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'.nuxt',
|
||||
'.cache',
|
||||
'.stably',
|
||||
'.vscode',
|
||||
'.idea',
|
||||
'.yarn',
|
||||
'.pnpm-store',
|
||||
'.terraform',
|
||||
'.docker',
|
||||
'.husky'
|
||||
])
|
||||
|
||||
function shouldDescend(name: string): boolean {
|
||||
if (name === 'node_modules' || HIDDEN_DIR_BLOCKLIST.has(name)) {
|
||||
return false
|
||||
}
|
||||
// Skip hidden directories (dotfiles other than those in the blocklist
|
||||
// are already covered, but be explicit for any remaining)
|
||||
if (name.startsWith('.') && HIDDEN_DIR_BLOCKLIST.has(name)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively list files under `rootPath` using fs.readdir.
|
||||
* Returns relative POSIX paths, capped at MAX_FILES with a timeout.
|
||||
*/
|
||||
export async function listFilesWithReaddir(rootPath: string): Promise<string[]> {
|
||||
const files: string[] = []
|
||||
const deadline = Date.now() + TIMEOUT_MS
|
||||
|
||||
async function walk(dir: string): Promise<void> {
|
||||
if (files.length >= MAX_FILES || Date.now() > deadline) {
|
||||
return
|
||||
}
|
||||
|
||||
let entries
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
// Permission denied, symlink loop, etc. — skip silently.
|
||||
return
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (files.length >= MAX_FILES || Date.now() > deadline) {
|
||||
return
|
||||
}
|
||||
|
||||
const name = entry.name
|
||||
if (entry.isDirectory()) {
|
||||
if (shouldDescend(name)) {
|
||||
await walk(join(dir, name))
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
const relPath = relative(rootPath, join(dir, name))
|
||||
files.push(relPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(rootPath)
|
||||
return files
|
||||
}
|
||||
+12
-3
@@ -24,6 +24,7 @@ import {
|
||||
checkRgAvailable
|
||||
} from './fs-handler-utils'
|
||||
import { listFilesWithGit, searchWithGitGrep } from './fs-handler-git-fallback'
|
||||
import { listFilesWithReaddir } from './fs-handler-readdir-fallback'
|
||||
|
||||
type WatchState = {
|
||||
rootPath: string
|
||||
@@ -228,10 +229,18 @@ export class FsHandler {
|
||||
const rootPath = expandTilde(params.rootPath as string)
|
||||
await this.context.validatePathResolved(rootPath)
|
||||
const rgAvailable = await checkRgAvailable()
|
||||
if (!rgAvailable) {
|
||||
return listFilesWithGit(rootPath)
|
||||
if (rgAvailable) {
|
||||
return listFilesWithRg(rootPath)
|
||||
}
|
||||
return listFilesWithRg(rootPath)
|
||||
// Why: git ls-files only works inside git repos. For non-git directories
|
||||
// (e.g. a plain folder added over SSH), the command exits with an error
|
||||
// and returns nothing. Fall back to a recursive readdir walk so quick-open
|
||||
// still populates for non-git folders without ripgrep.
|
||||
const gitFiles = await listFilesWithGit(rootPath)
|
||||
if (gitFiles.length > 0) {
|
||||
return gitFiles
|
||||
}
|
||||
return listFilesWithReaddir(rootPath)
|
||||
}
|
||||
|
||||
private async watch(params: Record<string, unknown>) {
|
||||
|
||||
Reference in New Issue
Block a user