fix(wsl): a workspace switch wakes the explorer and git status only while recent (#35897)

The explorer's workspace-open read and the first git status took a pending
user switch with no age limit, so a switch no consumer used (sidebar closed)
could be redeemed long after the distro stopped. They now use the same 10 s
window as pane restore and git history, through one shared
takeRecentWorkspaceSwitchWake.
This commit is contained in:
Neil
2026-10-29 03:21:57 -07:00
committed by GitHub
parent bbb563e83a
commit 38c111697a
6 changed files with 33 additions and 19 deletions
@@ -5,7 +5,8 @@ import type { DirCache } from './file-explorer-types'
import { useFileExplorerTreeLoadEffects } from './use-file-explorer-tree-load-effects'
import {
noteUserWorkspaceSwitch,
resetWorkspaceSwitchUserActsForTests
resetWorkspaceSwitchUserActsForTests,
WORKSPACE_SWITCH_MOUNT_WINDOW_MS
} from '@/lib/workspace-switch-user-act'
afterEach(() => {
@@ -141,6 +142,19 @@ it("only the user's own switch to a workspace lets its open read start a stopped
renderHook(useFileExplorerTreeLoadEffects, { initialProps: props })
expect(props.resetAndLoad).toHaveBeenCalledExactlyOnceWith({ wakeWslDistro: true })
})
it('an old switch no consumer used is not redeemed by a later mount (E9)', () => {
vi.useFakeTimers()
try {
const { props } = params()
// The user switched here with the sidebar closed; the explorer mounts long after.
noteUserWorkspaceSwitch(props.workspaceKey)
vi.advanceTimersByTime(WORKSPACE_SWITCH_MOUNT_WINDOW_MS + 1)
renderHook(useFileExplorerTreeLoadEffects, { initialProps: props })
expect(props.resetAndLoad).toHaveBeenCalledExactlyOnceWith(undefined)
} finally {
vi.useRealTimers()
}
})
it('a restore, remount or re-open with no new switch never wakes WSL', () => {
const { props } = params()
renderHook(useFileExplorerTreeLoadEffects, { initialProps: props })
@@ -4,15 +4,13 @@ import type { RuntimeClientTarget } from '../../../../../../shared/runtime-clien
import { getConnectionId } from '@/lib/connection-context'
import { getRuntimeGitHistory } from '@/runtime/runtime-git-status-client'
import type { GitHistoryPanelState } from './git-history-panel'
import { takeWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
import { takeRecentWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
const EMPTY_GIT_HISTORY_STATE: GitHistoryPanelState = { status: 'idle' }
/** `wakeWslDistro`: the user opening Commits or its Refresh, a user act that may start a stopped WSL distro. */
type HistoryRefresh = { wakeWslDistro?: boolean }
const SHOWN_HISTORY_LOAD: HistoryRefresh = { wakeWslDistro: true }
// The mounts and re-shows a user's own workspace switch causes land within this window.
const SWITCH_MOUNT_WINDOW_MS = 10_000
/** Cache/prune history per worktree and reject superseded requests; refreshGitHistoryRef lets effects refresh without callback resubscription. */
export function useSourceControlGitHistory({
@@ -169,10 +167,7 @@ export function useSourceControlGitHistory({
// workspace (once per switch). A remount, restore or Orca's own re-show stays background (E9).
const userAct =
userExpanded ||
(activeWorktreeId !== null &&
takeWorkspaceSwitchWake(activeWorktreeId, 'git-history', {
withinMs: SWITCH_MOUNT_WINDOW_MS
}))
(activeWorktreeId !== null && takeRecentWorkspaceSwitchWake(activeWorktreeId, 'git-history'))
void refreshGitHistoryRef.current(userAct ? SHOWN_HISTORY_LOAD : undefined)
}, [
// History uses compareBaseRef, which can move independently of effectiveBaseRef.
@@ -6,7 +6,7 @@ import { decideExpandedDirLoad } from './file-explorer-stale-dir-cache'
import { clearFileExplorerUndoHistory } from './fileExplorerUndoRedo'
import type { DirCache } from './file-explorer-types'
import { splitPathSegments } from './path-tree'
import { takeWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
import { takeRecentWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
type UseFileExplorerTreeLoadEffectsParams = {
/** The store worktree id, keying the user's own switches to this workspace. */
@@ -91,7 +91,7 @@ export function useFileExplorerTreeLoadEffects({
resetSelection()
setNameFilterQuery('')
// Only the user's own switch here may start its stopped WSL distro (E9), once per switch.
const wake = workspaceKey !== null && takeWorkspaceSwitchWake(workspaceKey, 'explorer')
const wake = workspaceKey !== null && takeRecentWorkspaceSwitchWake(workspaceKey, 'explorer')
resetAndLoad(wake ? { wakeWslDistro: true } : undefined)
clearFileExplorerUndoHistory()
}, [visibleFilesWorktreePath, resetSelection]) // eslint-disable-line react-hooks/exhaustive-deps
@@ -5,7 +5,7 @@ import { isGitRepoKind } from '../../../../shared/repo-kind'
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import { refreshGitStatusForWorktree } from './git-status-refresh'
import { isWindowVisible } from '@/lib/window-visibility-interval'
import { takeWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
import { takeRecentWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
import {
hasInteractiveActiveGitStatusConsumer,
shouldPollActiveGitStatus
@@ -134,7 +134,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
}
// Only the first status after the user's own switch here may start WSL (E9); polls, remounts
// and restores stay gated.
const wakeWslDistro = takeWorkspaceSwitchWake(activeWorktreeId, 'git-status')
const wakeWslDistro = takeRecentWorkspaceSwitchWake(activeWorktreeId, 'git-status')
await refreshGitStatusForWorktree({
target: copy.transport,
ownerHostId: copy.hostId,
@@ -1,4 +1,4 @@
import { takeWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
import { takeRecentWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
import { holdPaneForStoppedWslDistro } from '../wsl-distro-stopped-hold-state'
import { recordPtyConnectDiagnostic } from './pty-connect-limits'
import type { ConnectPanePtySession } from './connect-pane-pty-session'
@@ -7,9 +7,6 @@ import type { WslDistroHoldState } from '../../../../../shared/wsl-distro-stoppe
// Clears the saved screen the hold painted so the restore below paints it once.
const CLEAR_HELD_SCREEN = '\x1b[0m\x1b[2J\x1b[3J\x1b[H'
// The mounts a user's switch itself causes land well inside this; a later remount does not.
const SWITCH_MOUNT_WINDOW_MS = 10_000
/**
* Whether a local restore may start a stopped WSL distro: only Start, or this pane's first mount
* after the user's own switch to its workspace (once per switch, while recent). Anything else is
@@ -22,9 +19,7 @@ export function wslRestoreStartArgs(
if (
session.runtimeEnvironmentId ||
userAct ||
takeWorkspaceSwitchWake(session.deps.worktreeId, `pane-restore:${session.cacheKey}`, {
withinMs: SWITCH_MOUNT_WINDOW_MS
})
takeRecentWorkspaceSwitchWake(session.deps.worktreeId, `pane-restore:${session.cacheKey}`)
) {
return {}
}
@@ -30,6 +30,9 @@ export function lastUserWorkspaceSwitchAt(workspaceKey: string): number | undefi
return lastSwitchAt.get(workspaceKey)
}
/** How recent a switch must be for the mounts it causes to count as that switch (`withinMs`). */
export const WORKSPACE_SWITCH_MOUNT_WINDOW_MS = 10_000
/**
* True once per noted switch for each consumer; a remount or re-render with no new switch is false.
* `withinMs` also requires the switch to be that recent (the mounts a switch itself causes).
@@ -52,6 +55,13 @@ export function takeWorkspaceSwitchWake(
return true
}
/** `takeWorkspaceSwitchWake` for a consumer the switch itself mounts or runs. */
export function takeRecentWorkspaceSwitchWake(workspaceKey: string, consumer: string): boolean {
return takeWorkspaceSwitchWake(workspaceKey, consumer, {
withinMs: WORKSPACE_SWITCH_MOUNT_WINDOW_MS
})
}
export function resetWorkspaceSwitchUserActsForTests(): void {
lastSwitchAt.clear()
switchCounts.clear()