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 +103 -10
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/scripts/agent-spy.sh +1 -1
- package/public/app.js +329 -47
- package/public/index.html +19 -0
- package/public/style.css +235 -4
- package/server.js +334 -87
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 (
|
|
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.
|
|
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
|
-
//
|
|
660
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
1070
|
+
if (b.type === 'text' && b.text) return b.text;
|
|
978
1071
|
}
|
|
979
1072
|
}
|
|
980
1073
|
}
|
package/package.json
CHANGED
|
@@ -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)
|
|
61
|
+
toolInput: ((.tool_input | tostring) // ""),
|
|
62
62
|
timestamp: $ts
|
|
63
63
|
}' > "$DIR/_waiting.json"
|
|
64
64
|
exit 0
|