claude-code-kanban 3.5.3 → 3.7.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 +21 -0
- package/package.json +2 -1
- package/public/app.js +133 -18
- package/public/style.css +76 -8
- package/server.js +16 -5
package/lib/parsers.js
CHANGED
|
@@ -714,6 +714,27 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
714
714
|
|
|
715
715
|
function readCompactSummaries(jsonlPath) {
|
|
716
716
|
const results = [];
|
|
717
|
+
// Inline format: newer Claude Code stores the summary directly in the parent
|
|
718
|
+
// session JSONL as a user message with isCompactSummary: true (no subagent file).
|
|
719
|
+
try {
|
|
720
|
+
const content = readFileSync(jsonlPath, 'utf8');
|
|
721
|
+
for (const line of content.split('\n')) {
|
|
722
|
+
if (!line.trim() || !line.includes('isCompactSummary')) continue;
|
|
723
|
+
try {
|
|
724
|
+
const obj = JSON.parse(line);
|
|
725
|
+
if (!obj.isCompactSummary) continue;
|
|
726
|
+
const c = obj.message?.content;
|
|
727
|
+
let text = typeof c === 'string'
|
|
728
|
+
? c
|
|
729
|
+
: Array.isArray(c) ? c.filter(b => b?.type === 'text' && b.text).map(b => b.text).join('\n') : '';
|
|
730
|
+
if (!text) continue;
|
|
731
|
+
// Strip the "This session is being continued..." preamble if present.
|
|
732
|
+
text = text.replace(/^This session is being continued[^\n]*\n+(The summary below[^\n]*\n+)?/i, '').trim();
|
|
733
|
+
if (text) results.push({ timestamp: obj.timestamp, summary: text });
|
|
734
|
+
} catch (_) {}
|
|
735
|
+
}
|
|
736
|
+
} catch (_) {}
|
|
737
|
+
// Legacy format: summary lives in subagents/agent-acompact-*.jsonl.
|
|
717
738
|
try {
|
|
718
739
|
const subagentsDir = path.join(path.dirname(jsonlPath), path.basename(jsonlPath, '.jsonl'), 'subagents');
|
|
719
740
|
const files = readdirSync(subagentsDir).filter(f => f.startsWith('agent-acompact-') && f.endsWith('.jsonl'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"public/**/*"
|
|
52
52
|
],
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "2.4.14",
|
|
54
55
|
"ajv": "^8.18.0",
|
|
55
56
|
"ajv-formats": "^3.0.1",
|
|
56
57
|
"husky": "^9.1.7"
|
package/public/app.js
CHANGED
|
@@ -59,7 +59,9 @@ function getUrlState() {
|
|
|
59
59
|
project: params.get('project'),
|
|
60
60
|
owner: params.get('owner'),
|
|
61
61
|
search: params.get('search'),
|
|
62
|
-
messages: params.
|
|
62
|
+
messages: params.has('messages')
|
|
63
|
+
? params.get('messages') === '1'
|
|
64
|
+
: localStorage.getItem('message-panel-open') === 'true',
|
|
63
65
|
projectView: params.get('projectView'),
|
|
64
66
|
};
|
|
65
67
|
}
|
|
@@ -653,6 +655,7 @@ async function refreshProjectAgents() {
|
|
|
653
655
|
function toggleMessagePanel() {
|
|
654
656
|
const panel = document.getElementById('message-panel');
|
|
655
657
|
messagePanelOpen = !messagePanelOpen;
|
|
658
|
+
localStorage.setItem('message-panel-open', messagePanelOpen);
|
|
656
659
|
panel.classList.toggle('visible', messagePanelOpen);
|
|
657
660
|
document.getElementById('message-toggle')?.classList.toggle('active', messagePanelOpen);
|
|
658
661
|
if (messagePanelOpen && currentSessionId) {
|
|
@@ -667,10 +670,13 @@ async function openSessionWithBookmarks(sessionId) {
|
|
|
667
670
|
if (!messagePanelOpen) {
|
|
668
671
|
const panel = document.getElementById('message-panel');
|
|
669
672
|
messagePanelOpen = true;
|
|
673
|
+
localStorage.setItem('message-panel-open', 'true');
|
|
670
674
|
panel.classList.add('visible');
|
|
671
675
|
document.getElementById('message-toggle')?.classList.add('active');
|
|
672
676
|
}
|
|
673
677
|
await fetchTasks(sessionId);
|
|
678
|
+
if (currentMessages.length) renderMessages(currentMessages);
|
|
679
|
+
fetchMessages(sessionId);
|
|
674
680
|
}
|
|
675
681
|
|
|
676
682
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
@@ -1503,10 +1509,25 @@ function closeMsgDetailModal() {
|
|
|
1503
1509
|
msgDetailFollowLatest = false;
|
|
1504
1510
|
}
|
|
1505
1511
|
|
|
1512
|
+
function _setModalWidth(modal, slot, on, maxWidth, width) {
|
|
1513
|
+
const mwKey = `prev${slot}MaxWidth`;
|
|
1514
|
+
const wKey = `prev${slot}Width`;
|
|
1515
|
+
if (on) {
|
|
1516
|
+
modal.dataset[mwKey] = modal.style.maxWidth || '';
|
|
1517
|
+
modal.dataset[wKey] = modal.style.width || '';
|
|
1518
|
+
modal.style.maxWidth = maxWidth;
|
|
1519
|
+
modal.style.width = width;
|
|
1520
|
+
} else {
|
|
1521
|
+
modal.style.maxWidth = modal.dataset[mwKey] || '';
|
|
1522
|
+
modal.style.width = modal.dataset[wKey] || '';
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1506
1526
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1507
1527
|
function toggleModalFullscreen(modalId) {
|
|
1508
1528
|
const modal = document.querySelector(`#${modalId} .modal`);
|
|
1509
1529
|
const isFs = modal.classList.toggle('fullscreen');
|
|
1530
|
+
_setModalWidth(modal, 'Fs', isFs, '', '');
|
|
1510
1531
|
updateFullscreenBtnIcon(`${modalId}-fullscreen-btn`, isFs);
|
|
1511
1532
|
}
|
|
1512
1533
|
|
|
@@ -1623,9 +1644,9 @@ function getToolDetail(tool, params, detail) {
|
|
|
1623
1644
|
const parts = [];
|
|
1624
1645
|
if (params.offset) parts.push(`L${params.offset}`);
|
|
1625
1646
|
if (params.limit) parts.push(`+${params.limit}`);
|
|
1626
|
-
if (parts.length) extra = ` <span style="color:var(--text-muted)
|
|
1647
|
+
if (parts.length) extra = ` <span style="color:var(--text-muted)">${parts.join(' ')}</span>`;
|
|
1627
1648
|
}
|
|
1628
|
-
return ` <span style="color:var(--text-
|
|
1649
|
+
return ` <span style="color:var(--text-secondary)">${escapeHtml(detail)}</span>${extra}`;
|
|
1629
1650
|
}
|
|
1630
1651
|
function renderTaskResult(toolResult) {
|
|
1631
1652
|
if (!toolResult) return '';
|
|
@@ -1737,17 +1758,30 @@ function highlightBash(escaped) {
|
|
|
1737
1758
|
}
|
|
1738
1759
|
|
|
1739
1760
|
let _expandIdCounter = 0;
|
|
1761
|
+
function _toggleExpand(btn) {
|
|
1762
|
+
const f = document.getElementById(btn.dataset.expandId);
|
|
1763
|
+
const t = btn.parentElement.nextElementSibling;
|
|
1764
|
+
const expand = f.style.display === 'none';
|
|
1765
|
+
f.style.display = expand ? 'block' : 'none';
|
|
1766
|
+
t.style.display = expand ? 'none' : 'block';
|
|
1767
|
+
btn.textContent = expand ? 'Show less' : 'Show more';
|
|
1768
|
+
const panel = btn.closest('.message-panel');
|
|
1769
|
+
if (panel) panel.classList.toggle('msg-expanded-wide', expand);
|
|
1770
|
+
const modal = btn.closest('.modal');
|
|
1771
|
+
if (modal) _setModalWidth(modal, 'Expand', expand, '60vw', '60vw');
|
|
1772
|
+
}
|
|
1740
1773
|
function makeExpandToggle(_truncatedHtml, fullHtml, opts = {}) {
|
|
1741
1774
|
const id = `expand-${++_expandIdCounter}`;
|
|
1742
1775
|
const fontSize = opts.fontSize || '0.8rem';
|
|
1743
1776
|
const maxHeight = opts.maxHeight || '';
|
|
1744
|
-
const btn = `<button
|
|
1777
|
+
const btn = `<button data-expand-id="${id}" onclick="_toggleExpand(this)" style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:${fontSize};text-decoration:underline;margin-left:6px">Show more</button>`;
|
|
1745
1778
|
const mhStyle = maxHeight ? `max-height:${maxHeight};` : '';
|
|
1746
1779
|
const full = `<pre id="${id}" class="msg-detail-pre" style="${mhStyle}overflow:auto;display:none">${fullHtml}</pre>`;
|
|
1747
1780
|
return { btn, full };
|
|
1748
1781
|
}
|
|
1749
1782
|
|
|
1750
1783
|
function autoSizeModal(modal, body) {
|
|
1784
|
+
if (modal.classList.contains('fullscreen')) return;
|
|
1751
1785
|
modal.style.maxWidth = '';
|
|
1752
1786
|
modal.classList.remove('has-mermaid');
|
|
1753
1787
|
const hasMermaid = body.querySelector('pre.mermaid') !== null;
|
|
@@ -2277,7 +2311,10 @@ function renderSessions() {
|
|
|
2277
2311
|
const hasInProgress = session.inProgress > 0;
|
|
2278
2312
|
const isLive =
|
|
2279
2313
|
hasInProgress || (session.modifiedAt && Date.now() - new Date(session.modifiedAt).getTime() <= LIVE_INDICATOR_MS);
|
|
2280
|
-
const
|
|
2314
|
+
const rawName = session.name || session.id;
|
|
2315
|
+
const sessionName = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawName)
|
|
2316
|
+
? rawName.slice(0, 8)
|
|
2317
|
+
: rawName;
|
|
2281
2318
|
const useGrouped = sessionFilter === 'active' && session.project;
|
|
2282
2319
|
const primaryName = useGrouped ? sessionName : session.project ? session.project.split('/').pop() : sessionName;
|
|
2283
2320
|
const secondaryName = useGrouped ? null : session.project ? sessionName : null;
|
|
@@ -2299,8 +2336,9 @@ function renderSessions() {
|
|
|
2299
2336
|
const showCtx = !!session.contextStatus;
|
|
2300
2337
|
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
2301
2338
|
const bookmarksCount = loadPins(session.id).length;
|
|
2339
|
+
const tempClass = session.hasRecentLog || session.inProgress || session.hasWaitingForUser ? 'warm' : 'stale';
|
|
2302
2340
|
return `
|
|
2303
|
-
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${
|
|
2341
|
+
<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}">
|
|
2304
2342
|
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${escapeHtml(session.id)}')" title="${pinTitle} session">${SESSION_PIN_SVG}</span>
|
|
2305
2343
|
<div class="session-name">${escapeHtml(primaryName)}</div>
|
|
2306
2344
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
@@ -3957,6 +3995,19 @@ function matchKey(e, ...keys) {
|
|
|
3957
3995
|
return keys.some((k) => e.key === k || e.code === k);
|
|
3958
3996
|
}
|
|
3959
3997
|
|
|
3998
|
+
const MODAL_ESC_PRIORITY = ['preview-modal', 'msg-detail-modal', 'plan-modal'];
|
|
3999
|
+
const MODAL_CLOSERS = {
|
|
4000
|
+
'preview-modal': () => closePreviewModal(),
|
|
4001
|
+
'msg-detail-modal': () => {
|
|
4002
|
+
closeMsgDetailModal();
|
|
4003
|
+
msgDetailFollowLatest = false;
|
|
4004
|
+
},
|
|
4005
|
+
'plan-modal': () => closePlanModal(),
|
|
4006
|
+
'team-modal': () => closeTeamModal(),
|
|
4007
|
+
'agent-modal': () => closeAgentModal(),
|
|
4008
|
+
'help-modal': () => closeHelpModal(),
|
|
4009
|
+
};
|
|
4010
|
+
|
|
3960
4011
|
document.addEventListener('keydown', (e) => {
|
|
3961
4012
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
|
|
3962
4013
|
return;
|
|
@@ -3969,9 +4020,13 @@ document.addEventListener('keydown', (e) => {
|
|
|
3969
4020
|
closeScratchpad();
|
|
3970
4021
|
return;
|
|
3971
4022
|
}
|
|
3972
|
-
//
|
|
3973
|
-
document.querySelectorAll('.modal-overlay.visible')
|
|
3974
|
-
|
|
4023
|
+
// Close only the topmost so a child Esc doesn't also dismiss its parent.
|
|
4024
|
+
const visible = [...document.querySelectorAll('.modal-overlay.visible')];
|
|
4025
|
+
const topId = MODAL_ESC_PRIORITY.find((id) => visible.some((m) => m.id === id)) || visible[visible.length - 1].id;
|
|
4026
|
+
const close = MODAL_CLOSERS[topId];
|
|
4027
|
+
if (close) close();
|
|
4028
|
+
else document.getElementById(topId).classList.remove('visible');
|
|
4029
|
+
e.stopImmediatePropagation();
|
|
3975
4030
|
} else if (
|
|
3976
4031
|
e.code === 'KeyM' &&
|
|
3977
4032
|
e.shiftKey &&
|
|
@@ -4241,8 +4296,24 @@ function openPreviewModal(filePath, content) {
|
|
|
4241
4296
|
currentPreviewPath = filePath;
|
|
4242
4297
|
document.getElementById('preview-modal-title').textContent = filePath.split(/[\\/]/).pop();
|
|
4243
4298
|
const { fm, body } = /\.(md|markdown)$/i.test(filePath) ? splitFrontmatter(content) : { fm: null, body: content };
|
|
4244
|
-
document.getElementById('preview-modal-body')
|
|
4245
|
-
|
|
4299
|
+
const bodyEl = document.getElementById('preview-modal-body');
|
|
4300
|
+
bodyEl.innerHTML = (fm ? renderFrontmatterBlock(fm) : '') + renderMarkdown(body);
|
|
4301
|
+
if (!bodyEl.dataset.relLinkBound) {
|
|
4302
|
+
bodyEl.addEventListener('click', (e) => {
|
|
4303
|
+
const a = e.target.closest('a[href]');
|
|
4304
|
+
if (!a) return;
|
|
4305
|
+
const href = a.getAttribute('href');
|
|
4306
|
+
if (!href || href.startsWith('#')) return;
|
|
4307
|
+
const isAbsoluteUrl = /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//');
|
|
4308
|
+
const isAbsolutePath = href.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(href);
|
|
4309
|
+
if (isAbsoluteUrl) return;
|
|
4310
|
+
if (!/\.(md|markdown)(#.*)?$/i.test(href)) return;
|
|
4311
|
+
e.preventDefault();
|
|
4312
|
+
const cleanHref = href.replace(/#.*$/, '');
|
|
4313
|
+
openPreviewByPath(cleanHref, isAbsolutePath ? undefined : currentPreviewPath);
|
|
4314
|
+
});
|
|
4315
|
+
bodyEl.dataset.relLinkBound = '1';
|
|
4316
|
+
}
|
|
4246
4317
|
document.getElementById('preview-modal-meta').textContent = filePath;
|
|
4247
4318
|
document.getElementById('preview-modal').classList.add('visible');
|
|
4248
4319
|
updatePreviewLinkBtn();
|
|
@@ -4313,7 +4384,6 @@ function refreshInfoModalLinkedDocs() {
|
|
|
4313
4384
|
bindLinkedDocsHandlers(node, _infoModalSessionId);
|
|
4314
4385
|
}
|
|
4315
4386
|
|
|
4316
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
4317
4387
|
function closePreviewModal() {
|
|
4318
4388
|
resetModalFullscreen('preview-modal');
|
|
4319
4389
|
currentPreviewPath = null;
|
|
@@ -4325,10 +4395,12 @@ function openPreviewInEditor() {
|
|
|
4325
4395
|
postAndToast('/api/open-in-editor', { file: currentPreviewPath }, 'in editor');
|
|
4326
4396
|
}
|
|
4327
4397
|
|
|
4328
|
-
async function openPreviewByPath(filePath) {
|
|
4398
|
+
async function openPreviewByPath(filePath, base) {
|
|
4329
4399
|
if (!filePath) return;
|
|
4330
4400
|
try {
|
|
4331
|
-
const
|
|
4401
|
+
const qs = new URLSearchParams({ path: filePath });
|
|
4402
|
+
if (base) qs.set('base', base);
|
|
4403
|
+
const r = await fetch(`/api/preview?${qs}`);
|
|
4332
4404
|
if (!r.ok) {
|
|
4333
4405
|
showToast('Preview file unavailable');
|
|
4334
4406
|
return;
|
|
@@ -4372,22 +4444,33 @@ async function handlePreviewOpenEvent(data) {
|
|
|
4372
4444
|
openPreviewModal(filePath, content);
|
|
4373
4445
|
}
|
|
4374
4446
|
|
|
4447
|
+
function getSessionBaseDir(sessionId) {
|
|
4448
|
+
const s = sessions.find((x) => x.id === sessionId);
|
|
4449
|
+
return s?.cwd || s?.project || '';
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4375
4452
|
function renderLinkedDocsHtml(sessionId) {
|
|
4376
4453
|
const paths = getSessionPreviewPaths(sessionId);
|
|
4377
4454
|
if (!paths.length) return '';
|
|
4455
|
+
const baseDir = getSessionBaseDir(sessionId);
|
|
4378
4456
|
const items = paths
|
|
4379
4457
|
.map((p, i) => {
|
|
4380
4458
|
const name = p.split(/[\\/]/).pop();
|
|
4381
|
-
|
|
4459
|
+
const rel = baseDir ? toRelativeIfUnder(p, baseDir) : null;
|
|
4460
|
+
const pathSpan = rel ? `<span class="linked-doc-path" title="${escapeHtml(p)}">${escapeHtml(rel)}</span>` : '';
|
|
4461
|
+
return `<li class="linked-doc-item">
|
|
4462
|
+
<a href="#" class="linked-doc-link" data-idx="${i}" title="${escapeHtml(p)}">${escapeHtml(name)}</a>
|
|
4463
|
+
${pathSpan}
|
|
4464
|
+
</li>`;
|
|
4382
4465
|
})
|
|
4383
|
-
.join('
|
|
4466
|
+
.join('');
|
|
4384
4467
|
return `<div class="linked-docs-section" style="margin-bottom:16px;font-size:12px;">
|
|
4385
4468
|
<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;">
|
|
4386
4469
|
${linkSvg(12)}
|
|
4387
4470
|
<span>Linked documents</span>
|
|
4388
4471
|
<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>
|
|
4389
4472
|
</div>
|
|
4390
|
-
<
|
|
4473
|
+
<ul class="linked-doc-list">${items}</ul>
|
|
4391
4474
|
</div>`;
|
|
4392
4475
|
}
|
|
4393
4476
|
|
|
@@ -4396,10 +4479,11 @@ function bindLinkedDocsHandlers(container, sessionId) {
|
|
|
4396
4479
|
const links = container.querySelectorAll('.linked-doc-link');
|
|
4397
4480
|
if (!links.length) return;
|
|
4398
4481
|
const paths = getSessionPreviewPaths(sessionId);
|
|
4482
|
+
const base = getSessionBaseDir(sessionId);
|
|
4399
4483
|
for (const link of links) {
|
|
4400
4484
|
link.addEventListener('click', (e) => {
|
|
4401
4485
|
e.preventDefault();
|
|
4402
|
-
openPreviewByPath(paths[+link.dataset.idx]);
|
|
4486
|
+
openPreviewByPath(paths[+link.dataset.idx], base);
|
|
4403
4487
|
});
|
|
4404
4488
|
}
|
|
4405
4489
|
}
|
|
@@ -4748,6 +4832,18 @@ function escapeHtml(text) {
|
|
|
4748
4832
|
return div.innerHTML;
|
|
4749
4833
|
}
|
|
4750
4834
|
|
|
4835
|
+
function toRelativeIfUnder(filePath, baseDir) {
|
|
4836
|
+
if (!filePath || !baseDir) return null;
|
|
4837
|
+
const fp = filePath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
4838
|
+
const bd = baseDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
4839
|
+
const isWin = /^[a-zA-Z]:\//.test(fp) || /^[a-zA-Z]:\//.test(bd);
|
|
4840
|
+
const a = isWin ? fp.toLowerCase() : fp;
|
|
4841
|
+
const b = isWin ? bd.toLowerCase() : bd;
|
|
4842
|
+
if (a === b) return '.';
|
|
4843
|
+
if (!a.startsWith(`${b}/`)) return null;
|
|
4844
|
+
return fp.slice(bd.length + 1);
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4751
4847
|
function renderMarkdown(text) {
|
|
4752
4848
|
if (typeof DOMPurify !== 'undefined' && typeof marked !== 'undefined') {
|
|
4753
4849
|
return DOMPurify.sanitize(marked.parse(text));
|
|
@@ -5867,6 +5963,25 @@ document.addEventListener('keydown', (e) => {
|
|
|
5867
5963
|
}
|
|
5868
5964
|
});
|
|
5869
5965
|
|
|
5966
|
+
document.addEventListener('click', (e) => {
|
|
5967
|
+
if (!window.__HUB__?.enabled) return;
|
|
5968
|
+
const a = e.target.closest?.('a[href]');
|
|
5969
|
+
if (!a) return;
|
|
5970
|
+
const href = a.getAttribute('href');
|
|
5971
|
+
if (!href) return;
|
|
5972
|
+
let url;
|
|
5973
|
+
try {
|
|
5974
|
+
url = new URL(href, window.location.href);
|
|
5975
|
+
} catch (_) {
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
if (url.origin === window.location.origin) return;
|
|
5979
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
|
|
5980
|
+
e.preventDefault();
|
|
5981
|
+
e.stopPropagation();
|
|
5982
|
+
window.parent?.postMessage({ type: 'hub:openExternal', url: url.href }, '*');
|
|
5983
|
+
});
|
|
5984
|
+
|
|
5870
5985
|
window.hubNavigate = function hubNavigate(app, url) {
|
|
5871
5986
|
if (!window.__HUB__?.enabled) return;
|
|
5872
5987
|
window.parent?.postMessage({ type: 'hub:navigate', app, url }, '*');
|
package/public/style.css
CHANGED
|
@@ -549,14 +549,13 @@ body::before {
|
|
|
549
549
|
|
|
550
550
|
.session-branch {
|
|
551
551
|
font-size: 10px;
|
|
552
|
-
color: var(--text-
|
|
552
|
+
color: var(--text-secondary);
|
|
553
553
|
margin-top: 2px;
|
|
554
554
|
display: block;
|
|
555
555
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
556
556
|
white-space: nowrap;
|
|
557
557
|
overflow: hidden;
|
|
558
558
|
text-overflow: ellipsis;
|
|
559
|
-
opacity: 0.7;
|
|
560
559
|
}
|
|
561
560
|
|
|
562
561
|
.session-plan {
|
|
@@ -1825,6 +1824,9 @@ body::before {
|
|
|
1825
1824
|
.message-panel.visible {
|
|
1826
1825
|
display: flex;
|
|
1827
1826
|
}
|
|
1827
|
+
.message-panel.msg-expanded-wide {
|
|
1828
|
+
width: 60vw;
|
|
1829
|
+
}
|
|
1828
1830
|
.message-panel-header {
|
|
1829
1831
|
padding: 16px 20px;
|
|
1830
1832
|
display: flex;
|
|
@@ -1877,7 +1879,7 @@ body::before {
|
|
|
1877
1879
|
width: 16px;
|
|
1878
1880
|
height: 16px;
|
|
1879
1881
|
margin-top: 2px;
|
|
1880
|
-
opacity: 0.
|
|
1882
|
+
opacity: 0.9;
|
|
1881
1883
|
}
|
|
1882
1884
|
.msg-body {
|
|
1883
1885
|
flex: 1;
|
|
@@ -2575,6 +2577,40 @@ body::before {
|
|
|
2575
2577
|
color: var(--text-primary);
|
|
2576
2578
|
}
|
|
2577
2579
|
|
|
2580
|
+
.linked-doc-list {
|
|
2581
|
+
list-style: none;
|
|
2582
|
+
padding: 0;
|
|
2583
|
+
margin: 0;
|
|
2584
|
+
display: flex;
|
|
2585
|
+
flex-direction: column;
|
|
2586
|
+
gap: 4px;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
.linked-doc-item {
|
|
2590
|
+
display: flex;
|
|
2591
|
+
align-items: baseline;
|
|
2592
|
+
gap: 8px;
|
|
2593
|
+
min-width: 0;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
.linked-doc-link {
|
|
2597
|
+
color: var(--accent-text);
|
|
2598
|
+
text-decoration: underline;
|
|
2599
|
+
text-decoration-style: dotted;
|
|
2600
|
+
text-underline-offset: 3px;
|
|
2601
|
+
flex-shrink: 0;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
.linked-doc-path {
|
|
2605
|
+
color: var(--text-muted);
|
|
2606
|
+
font-size: 11px;
|
|
2607
|
+
opacity: 0.7;
|
|
2608
|
+
overflow: hidden;
|
|
2609
|
+
text-overflow: ellipsis;
|
|
2610
|
+
white-space: nowrap;
|
|
2611
|
+
min-width: 0;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2578
2614
|
/* #endregion */
|
|
2579
2615
|
|
|
2580
2616
|
/* #region PERMISSION_PENDING */
|
|
@@ -2754,8 +2790,8 @@ body.light .msg-assistant .msg-text {
|
|
|
2754
2790
|
}
|
|
2755
2791
|
|
|
2756
2792
|
.modal.fullscreen {
|
|
2757
|
-
width:
|
|
2758
|
-
max-width:
|
|
2793
|
+
width: 80vw;
|
|
2794
|
+
max-width: 80vw;
|
|
2759
2795
|
height: 92vh;
|
|
2760
2796
|
max-height: 92vh;
|
|
2761
2797
|
}
|
|
@@ -2769,8 +2805,8 @@ body.light .msg-assistant .msg-text {
|
|
|
2769
2805
|
}
|
|
2770
2806
|
|
|
2771
2807
|
.modal.plan-modal.fullscreen {
|
|
2772
|
-
width:
|
|
2773
|
-
max-width:
|
|
2808
|
+
width: 80vw;
|
|
2809
|
+
max-width: 80vw;
|
|
2774
2810
|
height: 92vh;
|
|
2775
2811
|
max-height: 92vh;
|
|
2776
2812
|
}
|
|
@@ -3678,7 +3714,39 @@ pre.mermaid svg {
|
|
|
3678
3714
|
.session-item.kb-selected::before {
|
|
3679
3715
|
width: 0;
|
|
3680
3716
|
}
|
|
3717
|
+
.session-item.stale .session-time,
|
|
3718
|
+
.session-item.warm .session-time {
|
|
3719
|
+
display: inline-flex;
|
|
3720
|
+
align-items: center;
|
|
3721
|
+
gap: 6px;
|
|
3722
|
+
}
|
|
3723
|
+
.session-item.stale .session-time::before,
|
|
3724
|
+
.session-item.warm .session-time::before {
|
|
3725
|
+
content: '';
|
|
3726
|
+
width: 6px;
|
|
3727
|
+
height: 6px;
|
|
3728
|
+
border-radius: 50%;
|
|
3729
|
+
flex-shrink: 0;
|
|
3730
|
+
}
|
|
3731
|
+
.session-item.warm .session-time {
|
|
3732
|
+
color: var(--success);
|
|
3733
|
+
}
|
|
3734
|
+
.session-item.warm .session-time::before {
|
|
3735
|
+
background: var(--success);
|
|
3736
|
+
}
|
|
3681
3737
|
.session-item.stale {
|
|
3682
|
-
opacity: 0.
|
|
3738
|
+
opacity: 0.85;
|
|
3739
|
+
}
|
|
3740
|
+
.session-item.stale:hover,
|
|
3741
|
+
.session-item.stale.active,
|
|
3742
|
+
.session-item.stale.kb-selected {
|
|
3743
|
+
opacity: 1;
|
|
3744
|
+
}
|
|
3745
|
+
.session-item.stale .session-time {
|
|
3746
|
+
color: var(--text-muted);
|
|
3747
|
+
}
|
|
3748
|
+
.session-item.stale .session-time::before {
|
|
3749
|
+
background: transparent;
|
|
3750
|
+
border: 1px solid var(--text-muted);
|
|
3683
3751
|
}
|
|
3684
3752
|
/* #endregion */
|
package/server.js
CHANGED
|
@@ -1475,15 +1475,26 @@ async function readMarkdownFile(absPath) {
|
|
|
1475
1475
|
}
|
|
1476
1476
|
}
|
|
1477
1477
|
|
|
1478
|
-
function resolvePreviewPath(filePath) {
|
|
1478
|
+
function resolvePreviewPath(filePath, base) {
|
|
1479
1479
|
if (!filePath || typeof filePath !== 'string') return null;
|
|
1480
|
-
|
|
1480
|
+
if (path.isAbsolute(filePath)) return filePath;
|
|
1481
|
+
if (base && typeof base === 'string' && path.isAbsolute(base)) {
|
|
1482
|
+
let baseDir = base;
|
|
1483
|
+
try {
|
|
1484
|
+
if (statSync(base).isFile()) baseDir = path.dirname(base);
|
|
1485
|
+
} catch {
|
|
1486
|
+
// base doesn't exist — fall back to dirname if it looks like a file
|
|
1487
|
+
if (path.extname(base)) baseDir = path.dirname(base);
|
|
1488
|
+
}
|
|
1489
|
+
return path.resolve(baseDir, filePath);
|
|
1490
|
+
}
|
|
1491
|
+
return path.resolve(filePath);
|
|
1481
1492
|
}
|
|
1482
1493
|
|
|
1483
1494
|
app.post('/api/preview', async (req, res) => {
|
|
1484
1495
|
try {
|
|
1485
|
-
const { path: filePath, sessionId } = req.body || {};
|
|
1486
|
-
const abs = resolvePreviewPath(filePath);
|
|
1496
|
+
const { path: filePath, sessionId, base } = req.body || {};
|
|
1497
|
+
const abs = resolvePreviewPath(filePath, base);
|
|
1487
1498
|
if (!abs) return res.status(400).json({ error: 'path is required' });
|
|
1488
1499
|
const content = await readMarkdownFile(abs);
|
|
1489
1500
|
broadcast({ type: 'preview:open', path: abs, content, sessionId: sessionId || null });
|
|
@@ -1548,7 +1559,7 @@ app.post('/api/session/pin', async (req, res) => {
|
|
|
1548
1559
|
|
|
1549
1560
|
app.get('/api/preview', async (req, res) => {
|
|
1550
1561
|
try {
|
|
1551
|
-
const abs = resolvePreviewPath(req.query.path);
|
|
1562
|
+
const abs = resolvePreviewPath(req.query.path, req.query.base);
|
|
1552
1563
|
if (!abs) return res.status(400).json({ error: 'path is required' });
|
|
1553
1564
|
const content = await readMarkdownFile(abs);
|
|
1554
1565
|
res.json({ path: abs, content });
|