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.
- package/README.md +45 -21
- package/bin/codex-overleaf-link.mjs +58 -0
- package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
- package/extension/assets/icons/codex-overleaf-icon.png +0 -0
- package/extension/assets/icons/icon128.png +0 -0
- package/extension/assets/icons/icon16.png +0 -0
- package/extension/assets/icons/icon32.png +0 -0
- package/extension/assets/icons/icon48.png +0 -0
- package/extension/bootstrap/background.js +404 -0
- package/extension/bootstrap/manifest.template.json +51 -0
- package/extension/bootstrap/popup.css +26 -0
- package/extension/bootstrap/popup.html +38 -0
- package/extension/bootstrap/popup.js +44 -0
- package/extension/bootstrap/runtimeContext.js +2 -0
- package/extension/runtime-manifest.json +60 -0
- package/extension/src/background.js +532 -0
- package/extension/src/content/applyResultFormatters.js +387 -0
- package/extension/src/content/composerAttachments.js +522 -0
- package/extension/src/content/composerPanel.js +254 -0
- package/extension/src/content/contentRuntime.js +8895 -0
- package/extension/src/content/contextTray.js +686 -0
- package/extension/src/content/diagnosticsController.js +659 -0
- package/extension/src/content/diagnosticsPanel.js +342 -0
- package/extension/src/content/diffReviewPanel.js +797 -0
- package/extension/src/content/localSkillsPanel.js +280 -0
- package/extension/src/content/markdownText.js +606 -0
- package/extension/src/content/mirrorHealth.js +92 -0
- package/extension/src/content/modelPicker.js +489 -0
- package/extension/src/content/nativeChannel.js +62 -0
- package/extension/src/content/otWarmMirror.js +964 -0
- package/extension/src/content/otWarmMirrorController.js +174 -0
- package/extension/src/content/panelMaintenance.js +367 -0
- package/extension/src/content/panelRenderer.js +253 -0
- package/extension/src/content/recentProjects.js +1063 -0
- package/extension/src/content/reviewHunks.js +247 -0
- package/extension/src/content/runController.js +242 -0
- package/extension/src/content/runTimelineView.js +1126 -0
- package/extension/src/content/sessionManager.js +487 -0
- package/extension/src/content/sessionPanel.js +263 -0
- package/extension/src/content/settingsPanel.js +527 -0
- package/extension/src/content/themeController.js +91 -0
- package/extension/src/content/updateIdle.js +67 -0
- package/extension/src/content/writebackController.js +300 -0
- package/extension/src/content/writebackOrchestrator.js +850 -0
- package/extension/src/contentScript.js +8 -0
- package/extension/src/page/compileBridge.js +460 -0
- package/extension/src/page/overleafCapabilities.js +100 -0
- package/extension/src/page/overleafEditor.js +282 -0
- package/extension/src/page/overleafProjectSnapshot.js +234 -0
- package/extension/src/page/overleafRealtimeObserver.js +491 -0
- package/extension/src/page/pageBridgeCapability.js +71 -0
- package/extension/src/page/snapshotRouter.js +948 -0
- package/extension/src/page/trackedChangesLifecycle.js +1506 -0
- package/extension/src/page/treeOperations.js +1584 -0
- package/extension/src/page/writeGuard.js +142 -0
- package/extension/src/page/writebackRouter.js +1934 -0
- package/extension/src/pageBridge.js +2224 -0
- package/extension/src/popup.js +247 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/styles/panel.css +4048 -0
- package/native-host/src/index.js +9 -2
- package/native-host/src/launcher.js +2 -0
- package/native-host/src/managedInstall.js +393 -0
- package/native-host/src/managedLauncherRuntime.js +71 -0
- package/native-host/src/nativeEnvironment.js +4 -0
- package/native-host/src/nativeHostPlatform.js +16 -0
- package/native-host/src/runtimeInstaller.js +6 -1
- package/native-host/src/taskRunnerRuntime.js +8 -0
- package/native-host/src/updateArchive.js +253 -0
- package/native-host/src/updateManager.js +548 -0
- package/native-host/src/updateTrust.js +156 -0
- package/package.json +11 -2
- package/scripts/install-managed.mjs +64 -0
- 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
|
+
});
|