Revert "fix(editor): evict stale mirrored file tabs (#21363)" (#21368)

This reverts commit 07e8c851b8.

The eviction keys on `selector_not_found`, which this repo documents twice as
UNKNOWN rather than absence:

- `remote-browser-stream-errors.ts`: "it means 'I could not resolve this right
  now', which is UNKNOWN, not proof the target is gone. Its producer is a live
  worktree scan behind a 1s-TTL cache ... a slow scan can surface it
  transiently. Treating that as permanent would strand the pane forever, which
  is the exact bug this file exists to prevent."
- `web-runtime-session-tab-lifecycle.ts`, added by #21277: "'selector_not_found'
  is a transient worktree resolver state (e.g. during scans or cache warm-up)
  and must not become a durable close tombstone."

Two unambiguous absence codes exist for this purpose -- `tab_not_found` and
`terminal_tab_not_found` -- and #21277 had just finished excluding
`selector_not_found` from them. This keyed on the excluded one.

Consequences, after roughly 3.75s of retries:

1. `closeFile` deletes `editorDrafts[fileId]` with no dirty check and no
   confirmation, so a transient resolver blip discards unsaved edits.
2. `closeFile` calls `notifyHostOfMirroredEditorClose`, so the host closes its
   copy too -- the eviction is not local and not recoverable.

The `!ownerNotReady` guard does not cover this: `ownerNotReady` means the host is
still connecting, while `selector_not_found` is emitted for a cold resolver cache
or an unhydrated catalog, which is a different state.

#21041 is still open. A correct fix keys on the two definitive absence codes,
refuses to evict a tab that has a draft, and has a test proving a dirty mirrored
tab survives `selector_not_found`.
This commit is contained in:
Neil
2026-09-17 22:12:46 -07:00
committed by GitHub
parent ffc812cdce
commit 945ea33541
3 changed files with 1 additions and 53 deletions
@@ -1,6 +1,6 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
import type { OpenFile } from '@/store/slices/editor'
import { useAppStore } from '@/store'
import type { useAppStore } from '@/store'
import type { DiffContent, FileContent } from './editor-panel-content-types'
import {
useEditorPanelExternalContentEvents,
@@ -194,7 +194,6 @@ export function useEditorPanelContentState({
fileLoadRetryAttemptsRef,
loadFileContent,
openFilesRef,
closeFile: useAppStore.getState().closeFile,
setFileContents
})
@@ -49,7 +49,6 @@ function Harness({
attemptsRef,
isVisible = true,
loadFileContent,
closeFile = vi.fn(),
setFileContents
}: {
file: OpenFile
@@ -57,7 +56,6 @@ function Harness({
attemptsRef: { current: Record<string, number> }
isVisible?: boolean
loadFileContent: (filePath: string, id: string) => Promise<void>
closeFile?: (fileId: string) => void
setFileContents: (
updater: (prev: Record<string, FileContent>) => Record<string, FileContent>
) => void
@@ -68,7 +66,6 @@ function Harness({
fileLoadRetryAttemptsRef: attemptsRef,
loadFileContent: loadFileContent as never,
openFilesRef: { current: [file] },
closeFile,
setFileContents: setFileContents as never
})
return null
@@ -106,35 +103,6 @@ describe('useEditorPanelFileLoadRetry — owner-not-ready bounding (#6648)', ()
expect(shouldRetryFileLoadError('Access denied: outside allowed directories')).toBe(false)
})
it('evicts a mirrored tab after selector resolution stays missing', () => {
const file = makeFile({ mirroredFromRuntimeSession: true })
const attemptsRef = { current: { [file.id]: 3 } }
const closeFile = vi.fn()
const fileContents: Record<string, FileContent> = {
[file.id]: { content: '', isBinary: false, loadError: 'selector_not_found' }
}
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
act(() => {
root?.render(
<Harness
file={file}
fileContents={fileContents}
attemptsRef={attemptsRef}
loadFileContent={vi.fn(async () => undefined)}
closeFile={closeFile}
setFileContents={(updater) => {
updater(fileContents)
}}
/>
)
})
expect(closeFile).toHaveBeenCalledWith(file.id)
})
it('does not spend retry budget when hiding cancels a pending retry', () => {
setTimeoutSpy.mockRestore()
setTimeoutSpy = vi.spyOn(window, 'setTimeout')
@@ -7,7 +7,6 @@ import {
} from './editor-panel-content-types'
const FILE_LOAD_RETRY_DELAYS_MS = [250, 1000, 2500]
const noopCloseFile = (): void => {}
// Why: a remote host can take a while to finish connecting. The owner-not-ready
// check is a pure local store read (it throws before any network call until the
// SSH repo hydrates), so poll it at a steady cadence — but cap the wait so a
@@ -31,14 +30,9 @@ type UseEditorPanelFileLoadRetryParams = {
relativePath?: string
) => Promise<void>
openFilesRef: MutableRefObject<OpenFile[]>
closeFile?: (fileId: string) => void
setFileContents: Dispatch<SetStateAction<Record<string, FileContent>>>
}
function isSelectorNotFoundError(message: string): boolean {
return message.trim().toLowerCase() === 'selector_not_found'
}
export function shouldRetryFileLoadError(message: string): boolean {
// Terminal: the owner-not-ready budget is spent; only an explicit Retry should
// restart it, never the automatic backoff.
@@ -60,7 +54,6 @@ export function useEditorPanelFileLoadRetry({
fileLoadRetryAttemptsRef,
loadFileContent,
openFilesRef,
closeFile = noopCloseFile,
setFileContents
}: UseEditorPanelFileLoadRetryParams): void {
const activeFileLoadRetryId = activeFile?.id ?? null
@@ -82,16 +75,6 @@ export function useEditorPanelFileLoadRetry({
? OWNER_NOT_READY_RETRY_LIMIT
: FILE_LOAD_RETRY_DELAYS_MS.length
if (retryCount >= retryLimit) {
if (
!ownerNotReady &&
isSelectorNotFoundError(activeFileLoadError) &&
activeFile?.mirroredFromRuntimeSession === true
) {
// A host-mirrored file whose worktree stays unresolvable after the normal
// read retries is stale; evict it before snapshots can select it again.
closeFile(activeFileLoadRetryId)
return
}
// Why: the remote host never finished connecting. Replace the transient
// "still connecting" text with a truthful terminal message so it does not
// look like it is still retrying; Retry starts a fresh budget (#6648).
@@ -143,8 +126,6 @@ export function useEditorPanelFileLoadRetry({
}, [
activeFileLoadRetryId,
activeFileLoadError,
activeFile?.mirroredFromRuntimeSession,
closeFile,
fileLoadRetryAttemptsRef,
loadFileContent,
openFilesRef,