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/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
- localStorage.setItem(LAST_VIEW_KEY, JSON.stringify(data));
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(localStorage.getItem(LAST_VIEW_KEY)) || null;
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
- localStorage.removeItem(LAST_VIEW_KEY);
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
- localStorage.setItem(`${containerId}Collapsed`, collapsed);
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
- localStorage.setItem('activityFilter', JSON.stringify([...activityFilter]));
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
- localStorage.setItem('message-panel-open', messagePanelOpen);
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
- localStorage.setItem('message-panel-open', 'true');
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 res = await fetch(`/api/sessions/${sessionId}/messages?limit=15`);
693
- if (!res.ok) return;
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 before = currentMessages[0].timestamp;
746
- const res = await fetch(`/api/sessions/${currentSessionId}/messages?limit=15&before=${encodeURIComponent(before)}`);
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(localStorage.getItem(`pinned-messages-${sessionId}`)) || [];
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
- localStorage.setItem(`pinned-messages-${sessionId}`, JSON.stringify(pins));
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(localStorage.getItem('pinned-sessions')) || []);
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(localStorage.getItem('sticky-sessions')) || []);
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
- localStorage.setItem('pinned-sessions', JSON.stringify([...pinnedSessionIds]));
1671
- localStorage.setItem('sticky-sessions', JSON.stringify([...stickySessionIds]));
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
- localStorage.setItem(`modal-fullscreen-${modalId}`, String(on));
2063
+ store.setItem(`modal-fullscreen-${modalId}`, String(on));
2026
2064
  }
2027
2065
 
2028
2066
  function loadModalFullscreen() {
2029
2067
  _forEachFullscreenModal((modalId) => {
2030
- if (localStorage.getItem(`modal-fullscreen-${modalId}`) === 'true') _applyModalFullscreen(modalId, true);
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 = localStorage.getItem(wKey);
2068
- const savedH = localStorage.getItem(hKey);
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
- localStorage.setItem(wKey, `${w}px`);
2103
- localStorage.setItem(hKey, `${h}px`);
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
- localStorage.removeItem(wKey);
2111
- localStorage.removeItem(hKey);
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(localStorage.getItem(MODAL_ZOOM_KEY)) || 1);
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
- localStorage.setItem(MODAL_ZOOM_KEY, String(modalZoom));
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 = localStorage.getItem('agentFooterCollapsed') === 'true';
2771
+ const collapsed = store.getItem('agentFooterCollapsed') === 'true';
2734
2772
  footer.classList.toggle('collapsed', collapsed);
2735
2773
  document.getElementById('agent-footer-toggle').innerHTML = collapsed ? '&#x25B4;' : '&#x25BE;';
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
- localStorage.setItem('agentFooterCollapsed', collapsed);
2849
+ store.setItem('agentFooterCollapsed', collapsed);
2812
2850
  document.getElementById('agent-footer-toggle').innerHTML = collapsed ? '&#x25B4;' : '&#x25BE;';
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
- if (!sessions.some((s) => s.id === id)) {
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 = !!(localStorage.getItem(_sessionScratchpadKey(session.id)) || '').trim();
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 = localStorage.getItem('groupPinnedSessions') !== 'false';
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, `__pinned_${projectPath}__`)}
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, '__pinned___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(localStorage.getItem(SESSION_GROUPS_KEY) || 'null')?.groups;
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
- localStorage.setItem(SESSION_GROUPS_KEY, JSON.stringify({ version: 1, groups: sessionGroups }));
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
- localStorage.setItem('collapsedGroups', JSON.stringify([...collapsedProjectGroups]));
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 (collapsedProjectGroups.delete(s.project || '__ungrouped__')) changed = true;
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
- if (sidebar.classList.contains('collapsed')) {
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 = localStorage.getItem(key) || '';
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 = !!(localStorage.getItem(key) || '').trim();
5345
+ const had = !!(store.getItem(key) || '').trim();
5284
5346
  const has = !!val.trim();
5285
5347
  if (has) {
5286
- localStorage.setItem(key, val);
5348
+ store.setItem(key, val);
5287
5349
  } else {
5288
- localStorage.removeItem(key);
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 (let i = 0; i < localStorage.length; i++) {
5311
- const k = localStorage.key(i);
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 (let i = 0; i < localStorage.length; i++) {
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(localStorage.getItem(key)) || [];
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
- localStorage.removeItem(`pinned-messages-${sessionId}`);
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(localStorage.getItem(key)) || [];
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) localStorage.setItem(key, JSON.stringify(pins));
5508
- else localStorage.removeItem(key);
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 (let i = 0; i < localStorage.length; i++) {
5522
- const key = localStorage.key(i);
5581
+ for (const key of store.keys()) {
5523
5582
  if (!key.startsWith('scratchpad-')) continue;
5524
- const val = localStorage.getItem(key) || '';
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(localStorage.getItem(key)) || [];
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
- localStorage.removeItem(key);
5666
+ store.removeItem(key);
5608
5667
  _renderStorageTab();
5609
5668
  _updateStorageTotal();
5610
5669
  }
5611
5670
 
5612
5671
  function _renderStorageLinkedDocs() {
5613
5672
  const entries = [];
5614
- for (let i = 0; i < localStorage.length; i++) {
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(localStorage.getItem(key)) || [];
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
- localStorage.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
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 (let i = 0; i < localStorage.length; i++) {
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
- localStorage.removeItem(key);
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 = localStorage.getItem(PREVIEW_STORAGE_PREFIX + sessionId);
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
- localStorage.setItem(PREVIEW_STORAGE_PREFIX + sessionId, JSON.stringify(paths.slice(0, 20)));
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) localStorage.setItem(PREVIEW_STORAGE_PREFIX + sessionId, JSON.stringify(paths));
6124
- else localStorage.removeItem(PREVIEW_STORAGE_PREFIX + sessionId);
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 = localStorage.getItem('theme');
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
- localStorage.setItem('groupPinnedSessions', 'false');
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
- localStorage.setItem('groupPinnedSessions', 'true');
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
- localStorage.setItem('theme', 'dark');
7732
+ store.setItem('theme', 'dark');
7676
7733
  } else {
7677
7734
  document.body.classList.add('light');
7678
7735
  document.body.classList.remove('dark-forced');
7679
- localStorage.setItem('theme', 'light');
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 = localStorage.getItem('theme');
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 = localStorage.getItem('color-theme');
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
- localStorage.removeItem('color-theme');
7828
+ store.removeItem('color-theme');
7772
7829
  } else {
7773
7830
  document.body.dataset.colorTheme = id;
7774
- localStorage.setItem('color-theme', id);
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
- localStorage.setItem('sidebar-collapsed', collapsed);
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 (localStorage.getItem('sidebar-collapsed') === 'true') {
7861
+ if (store.getItem('sidebar-collapsed') === 'true') {
7798
7862
  sidebar.classList.add('collapsed');
7799
7863
  }
7800
- const w = localStorage.getItem('sidebar-width');
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
- localStorage.setItem('sidebar-width', sidebar.style.getPropertyValue('--sidebar-width'));
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
- localStorage.setItem(storageKey, panel.style.getPropertyValue(cssVar));
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 = localStorage.getItem(`${id}-width`);
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 (localStorage.getItem('sessions-filtersCollapsed') === 'true') {
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(localStorage.getItem('collapsedGroups') || '[]');
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(localStorage.getItem('activityFilter') || '[]');
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
- // Its own branch: the Alt+digit case below requires !ctrlKey.
9177
- if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && e.key.toLowerCase() === 'p') {
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)) {