mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
Fix Cmd+J recent order when terminal entities hydrate late
Unified tabs can appear before tabsByWorktree entities on restore, which latched an all-IDLE ranking and buried blocked chats until reopen. Keep a provisional freeze, then re-capture once entities arrive.
This commit is contained in:
@@ -767,6 +767,36 @@ describe('WorktreeJumpPalette recent chats & terminals', () => {
|
||||
expect(getCommandValue()).toBe(movedTo)
|
||||
})
|
||||
|
||||
it('re-ranks once when terminal entities hydrate after unified tabs', async () => {
|
||||
// Why split hydration: unified tabs can land before tabsByWorktree; without a re-capture every
|
||||
// row ranks IDLE and the blocked chat stays under focus recency until reopen.
|
||||
const hydrated = makeRecentTabState({
|
||||
agentStatusByPaneKey: {
|
||||
[`term-alpha:${LEAF_ID}`]: makeAgentEntry('term-alpha', 'blocked', Date.now())
|
||||
},
|
||||
lastVisitedAtByWorktreeId: { 'wt-beta': Date.now() }
|
||||
})
|
||||
await renderPalette({ ...hydrated, tabsByWorktree: {} })
|
||||
expect(getTabRowIds()).toEqual(['tab-beta', 'tab-alpha'])
|
||||
|
||||
await act(async () => {
|
||||
useAppStore.setState({ tabsByWorktree: hydrated.tabsByWorktree } as Partial<AppState>)
|
||||
})
|
||||
await flushEffects()
|
||||
expect(getTabRowIds()).toEqual(['tab-alpha', 'tab-beta'])
|
||||
|
||||
// Why: incomplete→complete re-rank is one-shot — later status churn still freezes positions.
|
||||
await act(async () => {
|
||||
useAppStore.setState({
|
||||
agentStatusByPaneKey: {
|
||||
[`term-beta:${LEAF_ID}`]: makeAgentEntry('term-beta', 'blocked', Date.now())
|
||||
}
|
||||
} as Partial<AppState>)
|
||||
})
|
||||
await flushEffects()
|
||||
expect(getTabRowIds()).toEqual(['tab-alpha', 'tab-beta'])
|
||||
})
|
||||
|
||||
it('ranks a blocked agent above a more recently visited idle tab', async () => {
|
||||
await renderPalette(
|
||||
makeRecentTabState({
|
||||
|
||||
@@ -1046,20 +1046,48 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
||||
// send ⌘3 to the wrong row; dots keep updating, positions don't.
|
||||
const [recentTabOrder, setRecentTabOrder] = useState<readonly string[]>(EMPTY_RECENT_TAB_ORDER)
|
||||
const recentTabOrderCapturedRef = useRef(false)
|
||||
// Why: unified tabs can land before tabsByWorktree entities. A capture then ranks every chat as
|
||||
// IDLE; allow one re-capture when entities arrive, then freeze for good.
|
||||
const recentTabOrderAttentionReadyRef = useRef(false)
|
||||
// Terminal rows without a tabsByWorktree entity can't resolve attention yet (see orderRecent…).
|
||||
const recentOrderAttentionIncomplete = useMemo(() => {
|
||||
for (const item of openTabItems) {
|
||||
if (item.type !== 'workspace-tab' || item.result.contentType !== 'terminal') {
|
||||
continue
|
||||
}
|
||||
const worktree = worktreeMap.get(item.result.worktreeId)
|
||||
if (!worktree || worktree.isArchived || isCurrentOpenTabItem(item)) {
|
||||
continue
|
||||
}
|
||||
if (!terminalTabsById.has(item.result.entityId)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, [openTabItems, terminalTabsById, worktreeMap])
|
||||
// Why layout, not passive: a post-paint capture shows one frame of worktrees-only, which flashes
|
||||
// the list, renumbers ⌘1–6 under the user, and lets cmdk latch a worktree as the Enter target.
|
||||
useLayoutEffect(() => {
|
||||
if (!visible) {
|
||||
recentTabOrderCapturedRef.current = false
|
||||
recentTabOrderAttentionReadyRef.current = false
|
||||
autoSelectedItemIdRef.current = null
|
||||
setRecentTabOrder(EMPTY_RECENT_TAB_ORDER)
|
||||
return
|
||||
}
|
||||
// Why: the query and filter are cleared by the open effect below, which runs after this one —
|
||||
// capturing before that lands would freeze the *previous* session's filtered subset for good.
|
||||
if (recentTabOrderCapturedRef.current || hasQuery || query.length > 0 || filterActive) {
|
||||
if (hasQuery || query.length > 0 || filterActive) {
|
||||
return
|
||||
}
|
||||
// Fully frozen after an attention-ready capture; provisional freeze while entities still pending
|
||||
// so agent-status churn can't reshuffle under the cursor before the one-shot re-rank.
|
||||
if (recentTabOrderCapturedRef.current) {
|
||||
if (recentTabOrderAttentionReadyRef.current || recentOrderAttentionIncomplete) {
|
||||
return
|
||||
}
|
||||
// Incomplete → complete: fall through and re-capture with real attention ranks.
|
||||
}
|
||||
const order = orderRecentWorkspaceTabs({
|
||||
rows: recentTabRows,
|
||||
paneSources: recentTabPaneSources,
|
||||
@@ -1075,6 +1103,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
||||
return
|
||||
}
|
||||
recentTabOrderCapturedRef.current = true
|
||||
recentTabOrderAttentionReadyRef.current = !recentOrderAttentionIncomplete
|
||||
setRecentTabOrder(order)
|
||||
// Why: recents render above the worktrees, so a row auto-selected before they arrived is no
|
||||
// longer the list head — hand Enter back to the top, matching ⌘1. Untouched selections only:
|
||||
@@ -1089,6 +1118,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
||||
hasQuery,
|
||||
lastVisitedAtByWorktreeId,
|
||||
query.length,
|
||||
recentOrderAttentionIncomplete,
|
||||
recentTabPaneSources,
|
||||
recentTabRows,
|
||||
visible
|
||||
|
||||
Reference in New Issue
Block a user