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:
@@ -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`,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user