claude-code-kanban 4.23.1 → 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/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
- (line.includes('<status>killed</status>') || line.includes('<status>error</status>'))) {
1176
- const idMatch = taskIdRe.exec(line);
1177
- if (idMatch) killedAgentIds.add(idMatch[1]);
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
- // (totalTokens/totalToolUseCount/totalDurationMs) — the same numbers a
1185
- // background agent surfaces via its <task-notification> <usage> block. Keep
1186
- // the raw numbers (for the agent modal's per-stat chips) and a formatted
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: hasUsage ? usage : null, usageText };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "4.23.1",
3
+ "version": "4.23.2",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -2883,10 +2883,8 @@ function showAgentModal(agentId) {
2883
2883
  return `<span class="agent-chip${cls}"${style}${title}>${labelHtml}<span class="agent-chip-val">${value}</span></span>`;
2884
2884
  };
2885
2885
 
2886
- // Agent cost stats live on the launch row's message (resolved from the completion
2887
- // toolUseResult), not on the agent-activity record — so read them from there.
2888
2886
  const agentMsg = currentMessages.find((m) => m.tool === 'Agent' && m.agentId === agentId);
2889
- const usageStats = agentMsg?.agentUsageStats || null;
2887
+ const usageStats = agent.usage || null;
2890
2888
  const fmtTok = fmtTokens;
2891
2889
 
2892
2890
  const chips = [];
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
- if (entry.prompt && !byAgentId[entry.agentId]) byAgentId[entry.agentId] = entry.prompt;
2018
- if (entry.name && !nameByAgentId[entry.agentId]) nameByAgentId[entry.agentId] = entry.name;
2019
- if (entry.description && !descByAgentId[entry.agentId]) descByAgentId[entry.agentId] = entry.description;
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) => nameByAgentId[a.agentId] || null },
2033
- { field: 'description', flag: 'descriptionUnavailable', lookup: (a) => descByAgentId[a.agentId] || null },
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`);