mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Fix idle agent cursor after reattach
Reset the cursor after reattaching an already-idle Codex session so post-SIGWINCH repaint cannot leave the focused cursor as a steady bar.
This commit is contained in:
@@ -2434,6 +2434,53 @@ describe('connectPanePty', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('resets an already-idle agent cursor again after reattach SIGWINCH repaint', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('tab-pty')
|
||||
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) => {
|
||||
if (sessionId) {
|
||||
return { id: sessionId, snapshot: 'restored idle codex snapshot' }
|
||||
}
|
||||
return null
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: 'tab-pty', title: 'Codex done' }]
|
||||
},
|
||||
runtimePaneTitlesByTabId: {
|
||||
'tab-1': { 1: 'Codex done' }
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings
|
||||
}
|
||||
} as StoreState
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_1,
|
||||
restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' }
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(window.api.pty.signal).toHaveBeenCalledWith('tab-pty', 'SIGWINCH')
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
RESET_TERMINAL_CURSOR_STYLE,
|
||||
expect.any(Function)
|
||||
)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
RESET_TERMINAL_CURSOR_STYLE,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
// Why: when a reattach result carries both snapshot and replay (the daemon
|
||||
// host serves the snapshot, the relay replay buffer covers the same tail),
|
||||
// painting both into xterm doubles the same lines. This is the duplicated-
|
||||
|
||||
@@ -83,6 +83,7 @@ const HIDDEN_OUTPUT_RESTORE_PENDING_CHARS = 512 * 1024
|
||||
const HIDDEN_STARTUP_RENDERER_QUERY_WINDOW_MS = 10_000
|
||||
const STARTUP_COMMAND_EXTENSION_RE = /\.(?:exe|cmd|bat|ps1)$/i
|
||||
const TERMINAL_RENDERER_RISK_SCAN_TAIL_CHARS = 256
|
||||
const REATTACH_IDLE_AGENT_CURSOR_RESET_DELAY_MS = 250
|
||||
// Why: this is only shown if renderer backlog overflowed and main-owned
|
||||
// terminal state is unavailable, so the user has an explicit loss signal.
|
||||
const HIDDEN_OUTPUT_RESTORE_UNAVAILABLE_WARNING =
|
||||
@@ -330,6 +331,7 @@ export function connectPanePty(
|
||||
let wasAgentTaskCompleteNotificationEnabled = isAgentTaskCompleteNotificationEnabled()
|
||||
let terminalBellNotificationTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pendingTerminalBellNotification = false
|
||||
let reattachIdleAgentCursorResetTimer: ReturnType<typeof setTimeout> | null = null
|
||||
// Why: idle callbacks are registered before the deferred PTY output plumbing
|
||||
// exists. Start with the shared scheduler, then switch to the PTY writer
|
||||
// below so hidden-tab resets keep backlog-recovery callbacks and byte order.
|
||||
@@ -427,6 +429,41 @@ export function connectPanePty(
|
||||
}
|
||||
}, AGENT_INTERRUPT_SETTLE_MS)
|
||||
}
|
||||
const clearReattachIdleAgentCursorResetTimer = (): void => {
|
||||
if (reattachIdleAgentCursorResetTimer !== null) {
|
||||
clearTimeout(reattachIdleAgentCursorResetTimer)
|
||||
reattachIdleAgentCursorResetTimer = null
|
||||
}
|
||||
}
|
||||
const getCurrentTerminalTitle = (): string | null => {
|
||||
const state = useAppStore.getState()
|
||||
const runtimeTitle = state.runtimePaneTitlesByTabId?.[deps.tabId]?.[pane.id]
|
||||
const tabTitle = (state.tabsByWorktree[deps.worktreeId] ?? []).find(
|
||||
(entry) => entry.id === deps.tabId
|
||||
)?.title
|
||||
return runtimeTitle ?? tabTitle ?? null
|
||||
}
|
||||
const scheduleReattachIdleAgentCursorReset = (): void => {
|
||||
const status = detectAgentStatusFromTitle(getCurrentTerminalTitle() ?? '')
|
||||
if (status !== 'idle' && status !== 'permission') {
|
||||
return
|
||||
}
|
||||
clearReattachIdleAgentCursorResetTimer()
|
||||
reattachIdleAgentCursorResetTimer = setTimeout(() => {
|
||||
reattachIdleAgentCursorResetTimer = null
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
const latestStatus = detectAgentStatusFromTitle(getCurrentTerminalTitle() ?? '')
|
||||
if (latestStatus !== 'idle' && latestStatus !== 'permission') {
|
||||
return
|
||||
}
|
||||
// Why: restored idle agent TUIs can repaint after reattach SIGWINCH and
|
||||
// reapply DECSCUSR steady-bar; the normal working→idle reset will not
|
||||
// fire because the agent was already idle before Orca restarted.
|
||||
queueAgentIdleCursorReset()
|
||||
}, REATTACH_IDLE_AGENT_CURSOR_RESET_DELAY_MS)
|
||||
}
|
||||
const interruptInference = createAgentInterruptInference({
|
||||
paneKey: cacheKey,
|
||||
getStatusEntry: () => useAppStore.getState().agentStatusByPaneKey[cacheKey],
|
||||
@@ -1773,6 +1810,7 @@ export function connectPanePty(
|
||||
if (!isRemoteRuntimePtyId(currentPtyId)) {
|
||||
window.api.pty.signal(currentPtyId, 'SIGWINCH')
|
||||
}
|
||||
scheduleReattachIdleAgentCursorReset()
|
||||
}
|
||||
restoreScrollStateAfterSnapshotReplay(scrollState)
|
||||
}
|
||||
@@ -2076,6 +2114,7 @@ export function connectPanePty(
|
||||
if (!isRemoteRuntimePtyId(ptyId)) {
|
||||
window.api.pty.signal(ptyId, 'SIGWINCH')
|
||||
}
|
||||
scheduleReattachIdleAgentCursorReset()
|
||||
|
||||
scheduleRuntimeGraphSync()
|
||||
}
|
||||
@@ -2567,6 +2606,7 @@ export function connectPanePty(
|
||||
clearPendingAgentTaskCompleteNotification()
|
||||
pendingTerminalBellNotification = false
|
||||
clearTerminalBellNotificationTimer()
|
||||
clearReattachIdleAgentCursorResetTimer()
|
||||
unregisterBacklogRecovery?.()
|
||||
unregisterBacklogRecovery = null
|
||||
unregisterDocumentVisibilityRecovery?.()
|
||||
|
||||
Reference in New Issue
Block a user