claude-code-kanban 4.0.0 → 4.1.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 +68 -16
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/hooks/hooks.json +10 -0
- package/plugin/plugins/claude-code-kanban/scripts/agent-spy.sh +4 -4
- package/public/app.js +198 -42
- package/public/index.html +6 -0
- package/public/style.css +156 -17
- package/server.js +132 -27
package/lib/parsers.js
CHANGED
|
@@ -118,16 +118,23 @@ const TOOL_RESULT_MAX = 1500;
|
|
|
118
118
|
const USER_TEXT_MAX = 500;
|
|
119
119
|
const INTERRUPT_MARKER = '[Request interrupted by user]';
|
|
120
120
|
|
|
121
|
-
function pushUserMessage(messages, text, timestamp, sysLabel) {
|
|
121
|
+
function pushUserMessage(messages, text, timestamp, sysLabel, extras) {
|
|
122
122
|
if (sysLabel === '__skip__') return;
|
|
123
|
-
const
|
|
124
|
-
|
|
123
|
+
const safeText = text || '';
|
|
124
|
+
const truncated = safeText.length > USER_TEXT_MAX;
|
|
125
|
+
const msg = {
|
|
125
126
|
type: 'user',
|
|
126
|
-
text: truncated ?
|
|
127
|
-
fullText: truncated ?
|
|
127
|
+
text: truncated ? safeText.slice(0, USER_TEXT_MAX) + '...' : safeText,
|
|
128
|
+
fullText: truncated ? safeText : null,
|
|
128
129
|
timestamp,
|
|
129
130
|
...(sysLabel && { systemLabel: sysLabel })
|
|
130
|
-
}
|
|
131
|
+
};
|
|
132
|
+
if (extras) {
|
|
133
|
+
if (extras.uuid) msg.uuid = extras.uuid;
|
|
134
|
+
if (extras.images && extras.images.length) msg.images = extras.images;
|
|
135
|
+
if (extras.toolResultRefs && extras.toolResultRefs.length) msg.toolResultRefs = extras.toolResultRefs;
|
|
136
|
+
}
|
|
137
|
+
messages.push(msg);
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
// Cache: jsonlPath -> { scannedUpTo, customTitle }
|
|
@@ -532,16 +539,19 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
532
539
|
}
|
|
533
540
|
pushUserMessage(messages, t, obj.timestamp, getSystemMessageLabel(t));
|
|
534
541
|
} else if (Array.isArray(obj.message.content)) {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
542
|
+
const texts = [];
|
|
543
|
+
const images = [];
|
|
544
|
+
const toolResultRefs = [];
|
|
545
|
+
obj.message.content.forEach((block, idx) => {
|
|
546
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text) {
|
|
547
|
+
texts.push(block.text);
|
|
548
|
+
} else if (block.type === 'image' && block.source && block.source.type === 'base64') {
|
|
549
|
+
images.push({
|
|
550
|
+
blockIndex: idx,
|
|
551
|
+
mediaType: block.source.media_type || 'image/png',
|
|
552
|
+
dataLen: typeof block.source.data === 'string' ? block.source.data.length : 0
|
|
553
|
+
});
|
|
554
|
+
} else if (block.type === 'tool_result' && block.tool_use_id) {
|
|
545
555
|
let resultText = '';
|
|
546
556
|
if (typeof block.content === 'string') {
|
|
547
557
|
resultText = block.content;
|
|
@@ -554,7 +564,23 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
554
564
|
if (resultText) {
|
|
555
565
|
toolResults.set(block.tool_use_id, resultText);
|
|
556
566
|
}
|
|
567
|
+
toolResultRefs.push({
|
|
568
|
+
toolUseId: block.tool_use_id,
|
|
569
|
+
preview: resultText ? resultText.slice(0, 200) : ''
|
|
570
|
+
});
|
|
557
571
|
}
|
|
572
|
+
});
|
|
573
|
+
const joined = texts.join('\n').trim();
|
|
574
|
+
const hasText = joined && joined !== INTERRUPT_MARKER;
|
|
575
|
+
const hasImages = images.length > 0;
|
|
576
|
+
if (hasText || hasImages) {
|
|
577
|
+
pushUserMessage(
|
|
578
|
+
messages,
|
|
579
|
+
joined,
|
|
580
|
+
obj.timestamp,
|
|
581
|
+
getSystemMessageLabel(joined),
|
|
582
|
+
{ uuid: obj.uuid, images, toolResultRefs: hasText ? toolResultRefs : [] }
|
|
583
|
+
);
|
|
558
584
|
}
|
|
559
585
|
}
|
|
560
586
|
}
|
|
@@ -620,6 +646,31 @@ function readFullToolResult(jsonlPath, toolUseId) {
|
|
|
620
646
|
return null;
|
|
621
647
|
}
|
|
622
648
|
|
|
649
|
+
function readUserImage(jsonlPath, msgUuid, blockIndex) {
|
|
650
|
+
if (!msgUuid || !jsonlPath) return null;
|
|
651
|
+
const idx = Number(blockIndex);
|
|
652
|
+
if (!Number.isInteger(idx) || idx < 0) return null;
|
|
653
|
+
try {
|
|
654
|
+
const content = readFileSync(jsonlPath, 'utf8');
|
|
655
|
+
const lines = content.split('\n');
|
|
656
|
+
for (const line of lines) {
|
|
657
|
+
if (!line || line.indexOf(msgUuid) === -1) continue;
|
|
658
|
+
try {
|
|
659
|
+
const obj = JSON.parse(line);
|
|
660
|
+
if (obj?.uuid !== msgUuid) continue;
|
|
661
|
+
if (!Array.isArray(obj?.message?.content)) continue;
|
|
662
|
+
const block = obj.message.content[idx];
|
|
663
|
+
if (!block || block.type !== 'image' || !block.source || block.source.type !== 'base64') return null;
|
|
664
|
+
return {
|
|
665
|
+
mediaType: block.source.media_type || 'image/png',
|
|
666
|
+
data: block.source.data
|
|
667
|
+
};
|
|
668
|
+
} catch (_) {}
|
|
669
|
+
}
|
|
670
|
+
} catch (_) {}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
|
|
623
674
|
function readMessagesPage(jsonlPath, limit = 10, beforeTimestamp = null) {
|
|
624
675
|
const fetchLimit = limit + 1;
|
|
625
676
|
const applyFilter = beforeTimestamp
|
|
@@ -913,6 +964,7 @@ module.exports = {
|
|
|
913
964
|
readRecentMessages,
|
|
914
965
|
readMessagesPage,
|
|
915
966
|
readFullToolResult,
|
|
967
|
+
readUserImage,
|
|
916
968
|
buildAgentProgressMap,
|
|
917
969
|
buildSessionDigest,
|
|
918
970
|
readCompactSummaries,
|
package/package.json
CHANGED
|
@@ -38,17 +38,17 @@ if [ "$EVENT" = "SessionStart" ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
# PostToolUse / non-waiting PreToolUse: clear waiting state
|
|
41
|
-
if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ]; }; then
|
|
41
|
+
if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ] && [ "$TOOL_NAME" != "ExitPlanMode" ]; }; then
|
|
42
42
|
WFILE="$CCK_ACTIVITY/$SESSION_ID/_waiting.json"
|
|
43
43
|
rm -f "$WFILE"
|
|
44
44
|
[ "$EVENT" = "PostToolUse" ] && exit 0
|
|
45
45
|
fi
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
[ "$TOOL_NAME" = "EnterPlanMode" ]
|
|
47
|
+
# EnterPlanMode has no waiting semantics — skip
|
|
48
|
+
[ "$TOOL_NAME" = "EnterPlanMode" ] && exit 0
|
|
49
49
|
|
|
50
50
|
# Waiting-for-user events → write _waiting.json marker
|
|
51
|
-
if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" = "AskUserQuestion" ]; }; then
|
|
51
|
+
if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && { [ "$TOOL_NAME" = "AskUserQuestion" ] || [ "$TOOL_NAME" = "ExitPlanMode" ]; }; }; then
|
|
52
52
|
DIR="$CCK_ACTIVITY/$SESSION_ID"
|
|
53
53
|
mkdir -p "$DIR"
|
|
54
54
|
KIND="permission"
|
package/public/app.js
CHANGED
|
@@ -18,6 +18,7 @@ let bulkDeleteSessionId = null; // Track session for bulk delete
|
|
|
18
18
|
let ownerFilter = '';
|
|
19
19
|
let currentAgents = [];
|
|
20
20
|
let currentWaiting = null;
|
|
21
|
+
let lastWaitingHash = '';
|
|
21
22
|
let lastAgentsHash = '';
|
|
22
23
|
let messagePanelOpen = false;
|
|
23
24
|
let lastMessagesHash = '';
|
|
@@ -443,11 +444,12 @@ function renderActivityChip() {
|
|
|
443
444
|
let waiting = 0;
|
|
444
445
|
let active = 0;
|
|
445
446
|
for (const s of sessions) {
|
|
447
|
+
if (dismissedSessionIds.has(s.id)) continue;
|
|
446
448
|
if (s.hasWaitingForUser) waiting++;
|
|
447
449
|
else if (s.inProgress > 0 || s.hasRecentLog || s.hasRunningAgents) active++;
|
|
448
450
|
}
|
|
449
451
|
|
|
450
|
-
const key = `${waiting}|${active}|${[...activityFilter].sort().join(',')}`;
|
|
452
|
+
const key = `${waiting}|${active}|${dismissedSessionIds.size}|${[...activityFilter].sort().join(',')}`;
|
|
451
453
|
if (key === lastChipKey) return;
|
|
452
454
|
lastChipKey = key;
|
|
453
455
|
|
|
@@ -565,15 +567,18 @@ async function fetchTasks(sessionId) {
|
|
|
565
567
|
}
|
|
566
568
|
}
|
|
567
569
|
|
|
568
|
-
|
|
569
|
-
const _AGENT_STALE_MS = 5 * 60 * 1000; // kept for reference; no longer used for force-stopping
|
|
570
|
+
// #region TIMINGS
|
|
570
571
|
const WAITING_TTL_MS = 30 * 60 * 1000;
|
|
571
572
|
const AGENT_LOG_MAX = 8;
|
|
573
|
+
const LIVE_INDICATOR_MS = 10 * 1000;
|
|
574
|
+
const ACTIVE_PLAN_MS = 10 * 60 * 1000;
|
|
575
|
+
// #endregion
|
|
572
576
|
|
|
573
577
|
function resetAgentState() {
|
|
574
578
|
currentAgents = [];
|
|
575
579
|
currentWaiting = null;
|
|
576
580
|
lastAgentsHash = '';
|
|
581
|
+
lastWaitingHash = '';
|
|
577
582
|
renderAgentFooter();
|
|
578
583
|
}
|
|
579
584
|
|
|
@@ -595,6 +600,11 @@ async function fetchAgents(sessionId) {
|
|
|
595
600
|
for (const k of Object.keys(ownerColorCache)) delete ownerColorCache[k];
|
|
596
601
|
renderAgentFooter();
|
|
597
602
|
if (currentSessionId === sessionId) renderKanban();
|
|
603
|
+
const waitHash = JSON.stringify(currentWaiting);
|
|
604
|
+
if (waitHash !== lastWaitingHash) {
|
|
605
|
+
lastWaitingHash = waitHash;
|
|
606
|
+
if (messagePanelOpen && currentMessages.length) renderMessages(currentMessages);
|
|
607
|
+
}
|
|
598
608
|
} catch (e) {
|
|
599
609
|
console.error('[fetchAgents]', e);
|
|
600
610
|
}
|
|
@@ -899,6 +909,11 @@ function parseCommandMessage(text) {
|
|
|
899
909
|
return null;
|
|
900
910
|
}
|
|
901
911
|
|
|
912
|
+
function parseCommandArgs(text) {
|
|
913
|
+
const m = (text || '').match(/<command-args>([^<]*)<\/command-args>/);
|
|
914
|
+
return m?.[1].trim() || '';
|
|
915
|
+
}
|
|
916
|
+
|
|
902
917
|
function cleanMessageText(text) {
|
|
903
918
|
const cmd = parseCommandMessage(text);
|
|
904
919
|
if (cmd) return cmd;
|
|
@@ -1051,11 +1066,25 @@ function renderMessageList(messages) {
|
|
|
1051
1066
|
</div>`);
|
|
1052
1067
|
} else {
|
|
1053
1068
|
const cmd = parseCommandMessage(m.text);
|
|
1069
|
+
const cmdArgs = cmd ? parseCommandArgs(m.fullText || m.text) : '';
|
|
1054
1070
|
const displayText = cmd ? cmd : escapeHtml(cleanMessageText(m.text));
|
|
1055
1071
|
const isCmd = !!cmd;
|
|
1072
|
+
const cmdArgsHtml =
|
|
1073
|
+
cmd && cmdArgs ? ` <span style="color:var(--text-secondary)">${escapeHtml(cmdArgs)}</span>` : '';
|
|
1074
|
+
const chips = [];
|
|
1075
|
+
const imgCount = m.images?.length || 0;
|
|
1076
|
+
const trCount = m.toolResultRefs?.length || 0;
|
|
1077
|
+
if (imgCount) chips.push(`<span class="user-attach-chip">${imgCount} image${imgCount > 1 ? 's' : ''}</span>`);
|
|
1078
|
+
if (trCount)
|
|
1079
|
+
chips.push(`<span class="user-attach-chip">${trCount} tool result${trCount > 1 ? 's' : ''}</span>`);
|
|
1080
|
+
const chipsHtml = chips.length ? `<div class="user-attach-chips">${chips.join('')}</div>` : '';
|
|
1081
|
+
let textHtml;
|
|
1082
|
+
if (displayText) textHtml = isCmd ? `<code>${escapeHtml(displayText)}</code>` : displayText;
|
|
1083
|
+
else if (chips.length) textHtml = '<em class="msg-text-muted">(attachment)</em>';
|
|
1084
|
+
else textHtml = '';
|
|
1056
1085
|
parts.push(`<div class="msg-item msg-user${isCmd ? ' msg-cmd' : ''}" ${clickable}>
|
|
1057
1086
|
${MSG_ICON_USER}
|
|
1058
|
-
<div class="msg-body"><div class="msg-text">${
|
|
1087
|
+
<div class="msg-body"><div class="msg-text">${textHtml}${cmdArgsHtml}</div>${chipsHtml}<div class="msg-time">${formatDate(m.timestamp)}</div></div>${pinBtn}
|
|
1059
1088
|
</div>`);
|
|
1060
1089
|
}
|
|
1061
1090
|
} else if (m.type === 'assistant') {
|
|
@@ -1103,6 +1132,45 @@ function toggleToolGroup(id) {
|
|
|
1103
1132
|
if (el) el.classList.toggle('show');
|
|
1104
1133
|
}
|
|
1105
1134
|
|
|
1135
|
+
const WAITING_PLAN_PREVIEW_CHARS = 120;
|
|
1136
|
+
const WAITING_PREVIEW_MAX_CHARS = 200;
|
|
1137
|
+
|
|
1138
|
+
function getWaitingLabel(kind, tool) {
|
|
1139
|
+
if (kind !== 'question') return `Awaiting permission: ${tool}`;
|
|
1140
|
+
if (tool === 'ExitPlanMode') return 'Plan awaiting approval';
|
|
1141
|
+
return 'Question pending';
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function getWaitingPreview(toolInput) {
|
|
1145
|
+
if (!toolInput) return '';
|
|
1146
|
+
try {
|
|
1147
|
+
const parsed = JSON.parse(toolInput);
|
|
1148
|
+
if (parsed.questions?.[0]?.question) return parsed.questions[0].question;
|
|
1149
|
+
if (parsed.plan) {
|
|
1150
|
+
const t = parsed.plan.match(/^#\s+(.+)/m);
|
|
1151
|
+
return t ? t[1] : parsed.plan.slice(0, WAITING_PLAN_PREVIEW_CHARS);
|
|
1152
|
+
}
|
|
1153
|
+
if (parsed.command) return parsed.command;
|
|
1154
|
+
if (parsed.file_path) return parsed.file_path;
|
|
1155
|
+
} catch (_) {
|
|
1156
|
+
/* toolInput may be truncated/non-JSON */
|
|
1157
|
+
}
|
|
1158
|
+
return '';
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function renderWaitingEntry() {
|
|
1162
|
+
if (!currentWaiting?.timestamp) return '';
|
|
1163
|
+
const age = Date.now() - new Date(currentWaiting.timestamp).getTime();
|
|
1164
|
+
if (age >= WAITING_TTL_MS) return '';
|
|
1165
|
+
const tool = currentWaiting.toolName || 'unknown';
|
|
1166
|
+
const label = getWaitingLabel(currentWaiting.kind, tool);
|
|
1167
|
+
const preview = getWaitingPreview(currentWaiting.toolInput);
|
|
1168
|
+
const previewHtml = preview
|
|
1169
|
+
? `<div class="msg-waiting-preview">${escapeHtml(preview.slice(0, WAITING_PREVIEW_MAX_CHARS))}</div>`
|
|
1170
|
+
: '';
|
|
1171
|
+
return `<div class="msg-item msg-waiting">${ICON_CHAT}<div class="msg-body"><div class="msg-text">${escapeHtml(label)}</div>${previewHtml}<div class="msg-time">waiting…</div></div></div>`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1106
1174
|
function renderMessages(messages) {
|
|
1107
1175
|
const container = document.getElementById('message-panel-content');
|
|
1108
1176
|
const pinnedContainer = document.getElementById('message-panel-pinned');
|
|
@@ -1116,7 +1184,7 @@ function renderMessages(messages) {
|
|
|
1116
1184
|
currentMessages.length >= MSG_MAX_LOADED
|
|
1117
1185
|
? `<div class="msg-limit-banner">Showing last ${MSG_MAX_LOADED} messages</div>`
|
|
1118
1186
|
: '';
|
|
1119
|
-
container.innerHTML = limitBanner + msgsHtml;
|
|
1187
|
+
container.innerHTML = limitBanner + msgsHtml + renderWaitingEntry();
|
|
1120
1188
|
if (!msgUserScrolledUp) container.scrollTop = container.scrollHeight;
|
|
1121
1189
|
// Auto-load more if content doesn't overflow yet
|
|
1122
1190
|
if (
|
|
@@ -1152,6 +1220,16 @@ const ICON_TASK =
|
|
|
1152
1220
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
|
1153
1221
|
const ICON_WEB =
|
|
1154
1222
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
|
1223
|
+
const ICON_OPEN_EXTERNAL =
|
|
1224
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3h7v7"/><path d="M10 14L21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>';
|
|
1225
|
+
const ICON_COPY =
|
|
1226
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
1227
|
+
const ICON_CHECKMARK =
|
|
1228
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M20 6L9 17l-5-5"/></svg>';
|
|
1229
|
+
const ICON_AGENT_WAITING =
|
|
1230
|
+
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
|
|
1231
|
+
const ICON_CHAT =
|
|
1232
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
|
1155
1233
|
const TOOL_ICONS = {
|
|
1156
1234
|
Bash: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="18" rx="2"/><polyline points="7 10 10 13 7 16"/><line x1="13" y1="16" x2="17" y2="16"/></svg>',
|
|
1157
1235
|
Read: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
@@ -1169,8 +1247,8 @@ const TOOL_ICONS = {
|
|
|
1169
1247
|
TaskList: ICON_TASK,
|
|
1170
1248
|
ToolSearch:
|
|
1171
1249
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>',
|
|
1172
|
-
AskUserQuestion:
|
|
1173
|
-
|
|
1250
|
+
AskUserQuestion: ICON_CHAT,
|
|
1251
|
+
ExitPlanMode: ICON_CHAT,
|
|
1174
1252
|
Skill:
|
|
1175
1253
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
|
1176
1254
|
WebFetch: ICON_WEB,
|
|
@@ -1470,10 +1548,6 @@ function _renderPinToDetail(pin) {
|
|
|
1470
1548
|
}
|
|
1471
1549
|
|
|
1472
1550
|
const SESSION_PIN_SVG = PIN_SVG.replace('width="14" height="14"', 'width="12" height="12"');
|
|
1473
|
-
const MARKETPLACE_SVG =
|
|
1474
|
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>';
|
|
1475
|
-
const MEMORY_SVG =
|
|
1476
|
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>';
|
|
1477
1551
|
const LINK_SVG_PATHS =
|
|
1478
1552
|
'<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"/>';
|
|
1479
1553
|
const linkSvg = (size) =>
|
|
@@ -1482,6 +1556,40 @@ const linkSvg = (size) =>
|
|
|
1482
1556
|
//#endregion
|
|
1483
1557
|
|
|
1484
1558
|
//#region MODALS
|
|
1559
|
+
function renderUserAttachments(m) {
|
|
1560
|
+
const parts = [];
|
|
1561
|
+
if (m.images?.length && m.uuid && currentSessionId) {
|
|
1562
|
+
const imgs = m.images
|
|
1563
|
+
.map((img) => {
|
|
1564
|
+
const url = `/api/sessions/${encodeURIComponent(currentSessionId)}/user-image/${encodeURIComponent(m.uuid)}/${img.blockIndex}`;
|
|
1565
|
+
return `<img src="${url}" loading="lazy" alt="user image" class="user-attach-image" />`;
|
|
1566
|
+
})
|
|
1567
|
+
.join('');
|
|
1568
|
+
parts.push(
|
|
1569
|
+
`<div class="user-attach-section"><div class="user-attach-label">Attached images</div><div class="user-attach-images">${imgs}</div></div>`,
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
if (m.toolResultRefs?.length) {
|
|
1573
|
+
const refs = m.toolResultRefs
|
|
1574
|
+
.map((ref) => {
|
|
1575
|
+
const safeId = escapeHtml(ref.toolUseId);
|
|
1576
|
+
const shortId = ref.toolUseId.length > 14 ? `${ref.toolUseId.slice(0, 14)}…` : ref.toolUseId;
|
|
1577
|
+
const preview = ref.preview ? escapeHtml(ref.preview) : '<em>(no text)</em>';
|
|
1578
|
+
const expandId = `user-tr-${ref.toolUseId}`;
|
|
1579
|
+
return `<details class="user-attach-toolresult">
|
|
1580
|
+
<summary>Tool result <code>${escapeHtml(shortId)}</code></summary>
|
|
1581
|
+
<pre class="${TINTED_PRE_CLASS}" id="${expandId}">${preview}</pre>
|
|
1582
|
+
<button type="button" class="tool-result-expand-btn" data-expand-id="${expandId}" data-tool-use-id="${safeId}" onclick="_toggleToolResultExpand(this)">Show full</button>
|
|
1583
|
+
</details>`;
|
|
1584
|
+
})
|
|
1585
|
+
.join('');
|
|
1586
|
+
parts.push(
|
|
1587
|
+
`<div class="user-attach-section"><div class="user-attach-label">Tool results in this message</div>${refs}</div>`,
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return parts.join('');
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1485
1593
|
function showMsgDetail(idx) {
|
|
1486
1594
|
currentMsgDetailIdx = idx;
|
|
1487
1595
|
const m = currentMessages[idx];
|
|
@@ -1546,25 +1654,27 @@ function showMsgDetail(idx) {
|
|
|
1546
1654
|
body.innerHTML = renderMarkdown(text);
|
|
1547
1655
|
}
|
|
1548
1656
|
} else {
|
|
1549
|
-
const rawText = stripAnsi(m.fullText || m.text);
|
|
1657
|
+
const rawText = stripAnsi(m.fullText || m.text || '');
|
|
1550
1658
|
const cmd = m.type === 'user' ? parseCommandMessage(rawText) : null;
|
|
1551
1659
|
document.getElementById('msg-detail-title').textContent =
|
|
1552
1660
|
m.type === 'assistant' ? 'Claude' : m.systemLabel ? 'System' : 'User';
|
|
1553
1661
|
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
1662
|
+
const userExtras = m.type === 'user' ? renderUserAttachments(m) : '';
|
|
1554
1663
|
if (m.compactSummary) {
|
|
1555
|
-
body.innerHTML = renderMarkdown(m.compactSummary);
|
|
1664
|
+
body.innerHTML = renderMarkdown(m.compactSummary) + userExtras;
|
|
1556
1665
|
} else if (cmd) {
|
|
1557
|
-
const
|
|
1558
|
-
const args = argsMatch?.[1].trim() ? argsMatch[1].trim() : null;
|
|
1666
|
+
const args = parseCommandArgs(rawText) || null;
|
|
1559
1667
|
const cleanBody = rawText
|
|
1560
1668
|
.replace(/<command-[^>]+>[\s\S]*?<\/command-[^>]+>/g, '')
|
|
1561
1669
|
.replace(/<local-command-[^>]+>[\s\S]*?<\/local-command-[^>]+>/g, '')
|
|
1562
1670
|
.trim();
|
|
1563
1671
|
let cmdHtml = `<code>${escapeHtml(cmd)}${args ? ` ${escapeHtml(args)}` : ''}</code>`;
|
|
1564
1672
|
if (cleanBody) cmdHtml += `<div style="margin-top:10px">${renderMarkdown(cleanBody)}</div>`;
|
|
1565
|
-
body.innerHTML = cmdHtml;
|
|
1673
|
+
body.innerHTML = cmdHtml + userExtras;
|
|
1674
|
+
} else if (rawText) {
|
|
1675
|
+
body.innerHTML = renderMarkdown(rawText) + userExtras;
|
|
1566
1676
|
} else {
|
|
1567
|
-
body.innerHTML =
|
|
1677
|
+
body.innerHTML = userExtras || '<em>No content</em>';
|
|
1568
1678
|
}
|
|
1569
1679
|
}
|
|
1570
1680
|
const modal = document.getElementById('msg-detail-modal').querySelector('.modal');
|
|
@@ -1649,8 +1759,7 @@ async function copyWithFeedback(text, btn) {
|
|
|
1649
1759
|
await navigator.clipboard.writeText(text);
|
|
1650
1760
|
btn.dataset.copying = '1';
|
|
1651
1761
|
const svg = btn.innerHTML;
|
|
1652
|
-
btn.innerHTML =
|
|
1653
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M20 6L9 17l-5-5"/></svg>';
|
|
1762
|
+
btn.innerHTML = ICON_CHECKMARK;
|
|
1654
1763
|
setTimeout(() => {
|
|
1655
1764
|
btn.innerHTML = svg;
|
|
1656
1765
|
delete btn.dataset.copying;
|
|
@@ -2322,10 +2431,8 @@ function renderSessions() {
|
|
|
2322
2431
|
|
|
2323
2432
|
// Filter pipeline: active filter → force-include revealed/current (non-pinned) sessions →
|
|
2324
2433
|
// project filter → search filter → ensure pinned/sticky sessions are always included
|
|
2325
|
-
const LIVE_INDICATOR_MS = 10 * 1000;
|
|
2326
2434
|
let filteredSessions = sessions;
|
|
2327
2435
|
if (sessionFilter === 'active') {
|
|
2328
|
-
const ACTIVE_PLAN_MS = 15 * 60 * 1000;
|
|
2329
2436
|
const now = Date.now();
|
|
2330
2437
|
const activeSessionIds = new Set();
|
|
2331
2438
|
filteredSessions = filteredSessions.filter((s) => {
|
|
@@ -2476,12 +2583,9 @@ function renderSessions() {
|
|
|
2476
2583
|
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
2477
2584
|
${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
2585
|
${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>` : ''}
|
|
2479
|
-
${session.
|
|
2480
|
-
${session.
|
|
2481
|
-
${session.
|
|
2482
|
-
${(window.__HUB__?.enabled || appConfig.marketplaceUrl) && session.project ? `<span class="marketplace-btn" data-project-path="${escapeHtml(session.project)}" onclick="event.stopPropagation(); openMarketplace(this.dataset.projectPath)" title="Open in Marketplace">${MARKETPLACE_SVG}</span>` : ''}
|
|
2483
|
-
${(window.__HUB__?.enabled || appConfig.memoryUrl) && session.project ? `<span class="marketplace-btn" data-project-path="${escapeHtml(session.project)}" onclick="event.stopPropagation(); openMemory(this.dataset.projectPath)" title="Open in Memory">${MEMORY_SVG}</span>` : ''}
|
|
2484
|
-
${isLive ? '<span class="pulse"></span>' : ''}
|
|
2586
|
+
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')"><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>` : ''}
|
|
2587
|
+
${session.hasWaitingForUser ? `<span class="agent-badge agent-badge-waiting" title="Waiting for user">${ICON_AGENT_WAITING}</span>` : ''}
|
|
2588
|
+
${isLive || session.hasRunningAgents ? `<span class="pulse" title="${isLive ? 'Live' : 'Active agents'}"></span>` : ''}
|
|
2485
2589
|
</span>
|
|
2486
2590
|
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
|
|
2487
2591
|
<span class="progress-text">${session.completed}/${total}</span>
|
|
@@ -4380,6 +4484,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
4380
4484
|
dismissedSessionIds.add(contextSid);
|
|
4381
4485
|
updateDismissBtnState();
|
|
4382
4486
|
renderSessions();
|
|
4487
|
+
renderActivityChip();
|
|
4383
4488
|
const newItems = getNavigableItems();
|
|
4384
4489
|
const targetIdx = newItems.length > 0 ? Math.max(0, prevIdx - 1) : -1;
|
|
4385
4490
|
// If the dismissed session is currently open, navigate to the previous one
|
|
@@ -5088,7 +5193,7 @@ function renderAgentTabs(promptHtml, responseHtml, promptText, responseText) {
|
|
|
5088
5193
|
}
|
|
5089
5194
|
if (!tabs.length) return '';
|
|
5090
5195
|
const defaultTab = responseHtml ? 'response' : tabs[0].key;
|
|
5091
|
-
const copyBtnHtml = `<button class="agent-tab-copy" title="Copy" onclick="copyAgentTabActive('${id}',this)"
|
|
5196
|
+
const copyBtnHtml = `<button class="agent-tab-copy" title="Copy" onclick="copyAgentTabActive('${id}',this)">${ICON_COPY}</button>`;
|
|
5092
5197
|
const tabsHtml = tabs
|
|
5093
5198
|
.map(
|
|
5094
5199
|
(t) =>
|
|
@@ -5493,13 +5598,13 @@ async function showSessionInfoModal(sessionId) {
|
|
|
5493
5598
|
// and re-rendered when they arrive, so the modal doesn't block on network.
|
|
5494
5599
|
_planSessionId = sessionId;
|
|
5495
5600
|
const cachedTasks = currentSessionId === sessionId ? currentTasks : [];
|
|
5496
|
-
showInfoModal(session, null, cachedTasks, null);
|
|
5601
|
+
showInfoModal(session, null, cachedTasks, null, null);
|
|
5497
5602
|
|
|
5498
|
-
const rerender = (teamConfig, tasks, planContent) => {
|
|
5603
|
+
const rerender = (teamConfig, tasks, planContent, parentInfo) => {
|
|
5499
5604
|
if (_planSessionId !== sessionId) return; // user opened a different modal
|
|
5500
5605
|
const modal = document.getElementById('team-modal');
|
|
5501
5606
|
if (!modal?.classList.contains('visible')) return; // user closed modal — don't reopen
|
|
5502
|
-
showInfoModal(session, teamConfig, tasks, planContent);
|
|
5607
|
+
showInfoModal(session, teamConfig, tasks, planContent, parentInfo);
|
|
5503
5608
|
};
|
|
5504
5609
|
|
|
5505
5610
|
const teamPromise = session.isTeam
|
|
@@ -5520,8 +5625,17 @@ async function showSessionInfoModal(sessionId) {
|
|
|
5520
5625
|
.then((r) => (r.ok ? r.json() : []))
|
|
5521
5626
|
.catch(() => []);
|
|
5522
5627
|
|
|
5523
|
-
const
|
|
5524
|
-
|
|
5628
|
+
const parentPromise = fetch(`/api/sessions/${sessionId}/parent`)
|
|
5629
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
5630
|
+
.catch(() => null);
|
|
5631
|
+
|
|
5632
|
+
const [teamConfig, planContent, tasks, parentInfo] = await Promise.all([
|
|
5633
|
+
teamPromise,
|
|
5634
|
+
planPromise,
|
|
5635
|
+
tasksPromise,
|
|
5636
|
+
parentPromise,
|
|
5637
|
+
]);
|
|
5638
|
+
rerender(teamConfig, tasks, planContent, parentInfo);
|
|
5525
5639
|
}
|
|
5526
5640
|
|
|
5527
5641
|
let _infoModalSessionId = null;
|
|
@@ -5538,7 +5652,7 @@ function updateStickyBtnState() {
|
|
|
5538
5652
|
if (svg) svg.setAttribute('fill', isSticky ? 'currentColor' : 'none');
|
|
5539
5653
|
}
|
|
5540
5654
|
|
|
5541
|
-
function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
5655
|
+
function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
5542
5656
|
const modal = document.getElementById('team-modal');
|
|
5543
5657
|
const titleEl = document.getElementById('team-modal-title');
|
|
5544
5658
|
const bodyEl = document.getElementById('team-modal-body');
|
|
@@ -5558,6 +5672,13 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5558
5672
|
// Each row: [label, displayValue, { openPath?, copyValue? }]
|
|
5559
5673
|
const infoRows = [];
|
|
5560
5674
|
infoRows.push(['Session', session.id, { openClaudeDir: true, openFile: session.jsonlPath }]);
|
|
5675
|
+
if (parentInfo?.parentSessionId) {
|
|
5676
|
+
infoRows.push([
|
|
5677
|
+
'Forked from',
|
|
5678
|
+
parentInfo.parentSessionId,
|
|
5679
|
+
{ openClaudeDir: true, openFile: parentInfo.parentJsonlPath, openSession: parentInfo.parentSessionId },
|
|
5680
|
+
]);
|
|
5681
|
+
}
|
|
5561
5682
|
if (session.slug && session.hasPlan) {
|
|
5562
5683
|
infoRows.push(['Slug', session.slug, { openClaudeDir: true, openFile: session.planPath }]);
|
|
5563
5684
|
}
|
|
@@ -5589,19 +5710,25 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5589
5710
|
"font-family: 'IBM Plex Mono', monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; color: var(--accent-text); text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px;";
|
|
5590
5711
|
const plainStyle =
|
|
5591
5712
|
"font-family: 'IBM Plex Mono', monospace; font-size: 12px; user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
|
|
5592
|
-
html += `<div class="team-modal-meta
|
|
5713
|
+
html += `<div class="team-modal-meta info-grid">`;
|
|
5593
5714
|
infoRows.forEach(([label, value, opts]) => {
|
|
5594
5715
|
const copyVal = escapeHtml(value).replace(/"/g, '"');
|
|
5595
5716
|
html += `<span style="font-weight: 500; color: var(--text-secondary); font-size: 12px; white-space: nowrap;">${label}</span>`;
|
|
5596
|
-
if (opts?.
|
|
5597
|
-
const
|
|
5598
|
-
|
|
5599
|
-
html += `<span data-folder="${folder}" data-file="${file}" data-claude-dir="${opts.openClaudeDir ? '1' : ''}" onclick="openFolderInEditor(this.dataset.claudeDir ? undefined : this.dataset.folder, this.dataset.file || undefined)" style="${clickableStyle}" title="Open in editor">${escapeHtml(value)}</span>`;
|
|
5717
|
+
if (opts?.openSession) {
|
|
5718
|
+
const sid = _escapeForJsAttr(escapeHtml(opts.openSession).replace(/"/g, '"'));
|
|
5719
|
+
html += `<span onclick="openSessionFromInfo('${sid}')" style="${clickableStyle}" title="Open session in app">${escapeHtml(value)}</span>`;
|
|
5600
5720
|
} else {
|
|
5601
5721
|
html += `<span style="${plainStyle}" title="${copyVal}">${escapeHtml(value)}</span>`;
|
|
5602
5722
|
}
|
|
5603
5723
|
const jsCopyVal = _escapeForJsAttr(copyVal);
|
|
5604
|
-
|
|
5724
|
+
const copyBtn = `<button onclick="copyWithFeedback('${jsCopyVal}', this)" title="Copy">${ICON_COPY}</button>`;
|
|
5725
|
+
let openBtn = '';
|
|
5726
|
+
if (opts?.openClaudeDir || opts?.openPath) {
|
|
5727
|
+
const folder = opts.openClaudeDir ? '' : escapeHtml(opts.openPath).replace(/"/g, '"');
|
|
5728
|
+
const file = opts.openFile ? escapeHtml(opts.openFile).replace(/"/g, '"') : '';
|
|
5729
|
+
openBtn = `<button data-folder="${folder}" data-file="${file}" data-claude-dir="${opts.openClaudeDir ? '1' : ''}" onclick="openFolderInEditor(this.dataset.claudeDir ? undefined : this.dataset.folder, this.dataset.file || undefined)" title="Open in editor">${ICON_OPEN_EXTERNAL}</button>`;
|
|
5730
|
+
}
|
|
5731
|
+
html += `<span class="info-row-actions">${copyBtn}${openBtn}</span>`;
|
|
5605
5732
|
});
|
|
5606
5733
|
html += `</div>`;
|
|
5607
5734
|
|
|
@@ -5687,6 +5814,11 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5687
5814
|
updateDismissBtnState();
|
|
5688
5815
|
const costBtn = document.getElementById('session-info-cost-btn');
|
|
5689
5816
|
if (costBtn) costBtn.style.display = window.__HUB__?.enabled || appConfig.costUrl ? '' : 'none';
|
|
5817
|
+
const mkBtn = document.getElementById('session-info-marketplace-btn');
|
|
5818
|
+
const memBtn = document.getElementById('session-info-memory-btn');
|
|
5819
|
+
const proj = session.project;
|
|
5820
|
+
if (mkBtn) mkBtn.style.display = proj && (window.__HUB__?.enabled || appConfig.marketplaceUrl) ? '' : 'none';
|
|
5821
|
+
if (memBtn) memBtn.style.display = proj && (window.__HUB__?.enabled || appConfig.memoryUrl) ? '' : 'none';
|
|
5690
5822
|
modal.classList.add('visible');
|
|
5691
5823
|
|
|
5692
5824
|
if (alreadyVisible) return; // re-render during deferred hydration — key handler already attached
|
|
@@ -5707,6 +5839,12 @@ function closeTeamModal() {
|
|
|
5707
5839
|
_planSessionId = null;
|
|
5708
5840
|
}
|
|
5709
5841
|
|
|
5842
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5843
|
+
function openSessionFromInfo(sessionId) {
|
|
5844
|
+
closeTeamModal();
|
|
5845
|
+
fetchTasks(sessionId);
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5710
5848
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5711
5849
|
function toggleDismissSession(sessionId) {
|
|
5712
5850
|
if (dismissedSessionIds.has(sessionId)) {
|
|
@@ -5716,6 +5854,7 @@ function toggleDismissSession(sessionId) {
|
|
|
5716
5854
|
}
|
|
5717
5855
|
updateDismissBtnState();
|
|
5718
5856
|
renderSessions();
|
|
5857
|
+
renderActivityChip();
|
|
5719
5858
|
}
|
|
5720
5859
|
|
|
5721
5860
|
function updateDismissBtnState() {
|
|
@@ -5801,7 +5940,6 @@ function openCost(sessionId) {
|
|
|
5801
5940
|
}
|
|
5802
5941
|
}
|
|
5803
5942
|
|
|
5804
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5805
5943
|
function openMarketplace(projectPath) {
|
|
5806
5944
|
const params = new URLSearchParams({ project: projectPath });
|
|
5807
5945
|
if (window.__HUB__?.enabled) {
|
|
@@ -5813,7 +5951,6 @@ function openMarketplace(projectPath) {
|
|
|
5813
5951
|
}
|
|
5814
5952
|
}
|
|
5815
5953
|
|
|
5816
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5817
5954
|
function openMemory(projectPath) {
|
|
5818
5955
|
const params = new URLSearchParams({ project: projectPath });
|
|
5819
5956
|
if (window.__HUB__?.enabled) {
|
|
@@ -5825,6 +5962,19 @@ function openMemory(projectPath) {
|
|
|
5825
5962
|
}
|
|
5826
5963
|
}
|
|
5827
5964
|
|
|
5965
|
+
function openForInfoModalProject(open) {
|
|
5966
|
+
const s = sessions.find((x) => x.id === _infoModalSessionId);
|
|
5967
|
+
if (s?.project) open(s.project);
|
|
5968
|
+
}
|
|
5969
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5970
|
+
function openMarketplaceForInfoModal() {
|
|
5971
|
+
openForInfoModalProject(openMarketplace);
|
|
5972
|
+
}
|
|
5973
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5974
|
+
function openMemoryForInfoModal() {
|
|
5975
|
+
openForInfoModalProject(openMemory);
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5828
5978
|
//#endregion
|
|
5829
5979
|
|
|
5830
5980
|
//#region OWNER_FILTER
|
|
@@ -5990,6 +6140,12 @@ function makeLimitCell(label, bucket) {
|
|
|
5990
6140
|
const strong = document.createElement('strong');
|
|
5991
6141
|
strong.textContent = pct == null ? '-%' : `${Math.ceil(pct)}%`;
|
|
5992
6142
|
cell.appendChild(strong);
|
|
6143
|
+
if (reset) {
|
|
6144
|
+
const r = document.createElement('span');
|
|
6145
|
+
r.className = 'footer-limit-reset';
|
|
6146
|
+
r.textContent = ` (${reset})`;
|
|
6147
|
+
cell.appendChild(r);
|
|
6148
|
+
}
|
|
5993
6149
|
return cell;
|
|
5994
6150
|
}
|
|
5995
6151
|
function makeLimitSpan(rl) {
|
package/public/index.html
CHANGED
|
@@ -527,6 +527,12 @@
|
|
|
527
527
|
<button id="session-info-cost-btn" class="icon-btn" style="display:none" title="Open in Cost" onclick="openCost(_infoModalSessionId)">
|
|
528
528
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
|
|
529
529
|
</button>
|
|
530
|
+
<button id="session-info-marketplace-btn" class="icon-btn" style="display:none" title="Open in Marketplace" onclick="openMarketplaceForInfoModal()">
|
|
531
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
|
|
532
|
+
</button>
|
|
533
|
+
<button id="session-info-memory-btn" class="icon-btn" style="display:none" title="Open in Memory" onclick="openMemoryForInfoModal()">
|
|
534
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
|
535
|
+
</button>
|
|
530
536
|
<button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
|
|
531
537
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
532
538
|
<path d="M18 6L6 18M6 6l12 12"/>
|
package/public/style.css
CHANGED
|
@@ -724,6 +724,12 @@ body::before {
|
|
|
724
724
|
font-weight: 600;
|
|
725
725
|
}
|
|
726
726
|
|
|
727
|
+
.sidebar-footer .footer-limits .footer-limit-reset {
|
|
728
|
+
color: var(--text-tertiary);
|
|
729
|
+
font-weight: 400;
|
|
730
|
+
opacity: 0.75;
|
|
731
|
+
}
|
|
732
|
+
|
|
727
733
|
.sidebar-footer a:hover {
|
|
728
734
|
color: var(--text-secondary);
|
|
729
735
|
}
|
|
@@ -1743,6 +1749,55 @@ body::before {
|
|
|
1743
1749
|
border-top: 1px solid var(--border);
|
|
1744
1750
|
}
|
|
1745
1751
|
|
|
1752
|
+
.info-grid {
|
|
1753
|
+
display: grid;
|
|
1754
|
+
grid-template-columns: auto 1fr auto;
|
|
1755
|
+
gap: 6px 12px;
|
|
1756
|
+
align-items: center;
|
|
1757
|
+
margin-bottom: 16px;
|
|
1758
|
+
}
|
|
1759
|
+
.info-row-actions {
|
|
1760
|
+
display: flex;
|
|
1761
|
+
gap: 4px;
|
|
1762
|
+
opacity: 0.3;
|
|
1763
|
+
transition: opacity 0.15s ease;
|
|
1764
|
+
}
|
|
1765
|
+
.info-grid:hover .info-row-actions {
|
|
1766
|
+
opacity: 1;
|
|
1767
|
+
}
|
|
1768
|
+
.info-row-actions button {
|
|
1769
|
+
display: inline-flex;
|
|
1770
|
+
align-items: center;
|
|
1771
|
+
justify-content: center;
|
|
1772
|
+
width: 22px;
|
|
1773
|
+
height: 22px;
|
|
1774
|
+
padding: 0;
|
|
1775
|
+
background: var(--bg-elevated);
|
|
1776
|
+
border: 1px solid var(--border);
|
|
1777
|
+
border-radius: 4px;
|
|
1778
|
+
color: var(--text-secondary);
|
|
1779
|
+
cursor: pointer;
|
|
1780
|
+
transition:
|
|
1781
|
+
background 0.1s ease,
|
|
1782
|
+
border-color 0.1s ease,
|
|
1783
|
+
color 0.1s ease;
|
|
1784
|
+
}
|
|
1785
|
+
.info-row-actions button:hover {
|
|
1786
|
+
background: var(--bg-hover);
|
|
1787
|
+
border-color: var(--accent);
|
|
1788
|
+
color: var(--accent-text);
|
|
1789
|
+
}
|
|
1790
|
+
.info-row-actions button svg {
|
|
1791
|
+
width: 12px;
|
|
1792
|
+
height: 12px;
|
|
1793
|
+
display: block;
|
|
1794
|
+
}
|
|
1795
|
+
@media (hover: none) {
|
|
1796
|
+
.info-row-actions {
|
|
1797
|
+
opacity: 1;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1746
1801
|
/* #endregion */
|
|
1747
1802
|
|
|
1748
1803
|
/* #region OWNER_FILTER */
|
|
@@ -2107,6 +2162,11 @@ body::before {
|
|
|
2107
2162
|
opacity: 1;
|
|
2108
2163
|
color: var(--text-primary);
|
|
2109
2164
|
}
|
|
2165
|
+
.agent-tab-copy svg {
|
|
2166
|
+
width: 14px;
|
|
2167
|
+
height: 14px;
|
|
2168
|
+
display: block;
|
|
2169
|
+
}
|
|
2110
2170
|
.toast {
|
|
2111
2171
|
position: fixed;
|
|
2112
2172
|
bottom: 16px;
|
|
@@ -2222,6 +2282,59 @@ body::before {
|
|
|
2222
2282
|
border-radius: 3px;
|
|
2223
2283
|
font-size: 11px;
|
|
2224
2284
|
}
|
|
2285
|
+
.user-attach-chips {
|
|
2286
|
+
display: flex;
|
|
2287
|
+
gap: 4px;
|
|
2288
|
+
flex-wrap: wrap;
|
|
2289
|
+
margin-top: 4px;
|
|
2290
|
+
}
|
|
2291
|
+
.user-attach-chip {
|
|
2292
|
+
font-size: 10px;
|
|
2293
|
+
padding: 1px 6px;
|
|
2294
|
+
border-radius: 10px;
|
|
2295
|
+
background: var(--bg-hover);
|
|
2296
|
+
color: var(--text-secondary);
|
|
2297
|
+
}
|
|
2298
|
+
.msg-text-muted {
|
|
2299
|
+
color: var(--text-secondary);
|
|
2300
|
+
font-size: 12px;
|
|
2301
|
+
}
|
|
2302
|
+
.user-attach-section {
|
|
2303
|
+
margin-top: 12px;
|
|
2304
|
+
padding-top: 10px;
|
|
2305
|
+
border-top: 1px solid var(--border);
|
|
2306
|
+
}
|
|
2307
|
+
.user-attach-label {
|
|
2308
|
+
font-size: 11px;
|
|
2309
|
+
color: var(--text-secondary);
|
|
2310
|
+
margin-bottom: 6px;
|
|
2311
|
+
text-transform: uppercase;
|
|
2312
|
+
letter-spacing: 0.5px;
|
|
2313
|
+
}
|
|
2314
|
+
.user-attach-images {
|
|
2315
|
+
display: flex;
|
|
2316
|
+
flex-wrap: wrap;
|
|
2317
|
+
gap: 8px;
|
|
2318
|
+
}
|
|
2319
|
+
.user-attach-image {
|
|
2320
|
+
max-width: 100%;
|
|
2321
|
+
max-height: 480px;
|
|
2322
|
+
border-radius: 4px;
|
|
2323
|
+
border: 1px solid var(--border);
|
|
2324
|
+
background: var(--bg-hover);
|
|
2325
|
+
}
|
|
2326
|
+
.user-attach-toolresult {
|
|
2327
|
+
margin-top: 6px;
|
|
2328
|
+
}
|
|
2329
|
+
.user-attach-toolresult > summary {
|
|
2330
|
+
cursor: pointer;
|
|
2331
|
+
font-size: 12px;
|
|
2332
|
+
color: var(--text-secondary);
|
|
2333
|
+
padding: 4px 0;
|
|
2334
|
+
}
|
|
2335
|
+
.user-attach-toolresult > summary code {
|
|
2336
|
+
font-size: 10px;
|
|
2337
|
+
}
|
|
2225
2338
|
.msg-item.msg-system {
|
|
2226
2339
|
border-left: 3px solid var(--border);
|
|
2227
2340
|
}
|
|
@@ -2241,6 +2354,30 @@ body::before {
|
|
|
2241
2354
|
border-left: 3px solid var(--border);
|
|
2242
2355
|
opacity: 0.75;
|
|
2243
2356
|
}
|
|
2357
|
+
.msg-item.msg-waiting {
|
|
2358
|
+
border-left: 3px solid var(--warning, #f5a623);
|
|
2359
|
+
background: color-mix(in srgb, var(--warning, #f5a623) 8%, transparent);
|
|
2360
|
+
animation: msg-waiting-pulse 2s ease-in-out infinite;
|
|
2361
|
+
}
|
|
2362
|
+
.msg-waiting .msg-text {
|
|
2363
|
+
font-weight: 600;
|
|
2364
|
+
}
|
|
2365
|
+
.msg-waiting-preview {
|
|
2366
|
+
font-size: 11px;
|
|
2367
|
+
opacity: 0.85;
|
|
2368
|
+
margin-top: 2px;
|
|
2369
|
+
white-space: pre-wrap;
|
|
2370
|
+
word-break: break-word;
|
|
2371
|
+
}
|
|
2372
|
+
@keyframes msg-waiting-pulse {
|
|
2373
|
+
0%,
|
|
2374
|
+
100% {
|
|
2375
|
+
background: color-mix(in srgb, var(--warning, #f5a623) 8%, transparent);
|
|
2376
|
+
}
|
|
2377
|
+
50% {
|
|
2378
|
+
background: color-mix(in srgb, var(--warning, #f5a623) 16%, transparent);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2244
2381
|
.msg-item.msg-idle .msg-icon {
|
|
2245
2382
|
width: 12px;
|
|
2246
2383
|
height: 12px;
|
|
@@ -2531,8 +2668,26 @@ body::before {
|
|
|
2531
2668
|
border: 1px solid var(--border);
|
|
2532
2669
|
}
|
|
2533
2670
|
.agent-badge {
|
|
2534
|
-
|
|
2671
|
+
display: inline-flex;
|
|
2672
|
+
align-items: center;
|
|
2673
|
+
justify-content: center;
|
|
2535
2674
|
cursor: default;
|
|
2675
|
+
line-height: 1;
|
|
2676
|
+
flex-shrink: 0;
|
|
2677
|
+
color: var(--text-secondary);
|
|
2678
|
+
}
|
|
2679
|
+
.agent-badge-waiting {
|
|
2680
|
+
color: var(--warning);
|
|
2681
|
+
animation: agent-badge-pulse 1.6s ease-in-out infinite;
|
|
2682
|
+
}
|
|
2683
|
+
@keyframes agent-badge-pulse {
|
|
2684
|
+
0%,
|
|
2685
|
+
100% {
|
|
2686
|
+
opacity: 0.65;
|
|
2687
|
+
}
|
|
2688
|
+
50% {
|
|
2689
|
+
opacity: 1;
|
|
2690
|
+
}
|
|
2536
2691
|
}
|
|
2537
2692
|
|
|
2538
2693
|
.linked-docs-badge,
|
|
@@ -3502,22 +3657,6 @@ pre.mermaid svg {
|
|
|
3502
3657
|
color: var(--accent);
|
|
3503
3658
|
}
|
|
3504
3659
|
|
|
3505
|
-
.marketplace-btn {
|
|
3506
|
-
color: #888;
|
|
3507
|
-
cursor: pointer;
|
|
3508
|
-
display: inline-flex;
|
|
3509
|
-
align-items: center;
|
|
3510
|
-
transition:
|
|
3511
|
-
color 0.15s,
|
|
3512
|
-
filter 0.15s;
|
|
3513
|
-
border-radius: 3px;
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
|
-
.marketplace-btn:hover {
|
|
3517
|
-
color: var(--accent);
|
|
3518
|
-
filter: drop-shadow(0 0 3px var(--accent));
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
3660
|
.project-group-header .group-count {
|
|
3522
3661
|
font-weight: 400;
|
|
3523
3662
|
color: var(--text-muted);
|
package/server.js
CHANGED
|
@@ -20,7 +20,8 @@ const {
|
|
|
20
20
|
findTerminatedTeammates,
|
|
21
21
|
extractPromptFromTranscript,
|
|
22
22
|
extractModelFromTranscript,
|
|
23
|
-
readFullToolResult
|
|
23
|
+
readFullToolResult,
|
|
24
|
+
readUserImage
|
|
24
25
|
} = require('./lib/parsers');
|
|
25
26
|
|
|
26
27
|
if (process.argv.includes("--install") || process.argv.includes("--uninstall")) {
|
|
@@ -95,12 +96,16 @@ function writePins(pins) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
const WAITING_RESOLVE_GRACE_MS =
|
|
99
|
+
// #region TIMINGS
|
|
100
|
+
const PERMISSION_TTL_MS = 30 * 60 * 1000;
|
|
101
|
+
const AGENT_TTL_MS = 60 * 60 * 1000;
|
|
102
|
+
const AGENT_STALE_MS = 30 * 60 * 1000; // safety net for crashed sessions
|
|
103
|
+
const SESSION_STALE_MS = 5 * 60 * 1000;
|
|
104
|
+
const WAITING_RESOLVE_GRACE_MS = 15 * 1000;
|
|
105
|
+
const CTX_CLEANUP_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
106
|
+
const CLEANUP_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
|
|
107
|
+
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
|
|
108
|
+
// #endregion
|
|
104
109
|
|
|
105
110
|
function readAgentJsonl(filePath) {
|
|
106
111
|
const raw = readFileSync(filePath, 'utf8');
|
|
@@ -1252,30 +1257,122 @@ function subagentJsonlPath(meta, agentId) {
|
|
|
1252
1257
|
}
|
|
1253
1258
|
|
|
1254
1259
|
// Claude Code can scatter a session's records across multiple project dirs
|
|
1255
|
-
// (e.g. main repo + worktree)
|
|
1256
|
-
//
|
|
1257
|
-
// derived path is missing.
|
|
1260
|
+
// (e.g. main repo + worktree) and across sibling sessionId dirs when a
|
|
1261
|
+
// session is forked/resumed — the subagent JSONL stays under the original
|
|
1262
|
+
// parent sessionId. Fall back to scanning when the derived path is missing.
|
|
1258
1263
|
const subagentPathCache = new Map();
|
|
1264
|
+
function findSubagentJsonlInProject(projPath, sessionId, agentId) {
|
|
1265
|
+
const sameSid = path.join(projPath, sessionId, 'subagents', 'agent-' + agentId + '.jsonl');
|
|
1266
|
+
if (existsSync(sameSid)) return sameSid;
|
|
1267
|
+
let sessions;
|
|
1268
|
+
try { sessions = readdirSync(projPath, { withFileTypes: true }); } catch { return null; }
|
|
1269
|
+
for (const sess of sessions) {
|
|
1270
|
+
if (!sess.isDirectory() || sess.name === sessionId) continue;
|
|
1271
|
+
const candidate = path.join(projPath, sess.name, 'subagents', 'agent-' + agentId + '.jsonl');
|
|
1272
|
+
if (existsSync(candidate)) return candidate;
|
|
1273
|
+
}
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1259
1276
|
function resolveSubagentJsonl(meta, sessionId, agentId) {
|
|
1260
1277
|
const primary = subagentJsonlPath(meta, agentId);
|
|
1261
1278
|
if (existsSync(primary)) return primary;
|
|
1262
1279
|
const key = sessionId + '/' + agentId;
|
|
1263
|
-
|
|
1280
|
+
const cached = subagentPathCache.get(key);
|
|
1281
|
+
if (cached) return cached;
|
|
1264
1282
|
let found = null;
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1283
|
+
const parent = lookupParentSession(sessionId);
|
|
1284
|
+
if (parent.parentSessionId && parent.parentJsonlPath) {
|
|
1285
|
+
const projDir = path.dirname(parent.parentJsonlPath);
|
|
1286
|
+
const candidate = path.join(projDir, parent.parentSessionId, 'subagents', 'agent-' + agentId + '.jsonl');
|
|
1287
|
+
if (existsSync(candidate)) found = candidate;
|
|
1288
|
+
}
|
|
1289
|
+
if (!found) {
|
|
1290
|
+
try {
|
|
1291
|
+
for (const proj of readdirSync(PROJECTS_DIR, { withFileTypes: true })) {
|
|
1292
|
+
if (!proj.isDirectory()) continue;
|
|
1293
|
+
found = findSubagentJsonlInProject(path.join(PROJECTS_DIR, proj.name), sessionId, agentId);
|
|
1294
|
+
if (found) break;
|
|
1295
|
+
}
|
|
1296
|
+
} catch (_) { /* projects dir missing */ }
|
|
1297
|
+
}
|
|
1298
|
+
if (found) subagentPathCache.set(key, found);
|
|
1276
1299
|
return found || primary;
|
|
1277
1300
|
}
|
|
1278
1301
|
|
|
1302
|
+
// Claude Code marks fork lineage in two ways:
|
|
1303
|
+
// 1. `logicalParentUuid` on a system record (when present) points to a uuid
|
|
1304
|
+
// in the parent session's JSONL.
|
|
1305
|
+
// 2. When absent, the fork copies the parent's early records verbatim, so
|
|
1306
|
+
// the earliest `uuid` in this session also exists (same uuid+timestamp)
|
|
1307
|
+
// in the parent's JSONL.
|
|
1308
|
+
// We try (1) first, then fall back to (2).
|
|
1309
|
+
const parentSessionCache = new Map();
|
|
1310
|
+
// Both anchor signals live in the first few records (system marker on top,
|
|
1311
|
+
// fork-copy starts at line 0), so cap the scan instead of reading the whole file.
|
|
1312
|
+
const FORK_ANCHOR_SCAN_LINES = 10;
|
|
1313
|
+
function findForkAnchorUuid(jsonlPath) {
|
|
1314
|
+
let text;
|
|
1315
|
+
try { text = readFileSync(jsonlPath, 'utf8'); } catch { return null; }
|
|
1316
|
+
let firstUuid = null;
|
|
1317
|
+
let scanned = 0;
|
|
1318
|
+
for (const l of text.split('\n')) {
|
|
1319
|
+
if (!l) continue;
|
|
1320
|
+
if (scanned++ >= FORK_ANCHOR_SCAN_LINES) break;
|
|
1321
|
+
try {
|
|
1322
|
+
const d = JSON.parse(l);
|
|
1323
|
+
if (d.logicalParentUuid) return d.logicalParentUuid;
|
|
1324
|
+
if (!firstUuid && d.uuid) firstUuid = d.uuid;
|
|
1325
|
+
} catch { /* skip malformed */ }
|
|
1326
|
+
}
|
|
1327
|
+
return firstUuid;
|
|
1328
|
+
}
|
|
1329
|
+
function findSessionContainingUuid(projectDir, targetUuid, excludeJsonlPath) {
|
|
1330
|
+
let files;
|
|
1331
|
+
try { files = readdirSync(projectDir); } catch { return null; }
|
|
1332
|
+
const candidates = [];
|
|
1333
|
+
for (const f of files) {
|
|
1334
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
1335
|
+
const fp = path.join(projectDir, f);
|
|
1336
|
+
if (fp === excludeJsonlPath) continue;
|
|
1337
|
+
let text;
|
|
1338
|
+
try { text = readFileSync(fp, 'utf8'); } catch { continue; }
|
|
1339
|
+
if (!text.includes(targetUuid)) continue;
|
|
1340
|
+
for (const l of text.split('\n')) {
|
|
1341
|
+
if (!l || !l.includes(targetUuid)) continue;
|
|
1342
|
+
try {
|
|
1343
|
+
const d = JSON.parse(l);
|
|
1344
|
+
if (d.uuid === targetUuid && d.sessionId) {
|
|
1345
|
+
let mtime = 0;
|
|
1346
|
+
try { mtime = statSync(fp).mtimeMs; } catch { /* ignore */ }
|
|
1347
|
+
candidates.push({ parentSessionId: d.sessionId, parentJsonlPath: fp, mtime });
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
} catch { /* skip */ }
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (!candidates.length) return null;
|
|
1354
|
+
candidates.sort((a, b) => a.mtime - b.mtime);
|
|
1355
|
+
const { parentSessionId, parentJsonlPath } = candidates[0];
|
|
1356
|
+
return { parentSessionId, parentJsonlPath };
|
|
1357
|
+
}
|
|
1358
|
+
function lookupParentSession(sessionId) {
|
|
1359
|
+
if (parentSessionCache.has(sessionId)) return parentSessionCache.get(sessionId);
|
|
1360
|
+
const meta = loadSessionMetadata()[sessionId];
|
|
1361
|
+
const result = { parentSessionId: null, parentJsonlPath: null };
|
|
1362
|
+
if (meta?.jsonlPath) {
|
|
1363
|
+
const anchorUuid = findForkAnchorUuid(meta.jsonlPath);
|
|
1364
|
+
if (anchorUuid) {
|
|
1365
|
+
const hit = findSessionContainingUuid(path.dirname(meta.jsonlPath), anchorUuid, meta.jsonlPath);
|
|
1366
|
+
if (hit) Object.assign(result, hit);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (result.parentSessionId) parentSessionCache.set(sessionId, result);
|
|
1370
|
+
return result;
|
|
1371
|
+
}
|
|
1372
|
+
app.get('/api/sessions/:sessionId/parent', (req, res) => {
|
|
1373
|
+
res.json(lookupParentSession(resolveSessionId(req.params.sessionId)));
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1279
1376
|
app.get('/api/sessions/:sessionId/agents/:agentId/messages', (req, res) => {
|
|
1280
1377
|
const sessionId = resolveSessionId(req.params.sessionId);
|
|
1281
1378
|
const agentId = sanitizeAgentId(req.params.agentId);
|
|
@@ -1418,6 +1515,19 @@ app.get('/api/sessions/:sessionId/tool-result/:toolUseId', (req, res) => {
|
|
|
1418
1515
|
res.json({ toolUseId: req.params.toolUseId, content });
|
|
1419
1516
|
});
|
|
1420
1517
|
|
|
1518
|
+
app.get('/api/sessions/:sessionId/user-image/:msgUuid/:blockIndex', (req, res) => {
|
|
1519
|
+
const metadata = loadSessionMetadata();
|
|
1520
|
+
const meta = metadata[req.params.sessionId];
|
|
1521
|
+
const jsonlPath = meta?.jsonlPath;
|
|
1522
|
+
if (!jsonlPath) return res.status(404).end();
|
|
1523
|
+
const img = readUserImage(jsonlPath, req.params.msgUuid, req.params.blockIndex);
|
|
1524
|
+
if (!img) return res.status(404).end();
|
|
1525
|
+
const buf = Buffer.from(img.data, 'base64');
|
|
1526
|
+
res.setHeader('Content-Type', img.mediaType);
|
|
1527
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1528
|
+
res.end(buf);
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1421
1531
|
app.get('/api/version', (req, res) => {
|
|
1422
1532
|
const pkg = require('./package.json');
|
|
1423
1533
|
res.json({ version: pkg.version });
|
|
@@ -1920,7 +2030,6 @@ contextStatusWatcher.on('all', (event, filePath) => {
|
|
|
1920
2030
|
}
|
|
1921
2031
|
});
|
|
1922
2032
|
|
|
1923
|
-
const CTX_CLEANUP_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
1924
2033
|
async function cleanupContextStatus() {
|
|
1925
2034
|
try {
|
|
1926
2035
|
const entries = await fs.readdir(CONTEXT_STATUS_DIR);
|
|
@@ -1939,10 +2048,6 @@ async function cleanupContextStatus() {
|
|
|
1939
2048
|
} catch (e) { /* dir may not exist */ }
|
|
1940
2049
|
}
|
|
1941
2050
|
|
|
1942
|
-
// Cleanup agent-activity folders older than 2 days
|
|
1943
|
-
const CLEANUP_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
|
|
1944
|
-
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
|
|
1945
|
-
|
|
1946
2051
|
async function cleanupAgentActivity() {
|
|
1947
2052
|
try {
|
|
1948
2053
|
const entries = await fs.readdir(AGENT_ACTIVITY_DIR, { withFileTypes: true });
|