mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
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:
@@ -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 />
|
||||
|
||||
@@ -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"]',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user