claude-code-kanban 2.2.0-rc.9 → 2.2.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 +50 -2
- package/package.json +1 -1
- package/public/app.js +498 -149
- package/public/index.html +11 -5
- package/public/style.css +96 -4
- package/server.js +30 -13
package/public/app.js
CHANGED
|
@@ -30,9 +30,23 @@ let selectedSessionKbId = null;
|
|
|
30
30
|
let sessionJustSelected = false;
|
|
31
31
|
let agentLogMode = null;
|
|
32
32
|
let agentLogSSE = null;
|
|
33
|
+
let msgHasMore = false;
|
|
34
|
+
let msgLoadingMore = false;
|
|
35
|
+
let msgUserScrolledUp = false;
|
|
36
|
+
const MSG_MAX_LOADED = 200;
|
|
33
37
|
let currentProjectPath = null;
|
|
34
38
|
let currentProjectSessionIds = [];
|
|
35
39
|
|
|
40
|
+
function resetMessageScrollState() {
|
|
41
|
+
msgUserScrolledUp = false;
|
|
42
|
+
msgHasMore = false;
|
|
43
|
+
msgLoadingMore = false;
|
|
44
|
+
currentMessages = [];
|
|
45
|
+
lastMessagesHash = '';
|
|
46
|
+
const btn = document.getElementById('msg-jump-latest');
|
|
47
|
+
if (btn) btn.style.display = 'none';
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
function getUrlState() {
|
|
37
51
|
const params = new URLSearchParams(window.location.search);
|
|
38
52
|
return {
|
|
@@ -77,6 +91,7 @@ function resetState() {
|
|
|
77
91
|
currentSessionId = null;
|
|
78
92
|
currentProjectPath = null;
|
|
79
93
|
currentProjectSessionIds = [];
|
|
94
|
+
resetMessageScrollState();
|
|
80
95
|
const searchInput = document.getElementById('search-input');
|
|
81
96
|
if (searchInput) searchInput.value = '';
|
|
82
97
|
document.getElementById('search-clear-btn')?.classList.remove('visible');
|
|
@@ -116,6 +131,7 @@ async function fetchSessions() {
|
|
|
116
131
|
try {
|
|
117
132
|
const allPinnedIds = new Set([...pinnedSessionIds, ...stickySessionIds]);
|
|
118
133
|
if (revealedPlanSessionId) allPinnedIds.add(revealedPlanSessionId);
|
|
134
|
+
if (revealedStorageSessionId) allPinnedIds.add(revealedStorageSessionId);
|
|
119
135
|
const pinnedParam = allPinnedIds.size > 0 ? `&pinned=${[...allPinnedIds].join(',')}` : '';
|
|
120
136
|
const [newSessions, newTasks] = await Promise.all([
|
|
121
137
|
fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}`).then((r) => r.json()),
|
|
@@ -342,7 +358,7 @@ function fuzzyMatch(text, query) {
|
|
|
342
358
|
if (text.includes(query)) return true;
|
|
343
359
|
|
|
344
360
|
// Split by common delimiters to search in individual words
|
|
345
|
-
const words = text.split(/[\s\-_
|
|
361
|
+
const words = text.split(/[\s\-_/.\\]+/);
|
|
346
362
|
|
|
347
363
|
// Check if query matches start of any word
|
|
348
364
|
for (const word of words) {
|
|
@@ -440,10 +456,13 @@ async function fetchTasks(sessionId) {
|
|
|
440
456
|
if (revealedPlanSessionId && sessionId !== revealedPlanSessionId) {
|
|
441
457
|
revealedPlanSessionId = null;
|
|
442
458
|
}
|
|
459
|
+
if (revealedStorageSessionId && sessionId !== revealedStorageSessionId) {
|
|
460
|
+
revealedStorageSessionId = null;
|
|
461
|
+
}
|
|
443
462
|
currentSessionId = sessionId;
|
|
444
463
|
currentPins = loadPins(sessionId);
|
|
445
464
|
ownerFilter = '';
|
|
446
|
-
|
|
465
|
+
resetMessageScrollState();
|
|
447
466
|
for (const k of Object.keys(ownerColorCache)) delete ownerColorCache[k];
|
|
448
467
|
for (const k of Object.keys(teamColorMap)) delete teamColorMap[k];
|
|
449
468
|
sessionJustSelected = true;
|
|
@@ -613,6 +632,7 @@ async function viewAgentLog(agentId) {
|
|
|
613
632
|
const shortId = resolvedId.length > 8 ? resolvedId.slice(0, 8) : resolvedId;
|
|
614
633
|
const agentSessionId = agent._sourceSessionId || currentSessionId;
|
|
615
634
|
agentLogMode = { agentId: resolvedId, sessionId: agentSessionId, agentType: agent.type || 'unknown' };
|
|
635
|
+
resetMessageScrollState();
|
|
616
636
|
closeAgentModal();
|
|
617
637
|
document.getElementById('message-toggle')?.style.removeProperty('display');
|
|
618
638
|
if (!messagePanelOpen) toggleMessagePanel();
|
|
@@ -650,7 +670,7 @@ function exitAgentLogMode() {
|
|
|
650
670
|
}
|
|
651
671
|
const header = document.querySelector('.message-panel-header h3');
|
|
652
672
|
if (header) header.textContent = 'Session Log';
|
|
653
|
-
|
|
673
|
+
resetMessageScrollState();
|
|
654
674
|
if (currentSessionId) fetchMessages(currentSessionId);
|
|
655
675
|
}
|
|
656
676
|
|
|
@@ -682,9 +702,6 @@ async function fetchMessages(sessionId) {
|
|
|
682
702
|
const res = await fetch(`/api/sessions/${sessionId}/messages?limit=15`);
|
|
683
703
|
if (!res.ok) return;
|
|
684
704
|
const data = await res.json();
|
|
685
|
-
const hash = JSON.stringify(data.messages);
|
|
686
|
-
if (hash === lastMessagesHash) return;
|
|
687
|
-
lastMessagesHash = hash;
|
|
688
705
|
let agentEnriched = false;
|
|
689
706
|
for (const m of data.messages) {
|
|
690
707
|
if (m.agentId && m.agentPrompt) {
|
|
@@ -697,16 +714,75 @@ async function fetchMessages(sessionId) {
|
|
|
697
714
|
}
|
|
698
715
|
if (agentEnriched) renderAgentFooter();
|
|
699
716
|
if (agentLogMode) return;
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
717
|
+
|
|
718
|
+
if (!msgUserScrolledUp) {
|
|
719
|
+
const hash = JSON.stringify(data.messages);
|
|
720
|
+
if (hash === lastMessagesHash) return;
|
|
721
|
+
lastMessagesHash = hash;
|
|
722
|
+
msgHasMore = data.hasMore !== false;
|
|
723
|
+
currentMessages = data.messages;
|
|
724
|
+
if (messagePanelOpen) renderMessages(data.messages);
|
|
725
|
+
} else {
|
|
726
|
+
if (data.messages.length && currentMessages.length) {
|
|
727
|
+
const lastKnown = currentMessages[currentMessages.length - 1].timestamp;
|
|
728
|
+
const newMsgs = data.messages.filter((m) => m.timestamp > lastKnown);
|
|
729
|
+
if (newMsgs.length) {
|
|
730
|
+
currentMessages = [...currentMessages, ...newMsgs];
|
|
731
|
+
if (currentMessages.length > MSG_MAX_LOADED) {
|
|
732
|
+
currentMessages = currentMessages.slice(-MSG_MAX_LOADED);
|
|
733
|
+
msgHasMore = true;
|
|
734
|
+
}
|
|
735
|
+
if (messagePanelOpen) renderMessages(currentMessages);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (msgDetailFollowLatest && currentMessages.length) {
|
|
741
|
+
showMsgDetail(currentMessages.length - 1);
|
|
704
742
|
}
|
|
705
743
|
} catch (e) {
|
|
706
744
|
console.error('[fetchMessages]', e);
|
|
707
745
|
}
|
|
708
746
|
}
|
|
709
747
|
|
|
748
|
+
async function loadOlderMessages() {
|
|
749
|
+
if (agentLogMode || msgLoadingMore || !msgHasMore || !currentMessages.length) return;
|
|
750
|
+
msgLoadingMore = true;
|
|
751
|
+
const container = document.getElementById('message-panel-content');
|
|
752
|
+
const loader = document.createElement('div');
|
|
753
|
+
loader.className = 'msg-loading-more';
|
|
754
|
+
loader.textContent = 'Loading...';
|
|
755
|
+
container.prepend(loader);
|
|
756
|
+
try {
|
|
757
|
+
const before = currentMessages[0].timestamp;
|
|
758
|
+
const res = await fetch(`/api/sessions/${currentSessionId}/messages?limit=15&before=${encodeURIComponent(before)}`);
|
|
759
|
+
if (!res.ok) return;
|
|
760
|
+
const data = await res.json();
|
|
761
|
+
msgHasMore = data.hasMore && data.messages.length > 0;
|
|
762
|
+
if (data.messages.length) {
|
|
763
|
+
loader.remove();
|
|
764
|
+
const prevHeight = container.scrollHeight;
|
|
765
|
+
currentMessages = [...data.messages, ...currentMessages];
|
|
766
|
+
if (currentMessages.length > MSG_MAX_LOADED) {
|
|
767
|
+
currentMessages = currentMessages.slice(0, MSG_MAX_LOADED);
|
|
768
|
+
}
|
|
769
|
+
renderMessages(currentMessages);
|
|
770
|
+
container.scrollTop = container.scrollHeight - prevHeight;
|
|
771
|
+
}
|
|
772
|
+
} catch (e) {
|
|
773
|
+
console.error('[loadOlderMessages]', e);
|
|
774
|
+
} finally {
|
|
775
|
+
if (loader.parentNode) loader.remove();
|
|
776
|
+
requestAnimationFrame(() => {
|
|
777
|
+
msgLoadingMore = false;
|
|
778
|
+
// Chain auto-load if content still doesn't overflow
|
|
779
|
+
if (msgHasMore && currentMessages.length < MSG_MAX_LOADED && container.scrollHeight <= container.clientHeight) {
|
|
780
|
+
loadOlderMessages();
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
710
786
|
function parseCommandMessage(text) {
|
|
711
787
|
const nameMatch = text.match(/<command-name>([^<]+)<\/command-name>/);
|
|
712
788
|
if (nameMatch) return nameMatch[1].trim();
|
|
@@ -752,10 +828,10 @@ function renderPinnedSection() {
|
|
|
752
828
|
<div class="msg-body"><div class="msg-text">${escapeHtml(cleanMessageText(p.text || ''))}</div><div class="msg-time">${formatDate(p.timestamp)}</div></div>${unpin}
|
|
753
829
|
</div>`;
|
|
754
830
|
} else if (p.type === 'tool_use') {
|
|
755
|
-
const toolDetail = p.
|
|
756
|
-
const pinnedAgentLogBtn =
|
|
831
|
+
const toolDetail = getToolDetail(p.tool, p.params, p.detail);
|
|
832
|
+
const pinnedAgentLogBtn = resolveAgentLogBtn(p);
|
|
757
833
|
return `<div class="msg-item msg-tool" ${click}>
|
|
758
|
-
${
|
|
834
|
+
${getToolIcon(p.tool)}
|
|
759
835
|
<div class="msg-body"><div class="msg-text">${escapeHtml(p.tool || '')}${toolDetail}</div><div class="msg-time">${formatDate(p.timestamp)}</div></div>${pinnedAgentLogBtn}${unpin}
|
|
760
836
|
</div>`;
|
|
761
837
|
} else if (p.type === 'agent') {
|
|
@@ -786,96 +862,163 @@ function renderPinnedSection() {
|
|
|
786
862
|
</div>`;
|
|
787
863
|
}
|
|
788
864
|
|
|
789
|
-
function
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
865
|
+
function resolveAgentLogBtn(m) {
|
|
866
|
+
if (m.tool === 'Agent' && m.agentId) return agentLogButton(m.agentId);
|
|
867
|
+
if (m.tool === 'SendMessage' && m.params?.to) {
|
|
868
|
+
const recipient = currentAgents.find((a) => (a.type || a.name) === m.params.to);
|
|
869
|
+
if (recipient) return agentLogButton(recipient.agentId);
|
|
870
|
+
}
|
|
871
|
+
return '';
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function toolGroupKey(m) {
|
|
875
|
+
return m.type === 'tool_use' ? `${m.tool}\0${m.detail || ''}` : null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function renderToolItem(m, i, compact) {
|
|
879
|
+
const toolDetail = getToolDetail(m.tool, m.params, m.detail);
|
|
880
|
+
const agentLink =
|
|
881
|
+
m.tool === 'Agent' && m.agentId
|
|
882
|
+
? ` <span class="msg-agent-link" title="View agent" onclick="event.stopPropagation();showAgentModal('${escapeHtml(m.agentId)}')">⇗</span>`
|
|
883
|
+
: '';
|
|
884
|
+
const agentLogBtn = resolveAgentLogBtn(m);
|
|
885
|
+
const recipientColor = m.tool === 'SendMessage' && m.params?.to ? resolveNamedColor(teamColorMap[m.params.to]) : null;
|
|
886
|
+
const borderStyle = recipientColor ? `border-left:3px solid ${recipientColor.color};` : '';
|
|
887
|
+
const compactClass = compact ? ' msg-tool-grouped' : '';
|
|
888
|
+
const combinedStyle = `style="${borderStyle}cursor:pointer"`;
|
|
889
|
+
const itemClickAttr =
|
|
890
|
+
m.tool === 'Agent' && m.agentId
|
|
891
|
+
? `onclick="showAgentModal('${escapeHtml(m.agentId)}')" ${combinedStyle}`
|
|
892
|
+
: `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" ${combinedStyle}`;
|
|
893
|
+
const pinBtn = renderMsgPinBtn(m, i);
|
|
894
|
+
return `<div class="msg-item msg-tool${compactClass}" ${itemClickAttr}>
|
|
895
|
+
${getToolIcon(m.tool)}
|
|
896
|
+
<div class="msg-body"><div class="msg-text">${escapeHtml(m.tool)}${toolDetail}${agentLink}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${agentLogBtn}${pinBtn}
|
|
897
|
+
</div>`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function renderMessageList(messages) {
|
|
901
|
+
const parts = [];
|
|
902
|
+
let i = 0;
|
|
903
|
+
while (i < messages.length) {
|
|
904
|
+
const m = messages[i];
|
|
905
|
+
|
|
906
|
+
if (m.type === 'tool_use') {
|
|
907
|
+
const key = toolGroupKey(m);
|
|
908
|
+
let runEnd = i + 1;
|
|
909
|
+
while (runEnd < messages.length && toolGroupKey(messages[runEnd]) === key) runEnd++;
|
|
910
|
+
const count = runEnd - i;
|
|
911
|
+
|
|
912
|
+
if (count >= 2) {
|
|
913
|
+
const first = messages[i];
|
|
914
|
+
const last = messages[runEnd - 1];
|
|
915
|
+
const toolDetail = getToolDetail(first.tool, first.params, first.detail);
|
|
916
|
+
const gid = `tool-group-${i}`;
|
|
917
|
+
const timeRange = `${formatDate(first.timestamp)} – ${formatDate(last.timestamp)}`;
|
|
918
|
+
const grpAgentLogBtn = resolveAgentLogBtn(first);
|
|
919
|
+
const grpPinBtn = renderMsgPinBtn(first, i);
|
|
920
|
+
parts.push(`<div class="msg-tool-group">
|
|
921
|
+
<div class="msg-item msg-tool msg-tool-group-header" onclick="toggleToolGroup('${gid}')" style="cursor:pointer">
|
|
922
|
+
${getToolIcon(first.tool)}
|
|
923
|
+
<div class="msg-body"><div class="msg-text">${escapeHtml(first.tool)}${toolDetail}<span class="tool-count-badge">×${count}</span></div><div class="msg-time">${timeRange}</div></div>${grpAgentLogBtn}${grpPinBtn}
|
|
924
|
+
</div>
|
|
925
|
+
<div class="msg-tool-group-items" id="${gid}">${Array.from({ length: count }, (_, j) => renderToolItem(messages[i + j], i + j, true)).join('')}</div>
|
|
926
|
+
</div>`);
|
|
927
|
+
i = runEnd;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
parts.push(renderToolItem(m, i, false));
|
|
932
|
+
i++;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const clickable = `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" style="cursor:pointer"`;
|
|
937
|
+
const pinBtn = renderMsgPinBtn(m, i);
|
|
938
|
+
if (m.type === 'user') {
|
|
939
|
+
if (m.systemLabel) {
|
|
940
|
+
parts.push(`<div class="msg-item msg-system" ${clickable}>
|
|
941
|
+
${MSG_ICON_SYSTEM}
|
|
942
|
+
<div class="msg-body"><div class="msg-text"><code>${escapeHtml(m.systemLabel)}</code></div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${pinBtn}
|
|
943
|
+
</div>`);
|
|
944
|
+
} else {
|
|
808
945
|
const cmd = parseCommandMessage(m.text);
|
|
809
946
|
const displayText = cmd ? cmd : escapeHtml(cleanMessageText(m.text));
|
|
810
947
|
const isCmd = !!cmd;
|
|
811
|
-
|
|
948
|
+
parts.push(`<div class="msg-item msg-user${isCmd ? ' msg-cmd' : ''}" ${clickable}>
|
|
812
949
|
${MSG_ICON_USER}
|
|
813
950
|
<div class="msg-body"><div class="msg-text">${isCmd ? `<code>${escapeHtml(displayText)}</code>` : displayText}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${pinBtn}
|
|
814
|
-
</div
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
</div
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
<div class="msg-body"><div class="msg-text">${escapeHtml(m.tool)}${toolDetail}${agentLink}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${agentLogBtn}${pinBtn}
|
|
844
|
-
</div>`;
|
|
845
|
-
} else if (m.type === 'teammate') {
|
|
846
|
-
if (m.teammateId && m.color && !teamColorMap[m.teammateId]) teamColorMap[m.teammateId] = m.color;
|
|
847
|
-
const tmColor = m.color ? resolveNamedColor(m.color)?.color || m.color : '';
|
|
848
|
-
const nameSpan = `<span class="teammate-name" style="${tmColor ? `color:${escapeHtml(tmColor)}` : ''}">${escapeHtml(m.teammateId || 'teammate')}</span>`;
|
|
849
|
-
let tmLookupName = m.teammateId;
|
|
850
|
-
if (m.teammateId === 'system' && m.protocolType === 'teammate_terminated' && m.protocolData?.message) {
|
|
851
|
-
const shutMatch = m.protocolData.message.match(/^(.+?) has shut down/);
|
|
852
|
-
if (shutMatch) tmLookupName = shutMatch[1];
|
|
853
|
-
}
|
|
854
|
-
const tmAgent = tmLookupName ? currentAgents.find((a) => (a.type || a.name) === tmLookupName) : null;
|
|
855
|
-
const tmLogBtn = tmAgent ? agentLogButton(tmAgent.agentId) : '';
|
|
856
|
-
if (m.isIdle) {
|
|
857
|
-
return `<div class="msg-item msg-teammate msg-idle" ${clickable}>
|
|
858
|
-
${MSG_ICON_IDLE}
|
|
859
|
-
<div class="msg-body"><div class="msg-text">${nameSpan} <span class="idle-label">${escapeHtml(m.protocolLabel || 'idle')}</span></div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}
|
|
860
|
-
</div>`;
|
|
861
|
-
}
|
|
862
|
-
if (m.isProtocol) {
|
|
863
|
-
return `<div class="msg-item msg-teammate msg-protocol" ${clickable}>
|
|
864
|
-
${MSG_ICON_TEAMMATE}
|
|
865
|
-
<div class="msg-body"><div class="msg-text">${nameSpan} <span class="protocol-label">${escapeHtml(m.protocolLabel || m.protocolType)}</span></div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}
|
|
866
|
-
</div>`;
|
|
867
|
-
}
|
|
951
|
+
</div>`);
|
|
952
|
+
}
|
|
953
|
+
} else if (m.type === 'assistant') {
|
|
954
|
+
parts.push(`<div class="msg-item msg-assistant" ${clickable}>
|
|
955
|
+
${MSG_ICON_ASSISTANT}
|
|
956
|
+
<div class="msg-body"><div class="msg-text">${escapeHtml(cleanMessageText(m.text))}</div><div class="msg-time">${m.model ? `${escapeHtml(m.model)} · ` : ''}${formatDate(m.timestamp)}</div></div>${pinBtn}
|
|
957
|
+
</div>`);
|
|
958
|
+
} else if (m.type === 'teammate') {
|
|
959
|
+
if (m.teammateId && m.color && !teamColorMap[m.teammateId]) teamColorMap[m.teammateId] = m.color;
|
|
960
|
+
const tmColor = m.color ? resolveNamedColor(m.color)?.color || m.color : '';
|
|
961
|
+
const nameSpan = `<span class="teammate-name" style="${tmColor ? `color:${escapeHtml(tmColor)}` : ''}">${escapeHtml(m.teammateId || 'teammate')}</span>`;
|
|
962
|
+
let tmLookupName = m.teammateId;
|
|
963
|
+
if (m.teammateId === 'system' && m.protocolType === 'teammate_terminated' && m.protocolData?.message) {
|
|
964
|
+
const shutMatch = m.protocolData.message.match(/^(.+?) has shut down/);
|
|
965
|
+
if (shutMatch) tmLookupName = shutMatch[1];
|
|
966
|
+
}
|
|
967
|
+
const tmAgent = tmLookupName ? currentAgents.find((a) => (a.type || a.name) === tmLookupName) : null;
|
|
968
|
+
const tmLogBtn = tmAgent ? agentLogButton(tmAgent.agentId) : '';
|
|
969
|
+
if (m.isIdle) {
|
|
970
|
+
parts.push(`<div class="msg-item msg-teammate msg-idle" ${clickable}>
|
|
971
|
+
${MSG_ICON_IDLE}
|
|
972
|
+
<div class="msg-body"><div class="msg-text">${nameSpan} <span class="idle-label">${escapeHtml(m.protocolLabel || 'idle')}</span></div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}
|
|
973
|
+
</div>`);
|
|
974
|
+
} else if (m.isProtocol) {
|
|
975
|
+
parts.push(`<div class="msg-item msg-teammate msg-protocol" ${clickable}>
|
|
976
|
+
${MSG_ICON_TEAMMATE}
|
|
977
|
+
<div class="msg-body"><div class="msg-text">${nameSpan} <span class="protocol-label">${escapeHtml(m.protocolLabel || m.protocolType)}</span></div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}
|
|
978
|
+
</div>`);
|
|
979
|
+
} else {
|
|
868
980
|
const summaryText = m.summary ? escapeHtml(m.summary) : escapeHtml((m.text || '').slice(0, 80));
|
|
869
|
-
|
|
981
|
+
parts.push(`<div class="msg-item msg-teammate" ${clickable}>
|
|
870
982
|
${MSG_ICON_TEAMMATE}
|
|
871
983
|
<div class="msg-body"><div class="msg-text">${nameSpan} ${summaryText}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${tmLogBtn}${pinBtn}
|
|
872
|
-
</div
|
|
984
|
+
</div>`);
|
|
873
985
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
986
|
+
}
|
|
987
|
+
i++;
|
|
988
|
+
}
|
|
989
|
+
return parts.join('');
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
993
|
+
function toggleToolGroup(id) {
|
|
994
|
+
const el = document.getElementById(id);
|
|
995
|
+
if (el) el.classList.toggle('show');
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function renderMessages(messages) {
|
|
999
|
+
const container = document.getElementById('message-panel-content');
|
|
1000
|
+
const pinnedContainer = document.getElementById('message-panel-pinned');
|
|
1001
|
+
pinnedContainer.innerHTML = agentLogMode ? '' : renderPinnedSection();
|
|
1002
|
+
if (!messages.length) {
|
|
1003
|
+
container.innerHTML = '<div class="msg-empty">No messages found for this session</div>';
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const msgsHtml = renderMessageList(messages);
|
|
1007
|
+
const limitBanner =
|
|
1008
|
+
currentMessages.length >= MSG_MAX_LOADED
|
|
1009
|
+
? `<div class="msg-limit-banner">Showing last ${MSG_MAX_LOADED} messages</div>`
|
|
1010
|
+
: '';
|
|
1011
|
+
container.innerHTML = limitBanner + msgsHtml;
|
|
1012
|
+
if (!msgUserScrolledUp) container.scrollTop = container.scrollHeight;
|
|
1013
|
+
// Auto-load more if content doesn't overflow yet
|
|
1014
|
+
if (
|
|
1015
|
+
msgHasMore &&
|
|
1016
|
+
!msgLoadingMore &&
|
|
1017
|
+
currentMessages.length < MSG_MAX_LOADED &&
|
|
1018
|
+
container.scrollHeight <= container.clientHeight
|
|
1019
|
+
) {
|
|
1020
|
+
loadOlderMessages();
|
|
1021
|
+
}
|
|
879
1022
|
}
|
|
880
1023
|
|
|
881
1024
|
let currentMsgDetailIdx = null;
|
|
@@ -897,6 +1040,40 @@ const MSG_ICON_TEAMMATE =
|
|
|
897
1040
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>';
|
|
898
1041
|
const MSG_ICON_IDLE =
|
|
899
1042
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6"/></svg>';
|
|
1043
|
+
const ICON_TASK =
|
|
1044
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
|
1045
|
+
const ICON_WEB =
|
|
1046
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
|
1047
|
+
const TOOL_ICONS = {
|
|
1048
|
+
Bash: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="18" rx="2"/><polyline points="7 10 10 13 7 16"/><line x1="13" y1="16" x2="17" y2="16"/></svg>',
|
|
1049
|
+
Read: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
1050
|
+
Write:
|
|
1051
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>',
|
|
1052
|
+
Edit: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
|
|
1053
|
+
Glob: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><circle cx="14" cy="14" r="3"/><line x1="16.5" y1="16.5" x2="19" y2="19"/></svg>',
|
|
1054
|
+
Grep: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
1055
|
+
Agent: MSG_ICON_TEAMMATE,
|
|
1056
|
+
SendMessage:
|
|
1057
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
|
|
1058
|
+
TaskCreate: ICON_TASK,
|
|
1059
|
+
TaskUpdate: ICON_TASK,
|
|
1060
|
+
TaskGet: ICON_TASK,
|
|
1061
|
+
TaskList: ICON_TASK,
|
|
1062
|
+
ToolSearch:
|
|
1063
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>',
|
|
1064
|
+
AskUserQuestion:
|
|
1065
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
|
1066
|
+
Skill:
|
|
1067
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
|
1068
|
+
WebFetch: ICON_WEB,
|
|
1069
|
+
WebSearch: ICON_WEB,
|
|
1070
|
+
NotebookEdit:
|
|
1071
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',
|
|
1072
|
+
LSP: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
1073
|
+
};
|
|
1074
|
+
function getToolIcon(toolName) {
|
|
1075
|
+
return TOOL_ICONS[toolName] || MSG_ICON_TOOL;
|
|
1076
|
+
}
|
|
900
1077
|
const AGENT_LOG_ICON =
|
|
901
1078
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
|
902
1079
|
function agentLogButton(agentId) {
|
|
@@ -1077,17 +1254,26 @@ function savePinnedSessions() {
|
|
|
1077
1254
|
localStorage.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
|
|
1078
1255
|
}
|
|
1079
1256
|
|
|
1080
|
-
// unpinned → pinned → sticky → unpinned
|
|
1081
1257
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1082
1258
|
function toggleSessionPin(sessionId) {
|
|
1259
|
+
if (pinnedSessionIds.has(sessionId)) {
|
|
1260
|
+
pinnedSessionIds.delete(sessionId);
|
|
1261
|
+
stickySessionIds.delete(sessionId);
|
|
1262
|
+
} else {
|
|
1263
|
+
pinnedSessionIds.add(sessionId);
|
|
1264
|
+
}
|
|
1265
|
+
savePinnedSessions();
|
|
1266
|
+
renderSessions();
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1270
|
+
function toggleSessionSticky(sessionId) {
|
|
1083
1271
|
if (stickySessionIds.has(sessionId)) {
|
|
1084
1272
|
stickySessionIds.delete(sessionId);
|
|
1085
1273
|
pinnedSessionIds.delete(sessionId);
|
|
1086
|
-
} else if (pinnedSessionIds.has(sessionId)) {
|
|
1087
|
-
pinnedSessionIds.delete(sessionId);
|
|
1088
|
-
stickySessionIds.add(sessionId);
|
|
1089
1274
|
} else {
|
|
1090
1275
|
pinnedSessionIds.add(sessionId);
|
|
1276
|
+
stickySessionIds.add(sessionId);
|
|
1091
1277
|
}
|
|
1092
1278
|
savePinnedSessions();
|
|
1093
1279
|
renderSessions();
|
|
@@ -1184,7 +1370,7 @@ function showMsgDetail(idx) {
|
|
|
1184
1370
|
const detailRendered = m.tool === 'Bash' ? highlightBash(detailEscaped) : detailEscaped;
|
|
1185
1371
|
mainHtml = `${descHtml}<pre class="msg-detail-pre">${detailRendered}</pre>`;
|
|
1186
1372
|
} else {
|
|
1187
|
-
mainHtml = '<em>No details</em>';
|
|
1373
|
+
mainHtml = TASK_TOOLS.has(m.tool) ? '' : '<em>No details</em>';
|
|
1188
1374
|
}
|
|
1189
1375
|
body.innerHTML = mainHtml + toolParamsHtml + taskResultHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
|
|
1190
1376
|
} else if (m.type === 'teammate') {
|
|
@@ -1199,14 +1385,25 @@ function showMsgDetail(idx) {
|
|
|
1199
1385
|
body.innerHTML = renderMarkdown(text);
|
|
1200
1386
|
}
|
|
1201
1387
|
} else {
|
|
1202
|
-
const
|
|
1388
|
+
const rawText = stripAnsi(m.fullText || m.text);
|
|
1389
|
+
const cmd = m.type === 'user' ? parseCommandMessage(rawText) : null;
|
|
1203
1390
|
document.getElementById('msg-detail-title').textContent =
|
|
1204
1391
|
m.type === 'assistant' ? 'Claude' : m.systemLabel ? 'System' : 'User';
|
|
1205
1392
|
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
1206
1393
|
if (m.compactSummary) {
|
|
1207
1394
|
body.innerHTML = renderMarkdown(m.compactSummary);
|
|
1395
|
+
} else if (cmd) {
|
|
1396
|
+
const argsMatch = rawText.match(/<command-args>([^<]*)<\/command-args>/);
|
|
1397
|
+
const args = argsMatch?.[1].trim() ? argsMatch[1].trim() : null;
|
|
1398
|
+
const cleanBody = rawText
|
|
1399
|
+
.replace(/<command-[^>]+>[\s\S]*?<\/command-[^>]+>/g, '')
|
|
1400
|
+
.replace(/<local-command-[^>]+>[\s\S]*?<\/local-command-[^>]+>/g, '')
|
|
1401
|
+
.trim();
|
|
1402
|
+
let cmdHtml = `<code>${escapeHtml(cmd)}${args ? ` ${escapeHtml(args)}` : ''}</code>`;
|
|
1403
|
+
if (cleanBody) cmdHtml += `<div style="margin-top:10px">${renderMarkdown(cleanBody)}</div>`;
|
|
1404
|
+
body.innerHTML = cmdHtml;
|
|
1208
1405
|
} else {
|
|
1209
|
-
body.innerHTML = renderMarkdown(
|
|
1406
|
+
body.innerHTML = renderMarkdown(rawText);
|
|
1210
1407
|
}
|
|
1211
1408
|
}
|
|
1212
1409
|
const modal = document.getElementById('msg-detail-modal').querySelector('.modal');
|
|
@@ -1318,6 +1515,32 @@ function renderProtocolDetail(data) {
|
|
|
1318
1515
|
}
|
|
1319
1516
|
|
|
1320
1517
|
const TASK_TOOLS = new Set(['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList']);
|
|
1518
|
+
const TASK_STATUS_COLORS = {
|
|
1519
|
+
pending: 'var(--text-muted)',
|
|
1520
|
+
in_progress: 'var(--info)',
|
|
1521
|
+
completed: 'var(--success)',
|
|
1522
|
+
deleted: 'var(--danger)',
|
|
1523
|
+
};
|
|
1524
|
+
function formatTaskStatusBadge(status) {
|
|
1525
|
+
const color = TASK_STATUS_COLORS[status] || 'var(--text-muted)';
|
|
1526
|
+
return `<span style="color:${color};font-weight:600;text-transform:uppercase;font-size:0.85em">${escapeHtml(status)}</span>`;
|
|
1527
|
+
}
|
|
1528
|
+
function formatTaskToolDetail(params) {
|
|
1529
|
+
if (!params) return '';
|
|
1530
|
+
const parts = [];
|
|
1531
|
+
if (params.taskId) {
|
|
1532
|
+
const id = String(params.taskId).replace(/^#/, '');
|
|
1533
|
+
parts.push(`<span style="color:var(--text-muted)">#${escapeHtml(id)}</span>`);
|
|
1534
|
+
}
|
|
1535
|
+
if (params.status) parts.push(formatTaskStatusBadge(params.status));
|
|
1536
|
+
if (params.subject) parts.push(`<span style="color:var(--text-secondary)">${escapeHtml(params.subject)}</span>`);
|
|
1537
|
+
return parts.length ? ` ${parts.join(' ')}` : '';
|
|
1538
|
+
}
|
|
1539
|
+
function getToolDetail(tool, params, detail) {
|
|
1540
|
+
if (TASK_TOOLS.has(tool)) return formatTaskToolDetail(params);
|
|
1541
|
+
if (detail) return ` <span style="color:var(--text-muted)">${escapeHtml(detail)}</span>`;
|
|
1542
|
+
return '';
|
|
1543
|
+
}
|
|
1321
1544
|
function renderTaskResult(toolResult) {
|
|
1322
1545
|
if (!toolResult) return '';
|
|
1323
1546
|
const lines = toolResult.trim().split('\n');
|
|
@@ -1330,17 +1553,9 @@ function renderTaskResult(toolResult) {
|
|
|
1330
1553
|
const title = fields.find(([k]) => /^Task/.test(k));
|
|
1331
1554
|
const status = fields.find(([k]) => k === 'Status');
|
|
1332
1555
|
const rest = fields.filter(([k]) => !/^Task/.test(k) && k !== 'Status');
|
|
1333
|
-
const statusColors = {
|
|
1334
|
-
pending: 'var(--text-muted)',
|
|
1335
|
-
in_progress: 'var(--info)',
|
|
1336
|
-
completed: 'var(--success)',
|
|
1337
|
-
deleted: 'var(--danger)',
|
|
1338
|
-
};
|
|
1339
|
-
const sc = status ? statusColors[status[1]] || 'var(--text-muted)' : '';
|
|
1340
1556
|
let html = '<div class="protocol-detail">';
|
|
1341
1557
|
if (title) html += `<span class="protocol-type-badge">${escapeHtml(title[1])}</span>`;
|
|
1342
|
-
if (status)
|
|
1343
|
-
html += `<span style="display:inline-block;font-size:10px;font-weight:600;color:${sc};text-transform:uppercase;margin-bottom:6px">${escapeHtml(status[1])}</span>`;
|
|
1558
|
+
if (status) html += `<span style="display:inline-block;margin-bottom:6px">${formatTaskStatusBadge(status[1])}</span>`;
|
|
1344
1559
|
if (rest.length) {
|
|
1345
1560
|
html += '<div class="protocol-fields">';
|
|
1346
1561
|
for (const [k, v] of rest) {
|
|
@@ -1447,6 +1662,13 @@ function makeExpandToggle(_truncatedHtml, fullHtml, opts = {}) {
|
|
|
1447
1662
|
}
|
|
1448
1663
|
|
|
1449
1664
|
function autoSizeModal(modal, body) {
|
|
1665
|
+
modal.style.maxWidth = '';
|
|
1666
|
+
modal.classList.remove('has-mermaid');
|
|
1667
|
+
const hasMermaid = body.querySelector('pre.mermaid') !== null;
|
|
1668
|
+
if (hasMermaid) {
|
|
1669
|
+
modal.classList.add('has-mermaid');
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1450
1672
|
const hasTable = body.querySelector('table') !== null;
|
|
1451
1673
|
const hasPre = body.querySelector('pre') !== null;
|
|
1452
1674
|
const desired = hasTable ? 1100 : body.textContent.length > 2000 || hasPre ? 960 : 860;
|
|
@@ -1539,8 +1761,9 @@ function renderAgentFooter() {
|
|
|
1539
1761
|
// or started >30s after previous stopped (legitimate re-spawn). Filter the rest.
|
|
1540
1762
|
const byType = {};
|
|
1541
1763
|
for (const a of agents) {
|
|
1542
|
-
|
|
1543
|
-
byType[
|
|
1764
|
+
const groupKey = a.agentName || a.type;
|
|
1765
|
+
if (!byType[groupKey]) byType[groupKey] = [];
|
|
1766
|
+
byType[groupKey].push(a);
|
|
1544
1767
|
}
|
|
1545
1768
|
const filtered = [];
|
|
1546
1769
|
for (const group of Object.values(byType)) {
|
|
@@ -1614,10 +1837,15 @@ function renderAgentFooter() {
|
|
|
1614
1837
|
const colonIdx = rawType.indexOf(':');
|
|
1615
1838
|
const typeNs = colonIdx > 0 ? rawType.substring(0, colonIdx + 1) : '';
|
|
1616
1839
|
const typeName = colonIdx > 0 ? rawType.substring(colonIdx + 1) : rawType;
|
|
1840
|
+
const agentNameVal = a.agentName || null;
|
|
1841
|
+
const nameColor = agentNameVal ? getOwnerColor(agentNameVal) : null;
|
|
1842
|
+
const nameBadgeHtml = nameColor
|
|
1843
|
+
? `<span class="task-owner-badge task-owner-badge--compact" style="background:${nameColor.bg};color:${nameColor.color}">${escapeHtml(agentNameVal)}</span>`
|
|
1844
|
+
: '';
|
|
1617
1845
|
const agentColor = resolveNamedColor(a.color);
|
|
1618
1846
|
const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
|
|
1619
1847
|
return `<div class="agent-card"${colorStyle} onclick="showAgentModal('${a.agentId}')">
|
|
1620
|
-
<div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span
|
|
1848
|
+
<div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span>${nameBadgeHtml}</div>
|
|
1621
1849
|
<div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
|
|
1622
1850
|
${msgHtml}
|
|
1623
1851
|
</div>`;
|
|
@@ -1717,13 +1945,21 @@ function showAgentModal(agentId) {
|
|
|
1717
1945
|
const elapsed = stopped && started ? stopped.getTime() - started.getTime() : started ? now - started.getTime() : 0;
|
|
1718
1946
|
|
|
1719
1947
|
const statusDot = `<span class="agent-dot ${agent.status}" style="display:inline-block;vertical-align:middle;margin-right:6px;"></span>`;
|
|
1720
|
-
|
|
1948
|
+
const modalNameLabel = agent.agentName ? ` · ${escapeHtml(agent.agentName)}` : '';
|
|
1949
|
+
title.innerHTML = `${statusDot} ${escapeHtml(agent.type || 'unknown')}${modalNameLabel}`;
|
|
1721
1950
|
|
|
1722
1951
|
const rows = [
|
|
1723
1952
|
['Status', agent.status],
|
|
1724
1953
|
['Agent ID', `<code style="font-size:12px;color:var(--text-tertiary)">${escapeHtml(agent.agentId)}</code>`],
|
|
1725
1954
|
['Duration', formatDuration(elapsed)],
|
|
1726
1955
|
];
|
|
1956
|
+
if (agent.agentName) {
|
|
1957
|
+
const ownerColor = getOwnerColor(agent.agentName);
|
|
1958
|
+
rows.push([
|
|
1959
|
+
'Owner',
|
|
1960
|
+
`<span class="task-owner-badge" style="background:${ownerColor.bg};color:${ownerColor.color}">${escapeHtml(agent.agentName)}</span>`,
|
|
1961
|
+
]);
|
|
1962
|
+
}
|
|
1727
1963
|
if (agent.model)
|
|
1728
1964
|
rows.push(['Model', `<code style="font-size:12px;color:var(--text-tertiary)">${escapeHtml(agent.model)}</code>`]);
|
|
1729
1965
|
if (started) rows.push(['Started', started.toLocaleTimeString()]);
|
|
@@ -1774,6 +2010,7 @@ function closeAgentModal() {
|
|
|
1774
2010
|
|
|
1775
2011
|
//#region RENDERING
|
|
1776
2012
|
let revealedPlanSessionId = null;
|
|
2013
|
+
let revealedStorageSessionId = null;
|
|
1777
2014
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1778
2015
|
async function revealPlanSession(planSessionId) {
|
|
1779
2016
|
if (revealedPlanSessionId === planSessionId) {
|
|
@@ -1868,6 +2105,10 @@ function renderSessions() {
|
|
|
1868
2105
|
const planSession = sessions.find((s) => s.id === revealedPlanSessionId);
|
|
1869
2106
|
if (planSession) filteredSessions.push(planSession);
|
|
1870
2107
|
}
|
|
2108
|
+
if (revealedStorageSessionId && !filteredSessions.some((s) => s.id === revealedStorageSessionId)) {
|
|
2109
|
+
const storageSession = sessions.find((s) => s.id === revealedStorageSessionId);
|
|
2110
|
+
if (storageSession) filteredSessions.push(storageSession);
|
|
2111
|
+
}
|
|
1871
2112
|
}
|
|
1872
2113
|
if (filterProject) {
|
|
1873
2114
|
filteredSessions = filteredSessions.filter((s) => matchesProjectFilter(s.project));
|
|
@@ -1875,30 +2116,34 @@ function renderSessions() {
|
|
|
1875
2116
|
|
|
1876
2117
|
// Apply search filter
|
|
1877
2118
|
if (searchQuery) {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
if (
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2119
|
+
const taskMatchIds = new Set();
|
|
2120
|
+
for (const t of allTasksCache) {
|
|
2121
|
+
if (
|
|
2122
|
+
(t.subject && fuzzyMatch(t.subject, searchQuery)) ||
|
|
2123
|
+
(t.description && fuzzyMatch(t.description, searchQuery)) ||
|
|
2124
|
+
(t.activeForm && fuzzyMatch(t.activeForm, searchQuery))
|
|
2125
|
+
)
|
|
2126
|
+
taskMatchIds.add(t.sessionId);
|
|
2127
|
+
}
|
|
2128
|
+
const matchesSearch = (s) =>
|
|
2129
|
+
(s.name && fuzzyMatch(s.name, searchQuery)) ||
|
|
2130
|
+
(s.id && fuzzyMatch(s.id, searchQuery)) ||
|
|
2131
|
+
(s.project && fuzzyMatch(s.project, searchQuery)) ||
|
|
2132
|
+
(s.description && fuzzyMatch(s.description, searchQuery)) ||
|
|
2133
|
+
taskMatchIds.has(s.id);
|
|
2134
|
+
|
|
2135
|
+
filteredSessions = filteredSessions.filter(matchesSearch);
|
|
2136
|
+
|
|
2137
|
+
// Re-add pinned/sticky sessions that match the query but were excluded by active filter
|
|
2138
|
+
if (pinnedSessionIds.size > 0 || stickySessionIds.size > 0) {
|
|
2139
|
+
const filteredIds = new Set(filteredSessions.map((s) => s.id));
|
|
2140
|
+
const missingPinned = sessions.filter((s) => isAnyPinned(s.id) && !filteredIds.has(s.id) && matchesSearch(s));
|
|
2141
|
+
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
2142
|
+
}
|
|
1898
2143
|
}
|
|
1899
2144
|
|
|
1900
|
-
//
|
|
1901
|
-
if ((pinnedSessionIds.size > 0 || stickySessionIds.size > 0)
|
|
2145
|
+
// Include pinned/sticky sessions even if they don't match active/recent filter
|
|
2146
|
+
if (!searchQuery && (pinnedSessionIds.size > 0 || stickySessionIds.size > 0)) {
|
|
1902
2147
|
const filteredIds = new Set(filteredSessions.map((s) => s.id));
|
|
1903
2148
|
const missingPinned = sessions.filter((s) => isAnyPinned(s.id) && !filteredIds.has(s.id));
|
|
1904
2149
|
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
@@ -1956,7 +2201,7 @@ function renderSessions() {
|
|
|
1956
2201
|
|
|
1957
2202
|
const pinState = getSessionPinState(session.id);
|
|
1958
2203
|
const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
|
|
1959
|
-
const pinTitle = pinState === '
|
|
2204
|
+
const pinTitle = pinState === 'pinned' || pinState === 'sticky' ? 'Unpin' : 'Pin';
|
|
1960
2205
|
const showCtx = !!session.contextStatus;
|
|
1961
2206
|
return `
|
|
1962
2207
|
<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}">
|
|
@@ -3324,9 +3569,16 @@ function _renderStorageSessions() {
|
|
|
3324
3569
|
return html;
|
|
3325
3570
|
}
|
|
3326
3571
|
|
|
3327
|
-
function _storageViewSession(id) {
|
|
3572
|
+
async function _storageViewSession(id) {
|
|
3328
3573
|
closeStorageManager();
|
|
3329
|
-
|
|
3574
|
+
revealedStorageSessionId = id;
|
|
3575
|
+
if (!sessions.some((s) => s.id === id)) {
|
|
3576
|
+
lastSessionsHash = '';
|
|
3577
|
+
await fetchSessions();
|
|
3578
|
+
}
|
|
3579
|
+
await fetchTasks(id);
|
|
3580
|
+
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(id)}"]`);
|
|
3581
|
+
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
3330
3582
|
}
|
|
3331
3583
|
|
|
3332
3584
|
function _storageUnpinSession(id) {
|
|
@@ -4025,6 +4277,33 @@ function renderMarkdown(text) {
|
|
|
4025
4277
|
return `<pre style="white-space:pre-wrap;margin:0;">${escapeHtml(text)}</pre>`;
|
|
4026
4278
|
}
|
|
4027
4279
|
|
|
4280
|
+
function isLightTheme() {
|
|
4281
|
+
const saved = localStorage.getItem('theme');
|
|
4282
|
+
return (
|
|
4283
|
+
document.body.classList.contains('light') || (!saved && window.matchMedia('(prefers-color-scheme: light)').matches)
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
function getMermaidTheme() {
|
|
4288
|
+
return isLightTheme() ? 'default' : 'dark';
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
function initMermaidBlocks(container) {
|
|
4292
|
+
if (typeof mermaid === 'undefined') return;
|
|
4293
|
+
const blocks = (container || document).querySelectorAll('pre.mermaid:not([data-processed])');
|
|
4294
|
+
if (blocks.length) mermaid.run({ nodes: [...blocks] });
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
function reinitMermaidTheme() {
|
|
4298
|
+
if (typeof mermaid === 'undefined') return;
|
|
4299
|
+
mermaid.initialize({ startOnLoad: false, theme: getMermaidTheme() });
|
|
4300
|
+
document.querySelectorAll('pre.mermaid[data-processed]').forEach((el) => {
|
|
4301
|
+
el.removeAttribute('data-processed');
|
|
4302
|
+
el.innerHTML = escapeHtml(el.getAttribute('data-original') || '');
|
|
4303
|
+
});
|
|
4304
|
+
initMermaidBlocks();
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4028
4307
|
const _agentTabTexts = {};
|
|
4029
4308
|
|
|
4030
4309
|
function renderAgentTabs(promptHtml, responseHtml, promptText, responseText) {
|
|
@@ -4288,22 +4567,21 @@ function toggleTheme() {
|
|
|
4288
4567
|
updateThemeIcon();
|
|
4289
4568
|
updateThemeColor(!isCurrentlyLight);
|
|
4290
4569
|
syncHljsTheme();
|
|
4570
|
+
reinitMermaidTheme();
|
|
4291
4571
|
}
|
|
4292
4572
|
|
|
4293
4573
|
function syncHljsTheme() {
|
|
4294
|
-
const
|
|
4295
|
-
const dark = document.getElementById('hljs-theme-dark');
|
|
4296
|
-
const light = document.getElementById('hljs-theme-light');
|
|
4297
|
-
if (dark) dark
|
|
4298
|
-
if (light) light
|
|
4574
|
+
const light = isLightTheme();
|
|
4575
|
+
const dark$ = document.getElementById('hljs-theme-dark');
|
|
4576
|
+
const light$ = document.getElementById('hljs-theme-light');
|
|
4577
|
+
if (dark$) dark$.disabled = light;
|
|
4578
|
+
if (light$) light$.disabled = !light;
|
|
4299
4579
|
}
|
|
4300
4580
|
|
|
4301
4581
|
function updateThemeIcon() {
|
|
4302
|
-
const
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
document.getElementById('theme-icon-dark').style.display = isLight ? 'none' : 'block';
|
|
4306
|
-
document.getElementById('theme-icon-light').style.display = isLight ? 'block' : 'none';
|
|
4582
|
+
const light = isLightTheme();
|
|
4583
|
+
document.getElementById('theme-icon-dark').style.display = light ? 'none' : 'block';
|
|
4584
|
+
document.getElementById('theme-icon-light').style.display = light ? 'block' : 'none';
|
|
4307
4585
|
}
|
|
4308
4586
|
|
|
4309
4587
|
function loadTheme() {
|
|
@@ -4477,8 +4755,20 @@ async function showSessionInfoModal(sessionId) {
|
|
|
4477
4755
|
showInfoModal(session, teamConfig, tasks, planContent);
|
|
4478
4756
|
}
|
|
4479
4757
|
|
|
4758
|
+
let _infoModalSessionId = null;
|
|
4480
4759
|
let _pendingPlanContent = null;
|
|
4481
4760
|
|
|
4761
|
+
function updateStickyBtnState() {
|
|
4762
|
+
const stickyBtn = document.getElementById('session-info-sticky-btn');
|
|
4763
|
+
if (!stickyBtn || !_infoModalSessionId) return;
|
|
4764
|
+
const isSticky = stickySessionIds.has(_infoModalSessionId);
|
|
4765
|
+
stickyBtn.style.display = '';
|
|
4766
|
+
stickyBtn.classList.toggle('active', isSticky);
|
|
4767
|
+
stickyBtn.title = isSticky ? 'Remove sticky pin' : 'Sticky pin — always show at top';
|
|
4768
|
+
const svg = stickyBtn.querySelector('svg');
|
|
4769
|
+
if (svg) svg.setAttribute('fill', isSticky ? 'currentColor' : 'none');
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4482
4772
|
function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
4483
4773
|
const modal = document.getElementById('team-modal');
|
|
4484
4774
|
const titleEl = document.getElementById('team-modal-title');
|
|
@@ -4616,6 +4906,8 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
4616
4906
|
}
|
|
4617
4907
|
|
|
4618
4908
|
bodyEl.innerHTML = html;
|
|
4909
|
+
_infoModalSessionId = session.id;
|
|
4910
|
+
updateStickyBtnState();
|
|
4619
4911
|
modal.classList.add('visible');
|
|
4620
4912
|
|
|
4621
4913
|
const keyHandler = (e) => {
|
|
@@ -4778,6 +5070,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4778
5070
|
if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
|
|
4779
5071
|
const renderer = new marked.Renderer();
|
|
4780
5072
|
renderer.code = ({ text, lang }) => {
|
|
5073
|
+
if (lang === 'mermaid') {
|
|
5074
|
+
return `<pre class="mermaid" data-original="${escapeHtml(text)}">${escapeHtml(text)}</pre>`;
|
|
5075
|
+
}
|
|
4781
5076
|
let highlighted;
|
|
4782
5077
|
if (lang && hljs.getLanguage(lang)) {
|
|
4783
5078
|
highlighted = hljs.highlight(text, { language: lang }).value;
|
|
@@ -4788,6 +5083,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4788
5083
|
};
|
|
4789
5084
|
marked.use({ renderer });
|
|
4790
5085
|
}
|
|
5086
|
+
|
|
5087
|
+
if (typeof mermaid !== 'undefined') {
|
|
5088
|
+
mermaid.initialize({ startOnLoad: false, theme: getMermaidTheme() });
|
|
5089
|
+
let mermaidPending = false;
|
|
5090
|
+
const mo = new MutationObserver(() => {
|
|
5091
|
+
if (mermaidPending) return;
|
|
5092
|
+
mermaidPending = true;
|
|
5093
|
+
queueMicrotask(() => {
|
|
5094
|
+
mermaidPending = false;
|
|
5095
|
+
initMermaidBlocks();
|
|
5096
|
+
});
|
|
5097
|
+
});
|
|
5098
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
5099
|
+
}
|
|
4791
5100
|
});
|
|
4792
5101
|
|
|
4793
5102
|
loadSidebarState();
|
|
@@ -4800,6 +5109,42 @@ initSidebarResize();
|
|
|
4800
5109
|
loadPanelWidths();
|
|
4801
5110
|
initPanelResize('detail-panel', 'detail-panel-resize', '--detail-panel-width', 'detail-panel-width');
|
|
4802
5111
|
initPanelResize('message-panel', 'message-panel-resize', '--message-panel-width', 'message-panel-width');
|
|
5112
|
+
|
|
5113
|
+
const msgContentEl = document.getElementById('message-panel-content');
|
|
5114
|
+
const jumpLatestBtn = document.createElement('button');
|
|
5115
|
+
jumpLatestBtn.id = 'msg-jump-latest';
|
|
5116
|
+
jumpLatestBtn.className = 'msg-jump-latest';
|
|
5117
|
+
jumpLatestBtn.style.display = 'none';
|
|
5118
|
+
jumpLatestBtn.textContent = '\u2193 Latest';
|
|
5119
|
+
jumpLatestBtn.onclick = function () {
|
|
5120
|
+
msgContentEl.scrollTop = msgContentEl.scrollHeight;
|
|
5121
|
+
msgUserScrolledUp = false;
|
|
5122
|
+
this.style.display = 'none';
|
|
5123
|
+
};
|
|
5124
|
+
msgContentEl.parentElement.appendChild(jumpLatestBtn);
|
|
5125
|
+
|
|
5126
|
+
let msgScrollThrottled = false;
|
|
5127
|
+
msgContentEl.addEventListener('scroll', () => {
|
|
5128
|
+
if (msgScrollThrottled) return;
|
|
5129
|
+
msgScrollThrottled = true;
|
|
5130
|
+
requestAnimationFrame(() => {
|
|
5131
|
+
msgScrollThrottled = false;
|
|
5132
|
+
const el = msgContentEl;
|
|
5133
|
+
if (el.scrollTop === 0 && msgHasMore && !msgLoadingMore) {
|
|
5134
|
+
loadOlderMessages();
|
|
5135
|
+
}
|
|
5136
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
|
|
5137
|
+
msgUserScrolledUp = !nearBottom;
|
|
5138
|
+
jumpLatestBtn.style.display = msgUserScrolledUp ? '' : 'none';
|
|
5139
|
+
});
|
|
5140
|
+
});
|
|
5141
|
+
// Load older messages on wheel-up when content doesn't overflow
|
|
5142
|
+
msgContentEl.addEventListener('wheel', function (e) {
|
|
5143
|
+
if (e.deltaY < 0 && this.scrollTop === 0 && msgHasMore && !msgLoadingMore) {
|
|
5144
|
+
loadOlderMessages();
|
|
5145
|
+
}
|
|
5146
|
+
});
|
|
5147
|
+
|
|
4803
5148
|
fetch('/api/version')
|
|
4804
5149
|
.then((r) => r.json())
|
|
4805
5150
|
.then((d) => {
|
|
@@ -4838,6 +5183,10 @@ fetchSessions().then(async () => {
|
|
|
4838
5183
|
}
|
|
4839
5184
|
if (urlState.messages && currentSessionId) {
|
|
4840
5185
|
toggleMessagePanel();
|
|
5186
|
+
// Re-render after panel layout settles so scroll dimensions are correct
|
|
5187
|
+
requestAnimationFrame(() => {
|
|
5188
|
+
if (currentMessages.length) renderMessages(currentMessages);
|
|
5189
|
+
});
|
|
4841
5190
|
}
|
|
4842
5191
|
});
|
|
4843
5192
|
|