codex-overleaf-link 1.8.5 → 1.9.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.
Files changed (74) hide show
  1. package/README.md +45 -21
  2. package/bin/codex-overleaf-link.mjs +58 -0
  3. package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
  4. package/extension/assets/icons/codex-overleaf-icon.png +0 -0
  5. package/extension/assets/icons/icon128.png +0 -0
  6. package/extension/assets/icons/icon16.png +0 -0
  7. package/extension/assets/icons/icon32.png +0 -0
  8. package/extension/assets/icons/icon48.png +0 -0
  9. package/extension/bootstrap/background.js +404 -0
  10. package/extension/bootstrap/manifest.template.json +51 -0
  11. package/extension/bootstrap/popup.css +26 -0
  12. package/extension/bootstrap/popup.html +38 -0
  13. package/extension/bootstrap/popup.js +44 -0
  14. package/extension/bootstrap/runtimeContext.js +2 -0
  15. package/extension/runtime-manifest.json +60 -0
  16. package/extension/src/background.js +532 -0
  17. package/extension/src/content/applyResultFormatters.js +387 -0
  18. package/extension/src/content/composerAttachments.js +522 -0
  19. package/extension/src/content/composerPanel.js +254 -0
  20. package/extension/src/content/contentRuntime.js +8895 -0
  21. package/extension/src/content/contextTray.js +686 -0
  22. package/extension/src/content/diagnosticsController.js +659 -0
  23. package/extension/src/content/diagnosticsPanel.js +342 -0
  24. package/extension/src/content/diffReviewPanel.js +797 -0
  25. package/extension/src/content/localSkillsPanel.js +280 -0
  26. package/extension/src/content/markdownText.js +606 -0
  27. package/extension/src/content/mirrorHealth.js +92 -0
  28. package/extension/src/content/modelPicker.js +489 -0
  29. package/extension/src/content/nativeChannel.js +62 -0
  30. package/extension/src/content/otWarmMirror.js +964 -0
  31. package/extension/src/content/otWarmMirrorController.js +174 -0
  32. package/extension/src/content/panelMaintenance.js +367 -0
  33. package/extension/src/content/panelRenderer.js +253 -0
  34. package/extension/src/content/recentProjects.js +1063 -0
  35. package/extension/src/content/reviewHunks.js +247 -0
  36. package/extension/src/content/runController.js +242 -0
  37. package/extension/src/content/runTimelineView.js +1126 -0
  38. package/extension/src/content/sessionManager.js +487 -0
  39. package/extension/src/content/sessionPanel.js +263 -0
  40. package/extension/src/content/settingsPanel.js +527 -0
  41. package/extension/src/content/themeController.js +91 -0
  42. package/extension/src/content/updateIdle.js +67 -0
  43. package/extension/src/content/writebackController.js +300 -0
  44. package/extension/src/content/writebackOrchestrator.js +850 -0
  45. package/extension/src/contentScript.js +8 -0
  46. package/extension/src/page/compileBridge.js +460 -0
  47. package/extension/src/page/overleafCapabilities.js +100 -0
  48. package/extension/src/page/overleafEditor.js +282 -0
  49. package/extension/src/page/overleafProjectSnapshot.js +234 -0
  50. package/extension/src/page/overleafRealtimeObserver.js +491 -0
  51. package/extension/src/page/pageBridgeCapability.js +71 -0
  52. package/extension/src/page/snapshotRouter.js +948 -0
  53. package/extension/src/page/trackedChangesLifecycle.js +1506 -0
  54. package/extension/src/page/treeOperations.js +1584 -0
  55. package/extension/src/page/writeGuard.js +142 -0
  56. package/extension/src/page/writebackRouter.js +1934 -0
  57. package/extension/src/pageBridge.js +2224 -0
  58. package/extension/src/popup.js +247 -0
  59. package/extension/src/shared/compatibility.js +1 -1
  60. package/extension/styles/panel.css +4048 -0
  61. package/native-host/src/index.js +9 -2
  62. package/native-host/src/launcher.js +2 -0
  63. package/native-host/src/managedInstall.js +393 -0
  64. package/native-host/src/managedLauncherRuntime.js +71 -0
  65. package/native-host/src/nativeEnvironment.js +4 -0
  66. package/native-host/src/nativeHostPlatform.js +16 -0
  67. package/native-host/src/runtimeInstaller.js +6 -1
  68. package/native-host/src/taskRunnerRuntime.js +8 -0
  69. package/native-host/src/updateArchive.js +253 -0
  70. package/native-host/src/updateManager.js +548 -0
  71. package/native-host/src/updateTrust.js +156 -0
  72. package/package.json +11 -2
  73. package/scripts/install-managed.mjs +64 -0
  74. package/scripts/uninstall-managed.mjs +48 -0
