mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(sidebar): transfer launch provenance on pane detach
This commit is contained in:
@@ -323,6 +323,26 @@ describe('buildTitleDerivedAgentRows', () => {
|
||||
expect(rows[0]?.entry.terminalTitle).toBe('Rename the auth helper')
|
||||
})
|
||||
|
||||
it('keeps a live mirrored Cursor title that was also stamped as the tab default', () => {
|
||||
const rows = buildWorktreeAgentRows({
|
||||
tabs: [
|
||||
makeTab('tab-1', {
|
||||
launchAgent: 'cursor',
|
||||
launchAgentLeafId: LEAF_ID_1,
|
||||
defaultTitle: 'Cursor Agent'
|
||||
})
|
||||
],
|
||||
entries: [],
|
||||
retained: [],
|
||||
runtimePaneTitlesByTabId: { 'tab-1': { 1: 'Cursor Agent' } },
|
||||
ptyIdsByTabId: { 'tab-1': ['pty-cursor'] },
|
||||
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
|
||||
now: 2000
|
||||
})
|
||||
|
||||
expect(rows.map((row) => [row.agentType, row.state])).toEqual([['cursor', 'idle']])
|
||||
})
|
||||
|
||||
it('keeps a launched Codex pane visible under an arbitrary session title', () => {
|
||||
const rows = buildWorktreeAgentRows({
|
||||
tabs: [makeTab('tab-1', { launchAgent: 'codex', launchAgentLeafId: LEAF_ID_1 })],
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/ter
|
||||
import { emptyLayoutSnapshot, singlePaneLayoutSnapshot } from './terminal-helpers'
|
||||
import {
|
||||
resolveLaunchAgentLeafId,
|
||||
stampLaunchAgentLeafIdOnFirstLayout
|
||||
stampLaunchAgentLeafIdOnFirstLayout,
|
||||
transferLaunchAgentLeafStampOnDetach
|
||||
} from './launch-agent-leaf-stamp'
|
||||
|
||||
const LEAF_A = '11111111-1111-4111-8111-111111111111'
|
||||
@@ -116,3 +117,49 @@ describe('resolveLaunchAgentLeafId', () => {
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('transferLaunchAgentLeafStampOnDetach', () => {
|
||||
it('moves launch provenance with the detached leaf', () => {
|
||||
const tabs = transferLaunchAgentLeafStampOnDetach({
|
||||
tabs: [
|
||||
makeTab({ id: 'source', launchAgent: 'cursor', launchAgentLeafId: LEAF_A }),
|
||||
makeTab({ id: 'target' })
|
||||
],
|
||||
sourceTabId: 'source',
|
||||
targetTabId: 'target',
|
||||
detachedLeafId: LEAF_A
|
||||
})
|
||||
|
||||
expect(tabs?.find((tab) => tab.id === 'source')).not.toHaveProperty('launchAgent')
|
||||
expect(tabs?.find((tab) => tab.id === 'source')).not.toHaveProperty('launchAgentLeafId')
|
||||
expect(tabs?.find((tab) => tab.id === 'target')).toMatchObject({
|
||||
launchAgent: 'cursor',
|
||||
launchAgentLeafId: LEAF_A
|
||||
})
|
||||
})
|
||||
|
||||
it('does not overwrite existing target launch provenance', () => {
|
||||
expect(
|
||||
transferLaunchAgentLeafStampOnDetach({
|
||||
tabs: [
|
||||
makeTab({ id: 'source', launchAgent: 'cursor', launchAgentLeafId: LEAF_A }),
|
||||
makeTab({ id: 'target', launchAgent: 'codex', launchAgentLeafId: LEAF_B })
|
||||
],
|
||||
sourceTabId: 'source',
|
||||
targetTabId: 'target',
|
||||
detachedLeafId: LEAF_A
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('does not guess ownership for an unpinned launch', () => {
|
||||
expect(
|
||||
transferLaunchAgentLeafStampOnDetach({
|
||||
tabs: [makeTab({ id: 'source', launchAgent: 'cursor' }), makeTab({ id: 'target' })],
|
||||
sourceTabId: 'source',
|
||||
targetTabId: 'target',
|
||||
detachedLeafId: LEAF_A
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,6 +48,40 @@ export function stampLaunchAgentLeafIdOnFirstLayout(args: {
|
||||
return nextTabs
|
||||
}
|
||||
|
||||
/** Moves launch provenance with a detached leaf without overwriting existing target ownership. */
|
||||
export function transferLaunchAgentLeafStampOnDetach(args: {
|
||||
tabs: readonly TerminalTab[]
|
||||
sourceTabId: string
|
||||
targetTabId: string
|
||||
detachedLeafId: string
|
||||
}): TerminalTab[] | null {
|
||||
const sourceIndex = args.tabs.findIndex((tab) => tab.id === args.sourceTabId)
|
||||
const targetIndex = args.tabs.findIndex((tab) => tab.id === args.targetTabId)
|
||||
const source = args.tabs[sourceIndex]
|
||||
const target = args.tabs[targetIndex]
|
||||
if (
|
||||
!source?.launchAgent ||
|
||||
source.launchAgentLeafId !== args.detachedLeafId ||
|
||||
!target ||
|
||||
target.launchAgent ||
|
||||
target.launchAgentLeafId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sourceWithoutLaunchProvenance = { ...source }
|
||||
delete sourceWithoutLaunchProvenance.launchAgent
|
||||
delete sourceWithoutLaunchProvenance.launchAgentLeafId
|
||||
const nextTabs = [...args.tabs]
|
||||
nextTabs[sourceIndex] = sourceWithoutLaunchProvenance
|
||||
nextTabs[targetIndex] = {
|
||||
...target,
|
||||
launchAgent: source.launchAgent,
|
||||
launchAgentLeafId: args.detachedLeafId
|
||||
}
|
||||
return nextTabs
|
||||
}
|
||||
|
||||
function soleLeafIdOnFirstLayout(
|
||||
previousLayout: TerminalLayoutSnapshot | undefined,
|
||||
nextLayout: TerminalLayoutSnapshot
|
||||
|
||||
@@ -20,7 +20,13 @@ describe('syncPaneDetachPtyOwnership agent identity', () => {
|
||||
},
|
||||
tabsByWorktree: {
|
||||
[worktreeId]: [
|
||||
makeTab({ id: sourceTabId, worktreeId, ptyId: 'pty-droid' }),
|
||||
makeTab({
|
||||
id: sourceTabId,
|
||||
worktreeId,
|
||||
ptyId: 'pty-droid',
|
||||
launchAgent: 'droid',
|
||||
launchAgentLeafId: detachedLeafId
|
||||
}),
|
||||
makeTab({ id: targetTabId, worktreeId, ptyId: null })
|
||||
]
|
||||
},
|
||||
@@ -113,6 +119,13 @@ describe('syncPaneDetachPtyOwnership agent identity', () => {
|
||||
agent: 'antigravity',
|
||||
shellForeground: false
|
||||
})
|
||||
const tabs = state.tabsByWorktree[worktreeId] ?? []
|
||||
expect(tabs.find((tab) => tab.id === sourceTabId)).not.toHaveProperty('launchAgent')
|
||||
expect(tabs.find((tab) => tab.id === sourceTabId)).not.toHaveProperty('launchAgentLeafId')
|
||||
expect(tabs.find((tab) => tab.id === targetTabId)).toMatchObject({
|
||||
launchAgent: 'droid',
|
||||
launchAgentLeafId: detachedLeafId
|
||||
})
|
||||
expect(resolveWindowsShiftEnterEncodingForPane(state, targetPaneKey)).toBe('csi-u')
|
||||
expect(resolveWindowsShiftEnterEncodingForPane(state, siblingPaneKey)).toBe('alt-enter')
|
||||
})
|
||||
|
||||
@@ -51,7 +51,10 @@ import type { AiVaultSessionTitle } from '../../../../shared/ai-vault-session-ti
|
||||
import { scheduleRuntimeGraphSync } from '@/runtime/sync-runtime-graph'
|
||||
import { terminalLayoutEqual } from '@/lib/terminal-layout-equality'
|
||||
import { sweepRetiredTerminalTabState } from './retired-terminal-tab-state-sweep'
|
||||
import { stampLaunchAgentLeafIdOnFirstLayout } from './launch-agent-leaf-stamp'
|
||||
import {
|
||||
stampLaunchAgentLeafIdOnFirstLayout,
|
||||
transferLaunchAgentLeafStampOnDetach
|
||||
} from './launch-agent-leaf-stamp'
|
||||
import { clearTransientTerminalState, emptyLayoutSnapshot } from './terminal-helpers'
|
||||
import {
|
||||
collectReleasedLeafIds,
|
||||
@@ -3663,6 +3666,24 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
const nextTabsByWorktree = detachedPtyId
|
||||
? withTerminalTabPtyId(sourceTabsByWorktree, targetTabId, detachedPtyId)
|
||||
: sourceTabsByWorktree
|
||||
const sourceWorktreeId = getTerminalTabOwnerWorktreeId(nextTabsByWorktree, sourceTabId)
|
||||
const targetWorktreeId = getTerminalTabOwnerWorktreeId(nextTabsByWorktree, targetTabId)
|
||||
const transferredTabs =
|
||||
sourceWorktreeId !== null && sourceWorktreeId === targetWorktreeId
|
||||
? transferLaunchAgentLeafStampOnDetach({
|
||||
tabs: nextTabsByWorktree[sourceWorktreeId] ?? [],
|
||||
sourceTabId,
|
||||
targetTabId,
|
||||
detachedLeafId
|
||||
})
|
||||
: null
|
||||
const tabsByWorktree =
|
||||
transferredTabs && sourceWorktreeId !== null
|
||||
? { ...nextTabsByWorktree, [sourceWorktreeId]: transferredTabs }
|
||||
: nextTabsByWorktree
|
||||
if (transferredTabs) {
|
||||
scheduleRuntimeGraphSync()
|
||||
}
|
||||
const directSshLedger = transferDirectSshPaneDetachLedger(s, {
|
||||
detachedPtyId,
|
||||
sourcePtyId: sourcePrimaryPtyId,
|
||||
@@ -3675,7 +3696,7 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
ptyIdsByTabId: nextPtyIdsByTabId,
|
||||
lastKnownRelayPtyIdByTabId: nextLastKnownRelayPtyIdByTabId,
|
||||
...directSshLedger,
|
||||
...(nextTabsByWorktree !== s.tabsByWorktree ? { tabsByWorktree: nextTabsByWorktree } : {})
|
||||
...(tabsByWorktree !== s.tabsByWorktree ? { tabsByWorktree } : {})
|
||||
}
|
||||
})
|
||||
// Why: detach keeps the process and its pane key alive, so move resume/status authority to the new surface before the source closes.
|
||||
|
||||
Reference in New Issue
Block a user