'use client' import * as React from 'react' import { useAgentSession } from '@startsimpli/realtime/react' import type { AgentLifecycleMsg } from '@startsimpli/realtime' import { cn } from '../../lib/utils' import { Terminal, type TerminalHandle } from '../terminal/Terminal' import type { ChatMessageData } from '../chat/types' import { ConversationTimeline } from './ConversationTimeline' import { DiffViewer } from '../diff/DiffViewer' import { mapSocketStatus } from './status' export interface AgentSessionConsoleProps { /** WebSocket URL for the agent session (ws(s)://…). */ url: string /** Auth token appended by the socket (string or getter re-read on reconnect). */ token?: string | (() => string | null | undefined) /** A raw unified diff to show in the Diff tab (the parent fetches it). */ diff?: string /** Load/refresh the diff (wired to the Diff tab's refresh button). */ onLoadDiff?: () => void diffLoading?: boolean diffError?: string | null /** Ref/branch the diff is against (shown in the diff header). */ diffBaseRef?: string title?: string className?: string } type Tab = 'session' | 'diff' /** Turn a structured lifecycle event into a timeline row. Generic — no app knowledge. */ export function lifecycleToMessage(msg: AgentLifecycleMsg): ChatMessageData { const data = msg.data as { message?: string; tool?: string } | undefined const looksLikeTool = /tool|command|exec|edit|write|read|bash/i.test(msg.event) const content = data?.message ?? msg.event.replace(/[_-]/g, ' ') return { id: `lifecycle-${msg.ts ?? Date.now()}-${Math.random().toString(36).slice(2, 8)}`, role: looksLikeTool ? 'tool' : 'system', kind: looksLikeTool ? 'tool' : 'status', toolName: looksLikeTool ? (data?.tool ?? msg.event) : undefined, content, timestamp: msg.ts, } } /** * The live agent-session surface: a status header + a `` and * `` side by side (with a `` in a tab). It is * driven by `useAgentSession` from `@startsimpli/realtime/react`: * - inbound output is pumped into the terminal imperatively (no re-render/byte), * - keystrokes/resizes are forwarded to `session.send`/`session.resize`, * - lifecycle events accumulate into the conversation timeline, * - the composer steers the agent via `session.send`. * * Fully generic — no Foundry/app types. The parent supplies the URL, token, and * (optionally) a diff. */ export function AgentSessionConsole({ url, token, diff, onLoadDiff, diffLoading, diffError, diffBaseRef, title, className, }: AgentSessionConsoleProps) { const termHandleRef = React.useRef(null) const [tab, setTab] = React.useState('session') const [entries, setEntries] = React.useState([]) const session = useAgentSession({ url, token, onOutput: (d) => termHandleRef.current?.write(d), onLifecycle: (m) => setEntries((prev) => [...prev, lifecycleToMessage(m)]), }) const { status, lastEvent, send, sendMessage, resize, socket } = session // An error event trumps the socket status for the terminal indicator. const isError = lastEvent?.type === 'error' const terminalStatus = isError ? 'error' : mapSocketStatus(status) const exited = lastEvent?.type === 'exited' ? lastEvent : null const handleSend = React.useCallback( (text: string) => { // The composer sends a STEER MESSAGE (submitted as a new turn by the runner), NOT // raw keystrokes — otherwise it lands in the agent's prompt box without an Enter and // is never processed (bd 768w.16.7.14.9). Terminal typing still uses `send` (onData). sendMessage(text) setEntries((prev) => [ ...prev, { id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, role: 'user', content: text }, ]) }, [sendMessage], ) const reconnect = React.useCallback(() => socket?.connect(), [socket]) const statusLabel = exited ? `Exited (code ${exited.exitCode})` : isError ? 'Error' : status === 'open' ? 'Connected' : status === 'connecting' ? 'Connecting…' : status === 'reconnecting' ? 'Reconnecting…' : 'Disconnected' return (
{/* Status header + tabs */}
{title && {title}} {statusLabel}
{/* Session tab: Terminal | Conversation split. Terminal stays MOUNTED across tab switches (via `hidden`) so xterm is never re-initialised. */}
{ termHandleRef.current = h }} onData={send} onResize={resize} status={terminalStatus} onReconnect={reconnect} className="min-h-[320px]" /> Steer the agent by sending a message.} className="min-h-[320px]" />
{/* Diff tab */}
) }