claude-code-kanban 4.24.0 → 4.25.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/README.md +3 -1
- package/cli.js +1 -1
- package/install.js +14 -9
- package/lib/claude-dir.js +53 -0
- package/lib/open-editor.js +4 -1
- package/lib/parsers.js +54 -4
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/scripts/agent-spy.sh +2 -2
- package/plugin/plugins/claude-code-kanban/scripts/approval-gate.sh +6 -6
- package/plugin/plugins/claude-code-kanban/scripts/context-status.sh +2 -2
- package/plugin/plugins/claude-code-kanban/scripts/postman.js +2 -1
- package/public/app.js +235 -108
- package/public/index.html +7 -3
- package/public/style.css +62 -3
- package/server.js +14 -23
package/public/app.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
//#region STATE
|
|
2
|
+
// Set by the server in index.html: null for the default Claude dir, a per-dir prefix otherwise.
|
|
3
|
+
const STORAGE_NS = window.__STORAGE_NS__ ? `${window.__STORAGE_NS__}:` : '';
|
|
4
|
+
// Theme is hub-wide (echoed to every app via hub:theme), so it stays shared across config dirs.
|
|
5
|
+
const GLOBAL_KEYS = new Set(['theme', 'color-theme']);
|
|
6
|
+
const nsKey = (k) => (GLOBAL_KEYS.has(k) ? k : STORAGE_NS + k);
|
|
7
|
+
const store = {
|
|
8
|
+
keys() {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
11
|
+
const k = localStorage.key(i);
|
|
12
|
+
if (k.startsWith(STORAGE_NS)) out.push(k.slice(STORAGE_NS.length));
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
},
|
|
16
|
+
getItem: (k) => localStorage.getItem(nsKey(k)),
|
|
17
|
+
setItem: (k, v) => localStorage.setItem(nsKey(k), v),
|
|
18
|
+
removeItem: (k) => localStorage.removeItem(nsKey(k)),
|
|
19
|
+
};
|
|
20
|
+
|
|
2
21
|
// The baseline "nothing is filtered" state: what resetState() returns to, what updateUrl() omits
|
|
3
22
|
// from the query string, and what decides whether a control tints ember in the header summary.
|
|
4
23
|
const FILTER_DEFAULTS = { project: '__recent__', session: 'active', limit: '20' };
|
|
@@ -72,9 +91,7 @@ function getUrlState() {
|
|
|
72
91
|
project: params.get('project'),
|
|
73
92
|
owner: params.get('owner'),
|
|
74
93
|
search: params.get('search'),
|
|
75
|
-
messages: params.has('messages')
|
|
76
|
-
? params.get('messages') === '1'
|
|
77
|
-
: localStorage.getItem('message-panel-open') === 'true',
|
|
94
|
+
messages: params.has('messages') ? params.get('messages') === '1' : store.getItem('message-panel-open') === 'true',
|
|
78
95
|
projectView: params.get('projectView'),
|
|
79
96
|
};
|
|
80
97
|
}
|
|
@@ -107,12 +124,12 @@ function persistLastView() {
|
|
|
107
124
|
// base URL on reload, so the query string alone doesn't survive a hub refresh.
|
|
108
125
|
project: filterProject,
|
|
109
126
|
};
|
|
110
|
-
|
|
127
|
+
store.setItem(LAST_VIEW_KEY, JSON.stringify(data));
|
|
111
128
|
} catch (_) {}
|
|
112
129
|
}
|
|
113
130
|
function loadLastView() {
|
|
114
131
|
try {
|
|
115
|
-
return JSON.parse(
|
|
132
|
+
return JSON.parse(store.getItem(LAST_VIEW_KEY)) || null;
|
|
116
133
|
} catch (_) {
|
|
117
134
|
return null;
|
|
118
135
|
}
|
|
@@ -122,7 +139,7 @@ function loadLastView() {
|
|
|
122
139
|
function resetState() {
|
|
123
140
|
history.replaceState(null, '', window.location.pathname);
|
|
124
141
|
try {
|
|
125
|
-
|
|
142
|
+
store.removeItem(LAST_VIEW_KEY);
|
|
126
143
|
} catch (_) {}
|
|
127
144
|
sessionFilter = FILTER_DEFAULTS.session;
|
|
128
145
|
sessionLimit = FILTER_DEFAULTS.limit;
|
|
@@ -274,7 +291,7 @@ function toggleSection(containerId, chevronId) {
|
|
|
274
291
|
const chevron = document.getElementById(chevronId);
|
|
275
292
|
const collapsed = container.classList.toggle('collapsed');
|
|
276
293
|
chevron.classList.toggle('rotated', collapsed);
|
|
277
|
-
|
|
294
|
+
store.setItem(`${containerId}Collapsed`, collapsed);
|
|
278
295
|
}
|
|
279
296
|
|
|
280
297
|
function isWaitingSession(s) {
|
|
@@ -369,7 +386,7 @@ function setActivityFilter(kind) {
|
|
|
369
386
|
} else {
|
|
370
387
|
toggleActivityKind(kind);
|
|
371
388
|
}
|
|
372
|
-
|
|
389
|
+
store.setItem('activityFilter', JSON.stringify([...activityFilter]));
|
|
373
390
|
// active/waiting only make sense with the active session filter on
|
|
374
391
|
const targetFilter = activityFilter.size > 0 ? 'active' : sessionFilter;
|
|
375
392
|
if (targetFilter !== sessionFilter) {
|
|
@@ -581,7 +598,7 @@ async function refreshProjectAgents() {
|
|
|
581
598
|
function toggleMessagePanel() {
|
|
582
599
|
const panel = document.getElementById('message-panel');
|
|
583
600
|
messagePanelOpen = !messagePanelOpen;
|
|
584
|
-
|
|
601
|
+
store.setItem('message-panel-open', messagePanelOpen);
|
|
585
602
|
panel.classList.toggle('visible', messagePanelOpen);
|
|
586
603
|
document.getElementById('message-toggle')?.classList.toggle('active', messagePanelOpen);
|
|
587
604
|
if (messagePanelOpen && currentSessionId) {
|
|
@@ -596,7 +613,7 @@ async function openSessionWithBookmarks(sessionId) {
|
|
|
596
613
|
if (!messagePanelOpen) {
|
|
597
614
|
const panel = document.getElementById('message-panel');
|
|
598
615
|
messagePanelOpen = true;
|
|
599
|
-
|
|
616
|
+
store.setItem('message-panel-open', 'true');
|
|
600
617
|
panel.classList.add('visible');
|
|
601
618
|
document.getElementById('message-toggle')?.classList.add('active');
|
|
602
619
|
}
|
|
@@ -687,11 +704,19 @@ function openLiveLatestMessage() {
|
|
|
687
704
|
}
|
|
688
705
|
}
|
|
689
706
|
|
|
707
|
+
const MSG_PAGE_LIMIT = 15;
|
|
708
|
+
|
|
709
|
+
async function fetchSessionMessagesPage(sessionId, { before } = {}) {
|
|
710
|
+
const qs = new URLSearchParams({ limit: MSG_PAGE_LIMIT });
|
|
711
|
+
if (before) qs.set('before', before);
|
|
712
|
+
const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/messages?${qs}`);
|
|
713
|
+
return res.ok ? res.json() : null;
|
|
714
|
+
}
|
|
715
|
+
|
|
690
716
|
async function fetchMessages(sessionId) {
|
|
691
717
|
try {
|
|
692
|
-
const
|
|
693
|
-
if (!
|
|
694
|
-
const data = await res.json();
|
|
718
|
+
const data = await fetchSessionMessagesPage(sessionId);
|
|
719
|
+
if (!data) return;
|
|
695
720
|
let agentEnriched = false;
|
|
696
721
|
for (const m of data.messages) {
|
|
697
722
|
if (m.agentId && m.agentPrompt) {
|
|
@@ -742,10 +767,8 @@ async function loadOlderMessages() {
|
|
|
742
767
|
loader.textContent = 'Loading...';
|
|
743
768
|
container.prepend(loader);
|
|
744
769
|
try {
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
if (!res.ok) return;
|
|
748
|
-
const data = await res.json();
|
|
770
|
+
const data = await fetchSessionMessagesPage(currentSessionId, { before: currentMessages[0].timestamp });
|
|
771
|
+
if (!data) return;
|
|
749
772
|
msgHasMore = data.hasMore && data.messages.length > 0;
|
|
750
773
|
if (data.messages.length) {
|
|
751
774
|
loader.remove();
|
|
@@ -920,7 +943,12 @@ function buildAssistantPreview(text) {
|
|
|
920
943
|
return { md: out.join('\n').trim(), remainder };
|
|
921
944
|
}
|
|
922
945
|
|
|
946
|
+
// Set for the duration of a read-only renderMessageList call: the pin, agent-link and
|
|
947
|
+
// agent-log controls act on the live panel's state, so a display-only list omits them.
|
|
948
|
+
let msgRenderReadOnly = false;
|
|
949
|
+
|
|
923
950
|
function renderMsgPinBtn(m, i) {
|
|
951
|
+
if (msgRenderReadOnly) return '';
|
|
924
952
|
const pinned = isPinned(m);
|
|
925
953
|
return `<button class="msg-pin-btn${pinned ? ' pinned' : ''}" onclick="event.stopPropagation();togglePin(${i})" title="${pinned ? 'Unpin' : 'Pin'} message">${PIN_SVG}</button>`;
|
|
926
954
|
}
|
|
@@ -980,6 +1008,7 @@ function renderPinnedSection() {
|
|
|
980
1008
|
}
|
|
981
1009
|
|
|
982
1010
|
function resolveAgentLogBtn(m) {
|
|
1011
|
+
if (msgRenderReadOnly) return '';
|
|
983
1012
|
if (m.tool === 'Agent' && m.agentId) return agentLogButton(m.agentId);
|
|
984
1013
|
if (m.tool === 'SendMessage' && m.params?.to) {
|
|
985
1014
|
const recipient = currentAgents.find((a) => (a.type || a.name) === m.params.to);
|
|
@@ -998,7 +1027,7 @@ function renderToolItem(m, i, compact) {
|
|
|
998
1027
|
// from the live agent-activity files), so the ⇗ link, agent-log button, and modal
|
|
999
1028
|
// click all light up DURING the run, identical to post-completion.
|
|
1000
1029
|
const agentLink =
|
|
1001
|
-
m.tool === 'Agent' && m.agentId
|
|
1030
|
+
m.tool === 'Agent' && m.agentId && !msgRenderReadOnly
|
|
1002
1031
|
? ` <span class="msg-agent-link" title="View agent" onclick="event.stopPropagation();showAgentModal('${escAttrJs(m.agentId)}')">⇗</span>`
|
|
1003
1032
|
: '';
|
|
1004
1033
|
// Usage chip (tokens · tools · duration) on completed Agent rows — same stats a
|
|
@@ -1021,7 +1050,16 @@ function renderToolItem(m, i, compact) {
|
|
|
1021
1050
|
</div>`;
|
|
1022
1051
|
}
|
|
1023
1052
|
|
|
1024
|
-
function renderMessageList(messages) {
|
|
1053
|
+
function renderMessageList(messages, { readOnly = false } = {}) {
|
|
1054
|
+
msgRenderReadOnly = readOnly;
|
|
1055
|
+
try {
|
|
1056
|
+
return renderMessageListBody(messages);
|
|
1057
|
+
} finally {
|
|
1058
|
+
msgRenderReadOnly = false;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function renderMessageListBody(messages) {
|
|
1025
1063
|
const parts = [];
|
|
1026
1064
|
let i = 0;
|
|
1027
1065
|
while (i < messages.length) {
|
|
@@ -1479,14 +1517,14 @@ function getPinId(m) {
|
|
|
1479
1517
|
|
|
1480
1518
|
function loadPins(sessionId) {
|
|
1481
1519
|
try {
|
|
1482
|
-
return JSON.parse(
|
|
1520
|
+
return JSON.parse(store.getItem(`pinned-messages-${sessionId}`)) || [];
|
|
1483
1521
|
} catch {
|
|
1484
1522
|
return [];
|
|
1485
1523
|
}
|
|
1486
1524
|
}
|
|
1487
1525
|
|
|
1488
1526
|
function savePins(sessionId, pins) {
|
|
1489
|
-
|
|
1527
|
+
store.setItem(`pinned-messages-${sessionId}`, JSON.stringify(pins));
|
|
1490
1528
|
}
|
|
1491
1529
|
|
|
1492
1530
|
function isPinned(m) {
|
|
@@ -1652,7 +1690,7 @@ const deferredPinPlacement = new Set();
|
|
|
1652
1690
|
|
|
1653
1691
|
function loadPinnedSessions() {
|
|
1654
1692
|
try {
|
|
1655
|
-
return new Set(JSON.parse(
|
|
1693
|
+
return new Set(JSON.parse(store.getItem('pinned-sessions')) || []);
|
|
1656
1694
|
} catch {
|
|
1657
1695
|
return new Set();
|
|
1658
1696
|
}
|
|
@@ -1660,15 +1698,15 @@ function loadPinnedSessions() {
|
|
|
1660
1698
|
|
|
1661
1699
|
function loadStickySessions() {
|
|
1662
1700
|
try {
|
|
1663
|
-
return new Set(JSON.parse(
|
|
1701
|
+
return new Set(JSON.parse(store.getItem('sticky-sessions')) || []);
|
|
1664
1702
|
} catch {
|
|
1665
1703
|
return new Set();
|
|
1666
1704
|
}
|
|
1667
1705
|
}
|
|
1668
1706
|
|
|
1669
1707
|
function savePinnedSessions() {
|
|
1670
|
-
|
|
1671
|
-
|
|
1708
|
+
store.setItem('pinned-sessions', JSON.stringify([...pinnedSessionIds]));
|
|
1709
|
+
store.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
|
|
1672
1710
|
}
|
|
1673
1711
|
|
|
1674
1712
|
// Mirror pin state to server so it can be queried by the CLI. UI remains source of truth for itself.
|
|
@@ -2022,12 +2060,12 @@ function _applyModalFullscreen(modalId, on) {
|
|
|
2022
2060
|
function toggleModalFullscreen(modalId) {
|
|
2023
2061
|
const on = !_modalEl(modalId).classList.contains('fullscreen');
|
|
2024
2062
|
_applyModalFullscreen(modalId, on);
|
|
2025
|
-
|
|
2063
|
+
store.setItem(`modal-fullscreen-${modalId}`, String(on));
|
|
2026
2064
|
}
|
|
2027
2065
|
|
|
2028
2066
|
function loadModalFullscreen() {
|
|
2029
2067
|
_forEachFullscreenModal((modalId) => {
|
|
2030
|
-
if (
|
|
2068
|
+
if (store.getItem(`modal-fullscreen-${modalId}`) === 'true') _applyModalFullscreen(modalId, true);
|
|
2031
2069
|
});
|
|
2032
2070
|
}
|
|
2033
2071
|
|
|
@@ -2064,8 +2102,8 @@ function initModalResize() {
|
|
|
2064
2102
|
_forEachFullscreenModal((modalId, modal) => {
|
|
2065
2103
|
const wKey = `modal-width-${modalId}`;
|
|
2066
2104
|
const hKey = `modal-height-${modalId}`;
|
|
2067
|
-
const savedW =
|
|
2068
|
-
const savedH =
|
|
2105
|
+
const savedW = store.getItem(wKey);
|
|
2106
|
+
const savedH = store.getItem(hKey);
|
|
2069
2107
|
if (savedW && savedH) {
|
|
2070
2108
|
_setModalUserSize(modal, savedW, savedH);
|
|
2071
2109
|
modal.classList.add('user-sized');
|
|
@@ -2099,16 +2137,16 @@ function initModalResize() {
|
|
|
2099
2137
|
},
|
|
2100
2138
|
onEnd() {
|
|
2101
2139
|
if (w && h) {
|
|
2102
|
-
|
|
2103
|
-
|
|
2140
|
+
store.setItem(wKey, `${w}px`);
|
|
2141
|
+
store.setItem(hKey, `${h}px`);
|
|
2104
2142
|
}
|
|
2105
2143
|
},
|
|
2106
2144
|
});
|
|
2107
2145
|
|
|
2108
2146
|
handle.addEventListener('dblclick', () => {
|
|
2109
2147
|
modal.classList.remove('user-sized');
|
|
2110
|
-
|
|
2111
|
-
|
|
2148
|
+
store.removeItem(wKey);
|
|
2149
|
+
store.removeItem(hKey);
|
|
2112
2150
|
});
|
|
2113
2151
|
});
|
|
2114
2152
|
}
|
|
@@ -2116,7 +2154,7 @@ function initModalResize() {
|
|
|
2116
2154
|
const MODAL_ZOOM_KEY = 'modal-zoom';
|
|
2117
2155
|
const MODAL_ZOOM_MIN = 0.7;
|
|
2118
2156
|
const MODAL_ZOOM_MAX = 2.0;
|
|
2119
|
-
let modalZoom = clampModalZoom(Number.parseFloat(
|
|
2157
|
+
let modalZoom = clampModalZoom(Number.parseFloat(store.getItem(MODAL_ZOOM_KEY)) || 1);
|
|
2120
2158
|
|
|
2121
2159
|
function clampModalZoom(v) {
|
|
2122
2160
|
return Math.min(MODAL_ZOOM_MAX, Math.max(MODAL_ZOOM_MIN, v));
|
|
@@ -2131,7 +2169,7 @@ function adjustModalZoom(delta) {
|
|
|
2131
2169
|
const next = delta === 0 ? 1 : clampModalZoom(Math.round((modalZoom + delta) * 10) / 10);
|
|
2132
2170
|
if (next === modalZoom && delta !== 0) return;
|
|
2133
2171
|
modalZoom = next;
|
|
2134
|
-
|
|
2172
|
+
store.setItem(MODAL_ZOOM_KEY, String(modalZoom));
|
|
2135
2173
|
applyModalZoom();
|
|
2136
2174
|
showToast(`Text ${Math.round(modalZoom * 100)}%`);
|
|
2137
2175
|
}
|
|
@@ -2730,7 +2768,7 @@ function renderAgentFooter() {
|
|
|
2730
2768
|
footer.classList.add('visible');
|
|
2731
2769
|
label.textContent = `Agents Log (${visible.length})`;
|
|
2732
2770
|
|
|
2733
|
-
const collapsed =
|
|
2771
|
+
const collapsed = store.getItem('agentFooterCollapsed') === 'true';
|
|
2734
2772
|
footer.classList.toggle('collapsed', collapsed);
|
|
2735
2773
|
document.getElementById('agent-footer-toggle').innerHTML = collapsed ? '▴' : '▾';
|
|
2736
2774
|
|
|
@@ -2808,7 +2846,7 @@ function toggleAgentFooter() {
|
|
|
2808
2846
|
const footer = document.getElementById('agent-footer');
|
|
2809
2847
|
const collapsed = !footer.classList.contains('collapsed');
|
|
2810
2848
|
footer.classList.toggle('collapsed', collapsed);
|
|
2811
|
-
|
|
2849
|
+
store.setItem('agentFooterCollapsed', collapsed);
|
|
2812
2850
|
document.getElementById('agent-footer-toggle').innerHTML = collapsed ? '▴' : '▾';
|
|
2813
2851
|
}
|
|
2814
2852
|
|
|
@@ -2963,11 +3001,18 @@ let revealedStorageSessionId = null;
|
|
|
2963
3001
|
// Opens a session and scrolls the sidebar to it, refetching first when the id
|
|
2964
3002
|
// fell outside the session list the current filters asked for.
|
|
2965
3003
|
async function revealSession(id) {
|
|
2966
|
-
|
|
3004
|
+
let session = sessions.find((s) => s.id === id);
|
|
3005
|
+
if (!session) {
|
|
2967
3006
|
lastSessionsHash = '';
|
|
2968
3007
|
await fetchSessions();
|
|
3008
|
+
session = sessions.find((s) => s.id === id);
|
|
2969
3009
|
}
|
|
3010
|
+
const uncollapsed = session ? uncollapseFor(session) : false;
|
|
3011
|
+
if (uncollapsed) persistCollapsedGroups();
|
|
3012
|
+
expandSidebar();
|
|
2970
3013
|
await fetchTasks(id);
|
|
3014
|
+
// fetchTasks skips its render when the session and task hash are unchanged.
|
|
3015
|
+
if (uncollapsed) renderSessions();
|
|
2971
3016
|
const el = document.querySelector(`.session-item[data-session-id="${escSel(id)}"]`);
|
|
2972
3017
|
if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2973
3018
|
}
|
|
@@ -3186,7 +3231,7 @@ function renderSessions() {
|
|
|
3186
3231
|
const showCtx = !!session.contextStatus;
|
|
3187
3232
|
const linkedDocsCount = getSessionPreviewPaths(session.id).length;
|
|
3188
3233
|
const bookmarksCount = loadPins(session.id).length;
|
|
3189
|
-
const hasScratchpad = !!(
|
|
3234
|
+
const hasScratchpad = !!(store.getItem(_sessionScratchpadKey(session.id)) || '').trim();
|
|
3190
3235
|
const tempClass = session.hasRecentLog || session.inProgress || session.hasWaitingForUser ? 'warm' : 'stale';
|
|
3191
3236
|
const sid = escAttrJs(session.id);
|
|
3192
3237
|
return `
|
|
@@ -3222,7 +3267,7 @@ function renderSessions() {
|
|
|
3222
3267
|
`;
|
|
3223
3268
|
};
|
|
3224
3269
|
|
|
3225
|
-
const groupPinned =
|
|
3270
|
+
const groupPinned = store.getItem('groupPinnedSessions') !== 'false';
|
|
3226
3271
|
const pinWeight = (s) => (isPlacedSticky(s.id) ? 2 : isPlacedPinned(s.id) && !isSessionActive(s) ? 1 : 0);
|
|
3227
3272
|
const pinSort = (a, b) => pinWeight(b) - pinWeight(a);
|
|
3228
3273
|
const countHtml = (arr) => {
|
|
@@ -3296,7 +3341,7 @@ function renderSessions() {
|
|
|
3296
3341
|
</div>
|
|
3297
3342
|
<div class="project-group-breadcrumb${nestedCls}" data-full-path="${escapedPath}" title="Click to copy path">${breadcrumbHtml}</div>
|
|
3298
3343
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}${nestedCls}" data-project-path="${escapedPath}">
|
|
3299
|
-
${renderGroupSessions(projectSessions,
|
|
3344
|
+
${renderGroupSessions(projectSessions, pinKey(projectPath))}
|
|
3300
3345
|
</div>
|
|
3301
3346
|
`;
|
|
3302
3347
|
};
|
|
@@ -3435,7 +3480,7 @@ function renderSessions() {
|
|
|
3435
3480
|
${countHtml(ungrouped)}
|
|
3436
3481
|
</div>
|
|
3437
3482
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}">
|
|
3438
|
-
${renderGroupSessions(ungrouped, '
|
|
3483
|
+
${renderGroupSessions(ungrouped, pinKey('__ungrouped__'))}
|
|
3439
3484
|
</div>
|
|
3440
3485
|
`;
|
|
3441
3486
|
} else {
|
|
@@ -3885,9 +3930,15 @@ function sgKey(id) {
|
|
|
3885
3930
|
return `__group_${id}__`;
|
|
3886
3931
|
}
|
|
3887
3932
|
|
|
3933
|
+
// Collapse key of a project's pinned sub-section. The ungrouped spelling predates this helper
|
|
3934
|
+
// and is kept so saved collapse state still matches.
|
|
3935
|
+
function pinKey(projectPath) {
|
|
3936
|
+
return projectPath === '__ungrouped__' ? '__pinned___ungrouped__' : `__pinned_${projectPath}__`;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3888
3939
|
function loadSessionGroups() {
|
|
3889
3940
|
try {
|
|
3890
|
-
const list = JSON.parse(
|
|
3941
|
+
const list = JSON.parse(store.getItem(SESSION_GROUPS_KEY) || 'null')?.groups;
|
|
3891
3942
|
sessionGroups = (Array.isArray(list) ? list : [])
|
|
3892
3943
|
.filter((g) => g && typeof g.id === 'string')
|
|
3893
3944
|
.map((g) => ({
|
|
@@ -3909,7 +3960,7 @@ function loadSessionGroups() {
|
|
|
3909
3960
|
|
|
3910
3961
|
function persistSessionGroups() {
|
|
3911
3962
|
try {
|
|
3912
|
-
|
|
3963
|
+
store.setItem(SESSION_GROUPS_KEY, JSON.stringify({ version: 1, groups: sessionGroups }));
|
|
3913
3964
|
} catch (_) {}
|
|
3914
3965
|
}
|
|
3915
3966
|
|
|
@@ -4571,7 +4622,7 @@ function setGroupCollapsed(header, collapsed) {
|
|
|
4571
4622
|
|
|
4572
4623
|
function persistCollapsedGroups() {
|
|
4573
4624
|
try {
|
|
4574
|
-
|
|
4625
|
+
store.setItem('collapsedGroups', JSON.stringify([...collapsedProjectGroups]));
|
|
4575
4626
|
} catch (_) {}
|
|
4576
4627
|
}
|
|
4577
4628
|
|
|
@@ -4590,20 +4641,35 @@ function expandActiveGroups({ onlyNew = false } = {}) {
|
|
|
4590
4641
|
if (!isSessionActive(s)) continue;
|
|
4591
4642
|
activeIds.add(s.id);
|
|
4592
4643
|
if (onlyNew && (!primed || prevActiveSessionIds.has(s.id))) continue;
|
|
4593
|
-
if (
|
|
4594
|
-
// A named group wrapping that project (or the session itself) would keep it hidden.
|
|
4595
|
-
const group = sgGroupForSession(s);
|
|
4596
|
-
if (group && collapsedProjectGroups.delete(sgKey(group.id))) changed = true;
|
|
4597
|
-
// Which section holds it depends on the view, so open every one rather than re-deriving it.
|
|
4598
|
-
for (const key of [SECTION_GROUPS, SECTION_PROJECTS, SECTION_SESSIONS]) {
|
|
4599
|
-
if (collapsedProjectGroups.delete(key)) changed = true;
|
|
4600
|
-
}
|
|
4644
|
+
if (uncollapseFor(s)) changed = true;
|
|
4601
4645
|
}
|
|
4602
4646
|
prevActiveSessionIds = activeIds;
|
|
4603
4647
|
if (changed) persistCollapsedGroups();
|
|
4604
4648
|
// renderSessions() re-renders headers/containers from the updated set
|
|
4605
4649
|
}
|
|
4606
4650
|
|
|
4651
|
+
// Drops every collapse key that could hide this session: its project, a named group wrapping
|
|
4652
|
+
// the project or the session itself, the pinned sub-sections of both, and — since which section
|
|
4653
|
+
// holds it depends on the view — every section header. Returns whether anything changed.
|
|
4654
|
+
function uncollapseFor(session) {
|
|
4655
|
+
const project = session.project || '__ungrouped__';
|
|
4656
|
+
const group = sgGroupForSession(session);
|
|
4657
|
+
const keys = [
|
|
4658
|
+
project,
|
|
4659
|
+
pinKey(project),
|
|
4660
|
+
group && sgKey(group.id),
|
|
4661
|
+
group && `__pinned_group_${group.id}__`,
|
|
4662
|
+
SECTION_GROUPS,
|
|
4663
|
+
SECTION_PROJECTS,
|
|
4664
|
+
SECTION_SESSIONS,
|
|
4665
|
+
];
|
|
4666
|
+
let changed = false;
|
|
4667
|
+
for (const key of keys) {
|
|
4668
|
+
if (key && collapsedProjectGroups.delete(key)) changed = true;
|
|
4669
|
+
}
|
|
4670
|
+
return changed;
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4607
4673
|
function isGroupHeader(el) {
|
|
4608
4674
|
return el.matches(COLLAPSIBLE_HEADER_SELECTOR);
|
|
4609
4675
|
}
|
|
@@ -4667,16 +4733,12 @@ function activateSelectedSession(items) {
|
|
|
4667
4733
|
}
|
|
4668
4734
|
|
|
4669
4735
|
function setFocusZone(zone) {
|
|
4670
|
-
const sidebar = document.querySelector('.sidebar');
|
|
4671
4736
|
clearKbSelection();
|
|
4672
4737
|
clearTaskSelection();
|
|
4673
4738
|
|
|
4674
4739
|
focusZone = zone;
|
|
4675
4740
|
if (zone === 'sidebar') {
|
|
4676
|
-
|
|
4677
|
-
sidebar.classList.remove('collapsed');
|
|
4678
|
-
localStorage.setItem('sidebar-collapsed', false);
|
|
4679
|
-
}
|
|
4741
|
+
expandSidebar();
|
|
4680
4742
|
const items = getNavigableItems();
|
|
4681
4743
|
if (items.length > 0) {
|
|
4682
4744
|
const activeIdx = items.findIndex((el) => el.classList.contains('active'));
|
|
@@ -5260,7 +5322,7 @@ function showScratchpad(keyOverride) {
|
|
|
5260
5322
|
_scratchpadKeyOverride = keyOverride || null;
|
|
5261
5323
|
const key = _scratchpadKey();
|
|
5262
5324
|
if (!key) return;
|
|
5263
|
-
_scratchpadTextarea.value =
|
|
5325
|
+
_scratchpadTextarea.value = store.getItem(key) || '';
|
|
5264
5326
|
_scratchpadCharcount.textContent = `${_scratchpadTextarea.value.length} chars`;
|
|
5265
5327
|
_scratchpadModal.classList.add('visible');
|
|
5266
5328
|
_scratchpadTextarea.focus();
|
|
@@ -5280,12 +5342,12 @@ function saveScratchpad() {
|
|
|
5280
5342
|
const key = _scratchpadKey();
|
|
5281
5343
|
if (!key) return;
|
|
5282
5344
|
const val = _scratchpadTextarea.value;
|
|
5283
|
-
const had = !!(
|
|
5345
|
+
const had = !!(store.getItem(key) || '').trim();
|
|
5284
5346
|
const has = !!val.trim();
|
|
5285
5347
|
if (has) {
|
|
5286
|
-
|
|
5348
|
+
store.setItem(key, val);
|
|
5287
5349
|
} else {
|
|
5288
|
-
|
|
5350
|
+
store.removeItem(key);
|
|
5289
5351
|
}
|
|
5290
5352
|
if (had !== has && _isSessionScratchpadKey(key)) {
|
|
5291
5353
|
renderSessions();
|
|
@@ -5307,9 +5369,8 @@ _scratchpadTextarea.addEventListener('input', () => {
|
|
|
5307
5369
|
|
|
5308
5370
|
function _getStorageTotalSize() {
|
|
5309
5371
|
let bytes = 0;
|
|
5310
|
-
for (
|
|
5311
|
-
|
|
5312
|
-
bytes += k.length + localStorage.getItem(k).length;
|
|
5372
|
+
for (const k of store.keys()) {
|
|
5373
|
+
bytes += k.length + store.getItem(k).length;
|
|
5313
5374
|
}
|
|
5314
5375
|
return bytes * 2; // UTF-16
|
|
5315
5376
|
}
|
|
@@ -5399,12 +5460,11 @@ function _renderStorageSessions() {
|
|
|
5399
5460
|
const pinnedIds = [...new Set([...pinnedSessionIds, ...stickySessionIds])];
|
|
5400
5461
|
|
|
5401
5462
|
const msgMap = new Map();
|
|
5402
|
-
for (
|
|
5403
|
-
const key = localStorage.key(i);
|
|
5463
|
+
for (const key of store.keys()) {
|
|
5404
5464
|
if (!key.startsWith('pinned-messages-')) continue;
|
|
5405
5465
|
const sid = key.slice('pinned-messages-'.length);
|
|
5406
5466
|
try {
|
|
5407
|
-
const pins = JSON.parse(
|
|
5467
|
+
const pins = JSON.parse(store.getItem(key)) || [];
|
|
5408
5468
|
if (pins.length) msgMap.set(sid, { pins, key });
|
|
5409
5469
|
} catch {}
|
|
5410
5470
|
}
|
|
@@ -5487,7 +5547,7 @@ function _storageUnpinSession(id) {
|
|
|
5487
5547
|
}
|
|
5488
5548
|
|
|
5489
5549
|
function _storageClearSessionPins(sessionId) {
|
|
5490
|
-
|
|
5550
|
+
store.removeItem(`pinned-messages-${sessionId}`);
|
|
5491
5551
|
if (currentSessionId === sessionId) {
|
|
5492
5552
|
currentPins = [];
|
|
5493
5553
|
const el = document.getElementById('message-panel-pinned');
|
|
@@ -5500,12 +5560,12 @@ function _storageClearSessionPins(sessionId) {
|
|
|
5500
5560
|
function _storageUnpinMessage(sessionId, pinId) {
|
|
5501
5561
|
const key = `pinned-messages-${sessionId}`;
|
|
5502
5562
|
try {
|
|
5503
|
-
const pins = JSON.parse(
|
|
5563
|
+
const pins = JSON.parse(store.getItem(key)) || [];
|
|
5504
5564
|
const idx = pins.findIndex((p) => p.id === pinId);
|
|
5505
5565
|
if (idx < 0) return;
|
|
5506
5566
|
pins.splice(idx, 1);
|
|
5507
|
-
if (pins.length)
|
|
5508
|
-
else
|
|
5567
|
+
if (pins.length) store.setItem(key, JSON.stringify(pins));
|
|
5568
|
+
else store.removeItem(key);
|
|
5509
5569
|
if (currentSessionId === sessionId) {
|
|
5510
5570
|
currentPins = pins;
|
|
5511
5571
|
const el = document.getElementById('message-panel-pinned');
|
|
@@ -5518,10 +5578,9 @@ function _storageUnpinMessage(sessionId, pinId) {
|
|
|
5518
5578
|
|
|
5519
5579
|
function _renderStorageScratchpads() {
|
|
5520
5580
|
const allItems = [];
|
|
5521
|
-
for (
|
|
5522
|
-
const key = localStorage.key(i);
|
|
5581
|
+
for (const key of store.keys()) {
|
|
5523
5582
|
if (!key.startsWith('scratchpad-')) continue;
|
|
5524
|
-
const val =
|
|
5583
|
+
const val = store.getItem(key) || '';
|
|
5525
5584
|
const isProject = key.startsWith('scratchpad-project:');
|
|
5526
5585
|
const id = isProject ? key.slice('scratchpad-project:'.length) : key.slice('scratchpad-'.length);
|
|
5527
5586
|
allItems.push({ key, id, isProject, chars: val.length });
|
|
@@ -5590,7 +5649,7 @@ function _storagePreviewPin(sessionId, pinId) {
|
|
|
5590
5649
|
closeStorageManager();
|
|
5591
5650
|
const key = `pinned-messages-${sessionId}`;
|
|
5592
5651
|
try {
|
|
5593
|
-
const pins = JSON.parse(
|
|
5652
|
+
const pins = JSON.parse(store.getItem(key)) || [];
|
|
5594
5653
|
const pin = pins.find((p) => p.id === pinId);
|
|
5595
5654
|
if (!pin) return;
|
|
5596
5655
|
document.getElementById('msg-detail-pin-btn').style.display = 'none';
|
|
@@ -5604,18 +5663,17 @@ function _storagePreviewPin(sessionId, pinId) {
|
|
|
5604
5663
|
}
|
|
5605
5664
|
|
|
5606
5665
|
function _storageDeleteScratchpad(key) {
|
|
5607
|
-
|
|
5666
|
+
store.removeItem(key);
|
|
5608
5667
|
_renderStorageTab();
|
|
5609
5668
|
_updateStorageTotal();
|
|
5610
5669
|
}
|
|
5611
5670
|
|
|
5612
5671
|
function _renderStorageLinkedDocs() {
|
|
5613
5672
|
const entries = [];
|
|
5614
|
-
for (
|
|
5615
|
-
const key = localStorage.key(i);
|
|
5673
|
+
for (const key of store.keys()) {
|
|
5616
5674
|
if (!key.startsWith(PREVIEW_STORAGE_PREFIX)) continue;
|
|
5617
5675
|
try {
|
|
5618
|
-
const arr = JSON.parse(
|
|
5676
|
+
const arr = JSON.parse(store.getItem(key)) || [];
|
|
5619
5677
|
if (Array.isArray(arr) && arr.length) {
|
|
5620
5678
|
entries.push({ sessionId: key.slice(PREVIEW_STORAGE_PREFIX.length), paths: arr });
|
|
5621
5679
|
}
|
|
@@ -5679,7 +5737,7 @@ function _storageUnlinkDoc(sessionId, path) {
|
|
|
5679
5737
|
}
|
|
5680
5738
|
|
|
5681
5739
|
function _storageClearLinkedDocs(sessionId) {
|
|
5682
|
-
|
|
5740
|
+
store.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
|
|
5683
5741
|
afterLinkedDocsChanged(sessionId);
|
|
5684
5742
|
}
|
|
5685
5743
|
|
|
@@ -5689,8 +5747,7 @@ function _findOrphanedKeys() {
|
|
|
5689
5747
|
const orphaned = [];
|
|
5690
5748
|
for (const id of pinnedSessionIds) if (!known.has(id)) orphaned.push(`__pinned__${id}`);
|
|
5691
5749
|
for (const id of stickySessionIds) if (!known.has(id)) orphaned.push(`__sticky__${id}`);
|
|
5692
|
-
for (
|
|
5693
|
-
const key = localStorage.key(i);
|
|
5750
|
+
for (const key of store.keys()) {
|
|
5694
5751
|
if (key.startsWith('pinned-messages-')) {
|
|
5695
5752
|
if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
|
|
5696
5753
|
} else if (_isSessionScratchpadKey(key)) {
|
|
@@ -5725,7 +5782,7 @@ function cleanupOrphanedStorage() {
|
|
|
5725
5782
|
stickySessionIds.delete(key.slice('__sticky__'.length));
|
|
5726
5783
|
pinsChanged = true;
|
|
5727
5784
|
} else {
|
|
5728
|
-
|
|
5785
|
+
store.removeItem(key);
|
|
5729
5786
|
}
|
|
5730
5787
|
}
|
|
5731
5788
|
if (pinsChanged) savePinnedSessions();
|
|
@@ -6102,7 +6159,7 @@ let currentPreviewPath = null;
|
|
|
6102
6159
|
function getSessionPreviewPaths(sessionId) {
|
|
6103
6160
|
if (!sessionId) return [];
|
|
6104
6161
|
try {
|
|
6105
|
-
const raw =
|
|
6162
|
+
const raw = store.getItem(PREVIEW_STORAGE_PREFIX + sessionId);
|
|
6106
6163
|
const arr = raw ? JSON.parse(raw) : [];
|
|
6107
6164
|
return Array.isArray(arr) ? arr : [];
|
|
6108
6165
|
} catch {
|
|
@@ -6114,14 +6171,14 @@ function addSessionPreviewPath(sessionId, filePath) {
|
|
|
6114
6171
|
if (!sessionId || !filePath) return;
|
|
6115
6172
|
const paths = getSessionPreviewPaths(sessionId).filter((p) => p !== filePath);
|
|
6116
6173
|
paths.unshift(filePath);
|
|
6117
|
-
|
|
6174
|
+
store.setItem(PREVIEW_STORAGE_PREFIX + sessionId, JSON.stringify(paths.slice(0, 20)));
|
|
6118
6175
|
}
|
|
6119
6176
|
|
|
6120
6177
|
function removeSessionPreviewPath(sessionId, filePath) {
|
|
6121
6178
|
if (!sessionId) return;
|
|
6122
6179
|
const paths = getSessionPreviewPaths(sessionId).filter((p) => p !== filePath);
|
|
6123
|
-
if (paths.length)
|
|
6124
|
-
else
|
|
6180
|
+
if (paths.length) store.setItem(PREVIEW_STORAGE_PREFIX + sessionId, JSON.stringify(paths));
|
|
6181
|
+
else store.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
|
|
6125
6182
|
}
|
|
6126
6183
|
|
|
6127
6184
|
// Every surface that shows linked docs — info modal, session card badge, preview
|
|
@@ -7246,7 +7303,7 @@ function renderMarkdown(text) {
|
|
|
7246
7303
|
}
|
|
7247
7304
|
|
|
7248
7305
|
function isLightTheme() {
|
|
7249
|
-
const saved =
|
|
7306
|
+
const saved = store.getItem('theme');
|
|
7250
7307
|
return (
|
|
7251
7308
|
document.body.classList.contains('light') || (!saved && window.matchMedia('(prefers-color-scheme: light)').matches)
|
|
7252
7309
|
);
|
|
@@ -7552,13 +7609,13 @@ document.addEventListener('click', (e) => {
|
|
|
7552
7609
|
|
|
7553
7610
|
if (e.target.closest('.pinned-ungroup-btn')) {
|
|
7554
7611
|
e.stopPropagation();
|
|
7555
|
-
|
|
7612
|
+
store.setItem('groupPinnedSessions', 'false');
|
|
7556
7613
|
renderSessions();
|
|
7557
7614
|
return;
|
|
7558
7615
|
}
|
|
7559
7616
|
|
|
7560
7617
|
if (e.target.closest('.pinned-regroup-banner')) {
|
|
7561
|
-
|
|
7618
|
+
store.setItem('groupPinnedSessions', 'true');
|
|
7562
7619
|
renderSessions();
|
|
7563
7620
|
return;
|
|
7564
7621
|
}
|
|
@@ -7672,11 +7729,11 @@ function toggleTheme() {
|
|
|
7672
7729
|
if (isCurrentlyLight) {
|
|
7673
7730
|
document.body.classList.remove('light');
|
|
7674
7731
|
document.body.classList.add('dark-forced');
|
|
7675
|
-
|
|
7732
|
+
store.setItem('theme', 'dark');
|
|
7676
7733
|
} else {
|
|
7677
7734
|
document.body.classList.add('light');
|
|
7678
7735
|
document.body.classList.remove('dark-forced');
|
|
7679
|
-
|
|
7736
|
+
store.setItem('theme', 'light');
|
|
7680
7737
|
}
|
|
7681
7738
|
updateThemeIcon();
|
|
7682
7739
|
updateThemeColor(!isCurrentlyLight);
|
|
@@ -7699,7 +7756,7 @@ function updateThemeIcon() {
|
|
|
7699
7756
|
}
|
|
7700
7757
|
|
|
7701
7758
|
function loadTheme() {
|
|
7702
|
-
const saved =
|
|
7759
|
+
const saved = store.getItem('theme');
|
|
7703
7760
|
if (saved === 'light') {
|
|
7704
7761
|
document.body.classList.add('light');
|
|
7705
7762
|
document.body.classList.remove('dark-forced');
|
|
@@ -7712,7 +7769,7 @@ function loadTheme() {
|
|
|
7712
7769
|
updateThemeColor(document.body.classList.contains('light'));
|
|
7713
7770
|
syncHljsTheme();
|
|
7714
7771
|
buildThemeMenu();
|
|
7715
|
-
const colorTheme =
|
|
7772
|
+
const colorTheme = store.getItem('color-theme');
|
|
7716
7773
|
if (colorTheme) document.body.dataset.colorTheme = colorTheme;
|
|
7717
7774
|
syncColorThemeSelect(colorTheme || 'ember');
|
|
7718
7775
|
}
|
|
@@ -7768,10 +7825,10 @@ function syncColorThemeSelect(id) {
|
|
|
7768
7825
|
function setColorTheme(id) {
|
|
7769
7826
|
if (!id || id === 'ember') {
|
|
7770
7827
|
delete document.body.dataset.colorTheme;
|
|
7771
|
-
|
|
7828
|
+
store.removeItem('color-theme');
|
|
7772
7829
|
} else {
|
|
7773
7830
|
document.body.dataset.colorTheme = id;
|
|
7774
|
-
|
|
7831
|
+
store.setItem('color-theme', id);
|
|
7775
7832
|
}
|
|
7776
7833
|
syncColorThemeSelect(id);
|
|
7777
7834
|
}
|
|
@@ -7779,10 +7836,17 @@ function setColorTheme(id) {
|
|
|
7779
7836
|
//#endregion
|
|
7780
7837
|
|
|
7781
7838
|
//#region SIDEBAR_LAYOUT
|
|
7839
|
+
function expandSidebar() {
|
|
7840
|
+
const sidebar = document.querySelector('.sidebar');
|
|
7841
|
+
if (!sidebar.classList.contains('collapsed')) return;
|
|
7842
|
+
sidebar.classList.remove('collapsed');
|
|
7843
|
+
store.setItem('sidebar-collapsed', false);
|
|
7844
|
+
}
|
|
7845
|
+
|
|
7782
7846
|
function toggleSidebar() {
|
|
7783
7847
|
const sidebar = document.querySelector('.sidebar');
|
|
7784
7848
|
const collapsed = sidebar.classList.toggle('collapsed');
|
|
7785
|
-
|
|
7849
|
+
store.setItem('sidebar-collapsed', collapsed);
|
|
7786
7850
|
if (collapsed) {
|
|
7787
7851
|
sidebar.style.width = '';
|
|
7788
7852
|
if (focusZone === 'sidebar') setFocusZone('board');
|
|
@@ -7794,10 +7858,10 @@ function toggleSidebar() {
|
|
|
7794
7858
|
|
|
7795
7859
|
function loadSidebarState() {
|
|
7796
7860
|
const sidebar = document.querySelector('.sidebar');
|
|
7797
|
-
if (
|
|
7861
|
+
if (store.getItem('sidebar-collapsed') === 'true') {
|
|
7798
7862
|
sidebar.classList.add('collapsed');
|
|
7799
7863
|
}
|
|
7800
|
-
const w =
|
|
7864
|
+
const w = store.getItem('sidebar-width');
|
|
7801
7865
|
if (w) {
|
|
7802
7866
|
sidebar.style.setProperty('--sidebar-width', w);
|
|
7803
7867
|
}
|
|
@@ -7852,7 +7916,7 @@ function initSidebarResize() {
|
|
|
7852
7916
|
},
|
|
7853
7917
|
onEnd() {
|
|
7854
7918
|
sidebar.classList.remove('resizing');
|
|
7855
|
-
|
|
7919
|
+
store.setItem('sidebar-width', sidebar.style.getPropertyValue('--sidebar-width'));
|
|
7856
7920
|
},
|
|
7857
7921
|
});
|
|
7858
7922
|
}
|
|
@@ -7873,7 +7937,7 @@ function initPanelResize(panelId, handleId, cssVar, storageKey) {
|
|
|
7873
7937
|
},
|
|
7874
7938
|
onEnd() {
|
|
7875
7939
|
panel.classList.remove('resizing');
|
|
7876
|
-
|
|
7940
|
+
store.setItem(storageKey, panel.style.getPropertyValue(cssVar));
|
|
7877
7941
|
},
|
|
7878
7942
|
});
|
|
7879
7943
|
}
|
|
@@ -7883,7 +7947,7 @@ function loadPanelWidths() {
|
|
|
7883
7947
|
['detail-panel', '--detail-panel-width'],
|
|
7884
7948
|
['message-panel', '--message-panel-width'],
|
|
7885
7949
|
].forEach(([id, cssVar]) => {
|
|
7886
|
-
const w =
|
|
7950
|
+
const w = store.getItem(`${id}-width`);
|
|
7887
7951
|
if (w) document.getElementById(id).style.setProperty(cssVar, w);
|
|
7888
7952
|
});
|
|
7889
7953
|
}
|
|
@@ -8202,20 +8266,80 @@ let _planSessionId = null;
|
|
|
8202
8266
|
let spSource = [];
|
|
8203
8267
|
let spRows = [];
|
|
8204
8268
|
let spIdx = 0;
|
|
8269
|
+
let spPeekTimer = null;
|
|
8270
|
+
let spPeekToken = 0;
|
|
8271
|
+
let spPeekedId = null;
|
|
8272
|
+
// Per-open cache: spSource is frozen while the picker is up, so a fetched log stays valid.
|
|
8273
|
+
const spPeekCache = new Map();
|
|
8205
8274
|
|
|
8206
8275
|
function openSessionPicker() {
|
|
8207
8276
|
const input = document.getElementById('session-picker-input');
|
|
8208
8277
|
input.value = '';
|
|
8209
8278
|
spSource = getFilteredSessions().sort((a, b) => Date.parse(b.modifiedAt) - Date.parse(a.modifiedAt));
|
|
8279
|
+
spPeekCache.clear();
|
|
8210
8280
|
document.getElementById('session-picker-modal').classList.add('visible');
|
|
8211
8281
|
renderSessionPicker();
|
|
8212
8282
|
input.focus();
|
|
8213
8283
|
}
|
|
8214
8284
|
|
|
8215
8285
|
function closeSessionPicker() {
|
|
8286
|
+
spSetPeeking(false);
|
|
8216
8287
|
hideModalOverlay('session-picker-modal');
|
|
8217
8288
|
}
|
|
8218
8289
|
|
|
8290
|
+
function spIsPeeking() {
|
|
8291
|
+
return _modalEl('session-picker-modal').classList.contains('peeking');
|
|
8292
|
+
}
|
|
8293
|
+
|
|
8294
|
+
function spSetPeeking(on) {
|
|
8295
|
+
clearTimeout(spPeekTimer);
|
|
8296
|
+
spPeekToken++;
|
|
8297
|
+
spPeekedId = null;
|
|
8298
|
+
_modalEl('session-picker-modal').classList.toggle('peeking', on);
|
|
8299
|
+
if (on) spSchedulePeek();
|
|
8300
|
+
else document.getElementById('session-picker-peek').innerHTML = '';
|
|
8301
|
+
}
|
|
8302
|
+
|
|
8303
|
+
function spSchedulePeek() {
|
|
8304
|
+
if (!spIsPeeking() || spRows[spIdx]?.id === spPeekedId) return;
|
|
8305
|
+
clearTimeout(spPeekTimer);
|
|
8306
|
+
spPeekTimer = setTimeout(spLoadPeek, 120);
|
|
8307
|
+
}
|
|
8308
|
+
|
|
8309
|
+
function spRenderPeek(session, messages, note) {
|
|
8310
|
+
const pane = document.getElementById('session-picker-peek');
|
|
8311
|
+
const body =
|
|
8312
|
+
messages === null
|
|
8313
|
+
? ''
|
|
8314
|
+
: messages.length
|
|
8315
|
+
? `<div class="sp-peek-items">${renderMessageList(messages, { readOnly: true })}</div>`
|
|
8316
|
+
: '<div class="msg-empty">No messages in this session</div>';
|
|
8317
|
+
pane.innerHTML = `<div class="sp-peek-title"><span>${escapeHtml(sessionDisplayName(session))}</span><span>${note}</span></div>${body}`;
|
|
8318
|
+
pane.scrollTop = pane.scrollHeight;
|
|
8319
|
+
}
|
|
8320
|
+
|
|
8321
|
+
async function spLoadPeek() {
|
|
8322
|
+
const session = spRows[spIdx];
|
|
8323
|
+
if (!session) {
|
|
8324
|
+
document.getElementById('session-picker-peek').innerHTML = '<div class="msg-empty">No session selected</div>';
|
|
8325
|
+
return;
|
|
8326
|
+
}
|
|
8327
|
+
spPeekedId = session.id;
|
|
8328
|
+
let messages = spPeekCache.get(session.id);
|
|
8329
|
+
if (!messages) {
|
|
8330
|
+
const token = ++spPeekToken;
|
|
8331
|
+
spRenderPeek(session, null, 'loading…');
|
|
8332
|
+
try {
|
|
8333
|
+
messages = (await fetchSessionMessagesPage(session.id))?.messages || [];
|
|
8334
|
+
} catch (_) {
|
|
8335
|
+
messages = [];
|
|
8336
|
+
}
|
|
8337
|
+
if (token !== spPeekToken) return;
|
|
8338
|
+
spPeekCache.set(session.id, messages);
|
|
8339
|
+
}
|
|
8340
|
+
spRenderPeek(session, messages, `last ${messages.length} entries`);
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8219
8343
|
function spMatches(session, query) {
|
|
8220
8344
|
return (
|
|
8221
8345
|
fuzzyMatch(session.name, query) ||
|
|
@@ -8273,6 +8397,7 @@ function spSelect(idx) {
|
|
|
8273
8397
|
const row = list.children[spIdx];
|
|
8274
8398
|
row?.classList.add('selected');
|
|
8275
8399
|
row?.scrollIntoView({ block: 'nearest' });
|
|
8400
|
+
spSchedulePeek();
|
|
8276
8401
|
}
|
|
8277
8402
|
|
|
8278
8403
|
async function spOpen(idx) {
|
|
@@ -8291,6 +8416,7 @@ function initSessionPicker() {
|
|
|
8291
8416
|
else if (matchKey(e, 'ArrowDown') || (e.ctrlKey && e.key === 'n')) spSelect(spIdx + 1);
|
|
8292
8417
|
else if (matchKey(e, 'ArrowUp') || (e.ctrlKey && e.key === 'p')) spSelect(spIdx - 1);
|
|
8293
8418
|
else if (e.key === 'Enter') spOpen(spIdx);
|
|
8419
|
+
else if (e.ctrlKey && e.code === 'Space') spSetPeeking(!spIsPeeking());
|
|
8294
8420
|
else return;
|
|
8295
8421
|
e.preventDefault();
|
|
8296
8422
|
e.stopPropagation();
|
|
@@ -8892,7 +9018,7 @@ if ('serviceWorker' in navigator) {
|
|
|
8892
9018
|
|
|
8893
9019
|
//#region INIT
|
|
8894
9020
|
loadTheme();
|
|
8895
|
-
if (
|
|
9021
|
+
if (store.getItem('sessions-filtersCollapsed') === 'true') {
|
|
8896
9022
|
document.getElementById('sessions-filters').classList.add('collapsed');
|
|
8897
9023
|
document.getElementById('sessions-chevron').classList.add('rotated');
|
|
8898
9024
|
}
|
|
@@ -8932,7 +9058,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
8932
9058
|
|
|
8933
9059
|
loadSidebarState();
|
|
8934
9060
|
try {
|
|
8935
|
-
const cg = JSON.parse(
|
|
9061
|
+
const cg = JSON.parse(store.getItem('collapsedGroups') || '[]');
|
|
8936
9062
|
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach side-effect
|
|
8937
9063
|
cg.forEach((p) => collapsedProjectGroups.add(p));
|
|
8938
9064
|
} catch (_) {}
|
|
@@ -8940,7 +9066,7 @@ loadSessionGroups();
|
|
|
8940
9066
|
initSessionGroupsDnd();
|
|
8941
9067
|
initSessionPicker();
|
|
8942
9068
|
try {
|
|
8943
|
-
const af = JSON.parse(
|
|
9069
|
+
const af = JSON.parse(store.getItem('activityFilter') || '[]');
|
|
8944
9070
|
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach side-effect
|
|
8945
9071
|
af.forEach((k) => activityFilter.add(k));
|
|
8946
9072
|
} catch (_) {}
|
|
@@ -9173,8 +9299,9 @@ document.addEventListener('keydown', (e) => {
|
|
|
9173
9299
|
if (e.ctrlKey && e.altKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
9174
9300
|
fwd(e.key);
|
|
9175
9301
|
}
|
|
9176
|
-
//
|
|
9177
|
-
|
|
9302
|
+
// Own branch: the Alt+digit case below requires !ctrlKey. The hub owns the Ctrl+Alt+letter
|
|
9303
|
+
// keymap and ignores unbound letters.
|
|
9304
|
+
if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && /^[a-z]$/i.test(e.key)) {
|
|
9178
9305
|
fwd(e.key);
|
|
9179
9306
|
}
|
|
9180
9307
|
if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
|