claude-code-kanban 1.19.0 → 2.0.0

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/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
  [![license](https://img.shields.io/npm/l/claude-code-kanban)](LICENSE)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/claude-code-kanban)](https://www.npmjs.com/package/claude-code-kanban)
6
6
 
7
- > Watch Claude Code think, in real time.
7
+ **[Live Demo & Docs](https://nikiforovall.blog/claude-code-kanban/)**
8
+
9
+ > Watch Claude Code work, in real time.
8
10
 
9
11
  ![Dark mode](assets/screenshot-dark-v2.png)
10
12
 
package/lib/parsers.js CHANGED
@@ -112,11 +112,28 @@ function parseJsonlLine(line) {
112
112
  return base;
113
113
  }
114
114
 
115
+ const TOOL_RESULT_MAX = 1500;
116
+
115
117
  // Cache: jsonlPath -> { scannedUpTo, customTitle }
116
118
  // Only re-scan the new bytes appended since last scan
117
119
  const customTitleCache = new Map();
118
120
  const CUSTOM_TITLE_SCAN_SIZE = 1048576; // 1MB max scan on first read
119
121
 
122
+ function extractCustomTitleFromText(text) {
123
+ if (!text.includes('"custom-title"')) return null;
124
+ const lines = text.split('\n');
125
+ for (let i = lines.length - 1; i >= 0; i--) {
126
+ if (!lines[i].includes('"custom-title"')) continue;
127
+ try {
128
+ const data = JSON.parse(lines[i]);
129
+ if (data.type === 'custom-title' && data.customTitle && !data.customTitle.startsWith('<')) {
130
+ return data.customTitle;
131
+ }
132
+ } catch (e) {}
133
+ }
134
+ return null;
135
+ }
136
+
120
137
  function readCustomTitle(jsonlPath, existingStat) {
121
138
  try {
122
139
  const stat = existingStat || statSync(jsonlPath);
@@ -127,27 +144,21 @@ function readCustomTitle(jsonlPath, existingStat) {
127
144
  let customTitle = cached?.customTitle || null;
128
145
  const fd = fs.openSync(jsonlPath, 'r');
129
146
 
130
- // On first scan, read last 256KB; on subsequent, only read new bytes
131
- const scanStart = cached
132
- ? cached.scannedUpTo
133
- : Math.max(0, stat.size - CUSTOM_TITLE_SCAN_SIZE);
134
- const len = stat.size - scanStart;
135
- if (len > 0) {
136
- const buf = Buffer.alloc(len);
137
- fs.readSync(fd, buf, 0, len, scanStart);
138
- const text = buf.toString('utf8');
139
- if (text.includes('"custom-title"')) {
140
- const lines = text.split('\n');
141
- for (let i = lines.length - 1; i >= 0; i--) {
142
- if (!lines[i].includes('"custom-title"')) continue;
143
- try {
144
- const data = JSON.parse(lines[i]);
145
- if (data.type === 'custom-title' && data.customTitle) {
146
- customTitle = data.customTitle;
147
- break;
148
- }
149
- } catch (e) {}
150
- }
147
+ if (cached) {
148
+ const len = stat.size - cached.scannedUpTo;
149
+ if (len > 0) {
150
+ const buf = Buffer.alloc(len);
151
+ fs.readSync(fd, buf, 0, len, cached.scannedUpTo);
152
+ customTitle = extractCustomTitleFromText(buf.toString('utf8')) || customTitle;
153
+ }
154
+ } else {
155
+ const CHUNK = CUSTOM_TITLE_SCAN_SIZE;
156
+ for (let offset = 0; offset < stat.size; offset += CHUNK) {
157
+ const len = Math.min(CHUNK, stat.size - offset);
158
+ const buf = Buffer.alloc(len);
159
+ fs.readSync(fd, buf, 0, len, offset);
160
+ const found = extractCustomTitleFromText(buf.toString('utf8'));
161
+ if (found) customTitle = found;
151
162
  }
152
163
  }
153
164
 
@@ -203,12 +214,28 @@ function readSessionInfoFromJsonl(jsonlPath) {
203
214
  return result;
204
215
  }
205
216
 
217
+ function getSystemMessageLabel(text) {
218
+ const taskMatch = text.match(/<summary>([^<]+)<\/summary>/);
219
+ if (taskMatch) return taskMatch[1].trim();
220
+ if (text.includes('<task-notification>')) {
221
+ const statusMatch = text.match(/<status>([^<]+)<\/status>/);
222
+ return statusMatch ? `Background task ${statusMatch[1]}` : 'Background task notification';
223
+ }
224
+ if (text.includes('<local-command-stdout>') && text.includes('Compacted')) return 'Compacted';
225
+ if (text.includes('<local-command-stdout>')) return 'Command output';
226
+ if (text.includes('<local-command-caveat>')) return 'System notification';
227
+ if (text.includes('.output completed') && text.includes('Background command')) return 'Background task completed';
228
+ if (text.startsWith('This session is being continued from a previous conversation')) return '__skip__';
229
+ return null;
230
+ }
231
+
206
232
  function readRecentMessages(jsonlPath, limit = 10) {
207
233
  let fd;
208
234
  try {
209
235
  const stat = statSync(jsonlPath);
210
236
  fd = require('fs').openSync(jsonlPath, 'r');
211
237
  const messages = [];
238
+ const toolResults = new Map();
212
239
  let readSize = Math.min(65536, stat.size);
213
240
 
214
241
  while (messages.length < limit && readSize <= stat.size) {
@@ -222,6 +249,7 @@ function readRecentMessages(jsonlPath, limit = 10) {
222
249
  const clean = firstNewline >= 0 ? text.substring(firstNewline + 1) : text;
223
250
 
224
251
  messages.length = 0;
252
+ toolResults.clear();
225
253
  for (const line of clean.split('\n')) {
226
254
  if (!line.trim()) continue;
227
255
  try {
@@ -261,16 +289,73 @@ function readRecentMessages(jsonlPath, limit = 10) {
261
289
  }
262
290
  else if (inp.description) { detail = inp.description; fullDetail = inp.description; }
263
291
  }
292
+ const params = {};
293
+ if (inp) {
294
+ if (block.name === 'Edit') {
295
+ if (inp.old_string) params.old_string = inp.old_string;
296
+ if (inp.new_string) params.new_string = inp.new_string;
297
+ if (inp.replace_all) params.replace_all = true;
298
+ } else if (block.name === 'Write') {
299
+ if (inp.content) params.content = inp.content.length > TOOL_RESULT_MAX ? inp.content.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)' : inp.content;
300
+ } else if (block.name === 'Grep') {
301
+ if (inp.path) params.path = inp.path;
302
+ if (inp.glob) params.glob = inp.glob;
303
+ if (inp.type) params.type = inp.type;
304
+ if (inp.output_mode) params.output_mode = inp.output_mode;
305
+ if (inp['-i']) params.case_insensitive = true;
306
+ if (inp['-A']) params.after = inp['-A'];
307
+ if (inp['-B']) params.before = inp['-B'];
308
+ if (inp['-C'] || inp.context) params.context = inp['-C'] || inp.context;
309
+ if (inp.multiline) params.multiline = true;
310
+ if (inp.head_limit) params.head_limit = inp.head_limit;
311
+ } else if (block.name === 'Glob') {
312
+ if (inp.path) params.path = inp.path;
313
+ } else if (block.name === 'Bash') {
314
+ if (inp.timeout) params.timeout = inp.timeout;
315
+ if (inp.run_in_background) params.background = true;
316
+ } else if (block.name === 'Read') {
317
+ if (inp.offset) params.offset = inp.offset;
318
+ if (inp.limit) params.limit = inp.limit;
319
+ if (inp.pages) params.pages = inp.pages;
320
+ } else if (block.name === 'WebFetch') {
321
+ if (inp.prompt) params.prompt = inp.prompt;
322
+ } else if (block.name === 'WebSearch') {
323
+ if (inp.max_results) params.max_results = inp.max_results;
324
+ if (inp.allowed_domains) params.allowed_domains = inp.allowed_domains.join(', ');
325
+ if (inp.blocked_domains) params.blocked_domains = inp.blocked_domains.join(', ');
326
+ } else if (block.name === 'LSP') {
327
+ if (inp.operation) params.operation = inp.operation;
328
+ if (inp.filePath) params.filePath = inp.filePath;
329
+ if (inp.line != null) params.line = inp.line;
330
+ if (inp.character != null) params.character = inp.character;
331
+ } else if (block.name === 'ToolSearch') {
332
+ if (inp.max_results) params.max_results = inp.max_results;
333
+ } else if (block.name === 'TaskCreate') {
334
+ if (inp.description) params.description = inp.description;
335
+ } else if (block.name === 'TaskUpdate') {
336
+ if (inp.taskId) params.taskId = inp.taskId;
337
+ if (inp.status) params.status = inp.status;
338
+ } else if (block.name === 'NotebookEdit') {
339
+ if (inp.command) params.command = inp.command;
340
+ if (inp.cell_type) params.cell_type = inp.cell_type;
341
+ } else if (block.name === 'Agent') {
342
+ if (inp.mode) params.mode = inp.mode;
343
+ if (inp.model) params.model = inp.model;
344
+ if (inp.run_in_background) params.background = true;
345
+ if (inp.isolation) params.isolation = inp.isolation;
346
+ }
347
+ }
264
348
  const msg = {
265
349
  type: 'tool_use',
266
350
  tool: block.name,
267
351
  detail,
268
352
  fullDetail: fullDetail !== detail ? fullDetail : null,
269
353
  description: inp?.description || null,
354
+ params: Object.keys(params).length > 0 ? params : null,
270
355
  timestamp: obj.timestamp
271
356
  };
357
+ if (block.id) msg.toolUseId = block.id;
272
358
  if (block.name === 'Agent') {
273
- if (block.id) msg.toolUseId = block.id;
274
359
  if (inp) {
275
360
  msg.agentType = inp.subagent_type || null;
276
361
  if (inp.prompt) msg.agentPrompt = inp.prompt;
@@ -282,13 +367,33 @@ function readRecentMessages(jsonlPath, limit = 10) {
282
367
  } else if (obj.type === 'user' && obj.message?.role === 'user' && !obj.isMeta) {
283
368
  if (typeof obj.message.content === 'string') {
284
369
  const t = obj.message.content;
370
+ const sysLabel = getSystemMessageLabel(t);
371
+ if (sysLabel === '__skip__') continue;
285
372
  const uTruncated = t.length > 500;
286
373
  messages.push({
287
374
  type: 'user',
288
375
  text: uTruncated ? t.slice(0, 500) + '...' : t,
289
376
  fullText: uTruncated ? t : null,
290
- timestamp: obj.timestamp
377
+ timestamp: obj.timestamp,
378
+ ...(sysLabel && { systemLabel: sysLabel })
291
379
  });
380
+ } else if (Array.isArray(obj.message.content)) {
381
+ for (const block of obj.message.content) {
382
+ if (block.type === 'tool_result' && block.tool_use_id) {
383
+ let resultText = '';
384
+ if (typeof block.content === 'string') {
385
+ resultText = block.content;
386
+ } else if (Array.isArray(block.content)) {
387
+ resultText = block.content
388
+ .filter(c => c.type === 'text' && c.text)
389
+ .map(c => c.text)
390
+ .join('\n');
391
+ }
392
+ if (resultText) {
393
+ toolResults.set(block.tool_use_id, resultText);
394
+ }
395
+ }
396
+ }
292
397
  }
293
398
  }
294
399
  } catch (e) { /* partial line */ }
@@ -298,8 +403,20 @@ function readRecentMessages(jsonlPath, limit = 10) {
298
403
  readSize *= 4;
299
404
  }
300
405
 
406
+ // Attach tool results to their corresponding tool_use messages
407
+ for (const msg of messages) {
408
+ if (msg.type === 'tool_use' && msg.toolUseId && toolResults.has(msg.toolUseId)) {
409
+ const full = toolResults.get(msg.toolUseId);
410
+ const truncated = full.length > TOOL_RESULT_MAX;
411
+ msg.toolResult = truncated ? full.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)' : full;
412
+ msg.toolResultTruncated = truncated;
413
+ if (truncated) msg.toolResultFull = full;
414
+ }
415
+ }
416
+
301
417
  require('fs').closeSync(fd);
302
418
  fd = null;
419
+ messages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
303
420
  return messages.slice(-limit);
304
421
  } catch (e) {
305
422
  if (fd) try { require('fs').closeSync(fd); } catch (_) {}
@@ -343,6 +460,44 @@ function buildAgentProgressMap(jsonlPath) {
343
460
  return map;
344
461
  }
345
462
 
463
+ function readCompactSummaries(jsonlPath) {
464
+ const results = [];
465
+ try {
466
+ const subagentsDir = path.join(path.dirname(jsonlPath), path.basename(jsonlPath, '.jsonl'), 'subagents');
467
+ const files = readdirSync(subagentsDir).filter(f => f.startsWith('agent-acompact-') && f.endsWith('.jsonl'));
468
+ for (const file of files) {
469
+ const filePath = path.join(subagentsDir, file);
470
+ const content = readFileSync(filePath, 'utf8');
471
+ const lines = content.split('\n');
472
+ // Use last entry timestamp (closest to when compaction completed)
473
+ let lastTs;
474
+ for (let i = lines.length - 1; i >= 0; i--) {
475
+ if (!lines[i].trim()) continue;
476
+ try { lastTs = JSON.parse(lines[i]).timestamp; if (lastTs) break; } catch (_) {}
477
+ }
478
+ if (!lastTs) continue;
479
+ // Find the last assistant message with a <summary> tag
480
+ for (let i = lines.length - 1; i >= 0; i--) {
481
+ if (!lines[i].trim()) continue;
482
+ try {
483
+ const obj = JSON.parse(lines[i]);
484
+ if (obj.type !== 'assistant') continue;
485
+ const blocks = obj.message?.content;
486
+ if (!Array.isArray(blocks)) continue;
487
+ let found = false;
488
+ for (const b of blocks) {
489
+ if (b.type !== 'text' || !b.text) continue;
490
+ const match = b.text.match(/<summary>([\s\S]*?)(?:<\/summary>|$)/);
491
+ if (match) { results.push({ timestamp: lastTs, summary: match[1].trim() }); found = true; break; }
492
+ }
493
+ if (found) break;
494
+ } catch (_) {}
495
+ }
496
+ }
497
+ } catch (_) {}
498
+ return results.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
499
+ }
500
+
346
501
  module.exports = {
347
502
  parseTask,
348
503
  parseAgent,
@@ -352,5 +507,6 @@ module.exports = {
352
507
  parseJsonlLine,
353
508
  readSessionInfoFromJsonl,
354
509
  readRecentMessages,
355
- buildAgentProgressMap
510
+ buildAgentProgressMap,
511
+ readCompactSummaries
356
512
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "1.19.0",
3
+ "version": "2.0.0",
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": {