claude-code-kanban 2.1.0-rc.2 → 2.1.0-rc.4
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/install.js +78 -12
- package/lib/parsers.js +71 -6
- package/package.json +1 -1
- package/public/app.js +184 -29
- package/public/style.css +48 -1
- package/server.js +146 -122
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/install.js
CHANGED
|
@@ -45,7 +45,27 @@ function prompt(question) {
|
|
|
45
45
|
async function runInstall() {
|
|
46
46
|
console.log(`\n ${bold('claude-code-kanban')} — Agent Log hook installer\n`);
|
|
47
47
|
|
|
48
|
-
// 1. Check
|
|
48
|
+
// 1. Check bash
|
|
49
|
+
process.stdout.write(' Checking bash... ');
|
|
50
|
+
try {
|
|
51
|
+
const bashPath = execSync('which bash', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
52
|
+
console.log(green(`✓ found (${bashPath})`));
|
|
53
|
+
} catch {
|
|
54
|
+
const shell = process.env.SHELL || process.env.BASH || '';
|
|
55
|
+
if (shell.includes('bash')) {
|
|
56
|
+
console.log(green(`✓ found via $SHELL (${shell})`));
|
|
57
|
+
} else {
|
|
58
|
+
const currentShell = shell || process.env.ComSpec || 'unknown';
|
|
59
|
+
console.log(yellow(`⚠ bash not found (current shell: ${currentShell})`));
|
|
60
|
+
console.log(` ${dim('Hook scripts use #!/bin/bash and require a bash environment')}`);
|
|
61
|
+
if (!(await prompt(` Continue anyway? [Y/n] `))) {
|
|
62
|
+
console.log(`\n ${dim('Install cancelled.')}\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Check jq
|
|
49
69
|
process.stdout.write(' Checking jq... ');
|
|
50
70
|
try {
|
|
51
71
|
const ver = execSync('jq --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
@@ -84,15 +104,11 @@ async function runInstall() {
|
|
|
84
104
|
return false;
|
|
85
105
|
}
|
|
86
106
|
|
|
87
|
-
//
|
|
107
|
+
// 3. Hook scripts
|
|
88
108
|
const hookInstalled = await installScript('Hook script', HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
|
|
89
109
|
const ctxInstalled = await installScript('Context spy', CTX_SCRIPT_SRC, CTX_SCRIPT_DEST);
|
|
90
|
-
if (ctxInstalled) {
|
|
91
|
-
console.log(`\n ${yellow('To enable context tracking, pipe it before your statusline:')}`);
|
|
92
|
-
console.log(` ${dim('"statusLine": { "command": "~/.claude/hooks/context-status.sh | <your-statusline>" }')}`);
|
|
93
|
-
}
|
|
94
110
|
|
|
95
|
-
//
|
|
111
|
+
// 4. Settings.json
|
|
96
112
|
console.log(`\n Settings: ${dim(SETTINGS_PATH)}`);
|
|
97
113
|
let settings;
|
|
98
114
|
try {
|
|
@@ -141,6 +157,38 @@ async function runInstall() {
|
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
|
|
160
|
+
// 5. StatusLine setup (separate approval)
|
|
161
|
+
const CTX_COMMAND = '~/.claude/hooks/context-status.sh';
|
|
162
|
+
let statusLineUpdated = false;
|
|
163
|
+
if (ctxInstalled) {
|
|
164
|
+
const hasCtx = settings.statusLine?.command?.includes('context-status.sh');
|
|
165
|
+
if (hasCtx) {
|
|
166
|
+
console.log(`\n StatusLine: ${green('✓')} Already configured`);
|
|
167
|
+
statusLineUpdated = true;
|
|
168
|
+
} else if (!settings.statusLine) {
|
|
169
|
+
console.log(`\n StatusLine: ${dim('not configured')}`);
|
|
170
|
+
if (await prompt(` Set up context tracking statusline? [Y/n] `)) {
|
|
171
|
+
settings.statusLine = { command: CTX_COMMAND };
|
|
172
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
173
|
+
console.log(` ${green('✓')} StatusLine configured`);
|
|
174
|
+
statusLineUpdated = true;
|
|
175
|
+
} else {
|
|
176
|
+
console.log(` ${dim('Skipped')}`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
const existing = settings.statusLine.command;
|
|
180
|
+
console.log(`\n StatusLine: ${dim(`current: ${existing}`)}`);
|
|
181
|
+
if (await prompt(` Prepend context spy to existing statusline? [Y/n] `)) {
|
|
182
|
+
settings.statusLine.command = `${CTX_COMMAND} | ${existing}`;
|
|
183
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
184
|
+
console.log(` ${green('✓')} StatusLine updated`);
|
|
185
|
+
statusLineUpdated = true;
|
|
186
|
+
} else {
|
|
187
|
+
console.log(` ${dim('Skipped')}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
144
192
|
printSummary(hookInstalled, settingsUpdated);
|
|
145
193
|
}
|
|
146
194
|
|
|
@@ -161,20 +209,38 @@ async function runUninstall() {
|
|
|
161
209
|
if (fs.existsSync(SETTINGS_PATH)) {
|
|
162
210
|
try {
|
|
163
211
|
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
|
|
212
|
+
let removed = 0;
|
|
164
213
|
if (settings.hooks) {
|
|
165
|
-
let removed = 0;
|
|
166
214
|
const eventNames = [...new Set(HOOK_EVENTS.map(e => e.event))];
|
|
167
215
|
for (const event of eventNames) {
|
|
168
216
|
if (!Array.isArray(settings.hooks[event])) continue;
|
|
169
217
|
const before = settings.hooks[event].length;
|
|
170
|
-
settings.hooks[event] = settings.hooks[event].
|
|
171
|
-
!g.hooks?.some(h => h.command === HOOK_COMMAND)
|
|
172
|
-
|
|
218
|
+
settings.hooks[event] = settings.hooks[event].map(g => {
|
|
219
|
+
if (!g.hooks?.some(h => h.command === HOOK_COMMAND)) return g;
|
|
220
|
+
const filtered = g.hooks.filter(h => h.command !== HOOK_COMMAND);
|
|
221
|
+
return filtered.length > 0 ? { ...g, hooks: filtered } : null;
|
|
222
|
+
}).filter(Boolean);
|
|
173
223
|
removed += before - settings.hooks[event].length;
|
|
174
224
|
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
175
225
|
}
|
|
176
226
|
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
177
|
-
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Strip context-status.sh from statusLine, restore downstream command if any
|
|
230
|
+
if (settings.statusLine?.command?.includes('context-status.sh')) {
|
|
231
|
+
const cmd = settings.statusLine.command;
|
|
232
|
+
const stripped = cmd.replace(/~\/\.claude\/hooks\/context-status\.sh\s*\|\s*/, '').trim();
|
|
233
|
+
if (stripped && stripped !== cmd) {
|
|
234
|
+
settings.statusLine.command = stripped;
|
|
235
|
+
console.log(` StatusLine: ${green('✓')} Restored to "${stripped}"`);
|
|
236
|
+
} else {
|
|
237
|
+
delete settings.statusLine;
|
|
238
|
+
console.log(` StatusLine: ${green('✓')} Removed`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
243
|
+
if (removed > 0) {
|
|
178
244
|
console.log(` Settings: ${green('✓')} Removed ${removed} hook entries`);
|
|
179
245
|
} else {
|
|
180
246
|
console.log(` Settings: ${dim('No hook entries found')}`);
|
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 = {
|
|
@@ -399,7 +420,8 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
399
420
|
case 'shutdown_response': protocolLabel = protocol.approve ? 'shutdown approved' : `shutdown rejected: ${protocol.reason || ''}`; break;
|
|
400
421
|
case 'plan_approval_request': protocolLabel = 'plan approval requested'; break;
|
|
401
422
|
case 'plan_approval_response': protocolLabel = protocol.approve ? 'plan approved' : `plan rejected: ${protocol.feedback || ''}`; break;
|
|
402
|
-
|
|
423
|
+
case 'teammate_terminated': protocolLabel = protocol.message || 'shut down'; break;
|
|
424
|
+
default: protocolLabel = protocol.type.replace(/_/g, ' '); break;
|
|
403
425
|
}
|
|
404
426
|
}
|
|
405
427
|
const truncated = !isProtocol && body.length > 500;
|
|
@@ -412,6 +434,7 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
412
434
|
isProtocol,
|
|
413
435
|
protocolType: protocol?.type || null,
|
|
414
436
|
protocolLabel,
|
|
437
|
+
protocolData: protocol || null,
|
|
415
438
|
text: isProtocol ? null : (truncated ? body.slice(0, 500) + '...' : body),
|
|
416
439
|
fullText: isProtocol ? null : (truncated ? body : null),
|
|
417
440
|
timestamp: obj.timestamp
|
|
@@ -483,7 +506,9 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
483
506
|
const parentRe = /"parentToolUseID":"([^"]+)"/;
|
|
484
507
|
const promptRe = /"prompt":"((?:[^"\\]|\\.)*)"/;
|
|
485
508
|
const bgToolIdRe = /"tool_use_id":"([^"]+)"/;
|
|
486
|
-
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_@-]+)/;
|
|
487
512
|
for (const line of content.split('\n')) {
|
|
488
513
|
if (line.includes('"agent_progress"')) {
|
|
489
514
|
const agentMatch = re.exec(line);
|
|
@@ -505,6 +530,12 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
505
530
|
if (toolIdMatch && bgAgentMatch && !map[toolIdMatch[1]]) {
|
|
506
531
|
map[toolIdMatch[1]] = { agentId: bgAgentMatch[1], prompt: null };
|
|
507
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
|
+
}
|
|
508
539
|
}
|
|
509
540
|
}
|
|
510
541
|
} catch (_) {}
|
|
@@ -549,6 +580,39 @@ function readCompactSummaries(jsonlPath) {
|
|
|
549
580
|
return results.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
550
581
|
}
|
|
551
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
|
+
|
|
552
616
|
module.exports = {
|
|
553
617
|
parseTask,
|
|
554
618
|
parseAgent,
|
|
@@ -559,5 +623,6 @@ module.exports = {
|
|
|
559
623
|
readSessionInfoFromJsonl,
|
|
560
624
|
readRecentMessages,
|
|
561
625
|
buildAgentProgressMap,
|
|
562
|
-
readCompactSummaries
|
|
626
|
+
readCompactSummaries,
|
|
627
|
+
findTerminatedTeammates
|
|
563
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,33 +713,52 @@ 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>`;
|
|
716
735
|
} else if (m.type === 'teammate') {
|
|
717
|
-
|
|
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) : '';
|
|
718
746
|
if (m.isIdle) {
|
|
719
747
|
return `<div class="msg-item msg-teammate msg-idle" ${clickable}>
|
|
720
748
|
${MSG_ICON_IDLE}
|
|
721
|
-
<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
|
|
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}
|
|
722
750
|
</div>`;
|
|
723
751
|
}
|
|
724
752
|
if (m.isProtocol) {
|
|
725
753
|
return `<div class="msg-item msg-teammate msg-protocol" ${clickable}>
|
|
726
754
|
${MSG_ICON_TEAMMATE}
|
|
727
|
-
<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
|
|
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}
|
|
728
756
|
</div>`;
|
|
729
757
|
}
|
|
730
758
|
const summaryText = m.summary ? escapeHtml(m.summary) : escapeHtml((m.text || '').slice(0, 80));
|
|
731
759
|
return `<div class="msg-item msg-teammate" ${clickable}>
|
|
732
760
|
${MSG_ICON_TEAMMATE}
|
|
733
|
-
<div class="msg-body"><div class="msg-text">${nameSpan} ${summaryText}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${pinBtn}
|
|
761
|
+
<div class="msg-body"><div class="msg-text">${nameSpan} ${summaryText}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}${pinBtn}
|
|
734
762
|
</div>`;
|
|
735
763
|
}
|
|
736
764
|
return '';
|
|
@@ -986,12 +1014,25 @@ function showMsgDetail(idx) {
|
|
|
986
1014
|
} else {
|
|
987
1015
|
agentBtn.style.display = 'none';
|
|
988
1016
|
}
|
|
989
|
-
const
|
|
990
|
-
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);
|
|
991
1026
|
const hasAgentTabs = m.tool === 'Agent' && m.agentId && (m.agentLastMessage || m.agentPrompt);
|
|
992
1027
|
let mainHtml;
|
|
993
|
-
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) {
|
|
994
1033
|
mainHtml = descHtml || '';
|
|
1034
|
+
} else if (taskResultHtml) {
|
|
1035
|
+
mainHtml = '';
|
|
995
1036
|
} else if (fullText) {
|
|
996
1037
|
const detailEscaped = escapeHtml(fullText);
|
|
997
1038
|
const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
|
|
@@ -999,12 +1040,14 @@ function showMsgDetail(idx) {
|
|
|
999
1040
|
} else {
|
|
1000
1041
|
mainHtml = '<em>No details</em>';
|
|
1001
1042
|
}
|
|
1002
|
-
body.innerHTML = mainHtml + toolParamsHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
|
|
1043
|
+
body.innerHTML = mainHtml + toolParamsHtml + taskResultHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
|
|
1003
1044
|
} else if (m.type === 'teammate') {
|
|
1004
1045
|
document.getElementById('msg-detail-title').textContent = m.teammateId || 'Teammate';
|
|
1005
1046
|
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
1006
1047
|
if (m.isProtocol) {
|
|
1007
|
-
body.innerHTML =
|
|
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>`;
|
|
1008
1051
|
} else {
|
|
1009
1052
|
const text = stripAnsi(m.fullText || m.text || '');
|
|
1010
1053
|
body.innerHTML = renderMarkdown(text);
|
|
@@ -1099,6 +1142,69 @@ async function copyWithFeedback(text, btn) {
|
|
|
1099
1142
|
//#endregion
|
|
1100
1143
|
|
|
1101
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
|
+
|
|
1102
1208
|
function renderToolParamsHtml(params) {
|
|
1103
1209
|
if (!params) return '';
|
|
1104
1210
|
const BLOCK_KEYS = new Set(['old_string', 'new_string', 'content', 'plan']);
|
|
@@ -1304,9 +1410,14 @@ function renderAgentFooter() {
|
|
|
1304
1410
|
if (overlapped || reSpawn || isActive) filtered.push(group[i]);
|
|
1305
1411
|
}
|
|
1306
1412
|
}
|
|
1307
|
-
// Sort
|
|
1413
|
+
// Sort: active/idle first, then by updatedAt desc
|
|
1414
|
+
const statusOrder = { active: 0, idle: 1, stopped: 2 };
|
|
1308
1415
|
const visible = filtered
|
|
1309
|
-
.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
|
+
)
|
|
1310
1421
|
.slice(0, AGENT_LOG_MAX);
|
|
1311
1422
|
|
|
1312
1423
|
const permFresh = currentWaiting?.timestamp && now - new Date(currentWaiting.timestamp).getTime() < WAITING_TTL_MS;
|
|
@@ -1352,7 +1463,9 @@ function renderAgentFooter() {
|
|
|
1352
1463
|
const colonIdx = rawType.indexOf(':');
|
|
1353
1464
|
const typeNs = colonIdx > 0 ? rawType.substring(0, colonIdx + 1) : '';
|
|
1354
1465
|
const typeName = colonIdx > 0 ? rawType.substring(colonIdx + 1) : rawType;
|
|
1355
|
-
|
|
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}')">
|
|
1356
1469
|
<div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span></div>
|
|
1357
1470
|
<div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
|
|
1358
1471
|
${msgHtml}
|
|
@@ -2993,9 +3106,12 @@ function setupEventSource() {
|
|
|
2993
3106
|
function debouncedRefresh(sessionId, isMetadata) {
|
|
2994
3107
|
if (isMetadata) {
|
|
2995
3108
|
clearTimeout(metadataRefreshTimer);
|
|
2996
|
-
metadataRefreshTimer = setTimeout(() => {
|
|
3109
|
+
metadataRefreshTimer = setTimeout(async () => {
|
|
2997
3110
|
fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
|
|
2998
|
-
if (currentSessionId
|
|
3111
|
+
if (currentSessionId) {
|
|
3112
|
+
await fetchAgents(currentSessionId);
|
|
3113
|
+
if (!agentLogMode) fetchMessages(currentSessionId);
|
|
3114
|
+
}
|
|
2999
3115
|
}, 2000);
|
|
3000
3116
|
} else {
|
|
3001
3117
|
pendingTaskSessionIds.add(sessionId);
|
|
@@ -3284,13 +3400,46 @@ const ownerColors = [
|
|
|
3284
3400
|
{ bg: 'rgba(22, 163, 74, 0.14)', color: '#15803d' }, // green
|
|
3285
3401
|
{ bg: 'rgba(99, 102, 241, 0.14)', color: '#4f46e5' }, // indigo
|
|
3286
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
|
+
};
|
|
3287
3415
|
const ownerColorCache = {};
|
|
3416
|
+
const teamColorMap = {};
|
|
3288
3417
|
function isInternalTask(task) {
|
|
3289
3418
|
return task.metadata && task.metadata._internal === true;
|
|
3290
3419
|
}
|
|
3291
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
|
+
|
|
3292
3434
|
function getOwnerColor(name) {
|
|
3293
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
|
+
}
|
|
3294
3443
|
let hash = 5381;
|
|
3295
3444
|
for (let i = 0; i < name.length; i++) {
|
|
3296
3445
|
hash = ((hash * 33) ^ name.charCodeAt(i)) | 0;
|
|
@@ -3655,6 +3804,10 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3655
3804
|
if (session.tasksDir) {
|
|
3656
3805
|
infoRows.push(['Tasks Dir', session.tasksDir, { openPath: session.tasksDir }]);
|
|
3657
3806
|
}
|
|
3807
|
+
if (teamConfig?.configPath) {
|
|
3808
|
+
const configDir = teamConfig.configPath.replace(/[/\\][^/\\]+$/, '');
|
|
3809
|
+
infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
|
|
3810
|
+
}
|
|
3658
3811
|
const clickableStyle =
|
|
3659
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;";
|
|
3660
3813
|
const plainStyle =
|
|
@@ -3719,10 +3872,12 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3719
3872
|
members.forEach((member) => {
|
|
3720
3873
|
const taskCount = ownerCounts[member.name] || 0;
|
|
3721
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}"` : '';
|
|
3722
3878
|
html += `
|
|
3723
|
-
<div class="team-member-card">
|
|
3724
|
-
<div class="member-name"
|
|
3725
|
-
<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>
|
|
3726
3881
|
${member.model ? `<div class="member-detail">Model: ${escapeHtml(member.model)}</div>` : ''}
|
|
3727
3882
|
${memberDesc ? `<div class="member-detail" style="margin-top: 4px; font-style: italic; color: var(--text-secondary);">${escapeHtml(memberDesc.split('\n')[0])}</div>` : ''}
|
|
3728
3883
|
<div class="member-tasks">Tasks: ${taskCount} assigned</div>
|
package/public/style.css
CHANGED
|
@@ -2124,7 +2124,7 @@ body::before {
|
|
|
2124
2124
|
}
|
|
2125
2125
|
.msg-item.msg-idle {
|
|
2126
2126
|
border-left: 3px solid var(--border);
|
|
2127
|
-
opacity: 0.
|
|
2127
|
+
opacity: 0.75;
|
|
2128
2128
|
}
|
|
2129
2129
|
.msg-item.msg-idle .msg-icon {
|
|
2130
2130
|
width: 12px;
|
|
@@ -2180,6 +2180,7 @@ body::before {
|
|
|
2180
2180
|
}
|
|
2181
2181
|
.msg-agent-log-btn {
|
|
2182
2182
|
flex-shrink: 0;
|
|
2183
|
+
margin-left: 0;
|
|
2183
2184
|
background: none;
|
|
2184
2185
|
border: none;
|
|
2185
2186
|
color: var(--text-muted);
|
|
@@ -2197,6 +2198,52 @@ body::before {
|
|
|
2197
2198
|
opacity: 1;
|
|
2198
2199
|
color: var(--accent);
|
|
2199
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
|
+
}
|
|
2200
2247
|
|
|
2201
2248
|
/* #endregion */
|
|
2202
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');
|
|
@@ -56,6 +57,7 @@ const CONTEXT_STATUS_DIR = path.join(CLAUDE_DIR, 'context-status');
|
|
|
56
57
|
const PERMISSION_TTL_MS = 1800000;
|
|
57
58
|
const AGENT_TTL_MS = 3600000;
|
|
58
59
|
const AGENT_STALE_MS = 900000;
|
|
60
|
+
const SESSION_STALE_MS = 300000;
|
|
59
61
|
|
|
60
62
|
const WAITING_RESOLVE_GRACE_MS = 15000;
|
|
61
63
|
|
|
@@ -84,9 +86,10 @@ function getContextStatus(sessionId, meta) {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
function isAgentFresh(agent) {
|
|
87
|
-
if (!agent.updatedAt) return true;
|
|
88
89
|
if (isGhostAgent(agent)) return false;
|
|
89
|
-
|
|
90
|
+
const ts = agent.updatedAt || agent.startedAt;
|
|
91
|
+
if (!ts) return true;
|
|
92
|
+
return (Date.now() - new Date(ts).getTime()) < AGENT_TTL_MS;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
function getSessionLogStat(meta) {
|
|
@@ -97,17 +100,20 @@ function getSessionLogStat(meta) {
|
|
|
97
100
|
} catch (e) { return { mtime: null, hasMessages: false }; }
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
function checkAgentStatus(agentDir, stale, logMtime) {
|
|
103
|
+
function checkAgentStatus(agentDir, stale, logMtime, isTeam) {
|
|
101
104
|
const result = { hasActive: false, hasRunning: false, waitingForUser: null };
|
|
102
105
|
if (!existsSync(agentDir)) return result;
|
|
103
106
|
result.waitingForUser = checkWaitingForUser(agentDir, logMtime);
|
|
104
107
|
if (result.waitingForUser) result.hasActive = true;
|
|
105
|
-
if (stale) return result;
|
|
108
|
+
if (stale && !isTeam) return result;
|
|
106
109
|
try {
|
|
107
110
|
for (const file of readdirSync(agentDir).filter(f => f.endsWith('.json') && !f.startsWith('_'))) {
|
|
108
111
|
try {
|
|
109
112
|
const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
|
|
110
|
-
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)) {
|
|
111
117
|
if (agent.status === 'active') { result.hasActive = true; result.hasRunning = true; }
|
|
112
118
|
}
|
|
113
119
|
if (result.hasRunning && result.hasActive) break;
|
|
@@ -179,6 +185,7 @@ const messageCache = new Map();
|
|
|
179
185
|
const MESSAGE_CACHE_TTL = 5000;
|
|
180
186
|
const MAX_CACHE_ENTRIES = 200;
|
|
181
187
|
const progressMapCache = new Map();
|
|
188
|
+
const terminatedCache = new Map();
|
|
182
189
|
const compactSummaryCache = new Map();
|
|
183
190
|
const taskCountsCache = new Map();
|
|
184
191
|
const contextStatusCache = new Map();
|
|
@@ -217,36 +224,32 @@ function getTaskCounts(sessionPath) {
|
|
|
217
224
|
return result;
|
|
218
225
|
}
|
|
219
226
|
|
|
220
|
-
function
|
|
227
|
+
function cachedByMtime(cache, filePath, loadFn, fallback) {
|
|
221
228
|
try {
|
|
222
|
-
const cached =
|
|
223
|
-
if (cached && Date.now() - cached.ts < MESSAGE_CACHE_TTL) return cached.
|
|
224
|
-
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);
|
|
225
232
|
if (cached && cached.mtime === st.mtimeMs) {
|
|
226
233
|
cached.ts = Date.now();
|
|
227
|
-
return cached.
|
|
234
|
+
return cached.data;
|
|
228
235
|
}
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
evictStaleCache(
|
|
232
|
-
return
|
|
233
|
-
} 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());
|
|
234
249
|
}
|
|
235
250
|
|
|
236
251
|
function readRecentMessages(jsonlPath, limit = 10) {
|
|
237
|
-
|
|
238
|
-
const stat = statSync(jsonlPath);
|
|
239
|
-
const cached = messageCache.get(jsonlPath);
|
|
240
|
-
if (cached && cached.mtime === stat.mtimeMs && Date.now() - cached.ts < MESSAGE_CACHE_TTL) {
|
|
241
|
-
return cached.messages;
|
|
242
|
-
}
|
|
243
|
-
const messages = _readRecentMessagesUncached(jsonlPath, limit);
|
|
244
|
-
messageCache.set(jsonlPath, { messages, mtime: stat.mtimeMs, ts: Date.now() });
|
|
245
|
-
evictStaleCache(messageCache);
|
|
246
|
-
return messages;
|
|
247
|
-
} catch (e) {
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
252
|
+
return cachedByMtime(messageCache, jsonlPath, p => _readRecentMessagesUncached(p, limit), []);
|
|
250
253
|
}
|
|
251
254
|
|
|
252
255
|
/**
|
|
@@ -387,6 +390,42 @@ function getSessionDisplayName(sessionId, meta) {
|
|
|
387
390
|
return null;
|
|
388
391
|
}
|
|
389
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
|
+
|
|
390
429
|
// API: List all sessions
|
|
391
430
|
app.get('/api/sessions', async (req, res) => {
|
|
392
431
|
// Prevent browser caching
|
|
@@ -439,35 +478,23 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
439
478
|
const rid = teamConfig?.leadSessionId || entry.name;
|
|
440
479
|
return path.join(AGENT_ACTIVITY_DIR, rid);
|
|
441
480
|
})();
|
|
442
|
-
const agentStatus = checkAgentStatus(resolvedAgentDir, stale, logMtime);
|
|
443
|
-
|
|
444
|
-
sessionsMap.set(entry.name, {
|
|
445
|
-
|
|
446
|
-
name: getSessionDisplayName(entry.name, meta),
|
|
447
|
-
slug: meta.slug || null,
|
|
448
|
-
project: meta.project || null,
|
|
449
|
-
description: meta.description || null,
|
|
450
|
-
gitBranch: meta.gitBranch || null,
|
|
451
|
-
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,
|
|
452
485
|
taskCount,
|
|
453
486
|
completed,
|
|
454
487
|
inProgress,
|
|
455
488
|
pending,
|
|
456
|
-
|
|
457
|
-
modifiedAt: modifiedAt,
|
|
489
|
+
modifiedAt,
|
|
458
490
|
isTeam,
|
|
459
491
|
memberCount,
|
|
460
|
-
hasMessages: logStat.hasMessages,
|
|
461
492
|
hasActiveAgents: agentStatus.hasActive,
|
|
462
493
|
hasRunningAgents: agentStatus.hasRunning,
|
|
463
494
|
hasWaitingForUser: !!agentStatus.waitingForUser,
|
|
464
|
-
hasRecentLog: logAge <= AGENT_STALE_MS,
|
|
465
|
-
jsonlPath: meta.jsonlPath || null,
|
|
466
495
|
tasksDir: sessionPath,
|
|
467
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
468
|
-
contextStatus: getContextStatus(entry.name, meta),
|
|
469
496
|
...planInfo
|
|
470
|
-
});
|
|
497
|
+
}));
|
|
471
498
|
}
|
|
472
499
|
}
|
|
473
500
|
}
|
|
@@ -484,36 +511,16 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
484
511
|
const jsonlMtime = new Date(logMtime).toISOString();
|
|
485
512
|
if (!modifiedAt || jsonlMtime > modifiedAt) modifiedAt = jsonlMtime;
|
|
486
513
|
}
|
|
487
|
-
const
|
|
514
|
+
const metaIsTeam = isTeamSession(sessionId);
|
|
488
515
|
const metaAgentDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
|
|
489
|
-
const metaAgentStatus = checkAgentStatus(metaAgentDir, stale, logMtime);
|
|
490
|
-
sessionsMap.set(sessionId, {
|
|
491
|
-
|
|
492
|
-
name: getSessionDisplayName(sessionId, meta),
|
|
493
|
-
slug: meta.slug || null,
|
|
494
|
-
project: meta.project || null,
|
|
495
|
-
description: meta.description || null,
|
|
496
|
-
gitBranch: meta.gitBranch || null,
|
|
497
|
-
customTitle: meta.customTitle || null,
|
|
498
|
-
taskCount: 0,
|
|
499
|
-
completed: 0,
|
|
500
|
-
inProgress: 0,
|
|
501
|
-
pending: 0,
|
|
502
|
-
createdAt: meta.created || null,
|
|
516
|
+
const metaAgentStatus = checkAgentStatus(metaAgentDir, stale, logMtime, metaIsTeam);
|
|
517
|
+
sessionsMap.set(sessionId, buildSessionObject(sessionId, meta, {
|
|
518
|
+
_logStat: logStat,
|
|
503
519
|
modifiedAt: modifiedAt || new Date(0).toISOString(),
|
|
504
|
-
isTeam: false,
|
|
505
|
-
memberCount: 0,
|
|
506
|
-
hasMessages: logStat.hasMessages,
|
|
507
520
|
hasActiveAgents: metaAgentStatus.hasActive,
|
|
508
521
|
hasRunningAgents: metaAgentStatus.hasRunning,
|
|
509
522
|
hasWaitingForUser: !!metaAgentStatus.waitingForUser,
|
|
510
|
-
|
|
511
|
-
jsonlPath: meta.jsonlPath || null,
|
|
512
|
-
tasksDir: null,
|
|
513
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
514
|
-
contextStatus: getContextStatus(sessionId, meta),
|
|
515
|
-
...planInfo
|
|
516
|
-
});
|
|
523
|
+
}));
|
|
517
524
|
}
|
|
518
525
|
}
|
|
519
526
|
|
|
@@ -527,29 +534,12 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
527
534
|
const logStat = getSessionLogStat(meta);
|
|
528
535
|
const waiting = checkWaitingForUser(agentDir, logStat.mtime);
|
|
529
536
|
if (!waiting) continue;
|
|
530
|
-
sessionsMap.set(dir.name, {
|
|
531
|
-
|
|
532
|
-
name: getSessionDisplayName(dir.name, meta),
|
|
533
|
-
slug: meta.slug || null,
|
|
534
|
-
project: meta.project || null,
|
|
535
|
-
description: meta.description || null,
|
|
536
|
-
gitBranch: meta.gitBranch || null,
|
|
537
|
-
customTitle: meta.customTitle || null,
|
|
538
|
-
taskCount: 0,
|
|
539
|
-
completed: 0,
|
|
540
|
-
inProgress: 0,
|
|
541
|
-
pending: 0,
|
|
542
|
-
createdAt: meta.created || null,
|
|
537
|
+
sessionsMap.set(dir.name, buildSessionObject(dir.name, meta, {
|
|
538
|
+
_logStat: logStat,
|
|
543
539
|
modifiedAt: waiting.timestamp || new Date().toISOString(),
|
|
544
|
-
isTeam: false,
|
|
545
|
-
memberCount: 0,
|
|
546
|
-
hasMessages: logStat.hasMessages,
|
|
547
540
|
hasActiveAgents: true,
|
|
548
541
|
hasWaitingForUser: true,
|
|
549
|
-
|
|
550
|
-
tasksDir: null,
|
|
551
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
552
|
-
});
|
|
542
|
+
}));
|
|
553
543
|
}
|
|
554
544
|
} catch (e) { /* ignore */ }
|
|
555
545
|
}
|
|
@@ -563,8 +553,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
563
553
|
}
|
|
564
554
|
}
|
|
565
555
|
for (const leaderId of teamLeaderIds) {
|
|
566
|
-
|
|
567
|
-
if (session && session.taskCount === 0) {
|
|
556
|
+
if (sessionsMap.has(leaderId)) {
|
|
568
557
|
sessionsMap.delete(leaderId);
|
|
569
558
|
}
|
|
570
559
|
}
|
|
@@ -603,34 +592,17 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
603
592
|
if (sessionsMap.has(pid)) continue;
|
|
604
593
|
const meta = metadata[pid];
|
|
605
594
|
if (!meta) continue;
|
|
606
|
-
const
|
|
607
|
-
const
|
|
595
|
+
const pinnedLogStat = getSessionLogStat(meta);
|
|
596
|
+
const pinnedLogMtime = pinnedLogStat.mtime;
|
|
608
597
|
let modifiedAt = meta.created || null;
|
|
609
|
-
if (
|
|
610
|
-
const jsonlMtime = new Date(
|
|
598
|
+
if (pinnedLogMtime) {
|
|
599
|
+
const jsonlMtime = new Date(pinnedLogMtime).toISOString();
|
|
611
600
|
if (!modifiedAt || jsonlMtime > modifiedAt) modifiedAt = jsonlMtime;
|
|
612
601
|
}
|
|
613
|
-
sessionsMap.set(pid, {
|
|
614
|
-
|
|
615
|
-
name: getSessionDisplayName(pid, meta),
|
|
616
|
-
slug: meta.slug || null,
|
|
617
|
-
project: meta.project || null,
|
|
618
|
-
description: meta.description || null,
|
|
619
|
-
gitBranch: meta.gitBranch || null,
|
|
620
|
-
customTitle: meta.customTitle || null,
|
|
621
|
-
taskCount: 0, completed: 0, inProgress: 0, pending: 0,
|
|
622
|
-
createdAt: meta.created || null,
|
|
602
|
+
sessionsMap.set(pid, buildSessionObject(pid, meta, {
|
|
603
|
+
_logStat: pinnedLogStat,
|
|
623
604
|
modifiedAt: modifiedAt || new Date(0).toISOString(),
|
|
624
|
-
|
|
625
|
-
hasMessages: logStat.hasMessages,
|
|
626
|
-
hasActiveAgents: false, hasRunningAgents: false, hasWaitingForUser: false,
|
|
627
|
-
hasRecentLog: false,
|
|
628
|
-
jsonlPath: meta.jsonlPath || null,
|
|
629
|
-
tasksDir: null,
|
|
630
|
-
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
631
|
-
contextStatus: getContextStatus(pid, meta),
|
|
632
|
-
...getPlanInfo(meta.slug)
|
|
633
|
-
});
|
|
605
|
+
}));
|
|
634
606
|
}
|
|
635
607
|
|
|
636
608
|
// Convert map to array and sort by most recently modified
|
|
@@ -780,6 +752,7 @@ app.post('/api/open-in-editor', (req, res) => {
|
|
|
780
752
|
app.get('/api/teams/:name', (req, res) => {
|
|
781
753
|
const config = loadTeamConfig(req.params.name);
|
|
782
754
|
if (!config) return res.status(404).json({ error: 'Team not found' });
|
|
755
|
+
config.configPath = path.join(TEAMS_DIR, req.params.name, 'config.json');
|
|
783
756
|
res.json(config);
|
|
784
757
|
});
|
|
785
758
|
|
|
@@ -794,22 +767,60 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
794
767
|
const logMtime = getSessionLogStat(meta).mtime;
|
|
795
768
|
const sessionStale = logMtime ? (Date.now() - logMtime) > AGENT_STALE_MS : true;
|
|
796
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
|
+
|
|
797
783
|
const files = readdirSync(agentDir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
|
|
798
784
|
const agents = [];
|
|
799
785
|
for (const file of files) {
|
|
800
786
|
try {
|
|
801
787
|
const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
|
|
802
788
|
if (isGhostAgent(agent)) continue;
|
|
803
|
-
const
|
|
789
|
+
const agentTs = agent.updatedAt || agent.startedAt;
|
|
790
|
+
const agentStale = !sessionStale && agentTs && (Date.now() - new Date(agentTs).getTime()) > AGENT_STALE_MS;
|
|
804
791
|
if (!isAgentFresh(agent) || sessionStale || agentStale) {
|
|
805
792
|
if (agent.status === 'active' || agent.status === 'idle') {
|
|
806
|
-
agent.
|
|
807
|
-
|
|
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
|
+
}
|
|
808
799
|
}
|
|
809
800
|
}
|
|
810
801
|
agents.push(agent);
|
|
811
802
|
} catch (e) { /* skip invalid */ }
|
|
812
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
|
+
|
|
813
824
|
const agentsNeedingPrompt = agents.filter(a => !a.prompt);
|
|
814
825
|
if (agentsNeedingPrompt.length && meta.jsonlPath) {
|
|
815
826
|
try {
|
|
@@ -828,8 +839,21 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
828
839
|
}
|
|
829
840
|
} catch (_) {}
|
|
830
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
|
+
|
|
831
855
|
const waitingForUser = checkWaitingForUser(agentDir, logMtime);
|
|
832
|
-
res.json({ agents, waitingForUser });
|
|
856
|
+
res.json({ agents, waitingForUser, teamColors });
|
|
833
857
|
} catch (e) {
|
|
834
858
|
res.json({ agents: [], waitingForUser: null });
|
|
835
859
|
}
|