mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
fix(terminal): stabilize dominant park inputs
This commit is contained in:
@@ -10,6 +10,7 @@ import { shouldMountBackgroundWorktreeTab } from '../terminal/background-termina
|
||||
import { useNativeChatToggleShortcut } from '../native-chat/use-native-chat-toggle-shortcut'
|
||||
import { TerminalOverlaySlot } from './TerminalOverlaySlot'
|
||||
import { useTerminalTabColdParking } from './use-terminal-tab-cold-parking'
|
||||
import { useWorktreeParkTerminalAssignments } from './use-worktree-park-terminal-assignments'
|
||||
|
||||
type TerminalOverlayAssignment = {
|
||||
unifiedTabId: string
|
||||
@@ -99,11 +100,12 @@ const TerminalPaneOverlayLayer = memo(function TerminalPaneOverlayLayer({
|
||||
}
|
||||
return entries
|
||||
}, [groupActiveTabById, unifiedTabs])
|
||||
const parkingAssignments = useWorktreeParkTerminalAssignments(assignments, coldParkTerminalPanes)
|
||||
|
||||
const parkedTerminalTabIds = useTerminalTabColdParking({
|
||||
worktreeId,
|
||||
terminalTabs,
|
||||
assignments,
|
||||
assignments: parkingAssignments,
|
||||
isWorktreeActive,
|
||||
coldParkTerminalPanes,
|
||||
isForceParked,
|
||||
|
||||
+91
-9
@@ -1,5 +1,5 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
import { act } from 'react'
|
||||
import { act, StrictMode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types'
|
||||
@@ -73,6 +73,13 @@ vi.mock('./terminal-parked-tab-watchers', async () => {
|
||||
...tab,
|
||||
title: `${tab.id}-${harness.syncCalls}`
|
||||
}))
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
...state.unifiedTabsByWorktree,
|
||||
[args.worktreeId]: state.unifiedTabsByWorktree[args.worktreeId].map((tab) => ({
|
||||
...tab,
|
||||
label: `${tab.entityId}-${harness.syncCalls}`
|
||||
}))
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -94,7 +101,9 @@ const TAB_IDS = ['tab-a', 'tab-b'] as const
|
||||
const GROUP_ID = 'group-a'
|
||||
|
||||
type ParkingStoreState = {
|
||||
groupsByWorktree: Record<string, TabGroup[]>
|
||||
tabsByWorktree: Record<string, TerminalTab[]>
|
||||
unifiedTabsByWorktree: Record<string, Tab[]>
|
||||
}
|
||||
|
||||
const parkingStore = useAppStore as unknown as {
|
||||
@@ -133,12 +142,14 @@ function renderProductionLayer(root: Root, coldParkTerminalPanes = true): unknow
|
||||
try {
|
||||
act(() => {
|
||||
root.render(
|
||||
<TerminalPaneOverlayLayer
|
||||
worktreeId={harness.worktreeId}
|
||||
worktreePath="cold-park-pre-gate"
|
||||
isWorktreeActive={false}
|
||||
coldParkTerminalPanes={coldParkTerminalPanes}
|
||||
/>
|
||||
<StrictMode>
|
||||
<TerminalPaneOverlayLayer
|
||||
worktreeId={harness.worktreeId}
|
||||
worktreePath="cold-park-pre-gate"
|
||||
isWorktreeActive={false}
|
||||
coldParkTerminalPanes={coldParkTerminalPanes}
|
||||
/>
|
||||
</StrictMode>
|
||||
)
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -194,7 +205,7 @@ describe('TerminalPaneOverlayLayer cold-park pre-gate loop', () => {
|
||||
const thrown = renderProductionLayer(root)
|
||||
|
||||
expect(thrown).toBeNull()
|
||||
expect(harness.coverageCalls).toBe(1)
|
||||
expect(harness.coverageCalls).toBe(2)
|
||||
expect(harness.syncCalls).toBeLessThan(10)
|
||||
expect(harness.renderedParkedSets).not.toHaveLength(0)
|
||||
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
|
||||
@@ -215,13 +226,84 @@ describe('TerminalPaneOverlayLayer cold-park pre-gate loop', () => {
|
||||
})
|
||||
})
|
||||
|
||||
expect(harness.coverageCalls).toBe(2)
|
||||
expect(harness.coverageCalls).toBe(3)
|
||||
expect(harness.syncCalls).toBeGreaterThan(settledSyncCalls)
|
||||
expect(new Set(harness.renderedParkedSets.map((ids) => ids.join(',')))).toEqual(
|
||||
new Set([TAB_IDS.join(',')])
|
||||
)
|
||||
expect(harness.slotRenders).toBe(0)
|
||||
|
||||
const beforePendingSpawnSyncCalls = harness.syncCalls
|
||||
const pendingSpawnState = parkingStore.getState()
|
||||
act(() => {
|
||||
parkingStore.setState({
|
||||
tabsByWorktree: {
|
||||
...pendingSpawnState.tabsByWorktree,
|
||||
[harness.worktreeId]: pendingSpawnState.tabsByWorktree[harness.worktreeId].map((tab) =>
|
||||
tab.id === TAB_IDS[1] ? { ...tab, pendingActivationSpawn: 1 } : tab
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
expect(harness.syncCalls).toBeGreaterThan(beforePendingSpawnSyncCalls)
|
||||
|
||||
const beforeTabOrderSyncCalls = harness.syncCalls
|
||||
const tabOrderState = parkingStore.getState()
|
||||
act(() => {
|
||||
parkingStore.setState({
|
||||
tabsByWorktree: {
|
||||
...tabOrderState.tabsByWorktree,
|
||||
[harness.worktreeId]: tabOrderState.tabsByWorktree[harness.worktreeId].toReversed()
|
||||
}
|
||||
})
|
||||
})
|
||||
expect(harness.syncCalls).toBeGreaterThan(beforeTabOrderSyncCalls)
|
||||
|
||||
const beforeActiveTabChangeSyncCalls = harness.syncCalls
|
||||
const activeTabState = parkingStore.getState()
|
||||
act(() => {
|
||||
parkingStore.setState({
|
||||
groupsByWorktree: {
|
||||
...activeTabState.groupsByWorktree,
|
||||
[harness.worktreeId]: activeTabState.groupsByWorktree[harness.worktreeId].map(
|
||||
(group) => ({
|
||||
...group,
|
||||
activeTabId: `unified-${TAB_IDS[1]}`
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
expect(harness.syncCalls).toBeGreaterThan(beforeActiveTabChangeSyncCalls)
|
||||
|
||||
const beforeGroupChangeSyncCalls = harness.syncCalls
|
||||
const groupState = parkingStore.getState()
|
||||
const nextGroupId = 'group-b'
|
||||
act(() => {
|
||||
parkingStore.setState({
|
||||
groupsByWorktree: {
|
||||
...groupState.groupsByWorktree,
|
||||
[harness.worktreeId]: [
|
||||
...groupState.groupsByWorktree[harness.worktreeId],
|
||||
{
|
||||
id: nextGroupId,
|
||||
worktreeId: harness.worktreeId,
|
||||
activeTabId: `unified-${TAB_IDS[1]}`,
|
||||
tabOrder: [`unified-${TAB_IDS[1]}`],
|
||||
recentTabIds: [`unified-${TAB_IDS[1]}`]
|
||||
}
|
||||
]
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
...groupState.unifiedTabsByWorktree,
|
||||
[harness.worktreeId]: groupState.unifiedTabsByWorktree[harness.worktreeId].map((tab) =>
|
||||
tab.entityId === TAB_IDS[1] ? { ...tab, groupId: nextGroupId } : tab
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
expect(harness.syncCalls).toBeGreaterThan(beforeGroupChangeSyncCalls)
|
||||
|
||||
expect(renderProductionLayer(root, false)).toBeNull()
|
||||
expect(harness.slotRenders).toBeGreaterThan(0)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
type WorktreeParkTerminalAssignment = {
|
||||
unifiedTabId: string
|
||||
groupId: string
|
||||
isActiveInGroup: boolean
|
||||
}
|
||||
|
||||
function getTerminalParkingAssignmentsKey(
|
||||
assignments: ReadonlyMap<string, WorktreeParkTerminalAssignment>
|
||||
): string {
|
||||
return JSON.stringify(
|
||||
Array.from(assignments, ([tabId, assignment]) => [
|
||||
tabId,
|
||||
assignment.unifiedTabId,
|
||||
assignment.groupId,
|
||||
assignment.isActiveInGroup
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
export function useWorktreeParkTerminalAssignments(
|
||||
assignments: ReadonlyMap<string, WorktreeParkTerminalAssignment>,
|
||||
coldParkTerminalPanes: boolean
|
||||
): ReadonlyMap<string, WorktreeParkTerminalAssignment> {
|
||||
const parkingAssignmentsKey = coldParkTerminalPanes
|
||||
? getTerminalParkingAssignmentsKey(assignments)
|
||||
: assignments
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- decorative unified labels must not retrigger dominated pre-gate work.
|
||||
return useMemo(() => assignments, [parkingAssignmentsKey])
|
||||
}
|
||||
@@ -1,32 +1,17 @@
|
||||
import { useRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import type { TerminalTab } from '../../../../shared/types'
|
||||
|
||||
function haveSameTerminalParkingInputs(
|
||||
left: readonly TerminalTab[],
|
||||
right: readonly TerminalTab[]
|
||||
): boolean {
|
||||
return (
|
||||
left.length === right.length &&
|
||||
left.every(
|
||||
(tab, index) =>
|
||||
tab.id === right[index].id &&
|
||||
tab.ptyId === right[index].ptyId &&
|
||||
tab.pendingActivationSpawn === right[index].pendingActivationSpawn
|
||||
)
|
||||
)
|
||||
function getTerminalParkingInputsKey(terminalTabs: readonly TerminalTab[]): string {
|
||||
return JSON.stringify(terminalTabs.map((tab) => [tab.id, tab.ptyId, tab.pendingActivationSpawn]))
|
||||
}
|
||||
|
||||
export function useWorktreeParkTerminalTabs(
|
||||
terminalTabs: readonly TerminalTab[],
|
||||
coldParkTerminalPanes: boolean
|
||||
): readonly TerminalTab[] {
|
||||
const stableTabsRef = useRef(terminalTabs)
|
||||
// Why: a dominant worktree park hides pre-gate churn from final-verdict damping.
|
||||
if (
|
||||
!coldParkTerminalPanes ||
|
||||
!haveSameTerminalParkingInputs(stableTabsRef.current, terminalTabs)
|
||||
) {
|
||||
stableTabsRef.current = terminalTabs
|
||||
}
|
||||
return stableTabsRef.current
|
||||
const parkingInputsKey = coldParkTerminalPanes
|
||||
? getTerminalParkingInputsKey(terminalTabs)
|
||||
: terminalTabs
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- decorative tab publications must not retrigger dominated pre-gate work.
|
||||
return useMemo(() => terminalTabs, [parkingInputsKey])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user