taskplane 0.22.17 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,47 @@ function formatCost(usd) {
55
55
  return `$${usd.toFixed(2)}`;
56
56
  }
57
57
 
58
+ /**
59
+ * TP-107: Check if a lane has a live agent via the Runtime V2 registry.
60
+ * Returns true/false if registry data is available, null if no V2 data.
61
+ */
62
+ function isLaneAliveV2(laneNumber) {
63
+ if (!currentData || !currentData.runtimeRegistry || !currentData.runtimeRegistry.agents) return null;
64
+ const agents = Object.values(currentData.runtimeRegistry.agents);
65
+ const laneAgents = agents.filter(a => a.laneNumber === laneNumber);
66
+ if (laneAgents.length === 0) return null;
67
+ return laneAgents.some(a => a.status === 'running' || a.status === 'spawning');
68
+ }
69
+
70
+ /**
71
+ * TP-107: Merge Runtime V2 lane snapshot data onto legacy lane state.
72
+ * V2 fields take precedence when present; legacy fields are preserved as fallback.
73
+ */
74
+ function mergeV2LaneSnapshot(legacyLs, v2snap) {
75
+ const base = legacyLs ? { ...legacyLs } : {};
76
+ // Overlay V2 fields from nested worker snapshot onto flat legacy shape.
77
+ // RuntimeLaneSnapshot has worker: { status, elapsedMs, toolCalls, contextPct, ... }
78
+ const w = v2snap.worker;
79
+ if (w) {
80
+ if (w.status) base.workerStatus = w.status;
81
+ if (w.elapsedMs != null) base.workerElapsed = w.elapsedMs;
82
+ if (w.contextPct != null) base.workerContextPct = w.contextPct;
83
+ if (w.toolCalls != null) base.workerToolCount = w.toolCalls;
84
+ if (w.lastTool) base.workerLastTool = w.lastTool;
85
+ if (w.costUsd != null) base.workerCostUsd = w.costUsd;
86
+ if (w.inputTokens != null) base.workerInputTokens = w.inputTokens;
87
+ if (w.outputTokens != null) base.workerOutputTokens = w.outputTokens;
88
+ if (w.cacheReadTokens != null) base.workerCacheReadTokens = w.cacheReadTokens;
89
+ if (w.cacheWriteTokens != null) base.workerCacheWriteTokens = w.cacheWriteTokens;
90
+ }
91
+ if (v2snap.taskId) base.taskId = v2snap.taskId;
92
+ // Enrich progress display from V2 snapshot
93
+ if (v2snap.progress) {
94
+ base._v2Progress = v2snap.progress;
95
+ }
96
+ return base;
97
+ }
98
+
58
99
  /** Build a compact token summary string from lane state sidecar data.
59
100
  * Display: ↑total_input ↓output (cost)
60
101
  * Anthropic splits input into: uncached `input` + `cacheRead`.
@@ -428,6 +469,8 @@ function renderLanesTasks(batch, tmuxSessions) {
428
469
  const tmuxSet = new Set(tmuxSessions || []);
429
470
  const laneStates = currentData?.laneStates || {};
430
471
  const telemetry = currentData?.telemetry || {};
472
+ // TP-107: V2 lane snapshots take precedence over legacy lane states when present
473
+ const v2Snapshots = currentData?.runtimeLaneSnapshots || {};
431
474
  const showRepos = knownRepos.length >= 2;
432
475
  let html = "";
433
476
 
@@ -440,7 +483,9 @@ function renderLanesTasks(batch, tmuxSessions) {
440
483
  if (!laneMatchesRepo) continue;
441
484
  }
442
485
 
443
- const alive = tmuxSet.has(lane.tmuxSessionName);
486
+ // TP-107: check V2 registry for liveness first, fall back to tmux
487
+ const v2Alive = isLaneAliveV2(lane.laneNumber);
488
+ const alive = v2Alive !== null ? v2Alive : tmuxSet.has(lane.tmuxSessionName);
444
489
  const tmuxCmd = `tmux attach -t ${lane.tmuxSessionName}`;
445
490
 
446
491
  // Lane header
@@ -475,7 +520,10 @@ function renderLanesTasks(batch, tmuxSessions) {
475
520
  }
476
521
 
477
522
  // Get lane state and telemetry for worker stats
478
- const ls = laneStates[lane.tmuxSessionName] || null;
523
+ // TP-107: V2 lane snapshots take precedence when present
524
+ const v2snap = v2Snapshots[lane.laneNumber] || null;
525
+ const legacyLs = laneStates[lane.tmuxSessionName] || null;
526
+ const ls = v2snap ? mergeV2LaneSnapshot(legacyLs, v2snap) : legacyLs;
479
527
  const tel = telemetry[lane.tmuxSessionName] || null;
480
528
 
481
529
  for (const task of laneTasks) {
@@ -829,6 +877,164 @@ function renderMergeAgents(batch, tmuxSessions) {
829
877
  $mergeBody.innerHTML = html;
830
878
  }
831
879
 
880
+ // ─── Render: Runtime V2 Agents (TP-107) ─────────────────────────────────────
881
+
882
+ function renderAgentsPanel(registry) {
883
+ const $panel = document.getElementById('agents-panel');
884
+ const $body = document.getElementById('agents-body');
885
+ if (!$panel || !$body) return;
886
+
887
+ if (!registry || !registry.agents || Object.keys(registry.agents).length === 0) {
888
+ $panel.style.display = 'none';
889
+ return;
890
+ }
891
+
892
+ $panel.style.display = '';
893
+ const agents = Object.values(registry.agents);
894
+ let html = '<div class="agents-grid">';
895
+
896
+ for (const agent of agents) {
897
+ const isTerminal = ['exited', 'crashed', 'timed_out', 'killed'].includes(agent.status);
898
+ const statusClass = isTerminal ? 'agent-terminal' : 'agent-live';
899
+ const icon = isTerminal ? '\u{1F534}' : '\u{1F7E2}';
900
+ const elapsed = agent.startedAt ? Math.round((Date.now() - agent.startedAt) / 1000) : 0;
901
+ const elapsedStr = elapsed > 0 ? formatDuration(elapsed * 1000) : '';
902
+
903
+ html += `<div class="agent-card ${statusClass}">`;
904
+ html += `<div class="agent-header">${icon} <strong>${escapeHtml(agent.agentId)}</strong></div>`;
905
+ html += `<div class="agent-meta">`;
906
+ html += `<span class="agent-badge">${escapeHtml(agent.role)}</span>`;
907
+ if (agent.laneNumber != null) html += `<span class="agent-badge">lane ${agent.laneNumber}</span>`;
908
+ if (agent.taskId) html += `<span class="agent-badge">${escapeHtml(agent.taskId)}</span>`;
909
+ html += `<span class="agent-badge agent-status-${agent.status}">${escapeHtml(agent.status)}</span>`;
910
+ if (elapsedStr && !isTerminal) html += `<span class="agent-badge">${elapsedStr}</span>`;
911
+ html += `</div>`;
912
+ html += `</div>`;
913
+ }
914
+
915
+ html += '</div>';
916
+ $body.innerHTML = html;
917
+ }
918
+
919
+ // ─── Render: Mailbox Messages (TP-107) ──────────────────────────────────────
920
+
921
+ function renderMessagesPanel(mailbox) {
922
+ const $panel = document.getElementById('messages-panel');
923
+ const $body = document.getElementById('messages-body');
924
+ if (!$panel || !$body) return;
925
+
926
+ // TP-093: event-authoritative model — prefer audit events, fallback to directory scan
927
+ const auditEvents = mailbox?.auditEvents || [];
928
+ const dirMessages = mailbox?.messages || [];
929
+ const hasData = auditEvents.length > 0 || dirMessages.length > 0;
930
+
931
+ if (!mailbox || !hasData) {
932
+ $panel.style.display = 'none';
933
+ return;
934
+ }
935
+
936
+ $panel.style.display = '';
937
+ let html = '<div class="messages-list">';
938
+
939
+ if (auditEvents.length > 0) {
940
+ // Primary: render from audit event stream (authoritative, durable)
941
+ for (const evt of auditEvents) {
942
+ html += renderMailboxAuditEvent(evt);
943
+ }
944
+ } else {
945
+ // Fallback: render from directory scan (legacy compatibility)
946
+ for (const msg of dirMessages) {
947
+ html += renderMailboxDirMessage(msg);
948
+ }
949
+ }
950
+
951
+ html += '</div>';
952
+ $body.innerHTML = html;
953
+ }
954
+
955
+ /** Render a single mailbox audit event (events.jsonl row). */
956
+ function renderMailboxAuditEvent(evt) {
957
+ const ts = evt.ts ? new Date(evt.ts).toLocaleTimeString() : '';
958
+ const type = evt.type || '';
959
+
960
+ let direction = '';
961
+ let statusBadge = '';
962
+ let typeBadge = '';
963
+ let preview = '';
964
+
965
+ if (type === 'message_sent') {
966
+ const isBroadcast = evt.broadcast;
967
+ direction = isBroadcast ? '\u2192 all (broadcast)' : `\u2192 ${escapeHtml(evt.to || '')}`;
968
+ statusBadge = '<span class="msg-badge msg-delivered">sent</span>';
969
+ typeBadge = `<span class="msg-badge msg-type">${escapeHtml(evt.messageType || '')}</span>`;
970
+ preview = evt.contentPreview || '';
971
+ } else if (type === 'message_delivered') {
972
+ direction = `\u2192 ${escapeHtml(evt.to || '')}`;
973
+ statusBadge = evt.broadcast
974
+ ? '<span class="msg-badge msg-delivered">broadcast delivered</span>'
975
+ : '<span class="msg-badge msg-delivered">delivered</span>';
976
+ typeBadge = evt.messageType ? `<span class="msg-badge msg-type">${escapeHtml(evt.messageType)}</span>` : '';
977
+ preview = evt.contentPreview || '';
978
+ } else if (type === 'message_replied' || type === 'message_escalated') {
979
+ direction = `\u2190 ${escapeHtml(evt.from || '')}`;
980
+ statusBadge = type === 'message_escalated'
981
+ ? '<span class="msg-badge msg-reply">escalation</span>'
982
+ : '<span class="msg-badge msg-reply">reply</span>';
983
+ typeBadge = evt.messageType ? `<span class="msg-badge msg-type">${escapeHtml(evt.messageType)}</span>` : '';
984
+ preview = evt.contentPreview || '';
985
+ } else if (type === 'message_rate_limited') {
986
+ direction = `\u2192 ${escapeHtml(evt.to || '')}`;
987
+ statusBadge = '<span class="msg-badge msg-rate-limited">rate limited</span>';
988
+ const waitSec = evt.retryAfterMs ? Math.ceil(evt.retryAfterMs / 1000) : '?';
989
+ preview = `${evt.reason || 'Rate limited'} (retry in ${waitSec}s)`;
990
+ } else {
991
+ // Unknown event type — render generically
992
+ direction = evt.from ? `${escapeHtml(evt.from)}` : '';
993
+ preview = JSON.stringify(evt).slice(0, 120);
994
+ }
995
+
996
+ return `<div class="message-row">`
997
+ + `<span class="msg-time">${escapeHtml(ts)}</span>`
998
+ + `<span class="msg-direction">${direction}</span>`
999
+ + typeBadge
1000
+ + statusBadge
1001
+ + `<span class="msg-preview">${escapeHtml(preview)}</span>`
1002
+ + `</div>`;
1003
+ }
1004
+
1005
+ /** Render a single directory-scanned message (legacy fallback). */
1006
+ function renderMailboxDirMessage(msg) {
1007
+ const ts = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '';
1008
+ // TP-093: for broadcast per-agent ack markers, show recipient identity instead of "_broadcast"
1009
+ let direction;
1010
+ if (msg.to === 'supervisor') {
1011
+ direction = '\u2190 supervisor';
1012
+ } else if (msg._isBroadcast && msg._agentDir && msg._agentDir !== '_broadcast') {
1013
+ direction = `\u2192 ${escapeHtml(msg._agentDir)} (broadcast)`;
1014
+ } else {
1015
+ direction = `\u2192 ${escapeHtml(msg.to || msg._agentDir || '')}`;
1016
+ }
1017
+ let statusBadge;
1018
+ if (msg._status === 'pending') statusBadge = '<span class="msg-badge msg-pending">pending</span>';
1019
+ else if (msg._status === 'delivered') statusBadge = '<span class="msg-badge msg-delivered">delivered</span>';
1020
+ else if (msg._status === 'reply') statusBadge = '<span class="msg-badge msg-reply">reply</span>';
1021
+ else if (msg._status === 'reply-acked') statusBadge = '<span class="msg-badge msg-delivered">reply (acked)</span>';
1022
+ else statusBadge = '';
1023
+ const typeBadge = `<span class="msg-badge msg-type">${escapeHtml(msg.type || '')}</span>`;
1024
+ const preview = (msg.content || '').slice(0, 120);
1025
+ const broadcastTag = msg._isBroadcast ? ' <span class="msg-badge msg-type">broadcast</span>' : '';
1026
+
1027
+ return `<div class="message-row">`
1028
+ + `<span class="msg-time">${escapeHtml(ts)}</span>`
1029
+ + `<span class="msg-direction">${direction}</span>`
1030
+ + typeBadge
1031
+ + statusBadge
1032
+ + broadcastTag
1033
+ + `<span class="msg-preview">${escapeHtml(preview)}</span>`
1034
+ + `</div>`;
1035
+ }
1036
+
1037
+
832
1038
  // ─── Render: Errors ─────────────────────────────────────────────────────────
