claude-code-kanban 4.22.0 → 4.23.2
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/lib/file-url.js +2 -0
- package/lib/parsers.js +32 -19
- package/package.json +1 -1
- package/public/app.js +12 -13
- package/public/style.css +92 -76
- package/server.js +13 -9
package/lib/file-url.js
CHANGED
|
@@ -24,6 +24,8 @@ function fileUrlToPath(value) {
|
|
|
24
24
|
if (typeof value !== 'string' || !/^file:/i.test(value)) return value;
|
|
25
25
|
// Backslashes are not legal in a URL, so this cannot corrupt a well-formed one.
|
|
26
26
|
let url = value.replace(/\\/g, '/');
|
|
27
|
+
// A scheme with no path body: POSIX Node resolves it to "/" rather than throwing.
|
|
28
|
+
if (/^file:\/*$/i.test(url)) return value;
|
|
27
29
|
const drive = DRIVE_URL.exec(url);
|
|
28
30
|
if (drive) url = `file:///${drive[1]}`;
|
|
29
31
|
try {
|
package/lib/parsers.js
CHANGED
|
@@ -160,6 +160,17 @@ function formatTaskUsage(usage) {
|
|
|
160
160
|
return parts.length ? ' · ' + parts.join(' · ') : '';
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// Raw per-agent cost for the agent modal's chips plus the formatted one-liner for
|
|
164
|
+
// the inline message row. Null when the source carried no numbers.
|
|
165
|
+
function agentUsageEntry(subagentTokens, toolUses, durationMs) {
|
|
166
|
+
const usage = {};
|
|
167
|
+
if (subagentTokens != null) usage.tokens = subagentTokens;
|
|
168
|
+
if (toolUses != null) usage.tools = toolUses;
|
|
169
|
+
if (durationMs != null) usage.durationMs = durationMs;
|
|
170
|
+
if (!Object.keys(usage).length) return null;
|
|
171
|
+
return { usage, usageText: formatTaskUsage({ subagentTokens, toolUses, durationMs }) };
|
|
172
|
+
}
|
|
173
|
+
|
|
163
174
|
function pushUserMessage(messages, text, timestamp, sysLabel, extras) {
|
|
164
175
|
if (sysLabel === '__skip__') return;
|
|
165
176
|
let safeText = text || '';
|
|
@@ -1099,6 +1110,7 @@ function buildSessionDigest(jsonlPath) {
|
|
|
1099
1110
|
const taskIdRe = /<task-id>([a-zA-Z0-9_-]+)<\/task-id>/;
|
|
1100
1111
|
const nameByToolUseId = {};
|
|
1101
1112
|
const descByToolUseId = {};
|
|
1113
|
+
const usageByToolUseId = {};
|
|
1102
1114
|
for (const line of content.split('\n')) {
|
|
1103
1115
|
// Terminated-teammate detection: check first since cheap substring guards
|
|
1104
1116
|
if (line.includes('teammate-message') &&
|
|
@@ -1171,34 +1183,34 @@ function buildSessionDigest(jsonlPath) {
|
|
|
1171
1183
|
} else if (line.includes('User rejected tool use') && line.includes('"tool_use_id"')) {
|
|
1172
1184
|
const m = tmToolIdRe.exec(line);
|
|
1173
1185
|
if (m) rejectedToolUseIds.add(m[1]);
|
|
1174
|
-
} else if (line.includes('<task-notification>')
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1186
|
+
} else if (line.includes('<task-notification>')) {
|
|
1187
|
+
// The envelope's tags survive JSON escaping, so read them off the raw line.
|
|
1188
|
+
// This covers every delivery shape (user message, queue-operation,
|
|
1189
|
+
// queued_command attachment) without enumerating them.
|
|
1190
|
+
if (line.includes('<status>killed</status>') || line.includes('<status>error</status>')) {
|
|
1191
|
+
const idMatch = taskIdRe.exec(line);
|
|
1192
|
+
if (idMatch) killedAgentIds.add(idMatch[1]);
|
|
1193
|
+
}
|
|
1194
|
+
if (line.includes('<subagent_tokens>')) {
|
|
1195
|
+
const notif = parseTaskNotification(line);
|
|
1196
|
+
const u = notif?.usage;
|
|
1197
|
+
const entry = u && notif.toolUseId ? agentUsageEntry(u.subagentTokens, u.toolUses, u.durationMs) : null;
|
|
1198
|
+
if (entry) usageByToolUseId[notif.toolUseId] = entry;
|
|
1199
|
+
}
|
|
1178
1200
|
} else if (line.includes('"toolUseResult"') && line.includes('"agentId"') && line.includes('"tool_result"')) {
|
|
1179
1201
|
try {
|
|
1180
1202
|
const obj = JSON.parse(line);
|
|
1181
1203
|
const tur = obj.toolUseResult;
|
|
1182
1204
|
if (tur?.agentId) {
|
|
1183
|
-
// Foreground agents report their cost on the completion toolUseResult
|
|
1184
|
-
//
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
// one-liner via the shared formatter (for the inline message row):
|
|
1188
|
-
// " · 31.5k tok · 5 tools · 34s".
|
|
1189
|
-
const usage = {};
|
|
1190
|
-
if (tur.totalTokens != null) usage.tokens = tur.totalTokens;
|
|
1191
|
-
if (tur.totalToolUseCount != null) usage.tools = tur.totalToolUseCount;
|
|
1192
|
-
if (tur.totalDurationMs != null) usage.durationMs = tur.totalDurationMs;
|
|
1193
|
-
const hasUsage = Object.keys(usage).length > 0;
|
|
1194
|
-
const usageText = hasUsage
|
|
1195
|
-
? formatTaskUsage({ subagentTokens: usage.tokens, toolUses: usage.tools, durationMs: usage.durationMs })
|
|
1196
|
-
: null;
|
|
1205
|
+
// Foreground agents report their cost on the completion toolUseResult;
|
|
1206
|
+
// background agents report the same numbers in their <task-notification>.
|
|
1207
|
+
const { usage, usageText } = agentUsageEntry(tur.totalTokens, tur.totalToolUseCount, tur.totalDurationMs)
|
|
1208
|
+
|| { usage: null, usageText: null };
|
|
1197
1209
|
const blocks = obj.message?.content;
|
|
1198
1210
|
if (Array.isArray(blocks)) {
|
|
1199
1211
|
for (const b of blocks) {
|
|
1200
1212
|
if (b.type === 'tool_result' && b.tool_use_id && !map[b.tool_use_id]) {
|
|
1201
|
-
map[b.tool_use_id] = { agentId: tur.agentId, prompt: tur.prompt || null, usage
|
|
1213
|
+
map[b.tool_use_id] = { agentId: tur.agentId, prompt: tur.prompt || null, usage, usageText };
|
|
1202
1214
|
}
|
|
1203
1215
|
}
|
|
1204
1216
|
}
|
|
@@ -1207,6 +1219,7 @@ function buildSessionDigest(jsonlPath) {
|
|
|
1207
1219
|
}
|
|
1208
1220
|
}
|
|
1209
1221
|
for (const [key, entry] of Object.entries(map)) {
|
|
1222
|
+
if (!entry.usage && usageByToolUseId[key]) Object.assign(entry, usageByToolUseId[key]);
|
|
1210
1223
|
if (nameByToolUseId[key]) entry.name = nameByToolUseId[key];
|
|
1211
1224
|
if (descByToolUseId[key]) entry.description = descByToolUseId[key];
|
|
1212
1225
|
// Prefer the full prompt from the assistant's tool_use input — Claude Code's
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1460,8 +1460,8 @@ function getToolIcon(toolName) {
|
|
|
1460
1460
|
}
|
|
1461
1461
|
const AGENT_LOG_ICON =
|
|
1462
1462
|
'<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>';
|
|
1463
|
-
function agentLogButton(agentId) {
|
|
1464
|
-
return `<button class="
|
|
1463
|
+
function agentLogButton(agentId, cls = 'msg-agent-log-btn') {
|
|
1464
|
+
return `<button class="${escapeHtml(cls)}" onclick="event.stopPropagation();viewAgentLog('${escAttrJs(agentId)}')" title="View agent log">${AGENT_LOG_ICON}</button>`;
|
|
1465
1465
|
}
|
|
1466
1466
|
|
|
1467
1467
|
function getPinId(m) {
|
|
@@ -2763,9 +2763,14 @@ function renderAgentFooter() {
|
|
|
2763
2763
|
const agentColor = resolveNamedColor(a.color);
|
|
2764
2764
|
const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
|
|
2765
2765
|
const selectedClass = a.agentId === currentAgentModalId ? ' selected' : '';
|
|
2766
|
+
const cardModel = shortModelName(a.model);
|
|
2767
|
+
const modelChipHtml = cardModel
|
|
2768
|
+
? `<span class="agent-card-model" title="${escapeHtml(a.model)}">${escapeHtml(cardModel)}</span>`
|
|
2769
|
+
: '';
|
|
2766
2770
|
return `<div class="agent-card${selectedClass}" data-agent-id="${escapeHtml(a.agentId)}"${colorStyle} onclick="showAgentModal('${escAttrJs(a.agentId)}')">
|
|
2771
|
+
${agentLogButton(a.agentId, 'agent-card-log-btn')}
|
|
2767
2772
|
<div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span>${nameBadgeHtml}</div>
|
|
2768
|
-
<div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span
|
|
2773
|
+
<div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span>${modelChipHtml}</div>
|
|
2769
2774
|
${msgHtml}
|
|
2770
2775
|
</div>`;
|
|
2771
2776
|
})
|
|
@@ -2878,10 +2883,8 @@ function showAgentModal(agentId) {
|
|
|
2878
2883
|
return `<span class="agent-chip${cls}"${style}${title}>${labelHtml}<span class="agent-chip-val">${value}</span></span>`;
|
|
2879
2884
|
};
|
|
2880
2885
|
|
|
2881
|
-
// Agent cost stats live on the launch row's message (resolved from the completion
|
|
2882
|
-
// toolUseResult), not on the agent-activity record — so read them from there.
|
|
2883
2886
|
const agentMsg = currentMessages.find((m) => m.tool === 'Agent' && m.agentId === agentId);
|
|
2884
|
-
const usageStats =
|
|
2887
|
+
const usageStats = agent.usage || null;
|
|
2885
2888
|
const fmtTok = fmtTokens;
|
|
2886
2889
|
|
|
2887
2890
|
const chips = [];
|
|
@@ -3137,9 +3140,9 @@ function renderSessions() {
|
|
|
3137
3140
|
const total = session.taskCount;
|
|
3138
3141
|
const percent = total > 0 ? Math.round((session.completed / total) * 100) : 0;
|
|
3139
3142
|
const isActive = session.id === currentSessionId && viewMode === 'session';
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
+
// hasRecentLog carries the server's registry-idle correction: an open-but-idle
|
|
3144
|
+
// terminal keeps touching its JSONL, so mtime alone is not proof of activity.
|
|
3145
|
+
const isLive = session.hasRecentLog && Date.now() - Date.parse(session.modifiedAt) <= LIVE_INDICATOR_MS;
|
|
3143
3146
|
const rawName = session.name || session.id;
|
|
3144
3147
|
const sessionName = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawName)
|
|
3145
3148
|
? rawName.slice(0, 8)
|
|
@@ -4612,8 +4615,6 @@ function activateSelectedSession(items) {
|
|
|
4612
4615
|
|
|
4613
4616
|
function setFocusZone(zone) {
|
|
4614
4617
|
const sidebar = document.querySelector('.sidebar');
|
|
4615
|
-
// Clear all zone visuals
|
|
4616
|
-
sidebar.classList.remove('sidebar-focused');
|
|
4617
4618
|
clearKbSelection();
|
|
4618
4619
|
clearTaskSelection();
|
|
4619
4620
|
|
|
@@ -4623,7 +4624,6 @@ function setFocusZone(zone) {
|
|
|
4623
4624
|
sidebar.classList.remove('collapsed');
|
|
4624
4625
|
localStorage.setItem('sidebar-collapsed', false);
|
|
4625
4626
|
}
|
|
4626
|
-
sidebar.classList.add('sidebar-focused');
|
|
4627
4627
|
const items = getNavigableItems();
|
|
4628
4628
|
if (items.length > 0) {
|
|
4629
4629
|
const activeIdx = items.findIndex((el) => el.classList.contains('active'));
|
|
@@ -5767,7 +5767,6 @@ document.addEventListener('keydown', (e) => {
|
|
|
5767
5767
|
}
|
|
5768
5768
|
if (e.key === 'Escape') {
|
|
5769
5769
|
// Plain unfocus — drop the keyboard cursor without jumping into the board
|
|
5770
|
-
document.querySelector('.sidebar').classList.remove('sidebar-focused');
|
|
5771
5770
|
clearKbSelection();
|
|
5772
5771
|
focusZone = 'board';
|
|
5773
5772
|
return;
|
package/public/style.css
CHANGED
|
@@ -562,6 +562,7 @@ body::before {
|
|
|
562
562
|
}
|
|
563
563
|
|
|
564
564
|
.session-item {
|
|
565
|
+
position: relative;
|
|
565
566
|
display: block;
|
|
566
567
|
width: 100%;
|
|
567
568
|
padding: 13px 15px;
|
|
@@ -574,11 +575,63 @@ body::before {
|
|
|
574
575
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
575
576
|
}
|
|
576
577
|
|
|
578
|
+
/* `.active` is the open session, `.kb-selected` the keyboard cursor. The cursor
|
|
579
|
+
must win when both land on one card, which is why it is ordered second. */
|
|
577
580
|
.session-item.active {
|
|
578
581
|
background: var(--bg-deep);
|
|
579
582
|
border-color: var(--accent);
|
|
580
583
|
}
|
|
581
584
|
|
|
585
|
+
.session-item.kb-selected {
|
|
586
|
+
background: var(--bg-elevated);
|
|
587
|
+
border-color: var(--accent);
|
|
588
|
+
box-shadow: 0 0 0 1px var(--accent-dim);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.session-item.stale {
|
|
592
|
+
opacity: 0.85;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.session-item.stale:hover,
|
|
596
|
+
.session-item.stale.active,
|
|
597
|
+
.session-item.stale.kb-selected,
|
|
598
|
+
.session-item.stale:has(.session-pin-btn.sticky) {
|
|
599
|
+
opacity: 1;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.session-item.stale .session-time,
|
|
603
|
+
.session-item.warm .session-time {
|
|
604
|
+
display: inline-flex;
|
|
605
|
+
align-items: center;
|
|
606
|
+
gap: 6px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.session-item.stale .session-time::before,
|
|
610
|
+
.session-item.warm .session-time::before {
|
|
611
|
+
content: '';
|
|
612
|
+
width: 6px;
|
|
613
|
+
height: 6px;
|
|
614
|
+
border-radius: 50%;
|
|
615
|
+
flex-shrink: 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.session-item.warm .session-time {
|
|
619
|
+
color: var(--success);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.session-item.warm .session-time::before {
|
|
623
|
+
background: var(--success);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.session-item.stale .session-time {
|
|
627
|
+
color: var(--text-muted);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.session-item.stale .session-time::before {
|
|
631
|
+
background: transparent;
|
|
632
|
+
border: 1px solid var(--text-muted);
|
|
633
|
+
}
|
|
634
|
+
|
|
582
635
|
.session-name {
|
|
583
636
|
font-size: 13px;
|
|
584
637
|
color: var(--text-primary);
|
|
@@ -590,12 +643,11 @@ body::before {
|
|
|
590
643
|
}
|
|
591
644
|
|
|
592
645
|
.session-indicators .pulse {
|
|
593
|
-
width:
|
|
594
|
-
height:
|
|
646
|
+
width: 6px;
|
|
647
|
+
height: 6px;
|
|
595
648
|
background: var(--accent);
|
|
596
649
|
border-radius: 50%;
|
|
597
650
|
animation: pulse 2s ease-in-out infinite;
|
|
598
|
-
box-shadow: 0 0 12px var(--accent-glow);
|
|
599
651
|
}
|
|
600
652
|
|
|
601
653
|
@keyframes pulse {
|
|
@@ -694,6 +746,10 @@ body::before {
|
|
|
694
746
|
transition: width 0.5s ease-out;
|
|
695
747
|
}
|
|
696
748
|
|
|
749
|
+
.session-item:not(.active) .progress-fill {
|
|
750
|
+
background: color-mix(in srgb, var(--text-tertiary) 55%, transparent);
|
|
751
|
+
}
|
|
752
|
+
|
|
697
753
|
.progress-text {
|
|
698
754
|
font-size: 11px;
|
|
699
755
|
font-weight: 500;
|
|
@@ -3597,6 +3653,25 @@ body::before {
|
|
|
3597
3653
|
.agent-card:hover {
|
|
3598
3654
|
border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
|
|
3599
3655
|
}
|
|
3656
|
+
.agent-card-log-btn {
|
|
3657
|
+
position: absolute;
|
|
3658
|
+
top: 4px;
|
|
3659
|
+
right: 4px;
|
|
3660
|
+
display: flex;
|
|
3661
|
+
padding: 2px;
|
|
3662
|
+
background: none;
|
|
3663
|
+
border: none;
|
|
3664
|
+
color: var(--text-muted);
|
|
3665
|
+
cursor: pointer;
|
|
3666
|
+
opacity: 0.45;
|
|
3667
|
+
transition:
|
|
3668
|
+
opacity 0.15s,
|
|
3669
|
+
color 0.15s;
|
|
3670
|
+
}
|
|
3671
|
+
.agent-card-log-btn:hover {
|
|
3672
|
+
opacity: 1;
|
|
3673
|
+
color: var(--accent);
|
|
3674
|
+
}
|
|
3600
3675
|
.agent-card.selected,
|
|
3601
3676
|
.agent-card.selected:hover {
|
|
3602
3677
|
background: var(--bg-elevated);
|
|
@@ -3662,6 +3737,20 @@ body::before {
|
|
|
3662
3737
|
font-size: 11px;
|
|
3663
3738
|
color: var(--text-tertiary);
|
|
3664
3739
|
}
|
|
3740
|
+
.agent-card-model {
|
|
3741
|
+
margin-left: auto;
|
|
3742
|
+
padding: 1px 6px;
|
|
3743
|
+
font-family: var(--font-mono, monospace);
|
|
3744
|
+
font-size: 9.5px;
|
|
3745
|
+
line-height: 1.4;
|
|
3746
|
+
color: var(--text-tertiary);
|
|
3747
|
+
border: 1px solid var(--border);
|
|
3748
|
+
border-radius: 999px;
|
|
3749
|
+
white-space: nowrap;
|
|
3750
|
+
overflow: hidden;
|
|
3751
|
+
text-overflow: ellipsis;
|
|
3752
|
+
max-width: 45%;
|
|
3753
|
+
}
|
|
3665
3754
|
.agent-message {
|
|
3666
3755
|
font-size: 11px;
|
|
3667
3756
|
color: var(--text-muted);
|
|
@@ -5415,79 +5504,6 @@ pre.mermaid svg {
|
|
|
5415
5504
|
color: var(--accent);
|
|
5416
5505
|
}
|
|
5417
5506
|
|
|
5418
|
-
/* Session list hover accent bar */
|
|
5419
|
-
.session-item {
|
|
5420
|
-
position: relative;
|
|
5421
|
-
}
|
|
5422
|
-
|
|
5423
|
-
.session-item::before {
|
|
5424
|
-
content: '';
|
|
5425
|
-
position: absolute;
|
|
5426
|
-
left: 0;
|
|
5427
|
-
top: 50%;
|
|
5428
|
-
transform: translateY(-50%);
|
|
5429
|
-
width: 0;
|
|
5430
|
-
height: 60%;
|
|
5431
|
-
background: var(--accent);
|
|
5432
|
-
border-radius: 0 2px 2px 0;
|
|
5433
|
-
transition: width 0.15s ease;
|
|
5434
|
-
}
|
|
5435
|
-
|
|
5436
|
-
.session-item:hover::before {
|
|
5437
|
-
width: 0;
|
|
5438
|
-
}
|
|
5439
|
-
|
|
5440
|
-
.sidebar-focused .session-item.active::before {
|
|
5441
|
-
width: 0;
|
|
5442
|
-
}
|
|
5443
|
-
|
|
5444
|
-
.session-item.kb-selected,
|
|
5445
|
-
.session-item.kb-selected:hover,
|
|
5446
|
-
.session-item.active.kb-selected {
|
|
5447
|
-
background: var(--bg-elevated);
|
|
5448
|
-
border-color: var(--accent);
|
|
5449
|
-
box-shadow: 0 0 0 1px var(--accent-dim);
|
|
5450
|
-
}
|
|
5451
|
-
|
|
5452
|
-
.session-item.kb-selected::before {
|
|
5453
|
-
width: 0;
|
|
5454
|
-
}
|
|
5455
|
-
.session-item.stale .session-time,
|
|
5456
|
-
.session-item.warm .session-time {
|
|
5457
|
-
display: inline-flex;
|
|
5458
|
-
align-items: center;
|
|
5459
|
-
gap: 6px;
|
|
5460
|
-
}
|
|
5461
|
-
.session-item.stale .session-time::before,
|
|
5462
|
-
.session-item.warm .session-time::before {
|
|
5463
|
-
content: '';
|
|
5464
|
-
width: 6px;
|
|
5465
|
-
height: 6px;
|
|
5466
|
-
border-radius: 50%;
|
|
5467
|
-
flex-shrink: 0;
|
|
5468
|
-
}
|
|
5469
|
-
.session-item.warm .session-time {
|
|
5470
|
-
color: var(--success);
|
|
5471
|
-
}
|
|
5472
|
-
.session-item.warm .session-time::before {
|
|
5473
|
-
background: var(--success);
|
|
5474
|
-
}
|
|
5475
|
-
.session-item.stale {
|
|
5476
|
-
opacity: 0.85;
|
|
5477
|
-
}
|
|
5478
|
-
.session-item.stale:hover,
|
|
5479
|
-
.session-item.stale.active,
|
|
5480
|
-
.session-item.stale.kb-selected,
|
|
5481
|
-
.session-item.stale:has(.session-pin-btn.sticky) {
|
|
5482
|
-
opacity: 1;
|
|
5483
|
-
}
|
|
5484
|
-
.session-item.stale .session-time {
|
|
5485
|
-
color: var(--text-muted);
|
|
5486
|
-
}
|
|
5487
|
-
.session-item.stale .session-time::before {
|
|
5488
|
-
background: transparent;
|
|
5489
|
-
border: 1px solid var(--text-muted);
|
|
5490
|
-
}
|
|
5491
5507
|
/* #endregion */
|
|
5492
5508
|
|
|
5493
5509
|
/* #region SESSION_GROUPS */
|
package/server.js
CHANGED
|
@@ -2007,16 +2007,16 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
2007
2007
|
// (same pattern as agentsNeedingModel below). Each field shares the same resolve flow:
|
|
2008
2008
|
// look up in progressMap by agentId, fall back to per-field extractor, persist only
|
|
2009
2009
|
// on actual change.
|
|
2010
|
+
// progressMap is keyed by tool_use_id; re-key by agentId, first value per field wins.
|
|
2010
2011
|
const byAgentId = {};
|
|
2011
|
-
const nameByAgentId = {};
|
|
2012
|
-
const descByAgentId = {};
|
|
2013
2012
|
if (meta.jsonlPath) {
|
|
2014
2013
|
try {
|
|
2015
2014
|
const progressMap = getProgressMap(meta.jsonlPath);
|
|
2016
2015
|
for (const entry of Object.values(progressMap)) {
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2016
|
+
const e = (byAgentId[entry.agentId] ||= {});
|
|
2017
|
+
for (const f of ['prompt', 'name', 'description', 'usage']) {
|
|
2018
|
+
if (entry[f] && !e[f]) e[f] = entry[f];
|
|
2019
|
+
}
|
|
2020
2020
|
}
|
|
2021
2021
|
} catch (_) {}
|
|
2022
2022
|
}
|
|
@@ -2025,12 +2025,12 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
2025
2025
|
field: 'prompt',
|
|
2026
2026
|
flag: 'promptUnavailable',
|
|
2027
2027
|
lookup: (a) => {
|
|
2028
|
-
if (byAgentId[a.agentId]) return byAgentId[a.agentId];
|
|
2028
|
+
if (byAgentId[a.agentId]?.prompt) return byAgentId[a.agentId].prompt;
|
|
2029
2029
|
try { return extractPromptFromTranscript(subagentJsonlForExtraction(meta, a.agentId)); } catch (_) { return null; }
|
|
2030
2030
|
},
|
|
2031
2031
|
},
|
|
2032
|
-
{ field: 'agentName', flag: 'agentNameUnavailable', lookup: (a) =>
|
|
2033
|
-
{ field: 'description', flag: 'descriptionUnavailable', lookup: (a) =>
|
|
2032
|
+
{ field: 'agentName', flag: 'agentNameUnavailable', lookup: (a) => byAgentId[a.agentId]?.name || null },
|
|
2033
|
+
{ field: 'description', flag: 'descriptionUnavailable', lookup: (a) => byAgentId[a.agentId]?.description || null },
|
|
2034
2034
|
];
|
|
2035
2035
|
if (meta.jsonlPath) {
|
|
2036
2036
|
for (const { field, flag, lookup } of reconcileFields) {
|
|
@@ -2133,6 +2133,11 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
2133
2133
|
}
|
|
2134
2134
|
|
|
2135
2135
|
const waitingForUser = checkWaitingForUser(agentDir, logMtime);
|
|
2136
|
+
// Attached after persistAgent so it stays response-only.
|
|
2137
|
+
for (const agent of visibleAgents) {
|
|
2138
|
+
const usage = byAgentId[agent.agentId]?.usage;
|
|
2139
|
+
if (usage) agent.usage = usage;
|
|
2140
|
+
}
|
|
2136
2141
|
res.json({ agents: visibleAgents, waitingForUser, teamColors });
|
|
2137
2142
|
} catch {
|
|
2138
2143
|
res.json({ agents: [], waitingForUser: null });
|
|
@@ -2499,7 +2504,6 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
2499
2504
|
msg.agentId = entry.agentId;
|
|
2500
2505
|
if (entry.description) msg.agentDescription = entry.description;
|
|
2501
2506
|
if (entry.usageText) msg.agentUsage = entry.usageText;
|
|
2502
|
-
if (entry.usage) msg.agentUsageStats = entry.usage;
|
|
2503
2507
|
if (entry.prompt && !msg.agentPrompt) msg.agentPrompt = entry.prompt;
|
|
2504
2508
|
try {
|
|
2505
2509
|
const agentFile = path.join(agentDir, `${entry.agentId}.jsonl`);
|