claude-code-kanban 4.3.0 → 4.5.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/lib/parsers.js CHANGED
@@ -222,12 +222,29 @@ function scrapeScalarFromBlob(blob, re) {
222
222
  const sessionInfoCache = new Map();
223
223
  const SESSION_INFO_CACHE_MAX = 2000;
224
224
 
225
+ // Session-scoped /goal: a goal_status attachment carries the active condition
226
+ // (latest-wins as the Stop hook re-evaluates). Only an unmet goal is "active" —
227
+ // a met goal auto-clears in Claude Code, so treat met as removal, same as a
228
+ // `/goal clear` command line. Returns the goal value to apply, or `undefined`
229
+ // when the line is not a goal event (caller keeps its current value).
230
+ function extractGoalFromLine(data) {
231
+ if (data.type === 'attachment' && data.attachment?.type === 'goal_status') {
232
+ return data.attachment.met ? null : { condition: data.attachment.condition || '' };
233
+ }
234
+ if (data.type === 'user' && typeof data.message?.content === 'string'
235
+ && data.message.content.includes('<command-name>/goal</command-name>')
236
+ && /<command-args>\s*clear/.test(data.message.content)) {
237
+ return null;
238
+ }
239
+ return undefined;
240
+ }
241
+
225
242
  // gitBranch in the JSONL is pinned to the launch-time repo by Claude Code
226
243
  // and goes stale once cwd shifts (Bash `cd`, submodule). Callers needing the
227
244
  // live branch must resolve it from cwd separately. Cache is reset on inode
228
245
  // change or truncation (size < scannedUpTo).
229
246
  function readSessionInfoFromJsonl(jsonlPath) {
230
- const result = { slug: null, projectPath: null, cwd: null, gitBranch: null, customTitle: null };
247
+ const result = { slug: null, projectPath: null, cwd: null, gitBranch: null, customTitle: null, logicalParentUuid: null, goal: null };
231
248
  let stat;
232
249
  let fd;
233
250
  try {
@@ -247,6 +264,8 @@ function readSessionInfoFromJsonl(jsonlPath) {
247
264
  projectPath: cached.projectPath,
248
265
  cwd: cached.cwd,
249
266
  gitBranch: cached.gitBranch,
267
+ logicalParentUuid: cached.logicalParentUuid || null,
268
+ goal: cached.goal || null,
250
269
  customTitle: readCustomTitle(jsonlPath, stat)
251
270
  };
252
271
  }
@@ -256,6 +275,8 @@ function readSessionInfoFromJsonl(jsonlPath) {
256
275
  result.projectPath = cached.projectPath;
257
276
  result.cwd = cached.cwd;
258
277
  result.gitBranch = cached.gitBranch;
278
+ result.logicalParentUuid = cached.logicalParentUuid || null;
279
+ result.goal = cached.goal || null;
259
280
  }
260
281
 
261
282
  let lastCwdSeen = result.cwd;
@@ -268,6 +289,12 @@ function readSessionInfoFromJsonl(jsonlPath) {
268
289
  lastCwdSeen = data.cwd;
269
290
  }
270
291
  if (data.gitBranch) result.gitBranch = data.gitBranch;
292
+ if (data.subtype === 'compact_boundary' && data.logicalParentUuid && !result.logicalParentUuid) {
293
+ result.logicalParentUuid = data.logicalParentUuid;
294
+ }
295
+ // Forward → last wins.
296
+ const goalUpdate = extractGoalFromLine(data);
297
+ if (goalUpdate !== undefined) result.goal = goalUpdate;
271
298
  } catch (e) {}
272
299
  };
273
300
 
@@ -332,6 +359,9 @@ function readSessionInfoFromJsonl(jsonlPath) {
332
359
  const tn = fs.readSync(fd, tailBuf, 0, TAIL_SIZE, tailStart);
333
360
  const lines = tailBuf.toString('utf8', 0, tn).split('\n');
334
361
  let latestTailCwd = null;
362
+ // undefined = no goal event seen in tail (keep head's); otherwise the
363
+ // most recent tail goal/clear supersedes head (tail bytes are later).
364
+ let tailGoal;
335
365
  for (let i = lines.length - 1; i >= 0; i--) {
336
366
  try {
337
367
  const data = JSON.parse(lines[i]);
@@ -339,10 +369,12 @@ function readSessionInfoFromJsonl(jsonlPath) {
339
369
  if (!result.projectPath && data.cwd) result.projectPath = data.cwd;
340
370
  if (!result.gitBranch && data.gitBranch) result.gitBranch = data.gitBranch;
341
371
  if (!latestTailCwd && data.cwd) latestTailCwd = data.cwd;
342
- if (latestTailCwd && result.slug && result.projectPath && result.gitBranch) break;
372
+ if (tailGoal === undefined) tailGoal = extractGoalFromLine(data);
373
+ if (latestTailCwd && tailGoal !== undefined && result.slug && result.projectPath && result.gitBranch) break;
343
374
  } catch (e) {}
344
375
  }
345
376
  if (latestTailCwd) lastCwdSeen = latestTailCwd;
377
+ if (tailGoal !== undefined) result.goal = tailGoal;
346
378
  }
347
379
  scannedUpTo = stat.size;
348
380
  }
