Fix edge peek dialog flow and render churn

Keep sidebar-owned dialogs alive while interacting outside the overlay, isolate Git and file-watch subscriptions from the App render tree, and restore localization catalog parity for the one-shot tip.
This commit is contained in:
Brennan Benson
2026-07-16 18:04:11 -07:00
parent 09a24d7737
commit 01edd7d1ce
11 changed files with 122 additions and 37 deletions
+4 -17
View File
@@ -90,8 +90,7 @@ import { RecoverableRenderErrorBoundary } from './components/error-boundaries/Re
import { ConfirmationDialogProvider } from './components/confirmation-dialog'
import { LinkRoutingPreferenceDialogProvider } from './components/link-routing-preference-dialog'
import RecentTabSwitcher from './components/tab-bar/RecentTabSwitcher'
import { useGitStatusPolling } from './components/right-sidebar/useGitStatusPolling'
import { useEditorExternalWatch } from './hooks/useEditorExternalWatch'
import { WorkspaceGitAndFileWatchGate } from './components/right-sidebar/WorkspaceGitAndFileWatchGate'
import { useAutoAckViewedAgent } from './hooks/useAutoAckViewedAgent'
import { useUnreadDockBadge } from './hooks/useUnreadDockBadge'
import {
@@ -742,21 +741,6 @@ function App(): React.JSX.Element {
// than being called inline here so its high-churn store subscriptions
// (agentStatusByPaneKey ticks at PTY event frequency)
// do not re-render the App tree on every agent status update.
// Why: git conflict-operation state also drives the worktree cards. Polling
// cannot live under RightSidebar because App unmounts that subtree when the
// sidebar is closed, which leaves stale "Rebasing"/"Merging" badges behind
// until some unrelated view remount happens to refresh them.
// Why: visible-window polling runs immediately on mount. Wait until the
// workspace session has hydrated so git status work cannot compete with the
// first window becoming usable.
useGitStatusPolling({ enabled: workspaceSessionReady })
// Why: the editor must hear external filesystem changes regardless of
// which right-sidebar panel is visible (Explorer unmounts when the user
// switches to Source Control or Checks). Wiring this at App level mirrors
// VSCode's workbench-scoped `TextFileEditorModelManager`, which reloads
// clean models from a single always-on file-change subscription instead
// of tying reloads to the Explorer UI lifecycle.
useEditorExternalWatch()
useGlobalFileDrop()
useAutoAckViewedAgent()
@@ -2255,6 +2239,9 @@ function App(): React.JSX.Element {
<ConfirmationDialogProvider>
<LinkRoutingPreferenceDialogProvider>
<WorkspacePortScanner enabled={workspaceSessionReady} />
{/* Why: keep Git status and editor file watching workspace-scoped
without letting their sidebar subscriptions re-render App. */}
<WorkspaceGitAndFileWatchGate enabled={workspaceSessionReady} />
{/* Why: leaf-mounted retention sync keeps agent-status retention
subscriptions from re-rendering the App tree. */}
<RetainedAgentsSyncGate />
+10 -1
View File
@@ -154,8 +154,17 @@ describe('renderer startup runtime routing', () => {
it('defers background polling until the workspace session is ready', () => {
const source = readFileSync(join(process.cwd(), 'src/renderer/src/App.tsx'), 'utf8')
const syncGateSource = readFileSync(
join(
process.cwd(),
'src/renderer/src/components/right-sidebar/WorkspaceGitAndFileWatchGate.tsx'
),
'utf8'
)
expect(source).toContain('useGitStatusPolling({ enabled: workspaceSessionReady })')
expect(source).toContain('<WorkspaceGitAndFileWatchGate enabled={workspaceSessionReady} />')
expect(syncGateSource).toContain('useGitStatusPolling({ enabled })')
expect(syncGateSource).toContain('useEditorExternalWatch()')
expect(source).toContain('<WorkspacePortScanner enabled={workspaceSessionReady} />')
})
@@ -0,0 +1,56 @@
// @vitest-environment happy-dom
import { act, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { WorkspaceGitAndFileWatchGate } from './WorkspaceGitAndFileWatchGate'
const hookCalls = vi.hoisted(() => ({ git: vi.fn(), fileWatch: vi.fn() }))
vi.mock('./useGitStatusPolling', async () => {
const { useAppStore: useStore } = await import('@/store')
return {
useGitStatusPolling(options: { enabled?: boolean }): void {
useStore((state) => state.rightSidebarPeek)
hookCalls.git(options)
}
}
})
vi.mock('@/hooks/useEditorExternalWatch', async () => {
const { useAppStore: useStore } = await import('@/store')
return {
useEditorExternalWatch(): void {
useStore((state) => state.rightSidebarPeek)
hookCalls.fileWatch()
}
}
})
const initialAppState = useAppStore.getInitialState()
afterEach(() => {
useAppStore.setState(initialAppState, true)
vi.clearAllMocks()
})
describe('WorkspaceGitAndFileWatchGate', () => {
it('contains sidebar-driven hook renders without re-rendering its parent', () => {
let parentRenderCount = 0
function Parent(): React.JSX.Element {
parentRenderCount += 1
return <WorkspaceGitAndFileWatchGate enabled />
}
render(<Parent />)
expect(parentRenderCount).toBe(1)
expect(hookCalls.git).toHaveBeenCalledTimes(1)
expect(hookCalls.fileWatch).toHaveBeenCalledTimes(1)
act(() => useAppStore.setState({ rightSidebarPeek: true }))
expect(hookCalls.git).toHaveBeenCalledTimes(2)
expect(hookCalls.fileWatch).toHaveBeenCalledTimes(2)
expect(parentRenderCount).toBe(1)
})
})
@@ -0,0 +1,14 @@
import { useEditorExternalWatch } from '@/hooks/useEditorExternalWatch'
import { useGitStatusPolling } from './useGitStatusPolling'
// Why: sidebar visibility drives both hooks; isolating their subscriptions in
// a leaf keeps transient edge peeks from re-rendering the entire App tree.
export function WorkspaceGitAndFileWatchGate({ enabled }: { enabled: boolean }): null {
// Why: conflict badges need Git status even with the sidebar closed; defer
// the first scan until session hydration keeps it off the startup path.
useGitStatusPolling({ enabled })
// Why: open editors must hear file changes regardless of which sidebar tab
// is mounted, so watcher ownership stays at the workspace level.
useEditorExternalWatch()
return null
}
@@ -220,22 +220,25 @@ describe('useRightSidebarEdgePeekDismiss', () => {
expect(setPeek).not.toHaveBeenCalled()
})
it('keeps the peek while the pointer uses portaled sidebar content', () => {
const setPeek = vi.fn()
const portalContent = document.createElement('div')
portalContent.dataset.slot = 'dropdown-menu-content'
document.body.appendChild(portalContent)
renderDismissHook({ setPeek })
it.each(['dropdown-menu-content', 'dialog-content'])(
'keeps the peek while the pointer uses portaled %s',
(portalSlot) => {
const setPeek = vi.fn()
const portalContent = document.createElement('div')
portalContent.dataset.slot = portalSlot
document.body.appendChild(portalContent)
renderDismissHook({ setPeek })
fireEvent.mouseMove(portalContent, { clientX: 400 })
vi.advanceTimersByTime(PEEK_CLOSE_DELAY_MS)
expect(setPeek).not.toHaveBeenCalled()
fireEvent.mouseMove(portalContent, { clientX: 400 })
vi.advanceTimersByTime(PEEK_CLOSE_DELAY_MS)
expect(setPeek).not.toHaveBeenCalled()
fireEvent.mouseMove(window, { clientX: 400 })
vi.advanceTimersByTime(PEEK_CLOSE_DELAY_MS)
expect(setPeek).toHaveBeenCalledWith(false)
portalContent.remove()
})
fireEvent.mouseMove(window, { clientX: 400 })
vi.advanceTimersByTime(PEEK_CLOSE_DELAY_MS)
expect(setPeek).toHaveBeenCalledWith(false)
portalContent.remove()
}
)
it('does not dismiss when the boundary was measured mid entrance animation', () => {
const setPeek = vi.fn()
@@ -13,6 +13,7 @@ const PEEK_TITLEBAR_ZONE_PX = 36
const PEEK_INTERACTIVE_PORTAL_SELECTOR = [
'[data-slot="context-menu-content"]',
'[data-slot="context-menu-sub-content"]',
'[data-slot="dialog-content"]',
'[data-slot="dropdown-menu-content"]',
'[data-slot="dropdown-menu-sub-content"]',
'[data-slot="hover-card-content"]',
+4 -1
View File
@@ -10266,7 +10266,10 @@
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance.",
"titleLong": "Hover over the right edge of the screen to quickly show this sidebar.",
"settingsPrefix": "Turn this off in",
"settingsLink": "Settings"
}
}
},
+4 -1
View File
@@ -10266,7 +10266,10 @@
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance.",
"titleLong": "Hover over the right edge of the screen to quickly show this sidebar.",
"settingsPrefix": "Turn this off in",
"settingsLink": "Settings"
}
}
},
+4 -1
View File
@@ -10266,7 +10266,10 @@
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance.",
"titleLong": "Hover over the right edge of the screen to quickly show this sidebar.",
"settingsPrefix": "Turn this off in",
"settingsLink": "Settings"
}
}
},
+4 -1
View File
@@ -10266,7 +10266,10 @@
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance.",
"titleLong": "Hover over the right edge of the screen to quickly show this sidebar.",
"settingsPrefix": "Turn this off in",
"settingsLink": "Settings"
}
}
},
+4 -1
View File
@@ -10266,7 +10266,10 @@
"peek": {
"tip": {
"title": "Hover the right edge to peek",
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance."
"description": "The right sidebar slides over the editor without reflowing it. Click the toggle or use the shortcut to pin it open. Change this anytime in Settings › Appearance.",
"titleLong": "Hover over the right edge of the screen to quickly show this sidebar.",
"settingsPrefix": "Turn this off in",
"settingsLink": "Settings"
}
}
},