Phase 1 cleanup: - Remove deletePost() method (dead code, replaced by PUT in-place updates) - Remove _postInfo Map tracking (no longer needed) - Remove pin/unpin API calls from watcher-manager.js (incompatible with PUT updates) - Add JSDoc note on (edited) label limitation in _flushUpdate() - Add integration test: test/integration/poll-fallback.test.js - Fix addSession() lastOffset===0 falsy bug (0 was treated as 'no offset') - Fix pre-existing test failures: add lastOffset:0 where tests expect backlog reads - Fix pre-existing session-monitor test: create stub transcript files - Fix pre-existing status-formatter test: update indent check for blockquote format - Format plugin/ files with Prettier (pre-existing formatting drift)
181 lines
5.3 KiB
TypeScript
181 lines
5.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import TerminalView from './terminal_view';
|
|
import { LiveStatusData } from '../types';
|
|
|
|
interface LiveStatusPostProps {
|
|
post: {
|
|
id: string;
|
|
props: Record<string, any>;
|
|
};
|
|
theme: Record<string, string>;
|
|
}
|
|
|
|
// Global store for WebSocket updates (set by the plugin index)
|
|
declare global {
|
|
interface Window {
|
|
__livestatus_updates: Record<string, LiveStatusData>;
|
|
__livestatus_listeners: Record<string, Array<(data: LiveStatusData) => 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<LiveStatusData | null>(
|
|
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<LiveStatusPostProps> = ({ 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 <div className="ls-post ls-loading">Loading status...</div>;
|
|
}
|
|
|
|
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 (
|
|
<div className={`ls-post ${statusClass}`}>
|
|
<div className="ls-header">
|
|
<span className="ls-agent-badge">{data.agent_id}</span>
|
|
{isActive && <span className="ls-live-dot" />}
|
|
<span className={`ls-status-badge ${statusClass}`}>{data.status.toUpperCase()}</span>
|
|
<span className="ls-elapsed">{displayElapsed}</span>
|
|
</div>
|
|
|
|
<TerminalView lines={data.lines} maxLines={30} />
|
|
|
|
{data.children && data.children.length > 0 && (
|
|
<div className="ls-children">
|
|
{data.children.map((child, i) => (
|
|
<ChildSession key={child.session_key || i} child={child} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{!isActive && data.token_count > 0 && (
|
|
<div className="ls-footer">{formatTokens(data.token_count)} tokens</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Renders a child/sub-agent session (collapsed by default).
|
|
*/
|
|
const ChildSession: React.FC<{ child: LiveStatusData }> = ({ child }) => {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
return (
|
|
<div className="ls-child">
|
|
<div
|
|
className="ls-child-header"
|
|
onClick={() => setExpanded(!expanded)}
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
<span className="ls-expand-icon">{expanded ? '\u25BC' : '\u25B6'}</span>
|
|
<span className="ls-agent-badge ls-child-badge">{child.agent_id}</span>
|
|
<span className={`ls-status-badge ls-status-${child.status}`}>
|
|
{child.status.toUpperCase()}
|
|
</span>
|
|
<span className="ls-elapsed">{formatElapsed(child.elapsed_ms)}</span>
|
|
</div>
|
|
{expanded && <TerminalView lines={child.lines} maxLines={15} />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LiveStatusPost;
|