/** * Main view — the conversation of the active session + agent, rendered from * the message protocol (`/api/v3/ws` + `GET /api/v1/sessions/{id}/history`): * * - Persisted state comes from the REST history endpoint only: the initial * load reads the newest page, a full refresh re-reads it and re-covers * the previously loaded window, and "load earlier" pages further with a * `before_turn` cursor. * - The WS channel carries the recovery payload and all live traffic; both * are applied to the store through the same idempotent replace-by-id * path (delta family appended by id, entity content authoritative), so * there is no reset/buffer/cursor machinery. * - Every subscribe ack (initial and reconnect) triggers an `after_step` * catch-up from the newest terminal step; an empty catch-up whose * anchor vanished (undo/clear while away) falls back to a full refresh. * * Rendering groups the flat timeline by turn (system markers stay * standalone) and is typed entirely by the protocol schemas * (`@moonshot-ai/kap-server/protocol`). Cancels go through the * `agentLoopService` channel over the debug RPC * surface (`/api/v1/debug`); interaction answers (approve/reject, * answer/dismiss) go through the public REST endpoints * (`src/interactions/api.ts`); the running indicator derives from * `session.state`. */ import { IAgentLoopService } from '@moonshot-ai/agent-core-v2/agent/loop/loop'; import type { AssistantMessage, ContentPart, InteractionMessage, InteractionQuestionItem, SessionStateMessage, StepMessage, SystemMessage, TaskMessage, ThinkingMessage, TodoMessage, ToolCallMessage, TurnMessage, UserMessage, } from '@moonshot-ai/kap-server/protocol'; import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react'; import { AuditTrail } from '../audit/trail'; import { useConnection } from '../connection'; import { answerQuestion, decideApproval, dismissQuestion, type QuestionAnswerWire, } from '../interactions/api'; import type { SearchHit } from '../search/api'; import { ChatChannel } from '../transcript/channel'; import { EMPTY_CHAT_STATE, hasTurnId, type ChatState, type TimelineEntry, } from '../transcript/store'; import { ActionButton, Badge, ErrorLine, JsonView, relTime } from '../ui'; import { ChatSearchBar } from './ChatSearchBar'; const noopSubscribe = () => () => {}; /** Active session id for deeply nested interaction views (approve/answer buttons). */ const SessionContext = createContext(''); /** * A navigation request into the chat timeline (e.g. from a search hit). The * channel pages backwards until the turn enters the loaded window, then * scrolls it into view and flashes it briefly. */ export interface ChatJump { /** Turn to locate (`t`); omitted = switch session/agent only, no scroll. */ readonly turnId?: string | undefined; /** Step within the turn (`t.`); falls back to the turn card. */ readonly stepId?: string | undefined; /** Changes on every request so re-clicking the same hit re-triggers. */ readonly nonce: number; } interface ChatChannelState { /** Null until the effect has created the channel (pre-ready / no session). */ readonly channel: ChatChannel | null; readonly state: ChatState; /** Records every step that built the store (audit panel data source). */ readonly trail: AuditTrail | null; /** True once the initial REST page load succeeded. */ readonly loaded: boolean; /** Set when the initial/refresh load failed. */ readonly loadError: unknown; } /** * Owns the channel (store + REST + WS) for one (sessionId, agentId) pair. */ function useChatChannel( sessionId: string | null, agentId: string, ready: boolean, captureAnchor: () => void, ): ChatChannelState { const { baseUrl, config } = useConnection(); const token = config.token.trim(); const [channel, setChannel] = useState(null); const [loaded, setLoaded] = useState(false); const [loadError, setLoadError] = useState(null); useEffect(() => { if (!ready || sessionId === null) return; const authToken = token === '' ? undefined : token; const next = new ChatChannel({ baseUrl, token: authToken, sessionId, agentId, onWillReplace: captureAnchor, onLoaded: () => { setLoaded(true); setLoadError(null); }, onLoadError: (error) => { setLoadError(error); }, }); setChannel(next); setLoaded(false); setLoadError(null); next.start(); return () => { next.close(); setChannel(null); }; }, [sessionId, agentId, ready, baseUrl, token, captureAnchor]); const state = useSyncExternalStore( channel?.store.subscribe ?? noopSubscribe, () => channel?.store.getState() ?? EMPTY_CHAT_STATE, ); return { channel, state, trail: channel?.trail ?? null, loaded, loadError, }; } export function ChatView({ sessionId, agentId, ready, onTrailChange, jump, onJumpHandled, onOpenSearchHit, }: { sessionId: string | null; agentId: string; ready: boolean; /** Hands the audit trail of the current channel up to the app shell (the audit panel lives in the right dock, not inside this view). */ onTrailChange?: (trail: AuditTrail | null) => void; /** Pending navigation into the timeline (search result click). */ jump?: ChatJump | null | undefined; /** Called once the jump has been processed (or found un-actionable). */ onJumpHandled?: (() => void) | undefined; /** Hands an in-chat search hit up to the app shell (agent switch + jump). */ onOpenSearchHit?: ((hit: SearchHit) => void) | undefined; }) { const { klient } = useConnection(); const [sendError, setSendError] = useState(null); const [loadingOlder, setLoadingOlder] = useState(false); const [olderError, setOlderError] = useState(null); const scrollRef = useRef(null); /** Distance from the scroll bottom captured before a prepend (restore anchor). */ const anchorRef = useRef(null); /** Whether the viewport was pinned to the bottom before the last update. */ const stickBottomRef = useRef(true); /** The jump target being flashed (cleared on a timer). */ const [flash, setFlash] = useState<{ turnId: string; stepId?: string | undefined } | null>(null); const captureAnchor = useCallback(() => { const el = scrollRef.current; if (el !== null) anchorRef.current = el.scrollHeight - el.scrollTop; }, []); const { channel, state, trail, loaded, loadError } = useChatChannel( sessionId, agentId, ready, captureAnchor, ); const entries = state.entries; // The audit panel is rendered by the app shell's right dock; report the // trail (null while no channel exists) so it can subscribe to it there. useEffect(() => { onTrailChange?.(trail); }, [onTrailChange, trail]); // Jump navigation (search result click): once the channel has loaded, page // backwards until the target turn enters the window, then scroll to the // step (or the turn card) and flash it briefly. A turn that never appears // (cut by an undo) degrades to no scroll. useEffect(() => { if (jump === null || jump === undefined || !loaded || channel === null || sessionId === null) { return; } if (jump.turnId === undefined) { onJumpHandled?.(); return; } let cancelled = false; const isCancelled = (): boolean => cancelled; const turnId = jump.turnId; const stepId = jump.stepId; void (async () => { stickBottomRef.current = false; const store = channel.store; try { while ( !hasTurnId(store.getState().entries, turnId) && store.getState().hasMoreOlder && !isCancelled() ) { const before = store.getState().entries.length; await channel.loadOlder(); if (store.getState().entries.length === before) break; } } catch { // A failed older-page load leaves the window as-is; degrade to no scroll. } if (cancelled) return; if (!hasTurnId(store.getState().entries, turnId)) { onJumpHandled?.(); return; } setFlash({ turnId, stepId }); // The prepend renders asynchronously; wait two frames before scrolling. requestAnimationFrame(() => { requestAnimationFrame(() => { if (cancelled) return; const root = scrollRef.current; const stepEl = stepId !== undefined ? root?.querySelector(`[data-step-id="${CSS.escape(stepId)}"]`) : undefined; const target = stepEl ?? root?.querySelector(`[data-turn-id="${CSS.escape(turnId)}"]`); target?.scrollIntoView({ block: 'start' }); }); }); onJumpHandled?.(); })(); return () => { cancelled = true; }; }, [jump, loaded, channel, sessionId, onJumpHandled]); // The flash highlight clears itself after a short moment. useEffect(() => { if (flash === null) return; const timer = setTimeout(() => setFlash(null), 2400); return () => clearTimeout(timer); }, [flash]); useLayoutEffect(() => { const el = scrollRef.current; if (el === null) return; if (anchorRef.current !== null) { el.scrollTop = el.scrollHeight - anchorRef.current; anchorRef.current = null; return; } if (stickBottomRef.current) el.scrollTop = el.scrollHeight; }, [entries]); const onScroll = () => { const el = scrollRef.current; if (el === null) return; stickBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80; }; const loadOlder = async () => { if (channel === null || loadingOlder) return; captureAnchor(); setLoadingOlder(true); setOlderError(null); try { await channel.loadOlder(); } catch (error) { anchorRef.current = null; setOlderError(error); trail?.recordEvent( 'older-error', error instanceof Error ? error.message : String(error), channel.store.getState(), ); } finally { setLoadingOlder(false); } }; // Auto-paging: the top sentinel auto-loads the previous REST page when it // approaches the viewport (paused while a previous load failed — the retry // button re-arms it). This replaces any manual "load earlier" action. const loadOlderRef = useRef(loadOlder); loadOlderRef.current = loadOlder; const topSentinelRef = useRef(null); const hasMoreOlder = state.hasMoreOlder; useEffect(() => { const sentinel = topSentinelRef.current; const root = scrollRef.current; if (sentinel === null || root === null || olderError !== null) return; const observer = new IntersectionObserver( (observed) => { if (observed.some((entry) => entry.isIntersecting)) void loadOlderRef.current(); }, { root, rootMargin: '400px 0px 0px 0px' }, ); observer.observe(sentinel); return () => { observer.disconnect(); }; }, [hasMoreOlder, loaded, olderError, loadingOlder]); const running = (state.sessionState !== undefined && state.sessionState.status !== 'idle') || isAnyTurnRunning(entries); const pendingCount = [...state.interactions.values()].filter( (interaction) => interaction.status === 'pending', ).length; // Interactions render inline at their anchor tool call; entities without // an anchor (or whose anchor is outside the loaded window) collect here // and render floating at the bottom. Unanchored tasks (no tool call // references them, e.g. shell-command tasks) do the same. const anchoredToolCallIds = useMemo(() => collectToolCallIds(entries), [entries]); const unanchoredInteractions = [...state.interactions.values()].filter( (interaction) => interaction.tool_call_id === undefined || !anchoredToolCallIds.has(interaction.tool_call_id), ); const anchoredTaskIds = useMemo(() => collectTaskIds(entries), [entries]); const unanchoredTasks = [...state.tasks.values()].filter( (task) => !anchoredTaskIds.has(task.task_id), ); const latestTodo = latestTodoOf(state.todos); const cancel = async () => { if (sessionId === null) return; try { await klient.session(sessionId).agent(agentId).service(IAgentLoopService).cancel(undefined); trail?.recordEvent('cancel', undefined, state); } catch (error) { setSendError(error); } }; if (sessionId === null) { return (
Select a session on the left to open its conversation.
); } if (!ready) { return (
Loading session…
); } return (
{sessionId} agent: {agentId} {running ? turn running : idle} {pendingCount > 0 ? {pendingCount} pending : null} {state.sessionState !== undefined ? ( ) : null}
{state.hasMoreOlder ? (
{loadingOlder ? 'Loading earlier turns…' : ''}
) : null} {olderError !== null ? (
{ setOlderError(null); void loadOlder(); }} > Retry loading earlier turns
) : null} {loadError !== null ? (
Failed to load the session history — the server may be too old to expose the history API.
) : null} {entries.length === 0 && loadError === null ? (
{loaded ? 'Empty transcript.' : 'Loading transcript…'}
) : null} {latestTodo !== undefined && latestTodo.items.length > 0 ? ( ) : null} {unanchoredInteractions.map((interaction) => ( ))} {unanchoredTasks.map((task) => ( ))}
{sendError !== null ? (
) : null}
void cancel()} danger disabled={!running}> Cancel
); } // ---------------------------------------------------------------- timeline type RenderItem = | { readonly kind: 'group'; readonly turnId: string; readonly turn: TurnMessage | undefined; readonly items: readonly TimelineEntry[]; } | { readonly kind: 'system'; readonly key: string; readonly message: SystemMessage }; /** Pseudo-turn grouping queued (unread) user messages, which carry no turn_id yet. */ const QUEUED_TURN_ID = '$queued'; function groupTimeline(entries: readonly TimelineEntry[]): RenderItem[] { interface GroupDraft { turn?: TurnMessage; items: TimelineEntry[]; } const drafts = new Map(); const order: ( | { kind: 'group'; turnId: string } | { kind: 'system'; key: string; message: SystemMessage } )[] = []; for (const entry of entries) { const message = entry.message; if (message.type === 'system') { order.push({ kind: 'system', key: entry.key, message }); continue; } const turnId = message.turn_id ?? QUEUED_TURN_ID; let draft = drafts.get(turnId); if (draft === undefined) { draft = { items: [] }; drafts.set(turnId, draft); order.push({ kind: 'group', turnId }); } if (message.type === 'turn') draft.turn = message; draft.items.push(entry); } return order.map((item) => item.kind === 'system' ? item : { kind: 'group', turnId: item.turnId, turn: drafts.get(item.turnId)?.turn, items: drafts.get(item.turnId)?.items ?? [], }, ); } function Timeline({ items, interactions, tasks, flash, }: { items: readonly TimelineEntry[]; interactions: ReadonlyMap; tasks: ReadonlyMap; flash?: { turnId: string; stepId?: string | undefined } | null | undefined; }) { const renderItems = useMemo(() => groupTimeline(items), [items]); return ( <> {renderItems.map((item) => item.kind === 'system' ? ( // Native virtual screen: the browser skips layout/paint for // off-screen items and remembers their last rendered size.
) : (
), )} ); } function isAnyTurnRunning(entries: readonly TimelineEntry[]): boolean { return entries.some( (entry) => entry.message.type === 'turn' && entry.message.status === 'running', ); } function collectToolCallIds(entries: readonly TimelineEntry[]): Set { const ids = new Set(); for (const entry of entries) { if (entry.message.type === 'tool_call') ids.add(entry.message.tool_call_id); } return ids; } function collectTaskIds(entries: readonly TimelineEntry[]): Set { const ids = new Set(); for (const entry of entries) { if (entry.message.type === 'tool_call' && entry.message.task_id !== undefined) { ids.add(entry.message.task_id); } } return ids; } function latestTodoOf(todos: ReadonlyMap): TodoMessage | undefined { let latest: TodoMessage | undefined; for (const todo of todos.values()) { if (latest === undefined || todo.timestamp > latest.timestamp) latest = todo; } return latest; } // ---------------------------------------------------------------- turn group function TurnGroupView({ turnId, turn, items, interactions, tasks, flash, }: { turnId: string; turn: TurnMessage | undefined; items: readonly TimelineEntry[]; interactions: ReadonlyMap; tasks: ReadonlyMap; flash?: { turnId: string; stepId?: string | undefined } | null | undefined; }) { const turnFlashed = flash?.turnId === turnId && flash.stepId === undefined; const queued = turnId === QUEUED_TURN_ID; return (
{queued ? ( queued ) : ( {turnId} )} {turn !== undefined ? ( <> {turn.origin.kind} {turn.status} {turn.started_at !== undefined ? ( {relTime(Date.parse(turn.started_at))} ) : null} {turn.usage !== undefined ? ( {turnUsageText(turn.usage)} ) : null} ) : queued ? ( not consumed into a turn yet ) : ( turn header outside the window )}
{turn?.attachment_ids !== undefined && turn.attachment_ids.length > 0 ? ( ) : null} {items.map((entry) => ( ))}
); } function turnUsageText(usage: NonNullable): string { const parts: string[] = []; if (usage.input_tokens !== undefined) parts.push(`in ${usage.input_tokens}`); if (usage.output_tokens !== undefined) parts.push(`out ${usage.output_tokens}`); if (usage.cached_tokens !== undefined) parts.push(`cached ${usage.cached_tokens}`); if (usage.cost !== undefined) parts.push(`$${usage.cost.toFixed(4)}`); return parts.join(' / '); } function TimelineEntryView({ entry, interactions, tasks, flash, }: { entry: TimelineEntry; interactions: ReadonlyMap; tasks: ReadonlyMap; flash?: { turnId: string; stepId?: string | undefined } | null | undefined; }) { const message = entry.message; switch (message.type) { case 'turn': return null; case 'step': return ; case 'user': return ; case 'assistant': return ; case 'thinking': return ; case 'tool_call': return ; case 'system': return ; } } function StepRow({ step, flashed }: { step: StepMessage; flashed: boolean }) { return (
{step.step_id} {step.status} {step.retry !== undefined ? ( retry {step.retry.failed_attempt}→{step.retry.next_attempt}/{step.retry.max_attempts}:{' '} {step.retry.error_name} ) : null} {step.finish_reason !== undefined ? finish: {step.finish_reason} : null} {step.usage !== undefined ? ( in {step.usage.input_other + step.usage.input_cache_read + step.usage.input_cache_creation}{' '} / out {step.usage.output} ) : null} {step.end_reason !== undefined ? {step.end_reason} : null} {step.end_message !== undefined ? {step.end_message} : null}
); } // ---------------------------------------------------------------- messages function UserMessageView({ message }: { message: UserMessage }) { const isUserInput = message.origin === undefined || message.origin.kind === 'user'; return (
{message.message_id} {message.origin !== undefined && message.origin.kind !== 'user' ? ( {userOriginLabel(message.origin)} ) : null} {message.status === 'unread' ? queued : null}
{isUserInput ? (
) : (
)} {message.attachment_ids !== undefined && message.attachment_ids.length > 0 ? ( ) : null} {message.skill_activations !== undefined && message.skill_activations.length > 0 ? (
{message.skill_activations.map((skill) => ( skill: {skill.skill_name} ))}
) : null}
); } function userOriginLabel(origin: Exclude): string { switch (origin.kind) { case 'user': return 'user'; case 'cron': return `cron ${origin.cron_id ?? ''}`.trim(); case 'task': return `task: ${origin.title}`; case 'skill': return `skill: ${origin.skill_name}`; } } function UserContentParts({ parts }: { parts: readonly ContentPart[] }) { const text = parts .filter((part) => part.type === 'text' || part.type === 'think') .map((part) => part.text) .join('\n'); const media = parts.filter( (part) => part.type === 'image' || part.type === 'audio' || part.type === 'video', ); return ( <> {text} {media.length > 0 ? ( {media.map((part, index) => ( {part.type}: {mediaPartLabel(part)} ))} ) : null} ); } function mediaPartLabel(part: ContentPart): string { const name = part.meta['name']; if (typeof name === 'string' && name !== '') return name; const id = part.meta['id']; if (typeof id === 'string' && id !== '') return id; return part.text; } function AssistantMessageView({ message }: { message: AssistantMessage }) { return (
{message.text} {message.status === 'streaming' ? ▍ : null}
); } function ThinkingMessageView({ message }: { message: ThinkingMessage }) { return (
{message.text} {message.status === 'streaming' ? ▍ : null}
); } function AttachmentChips({ ids }: { ids: readonly string[] }) { return (
{ids.map((id) => ( 📎 {id} ))}
); } // ---------------------------------------------------------------- tool calls function ToolCallView({ call, interactions, tasks, }: { call: ToolCallMessage; interactions: ReadonlyMap; tasks: ReadonlyMap; }) { const task = call.task_id !== undefined ? tasks.get(call.task_id) : undefined; const linked = [...interactions.values()].filter( (interaction) => interaction.interaction_id === call.approval_id || interaction.tool_call_id === call.tool_call_id, ); return (
tool {call.name} {call.tool_call_id} {call.view !== undefined && call.view !== call.name ? ( view: {call.view} ) : null} {call.agent_refs?.map((ref) => ( agent: {ref.agent_id} ))} {task !== undefined ? task: {task.status} : null} {call.todo_id !== undefined ? ( todo: {call.todo_id} ) : null}
{call.input !== undefined ? ( typeof call.input === 'string' ? (
            {call.input}
          
) : ( ) ) : call.input_text !== undefined && call.input_text !== '' ? (
          {call.input_text}
        
) : null} {call.output !== undefined ? ( typeof call.output === 'string' ? (
            {call.output}
          
) : ( ) ) : task !== undefined && task.output_tail !== '' ? (
          {task.output_tail}
        
) : null} {call.error !== undefined && call.error !== call.output ? (
{call.error}
) : null} {call.progress !== undefined ? (
progress ({call.progress.kind}):{' '} {call.progress.text ?? (call.progress.percent !== undefined ? `${call.progress.percent}%` : call.progress.custom_kind ?? '')}
) : null} {linked.map((interaction) => ( ))}
); } // ---------------------------------------------------------------- interactions function InteractionEntityView({ interaction, nested, }: { interaction: InteractionMessage; nested?: boolean; }) { const { baseUrl, config } = useConnection(); const sessionId = useContext(SessionContext); const [busy, setBusy] = useState(false); const [respondError, setRespondError] = useState(null); /** Question answers in progress: question id → selected option labels. */ const [selections, setSelections] = useState>>({}); /** Question free-text ("Other") input: question id → draft. */ const [others, setOthers] = useState>>({}); const pending = interaction.status === 'pending'; const questionRequest = interaction.kind === 'question' ? interaction.request : undefined; const api = { baseUrl, token: config.token, sessionId }; const run = (fn: () => Promise): void => { setBusy(true); setRespondError(null); void fn() .catch((error: unknown) => { setRespondError(error); }) .finally(() => { setBusy(false); }); }; const decide = (decision: 'approved' | 'rejected'): void => { run(() => decideApproval(api, interaction.interaction_id, decision)); }; const toggleOption = (question: InteractionQuestionItem, label: string): void => { setSelections((prev) => { const current = prev[question.id] ?? []; const next = question.multi_select === true ? current.includes(label) ? current.filter((item) => item !== label) : [...current, label] : current.includes(label) ? [] : [label]; return { ...prev, [question.id]: next }; }); }; const submitAnswers = (): void => { const answers: Record = {}; for (const question of questionRequest?.questions ?? []) { const selected = selections[question.id] ?? []; const optionIds = selected.flatMap((label) => { const match = question.options.find((option) => option.label === label); return match === undefined ? [] : [match.id]; }); const other = (others[question.id] ?? '').trim(); if (other !== '' && optionIds.length > 0) { answers[question.id] = { kind: 'multi_with_other', option_ids: optionIds, other_text: other, }; } else if (other !== '') { answers[question.id] = { kind: 'other', text: other }; } else if (optionIds.length > 1 || (question.multi_select === true && optionIds.length > 0)) { answers[question.id] = { kind: 'multi', option_ids: optionIds }; } else if (optionIds.length === 1) { answers[question.id] = { kind: 'single', option_id: optionIds[0]! }; } } // Mirror the TUI adapter: no answers at all dismisses the question. if (Object.keys(answers).length === 0) { dismiss(); return; } run(() => answerQuestion(api, interaction.interaction_id, answers, 'enter')); }; const dismiss = (): void => { run(() => dismissQuestion(api, interaction.interaction_id)); }; return (
{interaction.kind} {interaction.status} tool: {interaction.tool_call_id}
{interaction.request !== undefined && questionRequest === undefined ? ( ) : null} {questionRequest !== undefined && !pending ? : null} {interaction.response !== undefined ? : null} {pending && interaction.kind === 'approval' ? (
decide('approved')} disabled={busy}> Approve decide('rejected')} danger disabled={busy}> Reject
) : null} {pending && questionRequest !== undefined ? (
{questionRequest.questions.map((question) => (
{question.header ?? question.question}
{question.options.map((option) => { const selected = (selections[question.id] ?? []).includes(option.label); return ( ); })}
{ setOthers((prev) => ({ ...prev, [question.id]: e.target.value })); }} />
))}
Answer Dismiss
) : null} {respondError !== null ? (
) : null}
); } // ---------------------------------------------------------------- state entities function SystemMarkerView({ message }: { message: SystemMessage }) { return (
system({message.subtype}) {message.system_id} {message.at !== undefined ? {relTime(Date.parse(message.at))} : null}
{message.payload !== undefined ? : null}
); } function TaskCard({ task }: { task: TaskMessage }) { const failed = task.status === 'failed' || task.status === 'timed_out' || task.status === 'lost'; return (
task: {task.kind} {task.description ?? task.task_id} {task.status} {task.detached ? ' (detached)' : ''} {task.child_agent_id !== undefined ? ( agent: {task.child_agent_id} ) : null}
{task.output_tail !== '' ? (
          {task.output_tail}
        
) : null} {task.error !== undefined ? (
          {task.error}
        
) : null} {task.result_summary !== undefined ? (
{task.result_summary}
) : null}
); } function TodoCard({ todo }: { todo: TodoMessage }) { return (
todo (latest)
{todo.items.map((entry, i) => (
{entry.status === 'done' ? '✔' : entry.status === 'in_progress' ? '◐' : '□'} {entry.title}
))}
); } function SessionStateBadges({ sessionState }: { sessionState: SessionStateMessage }) { return ( <> {sessionState.pending_interaction !== undefined && sessionState.pending_interaction !== 'none' ? ( {sessionState.pending_interaction} ) : null} {sessionState.model !== undefined ? {sessionState.model} : null} {sessionState.permission !== undefined ? ( perm: {sessionState.permission} ) : null} {sessionState.modes?.plan !== undefined ? plan mode : null} {sessionState.modes?.swarm !== undefined ? swarm : null} {sessionState.goal !== undefined ? ( goal: {sessionState.goal.status} ) : null} {sessionState.context_tokens !== undefined ? ( ctx {sessionState.context_tokens} {sessionState.max_context_tokens !== undefined ? `/${sessionState.max_context_tokens}` : ''} ) : null} ); }