mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
Open existing agent chats and retained panes from their rows (#34650)
* fix(agents): open existing chat and retained pane destinations * fix: keep agent navigation scoped to the current destination owner * test: initialize dashboard tabs without a type assertion * fix: complete navigation test and English catalog contracts
This commit is contained in:
@@ -4337,9 +4337,6 @@
|
||||
"dashboardPopout.card.you": {
|
||||
"ko": "나"
|
||||
},
|
||||
"dashboardPopout.terminal.closed": {
|
||||
"ko": "실시간 터미널이 없습니다 — 이 에이전트의 창이 닫혔습니다."
|
||||
},
|
||||
"dashboardPopout.terminal.focusWorktree": {
|
||||
"ko": "워크트리 열기"
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('AgentTerminalDialog', () => {
|
||||
expect(screen.queryByText(/pane has closed/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('still reports a closed pane for a local card with no live pty', () => {
|
||||
it('keeps a local agent openable without claiming its missing preview means closure', () => {
|
||||
render(
|
||||
<AgentTerminalDialog
|
||||
card={card({ ptyId: null, hostKind: 'local' })}
|
||||
@@ -111,7 +111,9 @@ describe('AgentTerminalDialog', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText(/pane has closed/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/No preview available/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/pane has closed/)).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Open worktree' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('labels acknowledged completions idle without review or pin controls', () => {
|
||||
|
||||
@@ -669,7 +669,7 @@ describe('AgentTerminalPreview', () => {
|
||||
expect(terminalHarness.instances).toHaveLength(1)
|
||||
expect(terminal.dispose).not.toHaveBeenCalled()
|
||||
expect(terminal.reset).not.toHaveBeenCalled()
|
||||
expect(view.queryByText(/No live terminal/)).not.toBeInTheDocument()
|
||||
expect(view.queryByText(/No preview available/)).not.toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
resolveRefresh({
|
||||
@@ -681,7 +681,7 @@ describe('AgentTerminalPreview', () => {
|
||||
expect(terminal.resize).toHaveBeenCalledWith(100, 30)
|
||||
expect(terminalHarness.instances).toHaveLength(1)
|
||||
expect(terminal.dispose).not.toHaveBeenCalled()
|
||||
expect(view.queryByText(/No live terminal/)).not.toBeInTheDocument()
|
||||
expect(view.queryByText(/No preview available/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('disposes a stale terminal when resync confirms the pty is gone', async () => {
|
||||
@@ -697,7 +697,7 @@ describe('AgentTerminalPreview', () => {
|
||||
|
||||
act(() => emitData?.({ type: 'resync', ptyId: 'pty-1' }))
|
||||
|
||||
await waitFor(() => expect(view.getByText(/No live terminal/)).toBeInTheDocument())
|
||||
await waitFor(() => expect(view.getByText(/No preview available/)).toBeInTheDocument())
|
||||
expect(terminal.dispose).toHaveBeenCalledTimes(1)
|
||||
expect(terminalHarness.userInputDispose).toHaveBeenCalledTimes(1)
|
||||
expect(unsubscribe).toHaveBeenCalledWith('pty-1')
|
||||
@@ -717,13 +717,13 @@ describe('AgentTerminalPreview', () => {
|
||||
replay: []
|
||||
})
|
||||
const view = render(<AgentTerminalPreview ptyId="pty-gone" />)
|
||||
await waitFor(() => expect(view.getByText(/No live terminal/)).toBeInTheDocument())
|
||||
await waitFor(() => expect(view.getByText(/No preview available/)).toBeInTheDocument())
|
||||
|
||||
view.rerender(<AgentTerminalPreview ptyId="pty-live" />)
|
||||
|
||||
await waitFor(() => expect(terminalHarness.instances).toHaveLength(1))
|
||||
expect(connect).toHaveBeenLastCalledWith('pty-live', { scrollbackRows: 24 })
|
||||
expect(view.queryByText(/No live terminal/)).not.toBeInTheDocument()
|
||||
expect(view.queryByText(/No preview available/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('claims a grid sized to the dialog box and never re-requests an unchanged target', async () => {
|
||||
@@ -810,6 +810,6 @@ describe('AgentTerminalPreview', () => {
|
||||
Object.assign(window.api.terminalPreview, { connect: async () => undefined })
|
||||
const view = render(<AgentTerminalPreview ptyId="pty-1" />)
|
||||
|
||||
await waitFor(() => expect(view.getByText(/No live terminal/)).toBeInTheDocument())
|
||||
await waitFor(() => expect(view.getByText(/No preview available/)).toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ export function terminalPreviewUnavailableMessage(source: {
|
||||
'No preview for this remote session — open the workspace to view the terminal.'
|
||||
)
|
||||
: translate(
|
||||
'dashboardPopout.terminal.closed',
|
||||
"No live terminal — this agent's pane has closed."
|
||||
'dashboardPopout.terminal.previewUnavailable',
|
||||
'No preview available — open the workspace to view this agent.'
|
||||
)
|
||||
}
|
||||
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
|
||||
import {
|
||||
makeCreatedAgentWorktree,
|
||||
seedAlreadyActiveWorktree
|
||||
} from '@/lib/worktree-activation-created-agent.test-fixture'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import type * as RuntimeRpcClient from '@/runtime/runtime-rpc-client'
|
||||
import type * as WebRuntimeSessionEnvironment from '@/runtime/web-runtime-session-environment'
|
||||
import { AgentKanbanBoard } from '../dashboard-popout/AgentKanbanBoard'
|
||||
import WorktreeCardAgents from '../sidebar/WorktreeCardAgents'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { buildDashboardSnapshot } from './build-dashboard-snapshot'
|
||||
import { revealDashboardAgent } from './reveal-dashboard-agent'
|
||||
import { activateRetainedAgentRow } from '../sidebar/activate-retained-agent-row'
|
||||
|
||||
const effects = vi.hoisted(() => ({
|
||||
rpc: vi.fn(async () => ({ ok: true })),
|
||||
composerFocus: vi.fn(),
|
||||
environmentCall: vi.fn(async (_environmentId: string, _args: unknown) => ({
|
||||
id: 'navigation-test',
|
||||
ok: false as const,
|
||||
error: { code: 'unavailable', message: 'Unit-test transport boundary' }
|
||||
}))
|
||||
}))
|
||||
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof RuntimeRpcClient>()),
|
||||
callRuntimeRpc: effects.rpc
|
||||
}))
|
||||
vi.mock('@/components/native-chat/use-native-chat-composer-reveal-focus', () => ({
|
||||
requestNativeChatComposerFocus: effects.composerFocus
|
||||
}))
|
||||
vi.mock('../dashboard-popout/AgentTerminalPreview', () => ({ AgentTerminalPreview: () => null }))
|
||||
vi.mock('@/runtime/web-runtime-session-environment', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WebRuntimeSessionEnvironment>()),
|
||||
captureRuntimeEnvironmentCall: (environmentId: string) => (args: unknown) =>
|
||||
effects.environmentCall(environmentId, args)
|
||||
}))
|
||||
|
||||
const initialState = useAppStore.getState()
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
const WORKSPACE_HOST = 'runtime:host-A' as const
|
||||
const TAB_HOST = 'runtime:host-B' as const
|
||||
const NOW = 1_000_000_000
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
useAppStore.setState(initialState, true)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Dashboard and retained sidebar destination contract', () => {
|
||||
it.each([
|
||||
['worktree', 'native'],
|
||||
['folder', 'native'],
|
||||
['worktree', 'terminal'],
|
||||
['folder', 'terminal']
|
||||
] as const)(
|
||||
'opens the exact %s %s destination and preserves reveal failure',
|
||||
async (scope, surface) => {
|
||||
const native = surface === 'native'
|
||||
const owner = native ? TAB_HOST : WORKSPACE_HOST
|
||||
const worktree = {
|
||||
...makeCreatedAgentWorktree(),
|
||||
hostId: WORKSPACE_HOST,
|
||||
...(scope === 'folder' ? { id: folderWorkspaceKey('folder-1') } : {})
|
||||
}
|
||||
seedAlreadyActiveWorktree(worktree)
|
||||
const seeded = useAppStore.getState()
|
||||
const terminal = { ...seeded.tabsByWorktree[worktree.id][0], executionHostId: owner }
|
||||
const tabId = native ? 'native-B' : terminal.id
|
||||
const paneKey = makePaneKey(tabId, LEAF)
|
||||
const group = seeded.groupsByWorktree[worktree.id][0]
|
||||
const nativeTab: Tab = {
|
||||
id: tabId,
|
||||
worktreeId: worktree.id,
|
||||
executionHostId: TAB_HOST,
|
||||
contentType: 'agent-session',
|
||||
entityId: 'session-B',
|
||||
groupId: group.id,
|
||||
label: 'Codex',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 1,
|
||||
createdAt: NOW
|
||||
}
|
||||
const folder = {
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'project-1',
|
||||
name: 'Notes',
|
||||
folderPath: '/workspace/notes',
|
||||
executionHostId: WORKSPACE_HOST,
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: NOW,
|
||||
createdAt: NOW,
|
||||
updatedAt: NOW
|
||||
}
|
||||
useAppStore.setState({
|
||||
activeWorktreeId: null,
|
||||
activeWorkspaceExecutionHostId: null,
|
||||
activeTabType: 'editor',
|
||||
agentActivityDisplayMode: 'compact',
|
||||
repos: scope === 'folder' ? [] : seeded.repos,
|
||||
worktreesByRepo:
|
||||
scope === 'folder'
|
||||
? {}
|
||||
: {
|
||||
[worktree.repoId]: [
|
||||
worktree,
|
||||
...(native ? [{ ...worktree, hostId: TAB_HOST }] : [])
|
||||
]
|
||||
},
|
||||
folderWorkspaces:
|
||||
scope === 'folder'
|
||||
? [folder, ...(native ? [{ ...folder, executionHostId: TAB_HOST }] : [])]
|
||||
: [],
|
||||
getFreshFolderWorkspacePathStatus: () => ({ path: folder.folderPath, exists: true }),
|
||||
tabsByWorktree: { [worktree.id]: [terminal] },
|
||||
unifiedTabsByWorktree: {
|
||||
[worktree.id]: [
|
||||
...seeded.unifiedTabsByWorktree[worktree.id],
|
||||
...(native ? [nativeTab] : [])
|
||||
]
|
||||
},
|
||||
groupsByWorktree: {
|
||||
[worktree.id]: [{ ...group, tabOrder: [...group.tabOrder, ...(native ? [tabId] : [])] }]
|
||||
},
|
||||
terminalLayoutsByTabId: native
|
||||
? {}
|
||||
: {
|
||||
[tabId]: {
|
||||
root: { type: 'leaf', leafId: LEAF },
|
||||
activeLeafId: LEAF,
|
||||
expandedLeafId: null
|
||||
}
|
||||
},
|
||||
retainedAgentsByPaneKey: {
|
||||
[paneKey]: {
|
||||
entry: {
|
||||
paneKey,
|
||||
tabId,
|
||||
worktreeId: worktree.id,
|
||||
state: 'done',
|
||||
agentType: 'codex',
|
||||
prompt: 'Open this conversation',
|
||||
updatedAt: NOW,
|
||||
stateStartedAt: NOW,
|
||||
stateHistory: []
|
||||
},
|
||||
worktreeId: worktree.id,
|
||||
tab: { ...terminal, id: tabId, ptyId: native ? null : terminal.ptyId },
|
||||
agentType: 'codex',
|
||||
startedAt: NOW
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const snapshot = buildDashboardSnapshot(
|
||||
{ ...useAppStore.getState(), chatNames: new Map() },
|
||||
NOW
|
||||
)
|
||||
const card = snapshot.cards.find((candidate) => candidate.executionHostId === WORKSPACE_HOST)
|
||||
expect(card).toMatchObject({ worktreeId: worktree.id, tabId, ptyId: null })
|
||||
if (!card) {
|
||||
throw new Error('Expected a card from the production snapshot builder')
|
||||
}
|
||||
const view = render(
|
||||
<TooltipProvider>
|
||||
<AgentKanbanBoard
|
||||
snapshot={{ ...snapshot, cards: [card] }}
|
||||
onAckAgent={() => {}}
|
||||
onRevealAgent={revealDashboardAgent}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: /Open this conversation/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open worktree' }))
|
||||
|
||||
const assertDestination = async (): Promise<void> => {
|
||||
const state = useAppStore.getState()
|
||||
expect(state.activeWorktreeId).toBe(worktree.id)
|
||||
expect(getExecutionHostIdForWorktree(state, worktree.id)).toBe(owner)
|
||||
expect(state.activeTabType).toBe(native ? 'agent-session' : 'terminal')
|
||||
if (!native) {
|
||||
expect(state.activeTabId).toBe(tabId)
|
||||
}
|
||||
expect(state.getActiveTab(worktree.id)?.id).toBe(tabId)
|
||||
expect(state.groupsByWorktree[worktree.id][0].activeTabId).toBe(tabId)
|
||||
if (native) {
|
||||
expect(effects.composerFocus).toHaveBeenLastCalledWith(tabId)
|
||||
expect(effects.rpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'host-B' },
|
||||
'session.tabs.activate',
|
||||
expect.objectContaining({ tabId: 'agent-session:session-B' })
|
||||
)
|
||||
} else {
|
||||
await vi.waitFor(() =>
|
||||
expect(effects.environmentCall).toHaveBeenCalledWith(
|
||||
'host-A',
|
||||
expect.objectContaining({
|
||||
method: 'session.tabs.activate',
|
||||
params: expect.objectContaining({ leafId: LEAF })
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
await assertDestination()
|
||||
view.unmount()
|
||||
|
||||
useAppStore.setState({
|
||||
activeWorktreeId: null,
|
||||
activeWorkspaceExecutionHostId: null,
|
||||
activeTabType: 'editor',
|
||||
activeTabId: null
|
||||
})
|
||||
const sidebar = render(
|
||||
<TooltipProvider>
|
||||
<WorktreeCardAgents worktreeId={worktree.id} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
const row = sidebar.container.querySelector('.compact-agent-row')
|
||||
if (!row) {
|
||||
throw new Error('Expected the retained sidebar row')
|
||||
}
|
||||
effects.environmentCall.mockClear()
|
||||
fireEvent.click(row)
|
||||
await assertDestination()
|
||||
|
||||
// Keep the exact tab alive while its workspace disappears: neither entry may focus or notify it.
|
||||
cleanup()
|
||||
useAppStore.setState({
|
||||
worktreesByRepo: {},
|
||||
folderWorkspaces: [],
|
||||
activeWorktreeId: null,
|
||||
activeWorkspaceExecutionHostId: null,
|
||||
activeTabType: 'editor',
|
||||
activeTabId: null
|
||||
})
|
||||
effects.rpc.mockClear()
|
||||
effects.composerFocus.mockClear()
|
||||
effects.environmentCall.mockClear()
|
||||
expect(revealDashboardAgent(card)).toBe(false)
|
||||
const missing = render(
|
||||
<TooltipProvider>
|
||||
<WorktreeCardAgents worktreeId={worktree.id} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
const missingRow = missing.container.querySelector('.compact-agent-row')
|
||||
if (!missingRow) {
|
||||
throw new Error('Expected the retained row after workspace removal')
|
||||
}
|
||||
fireEvent.click(missingRow)
|
||||
expect(useAppStore.getState()).toMatchObject({
|
||||
activeWorktreeId: null,
|
||||
activeTabId: null,
|
||||
activeTabType: 'editor'
|
||||
})
|
||||
expect(effects.composerFocus).not.toHaveBeenCalled()
|
||||
expect(effects.rpc).not.toHaveBeenCalled()
|
||||
expect(effects.environmentCall).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('notifies the exact live retained terminal owner instead of the workspace default', async () => {
|
||||
const worktree = { ...makeCreatedAgentWorktree(), hostId: WORKSPACE_HOST }
|
||||
seedAlreadyActiveWorktree(worktree, {
|
||||
worktreesByRepo: { [worktree.repoId]: [worktree, { ...worktree, hostId: TAB_HOST }] },
|
||||
activeWorkspaceExecutionHostId: WORKSPACE_HOST,
|
||||
ptyIdsByTabId: { 'tab-1': ['remote:host-B@@pty-B'] },
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf', leafId: LEAF },
|
||||
activeLeafId: LEAF,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF]: 'remote:host-B@@pty-B' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
activateRetainedAgentRow(worktree.id, 'tab-1', makePaneKey('tab-1', LEAF))
|
||||
expect(useAppStore.getState().activeWorkspaceExecutionHostId).toBe(TAB_HOST)
|
||||
await vi.waitFor(() =>
|
||||
expect(effects.environmentCall).toHaveBeenCalledWith(
|
||||
'host-B',
|
||||
expect.objectContaining({
|
||||
method: 'session.tabs.activate',
|
||||
params: expect.objectContaining({ leafId: LEAF })
|
||||
})
|
||||
)
|
||||
)
|
||||
expect(
|
||||
effects.environmentCall.mock.calls.every(([environmentId]) => environmentId === 'host-B')
|
||||
).toBe(true)
|
||||
expect(useAppStore.getState().getActiveTab(worktree.id)?.id).toBe('tab-1')
|
||||
})
|
||||
})
|
||||
@@ -1,57 +1,179 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createElement } from 'react'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
activateAndRevealWorkspace: vi.fn(),
|
||||
activateTabAndFocusPane: vi.fn(),
|
||||
activateTerminalTabOnOwner: vi.fn()
|
||||
activateTerminalTabOnOwner: vi.fn(),
|
||||
callRuntimeRpc: vi.fn(async () => ({ ok: true })),
|
||||
requestNativeChatComposerFocus: vi.fn(),
|
||||
focusGroup: vi.fn(),
|
||||
activateTab: vi.fn(),
|
||||
setActiveTabType: vi.fn(),
|
||||
tabs: new Array<Tab>()
|
||||
}))
|
||||
|
||||
const getState = () => ({
|
||||
unifiedTabsByWorktree: { 'wt-1': mocks.tabs },
|
||||
focusGroup: mocks.focusGroup,
|
||||
activateTab: mocks.activateTab,
|
||||
setActiveTabType: mocks.setActiveTabType
|
||||
})
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: () => getState() } }))
|
||||
vi.mock('@/store/index', () => ({ useAppStore: { getState: () => getState() } }))
|
||||
vi.mock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorkspace: mocks.activateAndRevealWorkspace
|
||||
}))
|
||||
vi.mock('@/lib/worktree-runtime-owner', () => ({
|
||||
runtimeEnvironmentIdOrLocalUnlessTwin: () => 'host-A'
|
||||
}))
|
||||
vi.mock('@/lib/activate-tab-and-focus-pane', () => ({
|
||||
activateTabAndFocusPane: mocks.activateTabAndFocusPane
|
||||
}))
|
||||
vi.mock('@/lib/terminal-tab-owner-activation', () => ({
|
||||
activateTerminalTabOnOwner: mocks.activateTerminalTabOnOwner
|
||||
}))
|
||||
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc }))
|
||||
vi.mock('@/components/native-chat/use-native-chat-composer-reveal-focus', () => ({
|
||||
requestNativeChatComposerFocus: mocks.requestNativeChatComposerFocus
|
||||
}))
|
||||
vi.mock('@/runtime/runtime-worktree-selector', () => ({
|
||||
toRuntimeWorktreeSelector: (id: string) => `id:${id}`
|
||||
}))
|
||||
|
||||
vi.mock('../dashboard-popout/AgentTerminalPreview', () => ({ AgentTerminalPreview: () => null }))
|
||||
|
||||
import { AgentKanbanBoard } from '../dashboard-popout/AgentKanbanBoard'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { revealDashboardAgent } from './reveal-dashboard-agent'
|
||||
|
||||
const args = {
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
executionHostId: 'runtime:env-1' as const,
|
||||
tabId: 'web-terminal-host-tab',
|
||||
executionHostId: 'runtime:host-A' as const,
|
||||
tabId: 'agent-tab',
|
||||
leafId: '11111111-1111-4111-8111-111111111111'
|
||||
}
|
||||
function nativeTab(agent: 'claude' | 'codex'): Tab {
|
||||
return {
|
||||
id: args.tabId,
|
||||
worktreeId: args.worktreeId,
|
||||
executionHostId: 'runtime:host-B',
|
||||
contentType: 'agent-session',
|
||||
entityId: 'session-1',
|
||||
groupId: 'group-1',
|
||||
label: agent,
|
||||
agentSessionAgent: agent,
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('revealDashboardAgent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.tabs = []
|
||||
mocks.activateAndRevealWorkspace.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it("focuses the card's own tab on the workspace's owner", () => {
|
||||
mocks.activateAndRevealWorkspace.mockReturnValue(true)
|
||||
it.each(['claude', 'codex'] as const)(
|
||||
'opens an existing %s chat without a preview PTY on its own host',
|
||||
(agent) => {
|
||||
mocks.tabs = [nativeTab(agent)]
|
||||
expect(revealDashboardAgent({ ...args, leafId: null })).toBe(true)
|
||||
expect(mocks.activateAndRevealWorkspace).toHaveBeenCalledWith('wt-1', {
|
||||
executionHostId: 'runtime:host-B'
|
||||
})
|
||||
expect(mocks.focusGroup).toHaveBeenCalledWith('wt-1', 'group-1')
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith(args.tabId, { worktreeId: 'wt-1' })
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('agent-session', 'wt-1')
|
||||
expect(mocks.requestNativeChatComposerFocus).toHaveBeenCalledWith(args.tabId)
|
||||
expect(mocks.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'host-B' },
|
||||
'session.tabs.activate',
|
||||
{ worktree: 'id:wt-1', tabId: 'agent-session:session-1' }
|
||||
)
|
||||
expect(mocks.activateTabAndFocusPane).not.toHaveBeenCalled()
|
||||
expect(mocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
|
||||
expect(mocks.activateAndRevealWorkspace.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mocks.activateTab.mock.invocationCallOrder[0]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('opens a native destination through the rendered board and dialog action', () => {
|
||||
mocks.tabs = [nativeTab('codex')]
|
||||
const card = {
|
||||
...args,
|
||||
leafId: null,
|
||||
paneKey: 'agent-tab:leaf',
|
||||
ptyId: null,
|
||||
agentType: 'codex' as const,
|
||||
bucket: 'working' as const,
|
||||
dotState: 'working' as const,
|
||||
task: 'Open this conversation',
|
||||
repoName: 'Repo',
|
||||
worktreeName: 'Workspace',
|
||||
startedAt: 0,
|
||||
finishedAt: null,
|
||||
stateChangedAt: 0,
|
||||
unseen: false
|
||||
}
|
||||
render(
|
||||
createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
createElement(AgentKanbanBoard, {
|
||||
snapshot: { generatedAt: 0, cards: [card] },
|
||||
onAckAgent: () => {},
|
||||
onRevealAgent: revealDashboardAgent
|
||||
})
|
||||
)
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: /Open this conversation/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open worktree' }))
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('agent-session', 'wt-1')
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith(args.tabId, { worktreeId: 'wt-1' })
|
||||
expect(mocks.requestNativeChatComposerFocus).toHaveBeenCalledWith(args.tabId)
|
||||
expect(mocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not fall back to a terminal when the current native owner has no runtime', () => {
|
||||
mocks.tabs = [{ ...nativeTab('codex'), executionHostId: 'ssh:host-C' }]
|
||||
expect(revealDashboardAgent(args)).toBe(false)
|
||||
expect(mocks.activateAndRevealWorkspace).not.toHaveBeenCalled()
|
||||
expect(mocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the terminal route when a native card was closed before its click', () => {
|
||||
mocks.tabs = [nativeTab('codex')]
|
||||
mocks.tabs = []
|
||||
expect(revealDashboardAgent(args)).toBe(true)
|
||||
|
||||
expect(mocks.activateTerminalTabOnOwner).toHaveBeenCalledWith('wt-1', args.tabId, args.leafId)
|
||||
expect(mocks.activateTabAndFocusPane).toHaveBeenCalledWith(args.tabId, args.leafId, {
|
||||
flashFocusedPane: true
|
||||
})
|
||||
// Why: the owner call captures the visible tab, so it must follow the local activation.
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
expect(mocks.activateTabAndFocusPane.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mocks.activateTerminalTabOnOwner.mock.invocationCallOrder[0]
|
||||
)
|
||||
})
|
||||
|
||||
it('touches no tab when the workspace cannot be revealed', () => {
|
||||
it.each([false, true])('touches no tab when workspace reveal fails (native=%s)', (native) => {
|
||||
mocks.tabs = native ? [nativeTab('codex')] : []
|
||||
mocks.activateAndRevealWorkspace.mockReturnValue(false)
|
||||
|
||||
expect(revealDashboardAgent(args)).toBe(false)
|
||||
|
||||
expect(mocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
|
||||
expect(mocks.activateTabAndFocusPane).not.toHaveBeenCalled()
|
||||
expect(mocks.activateTab).not.toHaveBeenCalled()
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
import { revealTerminalPane } from '@/lib/reveal-terminal-pane'
|
||||
import { activateAndRevealWorkspace } from '@/lib/worktree-activation'
|
||||
import { activateStructuredAgentSessionTab } from '@/lib/agent-session/structured-agent-session-tab-activation'
|
||||
import {
|
||||
structuredAgentSessionOwnerForTab,
|
||||
structuredAgentSessionTargetForTab
|
||||
} from '@/runtime/structured-agent-session-owner'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { DashboardRevealAgentArgs } from '../../../../shared/dashboard-snapshot'
|
||||
|
||||
export function revealDashboardAgent(args: DashboardRevealAgentArgs): boolean {
|
||||
const state = useAppStore.getState()
|
||||
const tab = state.unifiedTabsByWorktree[args.worktreeId]?.find(
|
||||
(candidate) =>
|
||||
candidate.id === args.tabId &&
|
||||
candidate.worktreeId === args.worktreeId &&
|
||||
candidate.contentType === 'agent-session'
|
||||
)
|
||||
if (tab) {
|
||||
const owner = structuredAgentSessionOwnerForTab(state, tab)
|
||||
const target = structuredAgentSessionTargetForTab(state, tab)
|
||||
if (!target) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
activateAndRevealWorkspace(
|
||||
args.worktreeId,
|
||||
owner ? { executionHostId: owner } : undefined
|
||||
) === false
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return activateStructuredAgentSessionTab({ worktreeId: args.worktreeId, tabId: tab.id, target })
|
||||
}
|
||||
return revealTerminalPane({
|
||||
worktreeId: args.worktreeId,
|
||||
tabId: args.tabId,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import type * as ActivateTabAndFocusPaneModule from '@/lib/activate-tab-and-focus-pane'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
|
||||
const LEAF_A = '11111111-1111-4111-8111-111111111111'
|
||||
@@ -51,8 +53,9 @@ function mockAgent({
|
||||
let mockAgents: DashboardAgentRowData[] = []
|
||||
let mockAgentActivityDisplayMode: 'compact' | 'full' | undefined
|
||||
let mockTabsByWorktree: Record<string, { id: string }[]> = {}
|
||||
let mockTerminalLayoutsByTabId: Record<string, unknown> = {}
|
||||
let mockPtyIdsByTabId: Record<string, string[]> = {}
|
||||
let mockUnifiedTabs: Tab[] = []
|
||||
let mockLayouts: Record<string, TerminalLayoutSnapshot> = {}
|
||||
let mockPtyIds: Record<string, string[]> = {}
|
||||
let mockStructuredTabIds = new Set<string>()
|
||||
let mockAgentStatusByPaneKey: Record<string, { worktreeId?: string }> = {}
|
||||
let mockActiveTabId: string | null = null
|
||||
@@ -85,8 +88,9 @@ function buildMockStoreState(): Record<string, unknown> {
|
||||
setActiveTab: mockSetActiveTab,
|
||||
setActiveTabType: mockSetActiveTabType,
|
||||
tabsByWorktree: mockTabsByWorktree,
|
||||
terminalLayoutsByTabId: mockTerminalLayoutsByTabId,
|
||||
ptyIdsByTabId: mockPtyIdsByTabId,
|
||||
unifiedTabsByWorktree: { 'wt-1': mockUnifiedTabs },
|
||||
terminalLayoutsByTabId: mockLayouts,
|
||||
ptyIdsByTabId: mockPtyIds,
|
||||
runtimePaneTitlesByTabId: {},
|
||||
sendPromptToSidebarAgentTarget: vi.fn(),
|
||||
settings: {
|
||||
@@ -98,6 +102,7 @@ function buildMockStoreState(): Record<string, unknown> {
|
||||
|
||||
const activationMocks = vi.hoisted(() => ({
|
||||
activateAndRevealWorktree: vi.fn(),
|
||||
activateAndRevealWorkspace: vi.fn(),
|
||||
activateTabAndFocusPane: vi.fn()
|
||||
}))
|
||||
|
||||
@@ -123,7 +128,8 @@ vi.mock('@/store', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorktree: activationMocks.activateAndRevealWorktree
|
||||
activateAndRevealWorktree: activationMocks.activateAndRevealWorktree,
|
||||
activateAndRevealWorkspace: activationMocks.activateAndRevealWorkspace
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/activate-tab-and-focus-pane', () => ({
|
||||
@@ -173,10 +179,12 @@ describe('WorktreeCardAgents activation', () => {
|
||||
activationMocks.activateAndRevealWorktree.mockImplementation(() => undefined)
|
||||
activationMocks.activateTabAndFocusPane.mockImplementation(() => undefined)
|
||||
mockAgents = []
|
||||
mockUnifiedTabs = []
|
||||
mockLayouts = {}
|
||||
mockPtyIds = {}
|
||||
activationMocks.activateAndRevealWorkspace.mockReturnValue(true)
|
||||
mockAgentActivityDisplayMode = undefined
|
||||
mockTabsByWorktree = {}
|
||||
mockTerminalLayoutsByTabId = {}
|
||||
mockPtyIdsByTabId = {}
|
||||
mockStructuredTabIds = new Set()
|
||||
mockAgentStatusByPaneKey = {}
|
||||
mockActiveTabId = null
|
||||
@@ -187,54 +195,179 @@ describe('WorktreeCardAgents activation', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['opens a retained row whose exact pane still has a live PTY', ['pty-a'], true],
|
||||
['keeps a retained row inert when its pane PTY is no longer live', [], false]
|
||||
])('%s (#20803)', async (_name, livePtyIds, shouldActivate) => {
|
||||
mockAgentActivityDisplayMode = 'full'
|
||||
const tabId = 'retained-tab'
|
||||
const paneKey = makePaneKey(tabId, LEAF_A)
|
||||
async function clickRetained(mode: 'compact' | 'full', workspaceId = 'wt-1'): Promise<void> {
|
||||
mockAgentActivityDisplayMode = mode
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
if (mode === 'full') {
|
||||
renderToStaticMarkup(<WorktreeCardAgents worktreeId={workspaceId} />)
|
||||
capturedRowActivations[0].onActivate('retained-tab', makePaneKey('retained-tab', LEAF_A))
|
||||
return
|
||||
}
|
||||
const host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
const root = createRoot(host)
|
||||
try {
|
||||
await act(async () =>
|
||||
root.render(
|
||||
<TooltipProvider>
|
||||
<WorktreeCardAgents worktreeId={workspaceId} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
)
|
||||
const row = host.querySelector('.compact-agent-row')
|
||||
expect(row).toBeInstanceOf(HTMLElement)
|
||||
await act(async () => {
|
||||
row?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
} finally {
|
||||
act(() => root.unmount())
|
||||
host.remove()
|
||||
}
|
||||
}
|
||||
|
||||
function retainedRow(): void {
|
||||
mockAgents = [
|
||||
{
|
||||
...mockAgent({ paneKey, tabId, agentType: 'codex', prompt: 'Idle', worktreeId: 'wt-1' }),
|
||||
...mockAgent({
|
||||
paneKey: makePaneKey('retained-tab', LEAF_A),
|
||||
tabId: 'retained-tab',
|
||||
agentType: 'codex',
|
||||
prompt: 'Retained',
|
||||
worktreeId: 'wt-1'
|
||||
}),
|
||||
rowSource: 'retained',
|
||||
state: 'done'
|
||||
}
|
||||
]
|
||||
mockTabsByWorktree = { 'wt-1': [{ id: tabId }] }
|
||||
mockTerminalLayoutsByTabId = {
|
||||
[tabId]: {
|
||||
mockTabsByWorktree = { 'wt-1': [{ id: 'retained-tab' }] }
|
||||
mockLayouts = {
|
||||
'retained-tab': {
|
||||
root: { type: 'leaf', leafId: LEAF_A },
|
||||
activeLeafId: LEAF_A,
|
||||
expandedLeafId: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.each([
|
||||
['compact', false],
|
||||
['compact', true],
|
||||
['full', false],
|
||||
['full', true]
|
||||
] as const)(
|
||||
'opens a retained remote pane without status in %s mode (split=%s)',
|
||||
async (mode, split) => {
|
||||
retainedRow()
|
||||
mockPtyIds = { 'retained-tab': ['remote:host-B@@pty-1'] }
|
||||
if (split) {
|
||||
mockLayouts['retained-tab'].root = {
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
first: { type: 'leaf', leafId: LEAF_A },
|
||||
second: { type: 'leaf', leafId: LEAF_B }
|
||||
}
|
||||
mockLayouts['retained-tab'].activeLeafId = LEAF_B
|
||||
mockLayouts['retained-tab'].ptyIdsByLeafId = {
|
||||
[LEAF_A]: 'remote:host-B@@pty-1',
|
||||
[LEAF_B]: 'remote:host-B@@pty-2'
|
||||
}
|
||||
mockPtyIds['retained-tab'].push('remote:host-B@@pty-2')
|
||||
}
|
||||
await clickRetained(mode)
|
||||
expect(activationMocks.activateAndRevealWorkspace).toHaveBeenCalledWith('wt-1', {
|
||||
executionHostId: 'runtime:host-B'
|
||||
})
|
||||
expect(activationMocks.activateAndRevealWorktree).not.toHaveBeenCalled()
|
||||
expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith(
|
||||
'retained-tab',
|
||||
LEAF_A,
|
||||
expect.objectContaining({ ackPaneKeyOnSuccess: makePaneKey('retained-tab', LEAF_A) })
|
||||
)
|
||||
expect(ownerActivationMocks.activateTerminalTabOnOwner).toHaveBeenCalledWith(
|
||||
'wt-1',
|
||||
'retained-tab',
|
||||
LEAF_A
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
'missing',
|
||||
'sleeping',
|
||||
'stale',
|
||||
'other-split',
|
||||
'foreign-owner',
|
||||
'reveal-failed'
|
||||
] as const)('keeps a retained %s destination inert', async (shape) => {
|
||||
retainedRow()
|
||||
mockPtyIds = { 'retained-tab': ['live-pty'] }
|
||||
if (shape === 'missing') {
|
||||
mockTabsByWorktree = {}
|
||||
}
|
||||
if (shape === 'sleeping') {
|
||||
mockPtyIds = {}
|
||||
mockLayouts['retained-tab'].ptyIdsByLeafId = { [LEAF_A]: 'persisted-pty' }
|
||||
}
|
||||
if (shape === 'stale') {
|
||||
mockLayouts['retained-tab'].ptyIdsByLeafId = { [LEAF_A]: 'exited-pty' }
|
||||
}
|
||||
if (shape === 'other-split') {
|
||||
mockLayouts['retained-tab'] = {
|
||||
root: {
|
||||
type: 'split',
|
||||
direction: 'vertical',
|
||||
direction: 'horizontal',
|
||||
first: { type: 'leaf', leafId: LEAF_A },
|
||||
second: { type: 'leaf', leafId: LEAF_B }
|
||||
},
|
||||
activeLeafId: LEAF_B,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_A]: 'pty-a', [LEAF_B]: 'pty-b' }
|
||||
ptyIdsByLeafId: { [LEAF_A]: 'exited-pty', [LEAF_B]: 'live-pty' }
|
||||
}
|
||||
}
|
||||
mockPtyIdsByTabId = { [tabId]: [...livePtyIds, 'pty-b'] }
|
||||
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
|
||||
|
||||
renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
|
||||
capturedRowActivations[0].onActivate(tabId, paneKey)
|
||||
|
||||
if (shouldActivate) {
|
||||
expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1')
|
||||
expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith(
|
||||
tabId,
|
||||
LEAF_A,
|
||||
expect.anything()
|
||||
)
|
||||
} else {
|
||||
expect(activationMocks.activateAndRevealWorktree).not.toHaveBeenCalled()
|
||||
expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled()
|
||||
if (shape === 'foreign-owner') {
|
||||
mockPtyIds = { 'retained-tab': ['remote:unscoped-handle'] }
|
||||
}
|
||||
if (shape === 'reveal-failed') {
|
||||
activationMocks.activateAndRevealWorkspace.mockReturnValue(false)
|
||||
}
|
||||
await clickRetained('full')
|
||||
if (shape !== 'reveal-failed') {
|
||||
expect(activationMocks.activateAndRevealWorkspace).not.toHaveBeenCalled()
|
||||
}
|
||||
expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled()
|
||||
expect(ownerActivationMocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
|
||||
expect(staleAgentRowMocks.dismissStaleAgentRowByKey).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens a retained native tab without a PTY using its explicit owner', async () => {
|
||||
retainedRow()
|
||||
mockTabsByWorktree = {}
|
||||
mockUnifiedTabs = [
|
||||
{
|
||||
id: 'retained-tab',
|
||||
entityId: 'session-1',
|
||||
contentType: 'agent-session',
|
||||
worktreeId: 'wt-1',
|
||||
executionHostId: 'runtime:host-B',
|
||||
groupId: 'g1',
|
||||
label: 'Codex',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
]
|
||||
mockStructuredTabIds.add('retained-tab')
|
||||
await clickRetained('full')
|
||||
expect(structuredActivationMocks.activateStructuredAgentSessionTab).toHaveBeenCalledWith({
|
||||
worktreeId: 'wt-1',
|
||||
tabId: 'retained-tab',
|
||||
target: { kind: 'environment', environmentId: 'host-B' }
|
||||
})
|
||||
expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled()
|
||||
expect(ownerActivationMocks.activateTerminalTabOnOwner).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('activates a projected structured session row through the unified tab path', async () => {
|
||||
mockAgentActivityDisplayMode = 'full'
|
||||
const tabId = 'structured-tab'
|
||||
|
||||
@@ -28,8 +28,8 @@ import { revealElementInScrollContainer } from './worktree-sidebar-reveal'
|
||||
import { useWorktreeAgentExpansionState } from './worktree-card-agents-expansion-state'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { activateStructuredAgentSessionTab } from '@/lib/agent-session/structured-agent-session-tab-activation'
|
||||
import { activateRetainedAgentRow } from './activate-retained-agent-row'
|
||||
import { selectAgentRowsUnread } from './worktree-card-agent-ack-inputs'
|
||||
import { stablePaneHasLivePty } from '@/lib/sleeping-agent-pane-ownership'
|
||||
|
||||
export const SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT =
|
||||
'orca-suppress-worktree-list-scroll-adjustment'
|
||||
@@ -187,24 +187,8 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
|
||||
[worktreeId]
|
||||
)
|
||||
const handleActivateRetainedAgent = useCallback(
|
||||
(tabId: string, paneKey: string) => {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
const state = useAppStore.getState()
|
||||
// Retained rows are completion evidence; only open a still-live pane so a sleeping target pane is never resumed.
|
||||
if (
|
||||
parsed?.tabId === tabId &&
|
||||
state.tabsByWorktree[worktreeId]?.some((tab) => tab.id === tabId) &&
|
||||
stablePaneHasLivePty(
|
||||
tabId,
|
||||
parsed.leafId,
|
||||
state.ptyIdsByTabId,
|
||||
state.terminalLayoutsByTabId[tabId]
|
||||
)
|
||||
) {
|
||||
handleActivateAgentTab(tabId, paneKey)
|
||||
}
|
||||
},
|
||||
[handleActivateAgentTab, worktreeId]
|
||||
(tabId: string, paneKey: string) => activateRetainedAgentRow(worktreeId, tabId, paneKey),
|
||||
[worktreeId]
|
||||
)
|
||||
|
||||
const now = useNow(30_000)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import { activateAndRevealWorkspace } from '@/lib/worktree-activation'
|
||||
import { activateTabAndFocusPane } from '@/lib/activate-tab-and-focus-pane'
|
||||
import { activateTerminalTabOnOwner } from '@/lib/terminal-tab-owner-activation'
|
||||
import { activateStructuredAgentSessionTab } from '@/lib/agent-session/structured-agent-session-tab-activation'
|
||||
import {
|
||||
structuredAgentSessionOwnerForTab,
|
||||
structuredAgentSessionTargetForTab
|
||||
} from '@/runtime/structured-agent-session-owner'
|
||||
import { stablePaneHasLivePty } from '@/lib/sleeping-agent-pane-ownership'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { getPtyExecutionHost } from '../../../../shared/terminal-execution-host'
|
||||
|
||||
export function activateRetainedAgentRow(worktreeId: string, tabId: string, paneKey: string): void {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
if (!parsed || parsed.tabId !== tabId) {
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const nativeTab = state.unifiedTabsByWorktree[worktreeId]?.find(
|
||||
(tab) =>
|
||||
tab.id === tabId && tab.worktreeId === worktreeId && tab.contentType === 'agent-session'
|
||||
)
|
||||
const target = nativeTab ? structuredAgentSessionTargetForTab(state, nativeTab) : null
|
||||
const layout = state.terminalLayoutsByTabId[tabId]
|
||||
const hasLiveTerminal =
|
||||
state.tabsByWorktree[worktreeId]?.some((tab) => tab.id === tabId) &&
|
||||
stablePaneHasLivePty(tabId, parsed.leafId, state.ptyIdsByTabId, layout)
|
||||
const owner = nativeTab
|
||||
? structuredAgentSessionOwnerForTab(state, nativeTab)
|
||||
: getPtyExecutionHost(
|
||||
layout?.ptyIdsByLeafId?.[parsed.leafId] ?? state.ptyIdsByTabId[tabId]?.[0]
|
||||
)
|
||||
if ((nativeTab ? !target : !hasLiveTerminal) || owner === 'foreign') {
|
||||
return
|
||||
}
|
||||
// Verify an existing destination before workspace activation can resume sleeping panes.
|
||||
if (
|
||||
activateAndRevealWorkspace(worktreeId, owner ? { executionHostId: owner } : undefined) === false
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (nativeTab && target) {
|
||||
activateStructuredAgentSessionTab({ worktreeId, tabId, target })
|
||||
} else {
|
||||
activateTabAndFocusPane(tabId, parsed.leafId, {
|
||||
ackPaneKeyOnSuccess: paneKey,
|
||||
flashFocusedPane: true,
|
||||
scrollToBottomIfOutputSinceLastView: true
|
||||
})
|
||||
activateTerminalTabOnOwner(worktreeId, tabId, parsed.leafId)
|
||||
}
|
||||
}
|
||||
@@ -19128,7 +19128,7 @@
|
||||
"subagents_other": "{{count}} subagents"
|
||||
},
|
||||
"terminal": {
|
||||
"closed": "No live terminal — this agent's pane has closed.",
|
||||
"previewUnavailable": "No preview available — open the workspace to view this agent.",
|
||||
"remotePreviewUnavailable": "No preview for this remote session — open the workspace to view the terminal.",
|
||||
"focusWorktree": "Open worktree",
|
||||
"close": "Close"
|
||||
|
||||
@@ -18832,7 +18832,6 @@
|
||||
"subagents_other": "{{count}} subagentes"
|
||||
},
|
||||
"terminal": {
|
||||
"closed": "No hay terminal activa: el panel de este agente se ha cerrado.",
|
||||
"focusWorktree": "Open worktree",
|
||||
"close": "Close",
|
||||
"remotePreviewUnavailable": "No hay vista previa para esta sesión remota: abre el espacio de trabajo para ver la terminal."
|
||||
|
||||
@@ -19011,7 +19011,6 @@
|
||||
"subagents_other": "{{count}} sous-agents"
|
||||
},
|
||||
"terminal": {
|
||||
"closed": "Pas de terminal en direct — le volet de cet agent est fermé.",
|
||||
"focusWorktree": "Ouvrir le worktree",
|
||||
"close": "Fermer",
|
||||
"remotePreviewUnavailable": "Aucun aperçu pour cette session à distance — ouvrez l'espace de travail pour afficher le terminal."
|
||||
|
||||
@@ -18831,7 +18831,6 @@
|
||||
"subagents_other": "{{count}} 個のサブ Agent"
|
||||
},
|
||||
"terminal": {
|
||||
"closed": "実行中のターミナルがありません。この Agent のペインは閉じられています。",
|
||||
"focusWorktree": "ワークツリーを開く",
|
||||
"close": "閉じる",
|
||||
"remotePreviewUnavailable": "このリモートセッションにはプレビューがありません。ワークスペースを開いてターミナルを表示してください。"
|
||||
|
||||
@@ -18831,7 +18831,6 @@
|
||||
"subagents_other": "서브에이전트 {{count}}개"
|
||||
},
|
||||
"terminal": {
|
||||
"closed": "실시간 터미널이 없습니다 — 이 에이전트의 창이 닫혔습니다.",
|
||||
"focusWorktree": "워크트리 열기",
|
||||
"close": "닫기",
|
||||
"remotePreviewUnavailable": "이 원격 세션에 대한 미리 보기가 없습니다. 터미널을 보려면 워크스페이스를 엽니다."
|
||||
|
||||
@@ -509,7 +509,6 @@
|
||||
"subagents_other": "{{count}} subagentes"
|
||||
},
|
||||
"terminal": {
|
||||
"closed": "Nenhum terminal ativo — o painel deste agente foi encerrado.",
|
||||
"remotePreviewUnavailable": "Nenhuma pré-visualização para esta sessão remota — abra o espaço de trabalho para ver o terminal.",
|
||||
"focusWorktree": "Abrir worktree",
|
||||
"close": "Fechar"
|
||||
|
||||
@@ -18832,7 +18832,6 @@
|
||||
"subagents_other": "{{count}} 个子智能体"
|
||||
},
|
||||
"terminal": {
|
||||
"closed": "没有活动的终端 — 该智能体的窗格已关闭。",
|
||||
"focusWorktree": "聚焦工作树",
|
||||
"close": "关闭",
|
||||
"remotePreviewUnavailable": "此远程会话没有预览 - 打开工作区以查看终端。"
|
||||
|
||||
@@ -74,7 +74,7 @@ export type DashboardWorkspace = {
|
||||
export type DashboardCard = {
|
||||
/** Stable identity for React keys. */
|
||||
paneKey: string
|
||||
/** Null when retained/done cards have no live pane; otherwise resolve the live preview PTY. */
|
||||
/** Live preview PTY; null does not imply that the agent's destination is closed. */
|
||||
ptyId: string | null
|
||||
agentType: AgentType
|
||||
bucket: DashboardBucket
|
||||
|
||||
Reference in New Issue
Block a user