@@ -361,7 +393,9 @@ function readSessionInfoFromJsonl(jsonlPath) {
361
393
  slug: result.slug,
362
394
  projectPath: result.projectPath,
363
395
  gitBranch: result.gitBranch,
364
- cwd: result.cwd
396
+ cwd: result.cwd,
397
+ logicalParentUuid: result.logicalParentUuid,
398
+ goal: result.goal
365
399
  });
366
400
  if (sessionInfoCache.size > SESSION_INFO_CACHE_MAX) {
367
401
  const firstKey = sessionInfoCache.keys().next().value;
@@ -396,6 +430,7 @@ function readRecentMessages(jsonlPath, limit = 10) {
396
430
  fd = require('fs').openSync(jsonlPath, 'r');
397
431
  const messages = [];
398
432
  const toolResults = new Map();
433
+ const toolResultExtras = new Map();
399
434
  let readSize = Math.min(65536, stat.size);
400
435
 
401
436
  while (messages.length < limit) {
@@ -411,6 +446,7 @@ function readRecentMessages(jsonlPath, limit = 10) {
411
446
 
412
447
  messages.length = 0;
413
448
  toolResults.clear();
449
+ toolResultExtras.clear();
414
450
  for (const line of clean.split('\n')) {
415
451
  if (!line.trim()) continue;
416
452
  try {
@@ -472,11 +508,20 @@ function readRecentMessages(jsonlPath, limit = 10) {
472
508
  const params = {};
473
509
  if (inp) {
474
510
  if (block.name === 'Edit') {
511
+ if (inp.file_path) params.file_path = inp.file_path;
475
512
  if (inp.old_string) params.old_string = inp.old_string;
476
513
  if (inp.new_string) params.new_string = inp.new_string;
477
514
  if (inp.replace_all) params.replace_all = true;
478
515
  } else if (block.name === 'Write') {
479
- if (inp.content) params.content = inp.content.length > TOOL_RESULT_MAX ? inp.content.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)' : inp.content;
516
+ if (inp.file_path) params.file_path = inp.file_path;
517
+ if (inp.content) {
518
+ if (inp.content.length > TOOL_RESULT_MAX) {
519
+ params.content = inp.content.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)';
520
+ params.contentFull = inp.content;
521
+ } else {
522
+ params.content = inp.content;
523
+ }
524
+ }
480
525
  } else if (block.name === 'Grep') {
481
526
  if (inp.path) params.path = inp.path;
482
527
  if (inp.glob) params.glob = inp.glob;
@@ -532,6 +577,19 @@ function readRecentMessages(jsonlPath, limit = 10) {
532
577
  if (inp.message && typeof inp.message === 'object') {
533
578
  params.protocol = inp.message;
534
579
  }
580
+ } else {
581
+ // Passthrough for unknown tools (e.g. MCP `mcp__...`) so the detail panel
582
+ // can render args instead of "No details". Truncate large strings to bound
583
+ // wire/cache size, matching the Write `content` cap above.
584
+ for (const [k, v] of Object.entries(inp)) {
585
+ if (k === 'description' || v == null) continue;
586
+ if (typeof v === 'string' && v.length > TOOL_RESULT_MAX) {
587
+ params[k] = v.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)';
588
+ params[k + 'Full'] = v;
589
+ } else {
590
+ params[k] = v;
591
+ }
592
+ }
535
593
  }
536
594
  }
537
595
  const msg = {
@@ -628,9 +686,25 @@ function readRecentMessages(jsonlPath, limit = 10) {
628
686
  if (resultText) {
629
687
  toolResults.set(block.tool_use_id, resultText);
630
688
  }
689
+ // AskUserQuestion (and similar) stash the structured
690
+ // {questions, answers} payload at the line-level
691
+ // obj.toolUseResult — the block.content string is just a
692
+ // short confirmation. Capture it so the renderer can show
693
+ // the actual answers + option descriptions.
694
+ const tur = obj.toolUseResult;
695
+ let answerPayload = null;
696
+ if (tur && typeof tur === 'object' && !Array.isArray(tur)
697
+ && tur.answers && typeof tur.answers === 'object') {
698
+ answerPayload = {
699
+ answers: tur.answers,
700
+ questions: Array.isArray(tur.questions) ? tur.questions : null,
701
+ };
702
+ toolResultExtras.set(block.tool_use_id, { answerPayload });
703
+ }
631
704
  toolResultRefs.push({
632
705
  toolUseId: block.tool_use_id,
633
- preview: resultText ? resultText.slice(0, 200) : ''
706
+ preview: resultText ? resultText.slice(0, 200) : '',
707
+ answerPayload,
634
708
  });
635
709
  }
636
710
  });
@@ -656,15 +730,27 @@ function readRecentMessages(jsonlPath, limit = 10) {
656
730
  }
657
731
 
658
732
  // Attach tool results to their corresponding tool_use messages.
659
- // For perf, we never ship the full text in the messages payload — when
660
- // truncated, the client lazy-fetches via /api/sessions/:id/tool-result/:toolUseId.
733
+ // When truncated, ship the full text inline as toolResultFull so the
734
+ // modal expand toggle is instant. The lazy fetch at
735
+ // /api/sessions/:id/tool-result/:toolUseId remains a fallback for older
736
+ // cached payloads that may lack toolResultFull.
661
737
  for (const msg of messages) {
662
738
  if (msg.type === 'tool_use' && msg.toolUseId && toolResults.has(msg.toolUseId)) {
663
739
  const full = toolResults.get(msg.toolUseId);
664
740
  const truncated = full.length > TOOL_RESULT_MAX;
665
- msg.toolResult = truncated ? full.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)' : full;
741
+ if (truncated) {
742
+ msg.toolResult = full.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)';
743
+ msg.toolResultFull = full;
744
+ } else {
745
+ msg.toolResult = full;
746
+ }
666
747
  msg.toolResultTruncated = truncated;
667
748
  }
749
+ if (msg.type === 'tool_use' && msg.tool === 'AskUserQuestion'
750
+ && msg.toolUseId && toolResultExtras.has(msg.toolUseId)) {
751
+ const extra = toolResultExtras.get(msg.toolUseId);
752
+ if (extra.answerPayload) msg.answerPayload = extra.answerPayload;
753
+ }
668
754
  }
669
755
 
670
756
  require('fs').closeSync(fd);
@@ -872,6 +958,13 @@ function buildSessionDigest(jsonlPath) {
872
958
  for (const [key, entry] of Object.entries(map)) {
873
959
  if (nameByToolUseId[key]) entry.name = nameByToolUseId[key];
874
960
  if (descByToolUseId[key]) entry.description = descByToolUseId[key];
961
+ // Prefer the full prompt from the assistant's tool_use input — Claude Code's
962
+ // agent_progress system messages embed a truncated prompt that ends mid-sentence.
963
+ // Only override when entry already had a prompt (i.e. agent_progress path);
964
+ // bg/teammate paths intentionally keep prompt null per existing contract.
965
+ if (entry.prompt && promptByToolUseId[key] && promptByToolUseId[key].length > entry.prompt.length) {
966
+ entry.prompt = promptByToolUseId[key];
967
+ }
875
968
  }
876
969
  } catch (_) {}
877
970
  const rejectedAgentIds = new Set();
@@ -971,10 +1064,10 @@ function extractPromptFromTranscript(jsonlPath) {
971
1064
  const obj = JSON.parse(firstLine);
972
1065
  if (obj.type === 'user') {
973
1066
  const content = obj.message?.content;
974
- if (typeof content === 'string') return content.slice(0, 500);
1067
+ if (typeof content === 'string') return content;
975
1068
  if (Array.isArray(content)) {
976
1069
  for (const b of content) {
977
- if (b.type === 'text' && b.text) return b.text.slice(0, 500);
1070
+ if (b.type === 'text' && b.text) return b.text;
978
1071
  }
979
1072
  }
980
1073
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "4.3.0",
3
+ "version": "4.5.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": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Agent activity tracking for claude-code-kanban dashboard"
5
5
  }
@@ -58,7 +58,7 @@ if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && { [ "$
58
58
  status: "waiting",
59
59
  kind: $kind,
60
60
  toolName: (.tool_name // "unknown"),
61
- toolInput: ((.tool_input | tostring)[0:200] // ""),
61
+ toolInput: ((.tool_input | tostring) // ""),
62
62
  timestamp: $ts
63
63
  }' > "$DIR/_waiting.json"
64
64
  exit 0