Show restored banner when slept agent resumes (#5467)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
bce7b7948a
commit
ef04f10dad
|
|
@ -261,13 +261,29 @@
|
|||
align-items: center;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--orca-pane-title-fg, rgb(255 255 255 / 0.52));
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.session-restored-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 8px;
|
||||
z-index: 5;
|
||||
height: var(--orca-pane-title-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--orca-pane-title-fg);
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pane-title-text {
|
||||
align-self: stretch;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { SESSION_RESTORED_BANNER_TEXT, SessionRestoredBanner } from './SessionRestoredBanner'
|
||||
|
||||
const mountedRoots: Root[] = []
|
||||
|
||||
async function renderBanner(visible: boolean): Promise<HTMLDivElement> {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
mountedRoots.push(root)
|
||||
|
||||
await act(async () => {
|
||||
root.render(<SessionRestoredBanner visible={visible} />)
|
||||
})
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
describe('SessionRestoredBanner', () => {
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
for (const root of mountedRoots.splice(0)) {
|
||||
root.unmount()
|
||||
}
|
||||
})
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('renders the exact restored-session marker when visible', async () => {
|
||||
const container = await renderBanner(true)
|
||||
|
||||
expect(container.textContent).toBe(SESSION_RESTORED_BANNER_TEXT)
|
||||
expect(container.querySelector('.session-restored-banner')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('does not render without the startup marker', async () => {
|
||||
const container = await renderBanner(false)
|
||||
|
||||
expect(container.textContent).toBe('')
|
||||
expect(container.querySelector('.session-restored-banner')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export const SESSION_RESTORED_BANNER_TEXT = '--- session restored ---'
|
||||
|
||||
type SessionRestoredBannerProps = {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export function SessionRestoredBanner({
|
||||
visible
|
||||
}: SessionRestoredBannerProps): React.JSX.Element | null {
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className="session-restored-banner">{SESSION_RESTORED_BANNER_TEXT}</div>
|
||||
}
|
||||
|
|
@ -40,6 +40,8 @@ import { TerminalErrorToast } from './TerminalErrorToast'
|
|||
import { TerminalSessionStateSaveFailureDialog } from './TerminalSessionStateSaveFailureDialog'
|
||||
import TerminalContextMenu from './TerminalContextMenu'
|
||||
import { TerminalAgentSessionForkDialog } from './TerminalAgentSessionForkDialog'
|
||||
import { SessionRestoredBanner } from './SessionRestoredBanner'
|
||||
import { useSessionRestoredBannerDismiss } from './useSessionRestoredBannerDismiss'
|
||||
import { useSystemPrefersDark } from './use-system-prefers-dark'
|
||||
import { useTerminalPaneGlobalEffects } from './use-terminal-pane-global-effects'
|
||||
import { useTerminalPaneLifecycle } from './use-terminal-pane-lifecycle'
|
||||
|
|
@ -413,6 +415,9 @@ export default function TerminalPane({
|
|||
// xterm may not know multi-line text needs bracketed-paste protection.
|
||||
const forceBracketedMultilineTextPaste = isWindowsUserAgent()
|
||||
const [startup] = useState(() => useAppStore.getState().pendingStartupByTabId[tabId])
|
||||
const [showSessionRestoredBanner, setShowSessionRestoredBanner] = useState(
|
||||
() => startup?.showSessionRestoredBanner === true
|
||||
)
|
||||
const shouldMeasureHiddenStartup = startup !== undefined && !isVisible
|
||||
const consumeTabStartupCommand = useAppStore((store) => store.consumeTabStartupCommand)
|
||||
const [setupSplit] = useState(() => useAppStore.getState().pendingSetupSplitByTabId[tabId])
|
||||
|
|
@ -427,6 +432,15 @@ export default function TerminalPane({
|
|||
}
|
||||
}, [startup, tabId, consumeTabStartupCommand])
|
||||
|
||||
const dismissSessionRestoredBanner = useCallback((): void => {
|
||||
setShowSessionRestoredBanner(false)
|
||||
}, [])
|
||||
useSessionRestoredBannerDismiss(
|
||||
showSessionRestoredBanner,
|
||||
containerRef,
|
||||
dismissSessionRestoredBanner
|
||||
)
|
||||
|
||||
const openDiskSpaceAnalyzer = useCallback(() => {
|
||||
setSessionStateSaveFailureOpen(false)
|
||||
openSpacePage()
|
||||
|
|
@ -1467,13 +1481,14 @@ export default function TerminalPane({
|
|||
}
|
||||
let needsFit = false
|
||||
for (const pane of manager.getPanes()) {
|
||||
// Show the title bar space when the pane has a title OR is being
|
||||
// inline-edited (so the input appears even for untitled panes).
|
||||
// Show the title bar space when the pane has a title, is being
|
||||
// inline-edited, or has transient startup chrome.
|
||||
// Unread activity does NOT reserve title-bar space — the bell is
|
||||
// rendered as an absolutely-positioned overlay in the pane's top-right
|
||||
// corner so it can appear and disappear without shifting terminal
|
||||
// content, avoiding the jarring reflow on bell toggles.
|
||||
const shouldShow = !!paneTitles[pane.id] || renamingPaneId === pane.id
|
||||
const shouldShow =
|
||||
!!paneTitles[pane.id] || renamingPaneId === pane.id || showSessionRestoredBanner
|
||||
const hadTitle = pane.container.hasAttribute('data-has-title')
|
||||
if (shouldShow && !hadTitle) {
|
||||
pane.container.setAttribute('data-has-title', '')
|
||||
|
|
@ -1486,7 +1501,7 @@ export default function TerminalPane({
|
|||
if (needsFit) {
|
||||
fitPanes(manager)
|
||||
}
|
||||
}, [paneTitles, renamingPaneId])
|
||||
}, [paneCount, paneTitles, renamingPaneId, showSessionRestoredBanner])
|
||||
|
||||
// Register a capture callback for shutdown. The beforeunload handler in
|
||||
// App.tsx calls all registered callbacks to serialize terminal buffers.
|
||||
|
|
@ -1967,6 +1982,15 @@ export default function TerminalPane({
|
|||
/>,
|
||||
activePane.container
|
||||
)}
|
||||
{showSessionRestoredBanner &&
|
||||
activePane?.container &&
|
||||
createPortal(
|
||||
// Why: resumed Codex TUIs repaint xterm immediately, so the wake marker
|
||||
// must live in the pane chrome instead of the PTY byte stream.
|
||||
<SessionRestoredBanner visible />,
|
||||
activePane.container,
|
||||
'session-restored-banner'
|
||||
)}
|
||||
<TerminalContextMenu
|
||||
open={contextMenu.open}
|
||||
onOpenChange={contextMenu.setOpen}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export type PtyConnectionDeps = {
|
|||
telemetry?: EventProps<'agent_started'>
|
||||
/** Initial prompt-start status for agents that lack native prompt hooks. */
|
||||
initialAgentStatus?: { agent: TuiAgent; prompt: string }
|
||||
/** Show the restored-session banner when this startup command mounts. */
|
||||
showSessionRestoredBanner?: boolean
|
||||
} | null
|
||||
restoredLeafId?: string | null
|
||||
restoredPtyIdByLeafId?: Record<string, string>
|
||||
|
|
|
|||
|
|
@ -3092,6 +3092,10 @@ describe('connectPanePty', () => {
|
|||
await new Promise((resolve) => setTimeout(resolve, 70))
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('cold-payload', expect.any(Function))
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('--- session restored ---'),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(transport.sendInput).toHaveBeenCalledWith(
|
||||
"codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'codex-session-1'\r"
|
||||
)
|
||||
|
|
@ -3150,6 +3154,17 @@ describe('connectPanePty', () => {
|
|||
await new Promise((resolve) => setTimeout(resolve, 70))
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('cold-payload', expect.any(Function))
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('--- session restored ---'),
|
||||
expect.any(Function)
|
||||
)
|
||||
const writeCalls = pane.terminal.write.mock.calls.map(([data]) => data)
|
||||
expect(writeCalls.indexOf('cold-payload')).toBeLessThan(
|
||||
writeCalls.findIndex((data) => data.includes('--- session restored ---'))
|
||||
)
|
||||
expect(writeCalls.findIndex((data) => data.includes('--- session restored ---'))).toBeLessThan(
|
||||
writeCalls.indexOf(POST_REPLAY_MODE_RESET)
|
||||
)
|
||||
expect(transport.sendInput).toHaveBeenCalledWith(
|
||||
"codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'codex-session-1'\r"
|
||||
)
|
||||
|
|
@ -3158,6 +3173,114 @@ describe('connectPanePty', () => {
|
|||
expect(mockStoreState.clearSleepingAgentSession).toHaveBeenCalledWith(paneKey)
|
||||
})
|
||||
|
||||
it('shows the restored banner when a sleeping resume falls back to a fresh shell', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const staleSessionId = 'wt-1@@stale-session'
|
||||
const transport = createMockTransport()
|
||||
transport.connect.mockImplementation(async (opts: { sessionId?: string }) => {
|
||||
if (opts.sessionId) {
|
||||
return undefined
|
||||
}
|
||||
const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as
|
||||
| ((ptyId: string) => void)
|
||||
| undefined
|
||||
onPtySpawn?.('fresh-pty')
|
||||
return 'fresh-pty'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
const paneKey = makePaneKey('tab-1', LEAF_2)
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: staleSessionId }]
|
||||
},
|
||||
ptyIdsByTabId: {
|
||||
'tab-1': [staleSessionId]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf', leafId: LEAF_2 },
|
||||
activeLeafId: LEAF_2,
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [LEAF_2]: staleSessionId }
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings,
|
||||
agentCmdOverrides: {}
|
||||
},
|
||||
agentStatusByPaneKey: {},
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
[paneKey]: {
|
||||
paneKey,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: 'codex-session-1' },
|
||||
prompt: 'finish the task',
|
||||
state: 'working',
|
||||
capturedAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
}
|
||||
} as StoreState
|
||||
|
||||
const pane = createPane(2)
|
||||
const manager = createManager(2)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_2,
|
||||
restoredPtyIdByLeafId: { [LEAF_2]: staleSessionId }
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(10)
|
||||
|
||||
expect(transport.connect).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ sessionId: staleSessionId })
|
||||
)
|
||||
expect(transport.connect).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.not.objectContaining({ sessionId: expect.any(String) })
|
||||
)
|
||||
expect(deps.clearTabPtyId).toHaveBeenCalledWith('tab-1', staleSessionId)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('--- session restored ---'),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(mockStoreState.clearSleepingAgentSession).toHaveBeenCalledWith(paneKey)
|
||||
})
|
||||
|
||||
it('does not write the restored banner through xterm bytes for sidebar-resumed startup commands', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-1')
|
||||
transportFactoryQueue.push(transport)
|
||||
const pane = createPane(1)
|
||||
|
||||
connectPanePty(
|
||||
pane as never,
|
||||
createManager(1) as never,
|
||||
createDeps({
|
||||
startup: {
|
||||
command: "codex 'resume' 'codex-session-1'",
|
||||
showSessionRestoredBanner: true
|
||||
}
|
||||
}) as never
|
||||
)
|
||||
await flushAsyncTicks(10)
|
||||
const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as
|
||||
| ((ptyId: string) => void)
|
||||
| undefined
|
||||
onPtySpawn?.('pty-1')
|
||||
await new Promise((resolve) => setTimeout(resolve, 70))
|
||||
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('--- session restored ---'),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(createdTransportOptions[0]?.command).toBe("codex 'resume' 'codex-session-1'")
|
||||
})
|
||||
|
||||
it('does not consume the sleeping record when daemon reattach returns a live snapshot', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('tab-pty')
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ const INACTIVE_FOREGROUND_IMMEDIATE_BUDGET_CHARS = 32 * 1024
|
|||
// terminal state is unavailable, so the user has an explicit loss signal.
|
||||
const HIDDEN_OUTPUT_RESTORE_UNAVAILABLE_WARNING =
|
||||
'\x18\x1b[0m\r\n[Orca skipped hidden terminal output because main recovery was unavailable.]\r\n'
|
||||
const SESSION_RESTORED_BANNER = '\r\n\x1b[2m--- session restored ---\x1b[0m\r\n\r\n'
|
||||
|
||||
type E2eTerminalPtyDataInjectionApi = {
|
||||
inject: (paneKey: string, data: string, meta?: PtyDataMeta) => boolean
|
||||
|
|
@ -1960,13 +1961,30 @@ export function connectPanePty(
|
|||
// stay renderer-delivered so xterm can apply bracketed-paste semantics.
|
||||
let pendingStartupCommand =
|
||||
shouldDeliverStartupViaTerminalPaste || connectionId ? (paneStartup?.command ?? null) : null
|
||||
let sessionRestoredBannerWritten = false
|
||||
const writeSessionRestoredBanner = (writeBanner?: (data: string) => void): void => {
|
||||
if (sessionRestoredBannerWritten) {
|
||||
return
|
||||
}
|
||||
sessionRestoredBannerWritten = true
|
||||
if (writeBanner) {
|
||||
writeBanner(SESSION_RESTORED_BANNER)
|
||||
return
|
||||
}
|
||||
writeTerminalOutput(pane.terminal, SESSION_RESTORED_BANNER, {
|
||||
foreground: true,
|
||||
beforeWrite: beforeTerminalOutputWrite
|
||||
})
|
||||
}
|
||||
const getColdRestoreAgentResumePlatform = (): NodeJS.Platform => {
|
||||
if (connectionId || (worktree?.path && isWslUncPath(worktree.path))) {
|
||||
return 'linux'
|
||||
}
|
||||
return CLIENT_PLATFORM
|
||||
}
|
||||
const prepareColdRestoreAgentResumeCommand = (): boolean => {
|
||||
const prepareColdRestoreAgentResumeCommand = (
|
||||
writeBanner?: (data: string) => void
|
||||
): boolean => {
|
||||
if (pendingStartupCommand) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1998,6 +2016,9 @@ export function connectPanePty(
|
|||
// Why: cold restore means the PTY process is gone but the agent provider
|
||||
// session is still resumable, so the replacement shell must launch it.
|
||||
pendingStartupCommand = startupPlan.launchCommand
|
||||
if (sleepingRecord) {
|
||||
writeSessionRestoredBanner(writeBanner)
|
||||
}
|
||||
if (!useLiveEntry && sleepingRecord) {
|
||||
state.clearSleepingAgentSession(cacheKey)
|
||||
}
|
||||
|
|
@ -3097,7 +3118,7 @@ export function connectPanePty(
|
|||
// land in the new shell's stdin. See replay-guard.ts.
|
||||
writeReplayData('\x1b[2J\x1b[3J\x1b[H')
|
||||
writeReplayData(connectResult.coldRestore.scrollback)
|
||||
writeReplayData('\r\n\x1b[2m--- session restored ---\x1b[0m\r\n\r\n')
|
||||
const didPrepareResume = prepareColdRestoreAgentResumeCommand(writeReplayData)
|
||||
// Cold-restore means the daemon lost the session and spawned a
|
||||
// fresh shell — no TUI is consuming the mode-setting bytes that a
|
||||
// crashed TUI (e.g. Claude's \e[?1004h) left in the scrollback, so
|
||||
|
|
@ -3106,7 +3127,7 @@ export function connectPanePty(
|
|||
if (!isRemoteRuntimePtyId(ptyId)) {
|
||||
window.api.pty.ackColdRestore(ptyId)
|
||||
}
|
||||
if (prepareColdRestoreAgentResumeCommand()) {
|
||||
if (didPrepareResume) {
|
||||
schedulePendingStartupCommandDelivery()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { useRef } from 'react'
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useSessionRestoredBannerDismiss } from './useSessionRestoredBannerDismiss'
|
||||
|
||||
const mountedRoots: Root[] = []
|
||||
|
||||
function Probe({ visible, dismiss }: { visible: boolean; dismiss: () => void }): React.JSX.Element {
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
useSessionRestoredBannerDismiss(visible, ref, dismiss)
|
||||
return <div ref={ref} data-testid="pane" />
|
||||
}
|
||||
|
||||
async function renderProbe(visible: boolean, dismiss = vi.fn()): Promise<HTMLDivElement> {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
mountedRoots.push(root)
|
||||
|
||||
await act(async () => {
|
||||
root.render(<Probe visible={visible} dismiss={dismiss} />)
|
||||
})
|
||||
|
||||
return container.querySelector('[data-testid="pane"]')!
|
||||
}
|
||||
|
||||
describe('useSessionRestoredBannerDismiss', () => {
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
for (const root of mountedRoots.splice(0)) {
|
||||
root.unmount()
|
||||
}
|
||||
})
|
||||
document.body.innerHTML = ''
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('dismisses the banner on pane keyboard input', async () => {
|
||||
const dismiss = vi.fn()
|
||||
const pane = await renderProbe(true, dismiss)
|
||||
|
||||
pane.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }))
|
||||
|
||||
expect(dismiss).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('dismisses the banner on pane pointer interaction', async () => {
|
||||
const dismiss = vi.fn()
|
||||
const pane = await renderProbe(true, dismiss)
|
||||
|
||||
pane.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
|
||||
expect(dismiss).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not attach dismissal handlers when the banner is hidden', async () => {
|
||||
const dismiss = vi.fn()
|
||||
const pane = await renderProbe(false, dismiss)
|
||||
|
||||
pane.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }))
|
||||
pane.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
|
||||
expect(dismiss).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { useEffect, type RefObject } from 'react'
|
||||
|
||||
export function useSessionRestoredBannerDismiss(
|
||||
visible: boolean,
|
||||
containerRef: RefObject<HTMLElement | null>,
|
||||
dismiss: () => void
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
const container = containerRef.current
|
||||
if (!container) {
|
||||
return
|
||||
}
|
||||
container.addEventListener('keydown', dismiss, { capture: true })
|
||||
container.addEventListener('pointerdown', dismiss, { capture: true })
|
||||
return () => {
|
||||
container.removeEventListener('keydown', dismiss, { capture: true })
|
||||
container.removeEventListener('pointerdown', dismiss, { capture: true })
|
||||
}
|
||||
}, [visible, containerRef, dismiss])
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@ describe('resumeSleepingAgentSessionsForWorktree', () => {
|
|||
const state = useAppStore.getState()
|
||||
const resumedTab = (state.tabsByWorktree['wt-1'] ?? []).find((tab) => tab.id !== 'tab-1')
|
||||
expect(resumedTab?.launchAgent).toBe('claude')
|
||||
expect(state.pendingStartupByTabId[resumedTab!.id]?.showSessionRestoredBanner).toBe(true)
|
||||
expect(state.sleepingAgentSessionsByPaneKey[record.paneKey]).toBeUndefined()
|
||||
})
|
||||
|
||||
|
|
@ -88,6 +89,7 @@ describe('resumeSleepingAgentSessionsForWorktree', () => {
|
|||
const tabs = state.tabsByWorktree['wt-1'] ?? []
|
||||
expect(tabs).toHaveLength(1)
|
||||
expect(tabs[0]?.launchAgent).toBe('claude')
|
||||
expect(state.pendingStartupByTabId[tabs[0]!.id]?.showSessionRestoredBanner).toBe(true)
|
||||
expect(state.sleepingAgentSessionsByPaneKey[record.paneKey]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ function launchSleepingAgentSession(record: SleepingAgentSessionRecord): boolean
|
|||
})
|
||||
state.queueTabStartupCommand(tab.id, {
|
||||
command: startupPlan.launchCommand,
|
||||
showSessionRestoredBanner: true,
|
||||
telemetry: {
|
||||
agent_kind: tuiAgentToAgentKind(record.agent),
|
||||
launch_source: 'sidebar',
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ describe('activateAndRevealWorktree created agent reopen', () => {
|
|||
expect(resumedTab?.launchAgent).toBe('codex')
|
||||
expect(state.pendingStartupByTabId[resumedTab!.id]).toEqual({
|
||||
command: "codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'codex-session-1'",
|
||||
showSessionRestoredBanner: true,
|
||||
telemetry: {
|
||||
agent_kind: 'codex',
|
||||
launch_source: 'sidebar',
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ type WorktreeActivationStore = Partial<WorktreeRuntimeOwnerState> & {
|
|||
command: string
|
||||
env?: Record<string, string>
|
||||
initialAgentStatus?: { agent: TuiAgent; prompt: string }
|
||||
showSessionRestoredBanner?: boolean
|
||||
telemetry?: AgentStartedTelemetry
|
||||
}
|
||||
) => void
|
||||
|
|
|
|||
|
|
@ -301,6 +301,8 @@ export type TerminalSlice = {
|
|||
env?: Record<string, string>
|
||||
/** Initial prompt-start status for agents that lack native prompt hooks. */
|
||||
initialAgentStatus?: { agent: TuiAgent; prompt: string }
|
||||
/** Show the restored-session banner when this startup command mounts. */
|
||||
showSessionRestoredBanner?: boolean
|
||||
/** Telemetry metadata for the `agent_started` event. Threaded all the
|
||||
* way to the `pty:spawn` IPC handler in main so the event fires only
|
||||
* after spawn confirms — never on click-intent. */
|
||||
|
|
@ -438,12 +440,18 @@ export type TerminalSlice = {
|
|||
delivery?: 'terminal-paste'
|
||||
env?: Record<string, string>
|
||||
initialAgentStatus?: { agent: TuiAgent; prompt: string }
|
||||
showSessionRestoredBanner?: boolean
|
||||
telemetry?: AgentStartedTelemetry
|
||||
}
|
||||
) => void
|
||||
consumeTabStartupCommand: (
|
||||
tabId: string
|
||||
) => { command: string; env?: Record<string, string>; telemetry?: AgentStartedTelemetry } | null
|
||||
consumeTabStartupCommand: (tabId: string) => {
|
||||
command: string
|
||||
delivery?: 'terminal-paste'
|
||||
env?: Record<string, string>
|
||||
initialAgentStatus?: { agent: TuiAgent; prompt: string }
|
||||
showSessionRestoredBanner?: boolean
|
||||
telemetry?: AgentStartedTelemetry
|
||||
} | null
|
||||
queueTabSetupSplit: (
|
||||
tabId: string,
|
||||
startup: { command: string; env?: Record<string, string>; direction: SetupSplitDirection }
|
||||
|
|
|
|||
Loading…
Reference in New Issue