@@ -0,0 +1,686 @@
1
+ (function initCodexOverleafContextTray(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafContextTray = factory();
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function contextTrayFactory() {
8
+ 'use strict';
9
+
10
+ const DEFAULT_CONTEXT_EXACT_FILE_LIST_ZIP_TIMEOUT_MS = 30000;
11
+
12
+ // Destructuring defaults handle the "dep omitted" case. Production callers
13
+ // always pass real functions; tests that exercise partial wiring can rely
14
+ // on undefined-only defaults to reach the noop. Passing `null` or a non-
15
+ // function value is no longer silently coerced — that was an antipattern
16
+ // (the typeof guards used to hide a missing wire-up by swapping in a noop).
17
+ function createContextTrayController({
18
+ root = (typeof window !== 'undefined' ? window : globalThis),
19
+ document: documentDep = null,
20
+ getPanel = () => null,
21
+ getState = () => null,
22
+ setState = () => {},
23
+ saveState = null,
24
+ saveStateSoon = () => {},
25
+ updateActiveSession = defaultUpdateActiveSession,
26
+ callPageBridge = () => Promise.resolve({ ok: false }),
27
+ getCurrentProjectId = () => '',
28
+ getLocationHref: getLocationHrefDep,
29
+ tr = (key) => key,
30
+ closeModelConfigPopover = () => {},
31
+ closeDiagnosticsMenu = () => {},
32
+ closeCustomInstructionsSettings = () => {},
33
+ closeSlashMenu = () => {},
34
+ projectFiles: projectFilesDep,
35
+ exactFileListZipTimeoutMs: exactFileListZipTimeoutMsDep
36
+ } = {}) {
37
+ const document = documentDep
38
+ || root.document
39
+ || (typeof globalThis !== 'undefined' ? globalThis.document : null);
40
+ const getLocationHref = getLocationHrefDep || (() => root.location?.href || '');
41
+ const projectFiles = projectFilesDep
42
+ || root.CodexOverleafProjectFiles
43
+ || (typeof window !== 'undefined' ? window.CodexOverleafProjectFiles : null);
44
+ const exactFileListZipTimeoutMs = Number(exactFileListZipTimeoutMsDep) ||
45
+ DEFAULT_CONTEXT_EXACT_FILE_LIST_ZIP_TIMEOUT_MS;
46
+
47
+ let contextProject = null;
48
+ let contextLoadId = 0;
49
+ let contextExpandedFolders = new Set();
50
+
51
+ function installContextDismiss() {
52
+ if (!document) {
53
+ return;
54
+ }
55
+ document.addEventListener('click', event => {
56
+ if (isContextTrayClickTarget(event.target)) {
57
+ return;
58
+ }
59
+ closeContextTray();
60
+ }, true);
61
+ document.addEventListener('keydown', event => {
62
+ if (event.key !== 'Escape') {
63
+ return;
64
+ }
65
+ // Layered dismissal: if the slash menu is open above the tray, let
66
+ // its own Escape handler close it first — one Escape, one layer.
67
+ const slashMenu = getPanel()?.querySelector('[data-slash-menu]');
68
+ if (slashMenu && !slashMenu.hidden) {
69
+ return;
70
+ }
71
+ const tray = getPanel()?.querySelector('[data-context-tray]');
72
+ if (tray && !tray.hidden) {
73
+ closeContextTray();
74
+ }
75
+ }, true);
76
+ }
77
+
78
+ function isContextTrayClickTarget(target) {
79
+ const panel = getPanel();
80
+ const tray = panel?.querySelector('[data-context-tray]');
81
+ const button = panel?.querySelector('[data-add-context]');
82
+ return Boolean(
83
+ (tray && tray.contains(target)) ||
84
+ (button && button.contains(target))
85
+ );
86
+ }
87
+
88
+ function toggleContextTray() {
89
+ const panel = getPanel();
90
+ const tray = panel?.querySelector('[data-context-tray]');
91
+ const button = panel?.querySelector('[data-add-context]');
92
+ if (!tray || !button) {
93
+ return;
94
+ }
95
+
96
+ const open = tray.hidden;
97
+ if (open) {
98
+ closeModelConfigPopover();
99
+ closeDiagnosticsMenu();
100
+ closeCustomInstructionsSettings();
101
+ closeSlashMenu();
102
+ }
103
+ tray.hidden = !open;
104
+ button.dataset.active = open ? 'true' : 'false';
105
+ button.setAttribute('aria-expanded', open ? 'true' : 'false');
106
+ renderContextSelection();
107
+ if (open) {
108
+ loadContextFiles({ force: false });
109
+ }
110
+ }
111
+
112
+ function closeContextTray() {
113
+ const panel = getPanel();
114
+ const tray = panel?.querySelector('[data-context-tray]');
115
+ const button = panel?.querySelector('[data-add-context]');
116
+ if (!tray || !button) {
117
+ return;
118
+ }
119
+ tray.hidden = true;
120
+ button.dataset.active = 'false';
121
+ button.setAttribute('aria-expanded', 'false');
122
+ }
123
+
124
+ async function loadContextFiles(options = {}) {
125
+ const list = getPanel()?.querySelector('[data-context-file-list]');
126
+ if (!list) {
127
+ return;
128
+ }
129
+ if (isExactContextFileListProject(contextProject) && !options.force) {
130
+ renderContextFiles(contextProject);
131
+ return;
132
+ }
133
+
134
+ const loadId = ++contextLoadId;
135
+ setContextStatus(tr('contextLoadingFiles'));
136
+ list.textContent = '';
137
+
138
+ try {
139
+ const project = await requestExactContextFiles({ force: Boolean(options.force) });
140
+ if (!project?.ok) {
141
+ throw new Error(project?.error || project?.reason || tr('unknownReason'));
142
+ }
143
+ if (loadId !== contextLoadId) {
144
+ return;
145
+ }
146
+ contextProject = project;
147
+ renderContextFiles(project);
148
+ } catch (error) {
149
+ if (loadId !== contextLoadId) {
150
+ return;
151
+ }
152
+ setContextStatus(tr('contextReadFailed', { message: error.message }));
153
+ }
154
+ }
155
+
156
+ async function requestExactContextFiles({ force = false } = {}) {
157
+ const project = await callPageBridge('getProjectSnapshot', {
158
+ force,
159
+ preferLightweight: true,
160
+ allowZipFallback: false,
161
+ allowEditorNavigation: false,
162
+ requireFullProject: true,
163
+ zipOnly: true,
164
+ includeBinaryFiles: true,
165
+ includeContent: false,
166
+ zipTimeoutMs: exactFileListZipTimeoutMs
167
+ });
168
+ return normalizeContextFileListFromZipSnapshot(project);
169
+ }
170
+
171
+ function isExactContextFileListProject(project) {
172
+ return project?.ok && project?.capabilities?.method === 'overleaf-zip-file-list';
173
+ }
174
+
175
+ function isContextFileListProject(project) {
176
+ const method = project?.capabilities?.method;
177
+ return method === 'overleaf-zip-file-list' || method === 'overleaf-file-tree';
178
+ }
179
+
180
+ function normalizeContextFileListFromZipSnapshot(project) {
181
+ const files = Array.isArray(project?.files) ? project.files : [];
182
+ if (project?.capabilities?.method !== 'overleaf-zip' || !files.length) {
183
+ return {
184
+ ok: false,
185
+ id: project?.id || getCurrentProjectId(),
186
+ url: project?.url || getLocationHref(),
187
+ activePath: project?.activePath || '',
188
+ files: [],
189
+ error: getZipSnapshotFailureReason(project),
190
+ capabilities: {
191
+ ...(project?.capabilities || {}),
192
+ method: project?.capabilities?.method || 'overleaf-zip-file-list-unavailable',
193
+ fullProjectSnapshot: false
194
+ }
195
+ };
196
+ }
197
+
198
+ return {
199
+ ok: true,
200
+ id: project.id || getCurrentProjectId(),
201
+ url: project.url || getLocationHref(),
202
+ activePath: project.activePath || '',
203
+ files: files
204
+ .map(file => normalizeContextFileFromZipSnapshot(file, project.activePath || ''))
205
+ .filter(Boolean),
206
+ capabilities: {
207
+ ...(project.capabilities || {}),
208
+ method: 'overleaf-zip-file-list',
209
+ fullProjectSnapshot: true,
210
+ note: 'Listed Overleaf project files from the exact source ZIP snapshot used by project-read diagnostics.'
211
+ }
212
+ };
213
+ }
214
+
215
+ function normalizeContextFileFromZipSnapshot(file, activePath) {
216
+ const path = normalizeProjectPath(file?.path);
217
+ if (!path) {
218
+ return null;
219
+ }
220
+ const isText = isTextProjectPath(path);
221
+ return {
222
+ path,
223
+ active: path === activePath,
224
+ source: 'overleaf-zip',
225
+ kind: isText ? 'text' : 'binary',
226
+ selectable: isText
227
+ };
228
+ }
229
+
230
+ function getZipSnapshotFailureReason(project) {
231
+ const skipped = Array.isArray(project?.capabilities?.skipped) ? project.capabilities.skipped : [];
232
+ const zipFailure = skipped.find(item => item?.path === 'project.zip' && item.reason);
233
+ return project?.error
234
+ || project?.reason
235
+ || zipFailure?.reason
236
+ || tr('unknownReason');
237
+ }
238
+
239
+ function renderContextFiles(project) {
240
+ const list = getPanel()?.querySelector('[data-context-file-list]');
241
+ if (!list) {
242
+ return;
243
+ }
244
+
245
+ list.textContent = '';
246
+ const contextFileProject = project || contextProject;
247
+ const files = getContextProjectFiles(contextFileProject?.files || []);
248
+ renderContextSelection();
249
+
250
+ if (!files.length) {
251
+ setContextStatus(tr('contextNoFiles'));
252
+ return;
253
+ }
254
+
255
+ const selected = getActiveFocusFiles();
256
+ setContextStatus(selected.length
257
+ ? tr('contextSelectedFiles', { count: selected.length })
258
+ : tr('contextDefaultWholeProject'));
259
+
260
+ const tree = buildContextTree(files);
261
+ for (const child of tree.children) {
262
+ renderContextTreeNode(child, list, selected, 0);
263
+ }
264
+ }
265
+
266
+ function getContextProjectFiles(files = []) {
267
+ const seen = new Set();
268
+ const result = [];
269
+ for (const file of files) {
270
+ if (!file?.path || seen.has(file.path)) {
271
+ continue;
272
+ }
273
+ seen.add(file.path);
274
+ result.push(file);
275
+ }
276
+ return result;
277
+ }
278
+
279
+ function buildContextTree(files = []) {
280
+ const rootNode = { type: 'folder', name: '', path: '', children: [] };
281
+ const folderByPath = new Map([['', rootNode]]);
282
+
283
+ for (const file of files) {
284
+ if (!file?.path) {
285
+ continue;
286
+ }
287
+ const parts = file.path.split('/').filter(Boolean);
288
+ if (!parts.length) {
289
+ continue;
290
+ }
291
+ let parent = rootNode;
292
+ let folderPath = '';
293
+ for (const part of parts.slice(0, -1)) {
294
+ folderPath = folderPath ? `${folderPath}/${part}` : part;
295
+ let folder = folderByPath.get(folderPath);
296
+ if (!folder) {
297
+ folder = { type: 'folder', name: part, path: folderPath, children: [] };
298
+ folderByPath.set(folderPath, folder);
299
+ parent.children.push(folder);
300
+ }
301
+ parent = folder;
302
+ }
303
+ parent.children.push({
304
+ type: 'file',
305
+ name: parts[parts.length - 1],
306
+ path: file.path,
307
+ file
308
+ });
309
+ }
310
+
311
+ sortContextTree(rootNode);
312
+ return rootNode;
313
+ }
314
+
315
+ function sortContextTree(node) {
316
+ node.children.sort((left, right) => {
317
+ if (left.type !== right.type) {
318
+ return left.type === 'folder' ? -1 : 1;
319
+ }
320
+ return left.name.localeCompare(right.name);
321
+ });
322
+ for (const child of node.children) {
323
+ if (child.type === 'folder') {
324
+ sortContextTree(child);
325
+ }
326
+ }
327
+ }
328
+
329
+ function renderContextTreeNode(node, container, selected, depth) {
330
+ if (!document) {
331
+ return;
332
+ }
333
+ if (node.type === 'folder') {
334
+ const folder = document.createElement('details');
335
+ folder.className = 'codex-context-folder';
336
+ folder.dataset.path = node.path;
337
+ folder.open = contextExpandedFolders.has(node.path);
338
+ folder.addEventListener('toggle', () => {
339
+ if (folder.open) {
340
+ contextExpandedFolders.add(node.path);
341
+ } else {
342
+ contextExpandedFolders.delete(node.path);
343
+ }
344
+ });
345
+
346
+ const label = document.createElement('summary');
347
+ label.className = 'codex-context-folder-name';
348
+ label.style.paddingLeft = `${depth * 14 + 7}px`;
349
+ label.textContent = node.name;
350
+
351
+ const children = document.createElement('div');
352
+ children.className = 'codex-context-folder-children';
353
+
354
+ for (const child of node.children) {
355
+ renderContextTreeNode(child, children, selected, depth + 1);
356
+ }
357
+ folder.append(label, children);
358
+ container.append(folder);
359
+ return;
360
+ }
361
+
362
+ const file = node.file;
363
+ const selectable = file.selectable !== false;
364
+ const button = document.createElement('button');
365
+ button.type = 'button';
366
+ button.className = 'codex-context-file';
367
+ button.dataset.path = file.path;
368
+ button.dataset.selected = selectable && selected.includes(file.path) ? 'true' : 'false';
369
+ button.dataset.selectable = selectable ? 'true' : 'false';
370
+ button.disabled = !selectable;
371
+ button.setAttribute('aria-pressed', selectable && selected.includes(file.path) ? 'true' : 'false');
372
+ button.style.paddingLeft = `${depth * 14 + 7}px`;
373
+ button.title = selectable ? file.path : tr('contextResourceTitle', { path: file.path });
374
+ if (selectable) {
375
+ button.addEventListener('click', () => selectFocusFile(file.path));
376
+ }
377
+
378
+ const name = document.createElement('span');
379
+ name.className = 'codex-context-file-name';
380
+ name.textContent = node.name;
381
+ name.title = node.path || node.name;
382
+
383
+ const meta = document.createElement('span');
384
+ meta.className = 'codex-context-file-meta';
385
+ meta.textContent = selectable
386
+ ? (file.content ? `${String(file.content).length} chars` : file.source || '')
387
+ : tr('resource');
388
+
389
+ button.append(name, meta);
390
+ container.append(button);
391
+ }
392
+
393
+ function renderContextSelection() {
394
+ const panel = getPanel();
395
+ const selection = panel?.querySelector('[data-context-selection]');
396
+ const addButton = panel?.querySelector('[data-add-context]');
397
+ if (!selection || !addButton || !document) {
398
+ return;
399
+ }
400
+
401
+ selection.textContent = '';
402
+ const selected = getActiveFocusFiles();
403
+ addButton.dataset.hasFocus = selected.length ? 'true' : 'false';
404
+ addButton.title = selected.length ? `@context: ${selected.map(path => `@file:${path}`).join(', ')}` : tr('addContext');
405
+
406
+ const chip = document.createElement('div');
407
+ chip.className = 'codex-context-chip';
408
+
409
+ const dot = document.createElement('span');
410
+ dot.className = 'codex-context-dot';
411
+ dot.setAttribute('aria-hidden', 'true');
412
+
413
+ const label = document.createElement('span');
414
+ label.textContent = selected.length ? selected.map(path => `@file:${path}`).join(', ') : tr('contextWholeProjectChip');
415
+ chip.append(dot, label);
416
+
417
+ if (selected.length) {
418
+ const clear = document.createElement('button');
419
+ clear.type = 'button';
420
+ clear.dataset.contextClear = '';
421
+ clear.title = tr('clearFiles');
422
+ clear.setAttribute('aria-label', tr('clearFiles'));
423
+ clear.textContent = '×';
424
+ clear.addEventListener('click', clearFocusFile);
425
+ chip.append(clear);
426
+ }
427
+
428
+ selection.append(chip);
429
+ renderContextSummary();
430
+ }
431
+
432
+ function renderContextSummary() {
433
+ const summary = getPanel()?.querySelector('[data-context-summary]');
434
+ if (!summary || !document) {
435
+ return;
436
+ }
437
+
438
+ const selected = getActiveFocusFiles();
439
+ hideContextSummaryTooltip();
440
+ summary.textContent = '';
441
+ summary.hidden = selected.length === 0;
442
+ if (!selected.length) {
443
+ return;
444
+ }
445
+
446
+ const prefix = document.createElement('span');
447
+ prefix.className = 'codex-context-summary-label';
448
+ prefix.textContent = '@context';
449
+ summary.append(prefix);
450
+
451
+ for (const path of selected) {
452
+ const chip = document.createElement('span');
453
+ chip.className = 'codex-context-summary-chip';
454
+ chip.title = path;
455
+ chip.setAttribute('aria-label', path);
456
+ chip.addEventListener('mouseenter', () => showContextSummaryTooltip(path, chip));
457
+ chip.addEventListener('mouseleave', hideContextSummaryTooltip);
458
+ chip.addEventListener('focusin', () => showContextSummaryTooltip(path, chip));
459
+ chip.addEventListener('focusout', hideContextSummaryTooltip);
460
+ const chipLabel = document.createElement('span');
461
+ chipLabel.className = 'codex-context-summary-chip-label';
462
+ chipLabel.textContent = path;
463
+ chipLabel.title = path;
464
+ const remove = document.createElement('button');
465
+ remove.type = 'button';
466
+ remove.className = 'codex-context-summary-remove';
467
+ remove.setAttribute('data-context-summary-remove', '');
468
+ remove.title = tr('removeContextFile', { path });
469
+ remove.setAttribute('aria-label', tr('removeContextFile', { path }));
470
+ remove.textContent = '×';
471
+ remove.addEventListener('click', event => {
472
+ event.preventDefault();
473
+ event.stopPropagation();
474
+ removeFocusFile(path);
475
+ });
476
+ chip.append(chipLabel, remove);
477
+ summary.append(chip);
478
+ }
479
+ }
480
+
481
+ function ensureContextSummaryTooltip() {
482
+ const panel = getPanel();
483
+ if (!panel || !document) {
484
+ return null;
485
+ }
486
+ const existing = panel.querySelector('[data-context-summary-tooltip]');
487
+ if (existing) {
488
+ return existing;
489
+ }
490
+ const tooltip = document.createElement('div');
491
+ tooltip.className = 'codex-context-summary-tooltip';
492
+ tooltip.setAttribute('data-context-summary-tooltip', '');
493
+ tooltip.hidden = true;
494
+ panel.append(tooltip);
495
+ return tooltip;
496
+ }
497
+
498
+ function showContextSummaryTooltip(path, anchor) {
499
+ const panel = getPanel();
500
+ const tooltip = ensureContextSummaryTooltip();
501
+ if (!panel || !tooltip) {
502
+ return;
503
+ }
504
+ tooltip.textContent = path;
505
+ tooltip.hidden = false;
506
+ tooltip.style.left = '8px';
507
+ tooltip.style.right = '8px';
508
+ const panelRect = typeof panel.getBoundingClientRect === 'function' ? panel.getBoundingClientRect() : null;
509
+ const anchorRect = typeof anchor?.getBoundingClientRect === 'function' ? anchor.getBoundingClientRect() : null;
510
+ if (!panelRect || !anchorRect) {
511
+ tooltip.style.top = 'auto';
512
+ tooltip.style.bottom = '80px';
513
+ return;
514
+ }
515
+ const maxTop = Math.max(8, Number(panelRect.height || 0) - 48);
516
+ let top = Number(anchorRect.top || 0) - Number(panelRect.top || 0) - 34;
517
+ if (top < 8) {
518
+ top = Number(anchorRect.bottom || 0) - Number(panelRect.top || 0) + 8;
519
+ }
520
+ tooltip.style.top = `${Math.min(Math.max(8, Math.round(top)), maxTop)}px`;
521
+ tooltip.style.bottom = 'auto';
522
+ }
523
+
524
+ function hideContextSummaryTooltip() {
525
+ const tooltip = getPanel()?.querySelector('[data-context-summary-tooltip]');
526
+ if (tooltip) {
527
+ tooltip.hidden = true;
528
+ }
529
+ }
530
+
531
+ async function selectFocusFile(path) {
532
+ if (typeof path !== 'string' || !path.trim()) {
533
+ return;
534
+ }
535
+ const normalizedPath = path.trim();
536
+ const currentFocusFiles = getActiveFocusFiles();
537
+ const nextFocusFiles = currentFocusFiles.includes(normalizedPath)
538
+ ? currentFocusFiles.filter(item => item !== normalizedPath)
539
+ : [...currentFocusFiles, normalizedPath].slice(-5);
540
+ await persistFocusFiles(nextFocusFiles);
541
+ renderContextSelection();
542
+ renderContextFiles(contextProject);
543
+ }
544
+
545
+ async function clearFocusFile() {
546
+ await persistFocusFiles([]);
547
+ renderContextSelection();
548
+ renderContextFiles(contextProject);
549
+ }
550
+
551
+ async function removeFocusFile(path) {
552
+ const normalizedPath = typeof path === 'string' ? path.trim() : '';
553
+ if (!normalizedPath) {
554
+ return;
555
+ }
556
+ const nextFocusFiles = getActiveFocusFiles().filter(item => item !== normalizedPath);
557
+ await persistFocusFiles(nextFocusFiles);
558
+ renderContextSelection();
559
+ renderContextFiles(contextProject);
560
+ }
561
+
562
+ function getActiveFocusFiles() {
563
+ const state = getState();
564
+ return Array.isArray(state?.session?.focusFiles) ? state.session.focusFiles : [];
565
+ }
566
+
567
+ function sortContextFiles(files, activePath) {
568
+ return files
569
+ .filter(file => typeof file?.path === 'string' && file.path.trim())
570
+ .sort((left, right) => {
571
+ if (left.path === activePath) {
572
+ return -1;
573
+ }
574
+ if (right.path === activePath) {
575
+ return 1;
576
+ }
577
+ const leftRank = getContextFileRank(left.path);
578
+ const rightRank = getContextFileRank(right.path);
579
+ if (leftRank !== rightRank) {
580
+ return leftRank - rightRank;
581
+ }
582
+ return left.path.localeCompare(right.path);
583
+ });
584
+ }
585
+
586
+ function getContextFileRank(path) {
587
+ if (/\.tex$/i.test(path)) {
588
+ return 0;
589
+ }
590
+ if (/\.bib$/i.test(path)) {
591
+ return 1;
592
+ }
593
+ return 2;
594
+ }
595
+
596
+ function setContextStatus(text) {
597
+ const status = getPanel()?.querySelector('[data-context-status]');
598
+ if (status) {
599
+ status.textContent = text;
600
+ status.dataset.customStatus = 'true';
601
+ }
602
+ }
603
+
604
+ async function persistFocusFiles(focusFiles) {
605
+ const nextState = updateActiveSession(getState(), {
606
+ focusFiles
607
+ });
608
+ setState(nextState);
609
+ if (saveState) {
610
+ await saveState();
611
+ return;
612
+ }
613
+ saveStateSoon();
614
+ }
615
+
616
+ function normalizeProjectPath(path) {
617
+ if (typeof projectFiles?.normalizePath === 'function') {
618
+ return projectFiles.normalizePath(path) || '';
619
+ }
620
+ return String(path || '').replace(/\\/g, '/').replace(/^\/+/, '').trim();
621
+ }
622
+
623
+ function isTextProjectPath(path) {
624
+ if (typeof projectFiles?.isTextProjectPath === 'function') {
625
+ return projectFiles.isTextProjectPath(path);
626
+ }
627
+ return /\.(?:tex|bib|cls|sty|bst|txt|md|rmd|ltx|tikz|csv|tsv|json|yaml|yml)$/i.test(path);
628
+ }
629
+
630
+ function resetContextProject() {
631
+ contextProject = null;
632
+ contextLoadId += 1;
633
+ }
634
+
635
+ function getContextProject() {
636
+ return contextProject;
637
+ }
638
+
639
+ return {
640
+ installContextDismiss,
641
+ isContextTrayClickTarget,
642
+ toggleContextTray,
643
+ closeContextTray,
644
+ loadContextFiles,
645
+ requestExactContextFiles,
646
+ isExactContextFileListProject,
647
+ isContextFileListProject,
648
+ normalizeContextFileListFromZipSnapshot,
649
+ normalizeContextFileFromZipSnapshot,
650
+ getZipSnapshotFailureReason,
651
+ renderContextFiles,
652
+ getContextProjectFiles,
653
+ buildContextTree,
654
+ sortContextTree,
655
+ renderContextTreeNode,
656
+ renderContextSelection,
657
+ renderContextSummary,
658
+ selectFocusFile,
659
+ clearFocusFile,
660
+ removeFocusFile,
661
+ getActiveFocusFiles,
662
+ sortContextFiles,
663
+ getContextFileRank,
664
+ setContextStatus,
665
+ resetContextProject,
666
+ getContextProject
667
+ };
668
+ }
669
+
670
+ function defaultUpdateActiveSession(state, patch = {}) {
671
+ return {
672
+ ...(state || {}),
673
+ session: {
674
+ ...((state && state.session) || {}),
675
+ ...patch
676
+ }
677
+ };
678
+ }
679
+
680
+ return {
681
+ createContextTrayController,
682
+ _private: {
683
+ DEFAULT_CONTEXT_EXACT_FILE_LIST_ZIP_TIMEOUT_MS
684
+ }
685
+ };
686
+ });