codex-overleaf-link 1.9.11 → 1.9.12

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.
@@ -0,0 +1,365 @@
1
+ (function initCodexOverleafProjectSettingsCoordinator() {
2
+ 'use strict';
3
+
4
+ function create(deps = {}) {
5
+ const {
6
+ CodexOverleafTheme,
7
+ GovernanceRules,
8
+ SettingsPanel,
9
+ closeContextTray,
10
+ closeDiagnosticsMenu,
11
+ closeDiagnosticsResult,
12
+ closeModelConfigPopover,
13
+ closeSlashMenu,
14
+ getCurrentProjectId,
15
+ getLocalSkillsPanel,
16
+ getLocale,
17
+ getPanel,
18
+ getPanelRendererInstance,
19
+ getSettingsPanelInstance,
20
+ getState,
21
+ isProjectEditorRoute,
22
+ refreshStorageUsageSummary,
23
+ renderAuditHistoryPanel,
24
+ renderRecentProjectsVariant,
25
+ saveStateSoon,
26
+ setState,
27
+ tr,
28
+ tx,
29
+ window
30
+ } = deps;
31
+ let customInstructionsEditorProjectId = '';
32
+ let customInstructionsEditorValue = '';
33
+ let themeAutoDisposer = null;
34
+
35
+ function normalizeCustomInstructionsByProject(value) {
36
+ const result = {};
37
+ const textMaxChars = 12000;
38
+ const keyMaxChars = 160;
39
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
40
+ return result;
41
+ }
42
+ for (const rawKey of Object.keys(value)) {
43
+ const key = typeof rawKey === 'string' ? rawKey.trim() : '';
44
+ if (!key) {
45
+ continue;
46
+ }
47
+ const normalizedKey = key.length <= keyMaxChars ? key : key.slice(0, keyMaxChars - 1) + '…';
48
+ const rawText = typeof value[rawKey] === 'string' ? value[rawKey] : '';
49
+ result[normalizedKey] = rawText.length <= textMaxChars
50
+ ? rawText
51
+ : rawText.slice(0, textMaxChars - 1) + '…';
52
+ }
53
+ return result;
54
+ }
55
+
56
+ function getCustomInstructionsForCurrentProject() {
57
+ const projectId = getCurrentProjectId();
58
+ const normalizedProject = normalizeCustomInstructionsByProject({ [projectId]: '' });
59
+ const normalizedProjectId = Object.keys(normalizedProject)[0] || '';
60
+ if (!normalizedProjectId) {
61
+ return '';
62
+ }
63
+ return normalizeCustomInstructionsByProject(getState()?.customInstructionsByProject)[normalizedProjectId] || '';
64
+ }
65
+
66
+ function setCustomInstructionsForProject(projectId, value) {
67
+ const normalizedProject = normalizeCustomInstructionsByProject({ [projectId]: value });
68
+ const normalizedProjectId = Object.keys(normalizedProject)[0] || '';
69
+ if (!normalizedProjectId) {
70
+ return;
71
+ }
72
+ setState({
73
+ ...getState(),
74
+ customInstructionsByProject: {
75
+ ...normalizeCustomInstructionsByProject(getState()?.customInstructionsByProject),
76
+ [normalizedProjectId]: normalizedProject[normalizedProjectId]
77
+ }
78
+ });
79
+ }
80
+
81
+ function openCustomInstructionsSettings() {
82
+ const settingsPanelInstance = getSettingsPanelInstance();
83
+ if (!settingsPanelInstance) {
84
+ return;
85
+ }
86
+ closeDiagnosticsMenu();
87
+ closeDiagnosticsResult();
88
+ closeModelConfigPopover();
89
+ closeContextTray();
90
+ closeSlashMenu();
91
+ clearProjectSettingsStatus();
92
+ syncCustomInstructionsEditorForProject(getCurrentProjectId(), { force: true });
93
+ syncProjectSettingsEditorForProject();
94
+ getPanelRendererInstance()?.setView?.('settings');
95
+ SettingsPanel.show(settingsPanelInstance);
96
+ refreshStorageUsageSummary();
97
+ if (settingsPanelInstance.container?.querySelector('[data-history-card]')?.open) {
98
+ renderAuditHistoryPanel();
99
+ }
100
+ refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
101
+ `Could not list local skills: ${error.message}`,
102
+ `无法列出本地技能:${error.message}`
103
+ ), 'failed'));
104
+ }
105
+
106
+ function toggleCustomInstructionsSettings() {
107
+ if (SettingsPanel.isVisible(getSettingsPanelInstance())) {
108
+ closeCustomInstructionsSettings();
109
+ return;
110
+ }
111
+ openCustomInstructionsSettings();
112
+ }
113
+
114
+ function closeCustomInstructionsSettings() {
115
+ SettingsPanel.hide(getSettingsPanelInstance());
116
+ if (isProjectEditorRoute(window.location)) {
117
+ getPanelRendererInstance()?.setView?.('session');
118
+ } else {
119
+ renderRecentProjectsVariant().catch(() => { /* swallow */ });
120
+ }
121
+ }
122
+
123
+ function openSkillsView() {
124
+ if (!getSettingsPanelInstance()) {
125
+ return;
126
+ }
127
+ getPanelRendererInstance()?.setView?.('skills');
128
+ refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
129
+ `Could not list local skills: ${error.message}`,
130
+ `无法列出本地技能:${error.message}`
131
+ ), 'failed'));
132
+ }
133
+
134
+ function closeSkillsView() {
135
+ getPanelRendererInstance()?.setView?.('settings');
136
+ }
137
+
138
+ function updateSkillsEntrySummary() {
139
+ const settingsPanelInstance = getSettingsPanelInstance();
140
+ if (!settingsPanelInstance) {
141
+ return;
142
+ }
143
+ const summary = getSkillLoadingSettings().loadCodexOverleafSkills === false
144
+ ? tr('codexOverleafSkillsSummaryOff')
145
+ : tr('codexOverleafSkillsSummaryCount', { count: countEnabledCodexOverleafSkills() });
146
+ SettingsPanel.setSkillsSummary(settingsPanelInstance, summary);
147
+ }
148
+
149
+ function countEnabledCodexOverleafSkills() {
150
+ const skills = Array.isArray(getState()?.codexOverleafSkills) ? getState().codexOverleafSkills : [];
151
+ return skills.reduce((total, skill) => {
152
+ const id = String(skill?.id || '').trim();
153
+ return id && isCodexOverleafSkillEnabled(id) ? total + 1 : total;
154
+ }, 0);
155
+ }
156
+
157
+ function syncCustomInstructionsEditorForProject(projectId = getCurrentProjectId(), options) {
158
+ const syncOptions = options || {};
159
+ const panel = getPanel();
160
+ const input = panel?.querySelector('[data-custom-instructions-input]');
161
+ if (!input) {
162
+ return;
163
+ }
164
+ const normalizedProject = normalizeCustomInstructionsByProject({ [projectId]: '' });
165
+ const normalizedProjectId = Object.keys(normalizedProject)[0] || '';
166
+ const storedValue = normalizedProjectId
167
+ ? normalizeCustomInstructionsByProject(getState()?.customInstructionsByProject)[normalizedProjectId] || ''
168
+ : '';
169
+ input.placeholder = tr('customInstructionsPlaceholder');
170
+ const editorIsOpen = panel?.dataset?.view === 'settings' || panel?.dataset?.view === 'skills';
171
+ const editorIsDirty = normalizedProjectId
172
+ && customInstructionsEditorProjectId === normalizedProjectId
173
+ && input.value !== customInstructionsEditorValue;
174
+ if (!syncOptions.force && editorIsOpen && editorIsDirty) {
175
+ return;
176
+ }
177
+ input.value = storedValue;
178
+ customInstructionsEditorProjectId = normalizedProjectId;
179
+ customInstructionsEditorValue = storedValue;
180
+ }
181
+
182
+ function normalizeGovernanceRulesByProject(value) {
183
+ const result = {};
184
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
185
+ return result;
186
+ }
187
+ for (const rawKey of Object.keys(value)) {
188
+ const key = normalizeProjectPreferenceKey(rawKey);
189
+ if (!key) {
190
+ continue;
191
+ }
192
+ result[key] = normalizeGovernanceRules(value[rawKey]);
193
+ }
194
+ return result;
195
+ }
196
+
197
+ function normalizeProjectPreferenceKey(value) {
198
+ const text = String(value || '').trim();
199
+ return text.length <= 160 ? text : text.slice(0, 159) + '…';
200
+ }
201
+
202
+ function normalizeGovernanceRules(value = {}) {
203
+ if (GovernanceRules?.normalizeGovernanceRules) {
204
+ return GovernanceRules.normalizeGovernanceRules(value);
205
+ }
206
+ return {
207
+ readonlyPatterns: normalizePatternTextList(value.readonlyPatterns),
208
+ writablePatterns: normalizePatternTextList(value.writablePatterns),
209
+ sensitiveCheckEnabled: value.sensitiveCheckEnabled !== false,
210
+ sensitiveConfirmAllowed: value.sensitiveConfirmAllowed === true
211
+ };
212
+ }
213
+
214
+ function normalizePatternTextList(value) {
215
+ if (Array.isArray(value)) {
216
+ return value.map(item => String(item || '').trim()).filter(Boolean);
217
+ }
218
+ return String(value || '').split(/\r?\n/).map(item => item.trim()).filter(Boolean);
219
+ }
220
+
221
+ function getGovernanceRulesForCurrentProject() {
222
+ const projectId = normalizeProjectPreferenceKey(getCurrentProjectId());
223
+ return normalizeGovernanceRulesByProject(getState()?.governanceRulesByProject)[projectId]
224
+ || normalizeGovernanceRules({});
225
+ }
226
+
227
+ function setGovernanceRulesForCurrentProject(rules) {
228
+ const projectId = normalizeProjectPreferenceKey(getCurrentProjectId());
229
+ if (!projectId) {
230
+ return;
231
+ }
232
+ setState({
233
+ ...getState(),
234
+ governanceRulesByProject: {
235
+ ...normalizeGovernanceRulesByProject(getState()?.governanceRulesByProject),
236
+ [projectId]: normalizeGovernanceRules(rules)
237
+ }
238
+ });
239
+ }
240
+
241
+ function getThemePreference() {
242
+ return CodexOverleafTheme.normalizeThemePreference(getState()?.theme);
243
+ }
244
+
245
+ function applyPanelTheme(preference) {
246
+ const normalized = CodexOverleafTheme.normalizeThemePreference(preference);
247
+ CodexOverleafTheme.applyTheme(normalized, getPanel());
248
+ if (themeAutoDisposer) {
249
+ themeAutoDisposer();
250
+ themeAutoDisposer = null;
251
+ }
252
+ themeAutoDisposer = CodexOverleafTheme.watchAuto(normalized, getPanel());
253
+ }
254
+
255
+ function getSkillLoadingSettings() {
256
+ return {
257
+ loadCodexLocalSkills: getState()?.loadCodexLocalSkills !== false,
258
+ loadCodexOverleafSkills: getState()?.loadCodexOverleafSkills !== false
259
+ };
260
+ }
261
+
262
+ function setSkillLoadingSettings(settings = {}) {
263
+ setState({
264
+ ...getState(),
265
+ loadCodexLocalSkills: settings.loadCodexLocalSkills !== false,
266
+ loadCodexOverleafSkills: settings.loadCodexOverleafSkills !== false
267
+ });
268
+ }
269
+
270
+ function getCodexOverleafSkillEnabled() {
271
+ const map = getState()?.codexOverleafSkillEnabled;
272
+ return map && typeof map === 'object' && !Array.isArray(map) ? map : {};
273
+ }
274
+
275
+ function isCodexOverleafSkillEnabled(skillId) {
276
+ const map = getCodexOverleafSkillEnabled();
277
+ if (!Object.prototype.hasOwnProperty.call(map, skillId)) {
278
+ return true;
279
+ }
280
+ return map[skillId] !== false;
281
+ }
282
+
283
+ function setCodexOverleafSkillEnabled(skillId, enabled) {
284
+ const map = getCodexOverleafSkillEnabled();
285
+ setState({
286
+ ...getState(),
287
+ codexOverleafSkillEnabled: {
288
+ ...map,
289
+ [skillId]: Boolean(enabled)
290
+ }
291
+ });
292
+ saveStateSoon();
293
+ renderLocalSkillList();
294
+ }
295
+
296
+ function readSkillLoadingSettingsFromSettings() {
297
+ return SettingsPanel.readState(getSettingsPanelInstance()).skillToggles;
298
+ }
299
+
300
+ function readGovernanceRulesFromSettings() {
301
+ return normalizeGovernanceRules(SettingsPanel.readState(getSettingsPanelInstance()).governanceRules);
302
+ }
303
+
304
+ function syncProjectSettingsEditorForProject() {
305
+ SettingsPanel.loadState(getSettingsPanelInstance(), {
306
+ governanceRules: getGovernanceRulesForCurrentProject(),
307
+ skillToggles: getSkillLoadingSettings(),
308
+ theme: getThemePreference(),
309
+ language: getLocale()
310
+ });
311
+ renderLocalSkillList();
312
+ }
313
+
314
+ function setProjectSettingsStatus(text, status = 'info') {
315
+ SettingsPanel.setStatus(getSettingsPanelInstance(), text, status);
316
+ }
317
+
318
+ function clearProjectSettingsStatus() {
319
+ SettingsPanel.clearStatus(getSettingsPanelInstance());
320
+ }
321
+
322
+ async function refreshLocalSkills() {
323
+ return getLocalSkillsPanel().refreshLocalSkills();
324
+ }
325
+
326
+ function renderLocalSkillList() {
327
+ getLocalSkillsPanel().renderLocalSkillList();
328
+ updateSkillsEntrySummary();
329
+ }
330
+
331
+ return {
332
+ applyPanelTheme,
333
+ clearProjectSettingsStatus,
334
+ closeCustomInstructionsSettings,
335
+ closeSkillsView,
336
+ getCodexOverleafSkillEnabled,
337
+ getCustomInstructionsForCurrentProject,
338
+ getGovernanceRulesForCurrentProject,
339
+ getSkillLoadingSettings,
340
+ getThemePreference,
341
+ isCodexOverleafSkillEnabled,
342
+ normalizeCustomInstructionsByProject,
343
+ normalizeGovernanceRules,
344
+ normalizeGovernanceRulesByProject,
345
+ normalizeProjectPreferenceKey,
346
+ openCustomInstructionsSettings,
347
+ openSkillsView,
348
+ readGovernanceRulesFromSettings,
349
+ readSkillLoadingSettingsFromSettings,
350
+ refreshLocalSkills,
351
+ renderLocalSkillList,
352
+ setCodexOverleafSkillEnabled,
353
+ setCustomInstructionsForProject,
354
+ setGovernanceRulesForCurrentProject,
355
+ setProjectSettingsStatus,
356
+ setSkillLoadingSettings,
357
+ syncCustomInstructionsEditorForProject,
358
+ syncProjectSettingsEditorForProject,
359
+ toggleCustomInstructionsSettings,
360
+ updateSkillsEntrySummary
361
+ };
362
+ }
363
+
364
+ window.CodexOverleafProjectSettingsCoordinator = { create };
365
+ })();
@@ -12,7 +12,7 @@
12
12
  deriveSessionTitle
