claude-code-kanban 4.0.0 → 4.2.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 +319 -86
- 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 +381 -47
- package/public/index.html +25 -0
- package/public/style.css +308 -17
- package/server.js +323 -39
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,12 @@ 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
|
+
maybeFollowLatest();
|
|
608
|
+
}
|
|
598
609
|
} catch (e) {
|
|
599
610
|
console.error('[fetchAgents]', e);
|
|
600
611
|
}
|
|
@@ -899,6 +910,11 @@ function parseCommandMessage(text) {
|
|
|
899
910
|
return null;
|
|
900
911
|
}
|
|
901
912
|
|
|
913
|
+
function parseCommandArgs(text) {
|
|
914
|
+
const m = (text || '').match(/<command-args>([^<]*)<\/command-args>/);
|
|
915
|
+
return m?.[1].trim() || '';
|
|
916
|
+
}
|
|
917
|
+
|
|
902
918
|
function cleanMessageText(text) {
|
|
903
919
|
const cmd = parseCommandMessage(text);
|
|
904
920
|
if (cmd) return cmd;
|
|
@@ -1051,11 +1067,25 @@ function renderMessageList(messages) {
|
|
|
1051
1067
|
</div>`);
|
|
1052
1068
|
} else {
|
|
1053
1069
|
const cmd = parseCommandMessage(m.text);
|
|
1070
|
+
const cmdArgs = cmd ? parseCommandArgs(m.fullText || m.text) : '';
|
|
1054
1071
|
const displayText = cmd ? cmd : escapeHtml(cleanMessageText(m.text));
|
|
1055
1072
|
const isCmd = !!cmd;
|
|
1073
|
+
const cmdArgsHtml =
|
|
1074
|
+
cmd && cmdArgs ? ` <span style="color:var(--text-secondary)">${escapeHtml(cmdArgs)}</span>` : '';
|
|
1075
|
+
const chips = [];
|
|
1076
|
+
const imgCount = m.images?.length || 0;
|
|
1077
|
+
const trCount = m.toolResultRefs?.length || 0;
|
|
1078
|
+
if (imgCount) chips.push(`<span class="user-attach-chip">${imgCount} image${imgCount > 1 ? 's' : ''}</span>`);
|
|
1079
|
+
if (trCount)
|
|
1080
|
+
chips.push(`<span class="user-attach-chip">${trCount} tool result${trCount > 1 ? 's' : ''}</span>`);
|
|
1081
|
+
const chipsHtml = chips.length ? `<div class="user-attach-chips">${chips.join('')}</div>` : '';
|
|
1082
|
+
let textHtml;
|
|
1083
|
+
if (displayText) textHtml = isCmd ? `<code>${escapeHtml(displayText)}</code>` : displayText;
|
|
1084
|
+
else if (chips.length) textHtml = '<em class="msg-text-muted">(attachment)</em>';
|
|
1085
|
+
else textHtml = '';
|
|
1056
1086
|
parts.push(`<div class="msg-item msg-user${isCmd ? ' msg-cmd' : ''}" ${clickable}>
|
|
1057
1087
|
${MSG_ICON_USER}
|
|
1058
|
-
<div class="msg-body"><div class="msg-text">${
|
|
1088
|
+
<div class="msg-body"><div class="msg-text">${textHtml}${cmdArgsHtml}</div>${chipsHtml}<div class="msg-time">${formatDate(m.timestamp)}</div></div>${pinBtn}
|
|
1059
1089
|
</div>`);
|
|
1060
1090
|
}
|
|
1061
1091
|
} else if (m.type === 'assistant') {
|
|
@@ -1103,6 +1133,62 @@ function toggleToolGroup(id) {
|
|
|
1103
1133
|
if (el) el.classList.toggle('show');
|
|
1104
1134
|
}
|
|
1105
1135
|
|
|
1136
|
+
const WAITING_PLAN_PREVIEW_CHARS = 120;
|
|
1137
|
+
const WAITING_PREVIEW_MAX_CHARS = 200;
|
|
1138
|
+
|
|
1139
|
+
function getWaitingLabel(kind, tool) {
|
|
1140
|
+
if (kind !== 'question') return `Awaiting permission: ${tool}`;
|
|
1141
|
+
if (tool === 'ExitPlanMode') return 'Plan awaiting approval';
|
|
1142
|
+
return 'Question pending';
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function getWaitingPreview(toolInput) {
|
|
1146
|
+
if (!toolInput) return '';
|
|
1147
|
+
try {
|
|
1148
|
+
const parsed = JSON.parse(toolInput);
|
|
1149
|
+
if (parsed.questions?.[0]?.question) return parsed.questions[0].question;
|
|
1150
|
+
if (parsed.plan) {
|
|
1151
|
+
const t = parsed.plan.match(/^#\s+(.+)/m);
|
|
1152
|
+
return t ? t[1] : parsed.plan.slice(0, WAITING_PLAN_PREVIEW_CHARS);
|
|
1153
|
+
}
|
|
1154
|
+
if (parsed.command) return parsed.command;
|
|
1155
|
+
if (parsed.file_path) return parsed.file_path;
|
|
1156
|
+
} catch (_) {
|
|
1157
|
+
/* toolInput may be truncated/non-JSON */
|
|
1158
|
+
}
|
|
1159
|
+
return '';
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function renderWaitingEntry() {
|
|
1163
|
+
if (!isWaitingFresh()) return '';
|
|
1164
|
+
const tool = currentWaiting.toolName || 'unknown';
|
|
1165
|
+
const label = getWaitingLabel(currentWaiting.kind, tool);
|
|
1166
|
+
const preview = getWaitingPreview(currentWaiting.toolInput);
|
|
1167
|
+
const previewHtml = preview
|
|
1168
|
+
? `<div class="msg-waiting-preview">${escapeHtml(preview.slice(0, WAITING_PREVIEW_MAX_CHARS))}</div>`
|
|
1169
|
+
: '';
|
|
1170
|
+
const discardBtn = `<button class="msg-waiting-discard" title="Discard permission prompt" onclick="event.stopPropagation();discardWaiting()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>`;
|
|
1171
|
+
return `<div class="msg-item msg-waiting" onclick="msgDetailFollowLatest=false;showWaitingDetail()">${ICON_CHAT}<div class="msg-body"><div class="msg-text">${escapeHtml(label)}</div>${previewHtml}<div class="msg-time">waiting…</div></div>${discardBtn}</div>`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
1175
|
+
async function discardWaiting() {
|
|
1176
|
+
if (!currentSessionId) return;
|
|
1177
|
+
try {
|
|
1178
|
+
const res = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/waiting/discard`, {
|
|
1179
|
+
method: 'POST',
|
|
1180
|
+
});
|
|
1181
|
+
if (res.ok) {
|
|
1182
|
+
currentWaiting = null;
|
|
1183
|
+
if (currentMsgDetailIdx === MSG_DETAIL_WAITING_IDX) closeMsgDetailModal();
|
|
1184
|
+
renderMessages(currentMessages);
|
|
1185
|
+
renderAgentFooter();
|
|
1186
|
+
}
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
console.error('[discardWaiting]', e);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1106
1192
|
function renderMessages(messages) {
|
|
1107
1193
|
const container = document.getElementById('message-panel-content');
|
|
1108
1194
|
const pinnedContainer = document.getElementById('message-panel-pinned');
|
|
@@ -1116,7 +1202,7 @@ function renderMessages(messages) {
|
|
|
1116
1202
|
currentMessages.length >= MSG_MAX_LOADED
|
|
1117
1203
|
? `<div class="msg-limit-banner">Showing last ${MSG_MAX_LOADED} messages</div>`
|
|
1118
1204
|
: '';
|
|
1119
|
-
container.innerHTML = limitBanner + msgsHtml;
|
|
1205
|
+
container.innerHTML = limitBanner + msgsHtml + renderWaitingEntry();
|
|
1120
1206
|
if (!msgUserScrolledUp) container.scrollTop = container.scrollHeight;
|
|
1121
1207
|
// Auto-load more if content doesn't overflow yet
|
|
1122
1208
|
if (
|
|
@@ -1131,6 +1217,7 @@ function renderMessages(messages) {
|
|
|
1131
1217
|
|
|
1132
1218
|
let currentMsgDetailIdx = null;
|
|
1133
1219
|
let msgDetailFollowLatest = false;
|
|
1220
|
+
const MSG_DETAIL_WAITING_IDX = -2;
|
|
1134
1221
|
let currentPins = [];
|
|
1135
1222
|
let pinnedCollapsed = false;
|
|
1136
1223
|
|
|
@@ -1152,6 +1239,16 @@ const ICON_TASK =
|
|
|
1152
1239
|
'<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
1240
|
const ICON_WEB =
|
|
1154
1241
|
'<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>';
|
|
1242
|
+
const ICON_OPEN_EXTERNAL =
|
|
1243
|
+
'<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>';
|
|
1244
|
+
const ICON_COPY =
|
|
1245
|
+
'<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>';
|
|
1246
|
+
const ICON_CHECKMARK =
|
|
1247
|
+
'<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>';
|
|
1248
|
+
const ICON_AGENT_WAITING =
|
|
1249
|
+
'<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>';
|
|
1250
|
+
const ICON_CHAT =
|
|
1251
|
+
'<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
1252
|
const TOOL_ICONS = {
|
|
1156
1253
|
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
1254
|
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 +1266,8 @@ const TOOL_ICONS = {
|
|
|
1169
1266
|
TaskList: ICON_TASK,
|
|
1170
1267
|
ToolSearch:
|
|
1171
1268
|
'<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
|
-
|
|
1269
|
+
AskUserQuestion: ICON_CHAT,
|
|
1270
|
+
ExitPlanMode: ICON_CHAT,
|
|
1174
1271
|
Skill:
|
|
1175
1272
|
'<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
1273
|
WebFetch: ICON_WEB,
|
|
@@ -1470,10 +1567,6 @@ function _renderPinToDetail(pin) {
|
|
|
1470
1567
|
}
|
|
1471
1568
|
|
|
1472
1569
|
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
1570
|
const LINK_SVG_PATHS =
|
|
1478
1571
|
'<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
1572
|
const linkSvg = (size) =>
|
|
@@ -1482,6 +1575,40 @@ const linkSvg = (size) =>
|
|
|
1482
1575
|
//#endregion
|
|
1483
1576
|
|
|
1484
1577
|
//#region MODALS
|
|
1578
|
+
function renderUserAttachments(m) {
|
|
1579
|
+
const parts = [];
|
|
1580
|
+
if (m.images?.length && m.uuid && currentSessionId) {
|
|
1581
|
+
const imgs = m.images
|
|
1582
|
+
.map((img) => {
|
|
1583
|
+
const url = `/api/sessions/${encodeURIComponent(currentSessionId)}/user-image/${encodeURIComponent(m.uuid)}/${img.blockIndex}`;
|
|
1584
|
+
return `<img src="${url}" loading="lazy" alt="user image" class="user-attach-image" />`;
|
|
1585
|
+
})
|
|
1586
|
+
.join('');
|
|
1587
|
+
parts.push(
|
|
1588
|
+
`<div class="user-attach-section"><div class="user-attach-label">Attached images</div><div class="user-attach-images">${imgs}</div></div>`,
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
if (m.toolResultRefs?.length) {
|
|
1592
|
+
const refs = m.toolResultRefs
|
|
1593
|
+
.map((ref) => {
|
|
1594
|
+
const safeId = escapeHtml(ref.toolUseId);
|
|
1595
|
+
const shortId = ref.toolUseId.length > 14 ? `${ref.toolUseId.slice(0, 14)}…` : ref.toolUseId;
|
|
1596
|
+
const preview = ref.preview ? escapeHtml(ref.preview) : '<em>(no text)</em>';
|
|
1597
|
+
const expandId = `user-tr-${ref.toolUseId}`;
|
|
1598
|
+
return `<details class="user-attach-toolresult">
|
|
1599
|
+
<summary>Tool result <code>${escapeHtml(shortId)}</code></summary>
|
|
1600
|
+
<pre class="${TINTED_PRE_CLASS}" id="${expandId}">${preview}</pre>
|
|
1601
|
+
<button type="button" class="tool-result-expand-btn" data-expand-id="${expandId}" data-tool-use-id="${safeId}" onclick="_toggleToolResultExpand(this)">Show full</button>
|
|
1602
|
+
</details>`;
|
|
1603
|
+
})
|
|
1604
|
+
.join('');
|
|
1605
|
+
parts.push(
|
|
1606
|
+
`<div class="user-attach-section"><div class="user-attach-label">Tool results in this message</div>${refs}</div>`,
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
return parts.join('');
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1485
1612
|
function showMsgDetail(idx) {
|
|
1486
1613
|
currentMsgDetailIdx = idx;
|
|
1487
1614
|
const m = currentMessages[idx];
|
|
@@ -1546,25 +1673,27 @@ function showMsgDetail(idx) {
|
|
|
1546
1673
|
body.innerHTML = renderMarkdown(text);
|
|
1547
1674
|
}
|
|
1548
1675
|
} else {
|
|
1549
|
-
const rawText = stripAnsi(m.fullText || m.text);
|
|
1676
|
+
const rawText = stripAnsi(m.fullText || m.text || '');
|
|
1550
1677
|
const cmd = m.type === 'user' ? parseCommandMessage(rawText) : null;
|
|
1551
1678
|
document.getElementById('msg-detail-title').textContent =
|
|
1552
1679
|
m.type === 'assistant' ? 'Claude' : m.systemLabel ? 'System' : 'User';
|
|
1553
1680
|
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
1681
|
+
const userExtras = m.type === 'user' ? renderUserAttachments(m) : '';
|
|
1554
1682
|
if (m.compactSummary) {
|
|
1555
|
-
body.innerHTML = renderMarkdown(m.compactSummary);
|
|
1683
|
+
body.innerHTML = renderMarkdown(m.compactSummary) + userExtras;
|
|
1556
1684
|
} else if (cmd) {
|
|
1557
|
-
const
|
|
1558
|
-
const args = argsMatch?.[1].trim() ? argsMatch[1].trim() : null;
|
|
1685
|
+
const args = parseCommandArgs(rawText) || null;
|
|
1559
1686
|
const cleanBody = rawText
|
|
1560
1687
|
.replace(/<command-[^>]+>[\s\S]*?<\/command-[^>]+>/g, '')
|
|
1561
1688
|
.replace(/<local-command-[^>]+>[\s\S]*?<\/local-command-[^>]+>/g, '')
|
|
1562
1689
|
.trim();
|
|
1563
1690
|
let cmdHtml = `<code>${escapeHtml(cmd)}${args ? ` ${escapeHtml(args)}` : ''}</code>`;
|
|
1564
1691
|
if (cleanBody) cmdHtml += `<div style="margin-top:10px">${renderMarkdown(cleanBody)}</div>`;
|
|
1565
|
-
body.innerHTML = cmdHtml;
|
|
1692
|
+
body.innerHTML = cmdHtml + userExtras;
|
|
1693
|
+
} else if (rawText) {
|
|
1694
|
+
body.innerHTML = renderMarkdown(rawText) + userExtras;
|
|
1566
1695
|
} else {
|
|
1567
|
-
body.innerHTML =
|
|
1696
|
+
body.innerHTML = userExtras || '<em>No content</em>';
|
|
1568
1697
|
}
|
|
1569
1698
|
}
|
|
1570
1699
|
const modal = document.getElementById('msg-detail-modal').querySelector('.modal');
|
|
@@ -1649,8 +1778,7 @@ async function copyWithFeedback(text, btn) {
|
|
|
1649
1778
|
await navigator.clipboard.writeText(text);
|
|
1650
1779
|
btn.dataset.copying = '1';
|
|
1651
1780
|
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>';
|
|
1781
|
+
btn.innerHTML = ICON_CHECKMARK;
|
|
1654
1782
|
setTimeout(() => {
|
|
1655
1783
|
btn.innerHTML = svg;
|
|
1656
1784
|
delete btn.dataset.copying;
|
|
@@ -2028,7 +2156,7 @@ function renderAgentFooter() {
|
|
|
2028
2156
|
)
|
|
2029
2157
|
.slice(0, AGENT_LOG_MAX);
|
|
2030
2158
|
|
|
2031
|
-
const permFresh =
|
|
2159
|
+
const permFresh = isWaitingFresh();
|
|
2032
2160
|
|
|
2033
2161
|
if (visible.length === 0 && !permFresh) {
|
|
2034
2162
|
footer.classList.remove('visible');
|
|
@@ -2322,10 +2450,8 @@ function renderSessions() {
|
|
|
2322
2450
|
|
|
2323
2451
|
// Filter pipeline: active filter → force-include revealed/current (non-pinned) sessions →
|
|
2324
2452
|
// project filter → search filter → ensure pinned/sticky sessions are always included
|
|
2325
|
-
const LIVE_INDICATOR_MS = 10 * 1000;
|
|
2326
2453
|
let filteredSessions = sessions;
|
|
2327
2454
|
if (sessionFilter === 'active') {
|
|
2328
|
-
const ACTIVE_PLAN_MS = 15 * 60 * 1000;
|
|
2329
2455
|
const now = Date.now();
|
|
2330
2456
|
const activeSessionIds = new Set();
|
|
2331
2457
|
filteredSessions = filteredSessions.filter((s) => {
|
|
@@ -2473,15 +2599,13 @@ function renderSessions() {
|
|
|
2473
2599
|
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
2474
2600
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
2475
2601
|
${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>` : ''}
|
|
2602
|
+
${renderLoopBadge(session)}
|
|
2476
2603
|
${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
2604
|
${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
2605
|
${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>' : ''}
|
|
2606
|
+
${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>` : ''}
|
|
2607
|
+
${session.hasWaitingForUser ? `<span class="agent-badge agent-badge-waiting" title="Waiting for user">${ICON_AGENT_WAITING}</span>` : ''}
|
|
2608
|
+
${isLive || session.hasRunningAgents ? `<span class="pulse" title="${isLive ? 'Live' : 'Active agents'}"></span>` : ''}
|
|
2485
2609
|
</span>
|
|
2486
2610
|
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
|
|
2487
2611
|
<span class="progress-text">${session.completed}/${total}</span>
|
|
@@ -4147,7 +4271,7 @@ function matchKey(e, ...keys) {
|
|
|
4147
4271
|
return keys.some((k) => e.key === k || e.code === k);
|
|
4148
4272
|
}
|
|
4149
4273
|
|
|
4150
|
-
const MODAL_ESC_PRIORITY = ['preview-modal', 'msg-detail-modal', 'plan-modal'];
|
|
4274
|
+
const MODAL_ESC_PRIORITY = ['preview-modal', 'msg-detail-modal', 'plan-modal', 'loop-modal'];
|
|
4151
4275
|
const MODAL_CLOSERS = {
|
|
4152
4276
|
'preview-modal': () => closePreviewModal(),
|
|
4153
4277
|
'msg-detail-modal': () => {
|
|
@@ -4155,6 +4279,7 @@ const MODAL_CLOSERS = {
|
|
|
4155
4279
|
msgDetailFollowLatest = false;
|
|
4156
4280
|
},
|
|
4157
4281
|
'plan-modal': () => closePlanModal(),
|
|
4282
|
+
'loop-modal': () => closeLoopModal(),
|
|
4158
4283
|
'team-modal': () => closeTeamModal(),
|
|
4159
4284
|
'agent-modal': () => closeAgentModal(),
|
|
4160
4285
|
'help-modal': () => closeHelpModal(),
|
|
@@ -4189,7 +4314,13 @@ document.addEventListener('keydown', (e) => {
|
|
|
4189
4314
|
} else if (document.getElementById('msg-detail-modal').classList.contains('visible')) {
|
|
4190
4315
|
if (matchKey(e, 'ArrowDown', 'KeyJ')) {
|
|
4191
4316
|
e.preventDefault();
|
|
4192
|
-
if (currentMsgDetailIdx
|
|
4317
|
+
if (currentMsgDetailIdx === MSG_DETAIL_WAITING_IDX) {
|
|
4318
|
+
msgDetailFollowLatest = true;
|
|
4319
|
+
showWaitingDetail();
|
|
4320
|
+
} else if (currentMsgDetailIdx === currentMessages.length - 1 && isWaitingFresh()) {
|
|
4321
|
+
msgDetailFollowLatest = false;
|
|
4322
|
+
showWaitingDetail();
|
|
4323
|
+
} else if (currentMsgDetailIdx < currentMessages.length - 1) {
|
|
4193
4324
|
msgDetailFollowLatest = false;
|
|
4194
4325
|
showMsgDetail(currentMsgDetailIdx + 1);
|
|
4195
4326
|
} else if (currentMsgDetailIdx === currentMessages.length - 1) {
|
|
@@ -4198,7 +4329,12 @@ document.addEventListener('keydown', (e) => {
|
|
|
4198
4329
|
}
|
|
4199
4330
|
} else if (matchKey(e, 'ArrowUp', 'KeyK')) {
|
|
4200
4331
|
e.preventDefault();
|
|
4201
|
-
if (currentMsgDetailIdx
|
|
4332
|
+
if (currentMsgDetailIdx === MSG_DETAIL_WAITING_IDX) {
|
|
4333
|
+
if (currentMessages.length) {
|
|
4334
|
+
msgDetailFollowLatest = false;
|
|
4335
|
+
showMsgDetail(currentMessages.length - 1);
|
|
4336
|
+
}
|
|
4337
|
+
} else if (currentMsgDetailIdx > 0) {
|
|
4202
4338
|
msgDetailFollowLatest = false;
|
|
4203
4339
|
showMsgDetail(currentMsgDetailIdx - 1);
|
|
4204
4340
|
}
|
|
@@ -4223,6 +4359,9 @@ document.addEventListener('keydown', (e) => {
|
|
|
4223
4359
|
const msgDetailModal = document.getElementById('msg-detail-modal');
|
|
4224
4360
|
if (msgDetailModal.classList.contains('visible')) {
|
|
4225
4361
|
closeMsgDetailModal();
|
|
4362
|
+
} else if (isWaitingFresh()) {
|
|
4363
|
+
msgDetailFollowLatest = true;
|
|
4364
|
+
showWaitingDetail();
|
|
4226
4365
|
} else if (currentMessages.length) {
|
|
4227
4366
|
msgDetailFollowLatest = true;
|
|
4228
4367
|
showMsgDetail(currentMessages.length - 1);
|
|
@@ -4380,6 +4519,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
4380
4519
|
dismissedSessionIds.add(contextSid);
|
|
4381
4520
|
updateDismissBtnState();
|
|
4382
4521
|
renderSessions();
|
|
4522
|
+
renderActivityChip();
|
|
4383
4523
|
const newItems = getNavigableItems();
|
|
4384
4524
|
const targetIdx = newItems.length > 0 ? Math.max(0, prevIdx - 1) : -1;
|
|
4385
4525
|
// If the dismissed session is currently open, navigate to the previous one
|
|
@@ -4986,11 +5126,50 @@ function renderContextDetail(raw) {
|
|
|
4986
5126
|
|
|
4987
5127
|
//#region UTILS
|
|
4988
5128
|
function maybeFollowLatest() {
|
|
4989
|
-
if (msgDetailFollowLatest
|
|
5129
|
+
if (!msgDetailFollowLatest) return;
|
|
5130
|
+
if (isWaitingFresh()) {
|
|
5131
|
+
showWaitingDetail();
|
|
5132
|
+
} else if (currentMessages.length) {
|
|
4990
5133
|
showMsgDetail(currentMessages.length - 1);
|
|
4991
5134
|
}
|
|
4992
5135
|
}
|
|
4993
5136
|
|
|
5137
|
+
function isWaitingFresh() {
|
|
5138
|
+
if (!currentWaiting?.timestamp) return false;
|
|
5139
|
+
return Date.now() - new Date(currentWaiting.timestamp).getTime() < WAITING_TTL_MS;
|
|
5140
|
+
}
|
|
5141
|
+
|
|
5142
|
+
function showWaitingDetail() {
|
|
5143
|
+
if (!isWaitingFresh()) return;
|
|
5144
|
+
currentMsgDetailIdx = MSG_DETAIL_WAITING_IDX;
|
|
5145
|
+
const tool = currentWaiting.toolName || 'unknown';
|
|
5146
|
+
const label = getWaitingLabel(currentWaiting.kind, tool);
|
|
5147
|
+
const body = document.getElementById('msg-detail-body');
|
|
5148
|
+
let inputHtml = '';
|
|
5149
|
+
if (currentWaiting.toolInput) {
|
|
5150
|
+
let pretty = currentWaiting.toolInput;
|
|
5151
|
+
try {
|
|
5152
|
+
pretty = JSON.stringify(JSON.parse(currentWaiting.toolInput), null, 2);
|
|
5153
|
+
} catch (_) {
|
|
5154
|
+
/* keep raw */
|
|
5155
|
+
}
|
|
5156
|
+
inputHtml = `<pre class="${TINTED_PRE_CLASS}">${escapeHtml(pretty)}</pre>`;
|
|
5157
|
+
}
|
|
5158
|
+
body.innerHTML = inputHtml;
|
|
5159
|
+
document.getElementById('msg-detail-title').textContent = label;
|
|
5160
|
+
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
5161
|
+
const modal = document.getElementById('msg-detail-modal').querySelector('.modal');
|
|
5162
|
+
autoSizeModal(modal, body);
|
|
5163
|
+
modal.classList.toggle('live', msgDetailFollowLatest);
|
|
5164
|
+
const overlay = document.getElementById('msg-detail-modal');
|
|
5165
|
+
overlay.classList.toggle('live-overlay', msgDetailFollowLatest);
|
|
5166
|
+
const meta = [formatDate(currentWaiting.timestamp), 'waiting'];
|
|
5167
|
+
document.getElementById('msg-detail-meta').textContent = meta.join(' · ');
|
|
5168
|
+
currentPinDetailId = null;
|
|
5169
|
+
updateMsgDetailPinState();
|
|
5170
|
+
overlay.classList.add('visible');
|
|
5171
|
+
}
|
|
5172
|
+
|
|
4994
5173
|
function isSessionActive(s) {
|
|
4995
5174
|
return s.hasRecentLog || s.inProgress > 0 || s.hasActiveAgents || s.hasWaitingForUser;
|
|
4996
5175
|
}
|
|
@@ -5088,7 +5267,7 @@ function renderAgentTabs(promptHtml, responseHtml, promptText, responseText) {
|
|
|
5088
5267
|
}
|
|
5089
5268
|
if (!tabs.length) return '';
|
|
5090
5269
|
const defaultTab = responseHtml ? 'response' : tabs[0].key;
|
|
5091
|
-
const copyBtnHtml = `<button class="agent-tab-copy" title="Copy" onclick="copyAgentTabActive('${id}',this)"
|
|
5270
|
+
const copyBtnHtml = `<button class="agent-tab-copy" title="Copy" onclick="copyAgentTabActive('${id}',this)">${ICON_COPY}</button>`;
|
|
5092
5271
|
const tabsHtml = tabs
|
|
5093
5272
|
.map(
|
|
5094
5273
|
(t) =>
|
|
@@ -5493,13 +5672,13 @@ async function showSessionInfoModal(sessionId) {
|
|
|
5493
5672
|
// and re-rendered when they arrive, so the modal doesn't block on network.
|
|
5494
5673
|
_planSessionId = sessionId;
|
|
5495
5674
|
const cachedTasks = currentSessionId === sessionId ? currentTasks : [];
|
|
5496
|
-
showInfoModal(session, null, cachedTasks, null);
|
|
5675
|
+
showInfoModal(session, null, cachedTasks, null, null);
|
|
5497
5676
|
|
|
5498
|
-
const rerender = (teamConfig, tasks, planContent) => {
|
|
5677
|
+
const rerender = (teamConfig, tasks, planContent, parentInfo) => {
|
|
5499
5678
|
if (_planSessionId !== sessionId) return; // user opened a different modal
|
|
5500
5679
|
const modal = document.getElementById('team-modal');
|
|
5501
5680
|
if (!modal?.classList.contains('visible')) return; // user closed modal — don't reopen
|
|
5502
|
-
showInfoModal(session, teamConfig, tasks, planContent);
|
|
5681
|
+
showInfoModal(session, teamConfig, tasks, planContent, parentInfo);
|
|
5503
5682
|
};
|
|
5504
5683
|
|
|
5505
5684
|
const teamPromise = session.isTeam
|
|
@@ -5520,8 +5699,17 @@ async function showSessionInfoModal(sessionId) {
|
|
|
5520
5699
|
.then((r) => (r.ok ? r.json() : []))
|
|
5521
5700
|
.catch(() => []);
|
|
5522
5701
|
|
|
5523
|
-
const
|
|
5524
|
-
|
|
5702
|
+
const parentPromise = fetch(`/api/sessions/${sessionId}/parent`)
|
|
5703
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
5704
|
+
.catch(() => null);
|
|
5705
|
+
|
|
5706
|
+
const [teamConfig, planContent, tasks, parentInfo] = await Promise.all([
|
|
5707
|
+
teamPromise,
|
|
5708
|
+
planPromise,
|
|
5709
|
+
tasksPromise,
|
|
5710
|
+
parentPromise,
|
|
5711
|
+
]);
|
|
5712
|
+
rerender(teamConfig, tasks, planContent, parentInfo);
|
|
5525
5713
|
}
|
|
5526
5714
|
|
|
5527
5715
|
let _infoModalSessionId = null;
|
|
@@ -5538,7 +5726,7 @@ function updateStickyBtnState() {
|
|
|
5538
5726
|
if (svg) svg.setAttribute('fill', isSticky ? 'currentColor' : 'none');
|
|
5539
5727
|
}
|
|
5540
5728
|
|
|
5541
|
-
function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
5729
|
+
function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
5542
5730
|
const modal = document.getElementById('team-modal');
|
|
5543
5731
|
const titleEl = document.getElementById('team-modal-title');
|
|
5544
5732
|
const bodyEl = document.getElementById('team-modal-body');
|
|
@@ -5558,6 +5746,13 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5558
5746
|
// Each row: [label, displayValue, { openPath?, copyValue? }]
|
|
5559
5747
|
const infoRows = [];
|
|
5560
5748
|
infoRows.push(['Session', session.id, { openClaudeDir: true, openFile: session.jsonlPath }]);
|
|
5749
|
+
if (parentInfo?.parentSessionId) {
|
|
5750
|
+
infoRows.push([
|
|
5751
|
+
'Forked from',
|
|
5752
|
+
parentInfo.parentSessionId,
|
|
5753
|
+
{ openClaudeDir: true, openFile: parentInfo.parentJsonlPath, openSession: parentInfo.parentSessionId },
|
|
5754
|
+
]);
|
|
5755
|
+
}
|
|
5561
5756
|
if (session.slug && session.hasPlan) {
|
|
5562
5757
|
infoRows.push(['Slug', session.slug, { openClaudeDir: true, openFile: session.planPath }]);
|
|
5563
5758
|
}
|
|
@@ -5589,19 +5784,25 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5589
5784
|
"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
5785
|
const plainStyle =
|
|
5591
5786
|
"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
|
|
5787
|
+
html += `<div class="team-modal-meta info-grid">`;
|
|
5593
5788
|
infoRows.forEach(([label, value, opts]) => {
|
|
5594
5789
|
const copyVal = escapeHtml(value).replace(/"/g, '"');
|
|
5595
5790
|
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>`;
|
|
5791
|
+
if (opts?.openSession) {
|
|
5792
|
+
const sid = _escapeForJsAttr(escapeHtml(opts.openSession).replace(/"/g, '"'));
|
|
5793
|
+
html += `<span onclick="openSessionFromInfo('${sid}')" style="${clickableStyle}" title="Open session in app">${escapeHtml(value)}</span>`;
|
|
5600
5794
|
} else {
|
|
5601
5795
|
html += `<span style="${plainStyle}" title="${copyVal}">${escapeHtml(value)}</span>`;
|
|
5602
5796
|
}
|
|
5603
5797
|
const jsCopyVal = _escapeForJsAttr(copyVal);
|
|
5604
|
-
|
|
5798
|
+
const copyBtn = `<button onclick="copyWithFeedback('${jsCopyVal}', this)" title="Copy">${ICON_COPY}</button>`;
|
|
5799
|
+
let openBtn = '';
|
|
5800
|
+
if (opts?.openClaudeDir || opts?.openPath) {
|
|
5801
|
+
const folder = opts.openClaudeDir ? '' : escapeHtml(opts.openPath).replace(/"/g, '"');
|
|
5802
|
+
const file = opts.openFile ? escapeHtml(opts.openFile).replace(/"/g, '"') : '';
|
|
5803
|
+
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>`;
|
|
5804
|
+
}
|
|
5805
|
+
html += `<span class="info-row-actions">${copyBtn}${openBtn}</span>`;
|
|
5605
5806
|
});
|
|
5606
5807
|
html += `</div>`;
|
|
5607
5808
|
|
|
@@ -5687,6 +5888,11 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5687
5888
|
updateDismissBtnState();
|
|
5688
5889
|
const costBtn = document.getElementById('session-info-cost-btn');
|
|
5689
5890
|
if (costBtn) costBtn.style.display = window.__HUB__?.enabled || appConfig.costUrl ? '' : 'none';
|
|
5891
|
+
const mkBtn = document.getElementById('session-info-marketplace-btn');
|
|
5892
|
+
const memBtn = document.getElementById('session-info-memory-btn');
|
|
5893
|
+
const proj = session.project;
|
|
5894
|
+
if (mkBtn) mkBtn.style.display = proj && (window.__HUB__?.enabled || appConfig.marketplaceUrl) ? '' : 'none';
|
|
5895
|
+
if (memBtn) memBtn.style.display = proj && (window.__HUB__?.enabled || appConfig.memoryUrl) ? '' : 'none';
|
|
5690
5896
|
modal.classList.add('visible');
|
|
5691
5897
|
|
|
5692
5898
|
if (alreadyVisible) return; // re-render during deferred hydration — key handler already attached
|
|
@@ -5707,6 +5913,12 @@ function closeTeamModal() {
|
|
|
5707
5913
|
_planSessionId = null;
|
|
5708
5914
|
}
|
|
5709
5915
|
|
|
5916
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5917
|
+
function openSessionFromInfo(sessionId) {
|
|
5918
|
+
closeTeamModal();
|
|
5919
|
+
fetchTasks(sessionId);
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5710
5922
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5711
5923
|
function toggleDismissSession(sessionId) {
|
|
5712
5924
|
if (dismissedSessionIds.has(sessionId)) {
|
|
@@ -5716,6 +5928,7 @@ function toggleDismissSession(sessionId) {
|
|
|
5716
5928
|
}
|
|
5717
5929
|
updateDismissBtnState();
|
|
5718
5930
|
renderSessions();
|
|
5931
|
+
renderActivityChip();
|
|
5719
5932
|
}
|
|
5720
5933
|
|
|
5721
5934
|
function updateDismissBtnState() {
|
|
@@ -5745,6 +5958,110 @@ function refreshOpenPlan() {
|
|
|
5745
5958
|
.catch(() => {});
|
|
5746
5959
|
}
|
|
5747
5960
|
|
|
5961
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5962
|
+
function showLoopModal(sessionId) {
|
|
5963
|
+
const body = document.getElementById('loop-modal-body');
|
|
5964
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Loading…</div>';
|
|
5965
|
+
document.getElementById('loop-modal').classList.add('visible');
|
|
5966
|
+
fetch(`/api/sessions/${sessionId}/loop`)
|
|
5967
|
+
.then((r) => (r.ok ? r.json() : { wakeups: [], crons: [] }))
|
|
5968
|
+
.catch(() => ({ wakeups: [], crons: [] }))
|
|
5969
|
+
.then((data) => {
|
|
5970
|
+
renderLoopModalBody(data);
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
|
|
5974
|
+
function fmtLoopDelay(s) {
|
|
5975
|
+
if (s == null) return '';
|
|
5976
|
+
if (s < 60) return `${s}s`;
|
|
5977
|
+
if (s < 3600) return `${Math.round(s / 60)}m`;
|
|
5978
|
+
return `${(s / 3600).toFixed(1)}h`;
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5981
|
+
function fmtLoopFireTime(timestamp, delaySeconds) {
|
|
5982
|
+
if (!timestamp || delaySeconds == null) return { abs: '', rel: '', status: '' };
|
|
5983
|
+
const fireMs = new Date(timestamp).getTime() + delaySeconds * 1000;
|
|
5984
|
+
const fireDate = new Date(fireMs);
|
|
5985
|
+
const diff = fireMs - Date.now();
|
|
5986
|
+
const abs = fireDate.toLocaleString(undefined, {
|
|
5987
|
+
month: 'short',
|
|
5988
|
+
day: 'numeric',
|
|
5989
|
+
hour: '2-digit',
|
|
5990
|
+
minute: '2-digit',
|
|
5991
|
+
});
|
|
5992
|
+
const absSec = Math.abs(Math.round(diff / 1000));
|
|
5993
|
+
const rel =
|
|
5994
|
+
absSec < 60 ? `${absSec}s` : absSec < 3600 ? `${Math.round(absSec / 60)}m` : `${(absSec / 3600).toFixed(1)}h`;
|
|
5995
|
+
if (diff > 0) return { abs, rel: `in ${rel}`, status: 'pending' };
|
|
5996
|
+
return { abs, rel: `${rel} ago`, status: 'fired' };
|
|
5997
|
+
}
|
|
5998
|
+
|
|
5999
|
+
const LOOP_CLOCK_SVG =
|
|
6000
|
+
'<svg width="14" height="14" 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"/><polyline points="12 6 12 12 16 14"/></svg>';
|
|
6001
|
+
const LOOP_CRON_SVG =
|
|
6002
|
+
'<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="M12 8v4l3 3"/><circle cx="12" cy="12" r="10"/></svg>';
|
|
6003
|
+
|
|
6004
|
+
function loopField(label, value, mono = false) {
|
|
6005
|
+
if (!value) return '';
|
|
6006
|
+
const inner = mono ? `<code>${escapeHtml(value)}</code>` : escapeHtml(value);
|
|
6007
|
+
return `<div class="loop-field"><div class="loop-field-label">${label}</div><div class="loop-field-val">${inner}</div></div>`;
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
function renderLoopRow(item, kind) {
|
|
6011
|
+
const when = item.timestamp ? formatDate(item.timestamp) : '';
|
|
6012
|
+
let headline = '';
|
|
6013
|
+
let footer = '';
|
|
6014
|
+
let fields = '';
|
|
6015
|
+
if (kind === 'wakeup') {
|
|
6016
|
+
const fire = fmtLoopFireTime(item.timestamp, item.delaySeconds);
|
|
6017
|
+
const delayLbl = item.delaySeconds != null ? `delay ${fmtLoopDelay(item.delaySeconds)}` : '';
|
|
6018
|
+
if (fire.abs) {
|
|
6019
|
+
headline = `<div class="loop-headline loop-fire-${fire.status}">${LOOP_CLOCK_SVG}<span class="loop-headline-rel">${fire.status === 'pending' ? 'Fires' : 'Fired'} ${escapeHtml(fire.rel)}</span><span class="loop-headline-abs">${escapeHtml(fire.abs)}</span></div>`;
|
|
6020
|
+
}
|
|
6021
|
+
fields = loopField('Reason', item.reason) + loopField('Prompt', item.prompt, true);
|
|
6022
|
+
footer = `<div class="loop-foot">scheduled ${escapeHtml(when)}${delayLbl ? ` · ${delayLbl}` : ''}</div>`;
|
|
6023
|
+
} else {
|
|
6024
|
+
if (item.cron) {
|
|
6025
|
+
headline = `<div class="loop-headline">${LOOP_CRON_SVG}<span class="loop-headline-rel"><code>${escapeHtml(item.cron)}</code></span></div>`;
|
|
6026
|
+
}
|
|
6027
|
+
fields = loopField('Description', item.description) + loopField('Prompt', item.prompt, true);
|
|
6028
|
+
footer = `<div class="loop-foot">created ${escapeHtml(when)}</div>`;
|
|
6029
|
+
}
|
|
6030
|
+
return `<div class="loop-row">${headline}${fields}${footer}</div>`;
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
function renderLoopModalBody(data) {
|
|
6034
|
+
const body = document.getElementById('loop-modal-body');
|
|
6035
|
+
const wakeups = data.wakeups || [];
|
|
6036
|
+
const crons = data.crons || [];
|
|
6037
|
+
if (!wakeups.length && !crons.length) {
|
|
6038
|
+
body.innerHTML =
|
|
6039
|
+
'<div style="padding:24px;text-align:center;color:var(--text-secondary);">No scheduled wakeups or cron jobs.</div>';
|
|
6040
|
+
return;
|
|
6041
|
+
}
|
|
6042
|
+
const section = (title, items, kind) =>
|
|
6043
|
+
items.length
|
|
6044
|
+
? `<h4 class="loop-section-title">${title} <span class="loop-count">${items.length}</span></h4>${items.map((i) => renderLoopRow(i, kind)).join('')}`
|
|
6045
|
+
: '';
|
|
6046
|
+
body.innerHTML = section('Wakeups', wakeups, 'wakeup') + section('Cron jobs', crons, 'cron');
|
|
6047
|
+
}
|
|
6048
|
+
|
|
6049
|
+
function renderLoopBadge(session) {
|
|
6050
|
+
const li = session.loopInfo;
|
|
6051
|
+
const total = (li?.wakeupCount || 0) + (li?.cronCount || 0);
|
|
6052
|
+
if (total === 0) return '';
|
|
6053
|
+
let tip = `${li.wakeupCount} wakeup${li.wakeupCount === 1 ? '' : 's'}, ${li.cronCount} cron${li.cronCount === 1 ? '' : 's'}`;
|
|
6054
|
+
if (li.latest?.timestamp && li.latest.delaySeconds != null) {
|
|
6055
|
+
const f = fmtLoopFireTime(li.latest.timestamp, li.latest.delaySeconds);
|
|
6056
|
+
if (f.abs) tip += ` — latest ${f.status === 'pending' ? 'fires' : 'fired'} ${f.rel} (${f.abs})`;
|
|
6057
|
+
}
|
|
6058
|
+
return `<span class="loop-badge" onclick="event.stopPropagation(); showLoopModal('${session.id}')" title="${escapeHtml(tip)}"><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"/><polyline points="12 6 12 12 16 14"/></svg></span>`;
|
|
6059
|
+
}
|
|
6060
|
+
|
|
6061
|
+
function closeLoopModal() {
|
|
6062
|
+
document.getElementById('loop-modal').classList.remove('visible');
|
|
6063
|
+
}
|
|
6064
|
+
|
|
5748
6065
|
function openPlanForSession(sid) {
|
|
5749
6066
|
fetch(`/api/sessions/${sid}/plan`)
|
|
5750
6067
|
.then((r) => (r.ok ? r.json() : null))
|
|
@@ -5801,7 +6118,6 @@ function openCost(sessionId) {
|
|
|
5801
6118
|
}
|
|
5802
6119
|
}
|
|
5803
6120
|
|
|
5804
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5805
6121
|
function openMarketplace(projectPath) {
|
|
5806
6122
|
const params = new URLSearchParams({ project: projectPath });
|
|
5807
6123
|
if (window.__HUB__?.enabled) {
|
|
@@ -5813,7 +6129,6 @@ function openMarketplace(projectPath) {
|
|
|
5813
6129
|
}
|
|
5814
6130
|
}
|
|
5815
6131
|
|
|
5816
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5817
6132
|
function openMemory(projectPath) {
|
|
5818
6133
|
const params = new URLSearchParams({ project: projectPath });
|
|
5819
6134
|
if (window.__HUB__?.enabled) {
|
|
@@ -5825,6 +6140,19 @@ function openMemory(projectPath) {
|
|
|
5825
6140
|
}
|
|
5826
6141
|
}
|
|
5827
6142
|
|
|
6143
|
+
function openForInfoModalProject(open) {
|
|
6144
|
+
const s = sessions.find((x) => x.id === _infoModalSessionId);
|
|
6145
|
+
if (s?.project) open(s.project);
|
|
6146
|
+
}
|
|
6147
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6148
|
+
function openMarketplaceForInfoModal() {
|
|
6149
|
+
openForInfoModalProject(openMarketplace);
|
|
6150
|
+
}
|
|
6151
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6152
|
+
function openMemoryForInfoModal() {
|
|
6153
|
+
openForInfoModalProject(openMemory);
|
|
6154
|
+
}
|
|
6155
|
+
|
|
5828
6156
|
//#endregion
|
|
5829
6157
|
|
|
5830
6158
|
//#region OWNER_FILTER
|
|
@@ -5990,6 +6318,12 @@ function makeLimitCell(label, bucket) {
|
|
|
5990
6318
|
const strong = document.createElement('strong');
|
|
5991
6319
|
strong.textContent = pct == null ? '-%' : `${Math.ceil(pct)}%`;
|
|
5992
6320
|
cell.appendChild(strong);
|
|
6321
|
+
if (reset) {
|
|
6322
|
+
const r = document.createElement('span');
|
|
6323
|
+
r.className = 'footer-limit-reset';
|
|
6324
|
+
r.textContent = ` (${reset})`;
|
|
6325
|
+
cell.appendChild(r);
|
|
6326
|
+
}
|
|
5993
6327
|
return cell;
|
|
5994
6328
|
}
|
|
5995
6329
|
function makeLimitSpan(rl) {
|