claude-code-kanban 4.17.0 → 4.19.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/contain.js +78 -0
- package/lib/inline-assets.js +238 -0
- package/lib/net-guard.js +267 -0
- package/lib/open-editor.js +267 -0
- package/lib/parsers.js +8 -1
- package/package.json +3 -3
- package/public/app.js +181 -77
- package/public/index.html +19 -11
- package/public/style.css +35 -2
- package/server.js +146 -52
package/public/app.js
CHANGED
|
@@ -332,7 +332,7 @@ function renderActivityChip() {
|
|
|
332
332
|
return `
|
|
333
333
|
<button type="button"
|
|
334
334
|
class="${classes}"
|
|
335
|
-
onclick="setActivityFilter('${c.kind}')"
|
|
335
|
+
onclick="setActivityFilter('${escAttrJs(c.kind)}')"
|
|
336
336
|
aria-pressed="${isOn ? 'true' : 'false'}"
|
|
337
337
|
title="${escapeHtml(c.title + hint)}">
|
|
338
338
|
<span class="activity-dot"></span>
|
|
@@ -991,7 +991,7 @@ function renderToolItem(m, i, compact) {
|
|
|
991
991
|
// click all light up DURING the run, identical to post-completion.
|
|
992
992
|
const agentLink =
|
|
993
993
|
m.tool === 'Agent' && m.agentId
|
|
994
|
-
? ` <span class="msg-agent-link" title="View agent" onclick="event.stopPropagation();showAgentModal('${
|
|
994
|
+
? ` <span class="msg-agent-link" title="View agent" onclick="event.stopPropagation();showAgentModal('${escAttrJs(m.agentId)}')">⇗</span>`
|
|
995
995
|
: '';
|
|
996
996
|
// Usage chip (tokens · tools · duration) on completed Agent rows — same stats a
|
|
997
997
|
// background agent shows, sourced here from the foreground toolUseResult.
|
|
@@ -1376,7 +1376,7 @@ function getToolIcon(toolName) {
|
|
|
1376
1376
|
const AGENT_LOG_ICON =
|
|
1377
1377
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
|
1378
1378
|
function agentLogButton(agentId) {
|
|
1379
|
-
return `<button class="msg-agent-log-btn" onclick="event.stopPropagation();viewAgentLog('${
|
|
1379
|
+
return `<button class="msg-agent-log-btn" onclick="event.stopPropagation();viewAgentLog('${escAttrJs(agentId)}')" title="View agent log">${AGENT_LOG_ICON}</button>`;
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
1382
|
function getPinId(m) {
|
|
@@ -1698,7 +1698,7 @@ const linkSvg = (size) =>
|
|
|
1698
1698
|
// Shared markup for an attachment image and the labeled section that holds a grid
|
|
1699
1699
|
// of them — used by both user-pasted images and agent tool-result images.
|
|
1700
1700
|
function attachImageTag(url, alt) {
|
|
1701
|
-
return `<img src="${url}" loading="lazy" alt="${alt}" class="user-attach-image" />`;
|
|
1701
|
+
return `<img src="${escapeHtml(url)}" loading="lazy" alt="${escapeHtml(alt)}" class="user-attach-image" />`;
|
|
1702
1702
|
}
|
|
1703
1703
|
function attachImageSection(label, imgsHtml) {
|
|
1704
1704
|
return imgsHtml
|
|
@@ -1736,7 +1736,7 @@ function renderUserAttachments(m) {
|
|
|
1736
1736
|
return `<details class="user-attach-toolresult">
|
|
1737
1737
|
<summary>Tool result <code>${escapeHtml(shortId)}</code></summary>
|
|
1738
1738
|
<pre class="${TINTED_PRE_CLASS}" id="${expandId}">${preview}</pre>
|
|
1739
|
-
<button type="button" class="tool-result-expand-btn" data-expand-id="${expandId}" data-tool-use-id="${safeId}" onclick="_toggleToolResultExpand(this)">Show full</button>
|
|
1739
|
+
<button type="button" class="tool-result-expand-btn" data-expand-id="${expandId}" data-tool-use-id="${escapeHtml(safeId)}" onclick="_toggleToolResultExpand(this)">Show full</button>
|
|
1740
1740
|
</details>`;
|
|
1741
1741
|
})
|
|
1742
1742
|
.join('');
|
|
@@ -1876,7 +1876,7 @@ function showMsgDetail(idx) {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
|
|
1878
1878
|
function closeMsgDetailModal() {
|
|
1879
|
-
|
|
1879
|
+
hideModalOverlay('msg-detail-modal');
|
|
1880
1880
|
msgDetailFollowLatest = false;
|
|
1881
1881
|
// Drop the message highlight on close, mirroring task-card behavior.
|
|
1882
1882
|
msgHighlightDimmed = true;
|
|
@@ -1900,19 +1900,32 @@ function _setModalWidth(modal, slot, on, maxWidth, width) {
|
|
|
1900
1900
|
}
|
|
1901
1901
|
}
|
|
1902
1902
|
|
|
1903
|
+
function _applyModalFullscreen(modalId, on) {
|
|
1904
|
+
const modal = document.querySelector(`#${modalId} .modal`);
|
|
1905
|
+
modal.classList.toggle('fullscreen', on);
|
|
1906
|
+
_setModalWidth(modal, 'Fs', on, '', '');
|
|
1907
|
+
updateFullscreenBtnIcon(`${modalId}-fullscreen-btn`, on);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1903
1910
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1904
1911
|
function toggleModalFullscreen(modalId) {
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
updateFullscreenBtnIcon(`${modalId}-fullscreen-btn`, isFs);
|
|
1912
|
+
const on = !document.querySelector(`#${modalId} .modal`).classList.contains('fullscreen');
|
|
1913
|
+
_applyModalFullscreen(modalId, on);
|
|
1914
|
+
localStorage.setItem(`modal-fullscreen-${modalId}`, String(on));
|
|
1909
1915
|
}
|
|
1910
1916
|
|
|
1911
|
-
function
|
|
1917
|
+
function loadModalFullscreen() {
|
|
1918
|
+
for (const btn of document.querySelectorAll('[id$="-fullscreen-btn"]')) {
|
|
1919
|
+
const modalId = btn.id.replace(/-fullscreen-btn$/, '');
|
|
1920
|
+
if (localStorage.getItem(`modal-fullscreen-${modalId}`) === 'true') _applyModalFullscreen(modalId, true);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Hides the overlay only — the fullscreen state stays on the dialog, it is a
|
|
1925
|
+
// remembered preference so the next open comes back the way it was left.
|
|
1926
|
+
function hideModalOverlay(modalId) {
|
|
1912
1927
|
const modal = document.getElementById(modalId);
|
|
1913
1928
|
modal.classList.remove('visible');
|
|
1914
|
-
modal.querySelector('.modal').classList.remove('fullscreen');
|
|
1915
|
-
updateFullscreenBtnIcon(`${modalId}-fullscreen-btn`, false);
|
|
1916
1929
|
return modal;
|
|
1917
1930
|
}
|
|
1918
1931
|
|
|
@@ -1924,6 +1937,38 @@ function updateFullscreenBtnIcon(btnId, isFullscreen) {
|
|
|
1924
1937
|
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';
|
|
1925
1938
|
}
|
|
1926
1939
|
|
|
1940
|
+
const MODAL_ZOOM_KEY = 'modal-zoom';
|
|
1941
|
+
const MODAL_ZOOM_MIN = 0.7;
|
|
1942
|
+
const MODAL_ZOOM_MAX = 2.0;
|
|
1943
|
+
let modalZoom = clampModalZoom(Number.parseFloat(localStorage.getItem(MODAL_ZOOM_KEY)) || 1);
|
|
1944
|
+
|
|
1945
|
+
function clampModalZoom(v) {
|
|
1946
|
+
return Math.min(MODAL_ZOOM_MAX, Math.max(MODAL_ZOOM_MIN, v));
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
function applyModalZoom() {
|
|
1950
|
+
document.documentElement.style.setProperty('--modal-zoom', String(modalZoom));
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// delta 0 resets to 100%
|
|
1954
|
+
function adjustModalZoom(delta) {
|
|
1955
|
+
const next = delta === 0 ? 1 : clampModalZoom(Math.round((modalZoom + delta) * 10) / 10);
|
|
1956
|
+
if (next === modalZoom && delta !== 0) return;
|
|
1957
|
+
modalZoom = next;
|
|
1958
|
+
localStorage.setItem(MODAL_ZOOM_KEY, String(modalZoom));
|
|
1959
|
+
applyModalZoom();
|
|
1960
|
+
showToast(`Text ${Math.round(modalZoom * 100)}%`);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// The markup decides what scales: `.modal-zoomable` marks a modal's reading
|
|
1964
|
+
// surface, and style.css hangs the zoom on that same class. Confirm dialogs
|
|
1965
|
+
// carry no such body — nothing there is worth enlarging.
|
|
1966
|
+
function isZoomableModalOpen() {
|
|
1967
|
+
return document.querySelector('.modal-overlay.visible .modal-zoomable') !== null;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const ZOOM_KEYS = { '+': 0.1, '=': 0.1, NumpadAdd: 0.1, '-': -0.1, _: -0.1, NumpadSubtract: -0.1, 0: 0, Numpad0: 0 };
|
|
1971
|
+
|
|
1927
1972
|
let _toastTimer = null;
|
|
1928
1973
|
let _manualRefreshing = false;
|
|
1929
1974
|
//#endregion
|
|
@@ -2538,7 +2583,7 @@ function renderAgentFooter() {
|
|
|
2538
2583
|
const agentColor = resolveNamedColor(a.color);
|
|
2539
2584
|
const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
|
|
2540
2585
|
const selectedClass = a.agentId === currentAgentModalId ? ' selected' : '';
|
|
2541
|
-
return `<div class="agent-card${selectedClass}" data-agent-id="${escapeHtml(a.agentId)}"${colorStyle} onclick="showAgentModal('${a.agentId}')">
|
|
2586
|
+
return `<div class="agent-card${selectedClass}" data-agent-id="${escapeHtml(a.agentId)}"${colorStyle} onclick="showAgentModal('${escAttrJs(a.agentId)}')">
|
|
2542
2587
|
<div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span>${nameBadgeHtml}</div>
|
|
2543
2588
|
<div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
|
|
2544
2589
|
${msgHtml}
|
|
@@ -2704,7 +2749,7 @@ function showAgentModal(agentId) {
|
|
|
2704
2749
|
}
|
|
2705
2750
|
|
|
2706
2751
|
function closeAgentModal() {
|
|
2707
|
-
|
|
2752
|
+
hideModalOverlay('agent-modal');
|
|
2708
2753
|
currentAgentModalId = null;
|
|
2709
2754
|
highlightSelectedAgent();
|
|
2710
2755
|
}
|
|
@@ -2714,7 +2759,7 @@ function highlightSelectedAgent() {
|
|
|
2714
2759
|
if (!content) return;
|
|
2715
2760
|
for (const el of content.querySelectorAll('.agent-card.selected')) el.classList.remove('selected');
|
|
2716
2761
|
if (currentAgentModalId == null) return;
|
|
2717
|
-
const el = content.querySelector(`.agent-card[data-agent-id="${currentAgentModalId}"]`);
|
|
2762
|
+
const el = content.querySelector(`.agent-card[data-agent-id="${escSel(currentAgentModalId)}"]`);
|
|
2718
2763
|
if (el) el.classList.add('selected');
|
|
2719
2764
|
}
|
|
2720
2765
|
|
|
@@ -2938,9 +2983,10 @@ function renderSessions() {
|
|
|
2938
2983
|
const bookmarksCount = loadPins(session.id).length;
|
|
2939
2984
|
const hasScratchpad = !!(localStorage.getItem(_sessionScratchpadKey(session.id)) || '').trim();
|
|
2940
2985
|
const tempClass = session.hasRecentLog || session.inProgress || session.hasWaitingForUser ? 'warm' : 'stale';
|
|
2986
|
+
const sid = escAttrJs(session.id);
|
|
2941
2987
|
return `
|
|
2942
|
-
<button onclick="fetchTasks('${
|
|
2943
|
-
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${
|
|
2988
|
+
<button onclick="fetchTasks('${sid}')" data-session-id="${escapeHtml(session.id)}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${tempClass} ${showCtx ? 'has-context' : ''}" title="${escapeHtml(tooltip)}">
|
|
2989
|
+
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${sid}')" title="${pinTitle} session">${pinState === 'sticky' ? SESSION_STAR_SVG : SESSION_PIN_SVG}</span>
|
|
2944
2990
|
<div class="session-name">${escapeHtml(primaryName)}</div>
|
|
2945
2991
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
2946
2992
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
@@ -2949,14 +2995,14 @@ function renderSessions() {
|
|
|
2949
2995
|
<div class="session-progress">
|
|
2950
2996
|
<span class="session-indicators">
|
|
2951
2997
|
${isTeam ? `<span class="team-badge" title="${memberCount} team members"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
|
|
2952
|
-
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${
|
|
2998
|
+
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${sid}')" title="View session info"><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"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>` : ''}
|
|
2953
2999
|
${renderWorkflowBadge(session)}
|
|
2954
3000
|
${renderLoopBadge(session)}
|
|
2955
|
-
${hasScratchpad ? `<span class="scratchpad-badge" onclick="event.stopPropagation(); openSessionScratchpad('${
|
|
2956
|
-
${bookmarksCount > 0 ? `<span class="bookmarks-badge" onclick="event.stopPropagation(); openSessionWithBookmarks('${
|
|
2957
|
-
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${
|
|
2958
|
-
${session.hasPlan && !session.planSourceSessionId ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${
|
|
2959
|
-
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${
|
|
3001
|
+
${hasScratchpad ? `<span class="scratchpad-badge" onclick="event.stopPropagation(); openSessionScratchpad('${sid}')" 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>` : ''}
|
|
3002
|
+
${bookmarksCount > 0 ? `<span class="bookmarks-badge" onclick="event.stopPropagation(); openSessionWithBookmarks('${sid}')" 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>` : ''}
|
|
3003
|
+
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${sid}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
3004
|
+
${session.hasPlan && !session.planSourceSessionId ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${sid}')" title="View plan">${ICON_PLAN}</span>` : ''}
|
|
3005
|
+
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escAttrJs(session.planSourceSessionId)}')">${ICON_PLAN}</span>` : ''}
|
|
2960
3006
|
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
2961
3007
|
${session.hasWaitingForUser ? `<span class="agent-badge agent-badge-waiting" title="Waiting for user">${ICON_AGENT_WAITING}</span>` : ''}
|
|
2962
3008
|
${session.hasRunningAgents && !session.hasWaitingForUser ? `<span class="agent-badge agent-badge-active" title="Agents running">${ICON_AGENT_ACTIVE}</span>` : ''}
|
|
@@ -3205,9 +3251,9 @@ function renderTaskCard(task) {
|
|
|
3205
3251
|
<div
|
|
3206
3252
|
role="listitem"
|
|
3207
3253
|
tabindex="0"
|
|
3208
|
-
data-task-id="${task.id}"
|
|
3209
|
-
data-session-id="${actualSessionId}"
|
|
3210
|
-
onclick="showTaskDetail('${task.id}', '${actualSessionId}')"
|
|
3254
|
+
data-task-id="${escapeHtml(task.id)}"
|
|
3255
|
+
data-session-id="${escapeHtml(actualSessionId)}"
|
|
3256
|
+
onclick="showTaskDetail('${escAttrJs(task.id)}', '${escAttrJs(actualSessionId)}')"
|
|
3211
3257
|
draggable="true"
|
|
3212
3258
|
ondragstart="onCardDragStart(event)"
|
|
3213
3259
|
ondragend="onCardDragEnd(event)"
|
|
@@ -3271,8 +3317,9 @@ function renderKanban() {
|
|
|
3271
3317
|
|
|
3272
3318
|
if (selectedTaskId) {
|
|
3273
3319
|
const card =
|
|
3274
|
-
document.querySelector(
|
|
3275
|
-
|
|
3320
|
+
document.querySelector(
|
|
3321
|
+
`.task-card[data-task-id="${escSel(selectedTaskId)}"][data-session-id="${escSel(selectedSessionId)}"]`,
|
|
3322
|
+
) || document.querySelector(`.task-card[data-task-id="${escSel(selectedTaskId)}"]`);
|
|
3276
3323
|
if (card) {
|
|
3277
3324
|
if (focusZone === 'board' && !taskHighlightDimmed) card.classList.add('selected');
|
|
3278
3325
|
} else {
|
|
@@ -3364,8 +3411,8 @@ function selectTask(taskId, sessionId) {
|
|
|
3364
3411
|
taskHighlightDimmed = false;
|
|
3365
3412
|
if (!taskId) return;
|
|
3366
3413
|
const card =
|
|
3367
|
-
document.querySelector(`.task-card[data-task-id="${taskId}"][data-session-id="${sessionId}"]`) ||
|
|
3368
|
-
document.querySelector(`.task-card[data-task-id="${taskId}"]`);
|
|
3414
|
+
document.querySelector(`.task-card[data-task-id="${escSel(taskId)}"][data-session-id="${escSel(sessionId)}"]`) ||
|
|
3415
|
+
document.querySelector(`.task-card[data-task-id="${escSel(taskId)}"]`);
|
|
3369
3416
|
if (card) {
|
|
3370
3417
|
card.classList.add('selected');
|
|
3371
3418
|
card.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
@@ -3641,7 +3688,7 @@ function setFocusZone(zone) {
|
|
|
3641
3688
|
}
|
|
3642
3689
|
if (selectedTaskId) {
|
|
3643
3690
|
const card = document.querySelector(
|
|
3644
|
-
`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`,
|
|
3691
|
+
`.task-card[data-task-id="${escSel(selectedTaskId)}"][data-session-id="${escSel(selectedSessionId)}"]`,
|
|
3645
3692
|
);
|
|
3646
3693
|
if (card) card.classList.add('selected');
|
|
3647
3694
|
} else {
|
|
@@ -3665,7 +3712,7 @@ function getAvailableTasksOptions(currentTaskId = null) {
|
|
|
3665
3712
|
if (pending.length > 0) {
|
|
3666
3713
|
options += '<optgroup label="Pending">';
|
|
3667
3714
|
pending.forEach((t, _idx) => {
|
|
3668
|
-
options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
3715
|
+
options += `<option value="${escapeHtml(t.id)}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
3669
3716
|
});
|
|
3670
3717
|
options += '</optgroup>';
|
|
3671
3718
|
}
|
|
@@ -3673,7 +3720,7 @@ function getAvailableTasksOptions(currentTaskId = null) {
|
|
|
3673
3720
|
if (inProgress.length > 0) {
|
|
3674
3721
|
options += '<optgroup label="In Progress">';
|
|
3675
3722
|
inProgress.forEach((t, _idx) => {
|
|
3676
|
-
options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
3723
|
+
options += `<option value="${escapeHtml(t.id)}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
3677
3724
|
});
|
|
3678
3725
|
options += '</optgroup>';
|
|
3679
3726
|
}
|
|
@@ -3681,7 +3728,7 @@ function getAvailableTasksOptions(currentTaskId = null) {
|
|
|
3681
3728
|
if (completed.length > 0) {
|
|
3682
3729
|
options += '<optgroup label="Completed">';
|
|
3683
3730
|
completed.forEach((t, _idx) => {
|
|
3684
|
-
options += `<option value="${t.id}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
3731
|
+
options += `<option value="${escapeHtml(t.id)}">#${t.id} - ${escapeHtml(t.subject)}</option>`;
|
|
3685
3732
|
});
|
|
3686
3733
|
options += '</optgroup>';
|
|
3687
3734
|
}
|
|
@@ -4188,11 +4235,6 @@ function _projectLabel(project) {
|
|
|
4188
4235
|
return project.split(/[/\\]/).pop() || project;
|
|
4189
4236
|
}
|
|
4190
4237
|
|
|
4191
|
-
function _escapeForJsAttr(str) {
|
|
4192
|
-
const jsEscaped = str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
4193
|
-
return escapeHtml(jsEscaped);
|
|
4194
|
-
}
|
|
4195
|
-
|
|
4196
4238
|
function _renderProjectGroup(label, meta, innerHtml) {
|
|
4197
4239
|
return `<div class="storage-project-group">
|
|
4198
4240
|
<div class="storage-project-header">
|
|
@@ -4260,7 +4302,7 @@ function _renderStorageSessions() {
|
|
|
4260
4302
|
function renderMessageItems(id) {
|
|
4261
4303
|
const g = msgMap.get(id);
|
|
4262
4304
|
if (!g) return '';
|
|
4263
|
-
const eid =
|
|
4305
|
+
const eid = escAttrJs(id);
|
|
4264
4306
|
const header = `<div class="storage-group-header" style="padding-left:12px;">
|
|
4265
4307
|
<span>${g.pins.length} pinned message${g.pins.length > 1 ? 's' : ''}</span>
|
|
4266
4308
|
<div class="storage-item-actions">
|
|
@@ -4271,8 +4313,8 @@ function _renderStorageSessions() {
|
|
|
4271
4313
|
.map((p) => {
|
|
4272
4314
|
const type = escapeHtml(p.type || '?');
|
|
4273
4315
|
const text = escapeHtml((p.text || p.tool || p.agentType || '').slice(0, 60));
|
|
4274
|
-
const pinId =
|
|
4275
|
-
const sid =
|
|
4316
|
+
const pinId = escAttrJs(p.id || '');
|
|
4317
|
+
const sid = escAttrJs(id);
|
|
4276
4318
|
return `<div class="storage-item storage-item-clickable" style="padding-left:24px;" onclick="_storagePreviewPin('${sid}','${pinId}')">
|
|
4277
4319
|
<span class="storage-item-badge">${type}</span>
|
|
4278
4320
|
<span class="storage-item-id">${text}</span>
|
|
@@ -4289,7 +4331,7 @@ function _renderStorageSessions() {
|
|
|
4289
4331
|
|
|
4290
4332
|
function renderSessionItem({ id, session }) {
|
|
4291
4333
|
const isPinned = isAnyPinned(id);
|
|
4292
|
-
const eid =
|
|
4334
|
+
const eid = escAttrJs(id);
|
|
4293
4335
|
const actions = isPinned
|
|
4294
4336
|
? `<button onclick="_storageViewSession('${eid}')">View</button>
|
|
4295
4337
|
<button class="danger" onclick="_storageUnpinSession('${eid}')">Unpin</button>`
|
|
@@ -4389,7 +4431,7 @@ function _renderStorageScratchpads() {
|
|
|
4389
4431
|
const typeBadge = item.isProject
|
|
4390
4432
|
? '<span class="storage-item-badge">project</span>'
|
|
4391
4433
|
: '<span class="storage-item-badge">session</span>';
|
|
4392
|
-
const jsKey =
|
|
4434
|
+
const jsKey = escAttrJs(item.key);
|
|
4393
4435
|
const label = item.isProject ? escapeHtml(_projectLabel(item.id)) : _sessionLabel(session, item.id);
|
|
4394
4436
|
return `<div class="storage-item">
|
|
4395
4437
|
<span class="storage-item-id" title="${escapeHtml(item.id)}">${label}</span>
|
|
@@ -4478,8 +4520,8 @@ function _renderStorageLinkedDocs() {
|
|
|
4478
4520
|
|
|
4479
4521
|
function renderDocRow(sessionId, p) {
|
|
4480
4522
|
const name = p.split(/[\\/]/).pop();
|
|
4481
|
-
const sid =
|
|
4482
|
-
const jsPath =
|
|
4523
|
+
const sid = escAttrJs(sessionId);
|
|
4524
|
+
const jsPath = escAttrJs(p);
|
|
4483
4525
|
return `<div class="storage-item" style="padding-left:24px;">
|
|
4484
4526
|
<span class="storage-item-id" title="${escapeHtml(p)}">${escapeHtml(name)}</span>
|
|
4485
4527
|
<div class="storage-item-actions">
|
|
@@ -4492,7 +4534,7 @@ function _renderStorageLinkedDocs() {
|
|
|
4492
4534
|
function renderSessionItem({ id, session }) {
|
|
4493
4535
|
const entry = byId.get(id);
|
|
4494
4536
|
if (!entry) return '';
|
|
4495
|
-
const eid =
|
|
4537
|
+
const eid = escAttrJs(id);
|
|
4496
4538
|
const count = entry.paths.length;
|
|
4497
4539
|
const header = `<div class="storage-group-header">
|
|
4498
4540
|
<span>${_sessionLabel(session, id)} <span class="storage-item-badge">${count} doc${count > 1 ? 's' : ''}</span></span>
|
|
@@ -4618,6 +4660,18 @@ const MODAL_CLOSERS = {
|
|
|
4618
4660
|
};
|
|
4619
4661
|
|
|
4620
4662
|
document.addEventListener('keydown', (e) => {
|
|
4663
|
+
// Scale the open modal's reading surface instead of letting the browser zoom
|
|
4664
|
+
// the whole page. Sits above the text-field guard so it still works with the
|
|
4665
|
+
// caret in a field inside the modal.
|
|
4666
|
+
if ((e.ctrlKey || e.metaKey) && !e.altKey) {
|
|
4667
|
+
const delta = ZOOM_KEYS[e.key] ?? ZOOM_KEYS[e.code];
|
|
4668
|
+
if (delta !== undefined && isZoomableModalOpen()) {
|
|
4669
|
+
e.preventDefault();
|
|
4670
|
+
adjustModalZoom(delta);
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4621
4675
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
|
|
4622
4676
|
return;
|
|
4623
4677
|
}
|
|
@@ -4992,7 +5046,8 @@ function renderFrontmatterBlock(fm) {
|
|
|
4992
5046
|
|
|
4993
5047
|
// Previewed HTML is rendered as authored, not sanitized: the sandbox without
|
|
4994
5048
|
// allow-same-origin puts it on an opaque origin, so it cannot touch this app's
|
|
4995
|
-
// storage, DOM or API.
|
|
5049
|
+
// storage, DOM or API. srcdoc has no base URL, so the server has already embedded
|
|
5050
|
+
// the document's local assets (lib/inline-assets.js); remote refs load normally.
|
|
4996
5051
|
function renderHtmlPreview(bodyEl, content) {
|
|
4997
5052
|
bodyEl.innerHTML = '';
|
|
4998
5053
|
const frame = document.createElement('iframe');
|
|
@@ -5087,7 +5142,7 @@ function refreshInfoModalLinkedDocs() {
|
|
|
5087
5142
|
}
|
|
5088
5143
|
|
|
5089
5144
|
function closePreviewModal() {
|
|
5090
|
-
|
|
5145
|
+
hideModalOverlay('preview-modal');
|
|
5091
5146
|
// Empty the body so an iframe preview is destroyed and its scripts/timers stop.
|
|
5092
5147
|
document.getElementById('preview-modal-body').innerHTML = '';
|
|
5093
5148
|
currentPreviewPath = null;
|
|
@@ -5663,10 +5718,36 @@ function stripTeammateWrapper(text) {
|
|
|
5663
5718
|
return match ? match[1].trim() : text;
|
|
5664
5719
|
}
|
|
5665
5720
|
|
|
5721
|
+
// The div.textContent -> innerHTML trick escapes only & < > : innerHTML serializes
|
|
5722
|
+
// for *text* context, where quotes are correctly left raw. That made every
|
|
5723
|
+
// attribute interpolation breakable with a bare " . It cannot be patched, so the
|
|
5724
|
+
// escaping is explicit here.
|
|
5725
|
+
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' };
|
|
5726
|
+
|
|
5666
5727
|
function escapeHtml(text) {
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5728
|
+
if (text == null) return '';
|
|
5729
|
+
return String(text).replace(/[&<>"'`]/g, (c) => HTML_ESCAPES[c]);
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
// For a value landing inside a quoted JS string inside an HTML attribute —
|
|
5733
|
+
// onclick="fn('${escAttrJs(x)}')". The browser HTML-decodes the attribute before
|
|
5734
|
+
// the JS parser sees it, so the JS escape must happen first and be escaped in turn.
|
|
5735
|
+
function escAttrJs(value) {
|
|
5736
|
+
const js = String(value == null ? '' : value)
|
|
5737
|
+
.replace(/\\/g, '\\\\')
|
|
5738
|
+
.replace(/'/g, "\\'")
|
|
5739
|
+
.replace(/\r/g, '\\r')
|
|
5740
|
+
.replace(/\n/g, '\\n');
|
|
5741
|
+
return escapeHtml(js);
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
// For a value inside a double-quoted CSS attribute selector: [data-x="${escSel(v)}"].
|
|
5745
|
+
// escapeHtml is wrong here — the DOM holds the decoded value, so " in the
|
|
5746
|
+
// selector matches nothing, and a bare " would end the selector string.
|
|
5747
|
+
function escSel(value) {
|
|
5748
|
+
return String(value == null ? '' : value)
|
|
5749
|
+
.replace(/\\/g, '\\\\')
|
|
5750
|
+
.replace(/"/g, '\\"');
|
|
5670
5751
|
}
|
|
5671
5752
|
|
|
5672
5753
|
function toRelativeIfUnder(filePath, baseDir) {
|
|
@@ -5744,7 +5825,7 @@ function renderAgentTabs(promptHtml, responseHtml, promptText, responseText) {
|
|
|
5744
5825
|
const panelsHtml = panels
|
|
5745
5826
|
.map(
|
|
5746
5827
|
(p) =>
|
|
5747
|
-
`<div class="agent-tab-panel${p.key === defaultTab ? ' active' : ''}" data-tab-group="${id}" data-tab-key="${p.key}"><div class="detail-desc rendered-md"
|
|
5828
|
+
`<div class="agent-tab-panel${p.key === defaultTab ? ' active' : ''}" data-tab-group="${id}" data-tab-key="${p.key}"><div class="detail-desc rendered-md">${p.html}</div></div>`,
|
|
5748
5829
|
)
|
|
5749
5830
|
.join('');
|
|
5750
5831
|
return `<div class="agent-tabs">${tabsHtml}${copyBtnHtml}</div>${panelsHtml}`;
|
|
@@ -6372,6 +6453,14 @@ function updateStickyBtnState() {
|
|
|
6372
6453
|
if (svg) svg.setAttribute('fill', isSticky ? 'currentColor' : 'none');
|
|
6373
6454
|
}
|
|
6374
6455
|
|
|
6456
|
+
// Purely cosmetic: the tmp root is identical for every session and the encoded
|
|
6457
|
+
// project segment is already shown in the Path row, so only the last two segments
|
|
6458
|
+
// carry information. The full path stays in the tooltip and the copy button.
|
|
6459
|
+
function abbreviateScratchpadDir(dir) {
|
|
6460
|
+
const sep = dir.includes('\\') ? '\\' : '/';
|
|
6461
|
+
return ['$tmp', '…', ...dir.split(/[/\\]/).slice(-2)].join(sep);
|
|
6462
|
+
}
|
|
6463
|
+
|
|
6375
6464
|
function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
6376
6465
|
const modal = document.getElementById('team-modal');
|
|
6377
6466
|
const titleEl = document.getElementById('team-modal-title');
|
|
@@ -6389,12 +6478,13 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
6389
6478
|
let html = '';
|
|
6390
6479
|
|
|
6391
6480
|
// Session & project details as compact key-value rows
|
|
6392
|
-
// Each row: [label,
|
|
6481
|
+
// Each row: [label, value, { openPath?, abbrev? }] — `value` is authoritative
|
|
6482
|
+
// (tooltip + copy); `abbrev` only replaces the rendered text.
|
|
6393
6483
|
const infoRows = [];
|
|
6394
6484
|
infoRows.push(['Session', session.id, { openClaudeDir: true, openFile: session.jsonlPath }]);
|
|
6395
6485
|
if (parentInfo?.parentSessionId) {
|
|
6396
6486
|
infoRows.push([
|
|
6397
|
-
'Forked from',
|
|
6487
|
+
parentInfo.relation === 'compact' ? 'Continued from' : 'Forked from',
|
|
6398
6488
|
parentInfo.parentSessionId,
|
|
6399
6489
|
{ openClaudeDir: true, openFile: parentInfo.parentJsonlPath, openSession: parentInfo.parentSessionId },
|
|
6400
6490
|
]);
|
|
@@ -6419,6 +6509,13 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
6419
6509
|
if (session.tasksDir) {
|
|
6420
6510
|
infoRows.push(['Tasks Dir', session.tasksDir, { openPath: session.tasksDir }]);
|
|
6421
6511
|
}
|
|
6512
|
+
if (session.scratchpadDir) {
|
|
6513
|
+
infoRows.push([
|
|
6514
|
+
'Scratchpad',
|
|
6515
|
+
session.scratchpadDir,
|
|
6516
|
+
{ openPath: session.scratchpadDir, abbrev: abbreviateScratchpadDir(session.scratchpadDir) },
|
|
6517
|
+
]);
|
|
6518
|
+
}
|
|
6422
6519
|
if (session.sharedTaskList) {
|
|
6423
6520
|
infoRows.push(['Shared Tasks', session.sharedTaskList]);
|
|
6424
6521
|
}
|
|
@@ -6432,20 +6529,17 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
6432
6529
|
'font-family: var(--mono); font-size: 12px; color: var(--text-primary); user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
|
|
6433
6530
|
html += `<div class="team-modal-meta info-grid">`;
|
|
6434
6531
|
infoRows.forEach(([label, value, opts]) => {
|
|
6435
|
-
const copyVal = escapeHtml(value).replace(/"/g, '"');
|
|
6436
6532
|
html += `<span style="font-weight: 500; color: var(--text-secondary); font-size: 12px; white-space: nowrap;">${label}</span>`;
|
|
6437
6533
|
if (opts?.openSession) {
|
|
6438
|
-
|
|
6439
|
-
html += `<span onclick="openSessionFromInfo('${sid}')" style="${clickableStyle}" title="Open session in app">${escapeHtml(value)}</span>`;
|
|
6534
|
+
html += `<span onclick="openSessionFromInfo('${escAttrJs(opts.openSession)}')" style="${clickableStyle}" title="Open session in app">${escapeHtml(value)}</span>`;
|
|
6440
6535
|
} else {
|
|
6441
|
-
html += `<span style="${plainStyle}" title="${
|
|
6536
|
+
html += `<span style="${plainStyle}" title="${escapeHtml(value)}">${escapeHtml(opts?.abbrev || value)}</span>`;
|
|
6442
6537
|
}
|
|
6443
|
-
const
|
|
6444
|
-
const copyBtn = `<button onclick="copyWithFeedback('${jsCopyVal}', this)" title="Copy">${ICON_COPY}</button>`;
|
|
6538
|
+
const copyBtn = `<button onclick="copyWithFeedback('${escAttrJs(value)}', this)" title="Copy">${ICON_COPY}</button>`;
|
|
6445
6539
|
let openBtn = '';
|
|
6446
6540
|
if (opts?.openClaudeDir || opts?.openPath) {
|
|
6447
|
-
const folder = opts.openClaudeDir ? '' : escapeHtml(opts.openPath)
|
|
6448
|
-
const file = opts.openFile ? escapeHtml(opts.openFile)
|
|
6541
|
+
const folder = opts.openClaudeDir ? '' : escapeHtml(opts.openPath);
|
|
6542
|
+
const file = opts.openFile ? escapeHtml(opts.openFile) : '';
|
|
6449
6543
|
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>`;
|
|
6450
6544
|
}
|
|
6451
6545
|
html += `<span class="info-row-actions">${copyBtn}${openBtn}</span>`;
|
|
@@ -6534,7 +6628,9 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
6534
6628
|
}
|
|
6535
6629
|
|
|
6536
6630
|
bodyEl.innerHTML = html;
|
|
6537
|
-
|
|
6631
|
+
// Bind to the section, not bodyEl: bodyEl outlives the modal, so listeners on it
|
|
6632
|
+
// would stack up once per open and fire the click handler N times.
|
|
6633
|
+
bindLinkedDocsHandlers(bodyEl.querySelector('.linked-docs-section'), session.id);
|
|
6538
6634
|
const alreadyVisible = modal.classList.contains('visible');
|
|
6539
6635
|
_infoModalSessionId = session.id;
|
|
6540
6636
|
updateStickyBtnState();
|
|
@@ -6709,7 +6805,7 @@ function renderGoalSubtitle(session) {
|
|
|
6709
6805
|
// Only active (unmet) goals reach here — a met goal auto-clears. Clicking
|
|
6710
6806
|
// opens the info modal (full text); stopPropagation so it doesn't also
|
|
6711
6807
|
// trigger the card's fetchTasks.
|
|
6712
|
-
return `<div class="session-goal" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${escapeHtml(g.condition)}"><span class="session-goal-icon">◎</span>${escapeHtml(short)}</div>`;
|
|
6808
|
+
return `<div class="session-goal" onclick="event.stopPropagation(); showSessionInfoModal('${escAttrJs(session.id)}')" title="${escapeHtml(g.condition)}"><span class="session-goal-icon">◎</span>${escapeHtml(short)}</div>`;
|
|
6713
6809
|
}
|
|
6714
6810
|
|
|
6715
6811
|
function renderLoopBadge(session) {
|
|
@@ -6721,7 +6817,7 @@ function renderLoopBadge(session) {
|
|
|
6721
6817
|
const f = fmtLoopFireTime(li.latest.timestamp, li.latest.delaySeconds);
|
|
6722
6818
|
if (f.abs) tip += ` — latest ${f.status === 'pending' ? 'fires' : 'fired'} ${f.rel} (${f.abs})`;
|
|
6723
6819
|
}
|
|
6724
|
-
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>`;
|
|
6820
|
+
return `<span class="loop-badge" onclick="event.stopPropagation(); showLoopModal('${escAttrJs(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>`;
|
|
6725
6821
|
}
|
|
6726
6822
|
|
|
6727
6823
|
function closeLoopModal() {
|
|
@@ -6739,7 +6835,7 @@ function renderWorkflowBadge(session) {
|
|
|
6739
6835
|
if (!session.hasWorkflow) return '';
|
|
6740
6836
|
const n = session.workflowCount || 0;
|
|
6741
6837
|
const tip = `${n} workflow${n === 1 ? '' : 's'}`;
|
|
6742
|
-
return `<span class="workflow-badge" onclick="event.stopPropagation(); showWorkflowModal('${session.id}')" title="${escapeHtml(tip)}">${WORKFLOW_GEAR_SVG}</span>`;
|
|
6838
|
+
return `<span class="workflow-badge" onclick="event.stopPropagation(); showWorkflowModal('${escAttrJs(session.id)}')" title="${escapeHtml(tip)}">${WORKFLOW_GEAR_SVG}</span>`;
|
|
6743
6839
|
}
|
|
6744
6840
|
|
|
6745
6841
|
let _workflowSessionId = null;
|
|
@@ -6788,11 +6884,11 @@ function renderWorkflowPicker(workflows) {
|
|
|
6788
6884
|
body.innerHTML = workflows
|
|
6789
6885
|
.map((w) => {
|
|
6790
6886
|
const when = w.modifiedAt ? formatDate(w.modifiedAt) : '';
|
|
6791
|
-
return `<div class="workflow-row" onclick="showWorkflowRun('${
|
|
6887
|
+
return `<div class="workflow-row" onclick="showWorkflowRun('${escAttrJs(w.id)}')">
|
|
6792
6888
|
<div class="workflow-row-head">
|
|
6793
6889
|
<span class="workflow-row-name">${escapeHtml(w.name || w.id)}</span>
|
|
6794
6890
|
<span class="workflow-row-id"><code>${escapeHtml(w.id)}</code></span>
|
|
6795
|
-
<button class="icon-btn workflow-open-icon" aria-label="Open in editor" title="Open in editor" onclick="event.stopPropagation(); openWorkflowInEditor('${
|
|
6891
|
+
<button class="icon-btn workflow-open-icon" aria-label="Open in editor" title="Open in editor" onclick="event.stopPropagation(); openWorkflowInEditor('${escAttrJs(w.id)}')">${ICON_OPEN_EDITOR}</button>
|
|
6796
6892
|
</div>
|
|
6797
6893
|
${w.description ? `<div class="workflow-row-desc">${escapeHtml(w.description)}</div>` : ''}
|
|
6798
6894
|
${when ? `<div class="workflow-row-meta">${escapeHtml(when)}</div>` : ''}
|
|
@@ -6858,7 +6954,7 @@ function renderWorkflowRun(run) {
|
|
|
6858
6954
|
.map((a) => {
|
|
6859
6955
|
const dur = a.durationMs != null ? formatDuration(a.durationMs) : '';
|
|
6860
6956
|
const running = a.status !== 'done';
|
|
6861
|
-
return `<div class="wf-agent" onclick="showAgentModal('${
|
|
6957
|
+
return `<div class="wf-agent" onclick="showAgentModal('${escAttrJs(a.agentId)}')" title="${escapeHtml(a.agentId)}">
|
|
6862
6958
|
<span class="wf-agent-type">${escapeHtml(a.type || 'agent')}</span>
|
|
6863
6959
|
<span class="wf-agent-model">${escapeHtml(shortModelName(a.model))}</span>
|
|
6864
6960
|
<span class="wf-agent-tok">${a.outputTokens ? `${fmtTokens(a.outputTokens)} tok` : ''}</span>
|
|
@@ -6960,7 +7056,7 @@ function openPlanModal() {
|
|
|
6960
7056
|
}
|
|
6961
7057
|
|
|
6962
7058
|
function closePlanModal() {
|
|
6963
|
-
|
|
7059
|
+
hideModalOverlay('plan-modal');
|
|
6964
7060
|
}
|
|
6965
7061
|
|
|
6966
7062
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
@@ -7230,6 +7326,8 @@ try {
|
|
|
7230
7326
|
af.forEach((k) => activityFilter.add(k));
|
|
7231
7327
|
} catch (_) {}
|
|
7232
7328
|
initSidebarResize();
|
|
7329
|
+
applyModalZoom();
|
|
7330
|
+
loadModalFullscreen();
|
|
7233
7331
|
loadPanelWidths();
|
|
7234
7332
|
initPanelResize('detail-panel', 'detail-panel-resize', '--detail-panel-width', 'detail-panel-width');
|
|
7235
7333
|
initPanelResize('message-panel', 'message-panel-resize', '--message-panel-width', 'message-panel-width');
|
|
@@ -7450,7 +7548,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
7450
7548
|
if (!window.__HUB__?.enabled) return;
|
|
7451
7549
|
const fwd = (key) => {
|
|
7452
7550
|
e.preventDefault();
|
|
7453
|
-
|
|
7551
|
+
hubPost({ type: 'hub:keydown', key, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey });
|
|
7454
7552
|
};
|
|
7455
7553
|
if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
7456
7554
|
fwd(e.key);
|
|
@@ -7480,17 +7578,24 @@ document.addEventListener('click', (e) => {
|
|
|
7480
7578
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
|
|
7481
7579
|
e.preventDefault();
|
|
7482
7580
|
e.stopPropagation();
|
|
7483
|
-
|
|
7581
|
+
hubPost({ type: 'hub:openExternal', url: url.href });
|
|
7484
7582
|
});
|
|
7485
7583
|
|
|
7486
7584
|
window.hubNavigate = function hubNavigate(app, url) {
|
|
7487
7585
|
if (!window.__HUB__?.enabled) return;
|
|
7488
|
-
|
|
7586
|
+
hubPost({ type: 'hub:navigate', app, url });
|
|
7489
7587
|
};
|
|
7490
7588
|
|
|
7491
7589
|
// Hoisted out of initHubTheme so initHubProject can share it.
|
|
7492
7590
|
const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origin : null);
|
|
7493
7591
|
|
|
7592
|
+
// Every send is addressed to the hub explicitly. With targetOrigin '*' any page that
|
|
7593
|
+
// framed this app also received the forwarded keystrokes and navigation intents.
|
|
7594
|
+
function hubPost(message) {
|
|
7595
|
+
const origin = hubOrigin();
|
|
7596
|
+
if (origin) window.parent?.postMessage(message, origin);
|
|
7597
|
+
}
|
|
7598
|
+
|
|
7494
7599
|
(function initHubTheme() {
|
|
7495
7600
|
const getTheme = () => (document.body.classList.contains('light') ? 'light' : 'dark');
|
|
7496
7601
|
const getColorTheme = () => document.body.dataset.colorTheme || 'ember';
|
|
@@ -7516,8 +7621,7 @@ const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origi
|
|
|
7516
7621
|
if (t === lastTheme && ct === lastColorTheme) return;
|
|
7517
7622
|
lastTheme = t;
|
|
7518
7623
|
lastColorTheme = ct;
|
|
7519
|
-
|
|
7520
|
-
if (origin) window.parent.postMessage({ type: 'hub:theme', theme: t, colorTheme: ct }, origin);
|
|
7624
|
+
hubPost({ type: 'hub:theme', theme: t, colorTheme: ct });
|
|
7521
7625
|
}).observe(document.body, {
|
|
7522
7626
|
attributes: true,
|
|
7523
7627
|
attributeFilter: ['class', 'data-color-theme'],
|