13
13
  } = window.CodexOverleafSessionState;
14
14
 
15
- // Session manager — the session lifecycle + list/rename/delete surface
15
+ // Session manager — the session lifecycle + list/delete surface
16
16
  // carved out of contentRuntime.js (v1.4.7 structural-debt phase 3). Code
17
17
  // moved verbatim except that mutable runtime state is read through
18
18
  // getState()/getPanel()/getSessionPanelInstance() and written back through
@@ -35,210 +35,26 @@
35
35
  formatSessionTime
36
36
  } = deps;
37
37
 
38
- function applySessionLabel() {
39
- const trigger = getPanel().querySelector('[data-session-menu-trigger]');
40
- if (trigger) {
41
- trigger.title = tr('sessionMenuOpen');
42
- trigger.setAttribute('aria-label', tr('sessionMenuOpen'));
43
- }
44
- const textNode = getPanel().querySelector('[data-session-label-text]');
45
- const deleteButton = getPanel().querySelector('[data-session-delete]');
46
- const renameButton = getPanel().querySelector('[data-session-rename]');
47
- const renameInput = getPanel().querySelector('[data-session-rename-input]');
48
- const active = getActiveSession(getState());
49
- if (textNode) {
50
- // Always show a title — fall back to the New Session placeholder for an
51
- // empty active session (which has no list row), so the session you are in
52
- // is never blank and unmanageable from the header.
53
- textNode.textContent = active && isDisplayableSession(active)
54
- ? getSessionDisplayTitle(active)
55
- : tr('newSessionFallback');
56
- }
57
- if (deleteButton) {
58
- const running = active ? isSessionRunning(active) : false;
59
- // Deleting the sole empty session is a documented no-op (it would just
60
- // mint another empty), so disable it rather than leave a dead click.
61
- const soleEmpty = Boolean(active) && !isDisplayableSession(active) && (getState()?.sessions || []).length <= 1;
62
- deleteButton.disabled = !active || running || soleEmpty;
63
- deleteButton.title = running ? tr('deleteRunningSession') : tr('deleteSession');
64
- deleteButton.setAttribute('aria-label', running ? tr('deleteRunningSession') : tr('deleteSession'));
65
- }
66
- if (renameButton) {
67
- renameButton.title = tr('renameSession');
68
- renameButton.setAttribute('aria-label', tr('renameSession'));
69
- }
70
- renameInput?.setAttribute('aria-label', tr('renameSession'));
71
- }
72
-
73
- // Wire the active-session header bar once (rename + delete act on the active
74
- // session, reusing the same paths as the per-row controls).
75
- function bindActiveSessionHeader() {
76
- getPanel().querySelector('[data-session-delete]')?.addEventListener('click', event => {
77
- event.stopPropagation();
78
- if (getState()?.activeSessionId) {
79
- deleteSessionWithConfirm(getState().activeSessionId);
80
- }
81
- });
82
- getPanel().querySelector('[data-session-rename]')?.addEventListener('click', event => {
83
- event.stopPropagation();
84
- beginActiveSessionRename();
85
- });
86
- getPanel().querySelector('[data-session-menu-trigger]')?.addEventListener('click', event => {
87
- event.stopPropagation();
88
- toggleSessionMenu();
89
- });
90
- // Close the dropdown on any click outside it (capture phase, so the
91
- // panel's own click-stoppers don't swallow the dismiss — same pattern as
92
- // the diagnostics menu) and on Escape.
93
- document.addEventListener('click', event => {
94
- const menu = getPanel()?.querySelector('[data-session-menu]');
95
- if (!menu || menu.hidden) {
96
- return;
97
- }
98
- const trigger = getPanel()?.querySelector('[data-session-menu-trigger]');
99
- if (menu.contains(event.target) || trigger?.contains(event.target)) {
100
- return;
101
- }
102
- closeSessionMenu();
103
- }, true);
104
- document.addEventListener('keydown', event => {
105
- if (event.key === 'Escape') {
106
- closeSessionMenu();
107
- }
108
- }, true);
109
- }
110
-
111
- // Header session dropdown: lists every saved session where the user works,
112
- // switches on click, and offers New session — the embedded list below stays
113
- // as the always-visible overview.
114
- function toggleSessionMenu() {
115
- const menu = getPanel()?.querySelector('[data-session-menu]');
116
- if (!menu) {
117
- return;
118
- }
119
- if (menu.hidden) {
120
- renderSessionMenu(menu);
121
- }
122
- menu.hidden = !menu.hidden;
123
- getPanel()?.querySelector('[data-session-menu-trigger]')
124
- ?.setAttribute('aria-expanded', menu.hidden ? 'false' : 'true');
125
- }
126
-
127
- function closeSessionMenu() {
128
- const menu = getPanel()?.querySelector('[data-session-menu]');
129
- if (!menu || menu.hidden) {
130
- return;
131
- }
132
- menu.hidden = true;
133
- getPanel()?.querySelector('[data-session-menu-trigger]')?.setAttribute('aria-expanded', 'false');
134
- }
135
-
136
- function renderSessionMenu(menu) {
137
- menu.replaceChildren();
138
- const state = getState();
139
- const sessions = (state?.sessions || [])
140
- .filter(isDisplayableSession)
141
- .slice()
142
- .sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
143
- for (const session of sessions) {
144
- const row = document.createElement('button');
145
- row.type = 'button';
146
- row.className = 'codex-session-menu-item';
147
- row.dataset.active = session.id === state?.activeSessionId ? 'true' : 'false';
148
- row.dataset.running = isSessionRunning(session) ? 'true' : 'false';
149
- const title = document.createElement('span');
150
- title.className = 'codex-session-menu-item-title';
151
- title.textContent = getSessionDisplayTitle(session);
152
- title.title = getSessionDisplayTitle(session);
153
- const time = document.createElement('time');
154
- time.className = 'codex-session-menu-item-time';
155
- time.textContent = formatSessionTime(session.updatedAt || session.createdAt);
156
- const absoluteTs = session.updatedAt || session.createdAt;
157
- if (absoluteTs) {
158
- time.title = new Date(absoluteTs).toLocaleString();
159
- }
160
- row.append(title, time);
161
- row.addEventListener('click', event => {
162
- event.stopPropagation();
163
- closeSessionMenu();
164
- if (session.id !== getState()?.activeSessionId) {
165
- switchSession(session.id);
166
- }
167
- });
168
- menu.append(row);
169
- }
170
- const create = document.createElement('button');
171
- create.type = 'button';
172
- create.className = 'codex-session-menu-new';
173
- create.textContent = `+ ${tr('newSession')}`;
174
- create.addEventListener('click', event => {
175
- event.stopPropagation();
176
- closeSessionMenu();
177
- startNewSession();
38
+ const sessionMenuView = window.CodexOverleafSessionMenuView.create({
39
+ commitSessionRename,
40
+ deleteSessionWithConfirm,
41
+ document,
42
+ formatSessionTime,
43
+ getActiveSession,
44
+ getPanel,
45
+ getSessionDisplayTitle,
46
+ getState,
47
+ isDisplayableSession,
48
+ isSessionRunning,
49
+ startNewSession,
50
+ switchSession,
51
+ tr
178
52
  });
179
- menu.append(create);
180
- }
181
-
182
- // Inline-rename the active session from the header bar (mirrors the per-row
183
- // rename: Enter / blur commits, Escape cancels).
184
- function beginActiveSessionRename() {
185
- const active = getActiveSession(getState());
186
- const wrap = getPanel().querySelector('[data-session-label]');
187
- const trigger = getPanel().querySelector('[data-session-menu-trigger]');
188
- const textNode = getPanel().querySelector('[data-session-label-text]');
189
- const input = getPanel().querySelector('[data-session-rename-input]');
190
- if (!active || !wrap || !textNode || !input) {
191
- return;
192
- }
193
- closeSessionMenu();
194
- let settled = false;
195
- wrap.dataset.editing = 'true';
196
- // Seed the box from a real title only; an empty/auto session opens blank so
197
- // accepting the default does not commit the placeholder as a manual title.
198
- const seed = isDisplayableSession(active) ? getSessionDisplayTitle(active) : '';
199
- input.value = seed;
200
- input.hidden = false;
201
- textNode.hidden = true;
202
- if (trigger) {
203
- trigger.hidden = true;
204
- }
205
- input.focus();
206
- input.select();
207
- const cleanup = () => {
208
- input.removeEventListener('keydown', onKeydown);
209
- input.removeEventListener('blur', onBlur);
210
- };
211
- const finish = commit => {
212
- if (settled) {
213
- return;
214
- }
215
- settled = true;
216
- cleanup();
217
- input.hidden = true;
218
- textNode.hidden = false;
219
- if (trigger) {
220
- trigger.hidden = false;
221
- }
222
- delete wrap.dataset.editing;
223
- // Treat an unchanged value (incl. a no-op blur) as a cancel so it never
224
- // rewrites the title source.
225
- if (commit && input.value.trim() !== seed.trim()) {
226
- commitSessionRename(active.id, input.value);
227
- }
228
- };
229
- const onKeydown = event => {
230
- if (event.key === 'Enter') {
231
- event.preventDefault();
232
- finish(true);
233
- } else if (event.key === 'Escape') {
234
- event.preventDefault();
235
- finish(false);
236
- }
237
- };
238
- const onBlur = () => finish(true);
239
- input.addEventListener('keydown', onKeydown);
240
- input.addEventListener('blur', onBlur);
241
- }
53
+ const {
54
+ applySessionLabel,
55
+ bindActiveSessionHeader,
56
+ closeSessionMenu
57
+ } = sessionMenuView;
242
58
 
243
59
  async function startNewSession() {
244
60
  // Leaving a still-running session is allowed but never silent: the run