import { PTY_LIVE_NOTE, describeUnconfirmedCloseStop } from '../shared/pty-liveness-verdict' import { describeTerminalWaitBlockedReason } from '../shared/terminal-wait-blocked-reason-legacy-alias' import { formatListingHostScope, type WithAnnotatedHostScope } from './omitted-host-scope-selectors' import type { RuntimeTerminalClose, RuntimeTerminalCreate, RuntimeTerminalFocus, RuntimeTerminalListResult, RuntimeTerminalVisualLayout, RuntimeTerminalVisualLayoutNode, RuntimeTerminalVisualPaneNode, RuntimeTerminalVisualTab, RuntimeTerminalRead, RuntimeTerminalRename, RuntimeTerminalSend, RuntimeTerminalShow, RuntimeTerminalSplit, RuntimeTerminalWait } from '../shared/runtime-types' export function formatTerminalList( result: WithAnnotatedHostScope ): string { const scope = formatListingHostScope(result.hostScope) if (result.terminals.length === 0) { return `No terminals listed.\n${scope}` } const body = result.terminals .map( (terminal) => `${terminal.handle} ${terminal.title ?? '(untitled)'} ${terminal.connected ? 'connected' : 'disconnected'} host=${terminal.executionHostId ?? 'unverifiable'} ${terminal.worktreePath}\n${terminal.preview ? `preview: ${terminal.preview}` : 'preview: '}` ) .join('\n\n') const visualLayout = formatTerminalVisualLayouts(result.visualLayouts) const bodyWithLayout = visualLayout ? `${body}\n\nvisual layout:\n${visualLayout}` : body const bodyWithScope = `${bodyWithLayout}\n\n${scope}` return result.truncated ? `${bodyWithScope}\ntruncated: showing ${result.terminals.length} of ${result.totalCount}` : bodyWithScope } function formatTerminalVisualLayouts( layouts: readonly RuntimeTerminalVisualLayout[] | undefined ): string | null { if (!layouts || layouts.length === 0) { return null } return layouts .map((layout) => [ `worktree: ${layout.worktreePath || layout.worktreeId}`, ...formatVisualLayoutNode(layout.root, 0) ].join('\n') ) .join('\n\n') } function formatVisualLayoutNode(node: RuntimeTerminalVisualLayoutNode, depth: number): string[] { const indent = ' '.repeat(depth) if (node.type === 'split') { return [ `${indent}split ${node.direction}`, ...formatVisualLayoutNode(node.first, depth + 1), ...formatVisualLayoutNode(node.second, depth + 1) ] } return [ `${indent}group ${node.groupId ?? '(default)'}`, ...node.tabs.flatMap((tab) => formatVisualTab(tab, depth + 1)) ] } function formatVisualTab(tab: RuntimeTerminalVisualTab, depth: number): string[] { const indent = ' '.repeat(depth) return [ `${indent}tab ${tab.tabId} ${tab.title ?? '(untitled)'}`, ...formatVisualPaneNode(tab.panes, depth + 1) ] } function formatVisualPaneNode(node: RuntimeTerminalVisualPaneNode, depth: number): string[] { const indent = ' '.repeat(depth) if (node.type === 'pane-split') { return [ `${indent}pane split ${node.direction}`, ...formatVisualPaneNode(node.first, depth + 1), ...formatVisualPaneNode(node.second, depth + 1) ] } const marker = node.active ? '* ' : ' ' return [ `${indent}${marker}${node.handle} ${node.title ?? '(untitled)'} tab=${node.tabId} leaf=${node.leafId}` ] } export function formatTerminalShow(result: { terminal: RuntimeTerminalShow }): string { const terminal = result.terminal return [ `handle: ${terminal.handle}`, `title: ${terminal.title ?? '(untitled)'}`, `worktree: ${terminal.worktreePath}`, `branch: ${terminal.branch}`, `leaf: ${terminal.leafId}`, `ptyId: ${terminal.ptyId ?? 'none'}`, `connected: ${terminal.connected}`, `writable: ${terminal.writable}`, // Why listed above the preview: the preview is where a reader would otherwise have to // spot the prompt by eye, which is the work this line exists to remove. `agentWait: ${formatAgentWait(terminal.agentWait)}`, `preview: ${terminal.preview || ''}` ].join('\n') } function formatAgentWait(agentWait: RuntimeTerminalShow['agentWait']): string { if (agentWait === undefined) { return 'unknown (not evaluated)' } if (!agentWait) { return 'none' } if (!agentWait.reason) { return `interactive prompt (via ${agentWait.source})` } return `${describeTerminalWaitBlockedReason(agentWait.reason)} (via ${agentWait.source})` } export function formatTerminalRead(result: { terminal: RuntimeTerminalRead }): string { const terminal = result.terminal const oldestCursor = typeof terminal.oldestCursor === 'string' ? [`oldest cursor: ${terminal.oldestCursor}`] : [] const latestCursor = typeof terminal.latestCursor === 'string' ? [`latest cursor: ${terminal.latestCursor}`] : [] const limitedWarning = formatTerminalReadLimitedWarning(terminal) const header = [ `handle: ${terminal.handle}`, `status: ${terminal.status}`, ...(terminal.source ? [`source: ${terminal.source}`] : []), ...(terminal.draft ? [`draft: ${JSON.stringify(terminal.draft)}`] : []), ...(terminal.nextCursor !== null ? [`cursor: ${terminal.nextCursor}`] : []), ...oldestCursor, ...latestCursor, ...(terminal.truncated ? ['warning: older output is no longer retained'] : []), ...(limitedWarning ? [limitedWarning] : []), // Why: the caller asked for the rendered screen; say plainly that this is not it rather // than let repaint fragments be read as what the terminal displayed. ...(terminal.source === 'screen-unavailable' ? [ 'warning: no rendered screen was available, so this is accumulated output; repainted lines may appear as stacked fragments' ] : []) ] return [...header, '', ...terminal.tail].join('\n') } function formatTerminalReadLimitedWarning(terminal: RuntimeTerminalRead): string | null { if (!terminal.limited) { return null } if ( typeof terminal.nextCursor === 'string' && typeof terminal.latestCursor === 'string' && terminal.nextCursor !== terminal.latestCursor ) { return `warning: output limited; continue with --cursor ${terminal.nextCursor}` } if ( typeof terminal.oldestCursor === 'string' && typeof terminal.latestCursor === 'string' && terminal.oldestCursor !== terminal.latestCursor ) { // A tail preview's next cursor is already latest, so oldestCursor is the retained history entry point. return `warning: output limited; page retained output with --cursor ${terminal.oldestCursor} --limit ` } return 'warning: output limited' } export function formatTerminalSend(result: { send: RuntimeTerminalSend }): string { if (!result.send.accepted) { const reason = result.send.refusedReason ? `: ${result.send.refusedReason}` : '' return `Input refused by ${result.send.handle}${reason}.` } const prompt = result.send.prompt if (!prompt) { return `Sent ${result.send.bytesWritten} bytes to ${result.send.handle}.` } return [ `Prompt ${prompt.requestId} on ${result.send.handle}: ${prompt.stages.join(' -> ')}.`, `provider: ${prompt.provider}`, `delivery observation: ${prompt.observation}`, ...terminalSendWarnings(result.send).map((warning) => `warning: ${warning}`) ].join('\n') } /** The same warnings the text formatter prints, so a --json caller sees them too. */ export function terminalSendWarnings(send: RuntimeTerminalSend): string[] { const warning = send.accepted && send.prompt ? promptObservationWarning(send.prompt) : null return warning ? [warning] : [] } function promptObservationWarning( prompt: NonNullable ): string | null { if (prompt.observation === 'permission') { return `delivery was not observed because the provider requires permission. Resolve the permission prompt in the terminal, then reissue the exact command with --retry-request ${prompt.requestId} and --wait-submit .` } if (prompt.observation === 'incarnation_replaced') { return 'delivery was not observed because the terminal process was replaced. Inspect the current terminal before sending a new prompt; do not retry with this request ID.' } // Ordered before the unsupported arm: an agent provider that never reached turn_started // needs the swallowed-Enter recovery even if this host could not observe the submit. if (prompt.provider !== 'unsupported' && prompt.provider !== 'old-host') { return prompt.stages.includes('turn_started') ? null : `input was accepted but no turn start was observed, so the Enter may have been swallowed. Confirm delivery by reissuing the exact command with --retry-request ${prompt.requestId} --wait-submit ; the same request ID replays the receipt instead of sending the prompt again.` } if (prompt.observation === 'unsupported') { return prompt.provider === 'old-host' ? 'this host predates durable prompt receipts. Update Orca on the execution host, and inspect the terminal before retrying an ambiguous send.' : 'input was accepted, but this provider cannot report delivery. Inspect the terminal before retrying.' } return null } export function formatTerminalRename(result: { rename: RuntimeTerminalRename }): string { return result.rename.title ? `Renamed terminal ${result.rename.handle} to "${result.rename.title}".` : `Cleared title for terminal ${result.rename.handle}.` } export function formatTerminalCreate(result: { terminal: RuntimeTerminalCreate }): string { const titleNote = result.terminal.title ? ` (title: "${result.terminal.title}")` : '' const surfaceNote = result.terminal.surface ? ` [${result.terminal.surface}]` : '' const warningNote = result.terminal.warning ? `\nwarning: ${result.terminal.warning}` : '' return `Created terminal ${result.terminal.handle}${titleNote}${surfaceNote}${warningNote}` } export function formatTerminalSplit(result: { split: RuntimeTerminalSplit }): string { return `Split pane ${result.split.handle} in tab ${result.split.tabId}` } export function formatTerminalFocus(result: { focus: RuntimeTerminalFocus }): string { if (result.focus.navigated === false) { return `Focus request for terminal ${result.focus.handle} was superseded or host navigation was skipped (tab ${result.focus.tabId}).` } return `Focused terminal ${result.focus.handle} (tab ${result.focus.tabId}).` } /** "PTY killed." is a claim of observed death, so only a confirmed kill earns it. */ function describePtyStop(close: RuntimeTerminalClose): string { if (close.ptyKilled) { return ' PTY killed.' } if (close.ptyStopVerdict === 'live') { return ` ${PTY_LIVE_NOTE}` } if (close.ptyStopVerdict === 'unverifiable') { return ` ${describeUnconfirmedCloseStop(close)}` } return '' } export function formatTerminalClose(result: { close: RuntimeTerminalClose }): string { if (result.close.closeMode === 'tab') { return `Closed terminal tab ${result.close.tabId} (${result.close.handle}).` } return `Closed terminal ${result.close.handle}.${describePtyStop(result.close)}` } export function formatTerminalWait(result: { wait: RuntimeTerminalWait }): string { const lines = [ `handle: ${result.wait.handle}`, `condition: ${result.wait.condition}`, `satisfied: ${result.wait.satisfied}`, `status: ${result.wait.status}`, `exitCode: ${result.wait.exitCode ?? 'null'}` ] if (result.wait.blockedReason) { lines.push(`blockedReason: ${describeTerminalWaitBlockedReason(result.wait.blockedReason)}`) } return lines.join('\n') }