claude-code-kanban 2.1.0 → 2.2.0-rc.1
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/hooks/agent-spy.sh +18 -0
- package/install.js +1 -0
- package/package.json +1 -1
- package/public/app.js +4 -0
- package/public/style.css +7 -0
- package/server.js +116 -10
package/hooks/agent-spy.sh
CHANGED
|
@@ -16,6 +16,24 @@ eval "$(echo "$INPUT" | jq -r '
|
|
|
16
16
|
|
|
17
17
|
[ -z "$SESSION_ID" ] && exit 0
|
|
18
18
|
|
|
19
|
+
# Map session to custom task list on session start
|
|
20
|
+
if [ "$EVENT" = "SessionStart" ]; then
|
|
21
|
+
TASK_LIST_ID="${CLAUDE_CODE_TASK_LIST_ID:-}"
|
|
22
|
+
if [ -n "$TASK_LIST_ID" ]; then
|
|
23
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
24
|
+
MAPS_DIR="$HOME/.claude/agent-activity/_task-maps"
|
|
25
|
+
mkdir -p "$MAPS_DIR"
|
|
26
|
+
MAP_FILE="$MAPS_DIR/$TASK_LIST_ID.json"
|
|
27
|
+
TMP_FILE="$MAP_FILE.$$"
|
|
28
|
+
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
29
|
+
EXISTING="{}"
|
|
30
|
+
[ -f "$MAP_FILE" ] && EXISTING=$(cat "$MAP_FILE")
|
|
31
|
+
echo "$EXISTING" | jq -c --arg sid "$SESSION_ID" --arg cwd "$CWD" --arg ts "$TS" \
|
|
32
|
+
'.[$sid] = {project: $cwd, updatedAt: $ts}' > "$TMP_FILE" && mv "$TMP_FILE" "$MAP_FILE"
|
|
33
|
+
fi
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
19
37
|
# PostToolUse / non-waiting PreToolUse: clear waiting state
|
|
20
38
|
if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ]; }; then
|
|
21
39
|
WFILE="$HOME/.claude/agent-activity/$SESSION_ID/_waiting.json"
|
package/install.js
CHANGED
|
@@ -17,6 +17,7 @@ const AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
|
|
|
17
17
|
|
|
18
18
|
const HOOK_COMMAND = '~/.claude/hooks/agent-spy.sh';
|
|
19
19
|
const HOOK_EVENTS = [
|
|
20
|
+
{ event: 'SessionStart' },
|
|
20
21
|
{ event: 'SubagentStart' },
|
|
21
22
|
{ event: 'SubagentStop' },
|
|
22
23
|
{ event: 'TeammateIdle' },
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1784,6 +1784,7 @@ function renderSessions() {
|
|
|
1784
1784
|
<div class="session-progress">
|
|
1785
1785
|
<span class="session-indicators">
|
|
1786
1786
|
${isTeam ? `<span class="team-badge" title="${memberCount} team members"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
|
|
1787
|
+
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>` : ''}
|
|
1787
1788
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
1788
1789
|
${session.hasPlan ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>` : ''}
|
|
1789
1790
|
${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
|
|
@@ -3804,6 +3805,9 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3804
3805
|
if (session.tasksDir) {
|
|
3805
3806
|
infoRows.push(['Tasks Dir', session.tasksDir, { openPath: session.tasksDir }]);
|
|
3806
3807
|
}
|
|
3808
|
+
if (session.sharedTaskList) {
|
|
3809
|
+
infoRows.push(['Shared Tasks', session.sharedTaskList]);
|
|
3810
|
+
}
|
|
3807
3811
|
if (teamConfig?.configPath) {
|
|
3808
3812
|
const configDir = teamConfig.configPath.replace(/[/\\][^/\\]+$/, '');
|
|
3809
3813
|
infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
|
package/public/style.css
CHANGED
|
@@ -1578,6 +1578,13 @@ body::before {
|
|
|
1578
1578
|
font-weight: 600;
|
|
1579
1579
|
}
|
|
1580
1580
|
|
|
1581
|
+
.shared-tasklist-badge {
|
|
1582
|
+
display: inline-flex;
|
|
1583
|
+
align-items: center;
|
|
1584
|
+
color: var(--accent);
|
|
1585
|
+
flex-shrink: 0;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1581
1588
|
.team-info-btn {
|
|
1582
1589
|
width: 24px;
|
|
1583
1590
|
height: 24px;
|
package/server.js
CHANGED
|
@@ -189,6 +189,9 @@ const terminatedCache = new Map();
|
|
|
189
189
|
const compactSummaryCache = new Map();
|
|
190
190
|
const taskCountsCache = new Map();
|
|
191
191
|
const contextStatusCache = new Map();
|
|
192
|
+
const TASK_MAPS_DIR = path.join(AGENT_ACTIVITY_DIR, '_task-maps');
|
|
193
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
194
|
+
function isUUID(s) { return UUID_RE.test(s); }
|
|
192
195
|
|
|
193
196
|
function evictStaleCache(cache) {
|
|
194
197
|
if (cache.size <= MAX_CACHE_ENTRIES) return;
|
|
@@ -196,6 +199,47 @@ function evictStaleCache(cache) {
|
|
|
196
199
|
if (oldest !== undefined) cache.delete(oldest);
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
let sessionToTaskListCache = null;
|
|
203
|
+
let lastTaskMapScan = 0;
|
|
204
|
+
const TASK_MAP_SCAN_TTL = 5000;
|
|
205
|
+
|
|
206
|
+
function loadAllTaskMaps() {
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
if (sessionToTaskListCache && now - lastTaskMapScan < TASK_MAP_SCAN_TTL) return sessionToTaskListCache;
|
|
209
|
+
|
|
210
|
+
const sessionToList = {};
|
|
211
|
+
const listToSessions = {};
|
|
212
|
+
if (!existsSync(TASK_MAPS_DIR)) {
|
|
213
|
+
sessionToTaskListCache = { sessionToList, listToSessions };
|
|
214
|
+
lastTaskMapScan = now;
|
|
215
|
+
return sessionToTaskListCache;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
for (const file of readdirSync(TASK_MAPS_DIR).filter(f => f.endsWith('.json'))) {
|
|
219
|
+
const taskListName = file.replace(/\.json$/, '');
|
|
220
|
+
const mapPath = path.join(TASK_MAPS_DIR, file);
|
|
221
|
+
try {
|
|
222
|
+
const map = JSON.parse(readFileSync(mapPath, 'utf8'));
|
|
223
|
+
listToSessions[taskListName] = map;
|
|
224
|
+
for (const sessionId of Object.keys(map)) {
|
|
225
|
+
sessionToList[sessionId] = taskListName;
|
|
226
|
+
}
|
|
227
|
+
} catch (e) { /* skip invalid */ }
|
|
228
|
+
}
|
|
229
|
+
} catch (e) { /* ignore */ }
|
|
230
|
+
sessionToTaskListCache = { sessionToList, listToSessions };
|
|
231
|
+
lastTaskMapScan = now;
|
|
232
|
+
return sessionToTaskListCache;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getCustomTaskDir(sessionId) {
|
|
236
|
+
const { sessionToList } = loadAllTaskMaps();
|
|
237
|
+
const taskListName = sessionToList[sessionId];
|
|
238
|
+
if (!taskListName) return null;
|
|
239
|
+
const dir = path.join(TASKS_DIR, taskListName);
|
|
240
|
+
return existsSync(dir) ? dir : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
199
243
|
function getTaskCounts(sessionPath) {
|
|
200
244
|
const cached = taskCountsCache.get(sessionPath);
|
|
201
245
|
if (cached) return cached;
|
|
@@ -449,7 +493,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
449
493
|
const entries = readdirSync(TASKS_DIR, { withFileTypes: true });
|
|
450
494
|
|
|
451
495
|
for (const entry of entries) {
|
|
452
|
-
if (entry.isDirectory()) {
|
|
496
|
+
if (entry.isDirectory() && isUUID(entry.name)) {
|
|
453
497
|
const sessionPath = path.join(TASKS_DIR, entry.name);
|
|
454
498
|
const stat = statSync(sessionPath);
|
|
455
499
|
const { taskCount, completed, inProgress, pending, newestTaskMtime } = getTaskCounts(sessionPath);
|
|
@@ -497,6 +541,44 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
497
541
|
}));
|
|
498
542
|
}
|
|
499
543
|
}
|
|
544
|
+
|
|
545
|
+
// Process custom task lists (non-UUID directories mapped via _task-maps)
|
|
546
|
+
const { listToSessions } = loadAllTaskMaps();
|
|
547
|
+
for (const [taskListName, map] of Object.entries(listToSessions)) {
|
|
548
|
+
const customTaskDir = path.join(TASKS_DIR, taskListName);
|
|
549
|
+
if (!existsSync(customTaskDir)) continue;
|
|
550
|
+
const counts = getTaskCounts(customTaskDir);
|
|
551
|
+
|
|
552
|
+
for (const [sessionId, info] of Object.entries(map)) {
|
|
553
|
+
const existing = sessionsMap.get(sessionId);
|
|
554
|
+
if (existing) {
|
|
555
|
+
Object.assign(existing, {
|
|
556
|
+
taskCount: counts.taskCount,
|
|
557
|
+
completed: counts.completed,
|
|
558
|
+
inProgress: counts.inProgress,
|
|
559
|
+
pending: counts.pending,
|
|
560
|
+
tasksDir: customTaskDir,
|
|
561
|
+
sharedTaskList: taskListName,
|
|
562
|
+
});
|
|
563
|
+
if (counts.newestTaskMtime) {
|
|
564
|
+
const taskMtime = counts.newestTaskMtime.toISOString();
|
|
565
|
+
if (taskMtime > existing.modifiedAt) existing.modifiedAt = taskMtime;
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
const meta = { ...(metadata[sessionId] || {}) };
|
|
569
|
+
if (!meta.project && info.project) meta.project = info.project;
|
|
570
|
+
sessionsMap.set(sessionId, buildSessionObject(sessionId, meta, {
|
|
571
|
+
taskCount: counts.taskCount,
|
|
572
|
+
completed: counts.completed,
|
|
573
|
+
inProgress: counts.inProgress,
|
|
574
|
+
pending: counts.pending,
|
|
575
|
+
modifiedAt: counts.newestTaskMtime ? counts.newestTaskMtime.toISOString() : (info.updatedAt || new Date(0).toISOString()),
|
|
576
|
+
tasksDir: customTaskDir,
|
|
577
|
+
sharedTaskList: taskListName,
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
500
582
|
}
|
|
501
583
|
|
|
502
584
|
// Add sessions from metadata that don't have task directories
|
|
@@ -645,7 +727,8 @@ app.get('/api/projects', (req, res) => {
|
|
|
645
727
|
// API: Get tasks for a session
|
|
646
728
|
app.get('/api/sessions/:sessionId', async (req, res) => {
|
|
647
729
|
try {
|
|
648
|
-
const
|
|
730
|
+
const customDir = getCustomTaskDir(req.params.sessionId);
|
|
731
|
+
const sessionPath = customDir || path.join(TASKS_DIR, req.params.sessionId);
|
|
649
732
|
|
|
650
733
|
if (!existsSync(sessionPath)) {
|
|
651
734
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -1200,21 +1283,44 @@ const watcher = chokidar.watch(TASKS_DIR, {
|
|
|
1200
1283
|
watcher.on('all', (event, filePath) => {
|
|
1201
1284
|
if ((event === 'add' || event === 'change' || event === 'unlink') && filePath.endsWith('.json')) {
|
|
1202
1285
|
const relativePath = path.relative(TASKS_DIR, filePath);
|
|
1203
|
-
const
|
|
1286
|
+
const dirName = relativePath.split(path.sep)[0];
|
|
1204
1287
|
|
|
1205
|
-
taskCountsCache.delete(path.join(TASKS_DIR,
|
|
1288
|
+
taskCountsCache.delete(path.join(TASKS_DIR, dirName));
|
|
1206
1289
|
|
|
1207
|
-
|
|
1208
|
-
type: 'update',
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
});
|
|
1290
|
+
if (isUUID(dirName)) {
|
|
1291
|
+
broadcast({ type: 'update', event, sessionId: dirName, file: path.basename(filePath) });
|
|
1292
|
+
} else {
|
|
1293
|
+
broadcastToMappedSessions(dirName, event, filePath);
|
|
1294
|
+
}
|
|
1213
1295
|
}
|
|
1214
1296
|
});
|
|
1215
1297
|
|
|
1298
|
+
function broadcastToMappedSessions(taskListName, event, filePath) {
|
|
1299
|
+
const { listToSessions } = loadAllTaskMaps();
|
|
1300
|
+
const map = listToSessions[taskListName];
|
|
1301
|
+
if (!map) return;
|
|
1302
|
+
for (const sid of Object.keys(map)) {
|
|
1303
|
+
broadcast({ type: 'update', event, sessionId: sid, file: path.basename(filePath) });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1216
1307
|
console.log(`Watching for changes in: ${TASKS_DIR}`);
|
|
1217
1308
|
|
|
1309
|
+
// Watch task maps directory for session→task-list mapping changes
|
|
1310
|
+
const taskMapsWatcher = chokidar.watch(TASK_MAPS_DIR, {
|
|
1311
|
+
persistent: true,
|
|
1312
|
+
ignoreInitial: true,
|
|
1313
|
+
depth: 1
|
|
1314
|
+
});
|
|
1315
|
+
taskMapsWatcher.on('all', (event, filePath) => {
|
|
1316
|
+
if ((event === 'add' || event === 'change' || event === 'unlink') && filePath.endsWith('.json')) {
|
|
1317
|
+
lastTaskMapScan = 0;
|
|
1318
|
+
const taskListName = path.basename(filePath, '.json');
|
|
1319
|
+
taskCountsCache.delete(path.join(TASKS_DIR, taskListName));
|
|
1320
|
+
broadcastToMappedSessions(taskListName, event, filePath);
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1218
1324
|
// Watch teams directory for config changes
|
|
1219
1325
|
const teamsWatcher = chokidar.watch(TEAMS_DIR, {
|
|
1220
1326
|
persistent: true,
|