claude-code-kanban 3.10.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 +17 -52
- package/public/app.js +221 -45
- package/public/index.html +6 -0
- package/public/style.css +160 -19
- package/server.js +193 -48
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) => {
|
|
@@ -2458,6 +2565,7 @@ function renderSessions() {
|
|
|
2458
2565
|
const showCtx = !!session.contextStatus;
|
|
2459
2566
|
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
2460
2567
|
const bookmarksCount = loadPins(session.id).length;
|
|
2568
|
+
const hasScratchpad = !!(localStorage.getItem(_sessionScratchpadKey(session.id)) || '').trim();
|
|
2461
2569
|
const tempClass = session.hasRecentLog || session.inProgress || session.hasWaitingForUser ? 'warm' : 'stale';
|
|
2462
2570
|
return `
|
|
2463
2571
|
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${tempClass} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
|
|
@@ -2474,12 +2582,10 @@ function renderSessions() {
|
|
|
2474
2582
|
${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>` : ''}
|
|
2475
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>` : ''}
|
|
2476
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>` : ''}
|
|
2477
|
-
${
|
|
2478
|
-
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')"
|
|
2479
|
-
${session.hasWaitingForUser ?
|
|
2480
|
-
${
|
|
2481
|
-
${(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>` : ''}
|
|
2482
|
-
${isLive ? '<span class="pulse"></span>' : ''}
|
|
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>` : ''}
|
|
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>` : ''}
|
|
2483
2589
|
</span>
|
|
2484
2590
|
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%"></div></div>
|
|
2485
2591
|
<span class="progress-text">${session.completed}/${total}</span>
|
|
@@ -3613,9 +3719,17 @@ const _scratchpadCharcount = document.getElementById('scratchpad-charcount');
|
|
|
3613
3719
|
|
|
3614
3720
|
let _scratchpadKeyOverride = null;
|
|
3615
3721
|
|
|
3722
|
+
function _sessionScratchpadKey(sessionId) {
|
|
3723
|
+
return `scratchpad-${sessionId}`;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
function _isSessionScratchpadKey(key) {
|
|
3727
|
+
return key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:');
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3616
3730
|
function _scratchpadKey() {
|
|
3617
3731
|
if (_scratchpadKeyOverride) return _scratchpadKeyOverride;
|
|
3618
|
-
if (currentSessionId) return
|
|
3732
|
+
if (currentSessionId) return _sessionScratchpadKey(currentSessionId);
|
|
3619
3733
|
if (currentProjectPath) return `scratchpad-project:${currentProjectPath}`;
|
|
3620
3734
|
return null;
|
|
3621
3735
|
}
|
|
@@ -3628,6 +3742,11 @@ function toggleScratchpad() {
|
|
|
3628
3742
|
}
|
|
3629
3743
|
}
|
|
3630
3744
|
|
|
3745
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
3746
|
+
function openSessionScratchpad(sessionId) {
|
|
3747
|
+
showScratchpad(_sessionScratchpadKey(sessionId));
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3631
3750
|
function showScratchpad(keyOverride) {
|
|
3632
3751
|
_scratchpadKeyOverride = keyOverride || null;
|
|
3633
3752
|
const key = _scratchpadKey();
|
|
@@ -3652,11 +3771,16 @@ function saveScratchpad() {
|
|
|
3652
3771
|
const key = _scratchpadKey();
|
|
3653
3772
|
if (!key) return;
|
|
3654
3773
|
const val = _scratchpadTextarea.value;
|
|
3655
|
-
|
|
3774
|
+
const had = !!(localStorage.getItem(key) || '').trim();
|
|
3775
|
+
const has = !!val.trim();
|
|
3776
|
+
if (has) {
|
|
3656
3777
|
localStorage.setItem(key, val);
|
|
3657
3778
|
} else {
|
|
3658
3779
|
localStorage.removeItem(key);
|
|
3659
3780
|
}
|
|
3781
|
+
if (had !== has && _isSessionScratchpadKey(key)) {
|
|
3782
|
+
renderSessions();
|
|
3783
|
+
}
|
|
3660
3784
|
}
|
|
3661
3785
|
|
|
3662
3786
|
_scratchpadTextarea.addEventListener('input', () => {
|
|
@@ -4075,7 +4199,7 @@ function _findOrphanedKeys() {
|
|
|
4075
4199
|
const key = localStorage.key(i);
|
|
4076
4200
|
if (key.startsWith('pinned-messages-')) {
|
|
4077
4201
|
if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
|
|
4078
|
-
} else if (
|
|
4202
|
+
} else if (_isSessionScratchpadKey(key)) {
|
|
4079
4203
|
if (!known.has(key.slice('scratchpad-'.length))) orphaned.push(key);
|
|
4080
4204
|
} else if (key.startsWith(PREVIEW_STORAGE_PREFIX)) {
|
|
4081
4205
|
if (!known.has(key.slice(PREVIEW_STORAGE_PREFIX.length))) orphaned.push(key);
|
|
@@ -4360,6 +4484,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
4360
4484
|
dismissedSessionIds.add(contextSid);
|
|
4361
4485
|
updateDismissBtnState();
|
|
4362
4486
|
renderSessions();
|
|
4487
|
+
renderActivityChip();
|
|
4363
4488
|
const newItems = getNavigableItems();
|
|
4364
4489
|
const targetIdx = newItems.length > 0 ? Math.max(0, prevIdx - 1) : -1;
|
|
4365
4490
|
// If the dismissed session is currently open, navigate to the previous one
|
|
@@ -5068,7 +5193,7 @@ function renderAgentTabs(promptHtml, responseHtml, promptText, responseText) {
|
|
|
5068
5193
|
}
|
|
5069
5194
|
if (!tabs.length) return '';
|
|
5070
5195
|
const defaultTab = responseHtml ? 'response' : tabs[0].key;
|
|
5071
|
-
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>`;
|
|
5072
5197
|
const tabsHtml = tabs
|
|
5073
5198
|
.map(
|
|
5074
5199
|
(t) =>
|
|
@@ -5473,13 +5598,13 @@ async function showSessionInfoModal(sessionId) {
|
|
|
5473
5598
|
// and re-rendered when they arrive, so the modal doesn't block on network.
|
|
5474
5599
|
_planSessionId = sessionId;
|
|
5475
5600
|
const cachedTasks = currentSessionId === sessionId ? currentTasks : [];
|
|
5476
|
-
showInfoModal(session, null, cachedTasks, null);
|
|
5601
|
+
showInfoModal(session, null, cachedTasks, null, null);
|
|
5477
5602
|
|
|
5478
|
-
const rerender = (teamConfig, tasks, planContent) => {
|
|
5603
|
+
const rerender = (teamConfig, tasks, planContent, parentInfo) => {
|
|
5479
5604
|
if (_planSessionId !== sessionId) return; // user opened a different modal
|
|
5480
5605
|
const modal = document.getElementById('team-modal');
|
|
5481
5606
|
if (!modal?.classList.contains('visible')) return; // user closed modal — don't reopen
|
|
5482
|
-
showInfoModal(session, teamConfig, tasks, planContent);
|
|
5607
|
+
showInfoModal(session, teamConfig, tasks, planContent, parentInfo);
|
|
5483
5608
|
};
|
|
5484
5609
|
|
|
5485
5610
|
const teamPromise = session.isTeam
|
|
@@ -5500,8 +5625,17 @@ async function showSessionInfoModal(sessionId) {
|
|
|
5500
5625
|
.then((r) => (r.ok ? r.json() : []))
|
|
5501
5626
|
.catch(() => []);
|
|
5502
5627
|
|
|
5503
|
-
const
|
|
5504
|
-
|
|
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);
|
|
5505
5639
|
}
|
|
5506
5640
|
|
|
5507
5641
|
let _infoModalSessionId = null;
|
|
@@ -5518,7 +5652,7 @@ function updateStickyBtnState() {
|
|
|
5518
5652
|
if (svg) svg.setAttribute('fill', isSticky ? 'currentColor' : 'none');
|
|
5519
5653
|
}
|
|
5520
5654
|
|
|
5521
|
-
function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
5655
|
+
function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
5522
5656
|
const modal = document.getElementById('team-modal');
|
|
5523
5657
|
const titleEl = document.getElementById('team-modal-title');
|
|
5524
5658
|
const bodyEl = document.getElementById('team-modal-body');
|
|
@@ -5538,6 +5672,13 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5538
5672
|
// Each row: [label, displayValue, { openPath?, copyValue? }]
|
|
5539
5673
|
const infoRows = [];
|
|
5540
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
|
+
}
|
|
5541
5682
|
if (session.slug && session.hasPlan) {
|
|
5542
5683
|
infoRows.push(['Slug', session.slug, { openClaudeDir: true, openFile: session.planPath }]);
|
|
5543
5684
|
}
|
|
@@ -5569,19 +5710,25 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5569
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;";
|
|
5570
5711
|
const plainStyle =
|
|
5571
5712
|
"font-family: 'IBM Plex Mono', monospace; font-size: 12px; user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
|
|
5572
|
-
html += `<div class="team-modal-meta
|
|
5713
|
+
html += `<div class="team-modal-meta info-grid">`;
|
|
5573
5714
|
infoRows.forEach(([label, value, opts]) => {
|
|
5574
5715
|
const copyVal = escapeHtml(value).replace(/"/g, '"');
|
|
5575
5716
|
html += `<span style="font-weight: 500; color: var(--text-secondary); font-size: 12px; white-space: nowrap;">${label}</span>`;
|
|
5576
|
-
if (opts?.
|
|
5577
|
-
const
|
|
5578
|
-
|
|
5579
|
-
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>`;
|
|
5580
5720
|
} else {
|
|
5581
5721
|
html += `<span style="${plainStyle}" title="${copyVal}">${escapeHtml(value)}</span>`;
|
|
5582
5722
|
}
|
|
5583
5723
|
const jsCopyVal = _escapeForJsAttr(copyVal);
|
|
5584
|
-
|
|
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>`;
|
|
5585
5732
|
});
|
|
5586
5733
|
html += `</div>`;
|
|
5587
5734
|
|
|
@@ -5667,6 +5814,11 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
5667
5814
|
updateDismissBtnState();
|
|
5668
5815
|
const costBtn = document.getElementById('session-info-cost-btn');
|
|
5669
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';
|
|
5670
5822
|
modal.classList.add('visible');
|
|
5671
5823
|
|
|
5672
5824
|
if (alreadyVisible) return; // re-render during deferred hydration — key handler already attached
|
|
@@ -5687,6 +5839,12 @@ function closeTeamModal() {
|
|
|
5687
5839
|
_planSessionId = null;
|
|
5688
5840
|
}
|
|
5689
5841
|
|
|
5842
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5843
|
+
function openSessionFromInfo(sessionId) {
|
|
5844
|
+
closeTeamModal();
|
|
5845
|
+
fetchTasks(sessionId);
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5690
5848
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5691
5849
|
function toggleDismissSession(sessionId) {
|
|
5692
5850
|
if (dismissedSessionIds.has(sessionId)) {
|
|
@@ -5696,6 +5854,7 @@ function toggleDismissSession(sessionId) {
|
|
|
5696
5854
|
}
|
|
5697
5855
|
updateDismissBtnState();
|
|
5698
5856
|
renderSessions();
|
|
5857
|
+
renderActivityChip();
|
|
5699
5858
|
}
|
|
5700
5859
|
|
|
5701
5860
|
function updateDismissBtnState() {
|
|
@@ -5781,7 +5940,6 @@ function openCost(sessionId) {
|
|
|
5781
5940
|
}
|
|
5782
5941
|
}
|
|
5783
5942
|
|
|
5784
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5785
5943
|
function openMarketplace(projectPath) {
|
|
5786
5944
|
const params = new URLSearchParams({ project: projectPath });
|
|
5787
5945
|
if (window.__HUB__?.enabled) {
|
|
@@ -5793,7 +5951,6 @@ function openMarketplace(projectPath) {
|
|
|
5793
5951
|
}
|
|
5794
5952
|
}
|
|
5795
5953
|
|
|
5796
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
5797
5954
|
function openMemory(projectPath) {
|
|
5798
5955
|
const params = new URLSearchParams({ project: projectPath });
|
|
5799
5956
|
if (window.__HUB__?.enabled) {
|
|
@@ -5805,6 +5962,19 @@ function openMemory(projectPath) {
|
|
|
5805
5962
|
}
|
|
5806
5963
|
}
|
|
5807
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
|
+
|
|
5808
5978
|
//#endregion
|
|
5809
5979
|
|
|
5810
5980
|
//#region OWNER_FILTER
|
|
@@ -5970,6 +6140,12 @@ function makeLimitCell(label, bucket) {
|
|
|
5970
6140
|
const strong = document.createElement('strong');
|
|
5971
6141
|
strong.textContent = pct == null ? '-%' : `${Math.ceil(pct)}%`;
|
|
5972
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
|
+
}
|
|
5973
6149
|
return cell;
|
|
5974
6150
|
}
|
|
5975
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"/>
|