mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Make resource session rows jump to focused terminals
- Close the resource popover before activating a terminal session so focus can transfer cleanly. - Preserve worktree reveal behavior and flash the focused pane when a stable pane key matches. - Cover stale mappings, malformed pane keys, and keyboard activation with tests.
This commit is contained in:
@@ -40,7 +40,6 @@ import type { AppMemory, UsageValues, Worktree } from '../../../../shared/types'
|
||||
import { ORPHAN_WORKTREE_ID } from '../../../../shared/constants'
|
||||
import { isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import { isWorkspaceOldForCleanup } from '../../../../shared/workspace-cleanup'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import {
|
||||
mergeSnapshotAndSessions,
|
||||
UNATTRIBUTED_REPO_ID,
|
||||
@@ -52,6 +51,10 @@ import {
|
||||
} from './mergeSnapshotAndSessions'
|
||||
import { WorkspaceSpaceCompactPanel } from './WorkspaceSpaceCompactPanel'
|
||||
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './status-bar-context-menu-policy'
|
||||
import {
|
||||
isResourceSessionActivationKey,
|
||||
navigateResourceSessionToTab
|
||||
} from './resource-session-navigation'
|
||||
|
||||
const POLL_MS = 2_000
|
||||
const SESSIONS_POLL_MS = 10_000
|
||||
@@ -334,7 +337,7 @@ function SessionRow({
|
||||
onKeyDown={
|
||||
clickable
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
if (isResourceSessionActivationKey(e.key)) {
|
||||
e.preventDefault()
|
||||
handleClick()
|
||||
}
|
||||
@@ -658,7 +661,6 @@ export function ResourceUsageStatusSegment({
|
||||
const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
|
||||
const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
|
||||
const runtimePaneTitlesByTabId = useAppStore((s) => s.runtimePaneTitlesByTabId)
|
||||
const setActiveView = useAppStore((s) => s.setActiveView)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const openSpacePage = useAppStore((s) => s.openSpacePage)
|
||||
const activeView = useAppStore((s) => s.activeView)
|
||||
@@ -949,10 +951,9 @@ export function ResourceUsageStatusSegment({
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Why: navigation handlers leave the popover open so users can chain
|
||||
// multiple jumps (e.g. browse worktrees, hop between terminals) without
|
||||
// having to re-open the popover each time. The popover still closes via
|
||||
// its own outside-click / Escape handlers.
|
||||
// Why: worktree navigation leaves the popover open so users can browse the
|
||||
// tree without reopening it; bound terminal rows close explicitly because
|
||||
// focus transfer is intentionally suppressed by onFocusOutside below.
|
||||
const navigateToWorktree = useCallback((worktreeId: string): void => {
|
||||
if (worktreeId === ORPHAN_WORKTREE_ID || worktreeId.startsWith(`${UNATTRIBUTED_REPO_ID}::`)) {
|
||||
return
|
||||
@@ -962,21 +963,14 @@ export function ResourceUsageStatusSegment({
|
||||
|
||||
const navigateToTab = useCallback(
|
||||
(tabId: string, paneKey: string | null) => {
|
||||
// Resolve the tab → worktree from the store so we can also reveal the
|
||||
// worktree in the sidebar before flipping the active tab.
|
||||
for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) {
|
||||
if (tabs.some((t) => t.id === tabId)) {
|
||||
activateAndRevealWorktree(worktreeId)
|
||||
break
|
||||
}
|
||||
}
|
||||
setActiveView('terminal')
|
||||
// Why: paneKey suffixes are stable UUID leaf ids after replay/reload.
|
||||
// Legacy numeric keys degrade to tab-only activation instead of guessing.
|
||||
const parsed = paneKey ? parsePaneKey(paneKey) : null
|
||||
activateTabAndFocusPane(tabId, parsed?.tabId === tabId ? parsed.leafId : null)
|
||||
navigateResourceSessionToTab(tabId, paneKey, {
|
||||
tabsByWorktree,
|
||||
setOpen,
|
||||
activateAndRevealWorktree,
|
||||
activateTabAndFocusPane
|
||||
})
|
||||
},
|
||||
[tabsByWorktree, setActiveView]
|
||||
[tabsByWorktree]
|
||||
)
|
||||
|
||||
const deleteWorktree = useCallback((worktreeId: string): void => {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
isResourceSessionActivationKey,
|
||||
navigateResourceSessionToTab,
|
||||
type ResourceSessionNavigationDeps
|
||||
} from './resource-session-navigation'
|
||||
|
||||
const TAB_ID = 'tab-1'
|
||||
const OTHER_TAB_ID = 'tab-2'
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
function makeDeps(events: string[] = []): ResourceSessionNavigationDeps {
|
||||
return {
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: TAB_ID }],
|
||||
'wt-2': [{ id: OTHER_TAB_ID }]
|
||||
},
|
||||
setOpen: vi.fn((open: boolean) => events.push(`open:${open}`)),
|
||||
activateAndRevealWorktree: vi.fn((worktreeId: string) => {
|
||||
events.push(`worktree:${worktreeId}`)
|
||||
}),
|
||||
activateTabAndFocusPane: vi.fn((tabId: string, leafId: string | null) => {
|
||||
events.push(`tab:${tabId}:${leafId ?? 'null'}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('resource session navigation', () => {
|
||||
it('closes the popover first, reveals the owning worktree, and focuses a matching pane', () => {
|
||||
const events: string[] = []
|
||||
const deps = makeDeps(events)
|
||||
|
||||
navigateResourceSessionToTab(TAB_ID, `${TAB_ID}:${LEAF_ID}`, deps)
|
||||
|
||||
expect(events).toEqual([`open:false`, 'worktree:wt-1', `tab:${TAB_ID}:${LEAF_ID}`])
|
||||
expect(deps.activateTabAndFocusPane).toHaveBeenCalledWith(TAB_ID, LEAF_ID, {
|
||||
flashFocusedPane: true
|
||||
})
|
||||
})
|
||||
|
||||
it('degrades malformed and mismatched pane keys to tab-only activation', () => {
|
||||
const malformed = makeDeps()
|
||||
navigateResourceSessionToTab(TAB_ID, `${TAB_ID}:1`, malformed)
|
||||
expect(malformed.activateTabAndFocusPane).toHaveBeenCalledWith(TAB_ID, null, {
|
||||
flashFocusedPane: true
|
||||
})
|
||||
|
||||
const mismatched = makeDeps()
|
||||
navigateResourceSessionToTab(TAB_ID, `${OTHER_TAB_ID}:${LEAF_ID}`, mismatched)
|
||||
expect(mismatched.activateTabAndFocusPane).toHaveBeenCalledWith(TAB_ID, null, {
|
||||
flashFocusedPane: true
|
||||
})
|
||||
})
|
||||
|
||||
it('still activates the tab when the worktree mapping is stale', () => {
|
||||
const deps = makeDeps()
|
||||
deps.tabsByWorktree = {}
|
||||
|
||||
navigateResourceSessionToTab(TAB_ID, `${TAB_ID}:${LEAF_ID}`, deps)
|
||||
|
||||
expect(deps.activateAndRevealWorktree).not.toHaveBeenCalled()
|
||||
expect(deps.activateTabAndFocusPane).toHaveBeenCalledWith(TAB_ID, LEAF_ID, {
|
||||
flashFocusedPane: true
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps keyboard activation equivalent to click for Enter and Space only', () => {
|
||||
expect(isResourceSessionActivationKey('Enter')).toBe(true)
|
||||
expect(isResourceSessionActivationKey(' ')).toBe(true)
|
||||
expect(isResourceSessionActivationKey('Escape')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
|
||||
type TabLookup = Record<string, { id: string }[]>
|
||||
|
||||
export type ResourceSessionNavigationDeps = {
|
||||
tabsByWorktree: TabLookup
|
||||
setOpen: (open: boolean) => void
|
||||
activateAndRevealWorktree: (worktreeId: string) => unknown
|
||||
activateTabAndFocusPane: (
|
||||
tabId: string,
|
||||
leafId: string | null,
|
||||
opts: { flashFocusedPane: true }
|
||||
) => void
|
||||
}
|
||||
|
||||
export function isResourceSessionActivationKey(key: string): boolean {
|
||||
return key === 'Enter' || key === ' '
|
||||
}
|
||||
|
||||
export function navigateResourceSessionToTab(
|
||||
tabId: string,
|
||||
paneKey: string | null,
|
||||
deps: ResourceSessionNavigationDeps
|
||||
): void {
|
||||
deps.setOpen(false)
|
||||
|
||||
for (const [worktreeId, tabs] of Object.entries(deps.tabsByWorktree)) {
|
||||
if (tabs.some((tab) => tab.id === tabId)) {
|
||||
deps.activateAndRevealWorktree(worktreeId)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Why: paneKey suffixes are stable UUID leaf ids after replay/reload.
|
||||
// Legacy numeric keys degrade to tab-only activation instead of guessing.
|
||||
const parsed = paneKey ? parsePaneKey(paneKey) : null
|
||||
deps.activateTabAndFocusPane(tabId, parsed?.tabId === tabId ? parsed.leafId : null, {
|
||||
flashFocusedPane: true
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user