claude-code-kanban 3.7.0 → 3.8.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 +32 -2
- package/package.json +1 -1
- package/public/app.js +180 -68
- package/public/index.html +12 -0
- package/public/style.css +15 -1
- package/server.js +14 -2
package/lib/parsers.js
CHANGED
|
@@ -552,14 +552,15 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
552
552
|
readSize *= 4;
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
-
// Attach tool results to their corresponding tool_use messages
|
|
555
|
+
// Attach tool results to their corresponding tool_use messages.
|
|
556
|
+
// For perf, we never ship the full text in the messages payload — when
|
|
557
|
+
// truncated, the client lazy-fetches via /api/sessions/:id/tool-result/:toolUseId.
|
|
556
558
|
for (const msg of messages) {
|
|
557
559
|
if (msg.type === 'tool_use' && msg.toolUseId && toolResults.has(msg.toolUseId)) {
|
|
558
560
|
const full = toolResults.get(msg.toolUseId);
|
|
559
561
|
const truncated = full.length > TOOL_RESULT_MAX;
|
|
560
562
|
msg.toolResult = truncated ? full.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)' : full;
|
|
561
563
|
msg.toolResultTruncated = truncated;
|
|
562
|
-
if (truncated) msg.toolResultFull = full;
|
|
563
564
|
}
|
|
564
565
|
}
|
|
565
566
|
|
|
@@ -578,6 +579,34 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
578
579
|
}
|
|
579
580
|
}
|
|
580
581
|
|
|
582
|
+
function readFullToolResult(jsonlPath, toolUseId) {
|
|
583
|
+
if (!toolUseId || !jsonlPath || !existsSync(jsonlPath)) return null;
|
|
584
|
+
try {
|
|
585
|
+
const content = readFileSync(jsonlPath, 'utf8');
|
|
586
|
+
const lines = content.split('\n');
|
|
587
|
+
for (const line of lines) {
|
|
588
|
+
if (!line || line.indexOf(toolUseId) === -1) continue;
|
|
589
|
+
try {
|
|
590
|
+
const obj = JSON.parse(line);
|
|
591
|
+
if (obj?.message?.content && Array.isArray(obj.message.content)) {
|
|
592
|
+
for (const block of obj.message.content) {
|
|
593
|
+
if (block.type === 'tool_result' && block.tool_use_id === toolUseId) {
|
|
594
|
+
if (typeof block.content === 'string') return block.content;
|
|
595
|
+
if (Array.isArray(block.content)) {
|
|
596
|
+
return block.content
|
|
597
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
598
|
+
.map((c) => c.text)
|
|
599
|
+
.join('\n');
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} catch (_) {}
|
|
605
|
+
}
|
|
606
|
+
} catch (_) {}
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
|
|
581
610
|
function readMessagesPage(jsonlPath, limit = 10, beforeTimestamp = null) {
|
|
582
611
|
const fetchLimit = limit + 1;
|
|
583
612
|
const applyFilter = beforeTimestamp
|
|
@@ -850,6 +879,7 @@ module.exports = {
|
|
|
850
879
|
readSessionInfoFromJsonl,
|
|
851
880
|
readRecentMessages,
|
|
852
881
|
readMessagesPage,
|
|
882
|
+
readFullToolResult,
|
|
853
883
|
buildAgentProgressMap,
|
|
854
884
|
buildSessionDigest,
|
|
855
885
|
readCompactSummaries,
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -496,6 +496,7 @@ async function fetchTasks(sessionId) {
|
|
|
496
496
|
if (revealedStorageSessionId && sessionId !== revealedStorageSessionId) {
|
|
497
497
|
revealedStorageSessionId = null;
|
|
498
498
|
}
|
|
499
|
+
if (currentSessionId && currentSessionId !== sessionId) deferredPinPlacement.delete(currentSessionId);
|
|
499
500
|
currentSessionId = sessionId;
|
|
500
501
|
currentPins = loadPins(sessionId);
|
|
501
502
|
ownerFilter = '';
|
|
@@ -1206,6 +1207,9 @@ function togglePin(msgIndex) {
|
|
|
1206
1207
|
text: m.text || null,
|
|
1207
1208
|
fullText: m.fullText || null,
|
|
1208
1209
|
tool: m.tool || null,
|
|
1210
|
+
toolUseId: m.toolUseId || null,
|
|
1211
|
+
toolResult: m.toolResult || null,
|
|
1212
|
+
toolResultTruncated: m.toolResultTruncated || false,
|
|
1209
1213
|
detail: m.detail || null,
|
|
1210
1214
|
fullDetail: m.fullDetail || null,
|
|
1211
1215
|
description: m.description || null,
|
|
@@ -1294,6 +1298,8 @@ function togglePinnedCollapse() {
|
|
|
1294
1298
|
//#region PINNING
|
|
1295
1299
|
let pinnedSessionIds = new Set();
|
|
1296
1300
|
let stickySessionIds = new Set();
|
|
1301
|
+
// Pinning the currently-selected session keeps it in place until deselected (less UI movement).
|
|
1302
|
+
const deferredPinPlacement = new Set();
|
|
1297
1303
|
|
|
1298
1304
|
function loadPinnedSessions() {
|
|
1299
1305
|
try {
|
|
@@ -1316,35 +1322,45 @@ function savePinnedSessions() {
|
|
|
1316
1322
|
localStorage.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
1319
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1320
1325
|
function toggleSessionPin(sessionId) {
|
|
1321
1326
|
if (pinnedSessionIds.has(sessionId)) {
|
|
1322
1327
|
pinnedSessionIds.delete(sessionId);
|
|
1323
1328
|
stickySessionIds.delete(sessionId);
|
|
1329
|
+
deferredPinPlacement.delete(sessionId);
|
|
1324
1330
|
} else {
|
|
1325
1331
|
pinnedSessionIds.add(sessionId);
|
|
1332
|
+
if (sessionId === currentSessionId) deferredPinPlacement.add(sessionId);
|
|
1326
1333
|
}
|
|
1327
1334
|
savePinnedSessions();
|
|
1328
1335
|
renderSessions();
|
|
1329
1336
|
}
|
|
1330
1337
|
|
|
1331
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1332
1338
|
function toggleSessionSticky(sessionId) {
|
|
1333
1339
|
if (stickySessionIds.has(sessionId)) {
|
|
1334
1340
|
stickySessionIds.delete(sessionId);
|
|
1335
1341
|
pinnedSessionIds.delete(sessionId);
|
|
1342
|
+
deferredPinPlacement.delete(sessionId);
|
|
1336
1343
|
} else {
|
|
1337
1344
|
pinnedSessionIds.add(sessionId);
|
|
1338
1345
|
stickySessionIds.add(sessionId);
|
|
1346
|
+
if (sessionId === currentSessionId) deferredPinPlacement.add(sessionId);
|
|
1339
1347
|
}
|
|
1340
1348
|
savePinnedSessions();
|
|
1341
1349
|
renderSessions();
|
|
1342
1350
|
}
|
|
1343
1351
|
|
|
1352
|
+
function isPlacedPinned(id) {
|
|
1353
|
+
return pinnedSessionIds.has(id) && !deferredPinPlacement.has(id);
|
|
1354
|
+
}
|
|
1355
|
+
function isPlacedSticky(id) {
|
|
1356
|
+
return stickySessionIds.has(id) && !deferredPinPlacement.has(id);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1344
1359
|
function handleSessionPinEvent({ id, state }) {
|
|
1345
1360
|
if (!id) return;
|
|
1346
1361
|
pinnedSessionIds.delete(id);
|
|
1347
1362
|
stickySessionIds.delete(id);
|
|
1363
|
+
deferredPinPlacement.delete(id);
|
|
1348
1364
|
if (state === 'pinned') pinnedSessionIds.add(id);
|
|
1349
1365
|
if (state === 'sticky') {
|
|
1350
1366
|
pinnedSessionIds.add(id);
|
|
@@ -1372,11 +1388,16 @@ function _renderPinToDetail(pin) {
|
|
|
1372
1388
|
document.getElementById('msg-detail-title').textContent = pin.tool || 'Tool';
|
|
1373
1389
|
const fullText = pin.fullDetail || pin.detail || '';
|
|
1374
1390
|
const pinParamsHtml = renderToolParamsHtml(pin.params);
|
|
1375
|
-
const pinResultHtml = renderToolResultHtml(
|
|
1391
|
+
const pinResultHtml = renderToolResultHtml(
|
|
1392
|
+
pin.toolResult,
|
|
1393
|
+
pin.toolResultTruncated,
|
|
1394
|
+
pin.toolResultFull,
|
|
1395
|
+
pin.toolUseId,
|
|
1396
|
+
);
|
|
1376
1397
|
const pinDetailEscaped = escapeHtml(fullText);
|
|
1377
1398
|
const pinDetailRendered = pin.tool === 'Bash' ? highlightBash(pinDetailEscaped) : pinDetailEscaped;
|
|
1378
1399
|
body.innerHTML =
|
|
1379
|
-
(fullText ? `<pre class="
|
|
1400
|
+
(fullText ? `<pre class="${TINTED_PRE_CLASS}">${pinDetailRendered}</pre>` : '<em>No details</em>') +
|
|
1380
1401
|
pinParamsHtml +
|
|
1381
1402
|
pinResultHtml;
|
|
1382
1403
|
} else if (pin.type === 'agent') {
|
|
@@ -1437,7 +1458,7 @@ function showMsgDetail(idx) {
|
|
|
1437
1458
|
const taskResultHtml = TASK_TOOLS.has(m.tool) ? renderTaskResult(m.toolResult) : '';
|
|
1438
1459
|
const toolResultHtml = hideResult
|
|
1439
1460
|
? ''
|
|
1440
|
-
: renderToolResultHtml(m.toolResult, m.toolResultTruncated, m.toolResultFull);
|
|
1461
|
+
: renderToolResultHtml(m.toolResult, m.toolResultTruncated, m.toolResultFull, m.toolUseId);
|
|
1441
1462
|
const hasAgentTabs = m.tool === 'Agent' && m.agentId && (m.agentLastMessage || m.agentPrompt);
|
|
1442
1463
|
let mainHtml;
|
|
1443
1464
|
if (sendProto) {
|
|
@@ -1451,7 +1472,7 @@ function showMsgDetail(idx) {
|
|
|
1451
1472
|
} else if (fullText) {
|
|
1452
1473
|
const detailEscaped = escapeHtml(fullText);
|
|
1453
1474
|
const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
|
|
1454
|
-
mainHtml = `${descHtml}<pre class="
|
|
1475
|
+
mainHtml = `${descHtml}<pre class="${TINTED_PRE_CLASS}">${detailRendered}</pre>`;
|
|
1455
1476
|
} else {
|
|
1456
1477
|
mainHtml = TASK_TOOLS.has(m.tool) ? '' : '<em>No details</em>';
|
|
1457
1478
|
}
|
|
@@ -1698,11 +1719,11 @@ function renderToolParamsHtml(params) {
|
|
|
1698
1719
|
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">`;
|
|
1699
1720
|
if (params.old_string) {
|
|
1700
1721
|
html += `<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">old_string</div>
|
|
1701
|
-
<pre class="
|
|
1722
|
+
<pre class="${TINTED_PRE_CLASS}" style="max-height:200px;overflow:auto;border-left:3px solid #e55;padding-left:8px">${escapeHtml(params.old_string)}</pre>`;
|
|
1702
1723
|
}
|
|
1703
1724
|
if (params.new_string) {
|
|
1704
1725
|
html += `<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px;margin-top:6px">new_string</div>
|
|
1705
|
-
<pre class="
|
|
1726
|
+
<pre class="${TINTED_PRE_CLASS}" style="max-height:200px;overflow:auto;border-left:3px solid #5b5;padding-left:8px">${escapeHtml(params.new_string)}</pre>`;
|
|
1706
1727
|
}
|
|
1707
1728
|
html += `</div>`;
|
|
1708
1729
|
}
|
|
@@ -1717,13 +1738,14 @@ function renderToolParamsHtml(params) {
|
|
|
1717
1738
|
const toggle = makeExpandToggle(escapeHtml(truncContent), escapeHtml(params.content), {
|
|
1718
1739
|
fontSize: '0.75rem',
|
|
1719
1740
|
maxHeight: '500px',
|
|
1741
|
+
tinted: true,
|
|
1720
1742
|
});
|
|
1721
1743
|
writeMoreBtn = ` ${toggle.btn}`;
|
|
1722
1744
|
fullBlock = toggle.full;
|
|
1723
1745
|
}
|
|
1724
1746
|
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">
|
|
1725
1747
|
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">content${writeMoreBtn}</div>
|
|
1726
|
-
<pre class="
|
|
1748
|
+
<pre class="${TINTED_PRE_CLASS}" style="max-height:300px;overflow:auto">${escapeHtml(truncContent)}</pre>
|
|
1727
1749
|
${fullBlock}
|
|
1728
1750
|
</div>`;
|
|
1729
1751
|
}
|
|
@@ -1757,26 +1779,31 @@ function highlightBash(escaped) {
|
|
|
1757
1779
|
.replace(/((?:^|\s)(?:&&|\|\||[|;])(?:\s|$))/g, '<span style="color:#d4d4d4;font-weight:bold">$1</span>');
|
|
1758
1780
|
}
|
|
1759
1781
|
|
|
1782
|
+
const TINTED_PRE_CLASS = 'msg-detail-pre msg-detail-pre-tinted';
|
|
1760
1783
|
let _expandIdCounter = 0;
|
|
1761
|
-
function
|
|
1762
|
-
const
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
t.style.display = expand ? 'none' : 'block';
|
|
1784
|
+
function _applyExpandToggle(btn, fullEl) {
|
|
1785
|
+
const truncEl = btn.parentElement.nextElementSibling;
|
|
1786
|
+
const expand = fullEl.style.display === 'none';
|
|
1787
|
+
fullEl.style.display = expand ? 'block' : 'none';
|
|
1788
|
+
if (truncEl) truncEl.style.display = expand ? 'none' : 'block';
|
|
1767
1789
|
btn.textContent = expand ? 'Show less' : 'Show more';
|
|
1768
1790
|
const panel = btn.closest('.message-panel');
|
|
1769
1791
|
if (panel) panel.classList.toggle('msg-expanded-wide', expand);
|
|
1770
1792
|
const modal = btn.closest('.modal');
|
|
1771
1793
|
if (modal) _setModalWidth(modal, 'Expand', expand, '60vw', '60vw');
|
|
1772
1794
|
}
|
|
1795
|
+
function _toggleExpand(btn) {
|
|
1796
|
+
const f = document.getElementById(btn.dataset.expandId);
|
|
1797
|
+
if (f) _applyExpandToggle(btn, f);
|
|
1798
|
+
}
|
|
1773
1799
|
function makeExpandToggle(_truncatedHtml, fullHtml, opts = {}) {
|
|
1774
1800
|
const id = `expand-${++_expandIdCounter}`;
|
|
1775
1801
|
const fontSize = opts.fontSize || '0.8rem';
|
|
1776
1802
|
const maxHeight = opts.maxHeight || '';
|
|
1777
|
-
const
|
|
1803
|
+
const cls = opts.tinted ? TINTED_PRE_CLASS : 'msg-detail-pre';
|
|
1804
|
+
const btn = `<button data-expand-id="${id}" onclick="_toggleExpand(this)" class="expand-toggle-btn" style="font-size:${fontSize}">Show more</button>`;
|
|
1778
1805
|
const mhStyle = maxHeight ? `max-height:${maxHeight};` : '';
|
|
1779
|
-
const full = `<pre id="${id}" class="
|
|
1806
|
+
const full = `<pre id="${id}" class="${cls}" style="${mhStyle}overflow:auto;display:none">${fullHtml}</pre>`;
|
|
1780
1807
|
return { btn, full };
|
|
1781
1808
|
}
|
|
1782
1809
|
|
|
@@ -1796,7 +1823,7 @@ function autoSizeModal(modal, body) {
|
|
|
1796
1823
|
if (desired > current) modal.style.maxWidth = `${desired}px`;
|
|
1797
1824
|
}
|
|
1798
1825
|
|
|
1799
|
-
function renderToolResultHtml(toolResult, isTruncated, fullResult) {
|
|
1826
|
+
function renderToolResultHtml(toolResult, isTruncated, fullResult, toolUseId) {
|
|
1800
1827
|
if (!toolResult) return '';
|
|
1801
1828
|
const stripped = stripLineNumbers(toolResult);
|
|
1802
1829
|
const escaped = escapeHtml(stripped);
|
|
@@ -1806,6 +1833,10 @@ function renderToolResultHtml(toolResult, isTruncated, fullResult) {
|
|
|
1806
1833
|
const toggle = makeExpandToggle(escaped, escapeHtml(stripLineNumbers(fullResult)));
|
|
1807
1834
|
truncLabel = toggle.btn;
|
|
1808
1835
|
fullBlock = toggle.full;
|
|
1836
|
+
} else if (isTruncated && toolUseId) {
|
|
1837
|
+
const id = `expand-${++_expandIdCounter}`;
|
|
1838
|
+
truncLabel = `<button data-expand-id="${id}" data-tool-use-id="${escapeHtml(toolUseId)}" onclick="_toggleToolResultExpand(this)" class="expand-toggle-btn" style="font-size:0.8rem">Show more</button>`;
|
|
1839
|
+
fullBlock = `<pre id="${id}" class="msg-detail-pre" style="overflow:auto;display:none"></pre>`;
|
|
1809
1840
|
} else if (isTruncated) {
|
|
1810
1841
|
truncLabel = '<span style="color:var(--text-muted);font-size:0.8rem;margin-left:6px">(truncated)</span>';
|
|
1811
1842
|
}
|
|
@@ -1816,12 +1847,42 @@ function renderToolResultHtml(toolResult, isTruncated, fullResult) {
|
|
|
1816
1847
|
</div>`;
|
|
1817
1848
|
}
|
|
1818
1849
|
|
|
1850
|
+
async function _toggleToolResultExpand(btn) {
|
|
1851
|
+
const f = document.getElementById(btn.dataset.expandId);
|
|
1852
|
+
if (!f) return;
|
|
1853
|
+
if (!btn.dataset.loaded) {
|
|
1854
|
+
if (!currentSessionId || !btn.dataset.toolUseId) return;
|
|
1855
|
+
btn.disabled = true;
|
|
1856
|
+
btn.textContent = 'Loading…';
|
|
1857
|
+
try {
|
|
1858
|
+
const r = await fetch(
|
|
1859
|
+
`/api/sessions/${encodeURIComponent(currentSessionId)}/tool-result/${encodeURIComponent(btn.dataset.toolUseId)}`,
|
|
1860
|
+
);
|
|
1861
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
1862
|
+
const { content } = await r.json();
|
|
1863
|
+
f.textContent = stripLineNumbers(content);
|
|
1864
|
+
btn.dataset.loaded = '1';
|
|
1865
|
+
} catch (_e) {
|
|
1866
|
+
btn.textContent = 'Show more';
|
|
1867
|
+
btn.disabled = false;
|
|
1868
|
+
showToast('Failed to load full output');
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
btn.disabled = false;
|
|
1872
|
+
}
|
|
1873
|
+
_applyExpandToggle(btn, f);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1819
1876
|
function buildToolContent(m) {
|
|
1820
1877
|
let content = m.fullDetail || m.detail || '';
|
|
1821
1878
|
if (m.toolResult) content += `\n\n--- Output ---\n\n${m.toolResultFull || m.toolResult}`;
|
|
1822
1879
|
return content;
|
|
1823
1880
|
}
|
|
1824
1881
|
|
|
1882
|
+
function getMessageDisplayContent(m) {
|
|
1883
|
+
return m.type === 'tool_use' ? buildToolContent(m) : m.compactSummary || stripAnsi(m.fullText || m.text);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1825
1886
|
function getDetailMsg() {
|
|
1826
1887
|
if (currentMsgDetailIdx != null) return currentMessages[currentMsgDetailIdx];
|
|
1827
1888
|
if (currentPinDetailId) return currentPins.find((p) => p.id === currentPinDetailId);
|
|
@@ -1832,8 +1893,7 @@ function getDetailMsg() {
|
|
|
1832
1893
|
async function copyMsgToClipboard(btn) {
|
|
1833
1894
|
const m = getDetailMsg();
|
|
1834
1895
|
if (!m) return;
|
|
1835
|
-
|
|
1836
|
-
copyWithFeedback(content, btn);
|
|
1896
|
+
copyWithFeedback(getMessageDisplayContent(m), btn);
|
|
1837
1897
|
}
|
|
1838
1898
|
|
|
1839
1899
|
async function postAndToast(url, body, label) {
|
|
@@ -1853,9 +1913,8 @@ async function postAndToast(url, body, label) {
|
|
|
1853
1913
|
async function openMsgInEditor() {
|
|
1854
1914
|
const m = getDetailMsg();
|
|
1855
1915
|
if (!m) return;
|
|
1856
|
-
const
|
|
1857
|
-
|
|
1858
|
-
postAndToast('/api/open-in-editor', { content, title }, 'in editor');
|
|
1916
|
+
const title = m.type === 'tool_use' ? m.tool : m.compactSummary ? 'compact-summary' : m.type;
|
|
1917
|
+
postAndToast('/api/open-in-editor', { content: getMessageDisplayContent(m), title }, 'in editor');
|
|
1859
1918
|
}
|
|
1860
1919
|
|
|
1861
1920
|
function formatDuration(ms) {
|
|
@@ -2332,7 +2391,8 @@ function renderSessions() {
|
|
|
2332
2391
|
|
|
2333
2392
|
const pinState = getSessionPinState(session.id);
|
|
2334
2393
|
const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
|
|
2335
|
-
const pinTitle =
|
|
2394
|
+
const pinTitle =
|
|
2395
|
+
pinState === 'pinned' || pinState === 'sticky' ? 'Unpin session (.)' : 'Pin session (. · > sticky)';
|
|
2336
2396
|
const showCtx = !!session.contextStatus;
|
|
2337
2397
|
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
2338
2398
|
const bookmarksCount = loadPins(session.id).length;
|
|
@@ -2383,12 +2443,10 @@ function renderSessions() {
|
|
|
2383
2443
|
const groupPinned = localStorage.getItem('groupPinnedSessions') !== 'false';
|
|
2384
2444
|
const renderGroupSessions = (sessions, pinKey) => {
|
|
2385
2445
|
if (!groupPinned || pinnedSessionIds.size === 0) return sessions.map(renderSessionCard).join('');
|
|
2386
|
-
const gPinned = sessions.filter((s) =>
|
|
2446
|
+
const gPinned = sessions.filter((s) => isPlacedPinned(s.id) && !isPlacedSticky(s.id));
|
|
2387
2447
|
if (gPinned.length === 0) return sessions.map(renderSessionCard).join('');
|
|
2388
2448
|
const gIdlePinned = gPinned.filter((s) => !isSessionActive(s));
|
|
2389
|
-
const gUnpinned = sessions.filter(
|
|
2390
|
-
(s) => !pinnedSessionIds.has(s.id) || isSessionActive(s) || stickySessionIds.has(s.id),
|
|
2391
|
-
);
|
|
2449
|
+
const gUnpinned = sessions.filter((s) => !isPlacedPinned(s.id) || isSessionActive(s) || isPlacedSticky(s.id));
|
|
2392
2450
|
const pinCollapsed = collapsedProjectGroups.has(pinKey);
|
|
2393
2451
|
if (gIdlePinned.length === 0 && !pinCollapsed) return gUnpinned.map(renderSessionCard).join('');
|
|
2394
2452
|
return (
|
|
@@ -2415,8 +2473,7 @@ function renderSessions() {
|
|
|
2415
2473
|
);
|
|
2416
2474
|
};
|
|
2417
2475
|
if (!groupPinned && (pinnedSessionIds.size > 0 || stickySessionIds.size > 0)) {
|
|
2418
|
-
const pinWeight = (s) =>
|
|
2419
|
-
stickySessionIds.has(s.id) ? 2 : pinnedSessionIds.has(s.id) && !isSessionActive(s) ? 1 : 0;
|
|
2476
|
+
const pinWeight = (s) => (isPlacedSticky(s.id) ? 2 : isPlacedPinned(s.id) && !isSessionActive(s) ? 1 : 0);
|
|
2420
2477
|
const pinSort = (a, b) => pinWeight(b) - pinWeight(a);
|
|
2421
2478
|
for (const [, arr] of groups) arr.sort(pinSort);
|
|
2422
2479
|
ungrouped.sort(pinSort);
|
|
@@ -2492,12 +2549,10 @@ function renderSessions() {
|
|
|
2492
2549
|
|
|
2493
2550
|
sessionsList.innerHTML = html;
|
|
2494
2551
|
} else {
|
|
2495
|
-
const sticky = filteredSessions.filter((s) =>
|
|
2496
|
-
const idlePinned = filteredSessions.filter((s) =>
|
|
2552
|
+
const sticky = filteredSessions.filter((s) => isPlacedSticky(s.id));
|
|
2553
|
+
const idlePinned = filteredSessions.filter((s) => isPlacedPinned(s.id) && !isSessionActive(s));
|
|
2497
2554
|
const rest = filteredSessions.filter(
|
|
2498
|
-
(s) =>
|
|
2499
|
-
(!pinnedSessionIds.has(s.id) && !stickySessionIds.has(s.id)) ||
|
|
2500
|
-
(pinnedSessionIds.has(s.id) && isSessionActive(s)),
|
|
2555
|
+
(s) => (!isPlacedPinned(s.id) && !isPlacedSticky(s.id)) || (isPlacedPinned(s.id) && isSessionActive(s)),
|
|
2501
2556
|
);
|
|
2502
2557
|
let html = '';
|
|
2503
2558
|
if (sticky.length > 0) {
|
|
@@ -2860,17 +2915,26 @@ function getGroupSessionsContainer(header) {
|
|
|
2860
2915
|
|
|
2861
2916
|
function getNavigableItems() {
|
|
2862
2917
|
const items = [];
|
|
2918
|
+
const walkGroupContainer = (container) => {
|
|
2919
|
+
if (!container) return;
|
|
2920
|
+
for (const child of container.children) {
|
|
2921
|
+
if (child.classList.contains('pinned-sub-section')) {
|
|
2922
|
+
const subHeader = child.querySelector('.pinned-sub-header');
|
|
2923
|
+
if (subHeader) items.push(subHeader);
|
|
2924
|
+
const subItems = child.querySelector('.pinned-sub-items');
|
|
2925
|
+
if (subItems && !subItems.classList.contains('collapsed')) {
|
|
2926
|
+
for (const s of subItems.querySelectorAll(':scope > .session-item')) items.push(s);
|
|
2927
|
+
}
|
|
2928
|
+
} else if (child.classList.contains('session-item')) {
|
|
2929
|
+
items.push(child);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2863
2933
|
for (const el of sessionsList.children) {
|
|
2864
2934
|
if (el.classList.contains('project-group-header')) {
|
|
2865
2935
|
items.push(el);
|
|
2866
2936
|
if (!collapsedProjectGroups.has(el.dataset.groupPath)) {
|
|
2867
|
-
|
|
2868
|
-
if (container) {
|
|
2869
|
-
for (const s of container.querySelectorAll('.session-item')) {
|
|
2870
|
-
if (s.closest('.pinned-sub-items.collapsed')) continue;
|
|
2871
|
-
items.push(s);
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2937
|
+
walkGroupContainer(getGroupSessionsContainer(el));
|
|
2874
2938
|
}
|
|
2875
2939
|
} else if (el.classList.contains('session-item')) {
|
|
2876
2940
|
items.push(el);
|
|
@@ -2931,49 +2995,57 @@ function setGroupCollapsed(header, collapsed) {
|
|
|
2931
2995
|
} catch (_) {}
|
|
2932
2996
|
}
|
|
2933
2997
|
|
|
2998
|
+
function isGroupHeader(el) {
|
|
2999
|
+
return el.classList.contains('project-group-header') || el.classList.contains('pinned-sub-header');
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
function findParentHeader(el) {
|
|
3003
|
+
const subContainer = el.closest('.pinned-sub-items');
|
|
3004
|
+
if (subContainer?.previousElementSibling?.classList.contains('pinned-sub-header')) {
|
|
3005
|
+
return subContainer.previousElementSibling;
|
|
3006
|
+
}
|
|
3007
|
+
const container = el.closest('.project-group-sessions');
|
|
3008
|
+
if (!container) return null;
|
|
3009
|
+
let header = container.previousElementSibling;
|
|
3010
|
+
while (header && !header.classList.contains('project-group-header')) header = header.previousElementSibling;
|
|
3011
|
+
return header;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2934
3014
|
function handleSidebarHorizontal(direction) {
|
|
2935
3015
|
const items = getNavigableItems();
|
|
2936
3016
|
if (selectedSessionIdx < 0 || selectedSessionIdx >= items.length) return;
|
|
2937
3017
|
const el = items[selectedSessionIdx];
|
|
2938
|
-
const isHeader = el.classList.contains('project-group-header');
|
|
2939
3018
|
const collapse = direction < 0;
|
|
2940
3019
|
|
|
2941
|
-
if (
|
|
2942
|
-
const
|
|
2943
|
-
const isCollapsed = collapsedProjectGroups.has(groupPath);
|
|
3020
|
+
if (isGroupHeader(el)) {
|
|
3021
|
+
const isCollapsed = collapsedProjectGroups.has(el.dataset.groupPath);
|
|
2944
3022
|
if (collapse) {
|
|
2945
3023
|
if (!isCollapsed) setGroupCollapsed(el, true);
|
|
3024
|
+
} else if (isCollapsed) {
|
|
3025
|
+
setGroupCollapsed(el, false);
|
|
2946
3026
|
} else {
|
|
2947
|
-
|
|
2948
|
-
setGroupCollapsed(el, false);
|
|
2949
|
-
} else {
|
|
2950
|
-
navigateSession(1);
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
} else {
|
|
2954
|
-
if (collapse) {
|
|
2955
|
-
const container = el.closest('.project-group-sessions');
|
|
2956
|
-
if (container) {
|
|
2957
|
-
let header = container.previousElementSibling;
|
|
2958
|
-
while (header && !header.classList.contains('project-group-header')) header = header.previousElementSibling;
|
|
2959
|
-
if (header) {
|
|
2960
|
-
const headerIdx = items.indexOf(header);
|
|
2961
|
-
if (headerIdx >= 0) selectSessionByIndex(headerIdx, items);
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
} else {
|
|
2965
|
-
activateSelectedSession(items);
|
|
3027
|
+
navigateSession(1);
|
|
2966
3028
|
}
|
|
3029
|
+
return;
|
|
2967
3030
|
}
|
|
3031
|
+
|
|
3032
|
+
if (!collapse) {
|
|
3033
|
+
activateSelectedSession(items);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
const header = findParentHeader(el);
|
|
3038
|
+
if (!header) return;
|
|
3039
|
+
const headerIdx = items.indexOf(header);
|
|
3040
|
+
if (headerIdx >= 0) selectSessionByIndex(headerIdx, items);
|
|
2968
3041
|
}
|
|
2969
3042
|
|
|
2970
3043
|
function activateSelectedSession(items) {
|
|
2971
3044
|
items = items || getNavigableItems();
|
|
2972
3045
|
if (selectedSessionIdx < 0 || selectedSessionIdx >= items.length) return;
|
|
2973
3046
|
const el = items[selectedSessionIdx];
|
|
2974
|
-
if (el
|
|
2975
|
-
|
|
2976
|
-
setGroupCollapsed(el, !collapsedProjectGroups.has(groupPath));
|
|
3047
|
+
if (isGroupHeader(el)) {
|
|
3048
|
+
setGroupCollapsed(el, !collapsedProjectGroups.has(el.dataset.groupPath));
|
|
2977
3049
|
} else {
|
|
2978
3050
|
el.click();
|
|
2979
3051
|
}
|
|
@@ -4082,6 +4154,14 @@ document.addEventListener('keydown', (e) => {
|
|
|
4082
4154
|
showStorageManager();
|
|
4083
4155
|
return;
|
|
4084
4156
|
}
|
|
4157
|
+
if (e.key === '.' || e.key === '>') {
|
|
4158
|
+
const sid = sessionsList.querySelector('.kb-selected')?.dataset.sessionId || currentSessionId;
|
|
4159
|
+
if (sid) {
|
|
4160
|
+
e.preventDefault();
|
|
4161
|
+
(e.shiftKey ? toggleSessionSticky : toggleSessionPin)(sid);
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4085
4165
|
|
|
4086
4166
|
// Tab toggles focus zone
|
|
4087
4167
|
if (e.key === 'Tab') {
|
|
@@ -4213,6 +4293,18 @@ document.addEventListener('keydown', (e) => {
|
|
|
4213
4293
|
hubNavigate('memory', mSession?.project ? `?project=${encodeURIComponent(mSession.project)}` : undefined);
|
|
4214
4294
|
return;
|
|
4215
4295
|
}
|
|
4296
|
+
if (e.code === 'KeyC' && e.shiftKey) {
|
|
4297
|
+
e.preventDefault();
|
|
4298
|
+
if (!contextSid) {
|
|
4299
|
+
showToast('No session selected');
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
navigator.clipboard
|
|
4303
|
+
.writeText(contextSid)
|
|
4304
|
+
.then(() => showToast(`Copied session id: ${contextSid.slice(0, 8)}`, 'success'))
|
|
4305
|
+
.catch(() => showToast('Failed to copy session id'));
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4216
4308
|
if (matchKey(e, 'KeyR')) {
|
|
4217
4309
|
e.preventDefault();
|
|
4218
4310
|
if (_manualRefreshing) return;
|
|
@@ -5986,4 +6078,24 @@ window.hubNavigate = function hubNavigate(app, url) {
|
|
|
5986
6078
|
if (!window.__HUB__?.enabled) return;
|
|
5987
6079
|
window.parent?.postMessage({ type: 'hub:navigate', app, url }, '*');
|
|
5988
6080
|
};
|
|
6081
|
+
|
|
6082
|
+
(function initHubTheme() {
|
|
6083
|
+
const getTheme = () => (document.body.classList.contains('light') ? 'light' : 'dark');
|
|
6084
|
+
const hubOrigin = () => (window.__HUB__?.url ? new URL(window.__HUB__.url).origin : null);
|
|
6085
|
+
let lastTheme = getTheme();
|
|
6086
|
+
window.addEventListener('message', (e) => {
|
|
6087
|
+
if (e.source !== window.parent || e.origin !== hubOrigin()) return;
|
|
6088
|
+
if (e.data?.type !== 'hub:theme') return;
|
|
6089
|
+
if (getTheme() === e.data.theme) return;
|
|
6090
|
+
window.toggleTheme();
|
|
6091
|
+
lastTheme = getTheme();
|
|
6092
|
+
});
|
|
6093
|
+
new MutationObserver(() => {
|
|
6094
|
+
const t = getTheme();
|
|
6095
|
+
if (t === lastTheme) return;
|
|
6096
|
+
lastTheme = t;
|
|
6097
|
+
const origin = hubOrigin();
|
|
6098
|
+
if (origin) window.parent.postMessage({ type: 'hub:theme', theme: t }, origin);
|
|
6099
|
+
}).observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
6100
|
+
})();
|
|
5989
6101
|
// #endregion HUB_INTEGRATION
|
package/public/index.html
CHANGED
|
@@ -421,6 +421,14 @@
|
|
|
421
421
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">N</kbd></td>
|
|
422
422
|
<td style="padding: 4px 0; color: var(--text-primary);">Toggle scratchpad</td>
|
|
423
423
|
</tr>
|
|
424
|
+
<tr>
|
|
425
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">.</kbd></td>
|
|
426
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Pin/unpin selected session</td>
|
|
427
|
+
</tr>
|
|
428
|
+
<tr>
|
|
429
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">></kbd></td>
|
|
430
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Toggle sticky on selected session</td>
|
|
431
|
+
</tr>
|
|
424
432
|
<tr>
|
|
425
433
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">T</kbd></td>
|
|
426
434
|
<td style="padding: 4px 0; color: var(--text-primary);">Toggle theme</td>
|
|
@@ -445,6 +453,10 @@
|
|
|
445
453
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Shift+S</kbd></td>
|
|
446
454
|
<td style="padding: 4px 0; color: var(--text-primary);">Storage manager</td>
|
|
447
455
|
</tr>
|
|
456
|
+
<tr>
|
|
457
|
+
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">Shift+C</kbd></td>
|
|
458
|
+
<td style="padding: 4px 0; color: var(--text-primary);">Copy session id</td>
|
|
459
|
+
</tr>
|
|
448
460
|
<tr>
|
|
449
461
|
<td style="padding: 4px 0; color: var(--text-secondary);"><kbd style="background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-family: monospace;">J/K</kbd></td>
|
|
450
462
|
<td style="padding: 4px 0; color: var(--text-primary);">Navigate messages in detail modal</td>
|
package/public/style.css
CHANGED
|
@@ -2236,6 +2236,19 @@ body::before {
|
|
|
2236
2236
|
font-family: var(--font-mono);
|
|
2237
2237
|
font-size: 0.85rem;
|
|
2238
2238
|
}
|
|
2239
|
+
.msg-detail-pre-tinted {
|
|
2240
|
+
background: rgba(127, 127, 127, 0.15);
|
|
2241
|
+
border-radius: 4px;
|
|
2242
|
+
padding: 8px 10px;
|
|
2243
|
+
}
|
|
2244
|
+
.expand-toggle-btn {
|
|
2245
|
+
background: none;
|
|
2246
|
+
border: none;
|
|
2247
|
+
color: var(--accent);
|
|
2248
|
+
cursor: pointer;
|
|
2249
|
+
text-decoration: underline;
|
|
2250
|
+
margin-left: 6px;
|
|
2251
|
+
}
|
|
2239
2252
|
.msg-cmd .msg-text code {
|
|
2240
2253
|
background: var(--bg-hover);
|
|
2241
2254
|
padding: 2px 6px;
|
|
@@ -3488,7 +3501,8 @@ pre.mermaid svg {
|
|
|
3488
3501
|
color: var(--text-primary);
|
|
3489
3502
|
}
|
|
3490
3503
|
|
|
3491
|
-
.project-group-header.kb-selected
|
|
3504
|
+
.project-group-header.kb-selected,
|
|
3505
|
+
.pinned-sub-header.kb-selected {
|
|
3492
3506
|
color: var(--text-primary);
|
|
3493
3507
|
background: var(--bg-hover);
|
|
3494
3508
|
border-radius: 4px;
|
package/server.js
CHANGED
|
@@ -19,7 +19,8 @@ const {
|
|
|
19
19
|
readCompactSummaries,
|
|
20
20
|
findTerminatedTeammates,
|
|
21
21
|
extractPromptFromTranscript,
|
|
22
|
-
extractModelFromTranscript
|
|
22
|
+
extractModelFromTranscript,
|
|
23
|
+
readFullToolResult
|
|
23
24
|
} = require('./lib/parsers');
|
|
24
25
|
|
|
25
26
|
if (process.argv.includes("--install") || process.argv.includes("--uninstall")) {
|
|
@@ -1293,12 +1294,23 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
1293
1294
|
}
|
|
1294
1295
|
}
|
|
1295
1296
|
for (const msg of messages) {
|
|
1296
|
-
|
|
1297
|
+
// Keep toolUseId on truncated tool results so the client can lazy-fetch the full text
|
|
1298
|
+
if (msg.toolUseId && !msg.toolResultTruncated) delete msg.toolUseId;
|
|
1297
1299
|
delete msg.promptId;
|
|
1298
1300
|
}
|
|
1299
1301
|
res.json({ messages, hasMore, sessionId: req.params.sessionId });
|
|
1300
1302
|
});
|
|
1301
1303
|
|
|
1304
|
+
app.get('/api/sessions/:sessionId/tool-result/:toolUseId', (req, res) => {
|
|
1305
|
+
const metadata = loadSessionMetadata();
|
|
1306
|
+
const meta = metadata[req.params.sessionId];
|
|
1307
|
+
const jsonlPath = meta?.jsonlPath;
|
|
1308
|
+
if (!jsonlPath) return res.status(404).json({ error: 'session not found' });
|
|
1309
|
+
const content = readFullToolResult(jsonlPath, req.params.toolUseId);
|
|
1310
|
+
if (content == null) return res.status(404).json({ error: 'tool result not found' });
|
|
1311
|
+
res.json({ toolUseId: req.params.toolUseId, content });
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1302
1314
|
app.get('/api/version', (req, res) => {
|
|
1303
1315
|
const pkg = require('./package.json');
|
|
1304
1316
|
res.json({ version: pkg.version });
|