refactor(file-explorer): use useMemo for paths; remove relay reference

Replace manual ref-based caching with proper React hooks for content-stable path memoization. Remove outdated relay grace-time reference documentation from code review cycle.
This commit is contained in:
Jinjing
2026-08-01 02:26:51 -07:00
parent 66c2435230
commit aa6cceda5b
4 changed files with 24 additions and 32 deletions
@@ -7,18 +7,19 @@ for finding E of the P1 review; line numbers are as of that change.
## What decides the grace window
`startGrace` (`src/relay/relay.ts:1140`) samples
`ptyHandler.configuredGraceTimeMs` and picks one branch, always passing an
explicit `timeoutMs`:
`ptyHandler.configuredGraceTimeMs` and takes the **first** branch below that
matches (`decideRelayGrace`, `src/relay/relay-grace-branch.ts`), always passing
an explicit `timeoutMs`. Listed in that precedence order:
- `shutdown-deferred` — a refused kill left the PTY pooled, so the idle branch is
unreachable; this branch supplies its own bound or a `grace=0` default would
arm no retry at all.
- `startup-empty-detached` — a detached relay that never accepted a client, so
it has no PTY state to preserve. Capped at `EMPTY_DETACHED_STARTUP_GRACE_MS`.
- `idle-no-ptys` — zero PTYs *and* zero pending creations
(`isRelayIdle`, `src/relay/relay.ts:1175`) under the unlimited default only.
Capped at `IDLE_RELAY_GRACE_MS` (`src/relay/relay.ts:74`, 15 min, overridable
via `ORCA_RELAY_IDLE_GRACE_MS`).
- `shutdown-deferred` — a refused kill left the PTY pooled, so the idle branch is
unreachable; this branch supplies its own bound or a `grace=0` default would
arm no retry at all.
- `configured` — the configured value verbatim, never clamped.
The zero-only gate is load-bearing: the idle cap bounds *only* the unlimited
@@ -51,8 +51,6 @@ export function useFileExplorerTree(
const staleDirsRef = useRef(new Set<string>())
// Why: separates a failed root read from a superseded one — loadDir returns false for both.
const rootReadFailedRef = useRef(false)
const expandedRef = useRef(expanded)
expandedRef.current = expanded
const loadDir = useCallback(
async (
@@ -179,11 +177,8 @@ export function useFileExplorerTree(
staleDirsRef.current.delete(dirPath)
}
}
for (const dirPath of collectStaleDirCachePaths(
dirCacheRef.current,
worktreePath,
expandedRef.current
)) {
// Why: callers use the latest refreshTree identity, so this closure has the live expanded set.
for (const dirPath of collectStaleDirCachePaths(dirCacheRef.current, worktreePath, expanded)) {
staleDirsRef.current.add(dirPath)
}
const refreshSession = dirLoadTrackerRef.current.getSession()
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from 'react'
import { useCallback, useMemo } from 'react'
import { useAppStore } from '@/store'
import { isDotfileRelativePath } from './file-explorer-entries'
import type { DirCache, TreeNode } from './file-explorer-types'
@@ -118,25 +118,17 @@ export function createVisibleFileExplorerRowProjection(
* exists to bound.
*/
function useContentStableRelativePaths(relativePaths: string[], enabled: boolean): string[] {
const cache = useRef<{ source: string[]; signature: string; value: string[] } | null>(null)
// Why: a name-filter list is a different population, so signing it would evict the tree
// signature — clearing the filter would then mint a fresh identity and re-issue the very
// whole-tree check-ignore this hook exists to prevent.
if (!enabled) {
return relativePaths
}
// Why: the caller memoizes, so an unchanged tree hands back the same array — skip rebuilding a
// signature over every visible path on re-renders that never touched the tree.
if (cache.current?.source === relativePaths) {
return cache.current.value
}
// NUL separator: it is the one byte no path segment can contain.
const signature = relativePaths.join('\u0000')
cache.current =
cache.current?.signature === signature
? { ...cache.current, source: relativePaths }
: { source: relativePaths, signature, value: relativePaths }
return cache.current.value
// Why: filters need fresh identities per keystroke and must not evict the tree signature.
// Why: NUL cannot occur in paths, so the signature can reconstruct the list losslessly.
const signature = useMemo(
() => (enabled ? relativePaths.join('\u0000') : null),
[enabled, relativePaths]
)
const stableTreePaths = useMemo(
() => (signature ? signature.split('\u0000') : EMPTY_RELATIVE_PATHS),
[signature]
)
return enabled ? stableTreePaths : relativePaths
}
export function useFileExplorerVisibleRowProjection(
@@ -143,6 +143,10 @@ export function useFileExplorerWatch({
const scheduler = createFileExplorerWatchRefreshScheduler({
refreshTree: () => refreshTreeRef.current(),
refreshDir: (dirPath) => refreshDirRef.current(dirPath),
// Why exact-match on `expanded`, not a normalized compare: dirPath already arrives as a
// resolved dirCache key (resolveCachedDirPath), and refreshTree re-reads dirCache under the
// verbatim expanded strings — a differently-spelled key it never wrote would be reported
// covered and left stale. Only the root is normalized: it is keyed by worktreePath.
isCoveredByFullRefresh: (dirPath) =>
normalizeRuntimePathForComparison(dirPath) ===
normalizeRuntimePathForComparison(currentWorktreePath) ||