claude-code-kanban 2.1.0 → 2.2.0-rc.10
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/hooks/agent-spy.sh +25 -2
- package/install.js +1 -0
- package/lib/parsers.js +37 -1
- package/package.json +2 -2
- package/public/app.js +857 -113
- package/public/index.html +36 -4
- package/public/style.css +315 -5
- package/server.js +277 -40
package/public/app.js
CHANGED
|
@@ -21,6 +21,7 @@ let messagePanelOpen = false;
|
|
|
21
21
|
let lastMessagesHash = '';
|
|
22
22
|
let currentMessages = [];
|
|
23
23
|
let agentDurationInterval = null;
|
|
24
|
+
let agentPollInterval = null;
|
|
24
25
|
let selectedTaskId = null;
|
|
25
26
|
let selectedSessionId = null;
|
|
26
27
|
let focusZone = 'board'; // 'board' | 'sidebar'
|
|
@@ -29,6 +30,8 @@ let selectedSessionKbId = null;
|
|
|
29
30
|
let sessionJustSelected = false;
|
|
30
31
|
let agentLogMode = null;
|
|
31
32
|
let agentLogSSE = null;
|
|
33
|
+
let currentProjectPath = null;
|
|
34
|
+
let currentProjectSessionIds = [];
|
|
32
35
|
|
|
33
36
|
function getUrlState() {
|
|
34
37
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -41,12 +44,14 @@ function getUrlState() {
|
|
|
41
44
|
owner: params.get('owner'),
|
|
42
45
|
search: params.get('search'),
|
|
43
46
|
messages: params.get('messages') === '1',
|
|
47
|
+
projectView: params.get('projectView'),
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
function updateUrl() {
|
|
48
52
|
const params = new URLSearchParams();
|
|
49
53
|
if (viewMode === 'all') params.set('view', 'all');
|
|
54
|
+
if (viewMode === 'project' && currentProjectPath) params.set('projectView', btoa(currentProjectPath));
|
|
50
55
|
if (currentSessionId) params.set('session', currentSessionId);
|
|
51
56
|
if (sessionFilter !== 'active') params.set('filter', sessionFilter);
|
|
52
57
|
if (sessionLimit !== '20') params.set('limit', sessionLimit);
|
|
@@ -70,6 +75,8 @@ function resetState() {
|
|
|
70
75
|
viewMode = 'all';
|
|
71
76
|
if (agentLogMode) exitAgentLogMode();
|
|
72
77
|
currentSessionId = null;
|
|
78
|
+
currentProjectPath = null;
|
|
79
|
+
currentProjectSessionIds = [];
|
|
73
80
|
const searchInput = document.getElementById('search-input');
|
|
74
81
|
if (searchInput) searchInput.value = '';
|
|
75
82
|
document.getElementById('search-clear-btn')?.classList.remove('visible');
|
|
@@ -106,9 +113,11 @@ let lastTasksHash = '';
|
|
|
106
113
|
|
|
107
114
|
//#region DATA_FETCHING
|
|
108
115
|
async function fetchSessions() {
|
|
109
|
-
console.log('[fetchSessions] Starting...');
|
|
110
116
|
try {
|
|
111
|
-
const
|
|
117
|
+
const allPinnedIds = new Set([...pinnedSessionIds, ...stickySessionIds]);
|
|
118
|
+
if (revealedPlanSessionId) allPinnedIds.add(revealedPlanSessionId);
|
|
119
|
+
if (revealedStorageSessionId) allPinnedIds.add(revealedStorageSessionId);
|
|
120
|
+
const pinnedParam = allPinnedIds.size > 0 ? `&pinned=${[...allPinnedIds].join(',')}` : '';
|
|
112
121
|
const [newSessions, newTasks] = await Promise.all([
|
|
113
122
|
fetch(`/api/sessions?limit=${sessionLimit}${pinnedParam}`).then((r) => r.json()),
|
|
114
123
|
fetch('/api/tasks/all').then((r) => r.json()),
|
|
@@ -117,7 +126,6 @@ async function fetchSessions() {
|
|
|
117
126
|
const sessionsHash = JSON.stringify(newSessions);
|
|
118
127
|
const tasksHash = JSON.stringify(newTasks);
|
|
119
128
|
if (sessionsHash === lastSessionsHash && tasksHash === lastTasksHash) {
|
|
120
|
-
console.log('[fetchSessions] No changes, skipping render');
|
|
121
129
|
return;
|
|
122
130
|
}
|
|
123
131
|
lastSessionsHash = sessionsHash;
|
|
@@ -125,9 +133,7 @@ async function fetchSessions() {
|
|
|
125
133
|
|
|
126
134
|
sessions = newSessions;
|
|
127
135
|
allTasksCache = newTasks;
|
|
128
|
-
console.log('[fetchSessions] Sessions loaded:', sessions.length);
|
|
129
136
|
renderSessions();
|
|
130
|
-
console.log('[fetchSessions] Render complete');
|
|
131
137
|
renderLiveUpdatesFromCache();
|
|
132
138
|
} catch (error) {
|
|
133
139
|
console.error('Failed to fetch sessions:', error);
|
|
@@ -410,6 +416,7 @@ let lastCurrentTasksHash = '';
|
|
|
410
416
|
async function fetchTasks(sessionId) {
|
|
411
417
|
try {
|
|
412
418
|
viewMode = 'session';
|
|
419
|
+
document.getElementById('message-toggle')?.style.removeProperty('display');
|
|
413
420
|
const res = await fetch(`/api/sessions/${sessionId}`);
|
|
414
421
|
|
|
415
422
|
let newTasks;
|
|
@@ -423,7 +430,6 @@ async function fetchTasks(sessionId) {
|
|
|
423
430
|
|
|
424
431
|
const hash = JSON.stringify(newTasks);
|
|
425
432
|
if (sessionId === currentSessionId && hash === lastCurrentTasksHash) {
|
|
426
|
-
console.log('[fetchTasks] No changes, skipping render');
|
|
427
433
|
return;
|
|
428
434
|
}
|
|
429
435
|
lastCurrentTasksHash = hash;
|
|
@@ -432,6 +438,12 @@ async function fetchTasks(sessionId) {
|
|
|
432
438
|
if (agentLogMode && sessionId !== currentSessionId) exitAgentLogMode();
|
|
433
439
|
if (sessionId !== currentSessionId && document.getElementById('scratchpad-modal').classList.contains('visible'))
|
|
434
440
|
closeScratchpad();
|
|
441
|
+
if (revealedPlanSessionId && sessionId !== revealedPlanSessionId) {
|
|
442
|
+
revealedPlanSessionId = null;
|
|
443
|
+
}
|
|
444
|
+
if (revealedStorageSessionId && sessionId !== revealedStorageSessionId) {
|
|
445
|
+
revealedStorageSessionId = null;
|
|
446
|
+
}
|
|
435
447
|
currentSessionId = sessionId;
|
|
436
448
|
currentPins = loadPins(sessionId);
|
|
437
449
|
ownerFilter = '';
|
|
@@ -483,6 +495,101 @@ async function fetchAgents(sessionId) {
|
|
|
483
495
|
}
|
|
484
496
|
}
|
|
485
497
|
|
|
498
|
+
async function fetchProjectView(projectPath) {
|
|
499
|
+
viewMode = 'project';
|
|
500
|
+
currentProjectPath = projectPath;
|
|
501
|
+
currentSessionId = null;
|
|
502
|
+
currentMessages = [];
|
|
503
|
+
lastMessagesHash = '';
|
|
504
|
+
if (messagePanelOpen) toggleMessagePanel();
|
|
505
|
+
document.getElementById('message-toggle')?.style.setProperty('display', 'none');
|
|
506
|
+
const msgContent = document.getElementById('message-panel-content');
|
|
507
|
+
if (msgContent) msgContent.innerHTML = '';
|
|
508
|
+
const msgPinned = document.getElementById('message-panel-pinned');
|
|
509
|
+
if (msgPinned) msgPinned.innerHTML = '';
|
|
510
|
+
const projectSessions = sessions.filter((s) => s.project === projectPath);
|
|
511
|
+
currentProjectSessionIds = projectSessions.map((s) => s.id);
|
|
512
|
+
const activeSessionIds = projectSessions.filter((s) => isSessionActive(s) || isAnyPinned(s.id)).map((s) => s.id);
|
|
513
|
+
|
|
514
|
+
const encoded = btoa(projectPath);
|
|
515
|
+
const [tasksResult, agentResults] = await Promise.all([
|
|
516
|
+
fetch(`/api/projects/${encodeURIComponent(encoded)}/tasks`)
|
|
517
|
+
.then((r) => r.json())
|
|
518
|
+
.catch((e) => {
|
|
519
|
+
console.error('[fetchProjectView] tasks:', e);
|
|
520
|
+
return [];
|
|
521
|
+
}),
|
|
522
|
+
Promise.all(
|
|
523
|
+
activeSessionIds.map((id) =>
|
|
524
|
+
fetch(`/api/sessions/${id}/agents`)
|
|
525
|
+
.then((r) => r.json())
|
|
526
|
+
.catch(() => ({ agents: [] })),
|
|
527
|
+
),
|
|
528
|
+
),
|
|
529
|
+
]);
|
|
530
|
+
currentTasks = tasksResult;
|
|
531
|
+
const seen = new Set();
|
|
532
|
+
currentAgents = [];
|
|
533
|
+
const mergedColors = {};
|
|
534
|
+
let mergedWaiting = null;
|
|
535
|
+
for (let i = 0; i < agentResults.length; i++) {
|
|
536
|
+
const r = agentResults[i];
|
|
537
|
+
const sid = activeSessionIds[i];
|
|
538
|
+
const agents = r.agents || (Array.isArray(r) ? r : []);
|
|
539
|
+
for (const a of agents) {
|
|
540
|
+
if (a.agentId && !seen.has(a.agentId)) {
|
|
541
|
+
seen.add(a.agentId);
|
|
542
|
+
a._sourceSessionId = sid;
|
|
543
|
+
currentAgents.push(a);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (r.teamColors) Object.assign(mergedColors, r.teamColors);
|
|
547
|
+
if (r.waitingForUser && !mergedWaiting) mergedWaiting = r.waitingForUser;
|
|
548
|
+
}
|
|
549
|
+
currentWaiting = mergedWaiting;
|
|
550
|
+
Object.assign(teamColorMap, mergedColors);
|
|
551
|
+
|
|
552
|
+
renderProjectView();
|
|
553
|
+
renderAgentFooter();
|
|
554
|
+
renderKanban();
|
|
555
|
+
updateUrl();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function refreshProjectAgents() {
|
|
559
|
+
if (!currentProjectPath) return;
|
|
560
|
+
const projectSessions = sessions.filter((s) => s.project === currentProjectPath);
|
|
561
|
+
const activeSessionIds = projectSessions.filter((s) => isSessionActive(s) || isAnyPinned(s.id)).map((s) => s.id);
|
|
562
|
+
const agentResults = await Promise.all(
|
|
563
|
+
activeSessionIds.map((id) =>
|
|
564
|
+
fetch(`/api/sessions/${id}/agents`)
|
|
565
|
+
.then((r) => r.json())
|
|
566
|
+
.catch(() => ({ agents: [] })),
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
const seen = new Set();
|
|
570
|
+
currentAgents = [];
|
|
571
|
+
let mergedWaiting = null;
|
|
572
|
+
for (let i = 0; i < agentResults.length; i++) {
|
|
573
|
+
const r = agentResults[i];
|
|
574
|
+
const sid = activeSessionIds[i];
|
|
575
|
+
const agents = r.agents || (Array.isArray(r) ? r : []);
|
|
576
|
+
for (const a of agents) {
|
|
577
|
+
if (a.agentId && !seen.has(a.agentId)) {
|
|
578
|
+
seen.add(a.agentId);
|
|
579
|
+
a._sourceSessionId = sid;
|
|
580
|
+
currentAgents.push(a);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (r.teamColors) Object.assign(teamColorMap, r.teamColors);
|
|
584
|
+
if (r.waitingForUser && !mergedWaiting) mergedWaiting = r.waitingForUser;
|
|
585
|
+
}
|
|
586
|
+
currentWaiting = mergedWaiting;
|
|
587
|
+
const hash = JSON.stringify({ agents: currentAgents, waiting: currentWaiting });
|
|
588
|
+
if (hash === lastAgentsHash) return;
|
|
589
|
+
lastAgentsHash = hash;
|
|
590
|
+
renderAgentFooter();
|
|
591
|
+
}
|
|
592
|
+
|
|
486
593
|
//#endregion
|
|
487
594
|
|
|
488
595
|
//#region MESSAGE_PANEL
|
|
@@ -500,15 +607,18 @@ function toggleMessagePanel() {
|
|
|
500
607
|
|
|
501
608
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
502
609
|
async function viewAgentLog(agentId) {
|
|
503
|
-
let agent =
|
|
610
|
+
let agent = findAgentById(agentId);
|
|
504
611
|
if (!agent && currentSessionId) {
|
|
505
612
|
await fetchAgents(currentSessionId);
|
|
506
|
-
agent =
|
|
613
|
+
agent = findAgentById(agentId);
|
|
507
614
|
}
|
|
508
615
|
if (!agent) return;
|
|
509
|
-
const
|
|
510
|
-
|
|
616
|
+
const resolvedId = agent.agentId;
|
|
617
|
+
const shortId = resolvedId.length > 8 ? resolvedId.slice(0, 8) : resolvedId;
|
|
618
|
+
const agentSessionId = agent._sourceSessionId || currentSessionId;
|
|
619
|
+
agentLogMode = { agentId: resolvedId, sessionId: agentSessionId, agentType: agent.type || 'unknown' };
|
|
511
620
|
closeAgentModal();
|
|
621
|
+
document.getElementById('message-toggle')?.style.removeProperty('display');
|
|
512
622
|
if (!messagePanelOpen) toggleMessagePanel();
|
|
513
623
|
const header = document.querySelector('.message-panel-header h3');
|
|
514
624
|
if (header) {
|
|
@@ -519,9 +629,9 @@ async function viewAgentLog(agentId) {
|
|
|
519
629
|
agentLogSSE.close();
|
|
520
630
|
agentLogSSE = null;
|
|
521
631
|
}
|
|
522
|
-
agentLogSSE = new EventSource(`/api/sessions/${agentLogMode.sessionId}/agents/${
|
|
632
|
+
agentLogSSE = new EventSource(`/api/sessions/${agentLogMode.sessionId}/agents/${resolvedId}/messages/stream`);
|
|
523
633
|
agentLogSSE.addEventListener('agent-log-update', (e) => {
|
|
524
|
-
if (!agentLogMode || agentLogMode.agentId !==
|
|
634
|
+
if (!agentLogMode || agentLogMode.agentId !== resolvedId) return;
|
|
525
635
|
try {
|
|
526
636
|
const data = JSON.parse(e.data);
|
|
527
637
|
currentMessages = data.messages;
|
|
@@ -537,6 +647,11 @@ function exitAgentLogMode() {
|
|
|
537
647
|
agentLogSSE.close();
|
|
538
648
|
agentLogSSE = null;
|
|
539
649
|
}
|
|
650
|
+
if (viewMode === 'project') {
|
|
651
|
+
if (messagePanelOpen) toggleMessagePanel();
|
|
652
|
+
document.getElementById('message-toggle')?.style.setProperty('display', 'none');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
540
655
|
const header = document.querySelector('.message-panel-header h3');
|
|
541
656
|
if (header) header.textContent = 'Session Log';
|
|
542
657
|
lastMessagesHash = '';
|
|
@@ -648,17 +763,16 @@ function renderPinnedSection() {
|
|
|
648
763
|
<div class="msg-body"><div class="msg-text">${escapeHtml(p.tool || '')}${toolDetail}</div><div class="msg-time">${formatDate(p.timestamp)}</div></div>${pinnedAgentLogBtn}${unpin}
|
|
649
764
|
</div>`;
|
|
650
765
|
} else if (p.type === 'agent') {
|
|
651
|
-
const agentClick = `onclick="showAgentModal('${escapeHtml(p.agentId)}')" style="cursor:pointer"`;
|
|
652
766
|
const agentLogBtn = agentLogButton(p.agentId);
|
|
653
767
|
const msgTrunc = p.lastMessage
|
|
654
768
|
? escapeHtml(
|
|
655
|
-
stripAnsi(p.lastMessage.trim())
|
|
769
|
+
stripAnsi(stripTeammateWrapper(p.lastMessage.trim()))
|
|
656
770
|
.replace(/[\r\n]+/g, ' ')
|
|
657
771
|
.slice(0, 60),
|
|
658
772
|
)
|
|
659
773
|
: '';
|
|
660
774
|
const agentDetail = msgTrunc ? ` <span style="color:var(--text-muted)">${msgTrunc}</span>` : '';
|
|
661
|
-
return `<div class="msg-item msg-tool" ${
|
|
775
|
+
return `<div class="msg-item msg-tool" ${click}>
|
|
662
776
|
${MSG_ICON_TOOL}
|
|
663
777
|
<div class="msg-body"><div class="msg-text">${escapeHtml(p.agentType || 'Agent')}${agentDetail}</div><div class="msg-time">${formatDate(p.timestamp)}</div></div>${agentLogBtn}${unpin}
|
|
664
778
|
</div>`;
|
|
@@ -909,27 +1023,8 @@ function showPinnedMsgDetail(pinIdx) {
|
|
|
909
1023
|
}
|
|
910
1024
|
currentMsgDetailIdx = null;
|
|
911
1025
|
currentPinDetailId = pin.id;
|
|
1026
|
+
_renderPinToDetail(pin);
|
|
912
1027
|
const body = document.getElementById('msg-detail-body');
|
|
913
|
-
const agentBtn = document.getElementById('msg-detail-agent-btn');
|
|
914
|
-
if (pin.type === 'tool_use') {
|
|
915
|
-
document.getElementById('msg-detail-title').textContent = pin.tool || 'Tool';
|
|
916
|
-
const fullText = pin.fullDetail || pin.detail || '';
|
|
917
|
-
const pinParamsHtml = renderToolParamsHtml(pin.params);
|
|
918
|
-
const pinResultHtml = renderToolResultHtml(pin.toolResult, pin.toolResultTruncated, pin.toolResultFull);
|
|
919
|
-
const pinDetailEscaped = escapeHtml(fullText);
|
|
920
|
-
const pinDetailRendered = pin.tool === 'Bash' ? highlightBash(pinDetailEscaped) : pinDetailEscaped;
|
|
921
|
-
body.innerHTML =
|
|
922
|
-
(fullText ? `<pre class="msg-detail-pre">${pinDetailRendered}</pre>` : '<em>No details</em>') +
|
|
923
|
-
pinParamsHtml +
|
|
924
|
-
pinResultHtml;
|
|
925
|
-
agentBtn.style.display = 'none';
|
|
926
|
-
} else {
|
|
927
|
-
const text = stripAnsi(pin.fullText || pin.text || '');
|
|
928
|
-
document.getElementById('msg-detail-title').textContent = pin.type === 'assistant' ? 'Claude' : 'User';
|
|
929
|
-
agentBtn.style.display = 'none';
|
|
930
|
-
body.innerHTML = renderMarkdown(text);
|
|
931
|
-
}
|
|
932
|
-
document.getElementById('msg-detail-meta').textContent = formatDate(pin.timestamp);
|
|
933
1028
|
const pinModal = document.getElementById('msg-detail-modal').querySelector('.modal');
|
|
934
1029
|
autoSizeModal(pinModal, body);
|
|
935
1030
|
const pinBtn = document.getElementById('msg-detail-pin-btn');
|
|
@@ -963,6 +1058,7 @@ function togglePinnedCollapse() {
|
|
|
963
1058
|
|
|
964
1059
|
//#region PINNING
|
|
965
1060
|
let pinnedSessionIds = new Set();
|
|
1061
|
+
let stickySessionIds = new Set();
|
|
966
1062
|
|
|
967
1063
|
function loadPinnedSessions() {
|
|
968
1064
|
try {
|
|
@@ -972,18 +1068,72 @@ function loadPinnedSessions() {
|
|
|
972
1068
|
}
|
|
973
1069
|
}
|
|
974
1070
|
|
|
1071
|
+
function loadStickySessions() {
|
|
1072
|
+
try {
|
|
1073
|
+
return new Set(JSON.parse(localStorage.getItem('sticky-sessions')) || []);
|
|
1074
|
+
} catch {
|
|
1075
|
+
return new Set();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
975
1079
|
function savePinnedSessions() {
|
|
976
1080
|
localStorage.setItem('pinned-sessions', JSON.stringify([...pinnedSessionIds]));
|
|
1081
|
+
localStorage.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
|
|
977
1082
|
}
|
|
978
1083
|
|
|
1084
|
+
// unpinned → pinned → sticky → unpinned
|
|
979
1085
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
980
1086
|
function toggleSessionPin(sessionId) {
|
|
981
|
-
if (
|
|
982
|
-
|
|
1087
|
+
if (stickySessionIds.has(sessionId)) {
|
|
1088
|
+
stickySessionIds.delete(sessionId);
|
|
1089
|
+
pinnedSessionIds.delete(sessionId);
|
|
1090
|
+
} else if (pinnedSessionIds.has(sessionId)) {
|
|
1091
|
+
pinnedSessionIds.delete(sessionId);
|
|
1092
|
+
stickySessionIds.add(sessionId);
|
|
1093
|
+
} else {
|
|
1094
|
+
pinnedSessionIds.add(sessionId);
|
|
1095
|
+
}
|
|
983
1096
|
savePinnedSessions();
|
|
984
1097
|
renderSessions();
|
|
985
1098
|
}
|
|
986
1099
|
|
|
1100
|
+
function getSessionPinState(sessionId) {
|
|
1101
|
+
if (stickySessionIds.has(sessionId)) return 'sticky';
|
|
1102
|
+
if (pinnedSessionIds.has(sessionId)) return 'pinned';
|
|
1103
|
+
return 'none';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function isAnyPinned(sessionId) {
|
|
1107
|
+
return pinnedSessionIds.has(sessionId) || stickySessionIds.has(sessionId);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function _renderPinToDetail(pin) {
|
|
1111
|
+
const body = document.getElementById('msg-detail-body');
|
|
1112
|
+
const agentBtn = document.getElementById('msg-detail-agent-btn');
|
|
1113
|
+
agentBtn.style.display = 'none';
|
|
1114
|
+
if (pin.type === 'tool_use') {
|
|
1115
|
+
document.getElementById('msg-detail-title').textContent = pin.tool || 'Tool';
|
|
1116
|
+
const fullText = pin.fullDetail || pin.detail || '';
|
|
1117
|
+
const pinParamsHtml = renderToolParamsHtml(pin.params);
|
|
1118
|
+
const pinResultHtml = renderToolResultHtml(pin.toolResult, pin.toolResultTruncated, pin.toolResultFull);
|
|
1119
|
+
const pinDetailEscaped = escapeHtml(fullText);
|
|
1120
|
+
const pinDetailRendered = pin.tool === 'Bash' ? highlightBash(pinDetailEscaped) : pinDetailEscaped;
|
|
1121
|
+
body.innerHTML =
|
|
1122
|
+
(fullText ? `<pre class="msg-detail-pre">${pinDetailRendered}</pre>` : '<em>No details</em>') +
|
|
1123
|
+
pinParamsHtml +
|
|
1124
|
+
pinResultHtml;
|
|
1125
|
+
} else if (pin.type === 'agent') {
|
|
1126
|
+
document.getElementById('msg-detail-title').textContent = pin.agentType || 'Agent';
|
|
1127
|
+
const lastMsg = stripAnsi(pin.lastMessage || '');
|
|
1128
|
+
body.innerHTML = lastMsg ? renderMarkdown(lastMsg) : '<em>No agent message</em>';
|
|
1129
|
+
} else {
|
|
1130
|
+
const text = stripAnsi(pin.fullText || pin.text || '');
|
|
1131
|
+
document.getElementById('msg-detail-title').textContent = pin.type === 'assistant' ? 'Claude' : 'User';
|
|
1132
|
+
body.innerHTML = renderMarkdown(text);
|
|
1133
|
+
}
|
|
1134
|
+
document.getElementById('msg-detail-meta').textContent = formatDate(pin.timestamp);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
987
1137
|
const SESSION_PIN_SVG = PIN_SVG.replace('width="14" height="14"', 'width="12" height="12"');
|
|
988
1138
|
|
|
989
1139
|
//#endregion
|
|
@@ -1400,14 +1550,17 @@ function renderAgentFooter() {
|
|
|
1400
1550
|
for (const group of Object.values(byType)) {
|
|
1401
1551
|
group.sort((a, b) => new Date(a.startedAt || 0) - new Date(b.startedAt || 0));
|
|
1402
1552
|
filtered.push(group[0]);
|
|
1553
|
+
let maxStop = group[0].stoppedAt ? new Date(group[0].stoppedAt).getTime() : Infinity;
|
|
1403
1554
|
for (let i = 1; i < group.length; i++) {
|
|
1404
|
-
const
|
|
1405
|
-
const
|
|
1406
|
-
const curStart = new Date(
|
|
1407
|
-
const overlapped = curStart <
|
|
1408
|
-
const reSpawn = curStart -
|
|
1409
|
-
const isActive =
|
|
1410
|
-
if (overlapped || reSpawn || isActive) filtered.push(
|
|
1555
|
+
const cur = group[i];
|
|
1556
|
+
const hasContent = cur.prompt || cur.lastMessage;
|
|
1557
|
+
const curStart = new Date(cur.startedAt || 0).getTime();
|
|
1558
|
+
const overlapped = curStart < maxStop;
|
|
1559
|
+
const reSpawn = curStart - maxStop > 30000;
|
|
1560
|
+
const isActive = cur.status === 'active' || cur.status === 'idle';
|
|
1561
|
+
if (overlapped || reSpawn || isActive || hasContent) filtered.push(cur);
|
|
1562
|
+
const curStop = cur.stoppedAt ? new Date(cur.stoppedAt).getTime() : Infinity;
|
|
1563
|
+
if (curStop > maxStop) maxStop = curStop;
|
|
1411
1564
|
}
|
|
1412
1565
|
}
|
|
1413
1566
|
// Sort: active/idle first, then by updatedAt desc
|
|
@@ -1426,6 +1579,8 @@ function renderAgentFooter() {
|
|
|
1426
1579
|
footer.classList.remove('visible');
|
|
1427
1580
|
clearInterval(agentDurationInterval);
|
|
1428
1581
|
agentDurationInterval = null;
|
|
1582
|
+
clearInterval(agentPollInterval);
|
|
1583
|
+
agentPollInterval = null;
|
|
1429
1584
|
return;
|
|
1430
1585
|
}
|
|
1431
1586
|
|
|
@@ -1454,7 +1609,7 @@ function renderAgentFooter() {
|
|
|
1454
1609
|
: a.status === 'idle'
|
|
1455
1610
|
? `idle · ${formatDuration(elapsed)}`
|
|
1456
1611
|
: `active · ${formatDuration(elapsed)}`;
|
|
1457
|
-
const promptTrimmed = stripAnsi((a.prompt || '').trim()).replace(/[\r\n]+/g, ' ');
|
|
1612
|
+
const promptTrimmed = stripAnsi(stripTeammateWrapper((a.prompt || '').trim())).replace(/[\r\n]+/g, ' ');
|
|
1458
1613
|
const promptTrunc = promptTrimmed.length > 60 ? `${promptTrimmed.substring(0, 60)}…` : promptTrimmed;
|
|
1459
1614
|
const msgHtml = promptTrunc
|
|
1460
1615
|
? `<div class="agent-message" title="${escapeHtml(promptTrimmed)}">${escapeHtml(promptTrunc)}</div>`
|
|
@@ -1476,8 +1631,19 @@ function renderAgentFooter() {
|
|
|
1476
1631
|
clearInterval(agentDurationInterval);
|
|
1477
1632
|
if (visible.some((a) => a.status === 'active' || a.status === 'idle')) {
|
|
1478
1633
|
agentDurationInterval = setInterval(() => renderAgentFooter(), 1000);
|
|
1634
|
+
if (!agentPollInterval) {
|
|
1635
|
+
agentPollInterval = setInterval(() => {
|
|
1636
|
+
if (viewMode === 'project' && currentProjectPath) {
|
|
1637
|
+
refreshProjectAgents();
|
|
1638
|
+
} else if (currentSessionId) {
|
|
1639
|
+
fetchAgents(currentSessionId);
|
|
1640
|
+
}
|
|
1641
|
+
}, 3000);
|
|
1642
|
+
}
|
|
1479
1643
|
} else {
|
|
1480
1644
|
agentDurationInterval = setInterval(() => renderAgentFooter(), 10000);
|
|
1645
|
+
clearInterval(agentPollInterval);
|
|
1646
|
+
agentPollInterval = null;
|
|
1481
1647
|
}
|
|
1482
1648
|
}
|
|
1483
1649
|
|
|
@@ -1531,9 +1697,19 @@ async function dismissAgent(agentId) {
|
|
|
1531
1697
|
}
|
|
1532
1698
|
}
|
|
1533
1699
|
|
|
1700
|
+
function findAgentById(agentId) {
|
|
1701
|
+
let agent = currentAgents.find((a) => a.agentId === agentId);
|
|
1702
|
+
if (!agent) {
|
|
1703
|
+
const atIdx = agentId.indexOf('@');
|
|
1704
|
+
const memberName = atIdx > 0 ? agentId.substring(0, atIdx) : null;
|
|
1705
|
+
if (memberName) agent = currentAgents.find((a) => a.type === memberName);
|
|
1706
|
+
}
|
|
1707
|
+
return agent || null;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1534
1710
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1535
1711
|
function showAgentModal(agentId) {
|
|
1536
|
-
const agent =
|
|
1712
|
+
const agent = findAgentById(agentId);
|
|
1537
1713
|
if (!agent) return;
|
|
1538
1714
|
currentAgentModalId = agentId;
|
|
1539
1715
|
const modal = document.getElementById('agent-modal');
|
|
@@ -1552,6 +1728,8 @@ function showAgentModal(agentId) {
|
|
|
1552
1728
|
['Agent ID', `<code style="font-size:12px;color:var(--text-tertiary)">${escapeHtml(agent.agentId)}</code>`],
|
|
1553
1729
|
['Duration', formatDuration(elapsed)],
|
|
1554
1730
|
];
|
|
1731
|
+
if (agent.model)
|
|
1732
|
+
rows.push(['Model', `<code style="font-size:12px;color:var(--text-tertiary)">${escapeHtml(agent.model)}</code>`]);
|
|
1555
1733
|
if (started) rows.push(['Started', started.toLocaleTimeString()]);
|
|
1556
1734
|
if (stopped) rows.push(['Stopped', stopped.toLocaleTimeString()]);
|
|
1557
1735
|
|
|
@@ -1567,7 +1745,7 @@ function showAgentModal(agentId) {
|
|
|
1567
1745
|
.join('') +
|
|
1568
1746
|
`</table>`;
|
|
1569
1747
|
|
|
1570
|
-
const promptText = agentMsg?.agentPrompt || agent.prompt || null;
|
|
1748
|
+
const promptText = stripTeammateWrapper(agentMsg?.agentPrompt || agent.prompt || null);
|
|
1571
1749
|
const responseText = agent.lastMessage ? stripAnsi(agent.lastMessage.trim()) : null;
|
|
1572
1750
|
_agentModalPromptText = promptText;
|
|
1573
1751
|
_agentModalResponseText = responseText;
|
|
@@ -1599,6 +1777,25 @@ function closeAgentModal() {
|
|
|
1599
1777
|
//#endregion
|
|
1600
1778
|
|
|
1601
1779
|
//#region RENDERING
|
|
1780
|
+
let revealedPlanSessionId = null;
|
|
1781
|
+
let revealedStorageSessionId = null;
|
|
1782
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
1783
|
+
async function revealPlanSession(planSessionId) {
|
|
1784
|
+
if (revealedPlanSessionId === planSessionId) {
|
|
1785
|
+
revealedPlanSessionId = null;
|
|
1786
|
+
renderSessions();
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
revealedPlanSessionId = planSessionId;
|
|
1790
|
+
if (!sessions.some((s) => s.id === planSessionId)) {
|
|
1791
|
+
lastSessionsHash = '';
|
|
1792
|
+
await fetchSessions();
|
|
1793
|
+
}
|
|
1794
|
+
await fetchTasks(planSessionId);
|
|
1795
|
+
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(planSessionId)}"]`);
|
|
1796
|
+
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1602
1799
|
async function showAllTasks() {
|
|
1603
1800
|
try {
|
|
1604
1801
|
viewMode = 'all';
|
|
@@ -1659,32 +1856,26 @@ function renderSessions() {
|
|
|
1659
1856
|
let filteredSessions = sessions;
|
|
1660
1857
|
if (sessionFilter === 'active') {
|
|
1661
1858
|
const ACTIVE_PLAN_MS = 15 * 60 * 1000;
|
|
1662
|
-
const RECENTLY_MODIFIED_MS = 5 * 60 * 1000;
|
|
1663
1859
|
const now = Date.now();
|
|
1664
1860
|
const activeSessionIds = new Set();
|
|
1665
1861
|
filteredSessions = filteredSessions.filter((s) => {
|
|
1666
1862
|
const isActive =
|
|
1667
1863
|
s.hasMessages &&
|
|
1668
|
-
(s.pending > 0 ||
|
|
1669
|
-
s.inProgress > 0 ||
|
|
1864
|
+
((!s.sharedTaskList && (s.pending > 0 || s.inProgress > 0)) ||
|
|
1670
1865
|
s.hasActiveAgents ||
|
|
1671
1866
|
s.hasWaitingForUser ||
|
|
1672
1867
|
s.hasRecentLog ||
|
|
1673
|
-
(s.hasPlan && !s.planImplementationSessionId && now - new Date(s.modifiedAt).getTime() <= ACTIVE_PLAN_MS)
|
|
1674
|
-
now - new Date(s.modifiedAt).getTime() <= RECENTLY_MODIFIED_MS);
|
|
1868
|
+
(s.hasPlan && !s.planImplementationSessionId && now - new Date(s.modifiedAt).getTime() <= ACTIVE_PLAN_MS));
|
|
1675
1869
|
if (isActive) activeSessionIds.add(s.id);
|
|
1676
1870
|
return isActive;
|
|
1677
1871
|
});
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
(
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
if (planSessions.length) {
|
|
1686
|
-
filteredSessions = filteredSessions.concat(planSessions);
|
|
1687
|
-
filteredSessions.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt));
|
|
1872
|
+
if (revealedPlanSessionId && !filteredSessions.some((s) => s.id === revealedPlanSessionId)) {
|
|
1873
|
+
const planSession = sessions.find((s) => s.id === revealedPlanSessionId);
|
|
1874
|
+
if (planSession) filteredSessions.push(planSession);
|
|
1875
|
+
}
|
|
1876
|
+
if (revealedStorageSessionId && !filteredSessions.some((s) => s.id === revealedStorageSessionId)) {
|
|
1877
|
+
const storageSession = sessions.find((s) => s.id === revealedStorageSessionId);
|
|
1878
|
+
if (storageSession) filteredSessions.push(storageSession);
|
|
1688
1879
|
}
|
|
1689
1880
|
}
|
|
1690
1881
|
if (filterProject) {
|
|
@@ -1715,10 +1906,10 @@ function renderSessions() {
|
|
|
1715
1906
|
});
|
|
1716
1907
|
}
|
|
1717
1908
|
|
|
1718
|
-
// Always include pinned sessions even if they don't match filters
|
|
1719
|
-
if (pinnedSessionIds.size > 0 && !searchQuery) {
|
|
1909
|
+
// Always include pinned/sticky sessions even if they don't match filters
|
|
1910
|
+
if ((pinnedSessionIds.size > 0 || stickySessionIds.size > 0) && !searchQuery) {
|
|
1720
1911
|
const filteredIds = new Set(filteredSessions.map((s) => s.id));
|
|
1721
|
-
const missingPinned = sessions.filter((s) =>
|
|
1912
|
+
const missingPinned = sessions.filter((s) => isAnyPinned(s.id) && !filteredIds.has(s.id));
|
|
1722
1913
|
if (missingPinned.length) filteredSessions = [...missingPinned, ...filteredSessions];
|
|
1723
1914
|
}
|
|
1724
1915
|
|
|
@@ -1772,11 +1963,13 @@ function renderSessions() {
|
|
|
1772
1963
|
const isTeam = session.isTeam;
|
|
1773
1964
|
const memberCount = session.memberCount || 0;
|
|
1774
1965
|
|
|
1775
|
-
const
|
|
1966
|
+
const pinState = getSessionPinState(session.id);
|
|
1967
|
+
const pinClass = pinState === 'sticky' ? ' sticky' : pinState === 'pinned' ? ' pinned' : '';
|
|
1968
|
+
const pinTitle = pinState === 'sticky' ? 'Unpin' : pinState === 'pinned' ? 'Sticky pin' : 'Pin';
|
|
1776
1969
|
const showCtx = !!session.contextStatus;
|
|
1777
1970
|
return `
|
|
1778
1971
|
<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}">
|
|
1779
|
-
<span class="session-pin-btn${
|
|
1972
|
+
<span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${escapeHtml(session.id)}')" title="${pinTitle} session">${SESSION_PIN_SVG}</span>
|
|
1780
1973
|
<div class="session-name">${escapeHtml(primaryName)}</div>
|
|
1781
1974
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
1782
1975
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
@@ -1784,10 +1977,11 @@ function renderSessions() {
|
|
|
1784
1977
|
<div class="session-progress">
|
|
1785
1978
|
<span class="session-indicators">
|
|
1786
1979
|
${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>` : ''}
|
|
1980
|
+
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}"><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="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"/></svg></span>` : ''}
|
|
1787
1981
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
1788
1982
|
${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>` : ''}
|
|
1789
1983
|
${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
|
|
1790
|
-
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to
|
|
1984
|
+
${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">📋</span>` : ''}
|
|
1791
1985
|
${session.hasWaitingForUser ? '<span class="agent-badge" title="Waiting for user">❓</span>' : ''}
|
|
1792
1986
|
${isLive ? '<span class="pulse"></span>' : ''}
|
|
1793
1987
|
</span>
|
|
@@ -1812,8 +2006,44 @@ function renderSessions() {
|
|
|
1812
2006
|
ungrouped.push(session);
|
|
1813
2007
|
}
|
|
1814
2008
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
2009
|
+
const groupPinned = localStorage.getItem('groupPinnedSessions') !== 'false';
|
|
2010
|
+
const renderGroupSessions = (sessions, pinKey) => {
|
|
2011
|
+
if (!groupPinned || pinnedSessionIds.size === 0) return sessions.map(renderSessionCard).join('');
|
|
2012
|
+
const gPinned = sessions.filter((s) => pinnedSessionIds.has(s.id) && !stickySessionIds.has(s.id));
|
|
2013
|
+
if (gPinned.length === 0) return sessions.map(renderSessionCard).join('');
|
|
2014
|
+
const gIdlePinned = gPinned.filter((s) => !isSessionActive(s));
|
|
2015
|
+
const gUnpinned = sessions.filter(
|
|
2016
|
+
(s) => !pinnedSessionIds.has(s.id) || isSessionActive(s) || stickySessionIds.has(s.id),
|
|
2017
|
+
);
|
|
2018
|
+
const pinCollapsed = collapsedProjectGroups.has(pinKey);
|
|
2019
|
+
if (gIdlePinned.length === 0 && !pinCollapsed) return gUnpinned.map(renderSessionCard).join('');
|
|
2020
|
+
return (
|
|
2021
|
+
'<div class="pinned-sub-section">' +
|
|
2022
|
+
'<div class="pinned-sub-header' +
|
|
2023
|
+
(pinCollapsed ? ' collapsed' : '') +
|
|
2024
|
+
'" data-group-path="' +
|
|
2025
|
+
escapeHtml(pinKey) +
|
|
2026
|
+
'">' +
|
|
2027
|
+
'<svg class="group-chevron" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>' +
|
|
2028
|
+
'<span class="pinned-sub-label">Pinned</span>' +
|
|
2029
|
+
'<span class="group-count">' +
|
|
2030
|
+
gIdlePinned.length +
|
|
2031
|
+
'</span>' +
|
|
2032
|
+
'<span class="pinned-ungroup-btn" title="Ungroup pinned sessions">×</span>' +
|
|
2033
|
+
'</div>' +
|
|
2034
|
+
'<div class="pinned-sub-items' +
|
|
2035
|
+
(pinCollapsed ? ' collapsed' : '') +
|
|
2036
|
+
'">' +
|
|
2037
|
+
gIdlePinned.map(renderSessionCard).join('') +
|
|
2038
|
+
'</div>' +
|
|
2039
|
+
'</div>' +
|
|
2040
|
+
gUnpinned.map(renderSessionCard).join('')
|
|
2041
|
+
);
|
|
2042
|
+
};
|
|
2043
|
+
if (!groupPinned && (pinnedSessionIds.size > 0 || stickySessionIds.size > 0)) {
|
|
2044
|
+
const pinWeight = (s) =>
|
|
2045
|
+
stickySessionIds.has(s.id) ? 2 : pinnedSessionIds.has(s.id) && !isSessionActive(s) ? 1 : 0;
|
|
2046
|
+
const pinSort = (a, b) => pinWeight(b) - pinWeight(a);
|
|
1817
2047
|
for (const [, arr] of groups) arr.sort(pinSort);
|
|
1818
2048
|
ungrouped.sort(pinSort);
|
|
1819
2049
|
}
|
|
@@ -1833,6 +2063,15 @@ function renderSessions() {
|
|
|
1833
2063
|
const sortedGroups = stableGroupOrder.map((p) => [p, groups.get(p)]);
|
|
1834
2064
|
|
|
1835
2065
|
let html = '';
|
|
2066
|
+
if (!groupPinned && pinnedSessionIds.size > 0) {
|
|
2067
|
+
const hasPinnedInView = filteredSessions.some((s) => pinnedSessionIds.has(s.id));
|
|
2068
|
+
if (hasPinnedInView) {
|
|
2069
|
+
html += `<div class="pinned-regroup-banner">
|
|
2070
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="17" x2="12" y2="3"/><path d="M5 10l7-7 7 7"/><line x1="4" y1="21" x2="20" y2="21"/></svg>
|
|
2071
|
+
Group pinned sessions
|
|
2072
|
+
</div>`;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
1836
2075
|
for (const [projectPath, projectSessions] of sortedGroups) {
|
|
1837
2076
|
const folderName = projectPath.split(/[/\\]/).pop();
|
|
1838
2077
|
const isCollapsed = collapsedProjectGroups.has(projectPath);
|
|
@@ -1850,13 +2089,13 @@ function renderSessions() {
|
|
|
1850
2089
|
<svg class="group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
1851
2090
|
<span class="group-name">${escapeHtml(folderName)}</span>
|
|
1852
2091
|
<span class="group-count">${projectSessions.length}</span>
|
|
1853
|
-
<span class="
|
|
1854
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><
|
|
2092
|
+
<span class="project-view-btn" data-project-path="${escapedPath}" title="Open project view — combined tasks from all sessions">
|
|
2093
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
|
1855
2094
|
</span>
|
|
1856
2095
|
</div>
|
|
1857
2096
|
<div class="project-group-breadcrumb" data-full-path="${escapedPath}" title="Click to copy path">${breadcrumbHtml}</div>
|
|
1858
2097
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}">
|
|
1859
|
-
${projectSessions
|
|
2098
|
+
${renderGroupSessions(projectSessions, `__pinned_${projectPath}__`)}
|
|
1860
2099
|
</div>
|
|
1861
2100
|
`;
|
|
1862
2101
|
}
|
|
@@ -1870,7 +2109,7 @@ function renderSessions() {
|
|
|
1870
2109
|
<span class="group-count">${ungrouped.length}</span>
|
|
1871
2110
|
</div>
|
|
1872
2111
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}">
|
|
1873
|
-
${ungrouped
|
|
2112
|
+
${renderGroupSessions(ungrouped, '__pinned___ungrouped__')}
|
|
1874
2113
|
</div>
|
|
1875
2114
|
`;
|
|
1876
2115
|
} else {
|
|
@@ -1879,19 +2118,28 @@ function renderSessions() {
|
|
|
1879
2118
|
|
|
1880
2119
|
sessionsList.innerHTML = html;
|
|
1881
2120
|
} else {
|
|
1882
|
-
const
|
|
1883
|
-
const
|
|
2121
|
+
const sticky = filteredSessions.filter((s) => stickySessionIds.has(s.id));
|
|
2122
|
+
const idlePinned = filteredSessions.filter((s) => pinnedSessionIds.has(s.id) && !isSessionActive(s));
|
|
2123
|
+
const rest = filteredSessions.filter(
|
|
2124
|
+
(s) =>
|
|
2125
|
+
(!pinnedSessionIds.has(s.id) && !stickySessionIds.has(s.id)) ||
|
|
2126
|
+
(pinnedSessionIds.has(s.id) && isSessionActive(s)),
|
|
2127
|
+
);
|
|
1884
2128
|
let html = '';
|
|
1885
|
-
if (
|
|
1886
|
-
|
|
2129
|
+
if (sticky.length > 0) {
|
|
2130
|
+
html += sticky.map(renderSessionCard).join('');
|
|
2131
|
+
}
|
|
2132
|
+
const isCollapsed = collapsedProjectGroups.has('__pinned__');
|
|
2133
|
+
const hasPinned = pinnedSessionIds.size > 0 && filteredSessions.some((s) => pinnedSessionIds.has(s.id));
|
|
2134
|
+
if (idlePinned.length > 0 || (hasPinned && isCollapsed)) {
|
|
1887
2135
|
html += `
|
|
1888
2136
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="__pinned__">
|
|
1889
2137
|
<svg class="group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
1890
2138
|
<span class="group-name">Pinned</span>
|
|
1891
|
-
<span class="group-count">${
|
|
2139
|
+
<span class="group-count">${idlePinned.length}</span>
|
|
1892
2140
|
</div>
|
|
1893
2141
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}">
|
|
1894
|
-
${
|
|
2142
|
+
${idlePinned.map(renderSessionCard).join('')}
|
|
1895
2143
|
</div>
|
|
1896
2144
|
`;
|
|
1897
2145
|
}
|
|
@@ -1969,12 +2217,37 @@ function renderSession() {
|
|
|
1969
2217
|
renderSessions();
|
|
1970
2218
|
}
|
|
1971
2219
|
|
|
2220
|
+
function renderProjectView() {
|
|
2221
|
+
noSession.style.display = 'none';
|
|
2222
|
+
sessionView.classList.add('visible');
|
|
2223
|
+
|
|
2224
|
+
const folderName = currentProjectPath ? currentProjectPath.split(/[/\\]/).pop() : 'Project';
|
|
2225
|
+
sessionTitle.textContent = folderName;
|
|
2226
|
+
|
|
2227
|
+
const metaParts = [`${currentProjectSessionIds.length} sessions`, `${currentTasks.length} tasks`];
|
|
2228
|
+
if (currentProjectPath) metaParts.push(currentProjectPath);
|
|
2229
|
+
sessionMeta.textContent = metaParts.join(' · ');
|
|
2230
|
+
|
|
2231
|
+
const completed = currentTasks.filter((t) => t.status === 'completed').length;
|
|
2232
|
+
const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
|
|
2233
|
+
|
|
2234
|
+
progressPercent.textContent = `${percent}%`;
|
|
2235
|
+
progressBar.style.width = `${percent}%`;
|
|
2236
|
+
const hasInProgress = currentTasks.some((t) => t.status === 'in_progress');
|
|
2237
|
+
progressBar.classList.toggle('shimmer', hasInProgress && percent < 100);
|
|
2238
|
+
|
|
2239
|
+
updateOwnerFilter();
|
|
2240
|
+
renderKanban();
|
|
2241
|
+
renderSessions();
|
|
2242
|
+
}
|
|
2243
|
+
|
|
1972
2244
|
function renderTaskCard(task) {
|
|
1973
2245
|
const isBlocked = task.blockedBy && task.blockedBy.length > 0;
|
|
1974
|
-
const
|
|
2246
|
+
const useSlug = viewMode === 'all' || viewMode === 'project';
|
|
2247
|
+
const taskId = useSlug ? `${(task._taskDir || task.sessionId || '')?.slice(0, 4)}-${task.id}` : task.id;
|
|
1975
2248
|
const sessionLabel = viewMode === 'all' && task.sessionName ? task.sessionName : null;
|
|
1976
2249
|
const statusClass = task.status.replace('_', '-');
|
|
1977
|
-
const actualSessionId = task.sessionId || currentSessionId;
|
|
2250
|
+
const actualSessionId = task._taskDir || task.sessionId || currentSessionId || '';
|
|
1978
2251
|
|
|
1979
2252
|
return `
|
|
1980
2253
|
<div
|
|
@@ -2112,7 +2385,9 @@ async function onColumnDrop(e) {
|
|
|
2112
2385
|
return;
|
|
2113
2386
|
}
|
|
2114
2387
|
const { taskId, sessionId } = data;
|
|
2115
|
-
const task = currentTasks.find(
|
|
2388
|
+
const task = currentTasks.find(
|
|
2389
|
+
(t) => t.id === taskId && (t._taskDir === sessionId || (t.sessionId || currentSessionId) === sessionId),
|
|
2390
|
+
);
|
|
2116
2391
|
if (!task || task.status === newStatus) return;
|
|
2117
2392
|
try {
|
|
2118
2393
|
const res = await fetch(`/api/tasks/${sessionId}/${taskId}`, {
|
|
@@ -2150,7 +2425,10 @@ function getSelectedCardInfo() {
|
|
|
2150
2425
|
for (let ci = 0; ci < COLUMNS.length; ci++) {
|
|
2151
2426
|
const cards = Array.from(COLUMNS[ci].el.querySelectorAll('.task-card'));
|
|
2152
2427
|
for (let i = 0; i < cards.length; i++) {
|
|
2153
|
-
if (
|
|
2428
|
+
if (
|
|
2429
|
+
cards[i].dataset.taskId === selectedTaskId &&
|
|
2430
|
+
(!selectedSessionId || cards[i].dataset.sessionId === selectedSessionId)
|
|
2431
|
+
) {
|
|
2154
2432
|
return { colIndex: ci, cardIndex: i, card: cards[i] };
|
|
2155
2433
|
}
|
|
2156
2434
|
}
|
|
@@ -2200,8 +2478,9 @@ function getKbId(el) {
|
|
|
2200
2478
|
}
|
|
2201
2479
|
|
|
2202
2480
|
function getGroupSessionsContainer(header) {
|
|
2481
|
+
const cls = header.classList.contains('pinned-sub-header') ? 'pinned-sub-items' : 'project-group-sessions';
|
|
2203
2482
|
let el = header.nextElementSibling;
|
|
2204
|
-
while (el && !el.classList.contains(
|
|
2483
|
+
while (el && !el.classList.contains(cls)) el = el.nextElementSibling;
|
|
2205
2484
|
return el;
|
|
2206
2485
|
}
|
|
2207
2486
|
|
|
@@ -2213,7 +2492,10 @@ function getNavigableItems() {
|
|
|
2213
2492
|
if (!collapsedProjectGroups.has(el.dataset.groupPath)) {
|
|
2214
2493
|
const container = getGroupSessionsContainer(el);
|
|
2215
2494
|
if (container) {
|
|
2216
|
-
for (const s of container.querySelectorAll('.session-item'))
|
|
2495
|
+
for (const s of container.querySelectorAll('.session-item')) {
|
|
2496
|
+
if (s.closest('.pinned-sub-items.collapsed')) continue;
|
|
2497
|
+
items.push(s);
|
|
2498
|
+
}
|
|
2217
2499
|
}
|
|
2218
2500
|
}
|
|
2219
2501
|
} else if (el.classList.contains('session-item')) {
|
|
@@ -2410,7 +2692,9 @@ function getAvailableTasksOptions(currentTaskId = null) {
|
|
|
2410
2692
|
|
|
2411
2693
|
//#region TASK_DETAIL
|
|
2412
2694
|
async function showTaskDetail(taskId, sessionId = null) {
|
|
2413
|
-
let task = currentTasks.find(
|
|
2695
|
+
let task = currentTasks.find(
|
|
2696
|
+
(t) => t.id === taskId && (!sessionId || t.sessionId === sessionId || t._taskDir === sessionId),
|
|
2697
|
+
);
|
|
2414
2698
|
|
|
2415
2699
|
// If task not found in currentTasks, fetch it from the session
|
|
2416
2700
|
if (!task && sessionId && sessionId !== 'undefined') {
|
|
@@ -2502,20 +2786,25 @@ async function showTaskDetail(taskId, sessionId = null) {
|
|
|
2502
2786
|
</div>
|
|
2503
2787
|
`;
|
|
2504
2788
|
|
|
2505
|
-
// Setup button handlers
|
|
2789
|
+
// Setup button handlers (read-only in project view)
|
|
2506
2790
|
const deleteBtn = document.getElementById('delete-task-btn');
|
|
2507
|
-
|
|
2508
|
-
deleteBtn.
|
|
2791
|
+
const isProjectView = viewMode === 'project';
|
|
2792
|
+
deleteBtn.style.display = isProjectView ? 'none' : '';
|
|
2793
|
+
if (!isProjectView) deleteBtn.onclick = () => deleteTask(task.id, actualSessionId);
|
|
2509
2794
|
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2795
|
+
const noteSection = detailContent.querySelector('.note-section');
|
|
2796
|
+
if (noteSection && isProjectView) noteSection.style.display = 'none';
|
|
2797
|
+
|
|
2798
|
+
if (!isProjectView) {
|
|
2799
|
+
const titleEl = detailContent.querySelector('.detail-title');
|
|
2800
|
+
if (titleEl) {
|
|
2801
|
+
titleEl.onclick = () => editTitle(titleEl, task, actualSessionId);
|
|
2802
|
+
}
|
|
2515
2803
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2804
|
+
const descEl = detailContent.querySelector('.detail-desc');
|
|
2805
|
+
if (descEl) {
|
|
2806
|
+
descEl.onclick = () => editDescription(descEl, task, actualSessionId);
|
|
2807
|
+
}
|
|
2519
2808
|
}
|
|
2520
2809
|
}
|
|
2521
2810
|
|
|
@@ -2815,6 +3104,15 @@ const _scratchpadModal = document.getElementById('scratchpad-modal');
|
|
|
2815
3104
|
const _scratchpadTextarea = document.getElementById('scratchpad-textarea');
|
|
2816
3105
|
const _scratchpadCharcount = document.getElementById('scratchpad-charcount');
|
|
2817
3106
|
|
|
3107
|
+
let _scratchpadKeyOverride = null;
|
|
3108
|
+
|
|
3109
|
+
function _scratchpadKey() {
|
|
3110
|
+
if (_scratchpadKeyOverride) return _scratchpadKeyOverride;
|
|
3111
|
+
if (currentSessionId) return `scratchpad-${currentSessionId}`;
|
|
3112
|
+
if (currentProjectPath) return `scratchpad-project:${currentProjectPath}`;
|
|
3113
|
+
return null;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
2818
3116
|
function toggleScratchpad() {
|
|
2819
3117
|
if (_scratchpadModal.classList.contains('visible')) {
|
|
2820
3118
|
closeScratchpad();
|
|
@@ -2823,9 +3121,11 @@ function toggleScratchpad() {
|
|
|
2823
3121
|
}
|
|
2824
3122
|
}
|
|
2825
3123
|
|
|
2826
|
-
function showScratchpad() {
|
|
2827
|
-
|
|
2828
|
-
|
|
3124
|
+
function showScratchpad(keyOverride) {
|
|
3125
|
+
_scratchpadKeyOverride = keyOverride || null;
|
|
3126
|
+
const key = _scratchpadKey();
|
|
3127
|
+
if (!key) return;
|
|
3128
|
+
_scratchpadTextarea.value = localStorage.getItem(key) || '';
|
|
2829
3129
|
_scratchpadCharcount.textContent = `${_scratchpadTextarea.value.length} chars`;
|
|
2830
3130
|
_scratchpadModal.classList.add('visible');
|
|
2831
3131
|
_scratchpadTextarea.focus();
|
|
@@ -2837,12 +3137,19 @@ function closeScratchpad() {
|
|
|
2837
3137
|
_scratchpadSaveTimer = null;
|
|
2838
3138
|
}
|
|
2839
3139
|
saveScratchpad();
|
|
3140
|
+
_scratchpadKeyOverride = null;
|
|
2840
3141
|
_scratchpadModal.classList.remove('visible');
|
|
2841
3142
|
}
|
|
2842
3143
|
|
|
2843
3144
|
function saveScratchpad() {
|
|
2844
|
-
|
|
2845
|
-
|
|
3145
|
+
const key = _scratchpadKey();
|
|
3146
|
+
if (!key) return;
|
|
3147
|
+
const val = _scratchpadTextarea.value;
|
|
3148
|
+
if (val.trim()) {
|
|
3149
|
+
localStorage.setItem(key, val);
|
|
3150
|
+
} else {
|
|
3151
|
+
localStorage.removeItem(key);
|
|
3152
|
+
}
|
|
2846
3153
|
}
|
|
2847
3154
|
|
|
2848
3155
|
_scratchpadTextarea.addEventListener('input', () => {
|
|
@@ -2856,6 +3163,374 @@ _scratchpadTextarea.addEventListener('input', () => {
|
|
|
2856
3163
|
|
|
2857
3164
|
//#endregion
|
|
2858
3165
|
|
|
3166
|
+
//#region STORAGE_MANAGER
|
|
3167
|
+
|
|
3168
|
+
function _getStorageTotalSize() {
|
|
3169
|
+
let bytes = 0;
|
|
3170
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3171
|
+
const k = localStorage.key(i);
|
|
3172
|
+
bytes += k.length + localStorage.getItem(k).length;
|
|
3173
|
+
}
|
|
3174
|
+
return bytes * 2; // UTF-16
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
function _updateStorageTotal() {
|
|
3178
|
+
const el = document.getElementById('storage-total');
|
|
3179
|
+
if (el) el.textContent = `${(_getStorageTotalSize() / 1024).toFixed(1)} KB`;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function _getKnownSessionIds() {
|
|
3183
|
+
return new Set(sessions.map((s) => s.id));
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
function _sessionLabel(session, id) {
|
|
3187
|
+
return session ? escapeHtml(session.name || session.slug || id.slice(0, 12)) : escapeHtml(id.slice(0, 12));
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
function _groupByProject(sessionIds) {
|
|
3191
|
+
const sessionMap = new Map(sessions.map((s) => [s.id, s]));
|
|
3192
|
+
const groups = new Map();
|
|
3193
|
+
const orphans = [];
|
|
3194
|
+
for (const id of sessionIds) {
|
|
3195
|
+
const session = sessionMap.get(id);
|
|
3196
|
+
if (!session) {
|
|
3197
|
+
orphans.push({ id, session: null });
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
const project = session.project || '(no project)';
|
|
3201
|
+
if (!groups.has(project)) groups.set(project, []);
|
|
3202
|
+
groups.get(project).push({ id, session });
|
|
3203
|
+
}
|
|
3204
|
+
return { groups, orphans };
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function _projectLabel(project) {
|
|
3208
|
+
if (project === '(no project)') return '(no project)';
|
|
3209
|
+
return project.split(/[/\\]/).pop() || project;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function _escapeForJsAttr(str) {
|
|
3213
|
+
const jsEscaped = str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
3214
|
+
return escapeHtml(jsEscaped);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function _renderProjectGroup(label, meta, innerHtml) {
|
|
3218
|
+
return `<div class="storage-project-group">
|
|
3219
|
+
<div class="storage-project-header">
|
|
3220
|
+
<span>${label}</span>
|
|
3221
|
+
<span class="storage-item-meta">${meta}</span>
|
|
3222
|
+
</div>
|
|
3223
|
+
<div class="storage-session-group">${innerHtml}</div>
|
|
3224
|
+
</div>`;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function _renderOrphanGroup(count, innerHtml) {
|
|
3228
|
+
return _renderProjectGroup('Orphaned', `<span class="storage-item-badge orphan">${count}</span>`, innerHtml);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
function showStorageManager() {
|
|
3232
|
+
_updateStorageTotal();
|
|
3233
|
+
_updateOrphanedCount();
|
|
3234
|
+
document.querySelectorAll('.storage-tab').forEach((t) => {
|
|
3235
|
+
t.classList.toggle('active', t.dataset.tab === 'sessions');
|
|
3236
|
+
});
|
|
3237
|
+
_renderStorageTab();
|
|
3238
|
+
document.getElementById('storage-modal').classList.add('visible');
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
function closeStorageManager() {
|
|
3242
|
+
document.getElementById('storage-modal').classList.remove('visible');
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
3246
|
+
function switchStorageTab(tab) {
|
|
3247
|
+
document.querySelectorAll('.storage-tab').forEach((t) => {
|
|
3248
|
+
t.classList.toggle('active', t.dataset.tab === tab);
|
|
3249
|
+
});
|
|
3250
|
+
_renderStorageTab();
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
function _renderStorageTab() {
|
|
3254
|
+
const body = document.getElementById('storage-modal-body');
|
|
3255
|
+
const tab = document.querySelector('.storage-tab.active')?.dataset.tab || 'sessions';
|
|
3256
|
+
if (tab === 'sessions') body.innerHTML = _renderStorageSessions();
|
|
3257
|
+
else if (tab === 'scratchpads') body.innerHTML = _renderStorageScratchpads();
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
function _renderStorageSessions() {
|
|
3261
|
+
const pinnedIds = [...new Set([...pinnedSessionIds, ...stickySessionIds])];
|
|
3262
|
+
|
|
3263
|
+
const msgMap = new Map();
|
|
3264
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3265
|
+
const key = localStorage.key(i);
|
|
3266
|
+
if (!key.startsWith('pinned-messages-')) continue;
|
|
3267
|
+
const sid = key.slice('pinned-messages-'.length);
|
|
3268
|
+
try {
|
|
3269
|
+
const pins = JSON.parse(localStorage.getItem(key)) || [];
|
|
3270
|
+
if (pins.length) msgMap.set(sid, { pins, key });
|
|
3271
|
+
} catch {}
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
const allIds = [...new Set([...pinnedIds, ...msgMap.keys()])];
|
|
3275
|
+
if (!allIds.length) return '<div class="storage-empty">No pinned sessions or messages</div>';
|
|
3276
|
+
const { groups, orphans } = _groupByProject(allIds);
|
|
3277
|
+
|
|
3278
|
+
function renderMessageItems(id) {
|
|
3279
|
+
const g = msgMap.get(id);
|
|
3280
|
+
if (!g) return '';
|
|
3281
|
+
const eid = escapeHtml(id);
|
|
3282
|
+
const header = `<div class="storage-group-header" style="padding-left:12px;">
|
|
3283
|
+
<span>${g.pins.length} pinned message${g.pins.length > 1 ? 's' : ''}</span>
|
|
3284
|
+
<div class="storage-item-actions">
|
|
3285
|
+
<button class="danger" onclick="_storageClearSessionPins('${eid}')">Clear All</button>
|
|
3286
|
+
</div>
|
|
3287
|
+
</div>`;
|
|
3288
|
+
const items = g.pins
|
|
3289
|
+
.map((p) => {
|
|
3290
|
+
const type = escapeHtml(p.type || '?');
|
|
3291
|
+
const text = escapeHtml((p.text || p.tool || p.agentType || '').slice(0, 60));
|
|
3292
|
+
const pinId = _escapeForJsAttr(p.id || '');
|
|
3293
|
+
const sid = _escapeForJsAttr(id);
|
|
3294
|
+
return `<div class="storage-item storage-item-clickable" style="padding-left:24px;" onclick="_storagePreviewPin('${sid}','${pinId}')">
|
|
3295
|
+
<span class="storage-item-badge">${type}</span>
|
|
3296
|
+
<span class="storage-item-id">${text}</span>
|
|
3297
|
+
<span class="storage-item-meta">${formatDate(p.timestamp)}</span>
|
|
3298
|
+
<div class="storage-item-actions">
|
|
3299
|
+
<button onclick="event.stopPropagation();_storagePreviewPin('${sid}','${pinId}')">View</button>
|
|
3300
|
+
<button class="danger" onclick="event.stopPropagation();_storageUnpinMessage('${sid}','${pinId}')">Unpin</button>
|
|
3301
|
+
</div>
|
|
3302
|
+
</div>`;
|
|
3303
|
+
})
|
|
3304
|
+
.join('');
|
|
3305
|
+
return header + items;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function renderSessionItem({ id, session }) {
|
|
3309
|
+
const isPinned = isAnyPinned(id);
|
|
3310
|
+
const eid = escapeHtml(id);
|
|
3311
|
+
const actions = isPinned
|
|
3312
|
+
? `<button onclick="_storageViewSession('${eid}')">View</button>
|
|
3313
|
+
<button class="danger" onclick="_storageUnpinSession('${eid}')">Unpin</button>`
|
|
3314
|
+
: `<button onclick="_storageViewSession('${eid}')">View</button>`;
|
|
3315
|
+
return `<div class="storage-group-header">
|
|
3316
|
+
<span>${_sessionLabel(session, id)}</span>
|
|
3317
|
+
<div class="storage-item-actions">${actions}</div>
|
|
3318
|
+
</div>${renderMessageItems(id)}`;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
let html = '';
|
|
3322
|
+
for (const [project, items] of groups) {
|
|
3323
|
+
const count = items.length;
|
|
3324
|
+
html += _renderProjectGroup(
|
|
3325
|
+
escapeHtml(_projectLabel(project)),
|
|
3326
|
+
`${count} session${count > 1 ? 's' : ''}`,
|
|
3327
|
+
items.map(renderSessionItem).join(''),
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
if (orphans.length) {
|
|
3331
|
+
html += _renderOrphanGroup(orphans.length, orphans.map(renderSessionItem).join(''));
|
|
3332
|
+
}
|
|
3333
|
+
return html;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
async function _storageViewSession(id) {
|
|
3337
|
+
closeStorageManager();
|
|
3338
|
+
revealedStorageSessionId = id;
|
|
3339
|
+
if (!sessions.some((s) => s.id === id)) {
|
|
3340
|
+
lastSessionsHash = '';
|
|
3341
|
+
await fetchSessions();
|
|
3342
|
+
}
|
|
3343
|
+
await fetchTasks(id);
|
|
3344
|
+
const el = document.querySelector(`.session-item[data-session-id="${CSS.escape(id)}"]`);
|
|
3345
|
+
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
function _storageUnpinSession(id) {
|
|
3349
|
+
pinnedSessionIds.delete(id);
|
|
3350
|
+
stickySessionIds.delete(id);
|
|
3351
|
+
savePinnedSessions();
|
|
3352
|
+
renderSessions();
|
|
3353
|
+
_renderStorageTab();
|
|
3354
|
+
_updateStorageTotal();
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
function _storageClearSessionPins(sessionId) {
|
|
3358
|
+
localStorage.removeItem(`pinned-messages-${sessionId}`);
|
|
3359
|
+
if (currentSessionId === sessionId) {
|
|
3360
|
+
currentPins = [];
|
|
3361
|
+
const el = document.getElementById('message-panel-pinned');
|
|
3362
|
+
if (el) el.innerHTML = '';
|
|
3363
|
+
}
|
|
3364
|
+
_renderStorageTab();
|
|
3365
|
+
_updateStorageTotal();
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
function _storageUnpinMessage(sessionId, pinId) {
|
|
3369
|
+
const key = `pinned-messages-${sessionId}`;
|
|
3370
|
+
try {
|
|
3371
|
+
const pins = JSON.parse(localStorage.getItem(key)) || [];
|
|
3372
|
+
const idx = pins.findIndex((p) => p.id === pinId);
|
|
3373
|
+
if (idx < 0) return;
|
|
3374
|
+
pins.splice(idx, 1);
|
|
3375
|
+
if (pins.length) localStorage.setItem(key, JSON.stringify(pins));
|
|
3376
|
+
else localStorage.removeItem(key);
|
|
3377
|
+
if (currentSessionId === sessionId) {
|
|
3378
|
+
currentPins = pins;
|
|
3379
|
+
const el = document.getElementById('message-panel-pinned');
|
|
3380
|
+
if (el) el.innerHTML = renderPinnedSection();
|
|
3381
|
+
}
|
|
3382
|
+
} catch {}
|
|
3383
|
+
_renderStorageTab();
|
|
3384
|
+
_updateStorageTotal();
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
function _renderStorageScratchpads() {
|
|
3388
|
+
const allItems = [];
|
|
3389
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3390
|
+
const key = localStorage.key(i);
|
|
3391
|
+
if (!key.startsWith('scratchpad-')) continue;
|
|
3392
|
+
const val = localStorage.getItem(key) || '';
|
|
3393
|
+
const isProject = key.startsWith('scratchpad-project:');
|
|
3394
|
+
const id = isProject ? key.slice('scratchpad-project:'.length) : key.slice('scratchpad-'.length);
|
|
3395
|
+
allItems.push({ key, id, isProject, chars: val.length });
|
|
3396
|
+
}
|
|
3397
|
+
if (!allItems.length) return '<div class="storage-empty">No scratchpads</div>';
|
|
3398
|
+
|
|
3399
|
+
const projectItems = allItems.filter((i) => i.isProject);
|
|
3400
|
+
const sessionItems = allItems.filter((i) => !i.isProject);
|
|
3401
|
+
const sessionIds = sessionItems.map((i) => i.id);
|
|
3402
|
+
const { groups: projectGroups, orphans } = _groupByProject(sessionIds);
|
|
3403
|
+
const scratchBySession = new Map(sessionItems.map((i) => [i.id, i]));
|
|
3404
|
+
|
|
3405
|
+
function renderScratchItem(item) {
|
|
3406
|
+
const session = !item.isProject ? sessions.find((s) => s.id === item.id) : null;
|
|
3407
|
+
const typeBadge = item.isProject
|
|
3408
|
+
? '<span class="storage-item-badge">project</span>'
|
|
3409
|
+
: '<span class="storage-item-badge">session</span>';
|
|
3410
|
+
const jsKey = _escapeForJsAttr(item.key);
|
|
3411
|
+
const label = item.isProject ? escapeHtml(_projectLabel(item.id)) : _sessionLabel(session, item.id);
|
|
3412
|
+
return `<div class="storage-item">
|
|
3413
|
+
<span class="storage-item-id" title="${escapeHtml(item.id)}">${label}</span>
|
|
3414
|
+
${typeBadge}
|
|
3415
|
+
<span class="storage-item-meta">${item.chars} chars</span>
|
|
3416
|
+
<div class="storage-item-actions">
|
|
3417
|
+
<button onclick="_storagePreviewScratchpad('${jsKey}')">View</button>
|
|
3418
|
+
<button class="danger" onclick="_storageDeleteScratchpad('${jsKey}')">Delete</button>
|
|
3419
|
+
</div>
|
|
3420
|
+
</div>`;
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
let html = '';
|
|
3424
|
+
|
|
3425
|
+
if (projectItems.length) {
|
|
3426
|
+
html += _renderProjectGroup(
|
|
3427
|
+
'Project Scratchpads',
|
|
3428
|
+
`${projectItems.length}`,
|
|
3429
|
+
projectItems.map(renderScratchItem).join(''),
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
for (const [project, items] of projectGroups) {
|
|
3434
|
+
const matching = items.map((i) => scratchBySession.get(i.id)).filter(Boolean);
|
|
3435
|
+
if (!matching.length) continue;
|
|
3436
|
+
html += _renderProjectGroup(
|
|
3437
|
+
escapeHtml(_projectLabel(project)),
|
|
3438
|
+
`${matching.length} scratchpad${matching.length > 1 ? 's' : ''}`,
|
|
3439
|
+
matching.map(renderScratchItem).join(''),
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
if (orphans.length) {
|
|
3444
|
+
const orphanItems = orphans.map((i) => scratchBySession.get(i.id)).filter(Boolean);
|
|
3445
|
+
if (orphanItems.length) {
|
|
3446
|
+
html += _renderOrphanGroup(orphanItems.length, orphanItems.map(renderScratchItem).join(''));
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return html;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
function _storagePreviewScratchpad(key) {
|
|
3453
|
+
closeStorageManager();
|
|
3454
|
+
showScratchpad(key);
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
function _storagePreviewPin(sessionId, pinId) {
|
|
3458
|
+
closeStorageManager();
|
|
3459
|
+
const key = `pinned-messages-${sessionId}`;
|
|
3460
|
+
try {
|
|
3461
|
+
const pins = JSON.parse(localStorage.getItem(key)) || [];
|
|
3462
|
+
const pin = pins.find((p) => p.id === pinId);
|
|
3463
|
+
if (!pin) return;
|
|
3464
|
+
document.getElementById('msg-detail-pin-btn').style.display = 'none';
|
|
3465
|
+
currentMsgDetailIdx = null;
|
|
3466
|
+
currentPinDetailId = null;
|
|
3467
|
+
_renderPinToDetail(pin);
|
|
3468
|
+
document.getElementById('msg-detail-modal').classList.add('visible');
|
|
3469
|
+
} catch (e) {
|
|
3470
|
+
console.error('_storagePreviewPin error:', e);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
function _storageDeleteScratchpad(key) {
|
|
3475
|
+
localStorage.removeItem(key);
|
|
3476
|
+
_renderStorageTab();
|
|
3477
|
+
_updateStorageTotal();
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
function _findOrphanedKeys() {
|
|
3481
|
+
const known = _getKnownSessionIds();
|
|
3482
|
+
if (!known.size) return [];
|
|
3483
|
+
const orphaned = [];
|
|
3484
|
+
for (const id of pinnedSessionIds) if (!known.has(id)) orphaned.push(`__pinned__${id}`);
|
|
3485
|
+
for (const id of stickySessionIds) if (!known.has(id)) orphaned.push(`__sticky__${id}`);
|
|
3486
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3487
|
+
const key = localStorage.key(i);
|
|
3488
|
+
if (key.startsWith('pinned-messages-')) {
|
|
3489
|
+
if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
|
|
3490
|
+
} else if (key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:')) {
|
|
3491
|
+
if (!known.has(key.slice('scratchpad-'.length))) orphaned.push(key);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
return orphaned;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
function _updateOrphanedCount() {
|
|
3498
|
+
const btn = document.getElementById('storage-cleanup-btn');
|
|
3499
|
+
if (!btn) return;
|
|
3500
|
+
const count = _findOrphanedKeys().length;
|
|
3501
|
+
btn.textContent = count ? `Clean Orphaned (${count})` : 'Clean Orphaned';
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
3505
|
+
function cleanupOrphanedStorage() {
|
|
3506
|
+
if (!sessions.length) {
|
|
3507
|
+
showToast('Sessions not loaded yet — try again after they appear');
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
const orphaned = _findOrphanedKeys();
|
|
3511
|
+
let pinsChanged = false;
|
|
3512
|
+
for (const key of orphaned) {
|
|
3513
|
+
if (key.startsWith('__pinned__')) {
|
|
3514
|
+
pinnedSessionIds.delete(key.slice('__pinned__'.length));
|
|
3515
|
+
pinsChanged = true;
|
|
3516
|
+
} else if (key.startsWith('__sticky__')) {
|
|
3517
|
+
stickySessionIds.delete(key.slice('__sticky__'.length));
|
|
3518
|
+
pinsChanged = true;
|
|
3519
|
+
} else {
|
|
3520
|
+
localStorage.removeItem(key);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
if (pinsChanged) savePinnedSessions();
|
|
3524
|
+
const removed = orphaned.length;
|
|
3525
|
+
|
|
3526
|
+
showToast(removed ? `Cleaned ${removed} orphaned item${removed > 1 ? 's' : ''}` : 'No orphaned items found');
|
|
3527
|
+
renderSessions();
|
|
3528
|
+
_renderStorageTab();
|
|
3529
|
+
_updateStorageTotal();
|
|
3530
|
+
_updateOrphanedCount();
|
|
3531
|
+
}
|
|
3532
|
+
//#endregion
|
|
3533
|
+
|
|
2859
3534
|
//#region KEYBOARD_SHORTCUTS
|
|
2860
3535
|
function matchKey(e, ...keys) {
|
|
2861
3536
|
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return false;
|
|
@@ -2927,6 +3602,11 @@ document.addEventListener('keydown', (e) => {
|
|
|
2927
3602
|
}
|
|
2928
3603
|
return;
|
|
2929
3604
|
}
|
|
3605
|
+
if (e.code === 'KeyS' && e.shiftKey) {
|
|
3606
|
+
e.preventDefault();
|
|
3607
|
+
showStorageManager();
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
2930
3610
|
|
|
2931
3611
|
// Tab toggles focus zone
|
|
2932
3612
|
if (e.key === 'Tab') {
|
|
@@ -3101,7 +3781,9 @@ function setupEventSource() {
|
|
|
3101
3781
|
|
|
3102
3782
|
let taskRefreshTimer = null;
|
|
3103
3783
|
let metadataRefreshTimer = null;
|
|
3784
|
+
let agentRefreshTimer = null;
|
|
3104
3785
|
const pendingTaskSessionIds = new Set();
|
|
3786
|
+
const pendingAgentSessionIds = new Set();
|
|
3105
3787
|
|
|
3106
3788
|
function debouncedRefresh(sessionId, isMetadata) {
|
|
3107
3789
|
if (isMetadata) {
|
|
@@ -3122,6 +3804,9 @@ function setupEventSource() {
|
|
|
3122
3804
|
currentTasks = filterProject ? allTasksCache.filter((t) => matchesProjectFilter(t.project)) : allTasksCache;
|
|
3123
3805
|
renderAllTasks();
|
|
3124
3806
|
renderLiveUpdatesFromCache();
|
|
3807
|
+
} else if (viewMode === 'project' && currentProjectPath) {
|
|
3808
|
+
const hasUpdate = currentProjectSessionIds.some((id) => pendingTaskSessionIds.has(id));
|
|
3809
|
+
if (hasUpdate) fetchProjectView(currentProjectPath);
|
|
3125
3810
|
} else if (currentSessionId && pendingTaskSessionIds.has(currentSessionId)) {
|
|
3126
3811
|
fetchTasks(currentSessionId);
|
|
3127
3812
|
}
|
|
@@ -3132,7 +3817,6 @@ function setupEventSource() {
|
|
|
3132
3817
|
|
|
3133
3818
|
eventSource.onmessage = (event) => {
|
|
3134
3819
|
const data = JSON.parse(event.data);
|
|
3135
|
-
console.log('[SSE] Event received:', data);
|
|
3136
3820
|
if (data.type === 'update' || data.type === 'metadata-update') {
|
|
3137
3821
|
if (data.type === 'metadata-update') projectsCacheDirty = true;
|
|
3138
3822
|
debouncedRefresh(data.sessionId, data.type === 'metadata-update');
|
|
@@ -3143,10 +3827,17 @@ function setupEventSource() {
|
|
|
3143
3827
|
}
|
|
3144
3828
|
|
|
3145
3829
|
if (data.type === 'agent-update') {
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3830
|
+
pendingAgentSessionIds.add(data.sessionId);
|
|
3831
|
+
clearTimeout(agentRefreshTimer);
|
|
3832
|
+
agentRefreshTimer = setTimeout(() => {
|
|
3833
|
+
fetchSessions().catch((err) => console.error('[SSE] fetchSessions failed:', err));
|
|
3834
|
+
if (viewMode === 'project' && currentProjectSessionIds.some((id) => pendingAgentSessionIds.has(id))) {
|
|
3835
|
+
refreshProjectAgents();
|
|
3836
|
+
} else if (currentSessionId && pendingAgentSessionIds.has(currentSessionId)) {
|
|
3837
|
+
fetchAgents(currentSessionId);
|
|
3838
|
+
}
|
|
3839
|
+
pendingAgentSessionIds.clear();
|
|
3840
|
+
}, 500);
|
|
3150
3841
|
}
|
|
3151
3842
|
|
|
3152
3843
|
if (data.type === 'context-update') {
|
|
@@ -3154,7 +3845,6 @@ function setupEventSource() {
|
|
|
3154
3845
|
}
|
|
3155
3846
|
|
|
3156
3847
|
if (data.type === 'team-update') {
|
|
3157
|
-
console.log('[SSE] Team update:', data.teamName);
|
|
3158
3848
|
debouncedRefresh(data.teamName, false);
|
|
3159
3849
|
}
|
|
3160
3850
|
};
|
|
@@ -3312,6 +4002,10 @@ function renderContextDetail(raw) {
|
|
|
3312
4002
|
//#endregion
|
|
3313
4003
|
|
|
3314
4004
|
//#region UTILS
|
|
4005
|
+
function isSessionActive(s) {
|
|
4006
|
+
return s.hasRecentLog || s.inProgress > 0 || s.hasActiveAgents || s.hasWaitingForUser;
|
|
4007
|
+
}
|
|
4008
|
+
|
|
3315
4009
|
function formatDate(dateStr) {
|
|
3316
4010
|
const date = new Date(dateStr);
|
|
3317
4011
|
const now = new Date();
|
|
@@ -3328,6 +4022,12 @@ function stripAnsi(text) {
|
|
|
3328
4022
|
return typeof text === 'string' ? text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') : text;
|
|
3329
4023
|
}
|
|
3330
4024
|
|
|
4025
|
+
function stripTeammateWrapper(text) {
|
|
4026
|
+
if (typeof text !== 'string') return text;
|
|
4027
|
+
const match = text.match(/^<teammate-message[^>]*>\n?([\s\S]*?)(?:<\/teammate-message>\s*)?$/);
|
|
4028
|
+
return match ? match[1].trim() : text;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
3331
4031
|
function escapeHtml(text) {
|
|
3332
4032
|
const div = document.createElement('div');
|
|
3333
4033
|
div.textContent = text;
|
|
@@ -3494,6 +4194,33 @@ document.addEventListener('click', (e) => {
|
|
|
3494
4194
|
return;
|
|
3495
4195
|
}
|
|
3496
4196
|
|
|
4197
|
+
const projectBtn = e.target.closest('.project-view-btn');
|
|
4198
|
+
if (projectBtn) {
|
|
4199
|
+
e.stopPropagation();
|
|
4200
|
+
const projectPath = projectBtn.dataset.projectPath;
|
|
4201
|
+
if (projectPath) fetchProjectView(projectPath);
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
if (e.target.closest('.pinned-ungroup-btn')) {
|
|
4206
|
+
e.stopPropagation();
|
|
4207
|
+
localStorage.setItem('groupPinnedSessions', 'false');
|
|
4208
|
+
renderSessions();
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
if (e.target.closest('.pinned-regroup-banner')) {
|
|
4213
|
+
localStorage.setItem('groupPinnedSessions', 'true');
|
|
4214
|
+
renderSessions();
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
const pinnedSubHeader = e.target.closest('.pinned-sub-header');
|
|
4219
|
+
if (pinnedSubHeader) {
|
|
4220
|
+
setGroupCollapsed(pinnedSubHeader, !collapsedProjectGroups.has(pinnedSubHeader.dataset.groupPath));
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
3497
4224
|
const header = e.target.closest('.project-group-header');
|
|
3498
4225
|
if (header) {
|
|
3499
4226
|
setGroupCollapsed(header, !collapsedProjectGroups.has(header.dataset.groupPath));
|
|
@@ -3731,8 +4458,9 @@ async function showSessionInfoModal(sessionId) {
|
|
|
3731
4458
|
// Fetch team config
|
|
3732
4459
|
let teamConfig = null;
|
|
3733
4460
|
if (session.isTeam) {
|
|
4461
|
+
const teamId = session.teamName || sessionId;
|
|
3734
4462
|
promises.push(
|
|
3735
|
-
fetch(`/api/teams/${
|
|
4463
|
+
fetch(`/api/teams/${teamId}`)
|
|
3736
4464
|
.then((r) => (r.ok ? r.json() : null))
|
|
3737
4465
|
.catch(() => null)
|
|
3738
4466
|
.then((data) => {
|
|
@@ -3804,6 +4532,9 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3804
4532
|
if (session.tasksDir) {
|
|
3805
4533
|
infoRows.push(['Tasks Dir', session.tasksDir, { openPath: session.tasksDir }]);
|
|
3806
4534
|
}
|
|
4535
|
+
if (session.sharedTaskList) {
|
|
4536
|
+
infoRows.push(['Shared Tasks', session.sharedTaskList]);
|
|
4537
|
+
}
|
|
3807
4538
|
if (teamConfig?.configPath) {
|
|
3808
4539
|
const configDir = teamConfig.configPath.replace(/[/\\][^/\\]+$/, '');
|
|
3809
4540
|
infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
|
|
@@ -3823,7 +4554,7 @@ function showInfoModal(session, teamConfig, tasks, planContent) {
|
|
|
3823
4554
|
} else {
|
|
3824
4555
|
html += `<span style="${plainStyle}" title="${copyVal}">${escapeHtml(value)}</span>`;
|
|
3825
4556
|
}
|
|
3826
|
-
const jsCopyVal = copyVal
|
|
4557
|
+
const jsCopyVal = _escapeForJsAttr(copyVal);
|
|
3827
4558
|
html += `<button onclick="navigator.clipboard.writeText('${jsCopyVal}'); this.textContent='✓'; setTimeout(() => this.textContent='Copy', 1000)" style="padding: 2px 8px; font-size: 11px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; color: var(--text-secondary); cursor: pointer; white-space: nowrap;">Copy</button>`;
|
|
3828
4559
|
});
|
|
3829
4560
|
html += `</div>`;
|
|
@@ -4101,6 +4832,7 @@ searchQuery = urlState.search || '';
|
|
|
4101
4832
|
|
|
4102
4833
|
loadPreferences();
|
|
4103
4834
|
pinnedSessionIds = loadPinnedSessions();
|
|
4835
|
+
stickySessionIds = loadStickySessions();
|
|
4104
4836
|
setupEventSource();
|
|
4105
4837
|
|
|
4106
4838
|
if (urlState.search) {
|
|
@@ -4109,7 +4841,13 @@ if (urlState.search) {
|
|
|
4109
4841
|
}
|
|
4110
4842
|
|
|
4111
4843
|
fetchSessions().then(async () => {
|
|
4112
|
-
if (urlState.
|
|
4844
|
+
if (urlState.projectView) {
|
|
4845
|
+
try {
|
|
4846
|
+
await fetchProjectView(atob(urlState.projectView));
|
|
4847
|
+
} catch (_) {
|
|
4848
|
+
showAllTasks();
|
|
4849
|
+
}
|
|
4850
|
+
} else if (urlState.session) {
|
|
4113
4851
|
await fetchTasks(urlState.session);
|
|
4114
4852
|
} else {
|
|
4115
4853
|
showAllTasks();
|
|
@@ -4127,7 +4865,13 @@ window.addEventListener('popstate', () => {
|
|
|
4127
4865
|
ownerFilter = s.owner || '';
|
|
4128
4866
|
searchQuery = s.search || '';
|
|
4129
4867
|
loadPreferences();
|
|
4130
|
-
if (s.
|
|
4868
|
+
if (s.projectView) {
|
|
4869
|
+
try {
|
|
4870
|
+
fetchProjectView(atob(s.projectView));
|
|
4871
|
+
} catch (_) {
|
|
4872
|
+
showAllTasks();
|
|
4873
|
+
}
|
|
4874
|
+
} else if (s.session) fetchTasks(s.session);
|
|
4131
4875
|
else showAllTasks();
|
|
4132
4876
|
if (s.messages !== messagePanelOpen) toggleMessagePanel();
|
|
4133
4877
|
});
|