claude-code-kanban 4.14.0 → 4.16.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/cli.js +54 -32
- package/install.js +37 -7
- package/lib/parsers.js +100 -24
- package/package.json +1 -1
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +2 -2
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +26 -25
- package/public/app.js +352 -127
- package/public/index.html +4 -4
- package/public/style.css +364 -74
- package/server.js +344 -47
package/public/app.js
CHANGED
|
@@ -1328,6 +1328,8 @@ const ICON_COPY =
|
|
|
1328
1328
|
'<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>';
|
|
1329
1329
|
const ICON_CHECKMARK =
|
|
1330
1330
|
'<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>';
|
|
1331
|
+
const ICON_PLAN =
|
|
1332
|
+
'<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>';
|
|
1331
1333
|
const ICON_AGENT_WAITING =
|
|
1332
1334
|
'<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>';
|
|
1333
1335
|
const ICON_AGENT_ACTIVE =
|
|
@@ -2634,7 +2636,7 @@ function showAgentModal(agentId) {
|
|
|
2634
2636
|
const modalNameLabel = agent.agentName ? ` · ${escapeHtml(agent.agentName)}` : '';
|
|
2635
2637
|
title.innerHTML = `${statusDot} ${escapeHtml(agent.type || 'unknown')}${modalNameLabel}`;
|
|
2636
2638
|
|
|
2637
|
-
const shortModel = agent.model
|
|
2639
|
+
const shortModel = shortModelName(agent.model);
|
|
2638
2640
|
const shortId = agent.agentId ? agent.agentId.slice(0, 8) : '';
|
|
2639
2641
|
const chip = (label, value, opts = {}) => {
|
|
2640
2642
|
const cls = opts.cls ? ` ${opts.cls}` : '';
|
|
@@ -2648,7 +2650,7 @@ function showAgentModal(agentId) {
|
|
|
2648
2650
|
// toolUseResult), not on the agent-activity record — so read them from there.
|
|
2649
2651
|
const agentMsg = currentMessages.find((m) => m.tool === 'Agent' && m.agentId === agentId);
|
|
2650
2652
|
const usageStats = agentMsg?.agentUsageStats || null;
|
|
2651
|
-
const fmtTok =
|
|
2653
|
+
const fmtTok = fmtTokens;
|
|
2652
2654
|
|
|
2653
2655
|
const chips = [];
|
|
2654
2656
|
if (agent.agentId) chips.push(chip('id', escapeHtml(shortId), { cls: 'agent-chip-mono', title: agent.agentId }));
|
|
@@ -2796,7 +2798,7 @@ function renderSessions() {
|
|
|
2796
2798
|
((!s.sharedTaskList && (s.pending > 0 || s.inProgress > 0)) ||
|
|
2797
2799
|
s.hasActiveAgents ||
|
|
2798
2800
|
s.hasWaitingForUser ||
|
|
2799
|
-
s.
|
|
2801
|
+
s.hasRecentActivity);
|
|
2800
2802
|
if (isActive) activeSessionIds.add(s.id);
|
|
2801
2803
|
return isActive;
|
|
2802
2804
|
});
|
|
@@ -2940,15 +2942,15 @@ function renderSessions() {
|
|
|
2940
2942
|
<div class="session-progress">
|
|
2941
2943
|
<span class="session-indicators">
|
|
2942
2944
|
${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>` : ''}
|
|
2943
|
-
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
2944
2945
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" 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>` : ''}
|
|
2945
|
-
${session.hasPlan && !session.planSourceSessionId ? `<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>` : ''}
|
|
2946
|
-
${renderLoopBadge(session)}
|
|
2947
2946
|
${renderWorkflowBadge(session)}
|
|
2948
|
-
${
|
|
2949
|
-
${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>` : ''}
|
|
2947
|
+
${renderLoopBadge(session)}
|
|
2950
2948
|
${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>` : ''}
|
|
2951
|
-
${
|
|
2949
|
+
${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>` : ''}
|
|
2950
|
+
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
2951
|
+
${session.hasPlan && !session.planSourceSessionId ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan">${ICON_PLAN}</span>` : ''}
|
|
2952
|
+
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">${ICON_PLAN}</span>` : ''}
|
|
2953
|
+
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
2952
2954
|
${session.hasWaitingForUser ? `<span class="agent-badge agent-badge-waiting" title="Waiting for user">${ICON_AGENT_WAITING}</span>` : ''}
|
|
2953
2955
|
${session.hasRunningAgents && !session.hasWaitingForUser ? `<span class="agent-badge agent-badge-active" title="Agents running">${ICON_AGENT_ACTIVE}</span>` : ''}
|
|
2954
2956
|
${isLive || session.hasRunningAgents ? `<span class="pulse" title="${isLive ? 'Live' : 'Active agents'}"></span>` : ''}
|
|
@@ -4204,8 +4206,8 @@ function showStorageManager() {
|
|
|
4204
4206
|
document.querySelectorAll('.storage-tab').forEach((t) => {
|
|
4205
4207
|
t.classList.toggle('active', t.dataset.tab === 'sessions');
|
|
4206
4208
|
});
|
|
4207
|
-
_renderStorageTab();
|
|
4208
4209
|
document.getElementById('storage-modal').classList.add('visible');
|
|
4210
|
+
_renderStorageTab();
|
|
4209
4211
|
}
|
|
4210
4212
|
|
|
4211
4213
|
function closeStorageManager() {
|
|
@@ -4221,6 +4223,8 @@ function switchStorageTab(tab) {
|
|
|
4221
4223
|
}
|
|
4222
4224
|
|
|
4223
4225
|
function _renderStorageTab() {
|
|
4226
|
+
const modal = document.getElementById('storage-modal');
|
|
4227
|
+
if (!modal?.classList.contains('visible')) return;
|
|
4224
4228
|
const body = document.getElementById('storage-modal-body');
|
|
4225
4229
|
const tab = document.querySelector('.storage-tab.active')?.dataset.tab || 'sessions';
|
|
4226
4230
|
if (tab === 'sessions') body.innerHTML = _renderStorageSessions();
|
|
@@ -4513,19 +4517,12 @@ function _storagePreviewLinkedDoc(path) {
|
|
|
4513
4517
|
}
|
|
4514
4518
|
|
|
4515
4519
|
function _storageUnlinkDoc(sessionId, path) {
|
|
4516
|
-
|
|
4517
|
-
if (sessionId === _infoModalSessionId) refreshInfoModalLinkedDocs();
|
|
4518
|
-
renderSessions();
|
|
4519
|
-
_renderStorageTab();
|
|
4520
|
-
_updateStorageTotal();
|
|
4520
|
+
setSessionDocLink(sessionId, path, true);
|
|
4521
4521
|
}
|
|
4522
4522
|
|
|
4523
4523
|
function _storageClearLinkedDocs(sessionId) {
|
|
4524
4524
|
localStorage.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
|
|
4525
|
-
|
|
4526
|
-
renderSessions();
|
|
4527
|
-
_renderStorageTab();
|
|
4528
|
-
_updateStorageTotal();
|
|
4525
|
+
afterLinkedDocsChanged(sessionId);
|
|
4529
4526
|
}
|
|
4530
4527
|
|
|
4531
4528
|
function _findOrphanedKeys() {
|
|
@@ -4946,6 +4943,25 @@ function removeSessionPreviewPath(sessionId, filePath) {
|
|
|
4946
4943
|
else localStorage.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
|
|
4947
4944
|
}
|
|
4948
4945
|
|
|
4946
|
+
// Every surface that shows linked docs — info modal, session card badge, preview
|
|
4947
|
+
// toolbar toggle, storage manager — refreshes from one place, so a new mutator
|
|
4948
|
+
// can never forget one of them.
|
|
4949
|
+
// The one mutator: link/unlink plus the refresh, so no caller can do half of it.
|
|
4950
|
+
function setSessionDocLink(sessionId, filePath, unlink) {
|
|
4951
|
+
if (!sessionId || !filePath) return;
|
|
4952
|
+
if (unlink) removeSessionPreviewPath(sessionId, filePath);
|
|
4953
|
+
else addSessionPreviewPath(sessionId, filePath);
|
|
4954
|
+
afterLinkedDocsChanged(sessionId);
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
function afterLinkedDocsChanged(sessionId) {
|
|
4958
|
+
if (_infoModalSessionId === sessionId) refreshInfoModalLinkedDocs();
|
|
4959
|
+
renderSessions();
|
|
4960
|
+
updatePreviewLinkBtn();
|
|
4961
|
+
_renderStorageTab();
|
|
4962
|
+
_updateStorageTotal();
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4949
4965
|
function splitFrontmatter(text) {
|
|
4950
4966
|
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
4951
4967
|
if (!m) return { fm: null, body: text };
|
|
@@ -4967,27 +4983,50 @@ function renderFrontmatterBlock(fm) {
|
|
|
4967
4983
|
return `<details class="preview-fm" open><summary>frontmatter</summary><div class="fm-grid">${rows}</div></details>`;
|
|
4968
4984
|
}
|
|
4969
4985
|
|
|
4970
|
-
|
|
4986
|
+
// Previewed HTML is rendered as authored, not sanitized: the sandbox without
|
|
4987
|
+
// allow-same-origin puts it on an opaque origin, so it cannot touch this app's
|
|
4988
|
+
// storage, DOM or API. Sibling local assets don't load — srcdoc has no base URL.
|
|
4989
|
+
function renderHtmlPreview(bodyEl, content) {
|
|
4990
|
+
bodyEl.innerHTML = '';
|
|
4991
|
+
const frame = document.createElement('iframe');
|
|
4992
|
+
frame.className = 'preview-html-frame';
|
|
4993
|
+
frame.setAttribute('sandbox', 'allow-scripts allow-popups');
|
|
4994
|
+
frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
4995
|
+
frame.srcdoc = content;
|
|
4996
|
+
bodyEl.appendChild(frame);
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
function bindPreviewRelativeLinks(bodyEl) {
|
|
5000
|
+
if (bodyEl.dataset.relLinkBound) return;
|
|
5001
|
+
bodyEl.addEventListener('click', (e) => {
|
|
5002
|
+
const a = e.target.closest('a[href]');
|
|
5003
|
+
if (!a) return;
|
|
5004
|
+
const href = a.getAttribute('href');
|
|
5005
|
+
if (!href || href.startsWith('#')) return;
|
|
5006
|
+
const isAbsoluteUrl = /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//');
|
|
5007
|
+
const isAbsolutePath = href.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(href);
|
|
5008
|
+
if (isAbsoluteUrl) return;
|
|
5009
|
+
const cleanHref = href.replace(/#.*$/, '');
|
|
5010
|
+
if (!isPreviewablePath(cleanHref)) return;
|
|
5011
|
+
e.preventDefault();
|
|
5012
|
+
openPreviewByPath(cleanHref, isAbsolutePath ? undefined : currentPreviewPath);
|
|
5013
|
+
});
|
|
5014
|
+
bodyEl.dataset.relLinkBound = '1';
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
// `kind` comes from the server, which is the only place that decides what is previewable.
|
|
5018
|
+
function openPreviewModal(filePath, content, kind) {
|
|
4971
5019
|
currentPreviewPath = filePath;
|
|
4972
5020
|
document.getElementById('preview-modal-title').textContent = filePath.split(/[\\/]/).pop();
|
|
4973
|
-
const { fm, body } = /\.(md|markdown)$/i.test(filePath) ? splitFrontmatter(content) : { fm: null, body: content };
|
|
4974
5021
|
const bodyEl = document.getElementById('preview-modal-body');
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
const isAbsolutePath = href.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(href);
|
|
4984
|
-
if (isAbsoluteUrl) return;
|
|
4985
|
-
if (!/\.(md|markdown)(#.*)?$/i.test(href)) return;
|
|
4986
|
-
e.preventDefault();
|
|
4987
|
-
const cleanHref = href.replace(/#.*$/, '');
|
|
4988
|
-
openPreviewByPath(cleanHref, isAbsolutePath ? undefined : currentPreviewPath);
|
|
4989
|
-
});
|
|
4990
|
-
bodyEl.dataset.relLinkBound = '1';
|
|
5022
|
+
const isHtml = kind === 'html';
|
|
5023
|
+
document.querySelector('#preview-modal .modal').classList.toggle('preview-html', isHtml);
|
|
5024
|
+
bindPreviewRelativeLinks(bodyEl);
|
|
5025
|
+
if (isHtml) {
|
|
5026
|
+
renderHtmlPreview(bodyEl, content);
|
|
5027
|
+
} else {
|
|
5028
|
+
const { fm, body } = splitFrontmatter(content);
|
|
5029
|
+
bodyEl.innerHTML = (fm ? renderFrontmatterBlock(fm) : '') + renderMarkdown(body);
|
|
4991
5030
|
}
|
|
4992
5031
|
document.getElementById('preview-modal-meta').textContent = filePath;
|
|
4993
5032
|
document.getElementById('preview-modal').classList.add('visible');
|
|
@@ -5018,49 +5057,32 @@ function togglePreviewSessionLink() {
|
|
|
5018
5057
|
showToast('Select a session first');
|
|
5019
5058
|
return;
|
|
5020
5059
|
}
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
} else {
|
|
5025
|
-
addSessionPreviewPath(currentSessionId, currentPreviewPath);
|
|
5026
|
-
showToast('Linked to session');
|
|
5027
|
-
}
|
|
5028
|
-
updatePreviewLinkBtn();
|
|
5029
|
-
if (_infoModalSessionId === currentSessionId) {
|
|
5030
|
-
refreshInfoModalLinkedDocs();
|
|
5031
|
-
}
|
|
5032
|
-
renderSessions();
|
|
5060
|
+
const unlink = isPreviewLinkedToCurrentSession();
|
|
5061
|
+
setSessionDocLink(currentSessionId, currentPreviewPath, unlink);
|
|
5062
|
+
showToast(unlink ? 'Unlinked from session' : 'Linked to session');
|
|
5033
5063
|
}
|
|
5034
5064
|
|
|
5035
5065
|
function refreshInfoModalLinkedDocs() {
|
|
5036
5066
|
const bodyEl = document.getElementById('team-modal-body');
|
|
5037
5067
|
if (!bodyEl) return;
|
|
5038
5068
|
const existing = bodyEl.querySelector('.linked-docs-section');
|
|
5039
|
-
const
|
|
5040
|
-
|
|
5041
|
-
|
|
5069
|
+
const wrap = document.createElement('div');
|
|
5070
|
+
wrap.innerHTML = renderLinkedDocsHtml(_infoModalSessionId);
|
|
5071
|
+
const node = wrap.firstElementChild;
|
|
5072
|
+
if (existing) {
|
|
5073
|
+
existing.replaceWith(node);
|
|
5074
|
+
} else {
|
|
5042
5075
|
const planCard = bodyEl.querySelector('[data-plan-card]');
|
|
5043
|
-
const wrap = document.createElement('div');
|
|
5044
|
-
wrap.innerHTML = html;
|
|
5045
|
-
const node = wrap.firstElementChild;
|
|
5046
5076
|
if (planCard?.nextSibling) planCard.parentNode.insertBefore(node, planCard.nextSibling);
|
|
5047
5077
|
else bodyEl.appendChild(node);
|
|
5048
|
-
bindLinkedDocsHandlers(node, _infoModalSessionId);
|
|
5049
|
-
return;
|
|
5050
5078
|
}
|
|
5051
|
-
if (!html) {
|
|
5052
|
-
existing.remove();
|
|
5053
|
-
return;
|
|
5054
|
-
}
|
|
5055
|
-
const wrap = document.createElement('div');
|
|
5056
|
-
wrap.innerHTML = html;
|
|
5057
|
-
const node = wrap.firstElementChild;
|
|
5058
|
-
existing.replaceWith(node);
|
|
5059
5079
|
bindLinkedDocsHandlers(node, _infoModalSessionId);
|
|
5060
5080
|
}
|
|
5061
5081
|
|
|
5062
5082
|
function closePreviewModal() {
|
|
5063
5083
|
resetModalFullscreen('preview-modal');
|
|
5084
|
+
// Empty the body so an iframe preview is destroyed and its scripts/timers stop.
|
|
5085
|
+
document.getElementById('preview-modal-body').innerHTML = '';
|
|
5064
5086
|
currentPreviewPath = null;
|
|
5065
5087
|
}
|
|
5066
5088
|
|
|
@@ -5081,7 +5103,7 @@ async function openPreviewByPath(filePath, base) {
|
|
|
5081
5103
|
return;
|
|
5082
5104
|
}
|
|
5083
5105
|
const data = await r.json();
|
|
5084
|
-
openPreviewModal(data.path, data.content);
|
|
5106
|
+
openPreviewModal(data.path, data.content, data.kind);
|
|
5085
5107
|
} catch {
|
|
5086
5108
|
showToast('Failed to load preview');
|
|
5087
5109
|
}
|
|
@@ -5108,7 +5130,7 @@ function handleSessionOpenEvent(data) {
|
|
|
5108
5130
|
}
|
|
5109
5131
|
|
|
5110
5132
|
async function handlePreviewOpenEvent(data) {
|
|
5111
|
-
const { path: filePath,
|
|
5133
|
+
const { path: filePath, sessionId } = data;
|
|
5112
5134
|
if (sessionId && sessionId !== currentSessionId) {
|
|
5113
5135
|
if (sessions.find((s) => s.id === sessionId)) {
|
|
5114
5136
|
await fetchTasks(sessionId);
|
|
@@ -5116,7 +5138,17 @@ async function handlePreviewOpenEvent(data) {
|
|
|
5116
5138
|
showToast(`Preview received for unknown session ${sessionId.slice(0, 8)}`);
|
|
5117
5139
|
}
|
|
5118
5140
|
}
|
|
5119
|
-
|
|
5141
|
+
// The broadcast carries the path only — each tab fetches the document itself.
|
|
5142
|
+
openPreviewByPath(filePath);
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
// Linked docs live in localStorage, so the CLI can only reach them through a tab:
|
|
5146
|
+
// the server broadcasts the resolved path and every tab applies it idempotently.
|
|
5147
|
+
function handleDocumentLinkEvent(data) {
|
|
5148
|
+
const { path: filePath, sessionId, unlink } = data;
|
|
5149
|
+
if (!filePath || !sessionId) return;
|
|
5150
|
+
setSessionDocLink(sessionId, filePath, unlink);
|
|
5151
|
+
showToast(`${unlink ? 'Unlinked' : 'Linked'} ${filePath.split(/[\\/]/).pop()}`);
|
|
5120
5152
|
}
|
|
5121
5153
|
|
|
5122
5154
|
function getSessionBaseDir(sessionId) {
|
|
@@ -5124,42 +5156,123 @@ function getSessionBaseDir(sessionId) {
|
|
|
5124
5156
|
return s?.cwd || s?.project || '';
|
|
5125
5157
|
}
|
|
5126
5158
|
|
|
5159
|
+
// Linked paths are stored as bare strings, so previewability is re-derived from the
|
|
5160
|
+
// extension on every render instead of being remembered alongside the path.
|
|
5161
|
+
function isPreviewablePath(p) {
|
|
5162
|
+
return /\.(md|markdown|html?)$/i.test(p);
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5127
5165
|
function renderLinkedDocsHtml(sessionId) {
|
|
5128
5166
|
const paths = getSessionPreviewPaths(sessionId);
|
|
5129
|
-
if (!paths.length) return '';
|
|
5130
5167
|
const baseDir = getSessionBaseDir(sessionId);
|
|
5131
5168
|
const items = paths
|
|
5132
|
-
.map((p
|
|
5169
|
+
.map((p) => {
|
|
5133
5170
|
const name = p.split(/[\\/]/).pop();
|
|
5134
5171
|
const rel = baseDir ? toRelativeIfUnder(p, baseDir) : null;
|
|
5172
|
+
const previewable = isPreviewablePath(p);
|
|
5173
|
+
const title = previewable ? p : `${p} — opens in editor`;
|
|
5135
5174
|
const pathSpan = rel ? `<span class="linked-doc-path" title="${escapeHtml(p)}">${escapeHtml(rel)}</span>` : '';
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
${
|
|
5175
|
+
const attr = escapeHtml(p);
|
|
5176
|
+
return `<li class="linked-doc-item${previewable ? '' : ' is-editor'}">
|
|
5177
|
+
<a href="#" class="linked-doc-link" data-path="${attr}" title="${escapeHtml(title)}">${escapeHtml(name)}</a>
|
|
5178
|
+
${pathSpan}${previewable ? '' : '<span class="linked-doc-path">(editor)</span>'}
|
|
5179
|
+
<button type="button" class="linked-doc-remove" data-path="${attr}" title="Unlink" aria-label="Unlink ${escapeHtml(name)}">×</button>
|
|
5139
5180
|
</li>`;
|
|
5140
5181
|
})
|
|
5141
5182
|
.join('');
|
|
5183
|
+
// Rendered even when empty — the add button has to stay reachable.
|
|
5184
|
+
const body = paths.length
|
|
5185
|
+
? `<ul class="linked-doc-list">${items}</ul>`
|
|
5186
|
+
: '<div class="linked-docs-empty">No linked files yet</div>';
|
|
5142
5187
|
return `<div class="linked-docs-section" style="margin-bottom:16px;font-size:12px;">
|
|
5143
5188
|
<div style="font-size:11px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px;">
|
|
5144
5189
|
${linkSvg(12)}
|
|
5145
5190
|
<span>Linked documents</span>
|
|
5146
5191
|
<span style="background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;padding:0 6px;font-size:10px;color:var(--text-secondary);">${paths.length}</span>
|
|
5192
|
+
<button type="button" class="linked-docs-add-btn" title="Link a file" aria-label="Link a file">+</button>
|
|
5147
5193
|
</div>
|
|
5148
|
-
<
|
|
5194
|
+
<div class="linked-doc-editor-slot"></div>
|
|
5195
|
+
${body}
|
|
5149
5196
|
</div>`;
|
|
5150
5197
|
}
|
|
5151
5198
|
|
|
5199
|
+
// One delegated listener per section: the list is re-rendered on every change, so
|
|
5200
|
+
// per-row handlers would only be rebound each time anyway.
|
|
5152
5201
|
function bindLinkedDocsHandlers(container, sessionId) {
|
|
5153
5202
|
if (!container) return;
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5203
|
+
container.addEventListener('click', (e) => {
|
|
5204
|
+
const hit = e.target.closest('.linked-doc-link, .linked-doc-remove, .linked-docs-add-btn');
|
|
5205
|
+
if (!hit) return;
|
|
5206
|
+
e.preventDefault();
|
|
5207
|
+
if (hit.classList.contains('linked-docs-add-btn')) {
|
|
5208
|
+
startLinkedDocInput(container, sessionId);
|
|
5209
|
+
} else if (hit.classList.contains('linked-doc-remove')) {
|
|
5210
|
+
removeSessionPreviewPath(sessionId, hit.dataset.path);
|
|
5211
|
+
afterLinkedDocsChanged(sessionId);
|
|
5212
|
+
} else if (isPreviewablePath(hit.dataset.path)) {
|
|
5213
|
+
openPreviewByPath(hit.dataset.path, getSessionBaseDir(sessionId));
|
|
5214
|
+
} else {
|
|
5215
|
+
postAndToast('/api/open-in-editor', { file: hit.dataset.path }, 'in editor');
|
|
5216
|
+
}
|
|
5217
|
+
});
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
function startLinkedDocInput(container, sessionId) {
|
|
5221
|
+
const slot = container.querySelector('.linked-doc-editor-slot');
|
|
5222
|
+
if (!slot) return;
|
|
5223
|
+
slot.innerHTML = `<input class="linked-doc-input" type="text" spellcheck="false" placeholder="Absolute path, or relative to the session cwd">
|
|
5224
|
+
<div class="linked-doc-hint">${escapeHtml(getSessionBaseDir(sessionId) || 'no session cwd — absolute paths only')}</div>
|
|
5225
|
+
<div class="linked-doc-error"></div>
|
|
5226
|
+
<div class="edit-actions">
|
|
5227
|
+
<button type="button" class="edit-cancel">Cancel</button>
|
|
5228
|
+
<button type="button" class="edit-save">Link</button>
|
|
5229
|
+
</div>`;
|
|
5230
|
+
const input = slot.querySelector('.linked-doc-input');
|
|
5231
|
+
const save = () => linkFileByPath(sessionId, input.value, slot);
|
|
5232
|
+
const cancel = () => {
|
|
5233
|
+
slot.innerHTML = '';
|
|
5234
|
+
};
|
|
5235
|
+
slot.querySelector('.edit-save').addEventListener('click', save);
|
|
5236
|
+
slot.querySelector('.edit-cancel').addEventListener('click', cancel);
|
|
5237
|
+
input.addEventListener('keydown', (e) => {
|
|
5238
|
+
if (e.key === 'Enter') {
|
|
5160
5239
|
e.preventDefault();
|
|
5161
|
-
|
|
5162
|
-
})
|
|
5240
|
+
save();
|
|
5241
|
+
} else if (e.key === 'Escape') {
|
|
5242
|
+
e.preventDefault();
|
|
5243
|
+
cancel();
|
|
5244
|
+
}
|
|
5245
|
+
});
|
|
5246
|
+
input.focus();
|
|
5247
|
+
}
|
|
5248
|
+
|
|
5249
|
+
async function linkFileByPath(sessionId, raw, slot) {
|
|
5250
|
+
// Paths pasted from Explorer or a shell often arrive quoted.
|
|
5251
|
+
const value = (raw || '').trim().replace(/^["']|["']$/g, '');
|
|
5252
|
+
// Reported inline so the input stays open and the path can be corrected in place.
|
|
5253
|
+
const fail = (msg) => {
|
|
5254
|
+
const err = slot?.querySelector('.linked-doc-error');
|
|
5255
|
+
if (err) err.textContent = msg;
|
|
5256
|
+
else showToast(msg, 'error');
|
|
5257
|
+
};
|
|
5258
|
+
if (!value) {
|
|
5259
|
+
fail('Enter a file path');
|
|
5260
|
+
return;
|
|
5261
|
+
}
|
|
5262
|
+
try {
|
|
5263
|
+
const qs = new URLSearchParams({ path: value });
|
|
5264
|
+
const base = getSessionBaseDir(sessionId);
|
|
5265
|
+
if (base) qs.set('base', base);
|
|
5266
|
+
const r = await fetch(`/api/file/resolve?${qs}`);
|
|
5267
|
+
const data = await r.json().catch(() => ({}));
|
|
5268
|
+
if (!r.ok) {
|
|
5269
|
+
fail(data.error || 'File not found');
|
|
5270
|
+
return;
|
|
5271
|
+
}
|
|
5272
|
+
setSessionDocLink(sessionId, data.path, false);
|
|
5273
|
+
showToast('Linked to session', 'success');
|
|
5274
|
+
} catch {
|
|
5275
|
+
fail('Failed to resolve file');
|
|
5163
5276
|
}
|
|
5164
5277
|
}
|
|
5165
5278
|
//#endregion
|
|
@@ -5281,6 +5394,10 @@ function setupEventSource() {
|
|
|
5281
5394
|
handlePreviewOpenEvent(data);
|
|
5282
5395
|
}
|
|
5283
5396
|
|
|
5397
|
+
if (data.type === 'document:link') {
|
|
5398
|
+
handleDocumentLinkEvent(data);
|
|
5399
|
+
}
|
|
5400
|
+
|
|
5284
5401
|
if (data.type === 'session:open') {
|
|
5285
5402
|
handleSessionOpenEvent(data);
|
|
5286
5403
|
}
|
|
@@ -5707,8 +5824,12 @@ function filterBySessions(value) {
|
|
|
5707
5824
|
sessionFilter = value;
|
|
5708
5825
|
if (value !== 'active') activityFilter.clear();
|
|
5709
5826
|
updateUrl();
|
|
5827
|
+
// Instant feedback from cached data, then refetch — the cached list was fetched
|
|
5828
|
+
// with the previous filter's server params (e.g. filter=active), so "All Sessions"
|
|
5829
|
+
// needs a server round-trip to actually include inactive sessions.
|
|
5710
5830
|
renderSessions();
|
|
5711
5831
|
renderActivityChip();
|
|
5832
|
+
fetchSessions(false);
|
|
5712
5833
|
}
|
|
5713
5834
|
|
|
5714
5835
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
@@ -5779,7 +5900,7 @@ document.addEventListener('click', (e) => {
|
|
|
5779
5900
|
}
|
|
5780
5901
|
});
|
|
5781
5902
|
|
|
5782
|
-
// biome
|
|
5903
|
+
// Used in HTML; also called by the hub project shim, so no biome suppression is needed.
|
|
5783
5904
|
function filterByProject(project) {
|
|
5784
5905
|
filterProject = project || null;
|
|
5785
5906
|
updateUrl();
|
|
@@ -5827,10 +5948,16 @@ async function updateProjectDropdown() {
|
|
|
5827
5948
|
|
|
5828
5949
|
function renderProjectDropdown(dropdown, projects) {
|
|
5829
5950
|
const recentSelected = filterProject === '__recent__' ? ' selected' : '';
|
|
5951
|
+
// A hub-pushed project with no session history isn't in /api/projects, so synthesize its
|
|
5952
|
+
// option here rather than in the hub shim — this also survives the SSE re-render below.
|
|
5953
|
+
const list =
|
|
5954
|
+
filterProject && filterProject !== '__recent__' && !projects.some((p) => p.path === filterProject)
|
|
5955
|
+
? [...projects, { path: filterProject, modifiedAt: null }]
|
|
5956
|
+
: projects;
|
|
5830
5957
|
dropdown.innerHTML =
|
|
5831
5958
|
'<option value="">All Projects</option>' +
|
|
5832
5959
|
`<option value="__recent__"${recentSelected}>Recent (24h)</option>` +
|
|
5833
|
-
|
|
5960
|
+
list
|
|
5834
5961
|
.map((p) => {
|
|
5835
5962
|
const name = p.path.split(/[/\\]/).pop();
|
|
5836
5963
|
const selected = p.path === filterProject ? ' selected' : '';
|
|
@@ -5922,7 +6049,7 @@ function buildThemeMenu() {
|
|
|
5922
6049
|
const menu = document.getElementById('themeMenu');
|
|
5923
6050
|
menu.innerHTML = COLOR_THEMES.map(
|
|
5924
6051
|
([id, label]) =>
|
|
5925
|
-
`<button type="button" class="theme-menu-item" data-theme-id="${id}"
|
|
6052
|
+
`<button type="button" class="theme-menu-item theme-swatch-${id}" data-theme-id="${id}"
|
|
5926
6053
|
onclick="event.stopPropagation(); setColorTheme('${id}'); toggleThemeMenu()">
|
|
5927
6054
|
<span class="theme-swatch theme-swatch-${id}"><i class="sw-bg"></i><i class="sw-accent"></i><i class="sw-ink"></i></span>${label}
|
|
5928
6055
|
</button>`,
|
|
@@ -6312,6 +6439,8 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
6312
6439
|
|
|
6313
6440
|
const keyHandler = (e) => {
|
|
6314
6441
|
if (e.key === 'Escape') {
|
|
6442
|
+
// An inline editor inside the modal owns Escape first — it reverts itself.
|
|
6443
|
+
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return;
|
|
6315
6444
|
if (document.getElementById('plan-modal').classList.contains('visible')) return;
|
|
6316
6445
|
if (document.getElementById('tool-stats-modal').classList.contains('visible')) return;
|
|
6317
6446
|
e.preventDefault();
|
|
@@ -6502,6 +6631,7 @@ function renderWorkflowBadge(session) {
|
|
|
6502
6631
|
|
|
6503
6632
|
let _workflowSessionId = null;
|
|
6504
6633
|
let _workflowHeaderId = null;
|
|
6634
|
+
let _workflowList = [];
|
|
6505
6635
|
|
|
6506
6636
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6507
6637
|
function showWorkflowModal(sessionId) {
|
|
@@ -6513,7 +6643,18 @@ function showWorkflowModal(sessionId) {
|
|
|
6513
6643
|
fetch(`/api/sessions/${sessionId}/workflows`)
|
|
6514
6644
|
.then((r) => (r.ok ? r.json() : { workflows: [] }))
|
|
6515
6645
|
.catch(() => ({ workflows: [] }))
|
|
6516
|
-
.then((data) =>
|
|
6646
|
+
.then((data) => {
|
|
6647
|
+
_workflowList = data.workflows || [];
|
|
6648
|
+
if (!_workflowList.length) {
|
|
6649
|
+
setWorkflowHeader('Workflows', null);
|
|
6650
|
+
body.innerHTML =
|
|
6651
|
+
'<div style="padding:24px;text-align:center;color:var(--text-secondary);">No workflow scripts for this session.</div>';
|
|
6652
|
+
} else if (_workflowList.length === 1) {
|
|
6653
|
+
showWorkflowRun(_workflowList[0].id);
|
|
6654
|
+
} else {
|
|
6655
|
+
renderWorkflowPicker(_workflowList);
|
|
6656
|
+
}
|
|
6657
|
+
});
|
|
6517
6658
|
}
|
|
6518
6659
|
|
|
6519
6660
|
function setWorkflowHeader(name, wfId) {
|
|
@@ -6527,44 +6668,118 @@ function setWorkflowHeader(name, wfId) {
|
|
|
6527
6668
|
if (headerBtn) headerBtn.style.display = wfId ? '' : 'none';
|
|
6528
6669
|
}
|
|
6529
6670
|
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
if (!workflows.length) {
|
|
6533
|
-
setWorkflowHeader('Workflows', null);
|
|
6534
|
-
body.innerHTML =
|
|
6535
|
-
'<div style="padding:24px;text-align:center;color:var(--text-secondary);">No workflow scripts for this session.</div>';
|
|
6536
|
-
return;
|
|
6537
|
-
}
|
|
6538
|
-
// Single workflow: promote its name to the header and open-button next to the close (X).
|
|
6539
|
-
if (workflows.length === 1) {
|
|
6540
|
-
const w = workflows[0];
|
|
6541
|
-
setWorkflowHeader(w.name || w.id, w.id);
|
|
6542
|
-
const when = w.modifiedAt ? formatDate(w.modifiedAt) : '';
|
|
6543
|
-
body.innerHTML = `<div class="workflow-single">
|
|
6544
|
-
<div class="workflow-row-meta"><code>${escapeHtml(w.id)}</code>${when ? ` · ${escapeHtml(when)}` : ''}</div>
|
|
6545
|
-
<div class="workflow-row-code"></div>
|
|
6546
|
-
</div>`;
|
|
6547
|
-
loadWorkflowCode(w.id, body.querySelector('.workflow-row-code'));
|
|
6548
|
-
return;
|
|
6549
|
-
}
|
|
6550
|
-
// Multiple workflows: generic header, collapsible list with per-row open icons.
|
|
6671
|
+
// Multiple workflows: pick one to drill into (we show a single run at a time).
|
|
6672
|
+
function renderWorkflowPicker(workflows) {
|
|
6551
6673
|
setWorkflowHeader('Workflows', null);
|
|
6674
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6552
6675
|
body.innerHTML = workflows
|
|
6553
6676
|
.map((w) => {
|
|
6554
6677
|
const when = w.modifiedAt ? formatDate(w.modifiedAt) : '';
|
|
6555
|
-
return `<div class="workflow-row" onclick="
|
|
6678
|
+
return `<div class="workflow-row" onclick="showWorkflowRun('${escapeHtml(w.id)}')">
|
|
6556
6679
|
<div class="workflow-row-head">
|
|
6557
6680
|
<span class="workflow-row-name">${escapeHtml(w.name || w.id)}</span>
|
|
6558
6681
|
<span class="workflow-row-id"><code>${escapeHtml(w.id)}</code></span>
|
|
6559
6682
|
<button class="icon-btn workflow-open-icon" aria-label="Open in editor" title="Open in editor" onclick="event.stopPropagation(); openWorkflowInEditor('${escapeHtml(w.id)}')">${ICON_OPEN_EDITOR}</button>
|
|
6560
6683
|
</div>
|
|
6684
|
+
${w.description ? `<div class="workflow-row-desc">${escapeHtml(w.description)}</div>` : ''}
|
|
6561
6685
|
${when ? `<div class="workflow-row-meta">${escapeHtml(when)}</div>` : ''}
|
|
6562
|
-
<div class="workflow-row-code"></div>
|
|
6563
6686
|
</div>`;
|
|
6564
6687
|
})
|
|
6565
6688
|
.join('');
|
|
6566
6689
|
}
|
|
6567
6690
|
|
|
6691
|
+
function showWorkflowRun(wfId) {
|
|
6692
|
+
const name = _workflowList.find((w) => w.id === wfId)?.name || wfId;
|
|
6693
|
+
setWorkflowHeader(name, wfId);
|
|
6694
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6695
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Loading…</div>';
|
|
6696
|
+
fetch(`/api/sessions/${_workflowSessionId}/workflows/${encodeURIComponent(wfId)}/run`)
|
|
6697
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
6698
|
+
.catch(() => null)
|
|
6699
|
+
.then((run) => {
|
|
6700
|
+
if (!run) {
|
|
6701
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Failed to load run state.</div>';
|
|
6702
|
+
return;
|
|
6703
|
+
}
|
|
6704
|
+
renderWorkflowRun(run);
|
|
6705
|
+
});
|
|
6706
|
+
}
|
|
6707
|
+
|
|
6708
|
+
function fmtTokens(t) {
|
|
6709
|
+
return t >= 1000 ? `${(t / 1000).toFixed(1).replace(/\.0$/, '')}k` : `${t}`;
|
|
6710
|
+
}
|
|
6711
|
+
|
|
6712
|
+
function shortModelName(model) {
|
|
6713
|
+
return model ? model.replace(/^claude-/, '').replace(/-\d{8}$/, '') : '';
|
|
6714
|
+
}
|
|
6715
|
+
|
|
6716
|
+
function renderWorkflowRun(run) {
|
|
6717
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6718
|
+
const allDone = run.startedCount > 0 && run.doneCount >= run.startedCount;
|
|
6719
|
+
const wall =
|
|
6720
|
+
run.startedAt && run.stoppedAt ? formatDuration(new Date(run.stoppedAt) - new Date(run.startedAt)) : null;
|
|
6721
|
+
|
|
6722
|
+
const back =
|
|
6723
|
+
_workflowList.length > 1
|
|
6724
|
+
? `<div class="wf-back" onclick="renderWorkflowPicker(_workflowList)">‹ All workflows</div>`
|
|
6725
|
+
: '';
|
|
6726
|
+
|
|
6727
|
+
const stats = [`${run.doneCount}/${run.startedCount} agents`, wall ? escapeHtml(wall) : null]
|
|
6728
|
+
.filter(Boolean)
|
|
6729
|
+
.join(' · ');
|
|
6730
|
+
|
|
6731
|
+
const desc = run.description ? `<div class="wf-run-desc">${escapeHtml(run.description)}</div>` : '';
|
|
6732
|
+
|
|
6733
|
+
const phases = (run.phases || [])
|
|
6734
|
+
.map(
|
|
6735
|
+
(p) => `<div class="wf-phase">
|
|
6736
|
+
<span class="wf-phase-mark${allDone ? ' done' : ''}">${allDone ? '✓' : ''}</span>
|
|
6737
|
+
<span class="wf-phase-title">${escapeHtml(p.title)}</span>
|
|
6738
|
+
${p.detail ? `<span class="wf-phase-detail">${escapeHtml(p.detail)}</span>` : ''}
|
|
6739
|
+
</div>`,
|
|
6740
|
+
)
|
|
6741
|
+
.join('');
|
|
6742
|
+
const phasesBlock = phases ? `<div class="wf-section-title">Phases</div><div class="wf-phases">${phases}</div>` : '';
|
|
6743
|
+
|
|
6744
|
+
const agents = (run.agents || [])
|
|
6745
|
+
.map((a) => {
|
|
6746
|
+
const dur = a.durationMs != null ? formatDuration(a.durationMs) : '';
|
|
6747
|
+
const running = a.status !== 'done';
|
|
6748
|
+
return `<div class="wf-agent" onclick="showAgentModal('${escapeHtml(a.agentId)}')" title="${escapeHtml(a.agentId)}">
|
|
6749
|
+
<span class="wf-agent-type">${escapeHtml(a.type || 'agent')}</span>
|
|
6750
|
+
<span class="wf-agent-model">${escapeHtml(shortModelName(a.model))}</span>
|
|
6751
|
+
<span class="wf-agent-tok">${a.outputTokens ? `${fmtTokens(a.outputTokens)} tok` : ''}</span>
|
|
6752
|
+
<span class="wf-agent-dur">${escapeHtml(dur)}</span>
|
|
6753
|
+
<span class="wf-agent-status ${running ? 'running' : 'done'}">${running ? '●' : '✓'}</span>
|
|
6754
|
+
</div>`;
|
|
6755
|
+
})
|
|
6756
|
+
.join('');
|
|
6757
|
+
const agentsBlock = run.agents?.length
|
|
6758
|
+
? `<div class="wf-section-title">Agents · ${run.agents.length}</div><div class="wf-agents">${agents}</div>`
|
|
6759
|
+
: '<div class="wf-section-title">Agents</div><div class="wf-empty">No agent runs recorded yet.</div>';
|
|
6760
|
+
|
|
6761
|
+
body.innerHTML = `<div class="workflow-run">
|
|
6762
|
+
${back}
|
|
6763
|
+
${desc}
|
|
6764
|
+
<div class="wf-run-stats">${stats} · <code>${escapeHtml(run.id)}</code></div>
|
|
6765
|
+
${phasesBlock}
|
|
6766
|
+
${agentsBlock}
|
|
6767
|
+
<details class="wf-source">
|
|
6768
|
+
<summary>Source</summary>
|
|
6769
|
+
<div class="workflow-row-code"></div>
|
|
6770
|
+
</details>
|
|
6771
|
+
</div>`;
|
|
6772
|
+
|
|
6773
|
+
const details = body.querySelector('.wf-source');
|
|
6774
|
+
details.addEventListener(
|
|
6775
|
+
'toggle',
|
|
6776
|
+
() => {
|
|
6777
|
+
if (details.open) loadWorkflowCode(run.id, details.querySelector('.workflow-row-code'));
|
|
6778
|
+
},
|
|
6779
|
+
{ once: true },
|
|
6780
|
+
);
|
|
6781
|
+
}
|
|
6782
|
+
|
|
6568
6783
|
function loadWorkflowCode(wfId, codeEl) {
|
|
6569
6784
|
if (!codeEl) return;
|
|
6570
6785
|
codeEl.innerHTML = '<div style="padding:8px 0;color:var(--text-secondary);">Loading…</div>';
|
|
@@ -6586,19 +6801,6 @@ function loadWorkflowCode(wfId, codeEl) {
|
|
|
6586
6801
|
});
|
|
6587
6802
|
}
|
|
6588
6803
|
|
|
6589
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6590
|
-
function viewWorkflowScript(wfId, rowEl) {
|
|
6591
|
-
const codeEl = rowEl.querySelector('.workflow-row-code');
|
|
6592
|
-
if (!codeEl) return;
|
|
6593
|
-
if (rowEl.classList.contains('expanded')) {
|
|
6594
|
-
rowEl.classList.remove('expanded');
|
|
6595
|
-
codeEl.innerHTML = '';
|
|
6596
|
-
return;
|
|
6597
|
-
}
|
|
6598
|
-
rowEl.classList.add('expanded');
|
|
6599
|
-
loadWorkflowCode(wfId, codeEl);
|
|
6600
|
-
}
|
|
6601
|
-
|
|
6602
6804
|
function openWorkflowInEditor(wfId) {
|
|
6603
6805
|
postAndToast(`/api/sessions/${_workflowSessionId}/workflows/${encodeURIComponent(wfId)}/open`, {}, 'in editor');
|
|
6604
6806
|
}
|
|
@@ -6611,6 +6813,7 @@ function openWorkflowHeaderScript() {
|
|
|
6611
6813
|
function closeWorkflowModal() {
|
|
6612
6814
|
document.getElementById('workflow-modal').classList.remove('visible');
|
|
6613
6815
|
_workflowSessionId = null;
|
|
6816
|
+
_workflowList = [];
|
|
6614
6817
|
setWorkflowHeader('Workflows', null);
|
|
6615
6818
|
}
|
|
6616
6819
|
|
|
@@ -7111,6 +7314,9 @@ window.addEventListener('popstate', () => {
|
|
|
7111
7314
|
ownerFilter = s.owner || '';
|
|
7112
7315
|
searchQuery = s.search || '';
|
|
7113
7316
|
loadPreferences();
|
|
7317
|
+
// fetchSessions derives query params from the globals set above — refetch so
|
|
7318
|
+
// back/forward across a filter change doesn't render a stale server-filtered list.
|
|
7319
|
+
fetchSessions(false);
|
|
7114
7320
|
if (s.projectView) {
|
|
7115
7321
|
try {
|
|
7116
7322
|
fetchProjectView(atob(s.projectView));
|
|
@@ -7126,13 +7332,19 @@ window.addEventListener('popstate', () => {
|
|
|
7126
7332
|
// #region HUB_INTEGRATION
|
|
7127
7333
|
document.addEventListener('keydown', (e) => {
|
|
7128
7334
|
if (!window.__HUB__?.enabled) return;
|
|
7129
|
-
|
|
7335
|
+
const fwd = (key) => {
|
|
7130
7336
|
e.preventDefault();
|
|
7131
|
-
window.parent?.postMessage({ type: 'hub:keydown', key: e.
|
|
7337
|
+
window.parent?.postMessage({ type: 'hub:keydown', key, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey }, '*');
|
|
7338
|
+
};
|
|
7339
|
+
if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
7340
|
+
fwd(e.key);
|
|
7341
|
+
}
|
|
7342
|
+
// Its own branch: the Alt+digit case below requires !ctrlKey.
|
|
7343
|
+
if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && e.key.toLowerCase() === 'p') {
|
|
7344
|
+
fwd(e.key);
|
|
7132
7345
|
}
|
|
7133
7346
|
if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
|
|
7134
|
-
e.
|
|
7135
|
-
window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
|
|
7347
|
+
fwd(e.key);
|
|
7136
7348
|
}
|
|
7137
7349
|
});
|
|
7138
7350
|
|
|
@@ -7160,10 +7372,12 @@ window.hubNavigate = function hubNavigate(app, url) {
|
|
|
7160
7372
|
window.parent?.postMessage({ type: 'hub:navigate', app, url }, '*');
|
|
7161
7373
|
};
|
|
7162
7374
|
|
|
7375
|
+
// Hoisted out of initHubTheme so initHubProject can share it.
|
|
7376
|
+
const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origin : null);
|
|
7377
|
+
|
|
7163
7378
|
(function initHubTheme() {
|
|
7164
7379
|
const getTheme = () => (document.body.classList.contains('light') ? 'light' : 'dark');
|
|
7165
7380
|
const getColorTheme = () => document.body.dataset.colorTheme || 'ember';
|
|
7166
|
-
const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origin : null);
|
|
7167
7381
|
// lastTheme/lastColorTheme are updated synchronously when applying a hub
|
|
7168
7382
|
// message, so the (async) observer sees no diff and doesn't echo it back.
|
|
7169
7383
|
let lastTheme = getTheme();
|
|
@@ -7193,4 +7407,15 @@ window.hubNavigate = function hubNavigate(app, url) {
|
|
|
7193
7407
|
attributeFilter: ['class', 'data-color-theme'],
|
|
7194
7408
|
});
|
|
7195
7409
|
})();
|
|
7410
|
+
|
|
7411
|
+
(function initHubProject() {
|
|
7412
|
+
window.addEventListener('message', (e) => {
|
|
7413
|
+
if (e.source !== window.parent || e.origin !== hubOrigin()) return;
|
|
7414
|
+
if (e.data?.type !== 'hub:project') return;
|
|
7415
|
+
const dirPath = e.data.project;
|
|
7416
|
+
if (typeof dirPath !== 'string' || !dirPath || dirPath === filterProject) return;
|
|
7417
|
+
filterByProject(dirPath);
|
|
7418
|
+
updateProjectDropdown();
|
|
7419
|
+
});
|
|
7420
|
+
})();
|
|
7196
7421
|
// #endregion HUB_INTEGRATION
|