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.
- package/dashboard/public/app.js +365 -7
- package/dashboard/public/index.html +16 -0
- package/dashboard/public/style.css +105 -0
- package/dashboard/server.cjs +199 -0
- package/extensions/task-runner.ts +64 -349
- package/extensions/taskplane/abort.ts +11 -1
- package/extensions/taskplane/agent-bridge-extension.ts +159 -0
- package/extensions/taskplane/agent-host.ts +686 -0
- package/extensions/taskplane/engine.ts +75 -3
- package/extensions/taskplane/execution.ts +403 -9
- package/extensions/taskplane/extension.ts +322 -28
- package/extensions/taskplane/lane-runner.ts +567 -0
- package/extensions/taskplane/mailbox.ts +349 -1
- package/extensions/taskplane/merge.ts +208 -51
- package/extensions/taskplane/process-registry.ts +345 -0
- package/extensions/taskplane/resume.ts +185 -47
- package/extensions/taskplane/supervisor.ts +16 -12
- package/extensions/taskplane/task-executor-core.ts +553 -0
- package/extensions/taskplane/types.ts +517 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +41 -33
- package/skills/create-taskplane-task/references/prompt-template.md +3 -3
package/dashboard/public/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
+
}
|