From 90ec263cee4f1bc2f6354ef9fc84045ad717305b Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 28 Mar 2026 21:25:05 -0700 Subject: [PATCH 1/5] fix workspace search regressions --- .../changes/utils/parse-status.test.ts | 1 + apps/desktop/src/main/windows/main.ts | 27 ++- .../workspace/$workspaceId/page.tsx | 32 +-- apps/desktop/src/shared/detect-language.ts | 1 + apps/desktop/src/shared/hotkeys.ts | 7 - packages/workspace-fs/src/search.test.ts | 73 +++++++ packages/workspace-fs/src/search.ts | 201 +++++++++++++++--- 7 files changed, 273 insertions(+), 69 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/changes/utils/parse-status.test.ts b/apps/desktop/src/lib/trpc/routers/changes/utils/parse-status.test.ts index d1c72efea3c..e9a481ca4cc 100644 --- a/apps/desktop/src/lib/trpc/routers/changes/utils/parse-status.test.ts +++ b/apps/desktop/src/lib/trpc/routers/changes/utils/parse-status.test.ts @@ -274,6 +274,7 @@ describe("detectLanguage", () => { test("detects web files", () => { expect(detectLanguage("index.html")).toBe("html"); + expect(detectLanguage("page.astro")).toBe("html"); expect(detectLanguage("styles.css")).toBe("css"); expect(detectLanguage("styles.scss")).toBe("scss"); }); diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index 9cfb1e3fa3c..a006240eb71 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -90,6 +90,7 @@ app.on("child-process-gone", (_event, details) => { export async function MainWindow() { const savedWindowState = loadWindowState(); const initialBounds = getInitialWindowBounds(savedWindowState); + let persistedZoomLevel = savedWindowState?.zoomLevel; const isDev = env.NODE_ENV === "development"; const workspaceName = isDev ? getEnvWorkspaceName() : undefined; @@ -225,6 +226,7 @@ export async function MainWindow() { // Gated by `initialized` so the initial maximize() doesn't immediately // write isMaximized: true back to disk before the user touches the window. let initialized = false; + let hasCompletedFirstLoad = false; let saveTimeout: ReturnType | null = null; const debouncedSave = () => { if (!initialized || window.isDestroyed()) return; @@ -243,21 +245,31 @@ export async function MainWindow() { isMaximized, zoomLevel: window.webContents.getZoomLevel(), }); + persistedZoomLevel = window.webContents.getZoomLevel(); }, 500); }; window.on("move", debouncedSave); window.on("resize", debouncedSave); + window.webContents.on("zoom-changed", () => { + persistedZoomLevel = window.webContents.getZoomLevel(); + debouncedSave(); + }); - window.webContents.once("did-finish-load", async () => { + window.webContents.on("did-finish-load", () => { console.log("[main-window] Renderer loaded successfully"); - if (initialBounds.isMaximized) { - window.maximize(); + + if (persistedZoomLevel !== undefined) { + window.webContents.setZoomLevel(persistedZoomLevel); } - if (savedWindowState?.zoomLevel !== undefined) { - window.webContents.setZoomLevel(savedWindowState.zoomLevel); + + if (!hasCompletedFirstLoad) { + if (initialBounds.isMaximized) { + window.maximize(); + } + window.show(); + initialized = true; + hasCompletedFirstLoad = true; } - window.show(); - initialized = true; }); window.webContents.on( @@ -295,6 +307,7 @@ export async function MainWindow() { isMaximized, zoomLevel, }); + persistedZoomLevel = zoomLevel; browserManager.unregisterAll(); server.close(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx index ed7656ab114..877e1056c4e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx @@ -16,10 +16,6 @@ import { CommandPalette, useCommandPalette, } from "renderer/screens/main/components/CommandPalette"; -import { - KeywordSearch, - useKeywordSearch, -} from "renderer/screens/main/components/KeywordSearch"; import { UnsavedChangesDialog } from "renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/UnsavedChangesDialog"; import { useWorkspaceFileEventBridge } from "renderer/screens/main/components/WorkspaceView/hooks/useWorkspaceFileEvents"; import { useWorkspaceRenameReconciliation } from "renderer/screens/main/components/WorkspaceView/hooks/useWorkspaceRenameReconciliation"; @@ -420,21 +416,10 @@ function WorkspacePage() { workspaceId, navigate, }); - const keywordSearch = useKeywordSearch({ - workspaceId, - }); const handleQuickOpen = useCallback(() => { - keywordSearch.handleOpenChange(false); commandPalette.toggle(); - }, [commandPalette.toggle, keywordSearch.handleOpenChange]); - const handleKeywordSearch = useCallback(() => { - commandPalette.handleOpenChange(false); - keywordSearch.toggle(); - }, [commandPalette.handleOpenChange, keywordSearch.toggle]); + }, [commandPalette.toggle]); useAppHotkey("QUICK_OPEN", handleQuickOpen, undefined, [handleQuickOpen]); - useAppHotkey("KEYWORD_SEARCH", handleKeywordSearch, undefined, [ - handleKeywordSearch, - ]); // Toggle changes sidebar (⌘L) useAppHotkey("TOGGLE_SIDEBAR", () => toggleSidebar(), undefined, [ @@ -678,21 +663,6 @@ function WorkspacePage() { : undefined } /> - { diff --git a/apps/desktop/src/shared/detect-language.ts b/apps/desktop/src/shared/detect-language.ts index 7f305cd2ba7..d4f42df7647 100644 --- a/apps/desktop/src/shared/detect-language.ts +++ b/apps/desktop/src/shared/detect-language.ts @@ -13,6 +13,7 @@ export function detectLanguage(filePath: string): string { // Web html: "html", htm: "html", + astro: "html", css: "css", scss: "scss", less: "less", diff --git a/apps/desktop/src/shared/hotkeys.ts b/apps/desktop/src/shared/hotkeys.ts index 5543cf388b0..d64340fc69a 100644 --- a/apps/desktop/src/shared/hotkeys.ts +++ b/apps/desktop/src/shared/hotkeys.ts @@ -784,13 +784,6 @@ export const HOTKEYS = { category: "Navigation", description: "Search and open files in the current workspace", }), - KEYWORD_SEARCH: defineHotkey({ - keys: "meta+shift+f", - label: "Keyword Search", - category: "Navigation", - description: - "Search for keyword matches across files in the current workspace", - }), // Chat FIND_IN_CHAT: defineHotkey({ diff --git a/packages/workspace-fs/src/search.test.ts b/packages/workspace-fs/src/search.test.ts index 0e4c589d9e8..add10cb5980 100644 --- a/packages/workspace-fs/src/search.test.ts +++ b/packages/workspace-fs/src/search.test.ts @@ -130,4 +130,77 @@ describe("patchSearchIndexesForRoot", () => { hiddenPath, ); }); + + it("rebuilds search indexes after a directory rename", async () => { + const rootPath = await createTempRoot(); + const oldDirectoryPath = path.join(rootPath, "old-dir"); + const newDirectoryPath = path.join(rootPath, "new-dir"); + const oldFilePath = path.join(oldDirectoryPath, "target.ts"); + const newFilePath = path.join(newDirectoryPath, "target.ts"); + + await fs.mkdir(oldDirectoryPath, { recursive: true }); + await fs.writeFile(oldFilePath, "export const target = 1;\n"); + + await searchFiles({ + rootPath, + query: "old-dir/target.ts", + }); + + await fs.rename(oldDirectoryPath, newDirectoryPath); + + patchSearchIndexesForRoot(rootPath, [ + createPatchEvent({ + kind: "rename", + absolutePath: newDirectoryPath, + oldAbsolutePath: oldDirectoryPath, + isDirectory: true, + }), + ]); + + const oldPathResults = await searchFiles({ + rootPath, + query: "old-dir/target.ts", + }); + const newPathResults = await searchFiles({ + rootPath, + query: "new-dir/target.ts", + }); + + expect( + oldPathResults.some( + (result) => result.relativePath === "old-dir/target.ts", + ), + ).toBeFalse(); + expect(newPathResults[0]?.absolutePath).toBe(newFilePath); + expect(newPathResults[0]?.relativePath).toBe("new-dir/target.ts"); + }); +}); + +describe("searchFiles", () => { + it("prioritizes exact filename matches ahead of fuzzy path matches", async () => { + const rootPath = await createTempRoot(); + const exactMatchPath = path.join(rootPath, "WorkspaceFiles.tsx"); + const fuzzyMatchPath = path.join(rootPath, "hooks", "useWorkspaceFiles.ts"); + + await fs.mkdir(path.dirname(fuzzyMatchPath), { recursive: true }); + await fs.writeFile(exactMatchPath, "export const exact = true;\n"); + await fs.writeFile(fuzzyMatchPath, "export const fuzzy = true;\n"); + + const results = await searchFiles({ + rootPath, + query: "WorkspaceFiles.tsx", + limit: 5, + }); + + expect(results[0]?.absolutePath).toBe(exactMatchPath); + expect(results).toHaveLength(1); + + const fuzzyResults = await searchFiles({ + rootPath, + query: "useWorkspaceFiles", + limit: 5, + }); + + expect(fuzzyResults[0]?.absolutePath).toBe(fuzzyMatchPath); + }); }); diff --git a/packages/workspace-fs/src/search.ts b/packages/workspace-fs/src/search.ts index 82cc60b892e..9d5e1c18837 100644 --- a/packages/workspace-fs/src/search.ts +++ b/packages/workspace-fs/src/search.ts @@ -32,11 +32,19 @@ interface SearchIndexEntry { absolutePath: string; relativePath: string; name: string; + lowerName: string; + lowerRelativePath: string; + compactName: string; + compactRelativePath: string; } interface FileSearchIndex { items: SearchIndexEntry[]; fuse: Fuse; + itemsByLowerName: Map; + itemsByCompactName: Map; + itemsByLowerRelativePath: Map; + itemsByCompactRelativePath: Map; } interface FileSearchCacheEntry { @@ -109,6 +117,8 @@ function createFileSearchFuse( keys: [ { name: "name", weight: 2 }, { name: "relativePath", weight: 1 }, + { name: "compactName", weight: 1.8 }, + { name: "compactRelativePath", weight: 0.9 }, ], threshold: 0.4, includeScore: true, @@ -116,6 +126,67 @@ function createFileSearchFuse( }); } +function normalizeSearchText(input: string): string { + return input.toLowerCase().replace(/[\\/\s._-]+/g, ""); +} + +function createSearchIndexEntry( + rootPath: string, + relativePath: string, +): SearchIndexEntry { + const absolutePath = path.join(rootPath, relativePath); + const name = path.basename(relativePath); + const lowerName = name.toLowerCase(); + const lowerRelativePath = relativePath.toLowerCase(); + + return { + absolutePath, + relativePath, + name, + lowerName, + lowerRelativePath, + compactName: normalizeSearchText(name), + compactRelativePath: normalizeSearchText(relativePath), + }; +} + +function addSearchIndexMapEntry( + index: Map, + key: string, + item: SearchIndexEntry, +): void { + const existing = index.get(key); + if (existing) { + existing.push(item); + return; + } + + index.set(key, [item]); +} + +function createFileSearchIndex(items: SearchIndexEntry[]): FileSearchIndex { + const itemsByLowerName = new Map(); + const itemsByCompactName = new Map(); + const itemsByLowerRelativePath = new Map(); + const itemsByCompactRelativePath = new Map(); + + for (const item of items) { + addSearchIndexMapEntry(itemsByLowerName, item.lowerName, item); + addSearchIndexMapEntry(itemsByCompactName, item.compactName, item); + itemsByLowerRelativePath.set(item.lowerRelativePath, item); + itemsByCompactRelativePath.set(item.compactRelativePath, item); + } + + return { + items, + fuse: createFileSearchFuse(items), + itemsByLowerName, + itemsByCompactName, + itemsByLowerRelativePath, + itemsByCompactRelativePath, + }; +} + function getSearchCacheKey({ rootPath, includeHidden, @@ -272,16 +343,11 @@ async function buildSearchIndex({ ignore: DEFAULT_IGNORE_PATTERNS, }); - const items: SearchIndexEntry[] = entries.map((relativePath) => ({ - absolutePath: path.join(normalizedRootPath, relativePath), - relativePath, - name: path.basename(relativePath), - })); + const items: SearchIndexEntry[] = entries.map((relativePath) => + createSearchIndexEntry(normalizedRootPath, relativePath), + ); - return { - items, - fuse: createFileSearchFuse(items), - }; + return createFileSearchIndex(items); } async function getSearchIndex( @@ -344,6 +410,78 @@ function safeSearchLimit(limit: number | undefined): number { return Math.max(1, Math.min(limit ?? 20, MAX_SEARCH_RESULTS)); } +function compareFileSearchMatches( + left: { item: SearchIndexEntry; score: number }, + right: { item: SearchIndexEntry; score: number }, +): number { + if (left.score !== right.score) { + return right.score - left.score; + } + + if (left.item.name.length !== right.item.name.length) { + return left.item.name.length - right.item.name.length; + } + + if (left.item.relativePath.length !== right.item.relativePath.length) { + return left.item.relativePath.length - right.item.relativePath.length; + } + + return left.item.relativePath.localeCompare(right.item.relativePath); +} + +function collectExactFileSearchMatches({ + index, + query, + pathMatcher, + limit, +}: { + index: FileSearchIndex; + query: string; + pathMatcher: PathFilterMatcher; + limit: number; +}): Array<{ item: SearchIndexEntry; score: number }> { + const lowerQuery = query.toLowerCase(); + const normalizedPathQuery = lowerQuery.replace(/\\/g, "/"); + const compactQuery = normalizeSearchText(query); + const matchesByPath = new Map< + string, + { item: SearchIndexEntry; score: number } + >(); + + const addMatches = ( + items: SearchIndexEntry[] | SearchIndexEntry | undefined, + score: number, + ): void => { + const candidates = Array.isArray(items) ? items : items ? [items] : []; + + for (const item of candidates) { + if ( + pathMatcher.hasFilters && + !matchesPathFilters(item.relativePath, pathMatcher) + ) { + continue; + } + + const existing = matchesByPath.get(item.absolutePath); + if (!existing || existing.score < score) { + matchesByPath.set(item.absolutePath, { item, score }); + } + } + }; + + addMatches(index.itemsByLowerName.get(lowerQuery), 1); + addMatches(index.itemsByLowerRelativePath.get(normalizedPathQuery), 0.995); + + if (compactQuery.length > 0) { + addMatches(index.itemsByCompactName.get(compactQuery), 0.99); + addMatches(index.itemsByCompactRelativePath.get(compactQuery), 0.985); + } + + return Array.from(matchesByPath.values()) + .sort(compareFileSearchMatches) + .slice(0, limit); +} + function isBinaryContent(buffer: Buffer): boolean { const checkLength = Math.min(buffer.length, BINARY_CHECK_SIZE); for (let index = 0; index < checkLength; index++) { @@ -678,11 +816,10 @@ function applySearchPatchEvent({ } const nextAbsolutePath = normalizeAbsolutePath(event.absolutePath); - itemsByPath.set(nextAbsolutePath, { - absolutePath: nextAbsolutePath, - relativePath: nextRelativePath, - name: path.basename(nextAbsolutePath), - }); + itemsByPath.set( + nextAbsolutePath, + createSearchIndexEntry(rootPath, nextRelativePath), + ); return; } @@ -698,11 +835,7 @@ function applySearchPatchEvent({ return; } - itemsByPath.set(absolutePath, { - absolutePath, - relativePath, - name: path.basename(absolutePath), - }); + itemsByPath.set(absolutePath, createSearchIndexEntry(rootPath, relativePath)); } export function invalidateSearchIndex(options: SearchIndexKeyOptions): void { @@ -738,6 +871,11 @@ export function patchSearchIndexesForRoot( return; } + if (events.some((event) => event.isDirectory)) { + invalidateSearchIndexesForRoot(rootPath); + return; + } + const normalizedRootPath = normalizeAbsolutePath(rootPath); for (const includeHidden of [true, false]) { @@ -772,10 +910,7 @@ export function patchSearchIndexesForRoot( const nextItems = Array.from(nextItemsByPath.values()); searchIndexCache.set(cacheKey, { - index: { - items: nextItems, - fuse: createFileSearchFuse(nextItems), - }, + index: createFileSearchIndex(nextItems), builtAt: Date.now(), }); } @@ -802,6 +937,24 @@ export async function searchFiles({ includePattern, excludePattern, }); + const safeLimit = safeSearchLimit(limit); + + const exactMatches = collectExactFileSearchMatches({ + index, + query: trimmedQuery, + pathMatcher, + limit: safeLimit, + }); + if (exactMatches.length > 0) { + return exactMatches.map((result) => ({ + absolutePath: result.item.absolutePath, + relativePath: result.item.relativePath, + name: result.item.name, + kind: "file" as const, + score: result.score, + })); + } + const searchableItems = pathMatcher.hasFilters ? index.items.filter((item) => matchesPathFilters(item.relativePath, pathMatcher), @@ -816,7 +969,7 @@ export async function searchFiles({ ? createFileSearchFuse(searchableItems) : index.fuse; const results = fuse.search(trimmedQuery, { - limit: safeSearchLimit(limit), + limit: safeLimit, }); return results.map((result) => ({ From 83745b511ab8325f26c5a6764411ba9338074c1e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 12:15:55 -0700 Subject: [PATCH 2/5] fix workspace-fs test matcher typing --- packages/workspace-fs/src/search.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/workspace-fs/src/search.test.ts b/packages/workspace-fs/src/search.test.ts index add10cb5980..4d1cba87646 100644 --- a/packages/workspace-fs/src/search.test.ts +++ b/packages/workspace-fs/src/search.test.ts @@ -170,9 +170,9 @@ describe("patchSearchIndexesForRoot", () => { oldPathResults.some( (result) => result.relativePath === "old-dir/target.ts", ), - ).toBeFalse(); - expect(newPathResults[0]?.absolutePath).toBe(newFilePath); - expect(newPathResults[0]?.relativePath).toBe("new-dir/target.ts"); + ).toEqual(false); + expect(newPathResults[0]?.absolutePath).toEqual(newFilePath); + expect(newPathResults[0]?.relativePath).toEqual("new-dir/target.ts"); }); }); @@ -192,7 +192,7 @@ describe("searchFiles", () => { limit: 5, }); - expect(results[0]?.absolutePath).toBe(exactMatchPath); + expect(results[0]?.absolutePath).toEqual(exactMatchPath); expect(results).toHaveLength(1); const fuzzyResults = await searchFiles({ @@ -201,6 +201,6 @@ describe("searchFiles", () => { limit: 5, }); - expect(fuzzyResults[0]?.absolutePath).toBe(fuzzyMatchPath); + expect(fuzzyResults[0]?.absolutePath).toEqual(fuzzyMatchPath); }); }); From 7ca4eb3e0767030e1e1fe4112bc633fd6166219b Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 14:49:31 -0700 Subject: [PATCH 3/5] fix compact path search collisions --- packages/workspace-fs/src/search.test.ts | 21 +++++++++++++++++++++ packages/workspace-fs/src/search.ts | 24 ++++++++++++++++-------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/workspace-fs/src/search.test.ts b/packages/workspace-fs/src/search.test.ts index 4d1cba87646..1caa7d688e3 100644 --- a/packages/workspace-fs/src/search.test.ts +++ b/packages/workspace-fs/src/search.test.ts @@ -203,4 +203,25 @@ describe("searchFiles", () => { expect(fuzzyResults[0]?.absolutePath).toEqual(fuzzyMatchPath); }); + + it("returns every compact path collision instead of dropping later entries", async () => { + const rootPath = await createTempRoot(); + const nestedPath = path.join(rootPath, "foo", "bar.ts"); + const flatPath = path.join(rootPath, "foo-bar.ts"); + + await fs.mkdir(path.dirname(nestedPath), { recursive: true }); + await fs.writeFile(nestedPath, "export const nested = true;\n"); + await fs.writeFile(flatPath, "export const flat = true;\n"); + + const results = await searchFiles({ + rootPath, + query: "foobarts", + limit: 5, + }); + + expect(results.map((result) => result.absolutePath)).toEqual([ + flatPath, + nestedPath, + ]); + }); }); diff --git a/packages/workspace-fs/src/search.ts b/packages/workspace-fs/src/search.ts index 9d5e1c18837..c50e9f3879e 100644 --- a/packages/workspace-fs/src/search.ts +++ b/packages/workspace-fs/src/search.ts @@ -43,8 +43,8 @@ interface FileSearchIndex { fuse: Fuse; itemsByLowerName: Map; itemsByCompactName: Map; - itemsByLowerRelativePath: Map; - itemsByCompactRelativePath: Map; + itemsByLowerRelativePath: Map; + itemsByCompactRelativePath: Map; } interface FileSearchCacheEntry { @@ -167,14 +167,22 @@ function addSearchIndexMapEntry( function createFileSearchIndex(items: SearchIndexEntry[]): FileSearchIndex { const itemsByLowerName = new Map(); const itemsByCompactName = new Map(); - const itemsByLowerRelativePath = new Map(); - const itemsByCompactRelativePath = new Map(); + const itemsByLowerRelativePath = new Map(); + const itemsByCompactRelativePath = new Map(); for (const item of items) { addSearchIndexMapEntry(itemsByLowerName, item.lowerName, item); addSearchIndexMapEntry(itemsByCompactName, item.compactName, item); - itemsByLowerRelativePath.set(item.lowerRelativePath, item); - itemsByCompactRelativePath.set(item.compactRelativePath, item); + addSearchIndexMapEntry( + itemsByLowerRelativePath, + item.lowerRelativePath, + item, + ); + addSearchIndexMapEntry( + itemsByCompactRelativePath, + item.compactRelativePath, + item, + ); } return { @@ -449,10 +457,10 @@ function collectExactFileSearchMatches({ >(); const addMatches = ( - items: SearchIndexEntry[] | SearchIndexEntry | undefined, + items: SearchIndexEntry[] | undefined, score: number, ): void => { - const candidates = Array.isArray(items) ? items : items ? [items] : []; + const candidates = items ?? []; for (const item of candidates) { if ( From c2a6123f126fb55c302e6b6f9e958387473dde1a Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 16:38:43 -0700 Subject: [PATCH 4/5] address review follow-up --- apps/desktop/src/main/windows/main.ts | 12 ++++++++---- packages/workspace-fs/src/search.test.ts | 17 +++++++++++++++++ packages/workspace-fs/src/search.ts | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index a006240eb71..e932fc634e2 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -237,22 +237,26 @@ export async function MainWindow() { const bounds = isMaximized ? window.getNormalBounds() : window.getBounds(); + const zoomLevel = window.webContents.getZoomLevel(); saveWindowState({ x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, isMaximized, - zoomLevel: window.webContents.getZoomLevel(), + zoomLevel, }); - persistedZoomLevel = window.webContents.getZoomLevel(); + persistedZoomLevel = zoomLevel; }, 500); }; window.on("move", debouncedSave); window.on("resize", debouncedSave); window.webContents.on("zoom-changed", () => { - persistedZoomLevel = window.webContents.getZoomLevel(); - debouncedSave(); + setTimeout(() => { + if (window.isDestroyed()) return; + persistedZoomLevel = window.webContents.getZoomLevel(); + debouncedSave(); + }, 0); }); window.webContents.on("did-finish-load", () => { diff --git a/packages/workspace-fs/src/search.test.ts b/packages/workspace-fs/src/search.test.ts index 1caa7d688e3..5e3be81e36e 100644 --- a/packages/workspace-fs/src/search.test.ts +++ b/packages/workspace-fs/src/search.test.ts @@ -204,6 +204,23 @@ describe("searchFiles", () => { expect(fuzzyResults[0]?.absolutePath).toEqual(fuzzyMatchPath); }); + it("normalizes exact relative path queries before lookup", async () => { + const rootPath = await createTempRoot(); + const targetPath = path.join(rootPath, "src", "file.ts"); + + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await fs.writeFile(targetPath, "export const value = true;\n"); + + const results = await searchFiles({ + rootPath, + query: "./src/file.ts", + limit: 5, + }); + + expect(results[0]?.absolutePath).toEqual(targetPath); + expect(results[0]?.relativePath).toEqual("src/file.ts"); + }); + it("returns every compact path collision instead of dropping later entries", async () => { const rootPath = await createTempRoot(); const nestedPath = path.join(rootPath, "foo", "bar.ts"); diff --git a/packages/workspace-fs/src/search.ts b/packages/workspace-fs/src/search.ts index c50e9f3879e..b224c305b7f 100644 --- a/packages/workspace-fs/src/search.ts +++ b/packages/workspace-fs/src/search.ts @@ -449,7 +449,7 @@ function collectExactFileSearchMatches({ limit: number; }): Array<{ item: SearchIndexEntry; score: number }> { const lowerQuery = query.toLowerCase(); - const normalizedPathQuery = lowerQuery.replace(/\\/g, "/"); + const normalizedPathQuery = normalizePathForGlob(lowerQuery); const compactQuery = normalizeSearchText(query); const matchesByPath = new Map< string, From 1484cf2815c630e9ab221d964b9465fcb52b414e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 16:51:56 -0700 Subject: [PATCH 5/5] canonicalize search index paths --- packages/workspace-fs/src/search.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/workspace-fs/src/search.ts b/packages/workspace-fs/src/search.ts index b224c305b7f..7defe3a5be4 100644 --- a/packages/workspace-fs/src/search.ts +++ b/packages/workspace-fs/src/search.ts @@ -134,19 +134,22 @@ function createSearchIndexEntry( rootPath: string, relativePath: string, ): SearchIndexEntry { - const absolutePath = path.join(rootPath, relativePath); - const name = path.basename(relativePath); + const normalizedRelativePath = normalizePathForGlob(relativePath); + const absolutePath = normalizeAbsolutePath( + path.join(rootPath, normalizedRelativePath), + ); + const name = path.basename(normalizedRelativePath); const lowerName = name.toLowerCase(); - const lowerRelativePath = relativePath.toLowerCase(); + const lowerRelativePath = normalizedRelativePath.toLowerCase(); return { absolutePath, - relativePath, + relativePath: normalizedRelativePath, name, lowerName, lowerRelativePath, compactName: normalizeSearchText(name), - compactRelativePath: normalizeSearchText(relativePath), + compactRelativePath: normalizeSearchText(normalizedRelativePath), }; }