/** Accessible, poll-backed transcript for externally started Hermes workers. */ import { Button } from "@nous-research/ui/ui/components/button"; import { Activity, ChevronDown, ChevronUp, RefreshCw, TerminalSquare, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { api, type SessionInfo, type SessionMessage, } from "@/lib/api"; const POLL_MS = 2_000; interface SessionActivityPanelProps { sessionId: string; profile?: string; } function messageLabel(message: SessionMessage): string { if (message.role === "tool") { return message.tool_name ? `Tool result: ${message.tool_name}` : "Tool result"; } if (message.tool_calls?.length) { const names = message.tool_calls .map((call) => call.function.name) .filter(Boolean) .join(", "); return names ? `Tool calls: ${names}` : "Tool calls"; } return message.role === "assistant" ? "Hermes" : message.role; } function latestTimestamp(messages: SessionMessage[]): number | null { let latest: number | null = null; for (const message of messages) { if (typeof message.timestamp !== "number") continue; latest = latest === null ? message.timestamp : Math.max(latest, message.timestamp); } return latest; } function ActivityMessage({ message, index, }: { message: SessionMessage; index: number; }) { const toolCalls = message.tool_calls ?? []; const content = message.content ?? ""; const isToolResult = message.role === "tool"; return (
{messageLabel(message)} {typeof message.timestamp === "number" && ( )}
{toolCalls.map((call) => (
{call.function.name || "tool"} arguments
            {call.function.arguments}
          
))} {content && isToolResult ? (
{content.length.toLocaleString()} characters of output
            {content}
          
) : content ? (
          {content}
        
) : toolCalls.length === 0 ? (
Waiting for the next recorded event…
) : null}
); } /** * Show every persisted message and tool event without relying on xterm's * canvas. Resumed sessions default to this view so an unknown or delayed * backend source classification cannot hide activity; the normal terminal * remains one click away and continues running underneath it. */ export function SessionActivityPanel({ sessionId, profile, }: SessionActivityPanelProps) { const [detail, setDetail] = useState(null); const [messages, setMessages] = useState([]); const [expanded, setExpanded] = useState(true); const [error, setError] = useState(null); const [pollTime, setPollTime] = useState(Date.now()); const requestRunning = useRef(false); const scrollRef = useRef(null); const stickToBottom = useRef(true); const load = useCallback(async () => { if (requestRunning.current) return; requestRunning.current = true; try { const [nextDetail, response] = await Promise.all([ api.getSessionDetail(sessionId, profile ?? ""), api.getSessionMessages(sessionId, profile ?? ""), ]); setDetail(nextDetail); setMessages(Array.isArray(response.messages) ? response.messages : []); setError(null); setPollTime(Date.now()); if (stickToBottom.current) { requestAnimationFrame(() => { const node = scrollRef.current; if (node) node.scrollTop = node.scrollHeight; }); } } catch (cause) { setError(cause instanceof Error ? cause.message : "activity feed unavailable"); } finally { requestRunning.current = false; } }, [profile, sessionId]); useEffect(() => { void load(); const timer = window.setInterval(() => void load(), POLL_MS); const onVisibility = () => { if (!document.hidden) void load(); }; document.addEventListener("visibilitychange", onVisibility); return () => { window.clearInterval(timer); document.removeEventListener("visibilitychange", onVisibility); }; }, [load]); const latest = useMemo(() => latestTimestamp(messages), [messages]); const quietSeconds = latest === null ? null : Math.max(0, pollTime / 1000 - latest); const running = detail?.ended_at == null; const status = error ? "Activity feed reconnecting" : running && quietSeconds !== null && quietSeconds > 15 ? "Working — waiting for the model or a tool result" : running ? "Working" : "Run finished"; if (!expanded) { return ( ); } return (
Live worker activity
{status} · {messages.length} recorded events
{error && (
{error}. Retrying automatically.
)}
{ const node = event.currentTarget; stickToBottom.current = node.scrollHeight - node.scrollTop - node.clientHeight < 80; }} > {messages.length === 0 ? (
Connecting to the worker transcript…
This view refreshes every two seconds.
) : ( messages.map((message, index) => ( )) )}
Poll-backed transcript; terminal rendering is not required. {expanded ? : } Updated {new Date(pollTime).toLocaleTimeString()}
); }