mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(search): bind content results to runtime owner (#9262)
Capture the worktree and runtime route that produced each committed content-search result set, then reuse that owner for opens and retries. This prevents active-worktree and ambient-runtime changes from retargeting remote matches while preserving explicit local and SSH routing. Closes #9185.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { openMatchResult } from './search-match-open'
|
||||
|
||||
type IntendedResultOwner = {
|
||||
worktreeId: string
|
||||
runtimeEnvironmentId: string | null
|
||||
}
|
||||
|
||||
function openResult(resultOwner: IntendedResultOwner) {
|
||||
const openFile = vi.fn()
|
||||
const setPendingEditorReveal = vi.fn()
|
||||
const params = {
|
||||
resultOwner,
|
||||
fileResult: {
|
||||
filePath: '/owner/repo/src/example.ts',
|
||||
relativePath: 'src/example.ts',
|
||||
matches: []
|
||||
},
|
||||
match: {
|
||||
line: 7,
|
||||
column: 4,
|
||||
matchLength: 6,
|
||||
lineContent: 'const result = owner'
|
||||
},
|
||||
openFile,
|
||||
setPendingEditorReveal,
|
||||
revealRafRef: { current: null },
|
||||
revealInnerRafRef: { current: null }
|
||||
} satisfies Parameters<typeof openMatchResult>[0]
|
||||
|
||||
openMatchResult(params)
|
||||
return { openFile, setPendingEditorReveal }
|
||||
}
|
||||
|
||||
describe('openMatchResult', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal(
|
||||
'requestAnimationFrame',
|
||||
vi.fn(() => 1)
|
||||
)
|
||||
vi.stubGlobal('cancelAnimationFrame', vi.fn())
|
||||
})
|
||||
|
||||
it('opens with the remote owner captured by the search after the active worktree changes', () => {
|
||||
const { openFile } = openResult({
|
||||
worktreeId: 'worktree-that-produced-results',
|
||||
runtimeEnvironmentId: 'runtime-that-produced-results'
|
||||
})
|
||||
|
||||
expect(openFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
worktreeId: 'worktree-that-produced-results',
|
||||
runtimeEnvironmentId: 'runtime-that-produced-results'
|
||||
}),
|
||||
{ suppressActiveRuntimeFallback: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps an explicitly local result local when another runtime is active', () => {
|
||||
const { openFile } = openResult({
|
||||
worktreeId: 'local-worktree',
|
||||
runtimeEnvironmentId: null
|
||||
})
|
||||
|
||||
expect(openFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
worktreeId: 'local-worktree',
|
||||
runtimeEnvironmentId: null
|
||||
}),
|
||||
{ suppressActiveRuntimeFallback: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('does not guess an owner for results without a committed search source', () => {
|
||||
const openFile = vi.fn()
|
||||
|
||||
openMatchResult({
|
||||
resultOwner: null,
|
||||
fileResult: {
|
||||
filePath: '/unresolved/repo/file.ts',
|
||||
relativePath: 'file.ts',
|
||||
matches: []
|
||||
},
|
||||
match: {
|
||||
line: 1,
|
||||
column: 1,
|
||||
matchLength: 4,
|
||||
lineContent: 'test'
|
||||
},
|
||||
openFile,
|
||||
setPendingEditorReveal: vi.fn(),
|
||||
revealRafRef: { current: null },
|
||||
revealInnerRafRef: { current: null }
|
||||
})
|
||||
|
||||
expect(openFile).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { detectLanguage } from '@/lib/language-detect'
|
||||
import type { FileSearchResultOwner } from '@/lib/file-search-result-owner'
|
||||
import type { SearchFileResult, SearchMatch } from '../../../../shared/types'
|
||||
|
||||
export function cancelRevealFrame(frameRef: React.RefObject<number | null>): void {
|
||||
@@ -9,16 +10,20 @@ export function cancelRevealFrame(frameRef: React.RefObject<number | null>): voi
|
||||
}
|
||||
|
||||
export function openMatchResult(params: {
|
||||
activeWorktreeId: string
|
||||
resultOwner: FileSearchResultOwner | null
|
||||
fileResult: SearchFileResult
|
||||
match: SearchMatch
|
||||
openFile: (file: {
|
||||
filePath: string
|
||||
relativePath: string
|
||||
worktreeId: string
|
||||
language: string
|
||||
mode: 'edit'
|
||||
}) => void
|
||||
openFile: (
|
||||
file: {
|
||||
filePath: string
|
||||
relativePath: string
|
||||
worktreeId: string
|
||||
language: string
|
||||
mode: 'edit'
|
||||
runtimeEnvironmentId: string | null
|
||||
},
|
||||
options: { suppressActiveRuntimeFallback: boolean }
|
||||
) => void
|
||||
setPendingEditorReveal: (
|
||||
reveal: {
|
||||
filePath: string
|
||||
@@ -31,7 +36,7 @@ export function openMatchResult(params: {
|
||||
revealInnerRafRef: React.RefObject<number | null>
|
||||
}): void {
|
||||
const {
|
||||
activeWorktreeId,
|
||||
resultOwner,
|
||||
fileResult,
|
||||
match,
|
||||
openFile,
|
||||
@@ -40,13 +45,23 @@ export function openMatchResult(params: {
|
||||
revealInnerRafRef
|
||||
} = params
|
||||
|
||||
openFile({
|
||||
filePath: fileResult.filePath,
|
||||
relativePath: fileResult.relativePath,
|
||||
worktreeId: activeWorktreeId,
|
||||
language: detectLanguage(fileResult.relativePath),
|
||||
mode: 'edit'
|
||||
})
|
||||
if (!resultOwner) {
|
||||
return
|
||||
}
|
||||
|
||||
openFile(
|
||||
{
|
||||
filePath: fileResult.filePath,
|
||||
relativePath: fileResult.relativePath,
|
||||
worktreeId: resultOwner.worktreeId,
|
||||
runtimeEnvironmentId: resultOwner.runtimeEnvironmentId,
|
||||
language: detectLanguage(fileResult.relativePath),
|
||||
mode: 'edit'
|
||||
},
|
||||
{
|
||||
suppressActiveRuntimeFallback: resultOwner.runtimeEnvironmentId === null
|
||||
}
|
||||
)
|
||||
|
||||
cancelRevealFrame(revealRafRef)
|
||||
cancelRevealFrame(revealInnerRafRef)
|
||||
|
||||
@@ -44,6 +44,7 @@ export function useFileSearchPanel(explorerView: 'files' | 'search'): FileSearch
|
||||
const fileSearchIncludePattern = searchState?.includePattern ?? ''
|
||||
const fileSearchExcludePattern = searchState?.excludePattern ?? ''
|
||||
const fileSearchResults = searchState?.results ?? null
|
||||
const fileSearchResultOwner = searchState?.resultOwner ?? null
|
||||
const fileSearchLoading = searchState?.loading ?? false
|
||||
const fileSearchCollapsedFiles = searchState?.collapsedFiles ?? EMPTY_COLLAPSED_FILES
|
||||
const fileSearchSeedRequestId = searchState?.seedRequestId
|
||||
@@ -127,18 +128,22 @@ export function useFileSearchPanel(explorerView: 'files' | 'search'): FileSearch
|
||||
useEffect(() => {
|
||||
if (!worktreePath) {
|
||||
cancelPendingSearch()
|
||||
updateActiveSearchState({ results: null })
|
||||
updateActiveSearchState({ results: null, resultOwner: null })
|
||||
}
|
||||
}, [worktreePath, cancelPendingSearch, updateActiveSearchState])
|
||||
|
||||
const deferredSearchResults = useDeferredValue(fileSearchResults)
|
||||
const committedSearchResults = useMemo(
|
||||
() => ({ results: fileSearchResults, owner: fileSearchResultOwner }),
|
||||
[fileSearchResultOwner, fileSearchResults]
|
||||
)
|
||||
const deferredSearchResults = useDeferredValue(committedSearchResults)
|
||||
const searchRows = useMemo(
|
||||
() =>
|
||||
buildSearchRows(
|
||||
fileSearchQuery.trim() && worktreePath ? deferredSearchResults : null,
|
||||
fileSearchQuery.trim() && worktreePath ? deferredSearchResults.results : null,
|
||||
fileSearchCollapsedFiles
|
||||
),
|
||||
[deferredSearchResults, fileSearchCollapsedFiles, fileSearchQuery, worktreePath]
|
||||
[deferredSearchResults.results, fileSearchCollapsedFiles, fileSearchQuery, worktreePath]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -218,11 +223,8 @@ export function useFileSearchPanel(explorerView: 'files' | 'search'): FileSearch
|
||||
|
||||
const handleMatchClick = useCallback(
|
||||
(fileResult: SearchFileResult, match: SearchMatch) => {
|
||||
if (!activeWorktreeId) {
|
||||
return
|
||||
}
|
||||
openMatchResult({
|
||||
activeWorktreeId,
|
||||
resultOwner: deferredSearchResults.owner,
|
||||
fileResult,
|
||||
match,
|
||||
openFile,
|
||||
@@ -231,7 +233,7 @@ export function useFileSearchPanel(explorerView: 'files' | 'search'): FileSearch
|
||||
revealInnerRafRef
|
||||
})
|
||||
},
|
||||
[activeWorktreeId, openFile, setPendingEditorReveal]
|
||||
[deferredSearchResults.owner, openFile, setPendingEditorReveal]
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -274,7 +276,7 @@ export function useFileSearchPanel(explorerView: 'files' | 'search'): FileSearch
|
||||
}
|
||||
},
|
||||
resultsProps: {
|
||||
results: deferredSearchResults,
|
||||
results: deferredSearchResults.results,
|
||||
hasCommittedResults: fileSearchResults !== null,
|
||||
query: fileSearchQuery,
|
||||
loading: fileSearchLoading,
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SearchResult } from '../../../../shared/types'
|
||||
import { useFileSearchRunner } from './useFileSearchRunner'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getConnectionId: vi.fn(),
|
||||
getState: vi.fn(),
|
||||
searchRuntimeFiles: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/connection-context', () => ({
|
||||
getConnectionId: mocks.getConnectionId
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/runtime-file-client', () => ({
|
||||
searchRuntimeFiles: mocks.searchRuntimeFiles
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(vi.fn(), { getState: mocks.getState })
|
||||
}))
|
||||
|
||||
const RESULTS: SearchResult = {
|
||||
files: [],
|
||||
totalMatches: 0,
|
||||
truncated: false
|
||||
}
|
||||
|
||||
function renderSearchRunner(state: Record<string, unknown>, worktreeId: string) {
|
||||
const updates: Record<string, unknown>[] = []
|
||||
mocks.getState.mockImplementation(() => state)
|
||||
mocks.searchRuntimeFiles.mockResolvedValue(RESULTS)
|
||||
|
||||
const hook = renderHook(() =>
|
||||
useFileSearchRunner({
|
||||
activeWorktreeId: worktreeId,
|
||||
worktreePath: '/repo',
|
||||
updateActiveSearchState: (update) => updates.push(update)
|
||||
})
|
||||
)
|
||||
|
||||
return { hook, updates }
|
||||
}
|
||||
|
||||
async function finishSearch(executeSearch: (query: string) => void): Promise<void> {
|
||||
await act(async () => {
|
||||
executeSearch('owner')
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
})
|
||||
}
|
||||
|
||||
describe('useFileSearchRunner result ownership', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
mocks.getConnectionId.mockReturnValue(null)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('commits the explicit remote owner used for the search, not the ambient runtime', async () => {
|
||||
const worktreeId = 'repo-a::/repo'
|
||||
const state = {
|
||||
settings: { activeRuntimeEnvironmentId: 'ambient-runtime-b' },
|
||||
repos: [{ id: 'repo-a', executionHostId: 'runtime:repo-runtime' }],
|
||||
worktreesByRepo: {
|
||||
'repo-a': [{ id: worktreeId, repoId: 'repo-a', hostId: 'runtime:search-runtime-a' }]
|
||||
},
|
||||
fileSearchStateByWorktree: { [worktreeId]: {} }
|
||||
}
|
||||
const { hook, updates } = renderSearchRunner(state, worktreeId)
|
||||
|
||||
await finishSearch(hook.result.current.executeSearch)
|
||||
|
||||
expect(mocks.searchRuntimeFiles).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
settings: { activeRuntimeEnvironmentId: 'search-runtime-a' },
|
||||
worktreeId
|
||||
}),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(updates).toContainEqual({
|
||||
results: RESULTS,
|
||||
resultOwner: {
|
||||
worktreeId,
|
||||
runtimeEnvironmentId: 'search-runtime-a'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('commits explicit local ownership without inheriting an ambient runtime', async () => {
|
||||
const worktreeId = 'repo-a::/repo'
|
||||
const state = {
|
||||
settings: { activeRuntimeEnvironmentId: 'ambient-runtime-b' },
|
||||
repos: [{ id: 'repo-a', executionHostId: 'runtime:repo-runtime' }],
|
||||
worktreesByRepo: {
|
||||
'repo-a': [{ id: worktreeId, repoId: 'repo-a', hostId: 'local' }]
|
||||
},
|
||||
fileSearchStateByWorktree: { [worktreeId]: {} }
|
||||
}
|
||||
const { hook, updates } = renderSearchRunner(state, worktreeId)
|
||||
|
||||
await finishSearch(hook.result.current.executeSearch)
|
||||
|
||||
expect(mocks.searchRuntimeFiles).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: null }, worktreeId }),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(updates).toContainEqual({
|
||||
results: RESULTS,
|
||||
resultOwner: { worktreeId, runtimeEnvironmentId: null }
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves SSH routing through the worktree connection without a runtime owner', async () => {
|
||||
const worktreeId = 'repo-a::/repo'
|
||||
const state = {
|
||||
settings: { activeRuntimeEnvironmentId: 'ambient-runtime-b' },
|
||||
repos: [{ id: 'repo-a', connectionId: 'ssh-target' }],
|
||||
worktreesByRepo: {
|
||||
'repo-a': [{ id: worktreeId, repoId: 'repo-a', hostId: 'ssh:ssh-target' }]
|
||||
},
|
||||
fileSearchStateByWorktree: { [worktreeId]: {} }
|
||||
}
|
||||
mocks.getConnectionId.mockReturnValue('ssh-target')
|
||||
const { hook, updates } = renderSearchRunner(state, worktreeId)
|
||||
|
||||
await finishSearch(hook.result.current.executeSearch)
|
||||
|
||||
expect(mocks.searchRuntimeFiles).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
worktreeId,
|
||||
connectionId: 'ssh-target'
|
||||
}),
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(updates).toContainEqual({
|
||||
results: RESULTS,
|
||||
resultOwner: { worktreeId, runtimeEnvironmentId: null }
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps an unresolved owner local when no runtime actually handled the search', async () => {
|
||||
const worktreeId = 'missing-repo::/repo'
|
||||
const state = {
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
repos: [],
|
||||
worktreesByRepo: {},
|
||||
fileSearchStateByWorktree: { [worktreeId]: {} }
|
||||
}
|
||||
const { hook, updates } = renderSearchRunner(state, worktreeId)
|
||||
|
||||
await finishSearch(hook.result.current.executeSearch)
|
||||
|
||||
expect(updates).toContainEqual({
|
||||
results: RESULTS,
|
||||
resultOwner: { worktreeId, runtimeEnvironmentId: null }
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { getConnectionId } from '@/lib/connection-context'
|
||||
import {
|
||||
createFileSearchResultOwner,
|
||||
type FileSearchResultOwner
|
||||
} from '@/lib/file-search-result-owner'
|
||||
import {
|
||||
createEmptyRuntimeFileSearchResult,
|
||||
getRuntimeFileSearchRejectedField
|
||||
@@ -12,7 +16,11 @@ import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-
|
||||
const SEARCH_DEBOUNCE_MS = 300
|
||||
const SEARCH_MAX_RESULTS = 2000
|
||||
|
||||
type UpdateSearchState = (updates: { loading?: boolean; results?: SearchResult | null }) => void
|
||||
type UpdateSearchState = (updates: {
|
||||
loading?: boolean
|
||||
results?: SearchResult | null
|
||||
resultOwner?: FileSearchResultOwner | null
|
||||
}) => void
|
||||
|
||||
type UseFileSearchRunnerArgs = {
|
||||
activeWorktreeId: string | null
|
||||
@@ -53,7 +61,7 @@ export function useFileSearchRunner({
|
||||
}
|
||||
|
||||
if (!worktreePath || !activeWorktreeId) {
|
||||
updateActiveSearchState({ results: null, loading: false })
|
||||
updateActiveSearchState({ results: null, resultOwner: null, loading: false })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,21 +73,26 @@ export function useFileSearchRunner({
|
||||
excludePattern: currentSearchState?.excludePattern || undefined
|
||||
})
|
||||
) {
|
||||
const runtimeSettings = getRightSidebarWorktreeRuntimeSettings(activeWorktreeId)
|
||||
updateActiveSearchState({
|
||||
results: createEmptyRuntimeFileSearchResult(),
|
||||
resultOwner: createFileSearchResultOwner(activeWorktreeId, runtimeSettings),
|
||||
loading: false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!query.trim()) {
|
||||
updateActiveSearchState({ results: null, loading: false })
|
||||
updateActiveSearchState({ results: null, resultOwner: null, loading: false })
|
||||
return
|
||||
}
|
||||
|
||||
updateActiveSearchState({ loading: true })
|
||||
searchTimerRef.current = setTimeout(async () => {
|
||||
searchTimerRef.current = null
|
||||
// Why: results can outlive the selected worktree; clicks must reuse the route that produced them.
|
||||
const runtimeSettings = getRightSidebarWorktreeRuntimeSettings(activeWorktreeId)
|
||||
const resultOwner = createFileSearchResultOwner(activeWorktreeId, runtimeSettings)
|
||||
try {
|
||||
const state = useAppStore.getState()
|
||||
const connectionId = getConnectionId(activeWorktreeId) ?? undefined
|
||||
@@ -94,6 +107,7 @@ export function useFileSearchRunner({
|
||||
if (latestSearchIdRef.current === searchId) {
|
||||
updateActiveSearchState({
|
||||
results: createEmptyRuntimeFileSearchResult(),
|
||||
resultOwner,
|
||||
loading: false
|
||||
})
|
||||
}
|
||||
@@ -101,7 +115,7 @@ export function useFileSearchRunner({
|
||||
}
|
||||
const results = await searchRuntimeFiles(
|
||||
{
|
||||
settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId),
|
||||
settings: runtimeSettings,
|
||||
worktreeId: activeWorktreeId,
|
||||
worktreePath,
|
||||
connectionId
|
||||
@@ -118,13 +132,14 @@ export function useFileSearchRunner({
|
||||
}
|
||||
)
|
||||
if (latestSearchIdRef.current === searchId) {
|
||||
updateActiveSearchState({ results })
|
||||
updateActiveSearchState({ results, resultOwner })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Search failed:', err)
|
||||
if (latestSearchIdRef.current === searchId) {
|
||||
updateActiveSearchState({
|
||||
results: { files: [], totalMatches: 0, truncated: false }
|
||||
results: { files: [], totalMatches: 0, truncated: false },
|
||||
resultOwner
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createFileSearchResultOwner } from './file-search-result-owner'
|
||||
|
||||
describe('createFileSearchResultOwner', () => {
|
||||
it('captures the exact runtime used by the completed search', () => {
|
||||
const settings = { activeRuntimeEnvironmentId: ' runtime-owner-a ' }
|
||||
const owner = createFileSearchResultOwner('worktree-a', settings)
|
||||
settings.activeRuntimeEnvironmentId = 'runtime-owner-b'
|
||||
|
||||
expect(owner).toEqual({
|
||||
worktreeId: 'worktree-a',
|
||||
runtimeEnvironmentId: 'runtime-owner-a'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([null, '', ' '])('records %j as an explicit non-runtime owner', (environmentId) => {
|
||||
expect(
|
||||
createFileSearchResultOwner('local-or-ssh-worktree', {
|
||||
activeRuntimeEnvironmentId: environmentId
|
||||
})
|
||||
).toEqual({
|
||||
worktreeId: 'local-or-ssh-worktree',
|
||||
runtimeEnvironmentId: null
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { GlobalSettings } from '../../../shared/types'
|
||||
|
||||
export type FileSearchResultOwner = {
|
||||
worktreeId: string
|
||||
runtimeEnvironmentId: string | null
|
||||
}
|
||||
|
||||
export function createFileSearchResultOwner(
|
||||
worktreeId: string,
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'>
|
||||
): FileSearchResultOwner {
|
||||
return {
|
||||
worktreeId,
|
||||
runtimeEnvironmentId: settings.activeRuntimeEnvironmentId?.trim() || null
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
import { createUntitledMarkdownFileWithTemplateSelection } from '@/lib/create-untitled-markdown'
|
||||
import { extractIpcErrorMessage } from '@/lib/ipc-error'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { FileSearchResultOwner } from '@/lib/file-search-result-owner'
|
||||
|
||||
export type {
|
||||
ActiveRightSidebarTab,
|
||||
@@ -87,6 +88,7 @@ const DEFAULT_FILE_SEARCH_STATE = {
|
||||
includePattern: '',
|
||||
excludePattern: '',
|
||||
results: null,
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set<string>()
|
||||
} satisfies Omit<
|
||||
@@ -723,6 +725,7 @@ export type EditorSlice = {
|
||||
includePattern: string
|
||||
excludePattern: string
|
||||
results: SearchResult | null
|
||||
resultOwner: FileSearchResultOwner | null
|
||||
loading: boolean
|
||||
collapsedFiles: Set<string>
|
||||
seedRequestId?: number
|
||||
@@ -1528,6 +1531,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
...(shouldSeed
|
||||
? {
|
||||
results: null,
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set<string>(),
|
||||
seedRequestId: (current.seedRequestId ?? 0) + 1
|
||||
@@ -4106,6 +4110,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
...current,
|
||||
query,
|
||||
results: null,
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set(),
|
||||
seedRequestId: (current.seedRequestId ?? 0) + 1
|
||||
@@ -4123,6 +4128,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
...current,
|
||||
includePattern,
|
||||
results: null,
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set(),
|
||||
seedRequestId: (current.seedRequestId ?? 0) + 1
|
||||
@@ -4177,6 +4183,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||
...current,
|
||||
query: '',
|
||||
results: null,
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set()
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ describe('removeWorktree cascade', () => {
|
||||
includePattern: '*.ts',
|
||||
excludePattern: 'dist/**',
|
||||
results: { files: [], totalMatches: 0, truncated: false },
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set(['/path/wt1/file.ts'])
|
||||
}
|
||||
@@ -697,6 +698,7 @@ describe('removeWorktree cascade', () => {
|
||||
includePattern: '',
|
||||
excludePattern: '',
|
||||
results: { files: [], totalMatches: 0, truncated: false },
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set()
|
||||
},
|
||||
@@ -708,6 +710,7 @@ describe('removeWorktree cascade', () => {
|
||||
includePattern: '*.md',
|
||||
excludePattern: '',
|
||||
results: { files: [], totalMatches: 1, truncated: false },
|
||||
resultOwner: null,
|
||||
loading: false,
|
||||
collapsedFiles: new Set(['/path/wt2/notes.md'])
|
||||
}
|
||||
|
||||
@@ -6882,6 +6882,9 @@ describe('migrateWorktreeIdentity', () => {
|
||||
renamingWorktreeId: { worktreeId: OLD, rowKey: 'all:old' },
|
||||
tabsByWorktree: { [OLD]: [{ id: 'tab1', worktreeId: OLD }] },
|
||||
rightSidebarExplorerViewByWorktree: { [OLD]: 'search' },
|
||||
fileSearchStateByWorktree: {
|
||||
[OLD]: { resultOwner: { worktreeId: OLD, runtimeEnvironmentId: 'runtime-a' } }
|
||||
},
|
||||
activeTabIdByWorktree: { [OLD]: 'tab1' },
|
||||
browserTabsByWorktree: { [OLD]: [{ id: 'browser1', worktreeId: OLD }] },
|
||||
browserPagesByWorkspace: { browser1: [{ id: 'page1', worktreeId: OLD }] },
|
||||
@@ -6945,6 +6948,11 @@ describe('migrateWorktreeIdentity', () => {
|
||||
expect(s.gitBranchCompareRequestStatusHeadByWorktree[NEW]).toBe('head-old')
|
||||
expect(s.rightSidebarExplorerViewByWorktree[OLD]).toBeUndefined()
|
||||
expect(s.rightSidebarExplorerViewByWorktree[NEW]).toBe('search')
|
||||
expect(s.fileSearchStateByWorktree[OLD]).toBeUndefined()
|
||||
expect(s.fileSearchStateByWorktree[NEW]?.resultOwner).toEqual({
|
||||
worktreeId: NEW,
|
||||
runtimeEnvironmentId: 'runtime-a'
|
||||
})
|
||||
expect(s.lastVisitedAtByWorktreeId[NEW]).toBe(123)
|
||||
expect(s.defaultTerminalTabsAppliedByWorktreeId[NEW]).toBe(true)
|
||||
// The two maps absent from the purge list are still re-keyed.
|
||||
|
||||
@@ -1824,6 +1824,10 @@ function buildWorktreeRenameState(
|
||||
workspace: withNewWorktreeId(snapshot.workspace),
|
||||
pages: snapshot.pages.map(withNewWorktreeId)
|
||||
})),
|
||||
fileSearchStateByWorktree: (searchState: AppState['fileSearchStateByWorktree'][string]) => ({
|
||||
...searchState,
|
||||
resultOwner: searchState.resultOwner ? withNewWorktreeId(searchState.resultOwner) : null
|
||||
}),
|
||||
unifiedTabsByWorktree: (tabs: { worktreeId: string }[]) => tabs.map(withNewWorktreeId),
|
||||
groupsByWorktree: (groups: { worktreeId: string }[]) => groups.map(withNewWorktreeId)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user