From ee3df6a7e692283ce729ad7e2fc7d8096551c487 Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 18 Sep 2026 03:29:19 -0700 Subject: [PATCH] fix(explorer): keep filename search results complete --- .../quick-open-file-list.react.test.tsx | 42 +++++++++---------- .../src/components/quick-open-file-list.ts | 15 +++++-- .../use-file-explorer-name-filter.ts | 11 +++-- ...runtime-file-client-search-listing.test.ts | 27 ++++++++++++ .../src/runtime/runtime-file-search-client.ts | 4 +- 5 files changed, 70 insertions(+), 29 deletions(-) diff --git a/src/renderer/src/components/quick-open-file-list.react.test.tsx b/src/renderer/src/components/quick-open-file-list.react.test.tsx index 2673e90af76..b94ddc041c1 100644 --- a/src/renderer/src/components/quick-open-file-list.react.test.tsx +++ b/src/renderer/src/components/quick-open-file-list.react.test.tsx @@ -270,7 +270,7 @@ describe('useRuntimeFileListForWorktree', () => { worktreeId: workspaceKey, worktreePath: '/srv/platform' }), - expect.objectContaining({ query: 'remote-folder', limit: 32 }) + expect.objectContaining({ query: 'remote-folder', limit: QUICK_OPEN_LISTING_MAX_RESULTS }) ) expect(listRuntimeFilesMock).not.toHaveBeenCalled() } finally { @@ -402,7 +402,7 @@ describe('useRuntimeFileListForWorktree', () => { }), { query: 'sta-4354-target', - limit: 32, + limit: QUICK_OPEN_LISTING_MAX_RESULTS, excludePaths: undefined, signal: expect.any(AbortSignal) } @@ -566,7 +566,8 @@ describe('useRuntimeFileListForWorktree', () => { } }) - it('does not restart local listings when only the query changes', async () => { + it('searches local workspaces by query instead of reusing the capped inventory', async () => { + vi.useFakeTimers() const workspaceKey = folderWorkspaceKey('folder-workspace-1') useAppStore.setState({ folderWorkspaces: [makeFolderWorkspace()], @@ -575,26 +576,25 @@ describe('useRuntimeFileListForWorktree', () => { worktreesByRepo: {} } as Partial) - const root = await renderProbe({ - enabled: true, - onState: () => {}, - query: 'one', - worktreeId: workspaceKey - }) - await waitForListRuntimeFilesCall() + try { + await renderProbe({ + enabled: true, + onState: () => {}, + query: 'AppDelegate.swift', + worktreeId: workspaceKey + }) + await act(async () => vi.advanceTimersByTimeAsync(120)) - await act(async () => { - root.render( - createElement(HookProbe, { - enabled: true, - onState: () => {}, - query: 'two', - worktreeId: workspaceKey + expect(searchRuntimeFilePathsMock).toHaveBeenCalledWith( + expect.objectContaining({ worktreePath: '/srv/platform' }), + expect.objectContaining({ + query: 'AppDelegate.swift', + limit: QUICK_OPEN_LISTING_MAX_RESULTS }) ) - }) - await flushEffects() - - expect(listRuntimeFilesMock).toHaveBeenCalledTimes(1) + expect(listRuntimeFilesMock).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } }) }) diff --git a/src/renderer/src/components/quick-open-file-list.ts b/src/renderer/src/components/quick-open-file-list.ts index 7f605ce2e41..a29a3741361 100644 --- a/src/renderer/src/components/quick-open-file-list.ts +++ b/src/renderer/src/components/quick-open-file-list.ts @@ -26,6 +26,8 @@ export type RuntimeFileListState = { loading: boolean loadError: string | null truncated?: boolean + /** Query that produced `files`; null means a request is still settling. */ + resolvedQuery?: string | null operationOwner?: FileExplorerOperationOwner } @@ -145,6 +147,7 @@ export function useRuntimeFileListForWorktree({ const [loading, setLoading] = useState(false) const [loadError, setLoadError] = useState(null) const [truncated, setTruncated] = useState(false) + const [resolvedQuery, setResolvedQuery] = useState(undefined) const [listedOperationOwner, setListedOperationOwner] = useState({ kind: 'unresolved' }) @@ -185,8 +188,7 @@ export function useRuntimeFileListForWorktree({ activeTargetStatus === 'connecting' || activeTargetStatus === 'deploying-relay' || activeTargetStatus === 'reconnecting' - const usesRuntimePathSearch = - (runtimeEnvironmentId !== null || connectionId !== undefined) && query !== undefined + const usesRuntimePathSearch = query !== undefined && operationRouteAvailable const remoteQuery = usesRuntimePathSearch ? query.trim() : '' const remoteQueryTooLarge = usesRuntimePathSearch && isQuickOpenRemoteQueryTooLarge(remoteQuery) const requestKey = useMemo( @@ -206,6 +208,7 @@ export function useRuntimeFileListForWorktree({ if (!enabled) { setLoading(false) setTruncated(false) + setResolvedQuery(null) setListedOperationOwner({ kind: 'unresolved' }) return } @@ -216,6 +219,7 @@ export function useRuntimeFileListForWorktree({ setLoadError(operationRouteAvailable ? null : getFileExplorerOwnerUnresolvedMessage()) setLoading(false) setTruncated(false) + setResolvedQuery(null) return } @@ -223,6 +227,7 @@ export function useRuntimeFileListForWorktree({ const requestKeyChanged = lastRequestKeyRef.current !== requestKey if (requestKeyChanged) { setFiles([]) + setResolvedQuery(null) } lastRequestKeyRef.current = requestKey setLoadError(null) @@ -231,6 +236,7 @@ export function useRuntimeFileListForWorktree({ if (usesRuntimePathSearch && (remoteQuery.length === 0 || remoteQueryTooLarge)) { setFiles([]) setLoading(false) + setResolvedQuery(remoteQuery) setListedOperationOwner(operationOwnerRef.current) return } @@ -252,7 +258,7 @@ export function useRuntimeFileListForWorktree({ ? debounceRuntimeFilePathSearch(120, requestAbortController.signal, () => searchRuntimeFilePaths(requestContext, { query: remoteQuery, - limit: 32, + limit: QUICK_OPEN_LISTING_MAX_RESULTS, excludePaths, ...(connectionId ? { requestToken } : {}), signal: requestAbortController.signal @@ -277,6 +283,7 @@ export function useRuntimeFileListForWorktree({ if (!cancelled) { setFiles(result.files) setTruncated(result.truncated) + setResolvedQuery(usesRuntimePathSearch ? remoteQuery : undefined) setListedOperationOwner(requestOperationOwner) } }) @@ -284,6 +291,7 @@ export function useRuntimeFileListForWorktree({ if (!cancelled) { setFiles([]) setTruncated(false) + setResolvedQuery(usesRuntimePathSearch ? remoteQuery : null) setLoadError(cleanRuntimeFileListError(error)) } }) @@ -323,6 +331,7 @@ export function useRuntimeFileListForWorktree({ loading: loading || connectionPending, loadError, truncated, + resolvedQuery, operationOwner: listedOperationOwner } } diff --git a/src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.ts b/src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.ts index 271e1c25cc9..883ca986abf 100644 --- a/src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.ts +++ b/src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.ts @@ -56,9 +56,13 @@ export function useFileExplorerNameFilter({ operationOwner: nameFilterFiles.operationOwner, relativePaths: nameFilterQueryTooLarge ? [] - : nameFilterFiles.loading && nameFilterFiles.files.length === 0 - ? null - : nameFilterFiles.files + : nameFilterFiles.resolvedQuery === nameFilterQuery.trim() + ? nameFilterFiles.loading + ? null + : nameFilterFiles.files + : nameFilterFiles.loading + ? null + : [] } : null, [ @@ -66,6 +70,7 @@ export function useFileExplorerNameFilter({ nameFilterFiles.files, nameFilterFiles.loading, nameFilterFiles.operationOwner, + nameFilterFiles.resolvedQuery, nameFilterQuery, nameFilterQueryTooLarge ] diff --git a/src/renderer/src/runtime/runtime-file-client-search-listing.test.ts b/src/renderer/src/runtime/runtime-file-client-search-listing.test.ts index 9126316665a..c86cfbd3cbe 100644 --- a/src/renderer/src/runtime/runtime-file-client-search-listing.test.ts +++ b/src/renderer/src/runtime/runtime-file-client-search-listing.test.ts @@ -205,6 +205,33 @@ describe('runtime file client', () => { expect(runtimeEnvironmentCall).not.toHaveBeenCalled() }) + it('searches local workspaces without requiring a connection owner', async () => { + fsListFiles.mockResolvedValue(['src/AppDelegate.swift', 'tests/AppDelegate.swift']) + + await expect( + searchRuntimeFilePaths( + { + settings: { activeRuntimeEnvironmentId: null }, + worktreeId: 'wt-1', + worktreePath: '/repo' + }, + { query: 'AppDelegate.swift', limit: 20_001 } + ) + ).resolves.toEqual({ + files: ['src/AppDelegate.swift', 'tests/AppDelegate.swift'], + truncated: false + }) + + expect(fsListFiles).toHaveBeenCalledWith({ + rootPath: '/repo', + connectionId: undefined, + excludePaths: undefined, + requestToken: undefined, + maxResults: 20_002, + searchQuery: 'AppDelegate.swift' + }) + }) + it('falls back to one cached legacy inventory and ranks evolving queries locally', async () => { replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1 }]) runtimeEnvironmentCall.mockImplementation(({ method }) => { diff --git a/src/renderer/src/runtime/runtime-file-search-client.ts b/src/renderer/src/runtime/runtime-file-search-client.ts index 8168b24bd54..e98de004959 100644 --- a/src/renderer/src/runtime/runtime-file-search-client.ts +++ b/src/renderer/src/runtime/runtime-file-search-client.ts @@ -90,7 +90,7 @@ export async function searchRuntimeFilePaths( ): Promise<{ files: string[]; truncated: boolean }> { const target = getActiveRuntimeTarget(context.settings) if (target.kind !== 'environment') { - if (!context.connectionId || !context.worktreePath) { + if (!context.worktreePath) { return { files: [], truncated: false } } const limit = args.limit ?? 32 @@ -108,7 +108,7 @@ export async function searchRuntimeFilePaths( return { files: [], truncated: false } } const worktreeSelector = toRuntimeWorktreeSelector(context.worktreeId) - const limit = args.limit ?? 32 + const limit = Math.min(args.limit ?? 32, 32) if (hasCachedLegacyQuickOpenInventory(target, worktreeSelector, context.worktreePath)) { return searchLegacyQuickOpenInventory({ target,