fix: remove dead delete+recreate and pin code, add poll fallback test
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)
This commit is contained in:
180
plugin/webapp/src/components/live_status_post.tsx
Normal file
180
plugin/webapp/src/components/live_status_post.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user