claude-code-kanban 3.9.0 → 4.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/lib/parsers.js +44 -11
- 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 +13 -48
- package/public/app.js +147 -71
- package/public/index.html +5 -25
- package/public/style.css +108 -143
- package/server.js +111 -23
package/lib/parsers.js
CHANGED
|
@@ -115,6 +115,20 @@ function parseJsonlLine(line) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const TOOL_RESULT_MAX = 1500;
|
|
118
|
+
const USER_TEXT_MAX = 500;
|
|
119
|
+
const INTERRUPT_MARKER = '[Request interrupted by user]';
|
|
120
|
+
|
|
121
|
+
function pushUserMessage(messages, text, timestamp, sysLabel) {
|
|
122
|
+
if (sysLabel === '__skip__') return;
|
|
123
|
+
const truncated = text.length > USER_TEXT_MAX;
|
|
124
|
+
messages.push({
|
|
125
|
+
type: 'user',
|
|
126
|
+
text: truncated ? text.slice(0, USER_TEXT_MAX) + '...' : text,
|
|
127
|
+
fullText: truncated ? text : null,
|
|
128
|
+
timestamp,
|
|
129
|
+
...(sysLabel && { systemLabel: sysLabel })
|
|
130
|
+
});
|
|
131
|
+
}
|
|
118
132
|
|
|
119
133
|
// Cache: jsonlPath -> { scannedUpTo, customTitle }
|
|
120
134
|
// Only re-scan the new bytes appended since last scan
|
|
@@ -516,17 +530,16 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
516
530
|
});
|
|
517
531
|
continue;
|
|
518
532
|
}
|
|
519
|
-
|
|
520
|
-
if (sysLabel === '__skip__') continue;
|
|
521
|
-
const uTruncated = t.length > 500;
|
|
522
|
-
messages.push({
|
|
523
|
-
type: 'user',
|
|
524
|
-
text: uTruncated ? t.slice(0, 500) + '...' : t,
|
|
525
|
-
fullText: uTruncated ? t : null,
|
|
526
|
-
timestamp: obj.timestamp,
|
|
527
|
-
...(sysLabel && { systemLabel: sysLabel })
|
|
528
|
-
});
|
|
533
|
+
pushUserMessage(messages, t, obj.timestamp, getSystemMessageLabel(t));
|
|
529
534
|
} else if (Array.isArray(obj.message.content)) {
|
|
535
|
+
const joined = obj.message.content
|
|
536
|
+
.filter(b => b.type === 'text' && typeof b.text === 'string' && b.text)
|
|
537
|
+
.map(b => b.text)
|
|
538
|
+
.join('\n')
|
|
539
|
+
.trim();
|
|
540
|
+
if (joined && joined !== INTERRUPT_MARKER) {
|
|
541
|
+
pushUserMessage(messages, joined, obj.timestamp, getSystemMessageLabel(joined));
|
|
542
|
+
}
|
|
530
543
|
for (const block of obj.message.content) {
|
|
531
544
|
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
532
545
|
let resultText = '';
|
|
@@ -633,6 +646,9 @@ function readMessagesPage(jsonlPath, limit = 10, beforeTimestamp = null) {
|
|
|
633
646
|
function buildSessionDigest(jsonlPath) {
|
|
634
647
|
const map = {};
|
|
635
648
|
const terminated = new Map();
|
|
649
|
+
const rejectedToolUseIds = new Set();
|
|
650
|
+
const promptByToolUseId = {};
|
|
651
|
+
const killedAgentIds = new Set();
|
|
636
652
|
try {
|
|
637
653
|
const content = readFileSync(jsonlPath, 'utf8');
|
|
638
654
|
const re = /"type":"agent_progress"[^}]*"agentId":"([^"]+)"/;
|
|
@@ -642,6 +658,7 @@ function buildSessionDigest(jsonlPath) {
|
|
|
642
658
|
const bgAgentIdRe = /agentId: ([a-zA-Z0-9_@-]+)/;
|
|
643
659
|
const tmToolIdRe = /"tool_use_id":"([^"]+)"/;
|
|
644
660
|
const tmAgentIdRe = /agent_id: ([a-zA-Z0-9_@-]+)/;
|
|
661
|
+
const taskIdRe = /<task-id>([a-zA-Z0-9_-]+)<\/task-id>/;
|
|
645
662
|
const nameByToolUseId = {};
|
|
646
663
|
const descByToolUseId = {};
|
|
647
664
|
for (const line of content.split('\n')) {
|
|
@@ -708,10 +725,18 @@ function buildSessionDigest(jsonlPath) {
|
|
|
708
725
|
if (b.type === 'tool_use' && b.name === 'Agent' && b.id) {
|
|
709
726
|
if (b.input?.name) nameByToolUseId[b.id] = b.input.name;
|
|
710
727
|
if (b.input?.description) descByToolUseId[b.id] = b.input.description;
|
|
728
|
+
if (b.input?.prompt) promptByToolUseId[b.id] = b.input.prompt;
|
|
711
729
|
}
|
|
712
730
|
}
|
|
713
731
|
}
|
|
714
732
|
} catch (_) {}
|
|
733
|
+
} else if (line.includes('User rejected tool use') && line.includes('"tool_use_id"')) {
|
|
734
|
+
const m = tmToolIdRe.exec(line);
|
|
735
|
+
if (m) rejectedToolUseIds.add(m[1]);
|
|
736
|
+
} else if (line.includes('<task-notification>') &&
|
|
737
|
+
(line.includes('<status>killed</status>') || line.includes('<status>error</status>'))) {
|
|
738
|
+
const idMatch = taskIdRe.exec(line);
|
|
739
|
+
if (idMatch) killedAgentIds.add(idMatch[1]);
|
|
715
740
|
} else if (line.includes('"toolUseResult"') && line.includes('"agentId"') && line.includes('"tool_result"')) {
|
|
716
741
|
try {
|
|
717
742
|
const obj = JSON.parse(line);
|
|
@@ -734,7 +759,15 @@ function buildSessionDigest(jsonlPath) {
|
|
|
734
759
|
if (descByToolUseId[key]) entry.description = descByToolUseId[key];
|
|
735
760
|
}
|
|
736
761
|
} catch (_) {}
|
|
737
|
-
|
|
762
|
+
const rejectedAgentIds = new Set();
|
|
763
|
+
const rejectedPrompts = new Set();
|
|
764
|
+
for (const toolUseId of rejectedToolUseIds) {
|
|
765
|
+
const entry = map[toolUseId];
|
|
766
|
+
if (entry?.agentId) rejectedAgentIds.add(entry.agentId);
|
|
767
|
+
const prompt = entry?.prompt || promptByToolUseId[toolUseId];
|
|
768
|
+
if (prompt) rejectedPrompts.add(prompt);
|
|
769
|
+
}
|
|
770
|
+
return { progressMap: map, terminated, rejectedAgentIds, rejectedPrompts, killedAgentIds };
|
|
738
771
|
}
|
|
739
772
|
|
|
740
773
|
function buildAgentProgressMap(jsonlPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Tracks subagent lifecycle: one
|
|
3
|
-
# Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.
|
|
2
|
+
# Tracks subagent lifecycle: one append-only JSONL file per agent, grouped by session
|
|
3
|
+
# Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.jsonl
|
|
4
|
+
# Each line is a lifecycle event (start | idle | stop). Server folds last-line-wins.
|
|
4
5
|
|
|
5
6
|
INPUT=$(cat)
|
|
6
7
|
|
|
@@ -70,23 +71,16 @@ if [ "$EVENT" = "TeammateIdle" ] && [ -z "$AGENT_ID" ] && [ -n "$TEAMMATE_NAME"
|
|
|
70
71
|
[ ! -f "$MAP_FILE" ] && exit 0
|
|
71
72
|
AGENT_ID=$(cat "$MAP_FILE")
|
|
72
73
|
[ -z "$AGENT_ID" ] && exit 0
|
|
73
|
-
FILE="$DIR/$AGENT_ID.
|
|
74
|
+
FILE="$DIR/$AGENT_ID.jsonl"
|
|
74
75
|
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
75
|
-
|
|
76
|
-
if [ -f "$FILE" ]; then
|
|
77
|
-
PREV_START=$(jq -r '.startedAt // ""' "$FILE" 2>/dev/null)
|
|
78
|
-
[ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
|
|
79
|
-
fi
|
|
80
|
-
cat > "$FILE" <<EOF
|
|
81
|
-
{"agentId":"$AGENT_ID","type":"$TEAMMATE_NAME","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
|
|
82
|
-
EOF
|
|
76
|
+
echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$TEAMMATE_NAME\",\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
|
|
83
77
|
exit 0
|
|
84
78
|
fi
|
|
85
79
|
|
|
86
80
|
[ -z "$AGENT_ID" ] && exit 0
|
|
87
81
|
|
|
88
82
|
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
89
|
-
FILE="$DIR/$AGENT_ID.
|
|
83
|
+
FILE="$DIR/$AGENT_ID.jsonl"
|
|
90
84
|
|
|
91
85
|
# On Start: skip if no type (internal agents like AskUserQuestion)
|
|
92
86
|
# On Stop/Idle: only skip if no existing file (never tracked)
|
|
@@ -102,48 +96,19 @@ mkdir -p "$DIR"
|
|
|
102
96
|
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
103
97
|
|
|
104
98
|
if [ "$EVENT" = "SubagentStart" ]; then
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
EOF
|
|
108
|
-
# Write name→id mapping for TeammateIdle resolution
|
|
109
|
-
# Delete previous file only if not active (idle/stopped = teammate re-spawn, active = parallel subagent)
|
|
99
|
+
echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$AGENT_TYPE_RAW\",\"event\":\"start\",\"status\":\"active\",\"startedAt\":\"$TS\",\"updatedAt\":\"$TS\"}" >> "$FILE"
|
|
100
|
+
# Mapping always points at latest agent of this type (used by TeammateIdle resolution).
|
|
110
101
|
if [ -n "$AGENT_TYPE_RAW" ]; then
|
|
111
|
-
|
|
112
|
-
if [ -f "$MAP_FILE" ]; then
|
|
113
|
-
OLD_ID=$(cat "$MAP_FILE")
|
|
114
|
-
if [ -n "$OLD_ID" ] && [ "$OLD_ID" != "$AGENT_ID" ]; then
|
|
115
|
-
OLD_FILE="$DIR/$OLD_ID.json"
|
|
116
|
-
OLD_STATUS=""
|
|
117
|
-
[ -f "$OLD_FILE" ] && OLD_STATUS=$(jq -r '.status // ""' "$OLD_FILE" 2>/dev/null)
|
|
118
|
-
[ "$OLD_STATUS" != "active" ] && rm -f "$OLD_FILE"
|
|
119
|
-
fi
|
|
120
|
-
fi
|
|
121
|
-
echo -n "$AGENT_ID" > "$MAP_FILE"
|
|
102
|
+
echo -n "$AGENT_ID" > "$DIR/_name-${AGENT_TYPE_RAW}.id"
|
|
122
103
|
fi
|
|
123
104
|
|
|
124
105
|
elif [ "$EVENT" = "SubagentStop" ]; then
|
|
125
|
-
AGENT_TYPE="$AGENT_TYPE_RAW"
|
|
126
|
-
STARTED_AT="$TS"
|
|
127
|
-
if [ -f "$FILE" ]; then
|
|
128
|
-
eval "$(jq -r '@sh "PREV_TYPE=\(.type // "unknown")", @sh "PREV_START=\(.startedAt // "")"' "$FILE")"
|
|
129
|
-
[ -z "$AGENT_TYPE" ] && AGENT_TYPE="$PREV_TYPE"
|
|
130
|
-
[ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
|
|
131
|
-
fi
|
|
132
106
|
echo "$INPUT" | jq -c \
|
|
133
|
-
--arg id "$AGENT_ID" --arg type "$
|
|
134
|
-
'{agentId: $id, type: $type,
|
|
107
|
+
--arg id "$AGENT_ID" --arg type "$AGENT_TYPE_RAW" --arg ts "$TS" \
|
|
108
|
+
'{agentId: $id, type: $type, event: "stop", status: "stopped",
|
|
135
109
|
lastMessage: (.last_assistant_message // ""), stoppedAt: $ts, updatedAt: $ts}' \
|
|
136
|
-
|
|
110
|
+
>> "$FILE"
|
|
137
111
|
|
|
138
112
|
elif [ "$EVENT" = "TeammateIdle" ]; then
|
|
139
|
-
|
|
140
|
-
STARTED_AT="$TS"
|
|
141
|
-
if [ -f "$FILE" ]; then
|
|
142
|
-
eval "$(jq -r '@sh "PREV_TYPE=\(.type // "unknown")", @sh "PREV_START=\(.startedAt // "")"' "$FILE")"
|
|
143
|
-
[ -z "$AGENT_TYPE" ] && AGENT_TYPE="$PREV_TYPE"
|
|
144
|
-
[ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
|
|
145
|
-
fi
|
|
146
|
-
cat > "$FILE" <<EOF
|
|
147
|
-
{"agentId":"$AGENT_ID","type":"$AGENT_TYPE","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
|
|
148
|
-
EOF
|
|
113
|
+
echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$AGENT_TYPE_RAW\",\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
|
|
149
114
|
fi
|
package/public/app.js
CHANGED
|
@@ -4,6 +4,8 @@ let currentSessionId = null;
|
|
|
4
4
|
let currentTasks = [];
|
|
5
5
|
let viewMode = 'session';
|
|
6
6
|
let sessionFilter = 'active';
|
|
7
|
+
// Only meaningful while sessionFilter === 'active' (filterBySessions clears it otherwise)
|
|
8
|
+
const activityFilter = new Set(); // kinds: 'waiting' | 'active'
|
|
7
9
|
let sessionLimit = '20';
|
|
8
10
|
let filterProject = '__recent__'; // null = all, '__recent__' = last 24h, or project path
|
|
9
11
|
let recentProjects = new Set();
|
|
@@ -144,7 +146,6 @@ const inProgressCount = document.getElementById('in-progress-count');
|
|
|
144
146
|
const completedCount = document.getElementById('completed-count');
|
|
145
147
|
const detailPanel = document.getElementById('detail-panel');
|
|
146
148
|
const detailContent = document.getElementById('detail-content');
|
|
147
|
-
const connectionStatus = document.getElementById('connection-status');
|
|
148
149
|
const CONTENT_TRUNCATE_MAX = 1500;
|
|
149
150
|
const COLUMNS = [{ el: pendingTasks }, { el: inProgressTasks }, { el: completedTasks }];
|
|
150
151
|
|
|
@@ -186,7 +187,7 @@ async function fetchSessions(includeTasks = true) {
|
|
|
186
187
|
|
|
187
188
|
sessions = newSessions;
|
|
188
189
|
renderSessions();
|
|
189
|
-
|
|
190
|
+
renderActivityChip();
|
|
190
191
|
} catch (error) {
|
|
191
192
|
console.error('Failed to fetch sessions:', error);
|
|
192
193
|
}
|
|
@@ -412,15 +413,7 @@ function fuzzyMatch(text, query) {
|
|
|
412
413
|
|
|
413
414
|
//#endregion
|
|
414
415
|
|
|
415
|
-
|
|
416
|
-
function renderLiveUpdatesFromCache() {
|
|
417
|
-
let activeTasks = allTasksCache.filter((t) => t.status === 'in_progress' && !isInternalTask(t));
|
|
418
|
-
if (filterProject) {
|
|
419
|
-
activeTasks = activeTasks.filter((t) => matchesProjectFilter(t.project));
|
|
420
|
-
}
|
|
421
|
-
renderLiveUpdates(activeTasks);
|
|
422
|
-
}
|
|
423
|
-
|
|
416
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
424
417
|
function toggleSection(containerId, chevronId) {
|
|
425
418
|
const container = document.getElementById(containerId);
|
|
426
419
|
const chevron = document.getElementById(chevronId);
|
|
@@ -429,38 +422,90 @@ function toggleSection(containerId, chevronId) {
|
|
|
429
422
|
localStorage.setItem(`${containerId}Collapsed`, collapsed);
|
|
430
423
|
}
|
|
431
424
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
425
|
+
function isWaitingSession(s) {
|
|
426
|
+
return !!s.hasWaitingForUser;
|
|
427
|
+
}
|
|
428
|
+
function isActiveSession(s) {
|
|
429
|
+
return !s.hasWaitingForUser && (s.inProgress > 0 || s.hasRecentLog || s.hasRunningAgents);
|
|
435
430
|
}
|
|
436
431
|
|
|
437
|
-
|
|
438
|
-
|
|
432
|
+
const ACTIVITY_PREDICATES = {
|
|
433
|
+
waiting: isWaitingSession,
|
|
434
|
+
active: isActiveSession,
|
|
435
|
+
};
|
|
439
436
|
|
|
440
|
-
|
|
441
|
-
container.innerHTML = '<div class="live-empty">No active tasks</div>';
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
437
|
+
let lastChipKey = '';
|
|
444
438
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
439
|
+
function renderActivityChip() {
|
|
440
|
+
const container = document.getElementById('activity-chips');
|
|
441
|
+
if (!container) return;
|
|
442
|
+
|
|
443
|
+
let waiting = 0;
|
|
444
|
+
let active = 0;
|
|
445
|
+
for (const s of sessions) {
|
|
446
|
+
if (s.hasWaitingForUser) waiting++;
|
|
447
|
+
else if (s.inProgress > 0 || s.hasRecentLog || s.hasRunningAgents) active++;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const key = `${waiting}|${active}|${[...activityFilter].sort().join(',')}`;
|
|
451
|
+
if (key === lastChipKey) return;
|
|
452
|
+
lastChipKey = key;
|
|
453
|
+
|
|
454
|
+
const chips = [
|
|
455
|
+
{
|
|
456
|
+
kind: 'waiting',
|
|
457
|
+
count: waiting,
|
|
458
|
+
label: `${waiting} waiting`,
|
|
459
|
+
title: `${waiting} session${waiting === 1 ? '' : 's'} waiting for input`,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
kind: 'active',
|
|
463
|
+
count: active,
|
|
464
|
+
label: `${active} active`,
|
|
465
|
+
title: `${active} session${active === 1 ? '' : 's'} with running work or recent activity`,
|
|
466
|
+
},
|
|
467
|
+
];
|
|
468
|
+
|
|
469
|
+
container.innerHTML = chips
|
|
470
|
+
.map((c) => {
|
|
471
|
+
const isOn = activityFilter.has(c.kind);
|
|
472
|
+
const classes = [
|
|
473
|
+
'activity-chip',
|
|
474
|
+
`activity-${c.kind}`,
|
|
475
|
+
c.count === 0 ? 'activity-zero' : '',
|
|
476
|
+
isOn ? 'activity-filter-on' : '',
|
|
477
|
+
]
|
|
478
|
+
.filter(Boolean)
|
|
479
|
+
.join(' ');
|
|
480
|
+
const hint = isOn ? ' — click to clear filter' : ` — click to filter to ${c.kind}`;
|
|
481
|
+
return `
|
|
482
|
+
<button type="button"
|
|
483
|
+
class="${classes}"
|
|
484
|
+
onclick="setActivityFilter('${c.kind}')"
|
|
485
|
+
aria-pressed="${isOn ? 'true' : 'false'}"
|
|
486
|
+
title="${escapeHtml(c.title + hint)}">
|
|
487
|
+
<span class="activity-dot"></span>
|
|
488
|
+
<span class="activity-label">${escapeHtml(c.label)}</span>
|
|
489
|
+
</button>
|
|
490
|
+
`;
|
|
491
|
+
})
|
|
457
492
|
.join('');
|
|
458
493
|
}
|
|
459
494
|
|
|
460
495
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
496
|
+
function setActivityFilter(kind) {
|
|
497
|
+
if (activityFilter.has(kind)) activityFilter.delete(kind);
|
|
498
|
+
else activityFilter.add(kind);
|
|
499
|
+
// active/waiting only make sense with the active session filter on
|
|
500
|
+
const targetFilter = activityFilter.size > 0 ? 'active' : sessionFilter;
|
|
501
|
+
if (targetFilter !== sessionFilter) {
|
|
502
|
+
sessionFilter = targetFilter;
|
|
503
|
+
const dropdown = document.getElementById('session-filter');
|
|
504
|
+
if (dropdown) dropdown.value = targetFilter;
|
|
505
|
+
updateUrl();
|
|
506
|
+
}
|
|
507
|
+
renderSessions();
|
|
508
|
+
renderActivityChip();
|
|
464
509
|
}
|
|
465
510
|
|
|
466
511
|
let lastCurrentTasksHash = '';
|
|
@@ -2239,7 +2284,7 @@ async function showAllTasks() {
|
|
|
2239
2284
|
updateUrl();
|
|
2240
2285
|
renderAllTasks();
|
|
2241
2286
|
renderSessions();
|
|
2242
|
-
|
|
2287
|
+
renderActivityChip();
|
|
2243
2288
|
} catch (error) {
|
|
2244
2289
|
console.error('Failed to fetch all tasks:', error);
|
|
2245
2290
|
}
|
|
@@ -2313,7 +2358,11 @@ function renderSessions() {
|
|
|
2313
2358
|
filteredSessions = filteredSessions.filter((s) => matchesProjectFilter(s.project));
|
|
2314
2359
|
}
|
|
2315
2360
|
|
|
2316
|
-
|
|
2361
|
+
if (activityFilter.size > 0) {
|
|
2362
|
+
const preds = [...activityFilter].map((k) => ACTIVITY_PREDICATES[k]).filter(Boolean);
|
|
2363
|
+
if (preds.length) filteredSessions = filteredSessions.filter((s) => preds.some((p) => p(s)));
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2317
2366
|
if (searchQuery) {
|
|
2318
2367
|
const taskMatchIds = new Set();
|
|
2319
2368
|
for (const t of allTasksCache) {
|
|
@@ -2334,7 +2383,7 @@ function renderSessions() {
|
|
|
2334
2383
|
filteredSessions = filteredSessions.filter(matchesSearch);
|
|
2335
2384
|
|
|
2336
2385
|
// Re-add pinned/sticky sessions that match the query but were excluded by active filter
|
|
2337
|
-
if (pinnedSessionIds.size > 0 || stickySessionIds.size > 0) {
|
|
2386
|
+
if (activityFilter.size === 0 && (pinnedSessionIds.size > 0 || stickySessionIds.size > 0)) {
|
|
2338
2387
|
const filteredIds = new Set(filteredSessions.map((s) => s.id));
|
|
2339
2388
|
const missingPinned = sessions.filter((s) => isAnyPinned(s.id) && !filteredIds.has(s.id) && matchesSearch(s));
|
|
2340
2389
|
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
@@ -2342,7 +2391,8 @@ function renderSessions() {
|
|
|
2342
2391
|
}
|
|
2343
2392
|
|
|
2344
2393
|
// Include pinned/sticky sessions even if they don't match active/recent filter
|
|
2345
|
-
|
|
2394
|
+
// (skipped when an activity chip filter is on — user explicitly asked for a slice)
|
|
2395
|
+
if (activityFilter.size === 0 && !searchQuery && (pinnedSessionIds.size > 0 || stickySessionIds.size > 0)) {
|
|
2346
2396
|
const filteredIds = new Set(filteredSessions.map((s) => s.id));
|
|
2347
2397
|
const missingPinned = sessions.filter((s) => isAnyPinned(s.id) && !filteredIds.has(s.id));
|
|
2348
2398
|
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
@@ -2408,6 +2458,7 @@ function renderSessions() {
|
|
|
2408
2458
|
const showCtx = !!session.contextStatus;
|
|
2409
2459
|
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
2410
2460
|
const bookmarksCount = loadPins(session.id).length;
|
|
2461
|
+
const hasScratchpad = !!(localStorage.getItem(_sessionScratchpadKey(session.id)) || '').trim();
|
|
2411
2462
|
const tempClass = session.hasRecentLog || session.inProgress || session.hasWaitingForUser ? 'warm' : 'stale';
|
|
2412
2463
|
return `
|
|
2413
2464
|
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${tempClass} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
|
|
@@ -2424,6 +2475,7 @@ function renderSessions() {
|
|
|
2424
2475
|
${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>` : ''}
|
|
2425
2476
|
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
2426
2477
|
${bookmarksCount > 0 ? `<span class="bookmarks-badge" onclick="event.stopPropagation(); openSessionWithBookmarks('${session.id}')" title="${bookmarksCount} bookmarked message${bookmarksCount > 1 ? 's' : ''}"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>${bookmarksCount}</span>` : ''}
|
|
2478
|
+
${hasScratchpad ? `<span class="scratchpad-badge" onclick="event.stopPropagation(); openSessionScratchpad('${session.id}')" title="Open scratchpad"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></span>` : ''}
|
|
2427
2479
|
${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
|
|
2428
2480
|
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">📋</span>` : ''}
|
|
2429
2481
|
${session.hasWaitingForUser ? '<span class="agent-badge" title="Waiting for user">❓</span>' : ''}
|
|
@@ -3546,7 +3598,6 @@ async function refreshCurrentView() {
|
|
|
3546
3598
|
await showAllTasks();
|
|
3547
3599
|
} else if (currentSessionId) {
|
|
3548
3600
|
await fetchTasks(currentSessionId);
|
|
3549
|
-
renderLiveUpdatesFromCache();
|
|
3550
3601
|
} else {
|
|
3551
3602
|
await fetchSessions();
|
|
3552
3603
|
}
|
|
@@ -3564,9 +3615,17 @@ const _scratchpadCharcount = document.getElementById('scratchpad-charcount');
|
|
|
3564
3615
|
|
|
3565
3616
|
let _scratchpadKeyOverride = null;
|
|
3566
3617
|
|
|
3618
|
+
function _sessionScratchpadKey(sessionId) {
|
|
3619
|
+
return `scratchpad-${sessionId}`;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
function _isSessionScratchpadKey(key) {
|
|
3623
|
+
return key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:');
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3567
3626
|
function _scratchpadKey() {
|
|
3568
3627
|
if (_scratchpadKeyOverride) return _scratchpadKeyOverride;
|
|
3569
|
-
if (currentSessionId) return
|
|
3628
|
+
if (currentSessionId) return _sessionScratchpadKey(currentSessionId);
|
|
3570
3629
|
if (currentProjectPath) return `scratchpad-project:${currentProjectPath}`;
|
|
3571
3630
|
return null;
|
|
3572
3631
|
}
|
|
@@ -3579,6 +3638,11 @@ function toggleScratchpad() {
|
|
|
3579
3638
|
}
|
|
3580
3639
|
}
|
|
3581
3640
|
|
|
3641
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
3642
|
+
function openSessionScratchpad(sessionId) {
|
|
3643
|
+
showScratchpad(_sessionScratchpadKey(sessionId));
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3582
3646
|
function showScratchpad(keyOverride) {
|
|
3583
3647
|
_scratchpadKeyOverride = keyOverride || null;
|
|
3584
3648
|
const key = _scratchpadKey();
|
|
@@ -3603,11 +3667,16 @@ function saveScratchpad() {
|
|
|
3603
3667
|
const key = _scratchpadKey();
|
|
3604
3668
|
if (!key) return;
|
|
3605
3669
|
const val = _scratchpadTextarea.value;
|
|
3606
|
-
|
|
3670
|
+
const had = !!(localStorage.getItem(key) || '').trim();
|
|
3671
|
+
const has = !!val.trim();
|
|
3672
|
+
if (has) {
|
|
3607
3673
|
localStorage.setItem(key, val);
|
|
3608
3674
|
} else {
|
|
3609
3675
|
localStorage.removeItem(key);
|
|
3610
3676
|
}
|
|
3677
|
+
if (had !== has && _isSessionScratchpadKey(key)) {
|
|
3678
|
+
renderSessions();
|
|
3679
|
+
}
|
|
3611
3680
|
}
|
|
3612
3681
|
|
|
3613
3682
|
_scratchpadTextarea.addEventListener('input', () => {
|
|
@@ -3997,7 +4066,6 @@ function _renderStorageLinkedDocs() {
|
|
|
3997
4066
|
}
|
|
3998
4067
|
|
|
3999
4068
|
function _storagePreviewLinkedDoc(path) {
|
|
4000
|
-
closeStorageManager();
|
|
4001
4069
|
openPreviewByPath(path);
|
|
4002
4070
|
}
|
|
4003
4071
|
|
|
@@ -4027,7 +4095,7 @@ function _findOrphanedKeys() {
|
|
|
4027
4095
|
const key = localStorage.key(i);
|
|
4028
4096
|
if (key.startsWith('pinned-messages-')) {
|
|
4029
4097
|
if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
|
|
4030
|
-
} else if (
|
|
4098
|
+
} else if (_isSessionScratchpadKey(key)) {
|
|
4031
4099
|
if (!known.has(key.slice('scratchpad-'.length))) orphaned.push(key);
|
|
4032
4100
|
} else if (key.startsWith(PREVIEW_STORAGE_PREFIX)) {
|
|
4033
4101
|
if (!known.has(key.slice(PREVIEW_STORAGE_PREFIX.length))) orphaned.push(key);
|
|
@@ -4305,6 +4373,33 @@ document.addEventListener('keydown', (e) => {
|
|
|
4305
4373
|
hubNavigate('memory', mSession?.project ? `?project=${encodeURIComponent(mSession.project)}` : undefined);
|
|
4306
4374
|
return;
|
|
4307
4375
|
}
|
|
4376
|
+
if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.key === 'd') {
|
|
4377
|
+
e.preventDefault();
|
|
4378
|
+
if (!contextSid || dismissedSessionIds.has(contextSid)) return;
|
|
4379
|
+
const prevIdx = selectedSessionIdx;
|
|
4380
|
+
dismissedSessionIds.add(contextSid);
|
|
4381
|
+
updateDismissBtnState();
|
|
4382
|
+
renderSessions();
|
|
4383
|
+
const newItems = getNavigableItems();
|
|
4384
|
+
const targetIdx = newItems.length > 0 ? Math.max(0, prevIdx - 1) : -1;
|
|
4385
|
+
// If the dismissed session is currently open, navigate to the previous one
|
|
4386
|
+
if (currentSessionId === contextSid || selectedSessionId === contextSid) {
|
|
4387
|
+
selectedSessionId = null;
|
|
4388
|
+
if (targetIdx >= 0) {
|
|
4389
|
+
const targetSid = newItems[targetIdx]?.dataset?.sessionId;
|
|
4390
|
+
if (targetSid) {
|
|
4391
|
+
fetchTasks(targetSid).then(() => selectSessionByIndex(targetIdx, getNavigableItems()));
|
|
4392
|
+
} else {
|
|
4393
|
+
showAllTasks().then(() => selectSessionByIndex(targetIdx, getNavigableItems()));
|
|
4394
|
+
}
|
|
4395
|
+
} else {
|
|
4396
|
+
showAllTasks();
|
|
4397
|
+
}
|
|
4398
|
+
} else if (targetIdx >= 0) {
|
|
4399
|
+
selectSessionByIndex(targetIdx, newItems);
|
|
4400
|
+
}
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4308
4403
|
if (e.code === 'KeyC' && e.shiftKey) {
|
|
4309
4404
|
e.preventDefault();
|
|
4310
4405
|
if (!contextSid) {
|
|
@@ -4624,20 +4719,12 @@ function setupEventSource() {
|
|
|
4624
4719
|
wasConnected = true;
|
|
4625
4720
|
retryDelay = 1000;
|
|
4626
4721
|
hideOffline();
|
|
4627
|
-
connectionStatus.innerHTML = `
|
|
4628
|
-
<span class="connection-dot live"></span>
|
|
4629
|
-
<span>Connected</span>
|
|
4630
|
-
`;
|
|
4631
4722
|
};
|
|
4632
4723
|
|
|
4633
4724
|
eventSource.onerror = () => {
|
|
4634
4725
|
eventSource.close();
|
|
4635
4726
|
failCount++;
|
|
4636
4727
|
console.warn('[SSE] Connection lost, retrying in', retryDelay, 'ms');
|
|
4637
|
-
connectionStatus.innerHTML = `
|
|
4638
|
-
<span class="connection-dot error"></span>
|
|
4639
|
-
<span>Reconnecting...</span>
|
|
4640
|
-
`;
|
|
4641
4728
|
if (failCount >= 2) showOffline();
|
|
4642
4729
|
setTimeout(connect, retryDelay);
|
|
4643
4730
|
retryDelay = Math.min(retryDelay * 2, 30000);
|
|
@@ -4667,7 +4754,7 @@ function setupEventSource() {
|
|
|
4667
4754
|
if (viewMode === 'all') {
|
|
4668
4755
|
currentTasks = filterProject ? allTasksCache.filter((t) => matchesProjectFilter(t.project)) : allTasksCache;
|
|
4669
4756
|
renderAllTasks();
|
|
4670
|
-
|
|
4757
|
+
renderActivityChip();
|
|
4671
4758
|
} else if (viewMode === 'project' && currentProjectPath) {
|
|
4672
4759
|
const hasUpdate = currentProjectSessionIds.some((id) => pendingTaskSessionIds.has(id));
|
|
4673
4760
|
if (hasUpdate) fetchProjectView(currentProjectPath);
|
|
@@ -5096,8 +5183,10 @@ function getOwnerColor(name) {
|
|
|
5096
5183
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5097
5184
|
function filterBySessions(value) {
|
|
5098
5185
|
sessionFilter = value;
|
|
5186
|
+
if (value !== 'active') activityFilter.clear();
|
|
5099
5187
|
updateUrl();
|
|
5100
5188
|
renderSessions();
|
|
5189
|
+
renderActivityChip();
|
|
5101
5190
|
}
|
|
5102
5191
|
|
|
5103
5192
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
@@ -5360,7 +5449,7 @@ function initPanelResize(panelId, handleId, cssVar, storageKey) {
|
|
|
5360
5449
|
});
|
|
5361
5450
|
|
|
5362
5451
|
function onMove(e) {
|
|
5363
|
-
const w = Math.
|
|
5452
|
+
const w = Math.max(200, startWidth - (e.clientX - startX));
|
|
5364
5453
|
panel.style.setProperty(cssVar, `${w}px`);
|
|
5365
5454
|
}
|
|
5366
5455
|
|
|
@@ -5784,15 +5873,6 @@ function filterByOwner(value) {
|
|
|
5784
5873
|
|
|
5785
5874
|
//#endregion
|
|
5786
5875
|
|
|
5787
|
-
//#region LAYOUT_SYNC
|
|
5788
|
-
const sidebarHeader = document.querySelector('.sidebar-header');
|
|
5789
|
-
const viewHeader = document.querySelector('.view-header');
|
|
5790
|
-
new ResizeObserver(() => {
|
|
5791
|
-
sidebarHeader.style.height = `${viewHeader.offsetHeight}px`;
|
|
5792
|
-
}).observe(viewHeader);
|
|
5793
|
-
|
|
5794
|
-
//#endregion
|
|
5795
|
-
|
|
5796
5876
|
//#region PWA
|
|
5797
5877
|
if ('serviceWorker' in navigator) {
|
|
5798
5878
|
navigator.serviceWorker.register('/sw.js');
|
|
@@ -5802,14 +5882,10 @@ if ('serviceWorker' in navigator) {
|
|
|
5802
5882
|
|
|
5803
5883
|
//#region INIT
|
|
5804
5884
|
loadTheme();
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
.getElementById(id === 'live-updates' ? 'live-updates-chevron' : 'sessions-chevron')
|
|
5810
|
-
.classList.add('rotated');
|
|
5811
|
-
}
|
|
5812
|
-
});
|
|
5885
|
+
if (localStorage.getItem('sessions-filtersCollapsed') === 'true') {
|
|
5886
|
+
document.getElementById('sessions-filters').classList.add('collapsed');
|
|
5887
|
+
document.getElementById('sessions-chevron').classList.add('rotated');
|
|
5888
|
+
}
|
|
5813
5889
|
|
|
5814
5890
|
document.addEventListener('DOMContentLoaded', () => {
|
|
5815
5891
|
if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
|
package/public/index.html
CHANGED
|
@@ -44,18 +44,7 @@
|
|
|
44
44
|
<!-- Sidebar -->
|
|
45
45
|
<aside class="sidebar">
|
|
46
46
|
<header class="sidebar-header">
|
|
47
|
-
<div class="
|
|
48
|
-
<div class="logo-mark">
|
|
49
|
-
<svg viewBox="4 6 16 12" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
50
|
-
<path d="M5 13l4 4L19 7"/>
|
|
51
|
-
</svg>
|
|
52
|
-
</div>
|
|
53
|
-
<span class="logo-text">Dashboard</span>
|
|
54
|
-
</div>
|
|
55
|
-
<div id="connection-status" class="connection">
|
|
56
|
-
<span class="connection-dot"></span>
|
|
57
|
-
<span>Connecting</span>
|
|
58
|
-
</div>
|
|
47
|
+
<div id="activity-chips" class="activity-chips"></div>
|
|
59
48
|
<button id="sidebar-toggle" class="sidebar-toggle-btn" onclick="toggleSidebar()" title="Toggle sidebar" aria-label="Toggle sidebar">
|
|
60
49
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
61
50
|
<path d="M15 18l-6-6 6-6"/>
|
|
@@ -63,19 +52,6 @@
|
|
|
63
52
|
</button>
|
|
64
53
|
</header>
|
|
65
54
|
|
|
66
|
-
<!-- Live Updates -->
|
|
67
|
-
<div class="sidebar-section">
|
|
68
|
-
<div class="section-header" onclick="toggleLiveUpdates()" style="cursor: pointer;">
|
|
69
|
-
<span>Live Updates</span>
|
|
70
|
-
<svg id="live-updates-chevron" class="collapse-chevron" viewBox="0 0 24 24">
|
|
71
|
-
<path d="M6 9l6 6 6-6"/>
|
|
72
|
-
</svg>
|
|
73
|
-
</div>
|
|
74
|
-
<div id="live-updates" class="live-updates">
|
|
75
|
-
<div class="live-empty">No active tasks</div>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
55
|
<!-- Tasks -->
|
|
80
56
|
<div class="sidebar-section flex-1">
|
|
81
57
|
<div class="section-header" onclick="toggleSection('sessions-filters', 'sessions-chevron')" style="cursor: pointer;">
|
|
@@ -413,6 +389,10 @@
|
|
|
413
389
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">I</kbd></td>
|
|
414
390
|
<td style="padding: 4px 0; color: var(--text-primary);">Open session info</td>
|
|
415
391
|
</tr>
|
|
392
|
+
<tr>
|
|
393
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Ctrl+D</kbd></td>
|
|
394
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Dismiss selected session</td>
|
|
395
|
+
</tr>
|
|
416
396
|
<tr>
|
|
417
397
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">D</kbd></td>
|
|
418
398
|
<td style="padding: 4px 0; color: var(--text-primary);">Delete selected task</td>
|
package/public/style.css
CHANGED
|
@@ -100,19 +100,118 @@ body::before {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.sidebar-header {
|
|
103
|
-
padding:
|
|
103
|
+
padding: 6px 10px;
|
|
104
104
|
border-bottom: none;
|
|
105
105
|
background-image: linear-gradient(to right, transparent, var(--border), transparent);
|
|
106
106
|
background-size: 100% 1px;
|
|
107
107
|
background-repeat: no-repeat;
|
|
108
108
|
background-position: bottom;
|
|
109
109
|
position: relative;
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.activity-chips {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 6px;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
min-width: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.activity-chip {
|
|
125
|
+
display: inline-flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 7px;
|
|
128
|
+
padding: 4px 10px 4px 9px;
|
|
129
|
+
font: inherit;
|
|
130
|
+
font-size: 11px;
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
letter-spacing: 0.04em;
|
|
133
|
+
color: var(--text-secondary);
|
|
134
|
+
background: var(--bg-deep);
|
|
135
|
+
border: 1px solid var(--border);
|
|
136
|
+
border-radius: 999px;
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
transition:
|
|
140
|
+
color 0.2s ease,
|
|
141
|
+
border-color 0.2s ease,
|
|
142
|
+
background 0.2s ease,
|
|
143
|
+
transform 0.1s ease;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.activity-chip:hover {
|
|
147
|
+
background: var(--bg-hover);
|
|
148
|
+
border-color: color-mix(in srgb, var(--text-secondary) 30%, var(--border));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.activity-chip:active {
|
|
152
|
+
transform: scale(0.97);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.activity-chip.activity-filter-on {
|
|
156
|
+
background: color-mix(in srgb, var(--accent) 14%, var(--bg-deep));
|
|
157
|
+
border-color: var(--accent);
|
|
158
|
+
box-shadow: inset 0 0 0 1px var(--accent);
|
|
159
|
+
}
|
|
160
|
+
.activity-chip.activity-waiting.activity-filter-on {
|
|
161
|
+
border-color: var(--warning);
|
|
162
|
+
box-shadow: inset 0 0 0 1px var(--warning);
|
|
163
|
+
background: color-mix(in srgb, var(--warning) 14%, var(--bg-deep));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.activity-dot {
|
|
167
|
+
width: 6px;
|
|
168
|
+
height: 6px;
|
|
169
|
+
border-radius: 50%;
|
|
170
|
+
background: var(--text-muted);
|
|
171
|
+
flex-shrink: 0;
|
|
172
|
+
transition:
|
|
173
|
+
background 0.2s ease,
|
|
174
|
+
box-shadow 0.2s ease;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.activity-chip.activity-zero {
|
|
178
|
+
color: var(--text-tertiary);
|
|
179
|
+
border-color: var(--border);
|
|
180
|
+
opacity: 0.6;
|
|
181
|
+
}
|
|
182
|
+
.activity-chip.activity-zero .activity-dot {
|
|
183
|
+
background: var(--text-muted);
|
|
184
|
+
box-shadow: none;
|
|
185
|
+
animation: none;
|
|
186
|
+
}
|
|
187
|
+
.activity-chip.activity-zero:hover {
|
|
188
|
+
opacity: 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.activity-chip.activity-waiting {
|
|
192
|
+
color: var(--warning);
|
|
193
|
+
border-color: color-mix(in srgb, var(--warning) 40%, var(--border));
|
|
194
|
+
}
|
|
195
|
+
.activity-chip.activity-waiting .activity-dot {
|
|
196
|
+
background: var(--warning);
|
|
197
|
+
box-shadow: 0 0 8px color-mix(in srgb, var(--warning) 60%, transparent);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.activity-chip.activity-active {
|
|
201
|
+
color: color-mix(in srgb, var(--accent) 70%, var(--text-secondary));
|
|
202
|
+
border-color: color-mix(in srgb, var(--accent) 18%, var(--border));
|
|
203
|
+
}
|
|
204
|
+
.activity-chip.activity-active .activity-dot {
|
|
205
|
+
background: color-mix(in srgb, var(--accent) 75%, var(--text-secondary));
|
|
206
|
+
box-shadow: 0 0 4px color-mix(in srgb, var(--accent) 30%, transparent);
|
|
207
|
+
animation: pulse 2.5s ease-in-out infinite;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.sidebar.collapsed .activity-chips {
|
|
211
|
+
display: none;
|
|
110
212
|
}
|
|
111
213
|
|
|
112
214
|
.sidebar-toggle-btn {
|
|
113
|
-
position: absolute;
|
|
114
|
-
top: 20px;
|
|
115
|
-
right: 8px;
|
|
116
215
|
width: 28px;
|
|
117
216
|
height: 28px;
|
|
118
217
|
display: flex;
|
|
@@ -178,62 +277,6 @@ body::before {
|
|
|
178
277
|
background: var(--accent-dim);
|
|
179
278
|
}
|
|
180
279
|
|
|
181
|
-
.logo {
|
|
182
|
-
display: flex;
|
|
183
|
-
align-items: center;
|
|
184
|
-
gap: 10px;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.logo-mark {
|
|
188
|
-
width: 24px;
|
|
189
|
-
height: 24px;
|
|
190
|
-
background: var(--accent);
|
|
191
|
-
border-radius: 6px;
|
|
192
|
-
display: flex;
|
|
193
|
-
align-items: center;
|
|
194
|
-
justify-content: center;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.logo-mark svg {
|
|
198
|
-
width: 14px;
|
|
199
|
-
height: 14px;
|
|
200
|
-
color: white;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.logo-text {
|
|
204
|
-
font-family: var(--serif);
|
|
205
|
-
font-size: 17px;
|
|
206
|
-
font-weight: 500;
|
|
207
|
-
letter-spacing: -0.02em;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.connection {
|
|
211
|
-
display: flex;
|
|
212
|
-
align-items: center;
|
|
213
|
-
gap: 5px;
|
|
214
|
-
margin-top: 10px;
|
|
215
|
-
font-size: 10px;
|
|
216
|
-
color: var(--text-tertiary);
|
|
217
|
-
text-transform: uppercase;
|
|
218
|
-
letter-spacing: 0.05em;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.connection-dot {
|
|
222
|
-
width: 6px;
|
|
223
|
-
height: 6px;
|
|
224
|
-
border-radius: 50%;
|
|
225
|
-
background: var(--warning);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
.connection-dot.live {
|
|
229
|
-
background: var(--success);
|
|
230
|
-
box-shadow: 0 0 8px var(--success);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
.connection-dot.error {
|
|
234
|
-
background: #ef4444;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
280
|
.offline-overlay {
|
|
238
281
|
display: none;
|
|
239
282
|
position: fixed;
|
|
@@ -370,7 +413,7 @@ body::before {
|
|
|
370
413
|
|
|
371
414
|
/* #endregion */
|
|
372
415
|
|
|
373
|
-
/* #region
|
|
416
|
+
/* #region COLLAPSIBLE */
|
|
374
417
|
.collapse-chevron {
|
|
375
418
|
width: 14px;
|
|
376
419
|
height: 14px;
|
|
@@ -401,80 +444,6 @@ body::before {
|
|
|
401
444
|
overflow: hidden;
|
|
402
445
|
}
|
|
403
446
|
|
|
404
|
-
.live-updates {
|
|
405
|
-
padding: 0 16px 8px;
|
|
406
|
-
max-height: 140px;
|
|
407
|
-
overflow-y: auto;
|
|
408
|
-
transition:
|
|
409
|
-
max-height 0.2s ease,
|
|
410
|
-
padding 0.2s ease,
|
|
411
|
-
opacity 0.2s ease;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.live-updates.collapsed {
|
|
415
|
-
max-height: 0;
|
|
416
|
-
padding: 0 16px;
|
|
417
|
-
overflow: hidden;
|
|
418
|
-
opacity: 0;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
.live-empty {
|
|
422
|
-
padding: 8px;
|
|
423
|
-
text-align: center;
|
|
424
|
-
font-size: 11px;
|
|
425
|
-
color: var(--text-muted);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
.live-item {
|
|
429
|
-
display: flex;
|
|
430
|
-
align-items: flex-start;
|
|
431
|
-
gap: 8px;
|
|
432
|
-
padding: 6px 10px;
|
|
433
|
-
background: var(--bg-deep);
|
|
434
|
-
border: 1px solid transparent;
|
|
435
|
-
border-radius: 6px;
|
|
436
|
-
margin-bottom: 3px;
|
|
437
|
-
cursor: pointer;
|
|
438
|
-
transition: all 0.15s ease;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.live-item:hover {
|
|
442
|
-
background: var(--bg-hover);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
.live-item .pulse {
|
|
446
|
-
width: 6px;
|
|
447
|
-
height: 6px;
|
|
448
|
-
margin-top: 4px;
|
|
449
|
-
background: var(--accent);
|
|
450
|
-
border-radius: 50%;
|
|
451
|
-
flex-shrink: 0;
|
|
452
|
-
animation: pulse 2s ease-in-out infinite;
|
|
453
|
-
box-shadow: 0 0 8px var(--accent-glow);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
.live-item-content {
|
|
457
|
-
flex: 1;
|
|
458
|
-
min-width: 0;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
.live-item-action {
|
|
462
|
-
font-size: 11px;
|
|
463
|
-
color: var(--text-primary);
|
|
464
|
-
white-space: nowrap;
|
|
465
|
-
overflow: hidden;
|
|
466
|
-
text-overflow: ellipsis;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
.live-item-session {
|
|
470
|
-
font-size: 10px;
|
|
471
|
-
color: var(--text-tertiary);
|
|
472
|
-
margin-top: 1px;
|
|
473
|
-
white-space: nowrap;
|
|
474
|
-
overflow: hidden;
|
|
475
|
-
text-overflow: ellipsis;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
447
|
/* #endregion */
|
|
479
448
|
|
|
480
449
|
/* #region SESSIONS */
|
|
@@ -762,8 +731,6 @@ body::before {
|
|
|
762
731
|
.sidebar.collapsed {
|
|
763
732
|
width: 48px;
|
|
764
733
|
}
|
|
765
|
-
.sidebar.collapsed .logo-text,
|
|
766
|
-
.sidebar.collapsed .connection,
|
|
767
734
|
.sidebar.collapsed .sidebar-section,
|
|
768
735
|
.sidebar.collapsed .sidebar-footer {
|
|
769
736
|
display: none;
|
|
@@ -2569,7 +2536,8 @@ body::before {
|
|
|
2569
2536
|
}
|
|
2570
2537
|
|
|
2571
2538
|
.linked-docs-badge,
|
|
2572
|
-
.bookmarks-badge
|
|
2539
|
+
.bookmarks-badge,
|
|
2540
|
+
.scratchpad-badge {
|
|
2573
2541
|
display: inline-flex;
|
|
2574
2542
|
align-items: center;
|
|
2575
2543
|
gap: 2px;
|
|
@@ -2585,7 +2553,8 @@ body::before {
|
|
|
2585
2553
|
}
|
|
2586
2554
|
|
|
2587
2555
|
.linked-docs-badge:hover,
|
|
2588
|
-
.bookmarks-badge:hover
|
|
2556
|
+
.bookmarks-badge:hover,
|
|
2557
|
+
.scratchpad-badge:hover {
|
|
2589
2558
|
border-color: var(--accent);
|
|
2590
2559
|
color: var(--text-primary);
|
|
2591
2560
|
}
|
|
@@ -3455,10 +3424,6 @@ pre.mermaid svg {
|
|
|
3455
3424
|
}
|
|
3456
3425
|
}
|
|
3457
3426
|
|
|
3458
|
-
.connection-dot.live {
|
|
3459
|
-
animation: breathe 3s ease-in-out infinite;
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
3427
|
/* Progress bar shimmer */
|
|
3463
3428
|
@keyframes shimmer {
|
|
3464
3429
|
0% {
|
package/server.js
CHANGED
|
@@ -102,12 +102,19 @@ const SESSION_STALE_MS = 300000;
|
|
|
102
102
|
|
|
103
103
|
const WAITING_RESOLVE_GRACE_MS = 15000;
|
|
104
104
|
|
|
105
|
+
function readAgentJsonl(filePath) {
|
|
106
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
107
|
+
const merged = {};
|
|
108
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
109
|
+
if (!line.trim()) continue;
|
|
110
|
+
try { Object.assign(merged, JSON.parse(line)); } catch (_) { /* skip malformed */ }
|
|
111
|
+
}
|
|
112
|
+
return merged;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
function persistAgent(dir, agent) {
|
|
106
|
-
const file = path.join(dir, agent.agentId + '.
|
|
107
|
-
|
|
108
|
-
fs.writeFile(tmp, JSON.stringify(agent), 'utf8')
|
|
109
|
-
.then(() => fs.rename(tmp, file))
|
|
110
|
-
.catch(() => { fs.unlink(tmp).catch(() => {}); });
|
|
116
|
+
const file = path.join(dir, agent.agentId + '.jsonl');
|
|
117
|
+
fs.appendFile(file, JSON.stringify({ ...agent, event: 'server-update' }) + '\n', 'utf8').catch(() => {});
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
function checkWaitingForUser(agentDir, logMtime) {
|
|
@@ -125,6 +132,10 @@ function checkWaitingForUser(agentDir, logMtime) {
|
|
|
125
132
|
return null;
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
function agentDisplayName(agent) {
|
|
136
|
+
return agent.type || agent.name;
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
function isGhostAgent(agent) {
|
|
129
140
|
if (agent.startedAt !== agent.updatedAt || agent.lastMessage) return false;
|
|
130
141
|
return (Date.now() - new Date(agent.startedAt).getTime()) >= AGENT_STALE_MS;
|
|
@@ -156,9 +167,9 @@ function checkAgentStatus(agentDir, stale, logMtime, isTeam) {
|
|
|
156
167
|
if (result.waitingForUser) result.hasActive = true;
|
|
157
168
|
if (stale && !isTeam) return result;
|
|
158
169
|
try {
|
|
159
|
-
for (const file of readdirSync(agentDir).filter(f => f.endsWith('.
|
|
170
|
+
for (const file of readdirSync(agentDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('_'))) {
|
|
160
171
|
try {
|
|
161
|
-
const agent =
|
|
172
|
+
const agent = readAgentJsonl(path.join(agentDir, file));
|
|
162
173
|
if (isTeam && (agent.status === 'active' || agent.status === 'idle')) {
|
|
163
174
|
result.hasActive = true;
|
|
164
175
|
if (agent.status === 'active') result.hasRunning = true;
|
|
@@ -1050,17 +1061,17 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1050
1061
|
const isTeam = !!teamConfig;
|
|
1051
1062
|
const teamMemberNames = isTeam ? new Set(teamConfig.members.map(m => m.name)) : null;
|
|
1052
1063
|
|
|
1053
|
-
const files = readdirSync(agentDir).filter(f => f.endsWith('.
|
|
1064
|
+
const files = readdirSync(agentDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('_'));
|
|
1054
1065
|
const agents = [];
|
|
1055
1066
|
for (const file of files) {
|
|
1056
1067
|
try {
|
|
1057
|
-
const agent =
|
|
1068
|
+
const agent = readAgentJsonl(path.join(agentDir, file));
|
|
1058
1069
|
if (isGhostAgent(agent)) continue;
|
|
1059
1070
|
const agentTs = agent.updatedAt || agent.startedAt;
|
|
1060
1071
|
const agentStale = !sessionStale && agentTs && (Date.now() - new Date(agentTs).getTime()) > AGENT_STALE_MS;
|
|
1061
1072
|
if (!isAgentFresh(agent) || sessionStale || agentStale) {
|
|
1062
1073
|
if (agent.status === 'active' || agent.status === 'idle') {
|
|
1063
|
-
const agentName = agent
|
|
1074
|
+
const agentName = agentDisplayName(agent);
|
|
1064
1075
|
const isTeamMember = isTeam && agentName && teamMemberNames.has(agentName);
|
|
1065
1076
|
if (!isTeamMember) {
|
|
1066
1077
|
agent.status = 'stopped';
|
|
@@ -1077,7 +1088,7 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1077
1088
|
const terminated = getTerminatedTeammates(meta.jsonlPath);
|
|
1078
1089
|
if (terminated.size) {
|
|
1079
1090
|
for (const agent of liveAgents) {
|
|
1080
|
-
const agentName = agent
|
|
1091
|
+
const agentName = agentDisplayName(agent);
|
|
1081
1092
|
if (agentName && terminated.has(agentName)) {
|
|
1082
1093
|
const terminatedAt = terminated.get(agentName);
|
|
1083
1094
|
if (terminatedAt && agent.startedAt && terminatedAt < agent.startedAt) continue;
|
|
@@ -1088,6 +1099,29 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1088
1099
|
}
|
|
1089
1100
|
}
|
|
1090
1101
|
} catch (_) {}
|
|
1102
|
+
// Mark agents whose spawning Agent tool_use was rejected by the user as stopped:
|
|
1103
|
+
// the parent will never read their output, so they're orphans. Match by agentId
|
|
1104
|
+
// when the digest already correlated tool_use→agent, else fall back to prompt text
|
|
1105
|
+
// (the agent-spy hook doesn't record the spawning tool_use_id).
|
|
1106
|
+
try {
|
|
1107
|
+
const { rejectedAgentIds = new Set(), rejectedPrompts = new Set(), killedAgentIds = new Set() } =
|
|
1108
|
+
getSessionDigest(meta.jsonlPath);
|
|
1109
|
+
if (rejectedAgentIds.size || rejectedPrompts.size || killedAgentIds.size) {
|
|
1110
|
+
for (const agent of liveAgents) {
|
|
1111
|
+
if (agent.status !== 'active' && agent.status !== 'idle') continue;
|
|
1112
|
+
let reason = null;
|
|
1113
|
+
if (killedAgentIds.has(agent.agentId)) reason = 'killed-by-harness';
|
|
1114
|
+
else if (rejectedAgentIds.has(agent.agentId) || (agent.prompt && rejectedPrompts.has(agent.prompt))) {
|
|
1115
|
+
reason = 'orphaned-by-rejection';
|
|
1116
|
+
}
|
|
1117
|
+
if (!reason) continue;
|
|
1118
|
+
agent.status = 'stopped';
|
|
1119
|
+
agent.stoppedAt = agent.stoppedAt || new Date().toISOString();
|
|
1120
|
+
agent.stopReason = agent.stopReason || reason;
|
|
1121
|
+
persistAgent(agentDir, agent);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
} catch (_) {}
|
|
1091
1125
|
}
|
|
1092
1126
|
|
|
1093
1127
|
const dirty = new Set();
|
|
@@ -1145,14 +1179,40 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1145
1179
|
}
|
|
1146
1180
|
if (Object.keys(teamColors).length) {
|
|
1147
1181
|
for (const agent of agents) {
|
|
1148
|
-
const name = agent
|
|
1182
|
+
const name = agentDisplayName(agent);
|
|
1149
1183
|
if (name && teamColors[name]) agent.color = teamColors[name];
|
|
1150
1184
|
}
|
|
1151
1185
|
}
|
|
1152
1186
|
}
|
|
1153
1187
|
|
|
1188
|
+
// Collapse teammate re-spawns: when a teammate goes idle and is later re-engaged,
|
|
1189
|
+
// a fresh agentId is spawned. Hide older idle/stopped entries when a newer same-name
|
|
1190
|
+
// teammate exists; never hide an `active` agent (parallel teammate work would vanish).
|
|
1191
|
+
// Subagents (Explore, general-purpose, etc.) are not in teamMemberNames and bypass
|
|
1192
|
+
// dedup entirely, so parallel siblings of the same subagent type remain visible.
|
|
1193
|
+
let visibleAgents = agents;
|
|
1194
|
+
if (teamMemberNames && teamMemberNames.size) {
|
|
1195
|
+
const groups = new Map();
|
|
1196
|
+
for (const a of agents) {
|
|
1197
|
+
const t = agentDisplayName(a);
|
|
1198
|
+
if (!t || !teamMemberNames.has(t)) continue;
|
|
1199
|
+
const list = groups.get(t) || [];
|
|
1200
|
+
list.push(a);
|
|
1201
|
+
groups.set(t, list);
|
|
1202
|
+
}
|
|
1203
|
+
const hidden = new Set();
|
|
1204
|
+
for (const list of groups.values()) {
|
|
1205
|
+
if (list.length < 2) continue;
|
|
1206
|
+
list.sort((a, b) => new Date(b.startedAt || 0) - new Date(a.startedAt || 0));
|
|
1207
|
+
for (const older of list.slice(1)) {
|
|
1208
|
+
if (older.status === 'idle' || older.status === 'stopped') hidden.add(older.agentId);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (hidden.size) visibleAgents = agents.filter(a => !hidden.has(a.agentId));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1154
1214
|
const waitingForUser = checkWaitingForUser(agentDir, logMtime);
|
|
1155
|
-
res.json({ agents, waitingForUser, teamColors });
|
|
1215
|
+
res.json({ agents: visibleAgents, waitingForUser, teamColors });
|
|
1156
1216
|
} catch (e) {
|
|
1157
1217
|
res.json({ agents: [], waitingForUser: null });
|
|
1158
1218
|
}
|
|
@@ -1161,13 +1221,14 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1161
1221
|
app.post('/api/sessions/:sessionId/agents/:agentId/stop', (req, res) => {
|
|
1162
1222
|
const sessionId = resolveSessionId(req.params.sessionId);
|
|
1163
1223
|
const agentId = sanitizeAgentId(req.params.agentId);
|
|
1164
|
-
const agentFile = path.join(AGENT_ACTIVITY_DIR, sessionId, agentId + '.
|
|
1224
|
+
const agentFile = path.join(AGENT_ACTIVITY_DIR, sessionId, agentId + '.jsonl');
|
|
1165
1225
|
if (!existsSync(agentFile)) return res.status(404).json({ error: 'Agent not found' });
|
|
1166
1226
|
try {
|
|
1167
|
-
const agent =
|
|
1227
|
+
const agent = readAgentJsonl(agentFile);
|
|
1168
1228
|
agent.status = 'stopped';
|
|
1169
1229
|
agent.stoppedAt = new Date().toISOString();
|
|
1170
|
-
|
|
1230
|
+
const stopEvt = { agentId, type: agent.type, event: 'user-stop', status: 'stopped', stoppedAt: agent.stoppedAt, updatedAt: agent.stoppedAt };
|
|
1231
|
+
writeFileSync(agentFile, readFileSync(agentFile, 'utf8') + JSON.stringify(stopEvt) + '\n', 'utf8'); // sync — response depends on write
|
|
1171
1232
|
// Also remove waiting state if present
|
|
1172
1233
|
const waitingFile = path.join(AGENT_ACTIVITY_DIR, sessionId, '_waiting.json');
|
|
1173
1234
|
if (existsSync(waitingFile)) unlinkSync(waitingFile);
|
|
@@ -1190,6 +1251,31 @@ function subagentJsonlPath(meta, agentId) {
|
|
|
1190
1251
|
);
|
|
1191
1252
|
}
|
|
1192
1253
|
|
|
1254
|
+
// Claude Code can scatter a session's records across multiple project dirs
|
|
1255
|
+
// (e.g. main repo + worktree), so the subagent JSONL may live under a
|
|
1256
|
+
// different project dir than meta.jsonlPath. Fall back to scanning when the
|
|
1257
|
+
// derived path is missing.
|
|
1258
|
+
const subagentPathCache = new Map();
|
|
1259
|
+
function resolveSubagentJsonl(meta, sessionId, agentId) {
|
|
1260
|
+
const primary = subagentJsonlPath(meta, agentId);
|
|
1261
|
+
if (existsSync(primary)) return primary;
|
|
1262
|
+
const key = sessionId + '/' + agentId;
|
|
1263
|
+
if (subagentPathCache.has(key)) return subagentPathCache.get(key) || primary;
|
|
1264
|
+
let found = null;
|
|
1265
|
+
try {
|
|
1266
|
+
for (const entry of readdirSync(PROJECTS_DIR, { withFileTypes: true })) {
|
|
1267
|
+
if (!entry.isDirectory()) continue;
|
|
1268
|
+
const candidate = path.join(
|
|
1269
|
+
PROJECTS_DIR, entry.name, sessionId,
|
|
1270
|
+
'subagents', 'agent-' + agentId + '.jsonl'
|
|
1271
|
+
);
|
|
1272
|
+
if (existsSync(candidate)) { found = candidate; break; }
|
|
1273
|
+
}
|
|
1274
|
+
} catch (_) { /* projects dir missing */ }
|
|
1275
|
+
subagentPathCache.set(key, found);
|
|
1276
|
+
return found || primary;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1193
1279
|
app.get('/api/sessions/:sessionId/agents/:agentId/messages', (req, res) => {
|
|
1194
1280
|
const sessionId = resolveSessionId(req.params.sessionId);
|
|
1195
1281
|
const agentId = sanitizeAgentId(req.params.agentId);
|
|
@@ -1197,7 +1283,7 @@ app.get('/api/sessions/:sessionId/agents/:agentId/messages', (req, res) => {
|
|
|
1197
1283
|
const metadata = loadSessionMetadata();
|
|
1198
1284
|
const meta = metadata[sessionId];
|
|
1199
1285
|
if (!meta?.jsonlPath) return res.json({ messages: [], agentId });
|
|
1200
|
-
const subagentJsonl =
|
|
1286
|
+
const subagentJsonl = resolveSubagentJsonl(meta, sessionId, agentId);
|
|
1201
1287
|
if (!existsSync(subagentJsonl)) return res.json({ messages: [], agentId });
|
|
1202
1288
|
const messages = readRecentMessages(subagentJsonl, limit);
|
|
1203
1289
|
res.json({ messages, agentId });
|
|
@@ -1212,7 +1298,7 @@ app.get('/api/sessions/:sessionId/agents/:agentId/messages/stream', (req, res) =
|
|
|
1212
1298
|
res.status(404).json({ error: 'Session not found' });
|
|
1213
1299
|
return;
|
|
1214
1300
|
}
|
|
1215
|
-
const subagentJsonl =
|
|
1301
|
+
const subagentJsonl = resolveSubagentJsonl(meta, sessionId, agentId);
|
|
1216
1302
|
|
|
1217
1303
|
res.writeHead(200, {
|
|
1218
1304
|
'Content-Type': 'text/event-stream',
|
|
@@ -1283,8 +1369,8 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
1283
1369
|
if (entry.description) msg.agentDescription = entry.description;
|
|
1284
1370
|
if (entry.prompt && !msg.agentPrompt) msg.agentPrompt = entry.prompt;
|
|
1285
1371
|
try {
|
|
1286
|
-
const agentFile = path.join(agentDir, entry.agentId + '.
|
|
1287
|
-
const agent =
|
|
1372
|
+
const agentFile = path.join(agentDir, entry.agentId + '.jsonl');
|
|
1373
|
+
const agent = readAgentJsonl(agentFile);
|
|
1288
1374
|
if (agent.lastMessage) msg.agentLastMessage = agent.lastMessage;
|
|
1289
1375
|
if (agent.prompt && !msg.agentPrompt) msg.agentPrompt = agent.prompt;
|
|
1290
1376
|
const prompt = msg.agentPrompt || entry.prompt;
|
|
@@ -1771,14 +1857,16 @@ const agentActivityWatcher = chokidar.watch(AGENT_ACTIVITY_DIR, {
|
|
|
1771
1857
|
const AGENT_FILE_CAP = 20;
|
|
1772
1858
|
|
|
1773
1859
|
agentActivityWatcher.on('all', (event, filePath) => {
|
|
1774
|
-
|
|
1860
|
+
const base = path.basename(filePath);
|
|
1861
|
+
const isAgentEvent = filePath.endsWith('.jsonl') || base === '_waiting.json';
|
|
1862
|
+
if ((event === 'add' || event === 'change' || event === 'unlink') && isAgentEvent) {
|
|
1775
1863
|
const relativePath = path.relative(AGENT_ACTIVITY_DIR, filePath);
|
|
1776
1864
|
const sessionId = relativePath.split(path.sep)[0];
|
|
1777
1865
|
// Cleanup: if session dir exceeds cap, delete oldest files by mtime
|
|
1778
|
-
if (event === 'add') {
|
|
1866
|
+
if (event === 'add' && filePath.endsWith('.jsonl')) {
|
|
1779
1867
|
try {
|
|
1780
1868
|
const sessionDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
|
|
1781
|
-
const files = readdirSync(sessionDir).filter(f => f.endsWith('.
|
|
1869
|
+
const files = readdirSync(sessionDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('_'));
|
|
1782
1870
|
if (files.length > AGENT_FILE_CAP) {
|
|
1783
1871
|
const withStats = files.map(f => {
|
|
1784
1872
|
const fp = path.join(sessionDir, f);
|