taskplane 0.22.18 → 0.23.1
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 +412 -21
- 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 +40 -286
- 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 +574 -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,52 @@ 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
|
+
// Map V2 agent status to legacy dashboard status strings
|
|
81
|
+
if (w.status) {
|
|
82
|
+
const statusMap = { running: 'running', spawning: 'running', exited: 'done', crashed: 'error', killed: 'error', timed_out: 'error', wrapping_up: 'running' };
|
|
83
|
+
base.workerStatus = statusMap[w.status] || w.status;
|
|
84
|
+
}
|
|
85
|
+
if (w.elapsedMs != null) base.workerElapsed = w.elapsedMs;
|
|
86
|
+
if (w.contextPct != null) base.workerContextPct = w.contextPct;
|
|
87
|
+
if (w.toolCalls != null) base.workerToolCount = w.toolCalls;
|
|
88
|
+
if (w.lastTool) base.workerLastTool = w.lastTool;
|
|
89
|
+
if (w.costUsd != null) base.workerCostUsd = w.costUsd;
|
|
90
|
+
if (w.inputTokens != null) base.workerInputTokens = w.inputTokens;
|
|
91
|
+
if (w.outputTokens != null) base.workerOutputTokens = w.outputTokens;
|
|
92
|
+
if (w.cacheReadTokens != null) base.workerCacheReadTokens = w.cacheReadTokens;
|
|
93
|
+
if (w.cacheWriteTokens != null) base.workerCacheWriteTokens = w.cacheWriteTokens;
|
|
94
|
+
}
|
|
95
|
+
if (v2snap.taskId) base.taskId = v2snap.taskId;
|
|
96
|
+
if (v2snap.batchId) base.batchId = v2snap.batchId;
|
|
97
|
+
// Enrich progress display from V2 snapshot
|
|
98
|
+
if (v2snap.progress) {
|
|
99
|
+
base._v2Progress = v2snap.progress;
|
|
100
|
+
}
|
|
101
|
+
return base;
|
|
102
|
+
}
|
|
103
|
+
|
|
58
104
|
/** Build a compact token summary string from lane state sidecar data.
|
|
59
105
|
* Display: ↑total_input ↓output (cost)
|
|
60
106
|
* Anthropic splits input into: uncached `input` + `cacheRead`.
|
|
@@ -376,21 +422,49 @@ function renderSummary(batch) {
|
|
|
376
422
|
let elapsedStr = `elapsed: ${formatDuration(elapsed)}`;
|
|
377
423
|
if (batch.updatedAt) elapsedStr += ` · updated: ${relativeTime(batch.updatedAt)}`;
|
|
378
424
|
|
|
379
|
-
// Aggregate tokens
|
|
425
|
+
// Aggregate tokens/cost for summary.
|
|
426
|
+
// Runtime V2 snapshots are authoritative when present; legacy lane-state sidecars are fallback.
|
|
380
427
|
const laneStates = currentData?.laneStates || {};
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
428
|
+
const runtimeLaneSnapshots = currentData?.runtimeLaneSnapshots || {};
|
|
429
|
+
const v2Snaps = Object.values(runtimeLaneSnapshots);
|
|
430
|
+
|
|
431
|
+
let batchInput = 0, batchOutput = 0, batchCacheRead = 0, batchCacheWrite = 0, batchCostFromSnapshots = 0;
|
|
432
|
+
|
|
433
|
+
if (v2Snaps.length > 0) {
|
|
434
|
+
for (const snap of v2Snaps) {
|
|
435
|
+
const w = snap?.worker || {};
|
|
436
|
+
batchInput += w.inputTokens || 0;
|
|
437
|
+
batchOutput += w.outputTokens || 0;
|
|
438
|
+
batchCacheRead += w.cacheReadTokens || 0;
|
|
439
|
+
batchCacheWrite += w.cacheWriteTokens || 0;
|
|
440
|
+
batchCostFromSnapshots += w.costUsd || 0;
|
|
441
|
+
|
|
442
|
+
const r = snap?.reviewer || null;
|
|
443
|
+
if (r) {
|
|
444
|
+
batchInput += r.inputTokens || 0;
|
|
445
|
+
batchOutput += r.outputTokens || 0;
|
|
446
|
+
batchCacheRead += r.cacheReadTokens || 0;
|
|
447
|
+
batchCacheWrite += r.cacheWriteTokens || 0;
|
|
448
|
+
batchCostFromSnapshots += r.costUsd || 0;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
// Legacy fallback
|
|
453
|
+
for (const ls of Object.values(laneStates)) {
|
|
454
|
+
batchInput += ls.workerInputTokens || 0;
|
|
455
|
+
batchOutput += ls.workerOutputTokens || 0;
|
|
456
|
+
batchCacheRead += ls.workerCacheReadTokens || 0;
|
|
457
|
+
batchCacheWrite += ls.workerCacheWriteTokens || 0;
|
|
458
|
+
batchCostFromSnapshots += ls.workerCostUsd || 0;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Keep server-computed cost as fallback for uncovered early-start lanes.
|
|
463
|
+
const batchCost = batchCostFromSnapshots > 0
|
|
464
|
+
? batchCostFromSnapshots
|
|
465
|
+
: ((currentData?.batchTotalCost != null && currentData.batchTotalCost > 0)
|
|
466
|
+
? currentData.batchTotalCost
|
|
467
|
+
: 0);
|
|
394
468
|
const batchTotalIn = batchInput + batchCacheRead;
|
|
395
469
|
if (batchTotalIn > 0 || batchOutput > 0) {
|
|
396
470
|
let tokenStr = ` · tokens: ↑${formatTokens(batchTotalIn)} ↓${formatTokens(batchOutput)}`;
|
|
@@ -428,6 +502,8 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
428
502
|
const tmuxSet = new Set(tmuxSessions || []);
|
|
429
503
|
const laneStates = currentData?.laneStates || {};
|
|
430
504
|
const telemetry = currentData?.telemetry || {};
|
|
505
|
+
// TP-107: V2 lane snapshots take precedence over legacy lane states when present
|
|
506
|
+
const v2Snapshots = currentData?.runtimeLaneSnapshots || {};
|
|
431
507
|
const showRepos = knownRepos.length >= 2;
|
|
432
508
|
let html = "";
|
|
433
509
|
|
|
@@ -440,7 +516,9 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
440
516
|
if (!laneMatchesRepo) continue;
|
|
441
517
|
}
|
|
442
518
|
|
|
443
|
-
|
|
519
|
+
// TP-107: check V2 registry for liveness first, fall back to tmux
|
|
520
|
+
const v2Alive = isLaneAliveV2(lane.laneNumber);
|
|
521
|
+
const alive = v2Alive !== null ? v2Alive : tmuxSet.has(lane.tmuxSessionName);
|
|
444
522
|
const tmuxCmd = `tmux attach -t ${lane.tmuxSessionName}`;
|
|
445
523
|
|
|
446
524
|
// Lane header
|
|
@@ -475,7 +553,10 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
475
553
|
}
|
|
476
554
|
|
|
477
555
|
// Get lane state and telemetry for worker stats
|
|
478
|
-
|
|
556
|
+
// TP-107: V2 lane snapshots take precedence when present
|
|
557
|
+
const v2snap = v2Snapshots[lane.laneNumber] || null;
|
|
558
|
+
const legacyLs = laneStates[lane.tmuxSessionName] || null;
|
|
559
|
+
const ls = v2snap ? mergeV2LaneSnapshot(legacyLs, v2snap) : legacyLs;
|
|
479
560
|
const tel = telemetry[lane.tmuxSessionName] || null;
|
|
480
561
|
|
|
481
562
|
for (const task of laneTasks) {
|
|
@@ -829,6 +910,164 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
829
910
|
$mergeBody.innerHTML = html;
|
|
830
911
|
}
|
|
831
912
|
|
|
913
|
+
// ─── Render: Runtime V2 Agents (TP-107) ─────────────────────────────────────
|
|
914
|
+
|
|
915
|
+
function renderAgentsPanel(registry) {
|
|
916
|
+
const $panel = document.getElementById('agents-panel');
|
|
917
|
+
const $body = document.getElementById('agents-body');
|
|
918
|
+
if (!$panel || !$body) return;
|
|
919
|
+
|
|
920
|
+
if (!registry || !registry.agents || Object.keys(registry.agents).length === 0) {
|
|
921
|
+
$panel.style.display = 'none';
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
$panel.style.display = '';
|
|
926
|
+
const agents = Object.values(registry.agents);
|
|
927
|
+
let html = '<div class="agents-grid">';
|
|
928
|
+
|
|
929
|
+
for (const agent of agents) {
|
|
930
|
+
const isTerminal = ['exited', 'crashed', 'timed_out', 'killed'].includes(agent.status);
|
|
931
|
+
const statusClass = isTerminal ? 'agent-terminal' : 'agent-live';
|
|
932
|
+
const icon = isTerminal ? '\u{1F534}' : '\u{1F7E2}';
|
|
933
|
+
const elapsed = agent.startedAt ? Math.round((Date.now() - agent.startedAt) / 1000) : 0;
|
|
934
|
+
const elapsedStr = elapsed > 0 ? formatDuration(elapsed * 1000) : '';
|
|
935
|
+
|
|
936
|
+
html += `<div class="agent-card ${statusClass}">`;
|
|
937
|
+
html += `<div class="agent-header">${icon} <strong>${escapeHtml(agent.agentId)}</strong></div>`;
|
|
938
|
+
html += `<div class="agent-meta">`;
|
|
939
|
+
html += `<span class="agent-badge">${escapeHtml(agent.role)}</span>`;
|
|
940
|
+
if (agent.laneNumber != null) html += `<span class="agent-badge">lane ${agent.laneNumber}</span>`;
|
|
941
|
+
if (agent.taskId) html += `<span class="agent-badge">${escapeHtml(agent.taskId)}</span>`;
|
|
942
|
+
html += `<span class="agent-badge agent-status-${agent.status}">${escapeHtml(agent.status)}</span>`;
|
|
943
|
+
if (elapsedStr && !isTerminal) html += `<span class="agent-badge">${elapsedStr}</span>`;
|
|
944
|
+
html += `</div>`;
|
|
945
|
+
html += `</div>`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
html += '</div>';
|
|
949
|
+
$body.innerHTML = html;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// ─── Render: Mailbox Messages (TP-107) ──────────────────────────────────────
|
|
953
|
+
|
|
954
|
+
function renderMessagesPanel(mailbox) {
|
|
955
|
+
const $panel = document.getElementById('messages-panel');
|
|
956
|
+
const $body = document.getElementById('messages-body');
|
|
957
|
+
if (!$panel || !$body) return;
|
|
958
|
+
|
|
959
|
+
// TP-093: event-authoritative model — prefer audit events, fallback to directory scan
|
|
960
|
+
const auditEvents = mailbox?.auditEvents || [];
|
|
961
|
+
const dirMessages = mailbox?.messages || [];
|
|
962
|
+
const hasData = auditEvents.length > 0 || dirMessages.length > 0;
|
|
963
|
+
|
|
964
|
+
if (!mailbox || !hasData) {
|
|
965
|
+
$panel.style.display = 'none';
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
$panel.style.display = '';
|
|
970
|
+
let html = '<div class="messages-list">';
|
|
971
|
+
|
|
972
|
+
if (auditEvents.length > 0) {
|
|
973
|
+
// Primary: render from audit event stream (authoritative, durable)
|
|
974
|
+
for (const evt of auditEvents) {
|
|
975
|
+
html += renderMailboxAuditEvent(evt);
|
|
976
|
+
}
|
|
977
|
+
} else {
|
|
978
|
+
// Fallback: render from directory scan (legacy compatibility)
|
|
979
|
+
for (const msg of dirMessages) {
|
|
980
|
+
html += renderMailboxDirMessage(msg);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
html += '</div>';
|
|
985
|
+
$body.innerHTML = html;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** Render a single mailbox audit event (events.jsonl row). */
|
|
989
|
+
function renderMailboxAuditEvent(evt) {
|
|
990
|
+
const ts = evt.ts ? new Date(evt.ts).toLocaleTimeString() : '';
|
|
991
|
+
const type = evt.type || '';
|
|
992
|
+
|
|
993
|
+
let direction = '';
|
|
994
|
+
let statusBadge = '';
|
|
995
|
+
let typeBadge = '';
|
|
996
|
+
let preview = '';
|
|
997
|
+
|
|
998
|
+
if (type === 'message_sent') {
|
|
999
|
+
const isBroadcast = evt.broadcast;
|
|
1000
|
+
direction = isBroadcast ? '\u2192 all (broadcast)' : `\u2192 ${escapeHtml(evt.to || '')}`;
|
|
1001
|
+
statusBadge = '<span class="msg-badge msg-delivered">sent</span>';
|
|
1002
|
+
typeBadge = `<span class="msg-badge msg-type">${escapeHtml(evt.messageType || '')}</span>`;
|
|
1003
|
+
preview = evt.contentPreview || '';
|
|
1004
|
+
} else if (type === 'message_delivered') {
|
|
1005
|
+
direction = `\u2192 ${escapeHtml(evt.to || '')}`;
|
|
1006
|
+
statusBadge = evt.broadcast
|
|
1007
|
+
? '<span class="msg-badge msg-delivered">broadcast delivered</span>'
|
|
1008
|
+
: '<span class="msg-badge msg-delivered">delivered</span>';
|
|
1009
|
+
typeBadge = evt.messageType ? `<span class="msg-badge msg-type">${escapeHtml(evt.messageType)}</span>` : '';
|
|
1010
|
+
preview = evt.contentPreview || '';
|
|
1011
|
+
} else if (type === 'message_replied' || type === 'message_escalated') {
|
|
1012
|
+
direction = `\u2190 ${escapeHtml(evt.from || '')}`;
|
|
1013
|
+
statusBadge = type === 'message_escalated'
|
|
1014
|
+
? '<span class="msg-badge msg-reply">escalation</span>'
|
|
1015
|
+
: '<span class="msg-badge msg-reply">reply</span>';
|
|
1016
|
+
typeBadge = evt.messageType ? `<span class="msg-badge msg-type">${escapeHtml(evt.messageType)}</span>` : '';
|
|
1017
|
+
preview = evt.contentPreview || '';
|
|
1018
|
+
} else if (type === 'message_rate_limited') {
|
|
1019
|
+
direction = `\u2192 ${escapeHtml(evt.to || '')}`;
|
|
1020
|
+
statusBadge = '<span class="msg-badge msg-rate-limited">rate limited</span>';
|
|
1021
|
+
const waitSec = evt.retryAfterMs ? Math.ceil(evt.retryAfterMs / 1000) : '?';
|
|
1022
|
+
preview = `${evt.reason || 'Rate limited'} (retry in ${waitSec}s)`;
|
|
1023
|
+
} else {
|
|
1024
|
+
// Unknown event type — render generically
|
|
1025
|
+
direction = evt.from ? `${escapeHtml(evt.from)}` : '';
|
|
1026
|
+
preview = JSON.stringify(evt).slice(0, 120);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return `<div class="message-row">`
|
|
1030
|
+
+ `<span class="msg-time">${escapeHtml(ts)}</span>`
|
|
1031
|
+
+ `<span class="msg-direction">${direction}</span>`
|
|
1032
|
+
+ typeBadge
|
|
1033
|
+
+ statusBadge
|
|
1034
|
+
+ `<span class="msg-preview">${escapeHtml(preview)}</span>`
|
|
1035
|
+
+ `</div>`;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** Render a single directory-scanned message (legacy fallback). */
|
|
1039
|
+
function renderMailboxDirMessage(msg) {
|
|
1040
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '';
|
|
1041
|
+
// TP-093: for broadcast per-agent ack markers, show recipient identity instead of "_broadcast"
|
|
1042
|
+
let direction;
|
|
1043
|
+
if (msg.to === 'supervisor') {
|
|
1044
|
+
direction = '\u2190 supervisor';
|
|
1045
|
+
} else if (msg._isBroadcast && msg._agentDir && msg._agentDir !== '_broadcast') {
|
|
1046
|
+
direction = `\u2192 ${escapeHtml(msg._agentDir)} (broadcast)`;
|
|
1047
|
+
} else {
|
|
1048
|
+
direction = `\u2192 ${escapeHtml(msg.to || msg._agentDir || '')}`;
|
|
1049
|
+
}
|
|
1050
|
+
let statusBadge;
|
|
1051
|
+
if (msg._status === 'pending') statusBadge = '<span class="msg-badge msg-pending">pending</span>';
|
|
1052
|
+
else if (msg._status === 'delivered') statusBadge = '<span class="msg-badge msg-delivered">delivered</span>';
|
|
1053
|
+
else if (msg._status === 'reply') statusBadge = '<span class="msg-badge msg-reply">reply</span>';
|
|
1054
|
+
else if (msg._status === 'reply-acked') statusBadge = '<span class="msg-badge msg-delivered">reply (acked)</span>';
|
|
1055
|
+
else statusBadge = '';
|
|
1056
|
+
const typeBadge = `<span class="msg-badge msg-type">${escapeHtml(msg.type || '')}</span>`;
|
|
1057
|
+
const preview = (msg.content || '').slice(0, 120);
|
|
1058
|
+
const broadcastTag = msg._isBroadcast ? ' <span class="msg-badge msg-type">broadcast</span>' : '';
|
|
1059
|
+
|
|
1060
|
+
return `<div class="message-row">`
|
|
1061
|
+
+ `<span class="msg-time">${escapeHtml(ts)}</span>`
|
|
1062
|
+
+ `<span class="msg-direction">${direction}</span>`
|
|
1063
|
+
+ typeBadge
|
|
1064
|
+
+ statusBadge
|
|
1065
|
+
+ broadcastTag
|
|
1066
|
+
+ `<span class="msg-preview">${escapeHtml(preview)}</span>`
|
|
1067
|
+
+ `</div>`;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
|
|
832
1071
|
// ─── Render: Errors ─────────────────────────────────────────────────────────
|
|
833
1072
|
|
|
834
1073
|
function renderErrors(batch) {
|
|
@@ -1156,6 +1395,9 @@ function render(data) {
|
|
|
1156
1395
|
renderSupervisor(data);
|
|
1157
1396
|
renderLanesTasks(batch, tmux);
|
|
1158
1397
|
renderMergeAgents(batch, tmux);
|
|
1398
|
+
// TP-107: Runtime V2 panels
|
|
1399
|
+
renderAgentsPanel(data.runtimeRegistry);
|
|
1400
|
+
renderMessagesPanel(data.mailbox);
|
|
1159
1401
|
renderErrors(batch);
|
|
1160
1402
|
|
|
1161
1403
|
const taskCount = (batch.tasks || []).length;
|
|
@@ -1217,7 +1459,31 @@ let convRenderedLines = 0;
|
|
|
1217
1459
|
// STATUS.md diff-and-skip state
|
|
1218
1460
|
let lastStatusMdText = "";
|
|
1219
1461
|
|
|
1220
|
-
// ── Open conversation viewer
|
|
1462
|
+
// ── Open conversation viewer (TP-107: V2 events preferred, legacy fallback) ──
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Resolve a lane's tmux session name to a Runtime V2 agent ID via the registry.
|
|
1466
|
+
* Returns null if no V2 registry data is available.
|
|
1467
|
+
*/
|
|
1468
|
+
function resolveV2AgentId(sessionName) {
|
|
1469
|
+
if (!currentData || !currentData.runtimeRegistry || !currentData.runtimeRegistry.agents) return null;
|
|
1470
|
+
const agents = currentData.runtimeRegistry.agents;
|
|
1471
|
+
// Direct match on agentId
|
|
1472
|
+
if (agents[sessionName]) return sessionName;
|
|
1473
|
+
// Match by tmux session prefix + "-worker" suffix (common V2 naming)
|
|
1474
|
+
const workerKey = sessionName + '-worker';
|
|
1475
|
+
if (agents[workerKey]) return workerKey;
|
|
1476
|
+
// Search by laneNumber match from lane snapshots
|
|
1477
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
1478
|
+
if (agent.role === 'worker' && agent.laneNumber != null) {
|
|
1479
|
+
const m = sessionName.match(/lane-(\d+)/);
|
|
1480
|
+
if (m && parseInt(m[1]) === agent.laneNumber) return id;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
let viewerV2AgentId = null; // Runtime V2 agent ID for current conversation view
|
|
1221
1487
|
|
|
1222
1488
|
function viewConversation(sessionName) {
|
|
1223
1489
|
// Toggle off if already viewing this session
|
|
@@ -1233,7 +1499,12 @@ function viewConversation(sessionName) {
|
|
|
1233
1499
|
autoScrollOn = true;
|
|
1234
1500
|
convRenderedLines = 0;
|
|
1235
1501
|
|
|
1236
|
-
|
|
1502
|
+
// TP-107: Resolve V2 agent ID for events endpoint
|
|
1503
|
+
const v2AgentId = resolveV2AgentId(sessionName);
|
|
1504
|
+
viewerV2AgentId = v2AgentId;
|
|
1505
|
+
|
|
1506
|
+
const label = v2AgentId || sessionName;
|
|
1507
|
+
$terminalTitle.textContent = `Worker Conversation — ${label}`;
|
|
1237
1508
|
$autoScrollText.textContent = 'Follow feed';
|
|
1238
1509
|
$autoScrollCheckbox.checked = true;
|
|
1239
1510
|
$terminalPanel.style.display = '';
|
|
@@ -1246,9 +1517,21 @@ function viewConversation(sessionName) {
|
|
|
1246
1517
|
}
|
|
1247
1518
|
|
|
1248
1519
|
function pollConversation() {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1520
|
+
// TP-107: prefer V2 agent events when available, fallback to legacy conversation
|
|
1521
|
+
const endpoint = viewerV2AgentId
|
|
1522
|
+
? `/api/agent-events/${encodeURIComponent(viewerV2AgentId)}`
|
|
1523
|
+
: `/api/conversation/${encodeURIComponent(viewerTarget)}`;
|
|
1524
|
+
const isV2 = !!viewerV2AgentId;
|
|
1525
|
+
|
|
1526
|
+
fetch(endpoint)
|
|
1527
|
+
.then(r => isV2 ? r.json() : r.text())
|
|
1528
|
+
.then(data => {
|
|
1529
|
+
if (isV2) {
|
|
1530
|
+
renderV2AgentEvents(data);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
// Legacy: data is JSONL text
|
|
1534
|
+
const text = data;
|
|
1252
1535
|
if (!text.trim()) {
|
|
1253
1536
|
if (convRenderedLines === 0) {
|
|
1254
1537
|
$terminalBody.innerHTML = '<div class="conv-empty">No conversation events yet…</div>';
|
|
@@ -1299,6 +1582,111 @@ function pollConversation() {
|
|
|
1299
1582
|
.catch(() => {});
|
|
1300
1583
|
}
|
|
1301
1584
|
|
|
1585
|
+
// ── Runtime V2 agent event renderer (TP-107) ──────────────────────────────
|
|
1586
|
+
|
|
1587
|
+
// Stable cursor for V2 event rendering.
|
|
1588
|
+
// Uses a signature string from the last rendered event so the sliding window
|
|
1589
|
+
// (server caps at 300) doesn't stall when new tail events push older ones out.
|
|
1590
|
+
let v2LastCursor = null; // signature of last rendered event
|
|
1591
|
+
let v2FirstRender = true;
|
|
1592
|
+
|
|
1593
|
+
function v2EventSignature(evt) {
|
|
1594
|
+
return `${evt.ts || 0}:${evt.type || ''}:${JSON.stringify(evt.payload || {}).slice(0, 80)}`;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function renderV2AgentEvents(events) {
|
|
1598
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
1599
|
+
if (v2FirstRender) {
|
|
1600
|
+
$terminalBody.innerHTML = '<div class="conv-empty">No agent events yet…</div>';
|
|
1601
|
+
}
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
let container = $terminalBody.querySelector('.conv-stream');
|
|
1606
|
+
|
|
1607
|
+
if (v2FirstRender || !container) {
|
|
1608
|
+
// First load or container missing: full render
|
|
1609
|
+
$terminalBody.innerHTML = '';
|
|
1610
|
+
container = document.createElement('div');
|
|
1611
|
+
container.className = 'conv-stream';
|
|
1612
|
+
$terminalBody.appendChild(container);
|
|
1613
|
+
for (const evt of events) {
|
|
1614
|
+
const html = renderV2Event(evt);
|
|
1615
|
+
if (html) container.insertAdjacentHTML('beforeend', html);
|
|
1616
|
+
}
|
|
1617
|
+
v2LastCursor = v2EventSignature(events[events.length - 1]);
|
|
1618
|
+
v2FirstRender = false;
|
|
1619
|
+
} else {
|
|
1620
|
+
// Incremental: find first unseen event after cursor
|
|
1621
|
+
let cursorIdx = -1;
|
|
1622
|
+
if (v2LastCursor) {
|
|
1623
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
1624
|
+
if (v2EventSignature(events[i]) === v2LastCursor) {
|
|
1625
|
+
cursorIdx = i;
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (cursorIdx === -1) {
|
|
1632
|
+
// Cursor not found (rotation/restart): full re-render
|
|
1633
|
+
container.innerHTML = '';
|
|
1634
|
+
for (const evt of events) {
|
|
1635
|
+
const html = renderV2Event(evt);
|
|
1636
|
+
if (html) container.insertAdjacentHTML('beforeend', html);
|
|
1637
|
+
}
|
|
1638
|
+
} else if (cursorIdx < events.length - 1) {
|
|
1639
|
+
// Append only new events after cursor
|
|
1640
|
+
const newEvents = events.slice(cursorIdx + 1);
|
|
1641
|
+
for (const evt of newEvents) {
|
|
1642
|
+
const html = renderV2Event(evt);
|
|
1643
|
+
if (html) container.insertAdjacentHTML('beforeend', html);
|
|
1644
|
+
}
|
|
1645
|
+
} else {
|
|
1646
|
+
// No new events
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
v2LastCursor = v2EventSignature(events[events.length - 1]);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (autoScrollOn) {
|
|
1654
|
+
isProgrammaticScroll = true;
|
|
1655
|
+
$terminalBody.scrollTop = $terminalBody.scrollHeight;
|
|
1656
|
+
requestAnimationFrame(() => { isProgrammaticScroll = false; });
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function renderV2Event(evt) {
|
|
1661
|
+
const ts = evt.ts ? new Date(evt.ts).toLocaleTimeString() : '';
|
|
1662
|
+
const type = evt.type || 'unknown';
|
|
1663
|
+
|
|
1664
|
+
switch (type) {
|
|
1665
|
+
case 'assistant_message':
|
|
1666
|
+
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>`;
|
|
1667
|
+
case 'prompt_sent':
|
|
1668
|
+
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>`;
|
|
1669
|
+
case 'tool_call':
|
|
1670
|
+
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>`;
|
|
1671
|
+
case 'tool_result':
|
|
1672
|
+
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>`;
|
|
1673
|
+
case 'agent_started':
|
|
1674
|
+
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>`;
|
|
1675
|
+
case 'agent_exited':
|
|
1676
|
+
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>`;
|
|
1677
|
+
case 'agent_crashed':
|
|
1678
|
+
case 'agent_killed':
|
|
1679
|
+
case 'agent_timeout':
|
|
1680
|
+
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>`;
|
|
1681
|
+
case 'message_delivered':
|
|
1682
|
+
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>`;
|
|
1683
|
+
case 'context_pressure':
|
|
1684
|
+
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>`;
|
|
1685
|
+
default:
|
|
1686
|
+
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>`;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1302
1690
|
// ── Open STATUS.md viewer ───────────────────────────────────────────────────
|
|
1303
1691
|
|
|
1304
1692
|
function viewStatusMd(taskId) {
|
|
@@ -1535,8 +1923,11 @@ function closeViewer() {
|
|
|
1535
1923
|
}
|
|
1536
1924
|
viewerMode = null;
|
|
1537
1925
|
viewerTarget = null;
|
|
1926
|
+
viewerV2AgentId = null;
|
|
1538
1927
|
autoScrollOn = false;
|
|
1539
1928
|
convRenderedLines = 0;
|
|
1929
|
+
v2LastCursor = null;
|
|
1930
|
+
v2FirstRender = true;
|
|
1540
1931
|
lastStatusMdText = '';
|
|
1541
1932
|
$terminalPanel.style.display = 'none';
|
|
1542
1933
|
$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
|
+
}
|