import React, { useState, useEffect } from 'react'; import TerminalView from './terminal_view'; import { LiveStatusData } from '../types'; interface LiveStatusPostProps { post: { id: string; props: Record; }; theme: Record; } // Global store for WebSocket updates (set by the plugin index) declare global { interface Window { __livestatus_updates: Record; __livestatus_listeners: Record void>>; } } if (typeof window !== 'undefined') { window.__livestatus_updates = window.__livestatus_updates || {}; window.__livestatus_listeners = window.__livestatus_listeners || {}; } /** * Subscribe to live status updates for a given post ID. */ function useStatusUpdates( postId: string, initialData: LiveStatusData | null, ): LiveStatusData | null { const [data, setData] = useState( window.__livestatus_updates[postId] || initialData, ); useEffect(() => { // Register listener if (!window.__livestatus_listeners[postId]) { window.__livestatus_listeners[postId] = []; } const listener = (newData: LiveStatusData) => setData(newData); window.__livestatus_listeners[postId].push(listener); // Check if we already have data if (window.__livestatus_updates[postId]) { setData(window.__livestatus_updates[postId]); } return () => { const listeners = window.__livestatus_listeners[postId]; if (listeners) { const idx = listeners.indexOf(listener); if (idx >= 0) listeners.splice(idx, 1); } }; }, [postId]); return data; } /** * Format elapsed time in human-readable form. */ function formatElapsed(ms: number): string { if (ms < 0) ms = 0; const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); const h = Math.floor(m / 60); if (h > 0) return `${h}h${m % 60}m`; if (m > 0) return `${m}m${s % 60}s`; return `${s}s`; } /** * Format token count compactly. */ function formatTokens(count: number): string { if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; if (count >= 1000) return `${(count / 1000).toFixed(1)}k`; return String(count); } /** * LiveStatusPost — custom post type component. * Renders a terminal-style live status view with auto-updating content. */ const LiveStatusPost: React.FC = ({ post, theme }) => { const [elapsed, setElapsed] = useState(0); // Build initial data from post props const initialData: LiveStatusData = { session_key: post.props.session_key || '', post_id: post.id, agent_id: post.props.agent_id || 'unknown', status: post.props.status || 'active', lines: post.props.final_lines || [], elapsed_ms: post.props.elapsed_ms || 0, token_count: post.props.token_count || 0, children: [], start_time_ms: post.props.start_time_ms || 0, }; const data = useStatusUpdates(post.id, initialData); const isActive = data?.status === 'active'; // Client-side elapsed time counter (ticks every 1s when active) useEffect(() => { if (!isActive || !data?.start_time_ms) return; const interval = setInterval(() => { setElapsed(Date.now() - data.start_time_ms); }, 1000); return () => clearInterval(interval); }, [isActive, data?.start_time_ms]); if (!data) { return
Loading status...
; } const displayElapsed = isActive && data.start_time_ms ? formatElapsed(elapsed || Date.now() - data.start_time_ms) : formatElapsed(data.elapsed_ms); const statusClass = `ls-status-${data.status}`; return (
{data.agent_id} {isActive && } {data.status.toUpperCase()} {displayElapsed}
{data.children && data.children.length > 0 && (
{data.children.map((child, i) => ( ))}
)} {!isActive && data.token_count > 0 && (
{formatTokens(data.token_count)} tokens
)}
); }; /** * Renders a child/sub-agent session (collapsed by default). */ const ChildSession: React.FC<{ child: LiveStatusData }> = ({ child }) => { const [expanded, setExpanded] = useState(false); return (
setExpanded(!expanded)} style={{ cursor: 'pointer' }} > {expanded ? '\u25BC' : '\u25B6'} {child.agent_id} {child.status.toUpperCase()} {formatElapsed(child.elapsed_ms)}
{expanded && }
); }; export default LiveStatusPost;