833
1039
 
834
1040
  function renderErrors(batch) {
@@ -1156,6 +1362,9 @@ function render(data) {
1156
1362
  renderSupervisor(data);
1157
1363
  renderLanesTasks(batch, tmux);
1158
1364
  renderMergeAgents(batch, tmux);
1365
+ // TP-107: Runtime V2 panels
1366
+ renderAgentsPanel(data.runtimeRegistry);
1367
+ renderMessagesPanel(data.mailbox);
1159
1368
  renderErrors(batch);
1160
1369
 
1161
1370
  const taskCount = (batch.tasks || []).length;
@@ -1217,7 +1426,31 @@ let convRenderedLines = 0;
1217
1426
  // STATUS.md diff-and-skip state
1218
1427
  let lastStatusMdText = "";
1219
1428
 
1220
- // ── Open conversation viewer ────────────────────────────────────────────────
1429
+ // ── Open conversation viewer (TP-107: V2 events preferred, legacy fallback) ──
1430
+
1431
+ /**
1432
+ * Resolve a lane's tmux session name to a Runtime V2 agent ID via the registry.
1433
+ * Returns null if no V2 registry data is available.
1434
+ */
1435
+ function resolveV2AgentId(sessionName) {
1436
+ if (!currentData || !currentData.runtimeRegistry || !currentData.runtimeRegistry.agents) return null;
1437
+ const agents = currentData.runtimeRegistry.agents;
1438
+ // Direct match on agentId
1439
+ if (agents[sessionName]) return sessionName;
1440
+ // Match by tmux session prefix + "-worker" suffix (common V2 naming)
1441
+ const workerKey = sessionName + '-worker';
1442
+ if (agents[workerKey]) return workerKey;
1443
+ // Search by laneNumber match from lane snapshots
1444
+ for (const [id, agent] of Object.entries(agents)) {
1445
+ if (agent.role === 'worker' && agent.laneNumber != null) {
1446
+ const m = sessionName.match(/lane-(\d+)/);
1447
+ if (m && parseInt(m[1]) === agent.laneNumber) return id;
1448
+ }
1449
+ }
1450
+ return null;
1451
+ }
1452
+
1453
+ let viewerV2AgentId = null; // Runtime V2 agent ID for current conversation view
1221
1454
 
1222
1455
  function viewConversation(sessionName) {
1223
1456
  // Toggle off if already viewing this session
@@ -1233,7 +1466,12 @@ function viewConversation(sessionName) {
1233
1466
  autoScrollOn = true;
1234
1467
  convRenderedLines = 0;
1235
1468
 
1236
- $terminalTitle.textContent = `Worker Conversation ${sessionName}`;
1469
+ // TP-107: Resolve V2 agent ID for events endpoint
1470
+ const v2AgentId = resolveV2AgentId(sessionName);
1471
+ viewerV2AgentId = v2AgentId;
1472
+
1473
+ const label = v2AgentId || sessionName;
1474
+ $terminalTitle.textContent = `Worker Conversation — ${label}`;
1237
1475
  $autoScrollText.textContent = 'Follow feed';
1238
1476
  $autoScrollCheckbox.checked = true;
1239
1477
  $terminalPanel.style.display = '';
@@ -1246,9 +1484,21 @@ function viewConversation(sessionName) {
1246
1484
  }
1247
1485
 
1248
1486
  function pollConversation() {
1249
- fetch(`/api/conversation/${encodeURIComponent(viewerTarget)}`)
1250
- .then(r => r.text())
1251
- .then(text => {
1487
+ // TP-107: prefer V2 agent events when available, fallback to legacy conversation
1488
+ const endpoint = viewerV2AgentId
1489
+ ? `/api/agent-events/${encodeURIComponent(viewerV2AgentId)}`
1490
+ : `/api/conversation/${encodeURIComponent(viewerTarget)}`;
1491
+ const isV2 = !!viewerV2AgentId;
1492
+
1493
+ fetch(endpoint)
1494
+ .then(r => isV2 ? r.json() : r.text())
1495
+ .then(data => {
1496
+ if (isV2) {
1497
+ renderV2AgentEvents(data);
1498
+ return;
1499
+ }
1500
+ // Legacy: data is JSONL text
1501
+ const text = data;
1252
1502
  if (!text.trim()) {
1253
1503
  if (convRenderedLines === 0) {
1254
1504
  $terminalBody.innerHTML = '<div class="conv-empty">No conversation events yet…</div>';
@@ -1299,6 +1549,111 @@ function pollConversation() {
1299
1549
  .catch(() => {});
1300
1550
  }
1301
1551
 
1552
+ // ── Runtime V2 agent event renderer (TP-107) ──────────────────────────────
1553
+
1554
+ // Stable cursor for V2 event rendering.
1555
+ // Uses a signature string from the last rendered event so the sliding window
1556
+ // (server caps at 300) doesn't stall when new tail events push older ones out.
1557
+ let v2LastCursor = null; // signature of last rendered event
1558
+ let v2FirstRender = true;
1559
+
1560
+ function v2EventSignature(evt) {
1561
+ return `${evt.ts || 0}:${evt.type || ''}:${JSON.stringify(evt.payload || {}).slice(0, 80)}`;
1562
+ }
1563
+
1564
+ function renderV2AgentEvents(events) {
1565
+ if (!Array.isArray(events) || events.length === 0) {
1566
+ if (v2FirstRender) {
1567
+ $terminalBody.innerHTML = '<div class="conv-empty">No agent events yet…</div>';
1568
+ }
1569
+ return;
1570
+ }
1571
+
1572
+ let container = $terminalBody.querySelector('.conv-stream');
1573
+
1574
+ if (v2FirstRender || !container) {
1575
+ // First load or container missing: full render
1576
+ $terminalBody.innerHTML = '';
1577
+ container = document.createElement('div');
1578
+ container.className = 'conv-stream';
1579
+ $terminalBody.appendChild(container);
1580
+ for (const evt of events) {
1581
+ const html = renderV2Event(evt);
1582
+ if (html) container.insertAdjacentHTML('beforeend', html);
1583
+ }
1584
+ v2LastCursor = v2EventSignature(events[events.length - 1]);
1585
+ v2FirstRender = false;
1586
+ } else {
1587
+ // Incremental: find first unseen event after cursor
1588
+ let cursorIdx = -1;
1589
+ if (v2LastCursor) {
1590
+ for (let i = events.length - 1; i >= 0; i--) {
1591
+ if (v2EventSignature(events[i]) === v2LastCursor) {
1592
+ cursorIdx = i;
1593
+ break;
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ if (cursorIdx === -1) {
1599
+ // Cursor not found (rotation/restart): full re-render
1600
+ container.innerHTML = '';
1601
+ for (const evt of events) {
1602
+ const html = renderV2Event(evt);
1603
+ if (html) container.insertAdjacentHTML('beforeend', html);
1604
+ }
1605
+ } else if (cursorIdx < events.length - 1) {
1606
+ // Append only new events after cursor
1607
+ const newEvents = events.slice(cursorIdx + 1);
1608
+ for (const evt of newEvents) {
1609
+ const html = renderV2Event(evt);
1610
+ if (html) container.insertAdjacentHTML('beforeend', html);
1611
+ }
1612
+ } else {
1613
+ // No new events
1614
+ return;
1615
+ }
1616
+
1617
+ v2LastCursor = v2EventSignature(events[events.length - 1]);
1618
+ }
1619
+
1620
+ if (autoScrollOn) {
1621
+ isProgrammaticScroll = true;
1622
+ $terminalBody.scrollTop = $terminalBody.scrollHeight;
1623
+ requestAnimationFrame(() => { isProgrammaticScroll = false; });
1624
+ }
1625
+ }
1626
+
1627
+ function renderV2Event(evt) {
1628
+ const ts = evt.ts ? new Date(evt.ts).toLocaleTimeString() : '';
1629
+ const type = evt.type || 'unknown';
1630
+
1631
+ switch (type) {
1632
+ case 'assistant_message':
1633
+ return `<div class="conv-event conv-assistant"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">assistant</span><span class="conv-text">${escapeHtml((evt.payload?.text || '').slice(0, 2000))}</span></div>`;
1634
+ case 'prompt_sent':
1635
+ return `<div class="conv-event conv-user"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">user</span><span class="conv-text">${escapeHtml((evt.payload?.text || '').slice(0, 2000))}</span></div>`;
1636
+ case 'tool_call':
1637
+ return `<div class="conv-event conv-tool"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">tool</span><span class="conv-text">${escapeHtml(evt.payload?.tool || type)} ${escapeHtml((evt.payload?.path || '').slice(0, 200))}</span></div>`;
1638
+ case 'tool_result':
1639
+ return `<div class="conv-event conv-tool-result"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">result</span><span class="conv-text">${escapeHtml((evt.payload?.summary || '').slice(0, 500))}</span></div>`;
1640
+ case 'agent_started':
1641
+ return `<div class="conv-event conv-lifecycle"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">▶</span><span class="conv-text">Agent started (${escapeHtml(evt.role || '')} lane ${evt.laneNumber ?? '?'})</span></div>`;
1642
+ case 'agent_exited':
1643
+ return `<div class="conv-event conv-lifecycle"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">■</span><span class="conv-text">Agent exited (code ${evt.payload?.exitCode ?? '?'})</span></div>`;
1644
+ case 'agent_crashed':
1645
+ case 'agent_killed':
1646
+ case 'agent_timeout':
1647
+ return `<div class="conv-event conv-lifecycle conv-error"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">⚠</span><span class="conv-text">${escapeHtml(type)} ${escapeHtml(evt.payload?.reason || '')}</span></div>`;
1648
+ case 'message_delivered':
1649
+ return `<div class="conv-event conv-steer"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">✉</span><span class="conv-text">Steering: ${escapeHtml((evt.payload?.content || '').slice(0, 500))}</span></div>`;
1650
+ case 'context_pressure':
1651
+ return `<div class="conv-event conv-lifecycle"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">⚠</span><span class="conv-text">Context pressure: ${evt.payload?.pct ?? '?'}%</span></div>`;
1652
+ default:
1653
+ return `<div class="conv-event conv-lifecycle"><span class="conv-ts">${escapeHtml(ts)}</span><span class="conv-role">•</span><span class="conv-text">${escapeHtml(type)}</span></div>`;
1654
+ }
1655
+ }
1656
+
1302
1657
  // ── Open STATUS.md viewer ───────────────────────────────────────────────────
1303
1658
 
1304
1659
  function viewStatusMd(taskId) {
@@ -1535,8 +1890,11 @@ function closeViewer() {
1535
1890
  }
1536
1891
  viewerMode = null;
1537
1892
  viewerTarget = null;
1893
+ viewerV2AgentId = null;
1538
1894
  autoScrollOn = false;
1539
1895
  convRenderedLines = 0;
1896
+ v2LastCursor = null;
1897
+ v2FirstRender = true;
1540
1898
  lastStatusMdText = '';
1541
1899
  $terminalPanel.style.display = 'none';
1542
1900
  $terminalBody.innerHTML = '';
@@ -87,6 +87,22 @@
87
87
  </div>
88
88
  </div>
89
89
 
90
+ <!-- TP-107: Runtime V2 Agents Panel -->
91
+ <div class="panel" id="agents-panel">
92
+ <div class="panel-header">Agents</div>
93
+ <div class="panel-body" id="agents-body">
94
+ <div class="empty-state">No agent registry data</div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- TP-107: Mailbox Messages Panel -->
99
+ <div class="panel" id="messages-panel">
100
+ <div class="panel-header">Messages</div>
101
+ <div class="panel-body" id="messages-body">
102
+ <div class="empty-state">No messages</div>
103
+ </div>
104
+ </div>
105
+
90
106
  <!-- Viewer Panel: Conversation + STATUS.md (hidden until activated) -->
91
107
  <div class="panel terminal-panel" id="terminal-panel" style="display:none;">
92
108
  <div class="panel-header">
@@ -1703,3 +1703,108 @@ body {
1703
1703
  color: var(--text-primary);
1704
1704
  margin: 16px 0 8px;
1705
1705
  }
1706
+
1707
+ /* TP-107: Agents Panel */
1708
+ .agents-grid {
1709
+ display: flex;
1710
+ flex-direction: column;
1711
+ gap: 6px;
1712
+ }
1713
+ .agent-card {
1714
+ padding: 8px 12px;
1715
+ border-radius: 6px;
1716
+ background: var(--bg-secondary);
1717
+ border-left: 3px solid var(--green);
1718
+ }
1719
+ .agent-card.agent-terminal {
1720
+ border-left-color: var(--text-faint);
1721
+ opacity: 0.7;
1722
+ }
1723
+ .agent-header {
1724
+ font-size: 0.85rem;
1725
+ margin-bottom: 4px;
1726
+ }
1727
+ .agent-meta {
1728
+ display: flex;
1729
+ flex-wrap: wrap;
1730
+ gap: 4px;
1731
+ }
1732
+ .agent-badge {
1733
+ font-size: 0.7rem;
1734
+ padding: 1px 6px;
1735
+ border-radius: 3px;
1736
+ background: var(--bg-primary);
1737
+ color: var(--text-secondary);
1738
+ }
1739
+ .agent-status-running { color: var(--green); }
1740
+ .agent-status-spawning { color: var(--yellow); }
1741
+ .agent-status-exited { color: var(--text-faint); }
1742
+ .agent-status-crashed { color: var(--red); }
1743
+ .agent-status-killed { color: var(--red); }
1744
+ .agent-status-timed_out { color: var(--yellow); }
1745
+
1746
+ /* TP-107: Messages Panel */
1747
+ .messages-list {
1748
+ display: flex;
1749
+ flex-direction: column;
1750
+ gap: 4px;
1751
+ }
1752
+ .message-row {
1753
+ display: flex;
1754
+ align-items: center;
1755
+ gap: 8px;
1756
+ padding: 4px 8px;
1757
+ font-size: 0.8rem;
1758
+ border-radius: 4px;
1759
+ background: var(--bg-secondary);
1760
+ }
1761
+ .msg-time {
1762
+ color: var(--text-faint);
1763
+ min-width: 60px;
1764
+ }
1765
+ .msg-direction {
1766
+ color: var(--text-secondary);
1767
+ min-width: 100px;
1768
+ }
1769
+ .msg-badge {
1770
+ font-size: 0.65rem;
1771
+ padding: 1px 5px;
1772
+ border-radius: 3px;
1773
+ text-transform: uppercase;
1774
+ }
1775
+ .msg-type {
1776
+ background: var(--bg-primary);
1777
+ color: var(--text-secondary);
1778
+ }
1779
+ .msg-pending {
1780
+ background: var(--yellow);
1781
+ color: var(--bg-primary);
1782
+ }
1783
+ .msg-delivered {
1784
+ background: var(--green);
1785
+ color: var(--bg-primary);
1786
+ }
1787
+ .msg-reply {
1788
+ background: var(--blue);
1789
+ color: var(--bg-primary);
1790
+ }
1791
+ .msg-preview {
1792
+ color: var(--text-primary);
1793
+ overflow: hidden;
1794
+ text-overflow: ellipsis;
1795
+ white-space: nowrap;
1796
+ flex: 1;
1797
+ }
1798
+ .msg-rate-limited {
1799
+ background: var(--red);
1800
+ color: var(--bg-primary);
1801
+ }
1802
+
1803
+ /* TP-107: V2 conversation event styles */
1804
+ .conv-steer {
1805
+ border-left: 3px solid var(--blue);
1806
+ padding-left: 8px;
1807
+ }
1808
+ .conv-error {
1809
+ color: var(--red);
1810
+ }