fix(explorer): keep filename search results complete

This commit is contained in:
Neil
2026-09-18 03:29:19 -07:00
parent 931ea88058
commit ee3df6a7e6
5 changed files with 70 additions and 29 deletions
@@ -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<AppState>)
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()
}
})
})
@@ -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<string | null>(null)
const [truncated, setTruncated] = useState(false)
const [resolvedQuery, setResolvedQuery] = useState<string | null | undefined>(undefined)
const [listedOperationOwner, setListedOperationOwner] = useState<FileExplorerOperationOwner>({
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
}
}
@@ -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
]
@@ -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 }) => {
@@ -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,