claude-code-kanban 3.3.0 → 3.4.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 +69 -33
- package/package.json +1 -1
- package/public/app.js +123 -12
- package/public/index.html +2 -1
- package/public/style.css +22 -0
- package/server.js +39 -33
package/lib/parsers.js
CHANGED
|
@@ -582,8 +582,9 @@ function readMessagesPage(jsonlPath, limit = 10, beforeTimestamp = null) {
|
|
|
582
582
|
};
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
-
function
|
|
585
|
+
function buildSessionDigest(jsonlPath) {
|
|
586
586
|
const map = {};
|
|
587
|
+
const terminated = new Map();
|
|
587
588
|
try {
|
|
588
589
|
const content = readFileSync(jsonlPath, 'utf8');
|
|
589
590
|
const re = /"type":"agent_progress"[^}]*"agentId":"([^"]+)"/;
|
|
@@ -596,6 +597,34 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
596
597
|
const nameByToolUseId = {};
|
|
597
598
|
const descByToolUseId = {};
|
|
598
599
|
for (const line of content.split('\n')) {
|
|
600
|
+
// Terminated-teammate detection: check first since cheap substring guards
|
|
601
|
+
if (line.includes('teammate-message') &&
|
|
602
|
+
(line.includes('teammate_terminated') || line.includes('shutdown_response'))) {
|
|
603
|
+
try {
|
|
604
|
+
const obj = JSON.parse(line);
|
|
605
|
+
if (obj.type === 'user') {
|
|
606
|
+
const text = typeof obj.message?.content === 'string' ? obj.message.content : null;
|
|
607
|
+
if (text) {
|
|
608
|
+
const ts = obj.timestamp || null;
|
|
609
|
+
for (const tmMatch of text.matchAll(/<teammate-message\s+[^>]*teammate_id="([^"]+)"[^>]*>([\s\S]*?)<\/teammate-message>/g)) {
|
|
610
|
+
try {
|
|
611
|
+
const tid = tmMatch[1];
|
|
612
|
+
const body = tmMatch[2].trim();
|
|
613
|
+
const protocol = JSON.parse(body);
|
|
614
|
+
if (protocol.type === 'teammate_terminated') {
|
|
615
|
+
const name = protocol.from || (protocol.message?.match(/^(\S+)\s/)?.[1]) || tid;
|
|
616
|
+
if (name !== 'system') terminated.set(name, ts);
|
|
617
|
+
} else if (protocol.type === 'shutdown_response' && protocol.approve) {
|
|
618
|
+
const name = protocol.from || tid;
|
|
619
|
+
if (name !== 'system') terminated.set(name, ts);
|
|
620
|
+
}
|
|
621
|
+
} catch (_) {}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch (_) {}
|
|
626
|
+
}
|
|
627
|
+
|
|
599
628
|
if (line.includes('"agent_progress"')) {
|
|
600
629
|
const agentMatch = re.exec(line);
|
|
601
630
|
const parentMatch = parentRe.exec(line);
|
|
@@ -657,7 +686,11 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
657
686
|
if (descByToolUseId[key]) entry.description = descByToolUseId[key];
|
|
658
687
|
}
|
|
659
688
|
} catch (_) {}
|
|
660
|
-
return map;
|
|
689
|
+
return { progressMap: map, terminated };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function buildAgentProgressMap(jsonlPath) {
|
|
693
|
+
return buildSessionDigest(jsonlPath).progressMap;
|
|
661
694
|
}
|
|
662
695
|
|
|
663
696
|
function readCompactSummaries(jsonlPath) {
|
|
@@ -699,36 +732,7 @@ function readCompactSummaries(jsonlPath) {
|
|
|
699
732
|
}
|
|
700
733
|
|
|
701
734
|
function findTerminatedTeammates(jsonlPath) {
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
const content = readFileSync(jsonlPath, 'utf8');
|
|
705
|
-
for (const line of content.split('\n')) {
|
|
706
|
-
if (!line.includes('teammate-message')) continue;
|
|
707
|
-
if (!line.includes('teammate_terminated') && !line.includes('shutdown_response')) continue;
|
|
708
|
-
try {
|
|
709
|
-
const obj = JSON.parse(line);
|
|
710
|
-
if (obj.type !== 'user') continue;
|
|
711
|
-
const text = typeof obj.message?.content === 'string' ? obj.message.content : null;
|
|
712
|
-
if (!text) continue;
|
|
713
|
-
const ts = obj.timestamp || null;
|
|
714
|
-
for (const tmMatch of text.matchAll(/<teammate-message\s+[^>]*teammate_id="([^"]+)"[^>]*>([\s\S]*?)<\/teammate-message>/g)) {
|
|
715
|
-
try {
|
|
716
|
-
const tid = tmMatch[1];
|
|
717
|
-
const body = tmMatch[2].trim();
|
|
718
|
-
const protocol = JSON.parse(body);
|
|
719
|
-
if (protocol.type === 'teammate_terminated') {
|
|
720
|
-
const name = protocol.from || (protocol.message?.match(/^(\S+)\s/)?.[1]) || tid;
|
|
721
|
-
if (name !== 'system') terminated.set(name, ts);
|
|
722
|
-
} else if (protocol.type === 'shutdown_response' && protocol.approve) {
|
|
723
|
-
const name = protocol.from || tid;
|
|
724
|
-
if (name !== 'system') terminated.set(name, ts);
|
|
725
|
-
}
|
|
726
|
-
} catch (_) {}
|
|
727
|
-
}
|
|
728
|
-
} catch (_) {}
|
|
729
|
-
}
|
|
730
|
-
} catch (_) {}
|
|
731
|
-
return terminated;
|
|
735
|
+
return buildSessionDigest(jsonlPath).terminated;
|
|
732
736
|
}
|
|
733
737
|
|
|
734
738
|
function extractPromptFromTranscript(jsonlPath) {
|
|
@@ -766,6 +770,36 @@ function extractPromptFromTranscript(jsonlPath) {
|
|
|
766
770
|
return null;
|
|
767
771
|
}
|
|
768
772
|
|
|
773
|
+
function extractModelFromTranscript(jsonlPath) {
|
|
774
|
+
const { openSync, readSync, closeSync } = fs;
|
|
775
|
+
const MAX_READ = 65536;
|
|
776
|
+
const CHUNK = 4096;
|
|
777
|
+
const fd = openSync(jsonlPath, 'r');
|
|
778
|
+
try {
|
|
779
|
+
let accumulated = '';
|
|
780
|
+
const buf = Buffer.alloc(CHUNK);
|
|
781
|
+
while (accumulated.length < MAX_READ) {
|
|
782
|
+
const bytesRead = readSync(fd, buf, 0, CHUNK, null);
|
|
783
|
+
if (bytesRead === 0) break;
|
|
784
|
+
accumulated += buf.toString('utf8', 0, bytesRead);
|
|
785
|
+
let nlIdx;
|
|
786
|
+
while ((nlIdx = accumulated.indexOf('\n')) !== -1) {
|
|
787
|
+
const line = accumulated.slice(0, nlIdx);
|
|
788
|
+
accumulated = accumulated.slice(nlIdx + 1);
|
|
789
|
+
if (!line.trim()) continue;
|
|
790
|
+
try {
|
|
791
|
+
const obj = JSON.parse(line);
|
|
792
|
+
const model = obj.model || (obj.message && obj.message.model);
|
|
793
|
+
if (model) return model;
|
|
794
|
+
} catch (_) {}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
} finally {
|
|
798
|
+
closeSync(fd);
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
769
803
|
module.exports = {
|
|
770
804
|
parseTask,
|
|
771
805
|
parseAgent,
|
|
@@ -777,7 +811,9 @@ module.exports = {
|
|
|
777
811
|
readRecentMessages,
|
|
778
812
|
readMessagesPage,
|
|
779
813
|
buildAgentProgressMap,
|
|
814
|
+
buildSessionDigest,
|
|
780
815
|
readCompactSummaries,
|
|
781
816
|
findTerminatedTeammates,
|
|
782
|
-
extractPromptFromTranscript
|
|
817
|
+
extractPromptFromTranscript,
|
|
818
|
+
extractModelFromTranscript
|
|
783
819
|
};
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -478,9 +478,10 @@ async function fetchTasks(sessionId) {
|
|
|
478
478
|
for (const k of Object.keys(ownerColorCache)) delete ownerColorCache[k];
|
|
479
479
|
for (const k of Object.keys(teamColorMap)) delete teamColorMap[k];
|
|
480
480
|
sessionJustSelected = true;
|
|
481
|
+
resetAgentState();
|
|
481
482
|
updateUrl();
|
|
482
483
|
renderSession();
|
|
483
|
-
|
|
484
|
+
fetchAgents(sessionId);
|
|
484
485
|
if (!agentLogMode) fetchMessages(sessionId);
|
|
485
486
|
} catch (error) {
|
|
486
487
|
console.error('Failed to fetch tasks:', error);
|
|
@@ -497,13 +498,18 @@ const _AGENT_STALE_MS = 5 * 60 * 1000; // kept for reference; no longer used for
|
|
|
497
498
|
const WAITING_TTL_MS = 30 * 60 * 1000;
|
|
498
499
|
const AGENT_LOG_MAX = 8;
|
|
499
500
|
|
|
501
|
+
function resetAgentState() {
|
|
502
|
+
currentAgents = [];
|
|
503
|
+
currentWaiting = null;
|
|
504
|
+
lastAgentsHash = '';
|
|
505
|
+
renderAgentFooter();
|
|
506
|
+
}
|
|
507
|
+
|
|
500
508
|
async function fetchAgents(sessionId) {
|
|
501
509
|
try {
|
|
502
510
|
const res = await fetch(`/api/sessions/${sessionId}/agents`);
|
|
503
511
|
if (!res.ok) {
|
|
504
|
-
|
|
505
|
-
currentWaiting = null;
|
|
506
|
-
renderAgentFooter();
|
|
512
|
+
resetAgentState();
|
|
507
513
|
return;
|
|
508
514
|
}
|
|
509
515
|
const data = await res.json();
|
|
@@ -632,6 +638,17 @@ function toggleMessagePanel() {
|
|
|
632
638
|
updateUrl();
|
|
633
639
|
}
|
|
634
640
|
|
|
641
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
642
|
+
async function openSessionWithBookmarks(sessionId) {
|
|
643
|
+
if (!messagePanelOpen) {
|
|
644
|
+
const panel = document.getElementById('message-panel');
|
|
645
|
+
messagePanelOpen = true;
|
|
646
|
+
panel.classList.add('visible');
|
|
647
|
+
document.getElementById('message-toggle')?.classList.add('active');
|
|
648
|
+
}
|
|
649
|
+
await fetchTasks(sessionId);
|
|
650
|
+
}
|
|
651
|
+
|
|
635
652
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
636
653
|
async function viewAgentLog(agentId) {
|
|
637
654
|
let agent = findAgentById(agentId);
|
|
@@ -1336,6 +1353,10 @@ const MARKETPLACE_SVG =
|
|
|
1336
1353
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>';
|
|
1337
1354
|
const MEMORY_SVG =
|
|
1338
1355
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>';
|
|
1356
|
+
const LINK_SVG_PATHS =
|
|
1357
|
+
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>';
|
|
1358
|
+
const linkSvg = (size) =>
|
|
1359
|
+
`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${LINK_SVG_PATHS}</svg>`;
|
|
1339
1360
|
|
|
1340
1361
|
//#endregion
|
|
1341
1362
|
|
|
@@ -2064,10 +2085,7 @@ async function showAllTasks() {
|
|
|
2064
2085
|
if (agentLogMode) exitAgentLogMode();
|
|
2065
2086
|
currentSessionId = null;
|
|
2066
2087
|
ownerFilter = '';
|
|
2067
|
-
|
|
2068
|
-
currentWaiting = null;
|
|
2069
|
-
lastAgentsHash = '';
|
|
2070
|
-
renderAgentFooter();
|
|
2088
|
+
resetAgentState();
|
|
2071
2089
|
const res = await fetch('/api/tasks/all');
|
|
2072
2090
|
allTasksCache = await res.json();
|
|
2073
2091
|
let tasks = allTasksCache;
|
|
@@ -2241,6 +2259,8 @@ function renderSessions() {
|
|
|
2241
2259
|
const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
|
|
2242
2260
|
const pinTitle = pinState === 'pinned' || pinState === 'sticky' ? 'Unpin' : 'Pin';
|
|
2243
2261
|
const showCtx = !!session.contextStatus;
|
|
2262
|
+
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
2263
|
+
const bookmarksCount = loadPins(session.id).length;
|
|
2244
2264
|
return `
|
|
2245
2265
|
<button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${!session.hasRecentLog && !session.inProgress && !session.hasWaitingForUser ? 'stale' : ''} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
|
|
2246
2266
|
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${escapeHtml(session.id)}')" title="${pinTitle} session">${SESSION_PIN_SVG}</span>
|
|
@@ -2251,9 +2271,11 @@ function renderSessions() {
|
|
|
2251
2271
|
<div class="session-progress">
|
|
2252
2272
|
<span class="session-indicators">
|
|
2253
2273
|
${isTeam ? `<span class="team-badge" title="${memberCount} team members"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
|
|
2254
|
-
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}"
|
|
2274
|
+
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
2255
2275
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
2256
2276
|
${session.hasPlan ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>` : ''}
|
|
2277
|
+
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
2278
|
+
${bookmarksCount > 0 ? `<span class="bookmarks-badge" onclick="event.stopPropagation(); openSessionWithBookmarks('${session.id}')" title="${bookmarksCount} bookmarked message${bookmarksCount > 1 ? 's' : ''}"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>${bookmarksCount}</span>` : ''}
|
|
2257
2279
|
${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
|
|
2258
2280
|
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">📋</span>` : ''}
|
|
2259
2281
|
${session.hasWaitingForUser ? '<span class="agent-badge" title="Waiting for user">❓</span>' : ''}
|
|
@@ -3531,6 +3553,7 @@ function _renderStorageTab() {
|
|
|
3531
3553
|
const tab = document.querySelector('.storage-tab.active')?.dataset.tab || 'sessions';
|
|
3532
3554
|
if (tab === 'sessions') body.innerHTML = _renderStorageSessions();
|
|
3533
3555
|
else if (tab === 'scratchpads') body.innerHTML = _renderStorageScratchpads();
|
|
3556
|
+
else if (tab === 'linked-docs') body.innerHTML = _renderStorageLinkedDocs();
|
|
3534
3557
|
}
|
|
3535
3558
|
|
|
3536
3559
|
function _renderStorageSessions() {
|
|
@@ -3753,6 +3776,87 @@ function _storageDeleteScratchpad(key) {
|
|
|
3753
3776
|
_updateStorageTotal();
|
|
3754
3777
|
}
|
|
3755
3778
|
|
|
3779
|
+
function _renderStorageLinkedDocs() {
|
|
3780
|
+
const entries = [];
|
|
3781
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3782
|
+
const key = localStorage.key(i);
|
|
3783
|
+
if (!key.startsWith(PREVIEW_STORAGE_PREFIX)) continue;
|
|
3784
|
+
try {
|
|
3785
|
+
const arr = JSON.parse(localStorage.getItem(key)) || [];
|
|
3786
|
+
if (Array.isArray(arr) && arr.length) {
|
|
3787
|
+
entries.push({ sessionId: key.slice(PREVIEW_STORAGE_PREFIX.length), paths: arr });
|
|
3788
|
+
}
|
|
3789
|
+
} catch {}
|
|
3790
|
+
}
|
|
3791
|
+
if (!entries.length) return '<div class="storage-empty">No linked documents</div>';
|
|
3792
|
+
|
|
3793
|
+
const byId = new Map(entries.map((e) => [e.sessionId, e]));
|
|
3794
|
+
const { groups, orphans } = _groupByProject(entries.map((e) => e.sessionId));
|
|
3795
|
+
|
|
3796
|
+
function renderDocRow(sessionId, p) {
|
|
3797
|
+
const name = p.split(/[\\/]/).pop();
|
|
3798
|
+
const sid = _escapeForJsAttr(sessionId);
|
|
3799
|
+
const jsPath = _escapeForJsAttr(p);
|
|
3800
|
+
return `<div class="storage-item" style="padding-left:24px;">
|
|
3801
|
+
<span class="storage-item-id" title="${escapeHtml(p)}">${escapeHtml(name)}</span>
|
|
3802
|
+
<div class="storage-item-actions">
|
|
3803
|
+
<button onclick="_storagePreviewLinkedDoc('${jsPath}')">View</button>
|
|
3804
|
+
<button class="danger" onclick="_storageUnlinkDoc('${sid}','${jsPath}')">Unlink</button>
|
|
3805
|
+
</div>
|
|
3806
|
+
</div>`;
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
function renderSessionItem({ id, session }) {
|
|
3810
|
+
const entry = byId.get(id);
|
|
3811
|
+
if (!entry) return '';
|
|
3812
|
+
const eid = escapeHtml(id);
|
|
3813
|
+
const count = entry.paths.length;
|
|
3814
|
+
const header = `<div class="storage-group-header">
|
|
3815
|
+
<span>${_sessionLabel(session, id)} <span class="storage-item-badge">${count} doc${count > 1 ? 's' : ''}</span></span>
|
|
3816
|
+
<div class="storage-item-actions">
|
|
3817
|
+
<button class="danger" onclick="_storageClearLinkedDocs('${eid}')">Clear All</button>
|
|
3818
|
+
</div>
|
|
3819
|
+
</div>`;
|
|
3820
|
+
const rows = entry.paths.map((p) => renderDocRow(id, p)).join('');
|
|
3821
|
+
return header + rows;
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
let html = '';
|
|
3825
|
+
for (const [project, items] of groups) {
|
|
3826
|
+
const count = items.length;
|
|
3827
|
+
html += _renderProjectGroup(
|
|
3828
|
+
escapeHtml(_projectLabel(project)),
|
|
3829
|
+
`${count} session${count > 1 ? 's' : ''}`,
|
|
3830
|
+
items.map(renderSessionItem).join(''),
|
|
3831
|
+
);
|
|
3832
|
+
}
|
|
3833
|
+
if (orphans.length) {
|
|
3834
|
+
html += _renderOrphanGroup(orphans.length, orphans.map(renderSessionItem).join(''));
|
|
3835
|
+
}
|
|
3836
|
+
return html;
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
function _storagePreviewLinkedDoc(path) {
|
|
3840
|
+
closeStorageManager();
|
|
3841
|
+
openPreviewByPath(path);
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
function _storageUnlinkDoc(sessionId, path) {
|
|
3845
|
+
removeSessionPreviewPath(sessionId, path);
|
|
3846
|
+
if (sessionId === _infoModalSessionId) refreshInfoModalLinkedDocs();
|
|
3847
|
+
renderSessions();
|
|
3848
|
+
_renderStorageTab();
|
|
3849
|
+
_updateStorageTotal();
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
function _storageClearLinkedDocs(sessionId) {
|
|
3853
|
+
localStorage.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
|
|
3854
|
+
if (sessionId === _infoModalSessionId) refreshInfoModalLinkedDocs();
|
|
3855
|
+
renderSessions();
|
|
3856
|
+
_renderStorageTab();
|
|
3857
|
+
_updateStorageTotal();
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3756
3860
|
function _findOrphanedKeys() {
|
|
3757
3861
|
const known = _getKnownSessionIds();
|
|
3758
3862
|
if (!known.size) return [];
|
|
@@ -3765,6 +3869,8 @@ function _findOrphanedKeys() {
|
|
|
3765
3869
|
if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
|
|
3766
3870
|
} else if (key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:')) {
|
|
3767
3871
|
if (!known.has(key.slice('scratchpad-'.length))) orphaned.push(key);
|
|
3872
|
+
} else if (key.startsWith(PREVIEW_STORAGE_PREFIX)) {
|
|
3873
|
+
if (!known.has(key.slice(PREVIEW_STORAGE_PREFIX.length))) orphaned.push(key);
|
|
3768
3874
|
}
|
|
3769
3875
|
}
|
|
3770
3876
|
return orphaned;
|
|
@@ -4136,6 +4242,7 @@ function togglePreviewSessionLink() {
|
|
|
4136
4242
|
if (_infoModalSessionId === currentSessionId) {
|
|
4137
4243
|
refreshInfoModalLinkedDocs();
|
|
4138
4244
|
}
|
|
4245
|
+
renderSessions();
|
|
4139
4246
|
}
|
|
4140
4247
|
|
|
4141
4248
|
function refreshInfoModalLinkedDocs() {
|
|
@@ -4212,11 +4319,11 @@ function handleSessionOpenEvent(data) {
|
|
|
4212
4319
|
fetchTasks(id);
|
|
4213
4320
|
}
|
|
4214
4321
|
|
|
4215
|
-
function handlePreviewOpenEvent(data) {
|
|
4322
|
+
async function handlePreviewOpenEvent(data) {
|
|
4216
4323
|
const { path: filePath, content, sessionId } = data;
|
|
4217
4324
|
if (sessionId && sessionId !== currentSessionId) {
|
|
4218
4325
|
if (sessions.find((s) => s.id === sessionId)) {
|
|
4219
|
-
fetchTasks(sessionId);
|
|
4326
|
+
await fetchTasks(sessionId);
|
|
4220
4327
|
} else {
|
|
4221
4328
|
showToast(`Preview received for unknown session ${sessionId.slice(0, 8)}`);
|
|
4222
4329
|
}
|
|
@@ -4234,7 +4341,11 @@ function renderLinkedDocsHtml(sessionId) {
|
|
|
4234
4341
|
})
|
|
4235
4342
|
.join(', ');
|
|
4236
4343
|
return `<div class="linked-docs-section" style="margin-bottom:16px;font-size:12px;">
|
|
4237
|
-
<div style="font-size:11px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;">
|
|
4344
|
+
<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;">
|
|
4345
|
+
${linkSvg(12)}
|
|
4346
|
+
<span>Linked documents</span>
|
|
4347
|
+
<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>
|
|
4348
|
+
</div>
|
|
4238
4349
|
<div>${items}</div>
|
|
4239
4350
|
</div>`;
|
|
4240
4351
|
}
|
package/public/index.html
CHANGED
|
@@ -544,7 +544,7 @@
|
|
|
544
544
|
</div>
|
|
545
545
|
<div id="team-modal-body" class="modal-body"></div>
|
|
546
546
|
<div class="modal-footer">
|
|
547
|
-
<button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId)">Dismiss</button>
|
|
547
|
+
<button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId); closeTeamModal()">Dismiss</button>
|
|
548
548
|
<button class="btn btn-primary" onclick="closeTeamModal()">Close</button>
|
|
549
549
|
</div>
|
|
550
550
|
</div>
|
|
@@ -671,6 +671,7 @@
|
|
|
671
671
|
<div class="storage-tabs">
|
|
672
672
|
<button class="storage-tab active" data-tab="sessions" onclick="switchStorageTab('sessions')">Sessions</button>
|
|
673
673
|
<button class="storage-tab" data-tab="scratchpads" onclick="switchStorageTab('scratchpads')">Scratchpads</button>
|
|
674
|
+
<button class="storage-tab" data-tab="linked-docs" onclick="switchStorageTab('linked-docs')">Linked Docs</button>
|
|
674
675
|
</div>
|
|
675
676
|
<div class="modal-body" id="storage-modal-body" style="overflow-y:auto;min-height:200px;max-height:60vh;padding-top:16px;padding-right:8px;"></div>
|
|
676
677
|
<div class="modal-footer">
|
package/public/style.css
CHANGED
|
@@ -2498,6 +2498,28 @@ body::before {
|
|
|
2498
2498
|
cursor: default;
|
|
2499
2499
|
}
|
|
2500
2500
|
|
|
2501
|
+
.linked-docs-badge,
|
|
2502
|
+
.bookmarks-badge {
|
|
2503
|
+
display: inline-flex;
|
|
2504
|
+
align-items: center;
|
|
2505
|
+
gap: 2px;
|
|
2506
|
+
font-size: 10px;
|
|
2507
|
+
padding: 2px 6px;
|
|
2508
|
+
background: var(--bg-elevated);
|
|
2509
|
+
border: 1px solid var(--border);
|
|
2510
|
+
border-radius: 10px;
|
|
2511
|
+
color: var(--text-secondary);
|
|
2512
|
+
cursor: pointer;
|
|
2513
|
+
flex-shrink: 0;
|
|
2514
|
+
line-height: 1;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
.linked-docs-badge:hover,
|
|
2518
|
+
.bookmarks-badge:hover {
|
|
2519
|
+
border-color: var(--accent);
|
|
2520
|
+
color: var(--text-primary);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2501
2523
|
/* #endregion */
|
|
2502
2524
|
|
|
2503
2525
|
/* #region PERMISSION_PENDING */
|
package/server.js
CHANGED
|
@@ -15,9 +15,11 @@ const {
|
|
|
15
15
|
readMessagesPage: _readMessagesPageUncached,
|
|
16
16
|
readSessionInfoFromJsonl,
|
|
17
17
|
buildAgentProgressMap,
|
|
18
|
+
buildSessionDigest,
|
|
18
19
|
readCompactSummaries,
|
|
19
20
|
findTerminatedTeammates,
|
|
20
|
-
extractPromptFromTranscript
|
|
21
|
+
extractPromptFromTranscript,
|
|
22
|
+
extractModelFromTranscript
|
|
21
23
|
} = require('./lib/parsers');
|
|
22
24
|
|
|
23
25
|
if (process.argv.includes("--install") || process.argv.includes("--uninstall")) {
|
|
@@ -80,7 +82,10 @@ const WAITING_RESOLVE_GRACE_MS = 15000;
|
|
|
80
82
|
|
|
81
83
|
function persistAgent(dir, agent) {
|
|
82
84
|
const file = path.join(dir, agent.agentId + '.json');
|
|
83
|
-
|
|
85
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
86
|
+
fs.writeFile(tmp, JSON.stringify(agent), 'utf8')
|
|
87
|
+
.then(() => fs.rename(tmp, file))
|
|
88
|
+
.catch(() => { fs.unlink(tmp).catch(() => {}); });
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
function checkWaitingForUser(agentDir, logMtime) {
|
|
@@ -210,8 +215,6 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|
|
210
215
|
const messageCache = new Map();
|
|
211
216
|
const MESSAGE_CACHE_TTL = 5000;
|
|
212
217
|
const MAX_CACHE_ENTRIES = 200;
|
|
213
|
-
const progressMapCache = new Map();
|
|
214
|
-
const terminatedCache = new Map();
|
|
215
218
|
const compactSummaryCache = new Map();
|
|
216
219
|
const taskCountsCache = new Map();
|
|
217
220
|
const contextStatusCache = new Map();
|
|
@@ -325,12 +328,17 @@ function cachedByMtime(cache, cacheKey, filePath, loadFn, fallback) {
|
|
|
325
328
|
} catch (_) { return fallback; }
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
const sessionDigestCache = new Map();
|
|
332
|
+
function getSessionDigest(jsonlPath) {
|
|
333
|
+
return cachedByMtime(sessionDigestCache, jsonlPath, jsonlPath, () => buildSessionDigest(jsonlPath), { progressMap: {}, terminated: new Map() });
|
|
334
|
+
}
|
|
335
|
+
|
|
328
336
|
function getProgressMap(jsonlPath) {
|
|
329
|
-
return
|
|
337
|
+
return getSessionDigest(jsonlPath).progressMap;
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
function getTerminatedTeammates(jsonlPath) {
|
|
333
|
-
return
|
|
341
|
+
return getSessionDigest(jsonlPath).terminated;
|
|
334
342
|
}
|
|
335
343
|
|
|
336
344
|
function readRecentMessages(jsonlPath, limit = 10) {
|
|
@@ -965,10 +973,14 @@ app.post('/api/open-folder', (req, res) => {
|
|
|
965
973
|
}
|
|
966
974
|
});
|
|
967
975
|
|
|
968
|
-
// API: Open
|
|
976
|
+
// API: Open file in editor — either an existing path ({ file }) or content as a temp file ({ content, title })
|
|
969
977
|
app.post('/api/open-in-editor', (req, res) => {
|
|
970
978
|
try {
|
|
971
|
-
const { content, title } = req.body;
|
|
979
|
+
const { content, title, file } = req.body;
|
|
980
|
+
if (file) {
|
|
981
|
+
openInEditor(file);
|
|
982
|
+
return res.json({ success: true, path: file });
|
|
983
|
+
}
|
|
972
984
|
if (!content) return res.status(400).json({ error: 'No content provided' });
|
|
973
985
|
|
|
974
986
|
const safeName = (title || 'message').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50);
|
|
@@ -1056,14 +1068,11 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1056
1068
|
} catch (_) {}
|
|
1057
1069
|
}
|
|
1058
1070
|
|
|
1059
|
-
|
|
1060
|
-
agent.prompt = prompt;
|
|
1061
|
-
persistAgent(agentDir, agent);
|
|
1062
|
-
}
|
|
1071
|
+
const dirty = new Set();
|
|
1063
1072
|
|
|
1064
|
-
const agentsNeedingPrompt = agents.filter(a => !a.prompt);
|
|
1065
|
-
const agentsNeedingName = agents.filter(a => !a.agentName);
|
|
1066
|
-
const agentsNeedingDesc = agents.filter(a => !a.description);
|
|
1073
|
+
const agentsNeedingPrompt = agents.filter(a => !a.prompt && !a.promptUnavailable);
|
|
1074
|
+
const agentsNeedingName = agents.filter(a => !a.agentName && !a.agentNameUnavailable);
|
|
1075
|
+
const agentsNeedingDesc = agents.filter(a => !a.description && !a.descriptionUnavailable);
|
|
1067
1076
|
if ((agentsNeedingPrompt.length || agentsNeedingName.length || agentsNeedingDesc.length) && meta.jsonlPath) {
|
|
1068
1077
|
let byAgentId = {};
|
|
1069
1078
|
let nameByAgentId = {};
|
|
@@ -1079,37 +1088,34 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1079
1088
|
for (const agent of agentsNeedingPrompt) {
|
|
1080
1089
|
const prompt = byAgentId[agent.agentId]
|
|
1081
1090
|
|| (() => { try { return extractPromptFromTranscript(subagentJsonlPath(meta, agent.agentId)); } catch (_) { return null; } })();
|
|
1082
|
-
if (prompt)
|
|
1091
|
+
if (prompt) agent.prompt = prompt;
|
|
1092
|
+
else agent.promptUnavailable = true;
|
|
1093
|
+
dirty.add(agent);
|
|
1083
1094
|
}
|
|
1084
1095
|
for (const agent of agentsNeedingName) {
|
|
1085
1096
|
if (nameByAgentId[agent.agentId]) agent.agentName = nameByAgentId[agent.agentId];
|
|
1097
|
+
else agent.agentNameUnavailable = true;
|
|
1098
|
+
dirty.add(agent);
|
|
1086
1099
|
}
|
|
1087
1100
|
for (const agent of agentsNeedingDesc) {
|
|
1088
1101
|
if (descByAgentId[agent.agentId]) agent.description = descByAgentId[agent.agentId];
|
|
1102
|
+
else agent.descriptionUnavailable = true;
|
|
1103
|
+
dirty.add(agent);
|
|
1089
1104
|
}
|
|
1090
1105
|
}
|
|
1091
1106
|
|
|
1092
|
-
const agentsNeedingModel = agents.filter(a => !a.model);
|
|
1107
|
+
const agentsNeedingModel = agents.filter(a => !a.model && !a.modelUnavailable);
|
|
1093
1108
|
if (agentsNeedingModel.length && meta.jsonlPath) {
|
|
1094
1109
|
for (const agent of agentsNeedingModel) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
try {
|
|
1101
|
-
const obj = JSON.parse(line);
|
|
1102
|
-
const model = obj.model || (obj.message && obj.message.model);
|
|
1103
|
-
if (model) {
|
|
1104
|
-
agent.model = model;
|
|
1105
|
-
persistAgent(agentDir, agent);
|
|
1106
|
-
break;
|
|
1107
|
-
}
|
|
1108
|
-
} catch (_) {}
|
|
1109
|
-
}
|
|
1110
|
-
} catch (_) {}
|
|
1110
|
+
let model = null;
|
|
1111
|
+
try { model = extractModelFromTranscript(subagentJsonlPath(meta, agent.agentId)); } catch (_) {}
|
|
1112
|
+
if (model) agent.model = model;
|
|
1113
|
+
else agent.modelUnavailable = true;
|
|
1114
|
+
dirty.add(agent);
|
|
1111
1115
|
}
|
|
1112
1116
|
}
|
|
1117
|
+
|
|
1118
|
+
for (const agent of dirty) persistAgent(agentDir, agent);
|
|
1113
1119
|
const teamColors = {};
|
|
1114
1120
|
if (teamConfig?.members) {
|
|
1115
1121
|
for (const m of teamConfig.members) {
|