Restore fuzzy project folder search and desktop browsing (#1968)

* fix(projects): restore folder search and browsing

* fix(projects): preserve fuzzy search edge cases

* test(projects): move picker cleanup into fixture

* fix(projects): preserve absolute path descendants

* test(server): compare canonical search paths

* fix(projects): anchor root path suggestions

* fix(projects): anchor Windows root suggestions

* fix(projects): anchor directory search paths

* test(server): canonicalize directory search root

* fix(app): anchor rooted basename suggestions

* refactor(search): unify directory suggestions

Use one parameterized daemon search engine for project paths and workspace entries. Keep local recommendations as a small client overlay and remove unnecessary response metadata.

* fix(search): preserve filesystem root semantics

Accept absolute queries through configured root aliases while keeping traversal and output canonical. Keep blank suffix browsing matcher-independent and avoid metadata-keyed listing caches where Windows cannot invalidate them reliably.
This commit is contained in:
Mohamed Boudra
2026-07-10 17:52:55 +08:00
committed by GitHub
parent 1f0285c06b
commit 61a9117a6b
30 changed files with 1552 additions and 1465 deletions
@@ -1,5 +1,5 @@
import { test, expect, beforeAll, afterAll } from "vitest";
import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
import { mkdirSync, mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { tmpdir, homedir } from "node:os";
import path from "node:path";
@@ -831,6 +831,7 @@ test("update_agent persists unloaded title and labels across auto-unarchive", as
test("returns home-scoped directory suggestions", async () => {
const insideHomeDir = mkdtempSync(path.join(homedir(), "paseo-dir-suggestion-"));
const rootBrowseDir = mkdtempSync(path.join(homedir(), "000-paseo-root-browse-"));
const outsideHomeDir = mkdtempSync(path.join(tmpdir(), "paseo-dir-suggestion-outside-"));
try {
@@ -842,6 +843,17 @@ test("returns home-scoped directory suggestions", async () => {
expect(insideResult.error).toBeNull();
expect(insideResult.directories).toContain(insideHomeDir);
const rootBrowseResult = await ctx.client.getDirectorySuggestions({
query: "~",
limit: 100,
});
expect(rootBrowseResult.error).toBeNull();
expect(rootBrowseResult.directories).toContain(rootBrowseDir);
const blankResult = await ctx.client.getDirectorySuggestions({ query: "", limit: 100 });
expect(blankResult.error).toBeNull();
expect(blankResult.entries).toEqual([]);
const outsideQuery = path.basename(outsideHomeDir);
const outsideResult = await ctx.client.getDirectorySuggestions({
query: outsideQuery,
@@ -851,10 +863,35 @@ test("returns home-scoped directory suggestions", async () => {
expect(outsideResult.directories).not.toContain(outsideHomeDir);
} finally {
rmSync(insideHomeDir, { recursive: true, force: true });
rmSync(rootBrowseDir, { recursive: true, force: true });
rmSync(outsideHomeDir, { recursive: true, force: true });
}
}, 30000);
test("returns typed relative suggestions within a requested directory", async () => {
const cwd = mkdtempSync(path.join(tmpdir(), "paseo-workspace-suggestion-"));
const target = path.join(cwd, "src", "components", "message-renderer.tsx");
try {
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, "");
const result = await ctx.client.getDirectorySuggestions({
cwd,
query: "msgrndr",
includeFiles: true,
includeDirectories: false,
limit: 20,
});
expect(result.error).toBeNull();
expect(result.directories).toEqual([]);
expect(result.entries).toEqual([{ path: "src/components/message-renderer.tsx", kind: "file" }]);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
}, 30000);
test("receives server_info on websocket connect", async () => {
const client = new DaemonClient({
url: `ws://127.0.0.1:${ctx.daemon.port}/ws`,
+26 -17
View File
@@ -174,7 +174,7 @@ import {
type AgentUpdatesService,
} from "./session/agent-updates/agent-updates-service.js";
import { expandTilde } from "../utils/path.js";
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
import { searchDirectoryEntries } from "../utils/directory-suggestions.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
import type { Resolvable } from "./speech/provider-resolver.js";
import type { SpeechReadinessSnapshot } from "./speech/speech-runtime.js";
@@ -223,6 +223,14 @@ import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dis
// the entire session message if they encounter an unknown provider.
const LEGACY_PROVIDER_IDS = new Set(["claude", "codex", "opencode"]);
const MIN_VERSION_ALL_PROVIDERS = "0.1.45";
const WORKSPACE_SEARCH_HIDDEN_DIRECTORIES = [
".agents",
".claude",
".codex",
".github",
".paseo",
".vscode",
];
function errorToFriendlyMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -3049,22 +3057,23 @@ export class Session {
try {
const workspaceCwd = cwd?.trim();
const entries = workspaceCwd
? await searchWorkspaceEntries({
cwd: expandTilde(workspaceCwd),
query,
limit,
includeFiles,
includeDirectories,
matchMode,
})
: (
await searchHomeDirectories({
homeDir: process.env.HOME ?? homedir(),
query,
limit,
})
).map((path) => ({ path, kind: "directory" as const }));
const searchesWorkspace = Boolean(workspaceCwd);
const entries = await searchDirectoryEntries({
root: workspaceCwd ? expandTilde(workspaceCwd) : (process.env.HOME ?? homedir()),
query,
pathFormat: searchesWorkspace ? "relative" : "absolute",
pathQueryPolicy: searchesWorkspace ? "slashes" : "rooted",
blankQueryBehavior: searchesWorkspace ? "children" : "none",
rootAliases: searchesWorkspace ? [] : ["~"],
traversableHiddenDirectoryNames: searchesWorkspace
? WORKSPACE_SEARCH_HIDDEN_DIRECTORIES
: [],
confidentResultScanThreshold: searchesWorkspace ? undefined : 5_000,
includeFiles,
includeDirectories,
matchMode,
limit,
});
const directories = entries
.filter((entry) => entry.kind === "directory")
.map((entry) => entry.path);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff