claude-code-kanban 3.6.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 +209 -63
- package/public/index.html +12 -0
- package/public/style.css +22 -5
- package/server.js +15 -10
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
|
@@ -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
|
}
|
|
@@ -494,6 +496,7 @@ async function fetchTasks(sessionId) {
|
|
|
494
496
|
if (revealedStorageSessionId && sessionId !== revealedStorageSessionId) {
|
|
495
497
|
revealedStorageSessionId = null;
|
|
496
498
|
}
|
|
499
|
+
if (currentSessionId && currentSessionId !== sessionId) deferredPinPlacement.delete(currentSessionId);
|
|
497
500
|
currentSessionId = sessionId;
|
|
498
501
|
currentPins = loadPins(sessionId);
|
|
499
502
|
ownerFilter = '';
|
|
@@ -653,6 +656,7 @@ async function refreshProjectAgents() {
|
|
|
653
656
|
function toggleMessagePanel() {
|
|
654
657
|
const panel = document.getElementById('message-panel');
|
|
655
658
|
messagePanelOpen = !messagePanelOpen;
|
|
659
|
+
localStorage.setItem('message-panel-open', messagePanelOpen);
|
|
656
660
|
panel.classList.toggle('visible', messagePanelOpen);
|
|
657
661
|
document.getElementById('message-toggle')?.classList.toggle('active', messagePanelOpen);
|
|
658
662
|
if (messagePanelOpen && currentSessionId) {
|
|
@@ -667,10 +671,13 @@ async function openSessionWithBookmarks(sessionId) {
|
|
|
667
671
|
if (!messagePanelOpen) {
|
|
668
672
|
const panel = document.getElementById('message-panel');
|
|
669
673
|
messagePanelOpen = true;
|
|
674
|
+
localStorage.setItem('message-panel-open', 'true');
|
|
670
675
|
panel.classList.add('visible');
|
|
671
676
|
document.getElementById('message-toggle')?.classList.add('active');
|
|
672
677
|
}
|
|
673
678
|
await fetchTasks(sessionId);
|
|
679
|
+
if (currentMessages.length) renderMessages(currentMessages);
|
|
680
|
+
fetchMessages(sessionId);
|
|
674
681
|
}
|
|
675
682
|
|
|
676
683
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
@@ -1200,6 +1207,9 @@ function togglePin(msgIndex) {
|
|
|
1200
1207
|
text: m.text || null,
|
|
1201
1208
|
fullText: m.fullText || null,
|
|
1202
1209
|
tool: m.tool || null,
|
|
1210
|
+
toolUseId: m.toolUseId || null,
|
|
1211
|
+
toolResult: m.toolResult || null,
|
|
1212
|
+
toolResultTruncated: m.toolResultTruncated || false,
|
|
1203
1213
|
detail: m.detail || null,
|
|
1204
1214
|
fullDetail: m.fullDetail || null,
|
|
1205
1215
|
description: m.description || null,
|
|
@@ -1288,6 +1298,8 @@ function togglePinnedCollapse() {
|
|
|
1288
1298
|
//#region PINNING
|
|
1289
1299
|
let pinnedSessionIds = new Set();
|
|
1290
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();
|
|
1291
1303
|
|
|
1292
1304
|
function loadPinnedSessions() {
|
|
1293
1305
|
try {
|
|
@@ -1310,35 +1322,45 @@ function savePinnedSessions() {
|
|
|
1310
1322
|
localStorage.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
|
|
1311
1323
|
}
|
|
1312
1324
|
|
|
1313
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1314
1325
|
function toggleSessionPin(sessionId) {
|
|
1315
1326
|
if (pinnedSessionIds.has(sessionId)) {
|
|
1316
1327
|
pinnedSessionIds.delete(sessionId);
|
|
1317
1328
|
stickySessionIds.delete(sessionId);
|
|
1329
|
+
deferredPinPlacement.delete(sessionId);
|
|
1318
1330
|
} else {
|
|
1319
1331
|
pinnedSessionIds.add(sessionId);
|
|
1332
|
+
if (sessionId === currentSessionId) deferredPinPlacement.add(sessionId);
|
|
1320
1333
|
}
|
|
1321
1334
|
savePinnedSessions();
|
|
1322
1335
|
renderSessions();
|
|
1323
1336
|
}
|
|
1324
1337
|
|
|
1325
|
-
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1326
1338
|
function toggleSessionSticky(sessionId) {
|
|
1327
1339
|
if (stickySessionIds.has(sessionId)) {
|
|
1328
1340
|
stickySessionIds.delete(sessionId);
|
|
1329
1341
|
pinnedSessionIds.delete(sessionId);
|
|
1342
|
+
deferredPinPlacement.delete(sessionId);
|
|
1330
1343
|
} else {
|
|
1331
1344
|
pinnedSessionIds.add(sessionId);
|
|
1332
1345
|
stickySessionIds.add(sessionId);
|
|
1346
|
+
if (sessionId === currentSessionId) deferredPinPlacement.add(sessionId);
|
|
1333
1347
|
}
|
|
1334
1348
|
savePinnedSessions();
|
|
1335
1349
|
renderSessions();
|
|
1336
1350
|
}
|
|
1337
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
|
+
|
|
1338
1359
|
function handleSessionPinEvent({ id, state }) {
|
|
1339
1360
|
if (!id) return;
|
|
1340
1361
|
pinnedSessionIds.delete(id);
|
|
1341
1362
|
stickySessionIds.delete(id);
|
|
1363
|
+
deferredPinPlacement.delete(id);
|
|
1342
1364
|
if (state === 'pinned') pinnedSessionIds.add(id);
|
|
1343
1365
|
if (state === 'sticky') {
|
|
1344
1366
|
pinnedSessionIds.add(id);
|
|
@@ -1366,11 +1388,16 @@ function _renderPinToDetail(pin) {
|
|
|
1366
1388
|
document.getElementById('msg-detail-title').textContent = pin.tool || 'Tool';
|
|
1367
1389
|
const fullText = pin.fullDetail || pin.detail || '';
|
|
1368
1390
|
const pinParamsHtml = renderToolParamsHtml(pin.params);
|
|
1369
|
-
const pinResultHtml = renderToolResultHtml(
|
|
1391
|
+
const pinResultHtml = renderToolResultHtml(
|
|
1392
|
+
pin.toolResult,
|
|
1393
|
+
pin.toolResultTruncated,
|
|
1394
|
+
pin.toolResultFull,
|
|
1395
|
+
pin.toolUseId,
|
|
1396
|
+
);
|
|
1370
1397
|
const pinDetailEscaped = escapeHtml(fullText);
|
|
1371
1398
|
const pinDetailRendered = pin.tool === 'Bash' ? highlightBash(pinDetailEscaped) : pinDetailEscaped;
|
|
1372
1399
|
body.innerHTML =
|
|
1373
|
-
(fullText ? `<pre class="
|
|
1400
|
+
(fullText ? `<pre class="${TINTED_PRE_CLASS}">${pinDetailRendered}</pre>` : '<em>No details</em>') +
|
|
1374
1401
|
pinParamsHtml +
|
|
1375
1402
|
pinResultHtml;
|
|
1376
1403
|
} else if (pin.type === 'agent') {
|
|
@@ -1431,7 +1458,7 @@ function showMsgDetail(idx) {
|
|
|
1431
1458
|
const taskResultHtml = TASK_TOOLS.has(m.tool) ? renderTaskResult(m.toolResult) : '';
|
|
1432
1459
|
const toolResultHtml = hideResult
|
|
1433
1460
|
? ''
|
|
1434
|
-
: renderToolResultHtml(m.toolResult, m.toolResultTruncated, m.toolResultFull);
|
|
1461
|
+
: renderToolResultHtml(m.toolResult, m.toolResultTruncated, m.toolResultFull, m.toolUseId);
|
|
1435
1462
|
const hasAgentTabs = m.tool === 'Agent' && m.agentId && (m.agentLastMessage || m.agentPrompt);
|
|
1436
1463
|
let mainHtml;
|
|
1437
1464
|
if (sendProto) {
|
|
@@ -1445,7 +1472,7 @@ function showMsgDetail(idx) {
|
|
|
1445
1472
|
} else if (fullText) {
|
|
1446
1473
|
const detailEscaped = escapeHtml(fullText);
|
|
1447
1474
|
const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
|
|
1448
|
-
mainHtml = `${descHtml}<pre class="
|
|
1475
|
+
mainHtml = `${descHtml}<pre class="${TINTED_PRE_CLASS}">${detailRendered}</pre>`;
|
|
1449
1476
|
} else {
|
|
1450
1477
|
mainHtml = TASK_TOOLS.has(m.tool) ? '' : '<em>No details</em>';
|
|
1451
1478
|
}
|
|
@@ -1503,10 +1530,25 @@ function closeMsgDetailModal() {
|
|
|
1503
1530
|
msgDetailFollowLatest = false;
|
|
1504
1531
|
}
|
|
1505
1532
|
|
|
1533
|
+
function _setModalWidth(modal, slot, on, maxWidth, width) {
|
|
1534
|
+
const mwKey = `prev${slot}MaxWidth`;
|
|
1535
|
+
const wKey = `prev${slot}Width`;
|
|
1536
|
+
if (on) {
|
|
1537
|
+
modal.dataset[mwKey] = modal.style.maxWidth || '';
|
|
1538
|
+
modal.dataset[wKey] = modal.style.width || '';
|
|
1539
|
+
modal.style.maxWidth = maxWidth;
|
|
1540
|
+
modal.style.width = width;
|
|
1541
|
+
} else {
|
|
1542
|
+
modal.style.maxWidth = modal.dataset[mwKey] || '';
|
|
1543
|
+
modal.style.width = modal.dataset[wKey] || '';
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1506
1547
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1507
1548
|
function toggleModalFullscreen(modalId) {
|
|
1508
1549
|
const modal = document.querySelector(`#${modalId} .modal`);
|
|
1509
1550
|
const isFs = modal.classList.toggle('fullscreen');
|
|
1551
|
+
_setModalWidth(modal, 'Fs', isFs, '', '');
|
|
1510
1552
|
updateFullscreenBtnIcon(`${modalId}-fullscreen-btn`, isFs);
|
|
1511
1553
|
}
|
|
1512
1554
|
|
|
@@ -1677,11 +1719,11 @@ function renderToolParamsHtml(params) {
|
|
|
1677
1719
|
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">`;
|
|
1678
1720
|
if (params.old_string) {
|
|
1679
1721
|
html += `<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">old_string</div>
|
|
1680
|
-
<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>`;
|
|
1681
1723
|
}
|
|
1682
1724
|
if (params.new_string) {
|
|
1683
1725
|
html += `<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px;margin-top:6px">new_string</div>
|
|
1684
|
-
<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>`;
|
|
1685
1727
|
}
|
|
1686
1728
|
html += `</div>`;
|
|
1687
1729
|
}
|
|
@@ -1696,13 +1738,14 @@ function renderToolParamsHtml(params) {
|
|
|
1696
1738
|
const toggle = makeExpandToggle(escapeHtml(truncContent), escapeHtml(params.content), {
|
|
1697
1739
|
fontSize: '0.75rem',
|
|
1698
1740
|
maxHeight: '500px',
|
|
1741
|
+
tinted: true,
|
|
1699
1742
|
});
|
|
1700
1743
|
writeMoreBtn = ` ${toggle.btn}`;
|
|
1701
1744
|
fullBlock = toggle.full;
|
|
1702
1745
|
}
|
|
1703
1746
|
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">
|
|
1704
1747
|
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">content${writeMoreBtn}</div>
|
|
1705
|
-
<pre class="
|
|
1748
|
+
<pre class="${TINTED_PRE_CLASS}" style="max-height:300px;overflow:auto">${escapeHtml(truncContent)}</pre>
|
|
1706
1749
|
${fullBlock}
|
|
1707
1750
|
</div>`;
|
|
1708
1751
|
}
|
|
@@ -1736,18 +1779,36 @@ function highlightBash(escaped) {
|
|
|
1736
1779
|
.replace(/((?:^|\s)(?:&&|\|\||[|;])(?:\s|$))/g, '<span style="color:#d4d4d4;font-weight:bold">$1</span>');
|
|
1737
1780
|
}
|
|
1738
1781
|
|
|
1782
|
+
const TINTED_PRE_CLASS = 'msg-detail-pre msg-detail-pre-tinted';
|
|
1739
1783
|
let _expandIdCounter = 0;
|
|
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';
|
|
1789
|
+
btn.textContent = expand ? 'Show less' : 'Show more';
|
|
1790
|
+
const panel = btn.closest('.message-panel');
|
|
1791
|
+
if (panel) panel.classList.toggle('msg-expanded-wide', expand);
|
|
1792
|
+
const modal = btn.closest('.modal');
|
|
1793
|
+
if (modal) _setModalWidth(modal, 'Expand', expand, '60vw', '60vw');
|
|
1794
|
+
}
|
|
1795
|
+
function _toggleExpand(btn) {
|
|
1796
|
+
const f = document.getElementById(btn.dataset.expandId);
|
|
1797
|
+
if (f) _applyExpandToggle(btn, f);
|
|
1798
|
+
}
|
|
1740
1799
|
function makeExpandToggle(_truncatedHtml, fullHtml, opts = {}) {
|
|
1741
1800
|
const id = `expand-${++_expandIdCounter}`;
|
|
1742
1801
|
const fontSize = opts.fontSize || '0.8rem';
|
|
1743
1802
|
const maxHeight = opts.maxHeight || '';
|
|
1744
|
-
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>`;
|
|
1745
1805
|
const mhStyle = maxHeight ? `max-height:${maxHeight};` : '';
|
|
1746
|
-
const full = `<pre id="${id}" class="
|
|
1806
|
+
const full = `<pre id="${id}" class="${cls}" style="${mhStyle}overflow:auto;display:none">${fullHtml}</pre>`;
|
|
1747
1807
|
return { btn, full };
|
|
1748
1808
|
}
|
|
1749
1809
|
|
|
1750
1810
|
function autoSizeModal(modal, body) {
|
|
1811
|
+
if (modal.classList.contains('fullscreen')) return;
|
|
1751
1812
|
modal.style.maxWidth = '';
|
|
1752
1813
|
modal.classList.remove('has-mermaid');
|
|
1753
1814
|
const hasMermaid = body.querySelector('pre.mermaid') !== null;
|
|
@@ -1762,7 +1823,7 @@ function autoSizeModal(modal, body) {
|
|
|
1762
1823
|
if (desired > current) modal.style.maxWidth = `${desired}px`;
|
|
1763
1824
|
}
|
|
1764
1825
|
|
|
1765
|
-
function renderToolResultHtml(toolResult, isTruncated, fullResult) {
|
|
1826
|
+
function renderToolResultHtml(toolResult, isTruncated, fullResult, toolUseId) {
|
|
1766
1827
|
if (!toolResult) return '';
|
|
1767
1828
|
const stripped = stripLineNumbers(toolResult);
|
|
1768
1829
|
const escaped = escapeHtml(stripped);
|
|
@@ -1772,6 +1833,10 @@ function renderToolResultHtml(toolResult, isTruncated, fullResult) {
|
|
|
1772
1833
|
const toggle = makeExpandToggle(escaped, escapeHtml(stripLineNumbers(fullResult)));
|
|
1773
1834
|
truncLabel = toggle.btn;
|
|
1774
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>`;
|
|
1775
1840
|
} else if (isTruncated) {
|
|
1776
1841
|
truncLabel = '<span style="color:var(--text-muted);font-size:0.8rem;margin-left:6px">(truncated)</span>';
|
|
1777
1842
|
}
|
|
@@ -1782,12 +1847,42 @@ function renderToolResultHtml(toolResult, isTruncated, fullResult) {
|
|
|
1782
1847
|
</div>`;
|
|
1783
1848
|
}
|
|
1784
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
|
+
|
|
1785
1876
|
function buildToolContent(m) {
|
|
1786
1877
|
let content = m.fullDetail || m.detail || '';
|
|
1787
1878
|
if (m.toolResult) content += `\n\n--- Output ---\n\n${m.toolResultFull || m.toolResult}`;
|
|
1788
1879
|
return content;
|
|
1789
1880
|
}
|
|
1790
1881
|
|
|
1882
|
+
function getMessageDisplayContent(m) {
|
|
1883
|
+
return m.type === 'tool_use' ? buildToolContent(m) : m.compactSummary || stripAnsi(m.fullText || m.text);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1791
1886
|
function getDetailMsg() {
|
|
1792
1887
|
if (currentMsgDetailIdx != null) return currentMessages[currentMsgDetailIdx];
|
|
1793
1888
|
if (currentPinDetailId) return currentPins.find((p) => p.id === currentPinDetailId);
|
|
@@ -1798,8 +1893,7 @@ function getDetailMsg() {
|
|
|
1798
1893
|
async function copyMsgToClipboard(btn) {
|
|
1799
1894
|
const m = getDetailMsg();
|
|
1800
1895
|
if (!m) return;
|
|
1801
|
-
|
|
1802
|
-
copyWithFeedback(content, btn);
|
|
1896
|
+
copyWithFeedback(getMessageDisplayContent(m), btn);
|
|
1803
1897
|
}
|
|
1804
1898
|
|
|
1805
1899
|
async function postAndToast(url, body, label) {
|
|
@@ -1819,9 +1913,8 @@ async function postAndToast(url, body, label) {
|
|
|
1819
1913
|
async function openMsgInEditor() {
|
|
1820
1914
|
const m = getDetailMsg();
|
|
1821
1915
|
if (!m) return;
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
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');
|
|
1825
1918
|
}
|
|
1826
1919
|
|
|
1827
1920
|
function formatDuration(ms) {
|
|
@@ -2298,7 +2391,8 @@ function renderSessions() {
|
|
|
2298
2391
|
|
|
2299
2392
|
const pinState = getSessionPinState(session.id);
|
|
2300
2393
|
const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
|
|
2301
|
-
const pinTitle =
|
|
2394
|
+
const pinTitle =
|
|
2395
|
+
pinState === 'pinned' || pinState === 'sticky' ? 'Unpin session (.)' : 'Pin session (. · > sticky)';
|
|
2302
2396
|
const showCtx = !!session.contextStatus;
|
|
2303
2397
|
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
2304
2398
|
const bookmarksCount = loadPins(session.id).length;
|
|
@@ -2349,12 +2443,10 @@ function renderSessions() {
|
|
|
2349
2443
|
const groupPinned = localStorage.getItem('groupPinnedSessions') !== 'false';
|
|
2350
2444
|
const renderGroupSessions = (sessions, pinKey) => {
|
|
2351
2445
|
if (!groupPinned || pinnedSessionIds.size === 0) return sessions.map(renderSessionCard).join('');
|
|
2352
|
-
const gPinned = sessions.filter((s) =>
|
|
2446
|
+
const gPinned = sessions.filter((s) => isPlacedPinned(s.id) && !isPlacedSticky(s.id));
|
|
2353
2447
|
if (gPinned.length === 0) return sessions.map(renderSessionCard).join('');
|
|
2354
2448
|
const gIdlePinned = gPinned.filter((s) => !isSessionActive(s));
|
|
2355
|
-
const gUnpinned = sessions.filter(
|
|
2356
|
-
(s) => !pinnedSessionIds.has(s.id) || isSessionActive(s) || stickySessionIds.has(s.id),
|
|
2357
|
-
);
|
|
2449
|
+
const gUnpinned = sessions.filter((s) => !isPlacedPinned(s.id) || isSessionActive(s) || isPlacedSticky(s.id));
|
|
2358
2450
|
const pinCollapsed = collapsedProjectGroups.has(pinKey);
|
|
2359
2451
|
if (gIdlePinned.length === 0 && !pinCollapsed) return gUnpinned.map(renderSessionCard).join('');
|
|
2360
2452
|
return (
|
|
@@ -2381,8 +2473,7 @@ function renderSessions() {
|
|
|
2381
2473
|
);
|
|
2382
2474
|
};
|
|
2383
2475
|
if (!groupPinned && (pinnedSessionIds.size > 0 || stickySessionIds.size > 0)) {
|
|
2384
|
-
const pinWeight = (s) =>
|
|
2385
|
-
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);
|
|
2386
2477
|
const pinSort = (a, b) => pinWeight(b) - pinWeight(a);
|
|
2387
2478
|
for (const [, arr] of groups) arr.sort(pinSort);
|
|
2388
2479
|
ungrouped.sort(pinSort);
|
|
@@ -2458,12 +2549,10 @@ function renderSessions() {
|
|
|
2458
2549
|
|
|
2459
2550
|
sessionsList.innerHTML = html;
|
|
2460
2551
|
} else {
|
|
2461
|
-
const sticky = filteredSessions.filter((s) =>
|
|
2462
|
-
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));
|
|
2463
2554
|
const rest = filteredSessions.filter(
|
|
2464
|
-
(s) =>
|
|
2465
|
-
(!pinnedSessionIds.has(s.id) && !stickySessionIds.has(s.id)) ||
|
|
2466
|
-
(pinnedSessionIds.has(s.id) && isSessionActive(s)),
|
|
2555
|
+
(s) => (!isPlacedPinned(s.id) && !isPlacedSticky(s.id)) || (isPlacedPinned(s.id) && isSessionActive(s)),
|
|
2467
2556
|
);
|
|
2468
2557
|
let html = '';
|
|
2469
2558
|
if (sticky.length > 0) {
|
|
@@ -2826,17 +2915,26 @@ function getGroupSessionsContainer(header) {
|
|
|
2826
2915
|
|
|
2827
2916
|
function getNavigableItems() {
|
|
2828
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
|
+
};
|
|
2829
2933
|
for (const el of sessionsList.children) {
|
|
2830
2934
|
if (el.classList.contains('project-group-header')) {
|
|
2831
2935
|
items.push(el);
|
|
2832
2936
|
if (!collapsedProjectGroups.has(el.dataset.groupPath)) {
|
|
2833
|
-
|
|
2834
|
-
if (container) {
|
|
2835
|
-
for (const s of container.querySelectorAll('.session-item')) {
|
|
2836
|
-
if (s.closest('.pinned-sub-items.collapsed')) continue;
|
|
2837
|
-
items.push(s);
|
|
2838
|
-
}
|
|
2839
|
-
}
|
|
2937
|
+
walkGroupContainer(getGroupSessionsContainer(el));
|
|
2840
2938
|
}
|
|
2841
2939
|
} else if (el.classList.contains('session-item')) {
|
|
2842
2940
|
items.push(el);
|
|
@@ -2897,49 +2995,57 @@ function setGroupCollapsed(header, collapsed) {
|
|
|
2897
2995
|
} catch (_) {}
|
|
2898
2996
|
}
|
|
2899
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
|
+
|
|
2900
3014
|
function handleSidebarHorizontal(direction) {
|
|
2901
3015
|
const items = getNavigableItems();
|
|
2902
3016
|
if (selectedSessionIdx < 0 || selectedSessionIdx >= items.length) return;
|
|
2903
3017
|
const el = items[selectedSessionIdx];
|
|
2904
|
-
const isHeader = el.classList.contains('project-group-header');
|
|
2905
3018
|
const collapse = direction < 0;
|
|
2906
3019
|
|
|
2907
|
-
if (
|
|
2908
|
-
const
|
|
2909
|
-
const isCollapsed = collapsedProjectGroups.has(groupPath);
|
|
3020
|
+
if (isGroupHeader(el)) {
|
|
3021
|
+
const isCollapsed = collapsedProjectGroups.has(el.dataset.groupPath);
|
|
2910
3022
|
if (collapse) {
|
|
2911
3023
|
if (!isCollapsed) setGroupCollapsed(el, true);
|
|
3024
|
+
} else if (isCollapsed) {
|
|
3025
|
+
setGroupCollapsed(el, false);
|
|
2912
3026
|
} else {
|
|
2913
|
-
|
|
2914
|
-
setGroupCollapsed(el, false);
|
|
2915
|
-
} else {
|
|
2916
|
-
navigateSession(1);
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
} else {
|
|
2920
|
-
if (collapse) {
|
|
2921
|
-
const container = el.closest('.project-group-sessions');
|
|
2922
|
-
if (container) {
|
|
2923
|
-
let header = container.previousElementSibling;
|
|
2924
|
-
while (header && !header.classList.contains('project-group-header')) header = header.previousElementSibling;
|
|
2925
|
-
if (header) {
|
|
2926
|
-
const headerIdx = items.indexOf(header);
|
|
2927
|
-
if (headerIdx >= 0) selectSessionByIndex(headerIdx, items);
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
} else {
|
|
2931
|
-
activateSelectedSession(items);
|
|
3027
|
+
navigateSession(1);
|
|
2932
3028
|
}
|
|
3029
|
+
return;
|
|
2933
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);
|
|
2934
3041
|
}
|
|
2935
3042
|
|
|
2936
3043
|
function activateSelectedSession(items) {
|
|
2937
3044
|
items = items || getNavigableItems();
|
|
2938
3045
|
if (selectedSessionIdx < 0 || selectedSessionIdx >= items.length) return;
|
|
2939
3046
|
const el = items[selectedSessionIdx];
|
|
2940
|
-
if (el
|
|
2941
|
-
|
|
2942
|
-
setGroupCollapsed(el, !collapsedProjectGroups.has(groupPath));
|
|
3047
|
+
if (isGroupHeader(el)) {
|
|
3048
|
+
setGroupCollapsed(el, !collapsedProjectGroups.has(el.dataset.groupPath));
|
|
2943
3049
|
} else {
|
|
2944
3050
|
el.click();
|
|
2945
3051
|
}
|
|
@@ -4048,6 +4154,14 @@ document.addEventListener('keydown', (e) => {
|
|
|
4048
4154
|
showStorageManager();
|
|
4049
4155
|
return;
|
|
4050
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
|
+
}
|
|
4051
4165
|
|
|
4052
4166
|
// Tab toggles focus zone
|
|
4053
4167
|
if (e.key === 'Tab') {
|
|
@@ -4179,6 +4293,18 @@ document.addEventListener('keydown', (e) => {
|
|
|
4179
4293
|
hubNavigate('memory', mSession?.project ? `?project=${encodeURIComponent(mSession.project)}` : undefined);
|
|
4180
4294
|
return;
|
|
4181
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
|
+
}
|
|
4182
4308
|
if (matchKey(e, 'KeyR')) {
|
|
4183
4309
|
e.preventDefault();
|
|
4184
4310
|
if (_manualRefreshing) return;
|
|
@@ -5952,4 +6078,24 @@ window.hubNavigate = function hubNavigate(app, url) {
|
|
|
5952
6078
|
if (!window.__HUB__?.enabled) return;
|
|
5953
6079
|
window.parent?.postMessage({ type: 'hub:navigate', app, url }, '*');
|
|
5954
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
|
+
})();
|
|
5955
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
|
@@ -1824,6 +1824,9 @@ body::before {
|
|
|
1824
1824
|
.message-panel.visible {
|
|
1825
1825
|
display: flex;
|
|
1826
1826
|
}
|
|
1827
|
+
.message-panel.msg-expanded-wide {
|
|
1828
|
+
width: 60vw;
|
|
1829
|
+
}
|
|
1827
1830
|
.message-panel-header {
|
|
1828
1831
|
padding: 16px 20px;
|
|
1829
1832
|
display: flex;
|
|
@@ -2233,6 +2236,19 @@ body::before {
|
|
|
2233
2236
|
font-family: var(--font-mono);
|
|
2234
2237
|
font-size: 0.85rem;
|
|
2235
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
|
+
}
|
|
2236
2252
|
.msg-cmd .msg-text code {
|
|
2237
2253
|
background: var(--bg-hover);
|
|
2238
2254
|
padding: 2px 6px;
|
|
@@ -2787,8 +2803,8 @@ body.light .msg-assistant .msg-text {
|
|
|
2787
2803
|
}
|
|
2788
2804
|
|
|
2789
2805
|
.modal.fullscreen {
|
|
2790
|
-
width:
|
|
2791
|
-
max-width:
|
|
2806
|
+
width: 80vw;
|
|
2807
|
+
max-width: 80vw;
|
|
2792
2808
|
height: 92vh;
|
|
2793
2809
|
max-height: 92vh;
|
|
2794
2810
|
}
|
|
@@ -2802,8 +2818,8 @@ body.light .msg-assistant .msg-text {
|
|
|
2802
2818
|
}
|
|
2803
2819
|
|
|
2804
2820
|
.modal.plan-modal.fullscreen {
|
|
2805
|
-
width:
|
|
2806
|
-
max-width:
|
|
2821
|
+
width: 80vw;
|
|
2822
|
+
max-width: 80vw;
|
|
2807
2823
|
height: 92vh;
|
|
2808
2824
|
max-height: 92vh;
|
|
2809
2825
|
}
|
|
@@ -3485,7 +3501,8 @@ pre.mermaid svg {
|
|
|
3485
3501
|
color: var(--text-primary);
|
|
3486
3502
|
}
|
|
3487
3503
|
|
|
3488
|
-
.project-group-header.kb-selected
|
|
3504
|
+
.project-group-header.kb-selected,
|
|
3505
|
+
.pinned-sub-header.kb-selected {
|
|
3489
3506
|
color: var(--text-primary);
|
|
3490
3507
|
background: var(--bg-hover);
|
|
3491
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")) {
|
|
@@ -119,18 +120,11 @@ function isAgentFresh(agent) {
|
|
|
119
120
|
return (Date.now() - new Date(ts).getTime()) < AGENT_TTL_MS;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
const sessionLogStatCache = new Map();
|
|
123
123
|
function getSessionLogStat(meta) {
|
|
124
124
|
if (!meta.jsonlPath) return { mtime: null, hasMessages: false };
|
|
125
125
|
try {
|
|
126
126
|
const st = statSync(meta.jsonlPath);
|
|
127
|
-
|
|
128
|
-
if (cached && cached.mtime === st.mtimeMs) return cached;
|
|
129
|
-
const content = readFileSync(meta.jsonlPath, 'utf8');
|
|
130
|
-
const hasMessages = content.includes('"type":"assistant"');
|
|
131
|
-
const data = { mtime: st.mtimeMs, hasMessages };
|
|
132
|
-
sessionLogStatCache.set(meta.jsonlPath, data);
|
|
133
|
-
return data;
|
|
127
|
+
return { mtime: st.mtimeMs, hasMessages: st.size > 1000 };
|
|
134
128
|
} catch (e) { return { mtime: null, hasMessages: false }; }
|
|
135
129
|
}
|
|
136
130
|
|
|
@@ -1300,12 +1294,23 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
1300
1294
|
}
|
|
1301
1295
|
}
|
|
1302
1296
|
for (const msg of messages) {
|
|
1303
|
-
|
|
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;
|
|
1304
1299
|
delete msg.promptId;
|
|
1305
1300
|
}
|
|
1306
1301
|
res.json({ messages, hasMore, sessionId: req.params.sessionId });
|
|
1307
1302
|
});
|
|
1308
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
|
+
|
|
1309
1314
|
app.get('/api/version', (req, res) => {
|
|
1310
1315
|
const pkg = require('./package.json');
|
|
1311
1316
|
res.json({ version: pkg.version });
|