claude-code-kanban 2.1.0-rc.1 → 2.1.0-rc.3
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/hooks/agent-spy.sh +33 -2
- package/lib/parsers.js +113 -5
- package/package.json +1 -1
- package/public/app.js +213 -25
- package/public/style.css +77 -0
- package/server.js +175 -141
package/hooks/agent-spy.sh
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
INPUT=$(cat)
|
|
6
6
|
|
|
7
|
-
# Single jq call to extract all routing fields
|
|
7
|
+
# Single jq call to extract all routing fields
|
|
8
8
|
eval "$(echo "$INPUT" | jq -r '
|
|
9
9
|
@sh "SESSION_ID=\(.session_id // "")",
|
|
10
10
|
@sh "AGENT_ID=\(.agent_id // "")",
|
|
11
11
|
@sh "EVENT=\(.hook_event_name // "")",
|
|
12
12
|
@sh "TOOL_NAME=\(.tool_name // "")",
|
|
13
|
-
@sh "AGENT_TYPE_RAW=\(.agent_type // "")"
|
|
13
|
+
@sh "AGENT_TYPE_RAW=\(.agent_type // "")",
|
|
14
|
+
@sh "TEAMMATE_NAME=\(.teammate_name // "")"
|
|
14
15
|
')"
|
|
15
16
|
|
|
16
17
|
[ -z "$SESSION_ID" ] && exit 0
|
|
@@ -42,6 +43,26 @@ if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TO
|
|
|
42
43
|
exit 0
|
|
43
44
|
fi
|
|
44
45
|
|
|
46
|
+
# TeammateIdle has no agent_id — resolve via name→id mapping file
|
|
47
|
+
if [ "$EVENT" = "TeammateIdle" ] && [ -z "$AGENT_ID" ] && [ -n "$TEAMMATE_NAME" ]; then
|
|
48
|
+
DIR="$HOME/.claude/agent-activity/$SESSION_ID"
|
|
49
|
+
MAP_FILE="$DIR/_name-${TEAMMATE_NAME}.id"
|
|
50
|
+
[ ! -f "$MAP_FILE" ] && exit 0
|
|
51
|
+
AGENT_ID=$(cat "$MAP_FILE")
|
|
52
|
+
[ -z "$AGENT_ID" ] && exit 0
|
|
53
|
+
FILE="$DIR/$AGENT_ID.json"
|
|
54
|
+
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
55
|
+
STARTED_AT="$TS"
|
|
56
|
+
if [ -f "$FILE" ]; then
|
|
57
|
+
PREV_START=$(jq -r '.startedAt // ""' "$FILE" 2>/dev/null)
|
|
58
|
+
[ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
|
|
59
|
+
fi
|
|
60
|
+
cat > "$FILE" <<EOF
|
|
61
|
+
{"agentId":"$AGENT_ID","type":"$TEAMMATE_NAME","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
|
|
62
|
+
EOF
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
45
66
|
[ -z "$AGENT_ID" ] && exit 0
|
|
46
67
|
|
|
47
68
|
DIR="$HOME/.claude/agent-activity/$SESSION_ID"
|
|
@@ -64,6 +85,16 @@ if [ "$EVENT" = "SubagentStart" ]; then
|
|
|
64
85
|
cat > "$FILE" <<EOF
|
|
65
86
|
{"agentId":"$AGENT_ID","type":"$AGENT_TYPE_RAW","status":"active","startedAt":"$TS","updatedAt":"$TS"}
|
|
66
87
|
EOF
|
|
88
|
+
# Write name→id mapping for TeammateIdle resolution
|
|
89
|
+
# Remove previous incarnation's agent file to avoid duplicates
|
|
90
|
+
if [ -n "$AGENT_TYPE_RAW" ]; then
|
|
91
|
+
MAP_FILE="$DIR/_name-${AGENT_TYPE_RAW}.id"
|
|
92
|
+
if [ -f "$MAP_FILE" ]; then
|
|
93
|
+
OLD_ID=$(cat "$MAP_FILE")
|
|
94
|
+
[ -n "$OLD_ID" ] && [ "$OLD_ID" != "$AGENT_ID" ] && rm -f "$DIR/$OLD_ID.json"
|
|
95
|
+
fi
|
|
96
|
+
echo -n "$AGENT_ID" > "$MAP_FILE"
|
|
97
|
+
fi
|
|
67
98
|
|
|
68
99
|
elif [ "$EVENT" = "SubagentStop" ]; then
|
|
69
100
|
AGENT_TYPE="$AGENT_TYPE_RAW"
|
package/lib/parsers.js
CHANGED
|
@@ -55,7 +55,8 @@ function parseTeamConfig(raw) {
|
|
|
55
55
|
name: m.name,
|
|
56
56
|
agentType: m.agentType || null,
|
|
57
57
|
model: m.model || null,
|
|
58
|
-
cwd: m.cwd || null
|
|
58
|
+
cwd: m.cwd || null,
|
|
59
|
+
color: m.color || null
|
|
59
60
|
})),
|
|
60
61
|
raw: config
|
|
61
62
|
};
|
|
@@ -287,6 +288,20 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
287
288
|
return text;
|
|
288
289
|
}).join('\n\n');
|
|
289
290
|
}
|
|
291
|
+
else if (inp.to) {
|
|
292
|
+
const proto = inp.message && typeof inp.message === 'object' ? inp.message : null;
|
|
293
|
+
if (proto?.type === 'shutdown_request') {
|
|
294
|
+
detail = '→ ' + inp.to + ': shutdown request' + (proto.reason ? ' (' + proto.reason + ')' : '');
|
|
295
|
+
} else if (proto?.type === 'shutdown_response') {
|
|
296
|
+
detail = '→ ' + inp.to + ': ' + (proto.approve ? 'shutdown approved' : 'shutdown rejected');
|
|
297
|
+
} else if (proto?.type === 'plan_approval_response') {
|
|
298
|
+
detail = '→ ' + inp.to + ': ' + (proto.approve ? 'plan approved' : 'plan rejected');
|
|
299
|
+
} else {
|
|
300
|
+
detail = '→ ' + inp.to + (inp.summary ? ': ' + inp.summary : '');
|
|
301
|
+
}
|
|
302
|
+
if (detail.length > 80) detail = detail.slice(0, 80) + '...';
|
|
303
|
+
fullDetail = typeof inp.message === 'string' ? inp.message : JSON.stringify(inp.message);
|
|
304
|
+
}
|
|
290
305
|
else if (inp.plan) {
|
|
291
306
|
const titleMatch = inp.plan.match(/^#\s+(.+)/m);
|
|
292
307
|
detail = titleMatch ? titleMatch[1] : 'Plan';
|
|
@@ -336,9 +351,9 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
336
351
|
} else if (block.name === 'ToolSearch') {
|
|
337
352
|
if (inp.max_results) params.max_results = inp.max_results;
|
|
338
353
|
} else if (block.name === 'TaskCreate') {
|
|
339
|
-
if (inp.
|
|
354
|
+
if (inp.subject) params.subject = inp.subject;
|
|
340
355
|
} else if (block.name === 'TaskUpdate') {
|
|
341
|
-
if (inp.taskId) params.taskId = inp.taskId;
|
|
356
|
+
if (inp.taskId) params.taskId = '#' + inp.taskId;
|
|
342
357
|
if (inp.status) params.status = inp.status;
|
|
343
358
|
} else if (block.name === 'NotebookEdit') {
|
|
344
359
|
if (inp.command) params.command = inp.command;
|
|
@@ -351,6 +366,12 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
351
366
|
} else if (block.name === 'ExitPlanMode') {
|
|
352
367
|
if (inp.plan) params.plan = inp.plan;
|
|
353
368
|
if (inp.planFilePath) params.planFilePath = inp.planFilePath;
|
|
369
|
+
} else if (block.name === 'SendMessage') {
|
|
370
|
+
if (inp.to) params.to = inp.to;
|
|
371
|
+
if (inp.summary) params.summary = inp.summary;
|
|
372
|
+
if (inp.message && typeof inp.message === 'object') {
|
|
373
|
+
params.protocol = inp.message;
|
|
374
|
+
}
|
|
354
375
|
}
|
|
355
376
|
}
|
|
356
377
|
const msg = {
|
|
@@ -375,6 +396,51 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
375
396
|
} else if (obj.type === 'user' && obj.message?.role === 'user' && !obj.isMeta) {
|
|
376
397
|
if (typeof obj.message.content === 'string') {
|
|
377
398
|
const t = obj.message.content;
|
|
399
|
+
const tmMatch = t.match(/<teammate-message\s+([^>]*)>([\s\S]*?)<\/teammate-message>/);
|
|
400
|
+
if (tmMatch) {
|
|
401
|
+
const attrs = tmMatch[1];
|
|
402
|
+
const body = tmMatch[2].trim();
|
|
403
|
+
const getAttr = (name) => (attrs.match(new RegExp(name + '="([^"]*)"')) || [])[1] || null;
|
|
404
|
+
const tid = getAttr('teammate_id');
|
|
405
|
+
const color = getAttr('color');
|
|
406
|
+
const summary = getAttr('summary');
|
|
407
|
+
let protocol = null;
|
|
408
|
+
try {
|
|
409
|
+
const j = JSON.parse(body);
|
|
410
|
+
if (j.type) protocol = j;
|
|
411
|
+
} catch (_) {}
|
|
412
|
+
const isIdle = protocol?.type === 'idle_notification';
|
|
413
|
+
const isProtocol = !!protocol;
|
|
414
|
+
let protocolLabel = null;
|
|
415
|
+
if (protocol) {
|
|
416
|
+
switch (protocol.type) {
|
|
417
|
+
case 'idle_notification': protocolLabel = protocol.idleReason || 'idle'; break;
|
|
418
|
+
case 'task_assignment': protocolLabel = `assigned #${protocol.taskId}: ${protocol.subject || ''}`; break;
|
|
419
|
+
case 'shutdown_request': protocolLabel = `shutdown: ${protocol.reason || 'requested'}`; break;
|
|
420
|
+
case 'shutdown_response': protocolLabel = protocol.approve ? 'shutdown approved' : `shutdown rejected: ${protocol.reason || ''}`; break;
|
|
421
|
+
case 'plan_approval_request': protocolLabel = 'plan approval requested'; break;
|
|
422
|
+
case 'plan_approval_response': protocolLabel = protocol.approve ? 'plan approved' : `plan rejected: ${protocol.feedback || ''}`; break;
|
|
423
|
+
case 'teammate_terminated': protocolLabel = protocol.message || 'shut down'; break;
|
|
424
|
+
default: protocolLabel = protocol.type.replace(/_/g, ' '); break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const truncated = !isProtocol && body.length > 500;
|
|
428
|
+
messages.push({
|
|
429
|
+
type: 'teammate',
|
|
430
|
+
teammateId: tid,
|
|
431
|
+
color,
|
|
432
|
+
summary,
|
|
433
|
+
isIdle,
|
|
434
|
+
isProtocol,
|
|
435
|
+
protocolType: protocol?.type || null,
|
|
436
|
+
protocolLabel,
|
|
437
|
+
protocolData: protocol || null,
|
|
438
|
+
text: isProtocol ? null : (truncated ? body.slice(0, 500) + '...' : body),
|
|
439
|
+
fullText: isProtocol ? null : (truncated ? body : null),
|
|
440
|
+
timestamp: obj.timestamp
|
|
441
|
+
});
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
378
444
|
const sysLabel = getSystemMessageLabel(t);
|
|
379
445
|
if (sysLabel === '__skip__') continue;
|
|
380
446
|
const uTruncated = t.length > 500;
|
|
@@ -440,7 +506,9 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
440
506
|
const parentRe = /"parentToolUseID":"([^"]+)"/;
|
|
441
507
|
const promptRe = /"prompt":"((?:[^"\\]|\\.)*)"/;
|
|
442
508
|
const bgToolIdRe = /"tool_use_id":"([^"]+)"/;
|
|
443
|
-
const bgAgentIdRe = /agentId: ([a-zA-Z0-9_
|
|
509
|
+
const bgAgentIdRe = /agentId: ([a-zA-Z0-9_@-]+)/;
|
|
510
|
+
const tmToolIdRe = /"tool_use_id":"([^"]+)"/;
|
|
511
|
+
const tmAgentIdRe = /agent_id: ([a-zA-Z0-9_@-]+)/;
|
|
444
512
|
for (const line of content.split('\n')) {
|
|
445
513
|
if (line.includes('"agent_progress"')) {
|
|
446
514
|
const agentMatch = re.exec(line);
|
|
@@ -462,6 +530,12 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
462
530
|
if (toolIdMatch && bgAgentMatch && !map[toolIdMatch[1]]) {
|
|
463
531
|
map[toolIdMatch[1]] = { agentId: bgAgentMatch[1], prompt: null };
|
|
464
532
|
}
|
|
533
|
+
} else if (line.includes('"teammate_spawned"')) {
|
|
534
|
+
const toolIdMatch = tmToolIdRe.exec(line);
|
|
535
|
+
const agentMatch = tmAgentIdRe.exec(line);
|
|
536
|
+
if (toolIdMatch && agentMatch && !map[toolIdMatch[1]]) {
|
|
537
|
+
map[toolIdMatch[1]] = { agentId: agentMatch[1], prompt: null };
|
|
538
|
+
}
|
|
465
539
|
}
|
|
466
540
|
}
|
|
467
541
|
} catch (_) {}
|
|
@@ -506,6 +580,39 @@ function readCompactSummaries(jsonlPath) {
|
|
|
506
580
|
return results.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
507
581
|
}
|
|
508
582
|
|
|
583
|
+
function findTerminatedTeammates(jsonlPath) {
|
|
584
|
+
const terminated = new Map();
|
|
585
|
+
try {
|
|
586
|
+
const content = readFileSync(jsonlPath, 'utf8');
|
|
587
|
+
for (const line of content.split('\n')) {
|
|
588
|
+
if (!line.includes('teammate-message')) continue;
|
|
589
|
+
if (!line.includes('teammate_terminated') && !line.includes('shutdown_response')) continue;
|
|
590
|
+
try {
|
|
591
|
+
const obj = JSON.parse(line);
|
|
592
|
+
if (obj.type !== 'user') continue;
|
|
593
|
+
const text = typeof obj.message?.content === 'string' ? obj.message.content : null;
|
|
594
|
+
if (!text) continue;
|
|
595
|
+
const ts = obj.timestamp || null;
|
|
596
|
+
for (const tmMatch of text.matchAll(/<teammate-message\s+[^>]*teammate_id="([^"]+)"[^>]*>([\s\S]*?)<\/teammate-message>/g)) {
|
|
597
|
+
try {
|
|
598
|
+
const tid = tmMatch[1];
|
|
599
|
+
const body = tmMatch[2].trim();
|
|
600
|
+
const protocol = JSON.parse(body);
|
|
601
|
+
if (protocol.type === 'teammate_terminated') {
|
|
602
|
+
const name = protocol.from || (protocol.message?.match(/^(\S+)\s/)?.[1]) || tid;
|
|
603
|
+
if (name !== 'system') terminated.set(name, ts);
|
|
604
|
+
} else if (protocol.type === 'shutdown_response' && protocol.approve) {
|
|
605
|
+
const name = protocol.from || tid;
|
|
606
|
+
if (name !== 'system') terminated.set(name, ts);
|
|
607
|
+
}
|
|
608
|
+
} catch (_) {}
|
|
609
|
+
}
|
|
610
|
+
} catch (_) {}
|
|
611
|
+
}
|
|
612
|
+
} catch (_) {}
|
|
613
|
+
return terminated;
|
|
614
|
+
}
|
|
615
|
+
|
|
509
616
|
module.exports = {
|
|
510
617
|
parseTask,
|
|
511
618
|
parseAgent,
|
|
@@ -516,5 +623,6 @@ module.exports = {
|
|
|
516
623
|
readSessionInfoFromJsonl,
|
|
517
624
|
readRecentMessages,
|
|
518
625
|
buildAgentProgressMap,
|
|
519
|
-
readCompactSummaries
|
|
626
|
+
readCompactSummaries,
|
|
627
|
+
findTerminatedTeammates
|
|
520
628
|
};
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -109,10 +109,10 @@ async function fetchSessions() {
|
|
|
109
109
|
console.log('[fetchSessions] Starting...');
|
|
110
110
|
try {
|
|
111
111
|
const pinnedParam = pinnedSessionIds.size > 0 ? `&pinned=${[...pinnedSessionIds].join(',')}` : '';
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const [newSessions, newTasks] = await Promise.all([
|
|
113
|
+
fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}`).then((r) => r.json()),
|
|
114
|
+
fetch('/api/tasks/all').then((r) => r.json()),
|
|
115
|
+
]);
|
|
116
116
|
|
|
117
117
|
const sessionsHash = JSON.stringify(newSessions);
|
|
118
118
|
const tasksHash = JSON.stringify(newTasks);
|
|
@@ -436,10 +436,12 @@ async function fetchTasks(sessionId) {
|
|
|
436
436
|
currentPins = loadPins(sessionId);
|
|
437
437
|
ownerFilter = '';
|
|
438
438
|
lastMessagesHash = '';
|
|
439
|
+
for (const k of Object.keys(ownerColorCache)) delete ownerColorCache[k];
|
|
440
|
+
for (const k of Object.keys(teamColorMap)) delete teamColorMap[k];
|
|
439
441
|
sessionJustSelected = true;
|
|
440
442
|
updateUrl();
|
|
441
443
|
renderSession();
|
|
442
|
-
fetchAgents(sessionId);
|
|
444
|
+
await fetchAgents(sessionId);
|
|
443
445
|
if (!agentLogMode) fetchMessages(sessionId);
|
|
444
446
|
} catch (error) {
|
|
445
447
|
console.error('Failed to fetch tasks:', error);
|
|
@@ -472,7 +474,10 @@ async function fetchAgents(sessionId) {
|
|
|
472
474
|
if (hash === lastAgentsHash) return;
|
|
473
475
|
lastAgentsHash = hash;
|
|
474
476
|
currentAgents = agents;
|
|
477
|
+
updateTeamColors(agents, data.teamColors);
|
|
478
|
+
for (const k of Object.keys(ownerColorCache)) delete ownerColorCache[k];
|
|
475
479
|
renderAgentFooter();
|
|
480
|
+
if (currentSessionId === sessionId) renderKanban();
|
|
476
481
|
} catch (e) {
|
|
477
482
|
console.error('[fetchAgents]', e);
|
|
478
483
|
}
|
|
@@ -494,8 +499,12 @@ function toggleMessagePanel() {
|
|
|
494
499
|
}
|
|
495
500
|
|
|
496
501
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
497
|
-
function viewAgentLog(agentId) {
|
|
498
|
-
|
|
502
|
+
async function viewAgentLog(agentId) {
|
|
503
|
+
let agent = currentAgents.find((a) => a.agentId === agentId);
|
|
504
|
+
if (!agent && currentSessionId) {
|
|
505
|
+
await fetchAgents(currentSessionId);
|
|
506
|
+
agent = currentAgents.find((a) => a.agentId === agentId);
|
|
507
|
+
}
|
|
499
508
|
if (!agent) return;
|
|
500
509
|
const shortId = agentId.length > 8 ? agentId.slice(0, 8) : agentId;
|
|
501
510
|
agentLogMode = { agentId, sessionId: currentSessionId, agentType: agent.type || 'unknown' };
|
|
@@ -704,15 +713,53 @@ function renderMessages(messages) {
|
|
|
704
713
|
m.tool === 'Agent' && m.agentId
|
|
705
714
|
? ` <span class="msg-agent-link" title="View agent" onclick="event.stopPropagation();showAgentModal('${escapeHtml(m.agentId)}')">⇗</span>`
|
|
706
715
|
: '';
|
|
707
|
-
|
|
708
|
-
|
|
716
|
+
let agentLogBtn = '';
|
|
717
|
+
if (m.tool === 'Agent' && m.agentId) {
|
|
718
|
+
agentLogBtn = agentLogButton(m.agentId);
|
|
719
|
+
} else if (m.tool === 'SendMessage' && m.params?.to) {
|
|
720
|
+
const recipient = currentAgents.find((a) => (a.type || a.name) === m.params.to);
|
|
721
|
+
if (recipient) agentLogBtn = agentLogButton(recipient.agentId);
|
|
722
|
+
}
|
|
723
|
+
const recipientColor =
|
|
724
|
+
m.tool === 'SendMessage' && m.params?.to ? resolveNamedColor(teamColorMap[m.params.to]) : null;
|
|
725
|
+
const borderStyle = recipientColor ? `border-left:3px solid ${recipientColor.color};` : '';
|
|
726
|
+
const combinedStyle = `style="${borderStyle}cursor:pointer"`;
|
|
727
|
+
const itemClickAttr =
|
|
709
728
|
m.tool === 'Agent' && m.agentId
|
|
710
|
-
? `onclick="showAgentModal('${escapeHtml(m.agentId)}')"
|
|
711
|
-
:
|
|
712
|
-
return `<div class="msg-item msg-tool" ${
|
|
729
|
+
? `onclick="showAgentModal('${escapeHtml(m.agentId)}')" ${combinedStyle}`
|
|
730
|
+
: `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" ${combinedStyle}`;
|
|
731
|
+
return `<div class="msg-item msg-tool" ${itemClickAttr}>
|
|
713
732
|
${MSG_ICON_TOOL}
|
|
714
733
|
<div class="msg-body"><div class="msg-text">${escapeHtml(m.tool)}${toolDetail}${agentLink}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${agentLogBtn}${pinBtn}
|
|
715
734
|
</div>`;
|
|
735
|
+
} else if (m.type === 'teammate') {
|
|
736
|
+
if (m.teammateId && m.color && !teamColorMap[m.teammateId]) teamColorMap[m.teammateId] = m.color;
|
|
737
|
+
const tmColor = m.color ? resolveNamedColor(m.color)?.color || m.color : '';
|
|
738
|
+
const nameSpan = `<span class="teammate-name" style="${tmColor ? `color:${escapeHtml(tmColor)}` : ''}">${escapeHtml(m.teammateId || 'teammate')}</span>`;
|
|
739
|
+
let tmLookupName = m.teammateId;
|
|
740
|
+
if (m.teammateId === 'system' && m.protocolType === 'teammate_terminated' && m.protocolData?.message) {
|
|
741
|
+
const shutMatch = m.protocolData.message.match(/^(.+?) has shut down/);
|
|
742
|
+
if (shutMatch) tmLookupName = shutMatch[1];
|
|
743
|
+
}
|
|
744
|
+
const tmAgent = tmLookupName ? currentAgents.find((a) => (a.type || a.name) === tmLookupName) : null;
|
|
745
|
+
const tmLogBtn = tmAgent ? agentLogButton(tmAgent.agentId) : '';
|
|
746
|
+
if (m.isIdle) {
|
|
747
|
+
return `<div class="msg-item msg-teammate msg-idle" ${clickable}>
|
|
748
|
+
${MSG_ICON_IDLE}
|
|
749
|
+
<div class="msg-body"><div class="msg-text">${nameSpan} <span class="idle-label">${escapeHtml(m.protocolLabel || 'idle')}</span></div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}
|
|
750
|
+
</div>`;
|
|
751
|
+
}
|
|
752
|
+
if (m.isProtocol) {
|
|
753
|
+
return `<div class="msg-item msg-teammate msg-protocol" ${clickable}>
|
|
754
|
+
${MSG_ICON_TEAMMATE}
|
|
755
|
+
<div class="msg-body"><div class="msg-text">${nameSpan} <span class="protocol-label">${escapeHtml(m.protocolLabel || m.protocolType)}</span></div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}
|
|
756
|
+
</div>`;
|
|
757
|
+
}
|
|
758
|
+
const summaryText = m.summary ? escapeHtml(m.summary) : escapeHtml((m.text || '').slice(0, 80));
|
|
759
|
+
return `<div class="msg-item msg-teammate" ${clickable}>
|
|
760
|
+
${MSG_ICON_TEAMMATE}
|
|
761
|
+
<div class="msg-body"><div class="msg-text">${nameSpan} ${summaryText}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}${pinBtn}
|
|
762
|
+
</div>`;
|
|
716
763
|
}
|
|
717
764
|
return '';
|
|
718
765
|
})
|
|
@@ -736,6 +783,10 @@ const MSG_ICON_TOOL =
|
|
|
736
783
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>';
|
|
737
784
|
const MSG_ICON_SYSTEM =
|
|
738
785
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
|
|
786
|
+
const MSG_ICON_TEAMMATE =
|
|
787
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
|
|
788
|
+
const MSG_ICON_IDLE =
|
|
789
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6"/></svg>';
|
|
739
790
|
const AGENT_LOG_ICON =
|
|
740
791
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
|
741
792
|
function agentLogButton(agentId) {
|
|
@@ -963,12 +1014,25 @@ function showMsgDetail(idx) {
|
|
|
963
1014
|
} else {
|
|
964
1015
|
agentBtn.style.display = 'none';
|
|
965
1016
|
}
|
|
966
|
-
const
|
|
967
|
-
const
|
|
1017
|
+
const sendProto = m.tool === 'SendMessage' && m.params?.protocol;
|
|
1018
|
+
const toolParamsHtml = renderToolParamsHtml(
|
|
1019
|
+
sendProto ? Object.fromEntries(Object.entries(m.params).filter(([k]) => k !== 'protocol')) : m.params,
|
|
1020
|
+
);
|
|
1021
|
+
const hideResult = m.tool === 'SendMessage' || TASK_TOOLS.has(m.tool);
|
|
1022
|
+
const taskResultHtml = TASK_TOOLS.has(m.tool) ? renderTaskResult(m.toolResult) : '';
|
|
1023
|
+
const toolResultHtml = hideResult
|
|
1024
|
+
? ''
|
|
1025
|
+
: renderToolResultHtml(m.toolResult, m.toolResultTruncated, m.toolResultFull);
|
|
968
1026
|
const hasAgentTabs = m.tool === 'Agent' && m.agentId && (m.agentLastMessage || m.agentPrompt);
|
|
969
1027
|
let mainHtml;
|
|
970
|
-
if (
|
|
1028
|
+
if (sendProto) {
|
|
1029
|
+
mainHtml = descHtml + renderProtocolDetail(m.params.protocol);
|
|
1030
|
+
} else if (m.tool === 'SendMessage' && fullText) {
|
|
1031
|
+
mainHtml = `${descHtml}<div class="markdown-body">${renderMarkdown(fullText)}</div>`;
|
|
1032
|
+
} else if (hasAgentTabs) {
|
|
971
1033
|
mainHtml = descHtml || '';
|
|
1034
|
+
} else if (taskResultHtml) {
|
|
1035
|
+
mainHtml = '';
|
|
972
1036
|
} else if (fullText) {
|
|
973
1037
|
const detailEscaped = escapeHtml(fullText);
|
|
974
1038
|
const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
|
|
@@ -976,7 +1040,18 @@ function showMsgDetail(idx) {
|
|
|
976
1040
|
} else {
|
|
977
1041
|
mainHtml = '<em>No details</em>';
|
|
978
1042
|
}
|
|
979
|
-
body.innerHTML = mainHtml + toolParamsHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
|
|
1043
|
+
body.innerHTML = mainHtml + toolParamsHtml + taskResultHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
|
|
1044
|
+
} else if (m.type === 'teammate') {
|
|
1045
|
+
document.getElementById('msg-detail-title').textContent = m.teammateId || 'Teammate';
|
|
1046
|
+
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
1047
|
+
if (m.isProtocol) {
|
|
1048
|
+
body.innerHTML = m.protocolData
|
|
1049
|
+
? renderProtocolDetail(m.protocolData)
|
|
1050
|
+
: `<div class="teammate-idle-detail"><span class="protocol-label">${escapeHtml(m.protocolLabel || m.protocolType)}</span></div>`;
|
|
1051
|
+
} else {
|
|
1052
|
+
const text = stripAnsi(m.fullText || m.text || '');
|
|
1053
|
+
body.innerHTML = renderMarkdown(text);
|
|
1054
|
+
}
|
|
980
1055
|
} else {
|
|
981
1056
|
const text = stripAnsi(m.fullText || m.text);
|
|
982
1057
|
document.getElementById('msg-detail-title').textContent =
|
|
@@ -1067,6 +1142,69 @@ async function copyWithFeedback(text, btn) {
|
|
|
1067
1142
|
//#endregion
|
|
1068
1143
|
|
|
1069
1144
|
//#region TOOL_RENDERING
|
|
1145
|
+
const PROTOCOL_SKIP_KEYS = new Set(['type', 'from', 'timestamp', 'paneId', 'backendType']);
|
|
1146
|
+
function renderProtocolDetail(data) {
|
|
1147
|
+
if (!data || typeof data !== 'object') return '';
|
|
1148
|
+
const typeBadge = data.type
|
|
1149
|
+
? `<span class="protocol-type-badge">${escapeHtml(data.type.replace(/_/g, ' '))}</span>`
|
|
1150
|
+
: '';
|
|
1151
|
+
const fields = Object.entries(data)
|
|
1152
|
+
.filter(([k]) => !PROTOCOL_SKIP_KEYS.has(k))
|
|
1153
|
+
.map(([k, v]) => {
|
|
1154
|
+
const label = escapeHtml(
|
|
1155
|
+
k
|
|
1156
|
+
.replace(/([A-Z])/g, ' $1')
|
|
1157
|
+
.replace(/_/g, ' ')
|
|
1158
|
+
.trim()
|
|
1159
|
+
.toLowerCase(),
|
|
1160
|
+
);
|
|
1161
|
+
let val;
|
|
1162
|
+
if (typeof v === 'boolean') {
|
|
1163
|
+
val = `<span class="protocol-bool protocol-bool-${v}">${v ? 'yes' : 'no'}</span>`;
|
|
1164
|
+
} else if (v == null) {
|
|
1165
|
+
val = `<span style="color:var(--text-muted)">null</span>`;
|
|
1166
|
+
} else {
|
|
1167
|
+
val = escapeHtml(String(v));
|
|
1168
|
+
}
|
|
1169
|
+
return `<div class="protocol-field"><span class="protocol-field-key">${label}</span>${val}</div>`;
|
|
1170
|
+
});
|
|
1171
|
+
return `<div class="protocol-detail">${typeBadge}${fields.length ? `<div class="protocol-fields">${fields.join('')}</div>` : ''}</div>`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const TASK_TOOLS = new Set(['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList']);
|
|
1175
|
+
function renderTaskResult(toolResult) {
|
|
1176
|
+
if (!toolResult) return '';
|
|
1177
|
+
const lines = toolResult.trim().split('\n');
|
|
1178
|
+
const fields = [];
|
|
1179
|
+
for (const line of lines) {
|
|
1180
|
+
const m = line.match(/^([A-Za-z #]+):\s*(.+)$/);
|
|
1181
|
+
if (m) fields.push([m[1].trim(), m[2].trim()]);
|
|
1182
|
+
}
|
|
1183
|
+
if (!fields.length) return '';
|
|
1184
|
+
const title = fields.find(([k]) => /^Task/.test(k));
|
|
1185
|
+
const status = fields.find(([k]) => k === 'Status');
|
|
1186
|
+
const rest = fields.filter(([k]) => !/^Task/.test(k) && k !== 'Status');
|
|
1187
|
+
const statusColors = {
|
|
1188
|
+
pending: 'var(--text-muted)',
|
|
1189
|
+
in_progress: 'var(--info)',
|
|
1190
|
+
completed: 'var(--success)',
|
|
1191
|
+
deleted: 'var(--danger)',
|
|
1192
|
+
};
|
|
1193
|
+
const sc = status ? statusColors[status[1]] || 'var(--text-muted)' : '';
|
|
1194
|
+
let html = '<div class="protocol-detail">';
|
|
1195
|
+
if (title) html += `<span class="protocol-type-badge">${escapeHtml(title[1])}</span>`;
|
|
1196
|
+
if (status)
|
|
1197
|
+
html += `<span style="display:inline-block;font-size:10px;font-weight:600;color:${sc};text-transform:uppercase;margin-bottom:6px">${escapeHtml(status[1])}</span>`;
|
|
1198
|
+
if (rest.length) {
|
|
1199
|
+
html += '<div class="protocol-fields">';
|
|
1200
|
+
for (const [k, v] of rest) {
|
|
1201
|
+
html += `<div class="protocol-field"><span class="protocol-field-key">${escapeHtml(k.toLowerCase())}</span>${escapeHtml(v)}</div>`;
|
|
1202
|
+
}
|
|
1203
|
+
html += '</div>';
|
|
1204
|
+
}
|
|
1205
|
+
return `${html}</div>`;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1070
1208
|
function renderToolParamsHtml(params) {
|
|
1071
1209
|
if (!params) return '';
|
|
1072
1210
|
const BLOCK_KEYS = new Set(['old_string', 'new_string', 'content', 'plan']);
|
|
@@ -1272,9 +1410,14 @@ function renderAgentFooter() {
|
|
|
1272
1410
|
if (overlapped || reSpawn || isActive) filtered.push(group[i]);
|
|
1273
1411
|
}
|
|
1274
1412
|
}
|
|
1275
|
-
// Sort
|
|
1413
|
+
// Sort: active/idle first, then by updatedAt desc
|
|
1414
|
+
const statusOrder = { active: 0, idle: 1, stopped: 2 };
|
|
1276
1415
|
const visible = filtered
|
|
1277
|
-
.sort(
|
|
1416
|
+
.sort(
|
|
1417
|
+
(a, b) =>
|
|
1418
|
+
(statusOrder[a.status] ?? 2) - (statusOrder[b.status] ?? 2) ||
|
|
1419
|
+
new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0),
|
|
1420
|
+
)
|
|
1278
1421
|
.slice(0, AGENT_LOG_MAX);
|
|
1279
1422
|
|
|
1280
1423
|
const permFresh = currentWaiting?.timestamp && now - new Date(currentWaiting.timestamp).getTime() < WAITING_TTL_MS;
|
|
@@ -1320,7 +1463,9 @@ function renderAgentFooter() {
|
|
|
1320
1463
|
const colonIdx = rawType.indexOf(':');
|
|
1321
1464
|
const typeNs = colonIdx > 0 ? rawType.substring(0, colonIdx + 1) : '';
|
|
1322
1465
|
const typeName = colonIdx > 0 ? rawType.substring(colonIdx + 1) : rawType;
|
|
1323
|
-
|
|
1466
|
+
const agentColor = resolveNamedColor(a.color);
|
|
1467
|
+
const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
|
|
1468
|
+
return `<div class="agent-card"${colorStyle} onclick="showAgentModal('${a.agentId}')">
|
|
1324
1469
|
<div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span></div>
|
|
1325
1470
|
<div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
|
|
1326
1471
|
${msgHtml}
|
|
@@ -2961,9 +3106,12 @@ function setupEventSource() {
|
|
|
2961
3106
|
function debouncedRefresh(sessionId, isMetadata) {
|
|
2962
3107
|
if (isMetadata) {
|
|
2963
3108
|
clearTimeout(metadataRefreshTimer);
|
|
2964
|
-
metadataRefreshTimer = setTimeout(() => {
|
|
3109
|
+
metadataRefreshTimer = setTimeout(async () => {
|
|
2965
3110
|
fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
|
|
2966
|
-
if (currentSessionId
|
|
3111
|
+
if (currentSessionId) {
|
|
3112
|
+
await fetchAgents(currentSessionId);
|
|
3113
|
+
if (!agentLogMode) fetchMessages(currentSessionId);
|
|
3114
|
+
}
|
|
2967
3115
|
}, 2000);
|
|
2968
3116
|
} else {
|
|
2969
3117
|
pendingTaskSessionIds.add(sessionId);
|
|
@@ -3252,13 +3400,46 @@ const ownerColors = [
|
|
|
3252
3400
|
{ bg: 'rgba(22, 163, 74, 0.14)', color: '#15803d' }, // green
|
|
3253
3401
|
{ bg: 'rgba(99, 102, 241, 0.14)', color: '#4f46e5' }, // indigo
|
|
3254
3402
|
];
|
|
3403
|
+
const namedColorMap = {
|
|
3404
|
+
red: { bg: 'rgba(239, 68, 68, 0.14)', color: '#dc2626' },
|
|
3405
|
+
blue: { bg: 'rgba(37, 99, 235, 0.14)', color: '#1d5bbf' },
|
|
3406
|
+
green: { bg: 'rgba(22, 163, 74, 0.14)', color: '#15803d' },
|
|
3407
|
+
purple: { bg: 'rgba(168, 85, 247, 0.14)', color: '#7c3aed' },
|
|
3408
|
+
orange: { bg: 'rgba(234, 88, 12, 0.14)', color: '#c2410c' },
|
|
3409
|
+
pink: { bg: 'rgba(219, 39, 119, 0.14)', color: '#b5246a' },
|
|
3410
|
+
yellow: { bg: 'rgba(202, 138, 4, 0.14)', color: '#92700c' },
|
|
3411
|
+
teal: { bg: 'rgba(14, 165, 133, 0.14)', color: '#0d7d65' },
|
|
3412
|
+
indigo: { bg: 'rgba(99, 102, 241, 0.14)', color: '#4f46e5' },
|
|
3413
|
+
cyan: { bg: 'rgba(6, 182, 212, 0.14)', color: '#0891b2' },
|
|
3414
|
+
};
|
|
3255
3415
|
const ownerColorCache = {};
|
|
3416
|
+
const teamColorMap = {};
|
|
3256
3417
|
function isInternalTask(task) {
|
|
3257
3418
|
return task.metadata && task.metadata._internal === true;
|
|
3258
3419
|
}
|
|
3259
3420
|
|
|
3421
|
+
function resolveNamedColor(colorName) {
|
|
3422
|
+
if (!colorName) return null;
|
|
3423
|
+
return namedColorMap[colorName.toLowerCase()] || null;
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
function updateTeamColors(agents, colors) {
|
|
3427
|
+
if (colors) Object.assign(teamColorMap, colors);
|
|
3428
|
+
for (const a of agents) {
|
|
3429
|
+
const name = a.type || a.name;
|
|
3430
|
+
if (name && a.color) teamColorMap[name] = a.color;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3260
3434
|
function getOwnerColor(name) {
|
|
3261
3435
|
if (ownerColorCache[name]) return ownerColorCache[name];
|
|
3436
|
+
if (teamColorMap[name]) {
|
|
3437
|
+
const c = resolveNamedColor(teamColorMap[name]);
|
|
3438
|
+
if (c) {
|
|
3439
|
+
ownerColorCache[name] = c;
|
|
3440
|
+
return c;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3262
3443
|
let hash = 5381;
|
|
3263
3444
|
for (let i = 0; i < name.length; i++) {
|
|
3264
3445
|
hash = ((hash * 33) ^ name.charCodeAt(i)) | 0;
|
|
@@ -3623,6 +3804,10 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3623
3804
|
if (session.tasksDir) {
|
|
3624
3805
|
infoRows.push(['Tasks Dir', session.tasksDir, { openPath: session.tasksDir }]);
|
|
3625
3806
|
}
|
|
3807
|
+
if (teamConfig?.configPath) {
|
|
3808
|
+
const configDir = teamConfig.configPath.replace(/[/\\][^/\\]+$/, '');
|
|
3809
|
+
infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
|
|
3810
|
+
}
|
|
3626
3811
|
const clickableStyle =
|
|
3627
3812
|
"font-family: 'IBM Plex Mono', monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; color: var(--accent-text); text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px;";
|
|
3628
3813
|
const plainStyle =
|
|
@@ -3638,7 +3823,8 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3638
3823
|
} else {
|
|
3639
3824
|
html += `<span style="${plainStyle}" title="${copyVal}">${escapeHtml(value)}</span>`;
|
|
3640
3825
|
}
|
|
3641
|
-
|
|
3826
|
+
const jsCopyVal = copyVal.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
3827
|
+
html += `<button onclick="navigator.clipboard.writeText('${jsCopyVal}'); this.textContent='✓'; setTimeout(() => this.textContent='Copy', 1000)" style="padding: 2px 8px; font-size: 11px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; color: var(--text-secondary); cursor: pointer; white-space: nowrap;">Copy</button>`;
|
|
3642
3828
|
});
|
|
3643
3829
|
html += `</div>`;
|
|
3644
3830
|
|
|
@@ -3686,10 +3872,12 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3686
3872
|
members.forEach((member) => {
|
|
3687
3873
|
const taskCount = ownerCounts[member.name] || 0;
|
|
3688
3874
|
const memberDesc = memberDescriptions[member.name];
|
|
3875
|
+
const mc = resolveNamedColor(member.color);
|
|
3876
|
+
const borderStyle = mc ? ` style="border-left:3px solid ${mc.color}"` : '';
|
|
3877
|
+
const nameStyle = mc ? ` style="color:${mc.color}"` : '';
|
|
3689
3878
|
html += `
|
|
3690
|
-
<div class="team-member-card">
|
|
3691
|
-
<div class="member-name"
|
|
3692
|
-
<div class="member-detail">Role: ${escapeHtml(member.agentType || 'unknown')}</div>
|
|
3879
|
+
<div class="team-member-card"${borderStyle}>
|
|
3880
|
+
<div class="member-name"${nameStyle}>${escapeHtml(member.name)}</div>
|
|
3693
3881
|
${member.model ? `<div class="member-detail">Model: ${escapeHtml(member.model)}</div>` : ''}
|
|
3694
3882
|
${memberDesc ? `<div class="member-detail" style="margin-top: 4px; font-style: italic; color: var(--text-secondary);">${escapeHtml(memberDesc.split('\n')[0])}</div>` : ''}
|
|
3695
3883
|
<div class="member-tasks">Tasks: ${taskCount} assigned</div>
|
package/public/style.css
CHANGED
|
@@ -2115,6 +2115,36 @@ body::before {
|
|
|
2115
2115
|
padding: 0;
|
|
2116
2116
|
font-size: 11px;
|
|
2117
2117
|
}
|
|
2118
|
+
.msg-item.msg-teammate {
|
|
2119
|
+
border-left: 3px solid var(--team);
|
|
2120
|
+
}
|
|
2121
|
+
.msg-item.msg-protocol {
|
|
2122
|
+
border-left: 3px solid var(--border);
|
|
2123
|
+
opacity: 0.7;
|
|
2124
|
+
}
|
|
2125
|
+
.msg-item.msg-idle {
|
|
2126
|
+
border-left: 3px solid var(--border);
|
|
2127
|
+
opacity: 0.75;
|
|
2128
|
+
}
|
|
2129
|
+
.msg-item.msg-idle .msg-icon {
|
|
2130
|
+
width: 12px;
|
|
2131
|
+
height: 12px;
|
|
2132
|
+
}
|
|
2133
|
+
.teammate-name {
|
|
2134
|
+
font-weight: 600;
|
|
2135
|
+
font-size: 11px;
|
|
2136
|
+
}
|
|
2137
|
+
.idle-label,
|
|
2138
|
+
.protocol-label {
|
|
2139
|
+
font-size: 10px;
|
|
2140
|
+
color: var(--text-muted);
|
|
2141
|
+
font-style: italic;
|
|
2142
|
+
}
|
|
2143
|
+
.teammate-idle-detail {
|
|
2144
|
+
padding: 12px;
|
|
2145
|
+
text-align: center;
|
|
2146
|
+
color: var(--text-muted);
|
|
2147
|
+
}
|
|
2118
2148
|
.msg-time {
|
|
2119
2149
|
font-size: 10px;
|
|
2120
2150
|
color: var(--text-muted);
|
|
@@ -2150,6 +2180,7 @@ body::before {
|
|
|
2150
2180
|
}
|
|
2151
2181
|
.msg-agent-log-btn {
|
|
2152
2182
|
flex-shrink: 0;
|
|
2183
|
+
margin-left: 0;
|
|
2153
2184
|
background: none;
|
|
2154
2185
|
border: none;
|
|
2155
2186
|
color: var(--text-muted);
|
|
@@ -2167,6 +2198,52 @@ body::before {
|
|
|
2167
2198
|
opacity: 1;
|
|
2168
2199
|
color: var(--accent);
|
|
2169
2200
|
}
|
|
2201
|
+
.protocol-detail {
|
|
2202
|
+
padding: 8px 12px;
|
|
2203
|
+
}
|
|
2204
|
+
.protocol-detail + div {
|
|
2205
|
+
margin-top: 8px;
|
|
2206
|
+
padding-top: 8px;
|
|
2207
|
+
border-top: 1px solid var(--border);
|
|
2208
|
+
}
|
|
2209
|
+
.protocol-type-badge {
|
|
2210
|
+
display: inline-block;
|
|
2211
|
+
padding: 2px 10px;
|
|
2212
|
+
border-radius: 4px;
|
|
2213
|
+
background: var(--bg-secondary);
|
|
2214
|
+
color: var(--text-secondary);
|
|
2215
|
+
font-size: 0.8rem;
|
|
2216
|
+
font-weight: 600;
|
|
2217
|
+
text-transform: capitalize;
|
|
2218
|
+
margin-bottom: 8px;
|
|
2219
|
+
}
|
|
2220
|
+
.protocol-fields {
|
|
2221
|
+
display: grid;
|
|
2222
|
+
grid-template-columns: auto 1fr;
|
|
2223
|
+
gap: 4px 10px;
|
|
2224
|
+
align-items: baseline;
|
|
2225
|
+
font-size: 0.85rem;
|
|
2226
|
+
}
|
|
2227
|
+
.protocol-field {
|
|
2228
|
+
display: contents;
|
|
2229
|
+
}
|
|
2230
|
+
.protocol-field-key {
|
|
2231
|
+
color: var(--text-muted);
|
|
2232
|
+
white-space: nowrap;
|
|
2233
|
+
text-align: right;
|
|
2234
|
+
}
|
|
2235
|
+
.protocol-field-key::after {
|
|
2236
|
+
content: ':';
|
|
2237
|
+
}
|
|
2238
|
+
.protocol-bool {
|
|
2239
|
+
font-weight: 600;
|
|
2240
|
+
}
|
|
2241
|
+
.protocol-bool-true {
|
|
2242
|
+
color: #4caf50;
|
|
2243
|
+
}
|
|
2244
|
+
.protocol-bool-false {
|
|
2245
|
+
color: #ef5350;
|
|
2246
|
+
}
|
|
2170
2247
|
|
|
2171
2248
|
/* #endregion */
|
|
2172
2249
|
|
package/server.js
CHANGED
|
@@ -14,7 +14,8 @@ const {
|
|
|
14
14
|
readRecentMessages: _readRecentMessagesUncached,
|
|
15
15
|
readSessionInfoFromJsonl,
|
|
16
16
|
buildAgentProgressMap,
|
|
17
|
-
readCompactSummaries
|
|
17
|
+
readCompactSummaries,
|
|
18
|
+
findTerminatedTeammates
|
|
18
19
|
} = require('./lib/parsers');
|
|
19
20
|
|
|
20
21
|
const isSetupCommand = process.argv.includes('--install') || process.argv.includes('--uninstall');
|
|
@@ -55,7 +56,8 @@ const CONTEXT_STATUS_DIR = path.join(CLAUDE_DIR, 'context-status');
|
|
|
55
56
|
|
|
56
57
|
const PERMISSION_TTL_MS = 1800000;
|
|
57
58
|
const AGENT_TTL_MS = 3600000;
|
|
58
|
-
const AGENT_STALE_MS =
|
|
59
|
+
const AGENT_STALE_MS = 900000;
|
|
60
|
+
const SESSION_STALE_MS = 300000;
|
|
59
61
|
|
|
60
62
|
const WAITING_RESOLVE_GRACE_MS = 15000;
|
|
61
63
|
|
|
@@ -74,10 +76,20 @@ function checkWaitingForUser(agentDir, logMtime) {
|
|
|
74
76
|
return null;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function isGhostAgent(agent) {
|
|
80
|
+
if (agent.startedAt !== agent.updatedAt || agent.lastMessage) return false;
|
|
81
|
+
return (Date.now() - new Date(agent.startedAt).getTime()) >= AGENT_STALE_MS;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getContextStatus(sessionId, meta) {
|
|
85
|
+
return contextStatusCache.get(sessionId) || (meta?.teamLeaderId ? contextStatusCache.get(meta.teamLeaderId) : null) || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
function isAgentFresh(agent) {
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
if (isGhostAgent(agent)) return false;
|
|
90
|
+
const ts = agent.updatedAt || agent.startedAt;
|
|
91
|
+
if (!ts) return true;
|
|
92
|
+
return (Date.now() - new Date(ts).getTime()) < AGENT_TTL_MS;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
function getSessionLogStat(meta) {
|
|
@@ -88,17 +100,20 @@ function getSessionLogStat(meta) {
|
|
|
88
100
|
} catch (e) { return { mtime: null, hasMessages: false }; }
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
function checkAgentStatus(agentDir, stale, logMtime) {
|
|
103
|
+
function checkAgentStatus(agentDir, stale, logMtime, isTeam) {
|
|
92
104
|
const result = { hasActive: false, hasRunning: false, waitingForUser: null };
|
|
93
105
|
if (!existsSync(agentDir)) return result;
|
|
94
106
|
result.waitingForUser = checkWaitingForUser(agentDir, logMtime);
|
|
95
107
|
if (result.waitingForUser) result.hasActive = true;
|
|
96
|
-
if (stale) return result;
|
|
108
|
+
if (stale && !isTeam) return result;
|
|
97
109
|
try {
|
|
98
110
|
for (const file of readdirSync(agentDir).filter(f => f.endsWith('.json') && !f.startsWith('_'))) {
|
|
99
111
|
try {
|
|
100
112
|
const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
|
|
101
|
-
if (
|
|
113
|
+
if (isTeam && (agent.status === 'active' || agent.status === 'idle')) {
|
|
114
|
+
result.hasActive = true;
|
|
115
|
+
if (agent.status === 'active') result.hasRunning = true;
|
|
116
|
+
} else if (isAgentFresh(agent)) {
|
|
102
117
|
if (agent.status === 'active') { result.hasActive = true; result.hasRunning = true; }
|
|
103
118
|
}
|
|
104
119
|
if (result.hasRunning && result.hasActive) break;
|
|
@@ -170,6 +185,7 @@ const messageCache = new Map();
|
|
|
170
185
|
const MESSAGE_CACHE_TTL = 5000;
|
|
171
186
|
const MAX_CACHE_ENTRIES = 200;
|
|
172
187
|
const progressMapCache = new Map();
|
|
188
|
+
const terminatedCache = new Map();
|
|
173
189
|
const compactSummaryCache = new Map();
|
|
174
190
|
const taskCountsCache = new Map();
|
|
175
191
|
const contextStatusCache = new Map();
|
|
@@ -208,36 +224,32 @@ function getTaskCounts(sessionPath) {
|
|
|
208
224
|
return result;
|
|
209
225
|
}
|
|
210
226
|
|
|
211
|
-
function
|
|
227
|
+
function cachedByMtime(cache, filePath, loadFn, fallback) {
|
|
212
228
|
try {
|
|
213
|
-
const cached =
|
|
214
|
-
if (cached && Date.now() - cached.ts < MESSAGE_CACHE_TTL) return cached.
|
|
215
|
-
const st = statSync(
|
|
229
|
+
const cached = cache.get(filePath);
|
|
230
|
+
if (cached && Date.now() - cached.ts < MESSAGE_CACHE_TTL) return cached.data;
|
|
231
|
+
const st = statSync(filePath);
|
|
216
232
|
if (cached && cached.mtime === st.mtimeMs) {
|
|
217
233
|
cached.ts = Date.now();
|
|
218
|
-
return cached.
|
|
234
|
+
return cached.data;
|
|
219
235
|
}
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
evictStaleCache(
|
|
223
|
-
return
|
|
224
|
-
} catch (_) { return
|
|
236
|
+
const data = loadFn(filePath);
|
|
237
|
+
cache.set(filePath, { data, mtime: st.mtimeMs, ts: Date.now() });
|
|
238
|
+
evictStaleCache(cache);
|
|
239
|
+
return data;
|
|
240
|
+
} catch (_) { return fallback; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getProgressMap(jsonlPath) {
|
|
244
|
+
return cachedByMtime(progressMapCache, jsonlPath, buildAgentProgressMap, {});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getTerminatedTeammates(jsonlPath) {
|
|
248
|
+
return cachedByMtime(terminatedCache, jsonlPath, findTerminatedTeammates, new Set());
|
|
225
249
|
}
|
|
226
250
|
|
|
227
251
|
function readRecentMessages(jsonlPath, limit = 10) {
|
|
228
|
-
|
|
229
|
-
const stat = statSync(jsonlPath);
|
|
230
|
-
const cached = messageCache.get(jsonlPath);
|
|
231
|
-
if (cached && cached.mtime === stat.mtimeMs && Date.now() - cached.ts < MESSAGE_CACHE_TTL) {
|
|
232
|
-
return cached.messages;
|
|
233
|
-
}
|
|
234
|
-
const messages = _readRecentMessagesUncached(jsonlPath, limit);
|
|
235
|
-
messageCache.set(jsonlPath, { messages, mtime: stat.mtimeMs, ts: Date.now() });
|
|
236
|
-
evictStaleCache(messageCache);
|
|
237
|
-
return messages;
|
|
238
|
-
} catch (e) {
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
252
|
+
return cachedByMtime(messageCache, jsonlPath, p => _readRecentMessagesUncached(p, limit), []);
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
/**
|
|
@@ -378,6 +390,42 @@ function getSessionDisplayName(sessionId, meta) {
|
|
|
378
390
|
return null;
|
|
379
391
|
}
|
|
380
392
|
|
|
393
|
+
function buildSessionObject(id, meta, overrides = {}) {
|
|
394
|
+
const logStat = overrides._logStat || getSessionLogStat(meta);
|
|
395
|
+
const logMtime = logStat.mtime;
|
|
396
|
+
const logAge = logMtime ? Date.now() - logMtime : Infinity;
|
|
397
|
+
return {
|
|
398
|
+
id,
|
|
399
|
+
name: getSessionDisplayName(id, meta),
|
|
400
|
+
slug: meta.slug || null,
|
|
401
|
+
project: meta.project || null,
|
|
402
|
+
description: meta.description || null,
|
|
403
|
+
gitBranch: meta.gitBranch || null,
|
|
404
|
+
customTitle: meta.customTitle || null,
|
|
405
|
+
taskCount: 0,
|
|
406
|
+
completed: 0,
|
|
407
|
+
inProgress: 0,
|
|
408
|
+
pending: 0,
|
|
409
|
+
createdAt: meta.created || null,
|
|
410
|
+
modifiedAt: overrides.modifiedAt || new Date(0).toISOString(),
|
|
411
|
+
isTeam: false,
|
|
412
|
+
memberCount: 0,
|
|
413
|
+
hasMessages: logStat.hasMessages,
|
|
414
|
+
hasActiveAgents: false,
|
|
415
|
+
hasRunningAgents: false,
|
|
416
|
+
hasWaitingForUser: false,
|
|
417
|
+
hasRecentLog: logAge <= SESSION_STALE_MS,
|
|
418
|
+
jsonlPath: meta.jsonlPath || null,
|
|
419
|
+
tasksDir: null,
|
|
420
|
+
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
421
|
+
contextStatus: getContextStatus(id, meta),
|
|
422
|
+
...getPlanInfo(meta.slug),
|
|
423
|
+
...overrides,
|
|
424
|
+
// Remove internal-only field
|
|
425
|
+
_logStat: undefined,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
381
429
|
// API: List all sessions
|
|
382
430
|
app.get('/api/sessions', async (req, res) => {
|
|
383
431
|
// Prevent browser caching
|
|
@@ -422,43 +470,31 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
422
470
|
}
|
|
423
471
|
|
|
424
472
|
const isTeam = isTeamSession(entry.name);
|
|
425
|
-
const
|
|
473
|
+
const teamConfig = isTeam ? loadTeamConfig(entry.name) : null;
|
|
474
|
+
const memberCount = teamConfig?.members?.length || 0;
|
|
426
475
|
const planInfo = getPlanInfo(meta.slug);
|
|
427
476
|
|
|
428
477
|
const resolvedAgentDir = (() => {
|
|
429
|
-
const
|
|
430
|
-
const rid = (tc && tc.leadSessionId) ? tc.leadSessionId : entry.name;
|
|
478
|
+
const rid = teamConfig?.leadSessionId || entry.name;
|
|
431
479
|
return path.join(AGENT_ACTIVITY_DIR, rid);
|
|
432
480
|
})();
|
|
433
|
-
const agentStatus = checkAgentStatus(resolvedAgentDir, stale, logMtime);
|
|
434
|
-
|
|
435
|
-
sessionsMap.set(entry.name, {
|
|
436
|
-
|
|
437
|
-
name: getSessionDisplayName(entry.name, meta),
|
|
438
|
-
slug: meta.slug || null,
|
|
439
|
-
project: meta.project || null,
|
|
440
|
-
description: meta.description || null,
|
|
441
|
-
gitBranch: meta.gitBranch || null,
|
|
442
|
-
customTitle: meta.customTitle || null,
|
|
481
|
+
const agentStatus = checkAgentStatus(resolvedAgentDir, stale, logMtime, isTeam);
|
|
482
|
+
|
|
483
|
+
sessionsMap.set(entry.name, buildSessionObject(entry.name, meta, {
|
|
484
|
+
_logStat: logStat,
|
|
443
485
|
taskCount,
|
|
444
486
|
completed,
|
|
445
487
|
inProgress,
|
|
446
488
|
pending,
|
|
447
|
-
|
|
448
|
-
modifiedAt: modifiedAt,
|
|
489
|
+
modifiedAt,
|
|
449
490
|
isTeam,
|
|
450
491
|
memberCount,
|
|
451
|
-
hasMessages: logStat.hasMessages,
|
|
452
492
|
hasActiveAgents: agentStatus.hasActive,
|
|
453
493
|
hasRunningAgents: agentStatus.hasRunning,
|
|
454
494
|
hasWaitingForUser: !!agentStatus.waitingForUser,
|
|
455
|
-
hasRecentLog: logAge <= AGENT_STALE_MS,
|
|
456
|
-
jsonlPath: meta.jsonlPath || null,
|
|
457
495
|
tasksDir: sessionPath,
|
|
458
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
459
|
-
contextStatus: contextStatusCache.get(entry.name) || (meta.teamLeaderId ? contextStatusCache.get(meta.teamLeaderId) : null) || null,
|
|
460
496
|
...planInfo
|
|
461
|
-
});
|
|
497
|
+
}));
|
|
462
498
|
}
|
|
463
499
|
}
|
|
464
500
|
}
|
|
@@ -475,36 +511,16 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
475
511
|
const jsonlMtime = new Date(logMtime).toISOString();
|
|
476
512
|
if (!modifiedAt || jsonlMtime > modifiedAt) modifiedAt = jsonlMtime;
|
|
477
513
|
}
|
|
478
|
-
const
|
|
514
|
+
const metaIsTeam = isTeamSession(sessionId);
|
|
479
515
|
const metaAgentDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
|
|
480
|
-
const metaAgentStatus = checkAgentStatus(metaAgentDir, stale, logMtime);
|
|
481
|
-
sessionsMap.set(sessionId, {
|
|
482
|
-
|
|
483
|
-
name: getSessionDisplayName(sessionId, meta),
|
|
484
|
-
slug: meta.slug || null,
|
|
485
|
-
project: meta.project || null,
|
|
486
|
-
description: meta.description || null,
|
|
487
|
-
gitBranch: meta.gitBranch || null,
|
|
488
|
-
customTitle: meta.customTitle || null,
|
|
489
|
-
taskCount: 0,
|
|
490
|
-
completed: 0,
|
|
491
|
-
inProgress: 0,
|
|
492
|
-
pending: 0,
|
|
493
|
-
createdAt: meta.created || null,
|
|
516
|
+
const metaAgentStatus = checkAgentStatus(metaAgentDir, stale, logMtime, metaIsTeam);
|
|
517
|
+
sessionsMap.set(sessionId, buildSessionObject(sessionId, meta, {
|
|
518
|
+
_logStat: logStat,
|
|
494
519
|
modifiedAt: modifiedAt || new Date(0).toISOString(),
|
|
495
|
-
isTeam: false,
|
|
496
|
-
memberCount: 0,
|
|
497
|
-
hasMessages: logStat.hasMessages,
|
|
498
520
|
hasActiveAgents: metaAgentStatus.hasActive,
|
|
499
521
|
hasRunningAgents: metaAgentStatus.hasRunning,
|
|
500
522
|
hasWaitingForUser: !!metaAgentStatus.waitingForUser,
|
|
501
|
-
|
|
502
|
-
jsonlPath: meta.jsonlPath || null,
|
|
503
|
-
tasksDir: null,
|
|
504
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
505
|
-
contextStatus: contextStatusCache.get(sessionId) || (meta.teamLeaderId ? contextStatusCache.get(meta.teamLeaderId) : null) || null,
|
|
506
|
-
...planInfo
|
|
507
|
-
});
|
|
523
|
+
}));
|
|
508
524
|
}
|
|
509
525
|
}
|
|
510
526
|
|
|
@@ -518,29 +534,12 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
518
534
|
const logStat = getSessionLogStat(meta);
|
|
519
535
|
const waiting = checkWaitingForUser(agentDir, logStat.mtime);
|
|
520
536
|
if (!waiting) continue;
|
|
521
|
-
sessionsMap.set(dir.name, {
|
|
522
|
-
|
|
523
|
-
name: getSessionDisplayName(dir.name, meta),
|
|
524
|
-
slug: meta.slug || null,
|
|
525
|
-
project: meta.project || null,
|
|
526
|
-
description: meta.description || null,
|
|
527
|
-
gitBranch: meta.gitBranch || null,
|
|
528
|
-
customTitle: meta.customTitle || null,
|
|
529
|
-
taskCount: 0,
|
|
530
|
-
completed: 0,
|
|
531
|
-
inProgress: 0,
|
|
532
|
-
pending: 0,
|
|
533
|
-
createdAt: meta.created || null,
|
|
537
|
+
sessionsMap.set(dir.name, buildSessionObject(dir.name, meta, {
|
|
538
|
+
_logStat: logStat,
|
|
534
539
|
modifiedAt: waiting.timestamp || new Date().toISOString(),
|
|
535
|
-
isTeam: false,
|
|
536
|
-
memberCount: 0,
|
|
537
|
-
hasMessages: logStat.hasMessages,
|
|
538
540
|
hasActiveAgents: true,
|
|
539
541
|
hasWaitingForUser: true,
|
|
540
|
-
|
|
541
|
-
tasksDir: null,
|
|
542
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
543
|
-
});
|
|
542
|
+
}));
|
|
544
543
|
}
|
|
545
544
|
} catch (e) { /* ignore */ }
|
|
546
545
|
}
|
|
@@ -554,8 +553,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
554
553
|
}
|
|
555
554
|
}
|
|
556
555
|
for (const leaderId of teamLeaderIds) {
|
|
557
|
-
|
|
558
|
-
if (session && session.taskCount === 0) {
|
|
556
|
+
if (sessionsMap.has(leaderId)) {
|
|
559
557
|
sessionsMap.delete(leaderId);
|
|
560
558
|
}
|
|
561
559
|
}
|
|
@@ -585,7 +583,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
585
583
|
const s = sessionsMap.get(pid);
|
|
586
584
|
if (s && !s.contextStatus) {
|
|
587
585
|
const meta = metadata[pid];
|
|
588
|
-
s.contextStatus =
|
|
586
|
+
s.contextStatus = getContextStatus(pid, meta);
|
|
589
587
|
}
|
|
590
588
|
}
|
|
591
589
|
|
|
@@ -594,34 +592,17 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
594
592
|
if (sessionsMap.has(pid)) continue;
|
|
595
593
|
const meta = metadata[pid];
|
|
596
594
|
if (!meta) continue;
|
|
597
|
-
const
|
|
598
|
-
const
|
|
595
|
+
const pinnedLogStat = getSessionLogStat(meta);
|
|
596
|
+
const pinnedLogMtime = pinnedLogStat.mtime;
|
|
599
597
|
let modifiedAt = meta.created || null;
|
|
600
|
-
if (
|
|
601
|
-
const jsonlMtime = new Date(
|
|
598
|
+
if (pinnedLogMtime) {
|
|
599
|
+
const jsonlMtime = new Date(pinnedLogMtime).toISOString();
|
|
602
600
|
if (!modifiedAt || jsonlMtime > modifiedAt) modifiedAt = jsonlMtime;
|
|
603
601
|
}
|
|
604
|
-
sessionsMap.set(pid, {
|
|
605
|
-
|
|
606
|
-
name: getSessionDisplayName(pid, meta),
|
|
607
|
-
slug: meta.slug || null,
|
|
608
|
-
project: meta.project || null,
|
|
609
|
-
description: meta.description || null,
|
|
610
|
-
gitBranch: meta.gitBranch || null,
|
|
611
|
-
customTitle: meta.customTitle || null,
|
|
612
|
-
taskCount: 0, completed: 0, inProgress: 0, pending: 0,
|
|
613
|
-
createdAt: meta.created || null,
|
|
602
|
+
sessionsMap.set(pid, buildSessionObject(pid, meta, {
|
|
603
|
+
_logStat: pinnedLogStat,
|
|
614
604
|
modifiedAt: modifiedAt || new Date(0).toISOString(),
|
|
615
|
-
|
|
616
|
-
hasMessages: logStat.hasMessages,
|
|
617
|
-
hasActiveAgents: false, hasRunningAgents: false, hasWaitingForUser: false,
|
|
618
|
-
hasRecentLog: false,
|
|
619
|
-
jsonlPath: meta.jsonlPath || null,
|
|
620
|
-
tasksDir: null,
|
|
621
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
622
|
-
contextStatus: contextStatusCache.get(pid) || (meta.teamLeaderId ? contextStatusCache.get(meta.teamLeaderId) : null) || null,
|
|
623
|
-
...getPlanInfo(meta.slug)
|
|
624
|
-
});
|
|
605
|
+
}));
|
|
625
606
|
}
|
|
626
607
|
|
|
627
608
|
// Convert map to array and sort by most recently modified
|
|
@@ -771,6 +752,7 @@ app.post('/api/open-in-editor', (req, res) => {
|
|
|
771
752
|
app.get('/api/teams/:name', (req, res) => {
|
|
772
753
|
const config = loadTeamConfig(req.params.name);
|
|
773
754
|
if (!config) return res.status(404).json({ error: 'Team not found' });
|
|
755
|
+
config.configPath = path.join(TEAMS_DIR, req.params.name, 'config.json');
|
|
774
756
|
res.json(config);
|
|
775
757
|
});
|
|
776
758
|
|
|
@@ -785,22 +767,60 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
785
767
|
const logMtime = getSessionLogStat(meta).mtime;
|
|
786
768
|
const sessionStale = logMtime ? (Date.now() - logMtime) > AGENT_STALE_MS : true;
|
|
787
769
|
|
|
770
|
+
let teamConfig = loadTeamConfig(req.params.sessionId);
|
|
771
|
+
if (!teamConfig && existsSync(TEAMS_DIR)) {
|
|
772
|
+
try {
|
|
773
|
+
for (const td of readdirSync(TEAMS_DIR, { withFileTypes: true })) {
|
|
774
|
+
if (!td.isDirectory()) continue;
|
|
775
|
+
const cfg = loadTeamConfig(td.name);
|
|
776
|
+
if (cfg && cfg.leadSessionId === sessionId) { teamConfig = cfg; break; }
|
|
777
|
+
}
|
|
778
|
+
} catch (_) {}
|
|
779
|
+
}
|
|
780
|
+
const isTeam = !!teamConfig;
|
|
781
|
+
const teamMemberNames = isTeam ? new Set(teamConfig.members.map(m => m.name)) : null;
|
|
782
|
+
|
|
788
783
|
const files = readdirSync(agentDir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
|
|
789
784
|
const agents = [];
|
|
790
785
|
for (const file of files) {
|
|
791
786
|
try {
|
|
792
787
|
const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
|
|
793
|
-
if (agent
|
|
794
|
-
const
|
|
788
|
+
if (isGhostAgent(agent)) continue;
|
|
789
|
+
const agentTs = agent.updatedAt || agent.startedAt;
|
|
790
|
+
const agentStale = !sessionStale && agentTs && (Date.now() - new Date(agentTs).getTime()) > AGENT_STALE_MS;
|
|
795
791
|
if (!isAgentFresh(agent) || sessionStale || agentStale) {
|
|
796
792
|
if (agent.status === 'active' || agent.status === 'idle') {
|
|
797
|
-
agent.
|
|
798
|
-
|
|
793
|
+
const agentName = agent.type || agent.name;
|
|
794
|
+
const isTeamMember = isTeam && agentName && teamMemberNames.has(agentName);
|
|
795
|
+
if (!isTeamMember) {
|
|
796
|
+
agent.status = 'stopped';
|
|
797
|
+
if (!agent.stoppedAt) agent.stoppedAt = agent.updatedAt || agent.startedAt;
|
|
798
|
+
}
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
801
|
agents.push(agent);
|
|
802
802
|
} catch (e) { /* skip invalid */ }
|
|
803
803
|
}
|
|
804
|
+
const liveAgents = agents.filter(a => a.status === 'active' || a.status === 'idle');
|
|
805
|
+
if (liveAgents.length && meta.jsonlPath) {
|
|
806
|
+
try {
|
|
807
|
+
const terminated = getTerminatedTeammates(meta.jsonlPath);
|
|
808
|
+
if (terminated.size) {
|
|
809
|
+
for (const agent of liveAgents) {
|
|
810
|
+
const agentName = agent.type || agent.name;
|
|
811
|
+
if (agentName && terminated.has(agentName)) {
|
|
812
|
+
const terminatedAt = terminated.get(agentName);
|
|
813
|
+
if (terminatedAt && agent.startedAt && terminatedAt < agent.startedAt) continue;
|
|
814
|
+
agent.status = 'stopped';
|
|
815
|
+
agent.stoppedAt = agent.stoppedAt || new Date().toISOString();
|
|
816
|
+
const agentFile = path.join(agentDir, agent.agentId + '.json');
|
|
817
|
+
fs.writeFile(agentFile, JSON.stringify(agent), 'utf8').catch(() => {});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
} catch (_) {}
|
|
822
|
+
}
|
|
823
|
+
|
|
804
824
|
const agentsNeedingPrompt = agents.filter(a => !a.prompt);
|
|
805
825
|
if (agentsNeedingPrompt.length && meta.jsonlPath) {
|
|
806
826
|
try {
|
|
@@ -819,8 +839,21 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
819
839
|
}
|
|
820
840
|
} catch (_) {}
|
|
821
841
|
}
|
|
842
|
+
const teamColors = {};
|
|
843
|
+
if (teamConfig?.members) {
|
|
844
|
+
for (const m of teamConfig.members) {
|
|
845
|
+
if (m.name && m.color) teamColors[m.name] = m.color;
|
|
846
|
+
}
|
|
847
|
+
if (Object.keys(teamColors).length) {
|
|
848
|
+
for (const agent of agents) {
|
|
849
|
+
const name = agent.type || agent.name;
|
|
850
|
+
if (name && teamColors[name]) agent.color = teamColors[name];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
822
855
|
const waitingForUser = checkWaitingForUser(agentDir, logMtime);
|
|
823
|
-
res.json({ agents, waitingForUser });
|
|
856
|
+
res.json({ agents, waitingForUser, teamColors });
|
|
824
857
|
} catch (e) {
|
|
825
858
|
res.json({ agents: [], waitingForUser: null });
|
|
826
859
|
}
|
|
@@ -828,7 +861,7 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
828
861
|
|
|
829
862
|
app.post('/api/sessions/:sessionId/agents/:agentId/stop', (req, res) => {
|
|
830
863
|
const sessionId = resolveSessionId(req.params.sessionId);
|
|
831
|
-
const agentId =
|
|
864
|
+
const agentId = sanitizeAgentId(req.params.agentId);
|
|
832
865
|
const agentFile = path.join(AGENT_ACTIVITY_DIR, sessionId, agentId + '.json');
|
|
833
866
|
if (!existsSync(agentFile)) return res.status(404).json({ error: 'Agent not found' });
|
|
834
867
|
try {
|
|
@@ -897,27 +930,28 @@ app.get('/api/sessions/:sessionId/agents/:agentId/messages/stream', (req, res) =
|
|
|
897
930
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
898
931
|
});
|
|
899
932
|
|
|
900
|
-
|
|
933
|
+
let closed = false;
|
|
934
|
+
const cleanup = () => { if (!closed) { closed = true; watcher.close(); } };
|
|
935
|
+
|
|
936
|
+
function emitMessages() {
|
|
937
|
+
const messages = readRecentMessages(subagentJsonl, 50);
|
|
938
|
+
lastSize = statSync(subagentJsonl).size;
|
|
939
|
+
res.write(`event: agent-log-update\ndata: ${JSON.stringify({ messages, agentId })}\n\n`);
|
|
940
|
+
}
|
|
901
941
|
|
|
902
942
|
watcher.on('change', () => {
|
|
903
943
|
try {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const messages = readRecentMessages(subagentJsonl, 50);
|
|
907
|
-
lastSize = currentSize;
|
|
908
|
-
res.write(`event: agent-log-update\ndata: ${JSON.stringify({ messages, agentId })}\n\n`);
|
|
944
|
+
if (statSync(subagentJsonl).size <= lastSize) return;
|
|
945
|
+
emitMessages();
|
|
909
946
|
} catch (_) {}
|
|
910
947
|
});
|
|
911
948
|
|
|
912
949
|
watcher.on('add', () => {
|
|
913
|
-
try {
|
|
914
|
-
const messages = readRecentMessages(subagentJsonl, 50);
|
|
915
|
-
lastSize = statSync(subagentJsonl).size;
|
|
916
|
-
res.write(`event: agent-log-update\ndata: ${JSON.stringify({ messages, agentId })}\n\n`);
|
|
917
|
-
} catch (_) {}
|
|
950
|
+
try { emitMessages(); } catch (_) {}
|
|
918
951
|
});
|
|
919
952
|
|
|
920
953
|
req.on('close', cleanup);
|
|
954
|
+
res.on('close', cleanup);
|
|
921
955
|
res.on('error', cleanup);
|
|
922
956
|
});
|
|
923
957
|
|