codex-overleaf-link 2.3.3 → 2.3.5
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 +318 -211
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +5 -0
- package/extension/src/content/composerPanel.js +1 -1
- package/extension/src/content/contentRuntime.js +68 -33
- package/extension/src/content/generated/content.bundle.js +604 -132
- package/extension/src/content/generated/content.bundle.meta.json +5 -3
- package/extension/src/content/globalPreferencesController.js +125 -0
- package/extension/src/content/legacyGlobalRegistry.js +2 -0
- package/extension/src/content/modelPicker.js +1 -1
- package/extension/src/content/panelMaintenance.js +2 -0
- package/extension/src/content/panelRenderer.js +86 -2
- package/extension/src/content/projectSettingsCoordinator.js +1 -0
- package/extension/src/content/providerSettingsCoordinator.js +15 -0
- package/extension/src/content/providerSettingsDialog.js +24 -15
- package/extension/src/content/runController.js +35 -0
- package/extension/src/content/settingsPanel.js +5 -3
- package/extension/src/content/themeController.js +3 -0
- package/extension/src/popup.js +10 -10
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/globalPreferences.js +131 -0
- package/extension/src/shared/i18n.js +2 -2
- package/extension/styles/panel.css +89 -2
- package/package.json +1 -1
- package/scripts/install-managed.mjs +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"entry": "extension/entries/content-entry.mjs",
|
|
4
4
|
"output": "extension/src/content/generated/content.bundle.js",
|
|
5
|
-
"sourceDigest": "
|
|
6
|
-
"outputDigest": "
|
|
7
|
-
"byteLength":
|
|
5
|
+
"sourceDigest": "7ed22d5c82b20220316824547e1dfc359e45dfcdf1f1bca2fb7280195a05361c",
|
|
6
|
+
"outputDigest": "50436e35fe942b3fd8540a6267fba79c41a53c216b1ffc42995e4ed026771de4",
|
|
7
|
+
"byteLength": 2337940,
|
|
8
8
|
"inputs": [
|
|
9
9
|
"extension/entries/content-entry.mjs",
|
|
10
10
|
"extension/src/content/activeTurnControl.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"extension/src/content/diagnosticsController.js",
|
|
18
18
|
"extension/src/content/diagnosticsPanel.js",
|
|
19
19
|
"extension/src/content/diffReviewPanel.js",
|
|
20
|
+
"extension/src/content/globalPreferencesController.js",
|
|
20
21
|
"extension/src/content/legacyGlobalRegistry.js",
|
|
21
22
|
"extension/src/content/localSkillsPanel.js",
|
|
22
23
|
"extension/src/content/markdownDomRenderer.js",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"extension/src/shared/compatibility.js",
|
|
69
70
|
"extension/src/shared/compileAdapter.js",
|
|
70
71
|
"extension/src/shared/failureReasons.js",
|
|
72
|
+
"extension/src/shared/globalPreferences.js",
|
|
71
73
|
"extension/src/shared/governanceRules.js",
|
|
72
74
|
"extension/src/shared/i18n.js",
|
|
73
75
|
"extension/src/shared/lineReferences.js",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory(require('../shared/globalPreferences'));
|
|
3
|
+
else root.CodexOverleafModuleRegistry.define('GlobalPreferencesController', ['GlobalPreferences'], factory);
|
|
4
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function (Preferences) {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const CONTROLS = Object.freeze({ theme: '[data-theme-select]', locale: '[data-language-select]',
|
|
8
|
+
preloadProjectContext: '[data-preload-project-context]', loadCodexLocalSkills: '[data-load-codex-local-skills]',
|
|
9
|
+
loadCodexOverleafSkills: '[data-load-codex-overleaf-skills]' });
|
|
10
|
+
|
|
11
|
+
function create(options) {
|
|
12
|
+
const chromeApi = options.chromeApi;
|
|
13
|
+
const fallback = Preferences.pick(options.getState?.() || {});
|
|
14
|
+
let snapshot = null;
|
|
15
|
+
let loading = null;
|
|
16
|
+
let subscribed = false;
|
|
17
|
+
let disposed = false;
|
|
18
|
+
let lastError = null;
|
|
19
|
+
let tail = Promise.resolve();
|
|
20
|
+
const pending = [];
|
|
21
|
+
|
|
22
|
+
function view() {
|
|
23
|
+
return pending.reduce((values, operation) => Preferences.applyPatch(values, operation.patch), snapshot?.values || fallback);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function render(force = false) {
|
|
27
|
+
if (disposed) return;
|
|
28
|
+
const previous = options.getState?.() || {};
|
|
29
|
+
const values = view();
|
|
30
|
+
const changed = Preferences.changedKeys(Preferences.pick(previous), values);
|
|
31
|
+
options.setState?.({ ...previous, ...values });
|
|
32
|
+
const panel = options.getPanel?.();
|
|
33
|
+
if (!panel) return;
|
|
34
|
+
for (const [field, selector] of Object.entries(CONTROLS)) {
|
|
35
|
+
const control = panel.querySelector(selector);
|
|
36
|
+
if (!control) continue;
|
|
37
|
+
if (typeof values[field] === 'boolean') control.checked = values[field];
|
|
38
|
+
else if (control.value !== values[field]) control.value = values[field];
|
|
39
|
+
}
|
|
40
|
+
if (force || changed.includes('theme')) options.applyTheme?.(values.theme);
|
|
41
|
+
if (changed.includes('locale')) options.applyLocale?.();
|
|
42
|
+
if (force || changed.includes('preloadProjectContext')) options.onPreloadChange?.(values.preloadProjectContext);
|
|
43
|
+
if (changed.some(key => key === 'codexOverleafSkillEnabled' || key.startsWith('loadCodex'))) options.onSkillsChange?.();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function accept(value) {
|
|
47
|
+
const next = Preferences.validateSnapshot(value);
|
|
48
|
+
lastError = null;
|
|
49
|
+
if (!snapshot || next.revision > snapshot.revision) {
|
|
50
|
+
snapshot = next;
|
|
51
|
+
render();
|
|
52
|
+
}
|
|
53
|
+
return snapshot;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function request(type, patch) {
|
|
57
|
+
const response = await chromeApi.runtime.sendMessage({ type, ...(patch ? { patch } : {}) });
|
|
58
|
+
if (!response?.ok) throw new Error(response?.error?.message || 'Global preferences are unavailable.');
|
|
59
|
+
return response.result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onStorageChange(changes, area) {
|
|
63
|
+
if (disposed || area !== 'local' || !changes[Preferences.STORAGE_KEY]?.newValue) return;
|
|
64
|
+
try { accept(changes[Preferences.STORAGE_KEY].newValue); } catch (error) { reportError(error); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function reportError(error) {
|
|
68
|
+
lastError = error;
|
|
69
|
+
options.onError?.(error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function initialize() {
|
|
73
|
+
if (!subscribed) {
|
|
74
|
+
chromeApi.storage.onChanged.addListener(onStorageChange);
|
|
75
|
+
subscribed = true;
|
|
76
|
+
}
|
|
77
|
+
if (snapshot) return snapshot;
|
|
78
|
+
if (!loading) {
|
|
79
|
+
loading = request(Preferences.READ_MESSAGE).then(accept).catch(error => {
|
|
80
|
+
reportError(error);
|
|
81
|
+
return null;
|
|
82
|
+
}).finally(() => { loading = null; });
|
|
83
|
+
}
|
|
84
|
+
return loading;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function update(input) {
|
|
88
|
+
const patch = Preferences.validatePatch(input);
|
|
89
|
+
if (!Preferences.changedKeys(view(), Preferences.applyPatch(view(), patch)).length) return pending.length ? tail : Promise.resolve(snapshot);
|
|
90
|
+
const operation = { patch };
|
|
91
|
+
pending.push(operation);
|
|
92
|
+
render();
|
|
93
|
+
const result = tail.catch(() => {}).then(() => request(Preferences.PATCH_MESSAGE, patch)).then(Preferences.validateSnapshot).then(value => {
|
|
94
|
+
accept(value);
|
|
95
|
+
pending.splice(pending.indexOf(operation), 1);
|
|
96
|
+
render();
|
|
97
|
+
return snapshot;
|
|
98
|
+
}, error => {
|
|
99
|
+
pending.splice(pending.indexOf(operation), 1);
|
|
100
|
+
render();
|
|
101
|
+
reportError(error);
|
|
102
|
+
throw error;
|
|
103
|
+
});
|
|
104
|
+
tail = result;
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleInput(event) {
|
|
109
|
+
const field = Object.keys(CONTROLS).find(key => event?.target?.matches?.(CONTROLS[key]));
|
|
110
|
+
if (!field) return null;
|
|
111
|
+
return update({ [field]: ['theme', 'locale'].includes(field) ? event.target.value : Boolean(event.target.checked) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Object.freeze({ initialize, update, handleInput,
|
|
115
|
+
overlay: state => snapshot || pending.length ? { ...state, ...view() } : state,
|
|
116
|
+
refreshView: () => { if (snapshot) render(true); if (lastError) options.onError?.(lastError); },
|
|
117
|
+
destroy() {
|
|
118
|
+
disposed = true;
|
|
119
|
+
if (subscribed) chromeApi.storage.onChanged.removeListener(onStorageChange);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Object.freeze({ create });
|
|
125
|
+
});
|
|
@@ -319,7 +319,7 @@
|
|
|
319
319
|
if (!efforts.length) {
|
|
320
320
|
efforts = presentation === 'none' ? ['none'] : ['low', 'medium', 'high', 'xhigh'];
|
|
321
321
|
}
|
|
322
|
-
const current =
|
|
322
|
+
const current = getState()?.reasoningEffort || reasoningSelect.value || '';
|
|
323
323
|
const fallback = efforts.includes(model?.defaultReasoningEffort)
|
|
324
324
|
? model.defaultReasoningEffort
|
|
325
325
|
: efforts.includes('high') ? 'high' : efforts[0];
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
setState,
|
|
30
30
|
getCurrentRunView,
|
|
31
31
|
getSettingsPanelInstance,
|
|
32
|
+
prepareRetryReplacement,
|
|
32
33
|
StorageDb
|
|
33
34
|
} = deps;
|
|
34
35
|
|
|
@@ -218,6 +219,7 @@
|
|
|
218
219
|
showPluginToast(tx('Nothing to refill: the original task text is unavailable.', '无法回填:原任务文本不可用。'));
|
|
219
220
|
return;
|
|
220
221
|
}
|
|
222
|
+
prepareRetryReplacement?.(run || null);
|
|
221
223
|
input.value = task;
|
|
222
224
|
getState().task = task;
|
|
223
225
|
autosizeTaskTextarea();
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
const ROOT_CLASS = 'codex-overleaf-panel-mounted';
|
|
5
5
|
const RESIZE_CLASS = 'codex-overleaf-panel-resizing';
|
|
6
6
|
const COMPACT_CLASS = 'codex-overleaf-panel-compact';
|
|
7
|
+
const LAUNCHER_VISIBILITY_STORAGE_KEY = 'codex-overleaf:launcher-visible';
|
|
7
8
|
|
|
8
9
|
function codexIcon(name) {
|
|
9
10
|
const icons = {
|
|
10
11
|
refresh: '<path d="M4.2 5.3A5 5 0 1 1 3.6 10"/><path d="M4.2 2.2v3.1H1.1"/>',
|
|
11
12
|
plus: '<path d="M8 3.2v9.6"/><path d="M3.2 8h9.6"/>',
|
|
13
|
+
panel: '<rect x="2.3" y="2.3" width="11.4" height="11.4" rx="2"/><path d="M9.2 2.5v11"/><path d="M5.1 6.1h1.5M5.1 8h1.5M5.1 9.9h1.5"/>',
|
|
14
|
+
collapse: '<path d="m6.1 3.3 4.7 4.7-4.7 4.7"/>',
|
|
12
15
|
settings: '<path d="M3.4 5.2h5.9"/><path d="M11.7 5.2h.9"/><path d="M10.1 3.6v3.2"/><path d="M3.4 10.8h1"/><path d="M6.8 10.8h5.8"/><path d="M5.9 9.2v3.2"/>'
|
|
13
16
|
};
|
|
14
17
|
const safeName = Object.prototype.hasOwnProperty.call(icons, name) ? name : 'settings';
|
|
@@ -19,9 +22,11 @@
|
|
|
19
22
|
const doc = options.document || document;
|
|
20
23
|
const container = options.container || doc.documentElement;
|
|
21
24
|
const panelEl = doc.createElement('aside');
|
|
25
|
+
const launcherEl = doc.createElement('button');
|
|
22
26
|
const callbacks = options.callbacks || {};
|
|
23
27
|
const instance = {
|
|
24
28
|
panelEl,
|
|
29
|
+
launcherEl,
|
|
25
30
|
callbacks,
|
|
26
31
|
defaultWidth: options.defaultWidth || 380,
|
|
27
32
|
minWidth: options.minWidth || 340,
|
|
@@ -30,9 +35,18 @@
|
|
|
30
35
|
lastDesktopWidth: options.initialWidth || options.defaultWidth || 380,
|
|
31
36
|
listeners: [],
|
|
32
37
|
document: doc,
|
|
33
|
-
window: options.window || window
|
|
38
|
+
window: options.window || window,
|
|
39
|
+
chromeApi: options.chromeApi || (typeof chrome !== 'undefined' ? chrome : null),
|
|
40
|
+
storageChangeListener: null
|
|
34
41
|
};
|
|
35
42
|
|
|
43
|
+
launcherEl.id = 'codex-overleaf-launcher';
|
|
44
|
+
launcherEl.type = 'button';
|
|
45
|
+
launcherEl.title = options.launcherLabel || 'Show or hide Codex panel';
|
|
46
|
+
launcherEl.setAttribute('aria-label', launcherEl.title);
|
|
47
|
+
launcherEl.setAttribute('aria-expanded', 'false');
|
|
48
|
+
launcherEl.innerHTML = codexIcon('panel');
|
|
49
|
+
|
|
36
50
|
panelEl.id = options.panelId || 'codex-overleaf-panel';
|
|
37
51
|
panelEl.dataset.view = 'session';
|
|
38
52
|
panelEl.innerHTML = `
|
|
@@ -44,6 +58,7 @@
|
|
|
44
58
|
<div data-diagnostics-slot></div>
|
|
45
59
|
<button type="button" data-new-session title="New Session" aria-label="New Session">${codexIcon('plus')}</button>
|
|
46
60
|
<button type="button" data-custom-instructions-settings title="Project Settings" aria-label="Project Settings" aria-expanded="false">${codexIcon('settings')}</button>
|
|
61
|
+
<button type="button" data-close-panel title="Close Codex panel" aria-label="Close Codex panel">${codexIcon('collapse')}</button>
|
|
47
62
|
</div>
|
|
48
63
|
</div>
|
|
49
64
|
<div data-settings-slot></div>
|
|
@@ -59,6 +74,7 @@
|
|
|
59
74
|
`;
|
|
60
75
|
|
|
61
76
|
container.append(panelEl);
|
|
77
|
+
container.append(launcherEl);
|
|
62
78
|
doc.documentElement.classList.add(ROOT_CLASS);
|
|
63
79
|
|
|
64
80
|
instance.headerEl = panelEl.querySelector('[data-panel-header]');
|
|
@@ -73,6 +89,8 @@
|
|
|
73
89
|
bind(instance, panelEl.querySelector('[data-refresh]'), 'click', () => callbacks.onRefresh?.());
|
|
74
90
|
bind(instance, panelEl.querySelector('[data-new-session]'), 'click', () => callbacks.onNewSession?.());
|
|
75
91
|
bind(instance, panelEl.querySelector('[data-custom-instructions-settings]'), 'click', () => callbacks.onSettingsClick?.());
|
|
92
|
+
bind(instance, panelEl.querySelector('[data-close-panel]'), 'click', () => callbacks.onClosePanel?.());
|
|
93
|
+
bind(instance, launcherEl, 'click', () => callbacks.onLauncherToggle?.());
|
|
76
94
|
bind(instance, panelEl.querySelector('[data-panel-resize-handle]'), 'pointerdown', event => startResize(instance, event));
|
|
77
95
|
bind(instance, panelEl.querySelector('[data-panel-resize-handle]'), 'dblclick', event => {
|
|
78
96
|
event?.preventDefault?.();
|
|
@@ -87,9 +105,11 @@
|
|
|
87
105
|
});
|
|
88
106
|
|
|
89
107
|
setWidth(instance, options.initialWidth || instance.defaultWidth, { notify: false });
|
|
108
|
+
initializeLauncherVisibility(instance);
|
|
90
109
|
|
|
91
110
|
return {
|
|
92
111
|
panelEl,
|
|
112
|
+
launcherEl,
|
|
93
113
|
headerEl: instance.headerEl,
|
|
94
114
|
bodyEl: instance.bodyEl,
|
|
95
115
|
diagnosticsSlot: instance.diagnosticsSlot,
|
|
@@ -198,11 +218,64 @@
|
|
|
198
218
|
function setVisible(panelEl, visible) {
|
|
199
219
|
panelEl?.classList?.toggle('is-open', Boolean(visible));
|
|
200
220
|
document.documentElement.classList.toggle(ROOT_CLASS, Boolean(visible));
|
|
221
|
+
const launcherEl = panelEl?.ownerDocument?.getElementById?.('codex-overleaf-launcher');
|
|
222
|
+
launcherEl?.classList?.toggle('is-panel-open', Boolean(visible));
|
|
223
|
+
launcherEl?.setAttribute?.('aria-expanded', visible ? 'true' : 'false');
|
|
224
|
+
if (launcherEl) {
|
|
225
|
+
launcherEl.hidden = Boolean(visible) || launcherEl.dataset.enabled === 'false';
|
|
226
|
+
launcherEl.setAttribute('aria-hidden', launcherEl.hidden ? 'true' : 'false');
|
|
227
|
+
}
|
|
201
228
|
if (!visible) {
|
|
202
229
|
document.documentElement.classList.remove(RESIZE_CLASS, COMPACT_CLASS);
|
|
203
230
|
}
|
|
204
231
|
}
|
|
205
232
|
|
|
233
|
+
function initializeLauncherVisibility(instance) {
|
|
234
|
+
const storage = instance.chromeApi?.storage;
|
|
235
|
+
if (!storage?.local?.get) {
|
|
236
|
+
setLauncherVisible(instance, true, { persist: false });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
Promise.resolve(storage.local.get({ [LAUNCHER_VISIBILITY_STORAGE_KEY]: true }))
|
|
240
|
+
.then(values => applyLauncherVisibility(instance, values?.[LAUNCHER_VISIBILITY_STORAGE_KEY]))
|
|
241
|
+
.catch(() => applyLauncherVisibility(instance, true));
|
|
242
|
+
if (storage.onChanged?.addListener) {
|
|
243
|
+
instance.storageChangeListener = (changes, areaName) => {
|
|
244
|
+
if (areaName === 'local' && changes?.[LAUNCHER_VISIBILITY_STORAGE_KEY]) {
|
|
245
|
+
applyLauncherVisibility(instance, changes[LAUNCHER_VISIBILITY_STORAGE_KEY].newValue);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
storage.onChanged.addListener(instance.storageChangeListener);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function applyLauncherVisibility(instance, value) {
|
|
253
|
+
const visible = value !== false;
|
|
254
|
+
setLauncherVisible(instance, visible, { persist: false });
|
|
255
|
+
instance.callbacks.onLauncherVisibilityChange?.(visible);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function setLauncherVisible(target, visible, options = {}) {
|
|
259
|
+
const instance = target?._instance || target;
|
|
260
|
+
const launcherEl = instance?.launcherEl;
|
|
261
|
+
if (!launcherEl) return false;
|
|
262
|
+
const nextVisible = Boolean(visible);
|
|
263
|
+
launcherEl.dataset.enabled = nextVisible ? 'true' : 'false';
|
|
264
|
+
launcherEl.hidden = !nextVisible || instance.panelEl?.classList?.contains('is-open') === true;
|
|
265
|
+
launcherEl.setAttribute('aria-hidden', launcherEl.hidden ? 'true' : 'false');
|
|
266
|
+
if (options.persist === true) {
|
|
267
|
+
Promise.resolve(instance.chromeApi?.storage?.local?.set?.({
|
|
268
|
+
[LAUNCHER_VISIBILITY_STORAGE_KEY]: nextVisible
|
|
269
|
+
})).catch(() => {});
|
|
270
|
+
}
|
|
271
|
+
return nextVisible;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isLauncherVisible(target) {
|
|
275
|
+
const instance = target?._instance || target;
|
|
276
|
+
return Boolean(instance?.launcherEl && instance.launcherEl.dataset.enabled !== 'false');
|
|
277
|
+
}
|
|
278
|
+
|
|
206
279
|
function setBadge(headerEl, badge = {}) {
|
|
207
280
|
const actions = headerEl?.querySelector?.('.codex-vscode-head-actions');
|
|
208
281
|
if (!actions) {
|
|
@@ -254,7 +327,8 @@
|
|
|
254
327
|
|
|
255
328
|
return new Promise(resolve => {
|
|
256
329
|
let settled = false;
|
|
257
|
-
const
|
|
330
|
+
const returnFocus = doc.activeElement;
|
|
331
|
+
const overlay = doc.createElement('dialog');
|
|
258
332
|
overlay.className = 'codex-plugin-confirm';
|
|
259
333
|
overlay.setAttribute('data-plugin-confirm', 'true');
|
|
260
334
|
overlay.setAttribute('role', 'dialog');
|
|
@@ -298,13 +372,16 @@
|
|
|
298
372
|
card.append(head, body, actions);
|
|
299
373
|
overlay.append(card);
|
|
300
374
|
panel.append(overlay);
|
|
375
|
+
overlay.showModal?.();
|
|
301
376
|
|
|
302
377
|
const cleanup = value => {
|
|
303
378
|
if (settled) return;
|
|
304
379
|
settled = true;
|
|
305
380
|
activeResolve = null;
|
|
306
381
|
doc.removeEventListener('keydown', onKeydown, true);
|
|
382
|
+
overlay.close?.();
|
|
307
383
|
overlay.remove();
|
|
384
|
+
returnFocus?.focus?.();
|
|
308
385
|
resolve(value);
|
|
309
386
|
};
|
|
310
387
|
activeResolve = cleanup;
|
|
@@ -325,6 +402,7 @@
|
|
|
325
402
|
overlay.addEventListener('click', event => {
|
|
326
403
|
if (event.target === overlay) cleanup(false);
|
|
327
404
|
});
|
|
405
|
+
overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(false); });
|
|
328
406
|
cancel.addEventListener('click', () => cleanup(false));
|
|
329
407
|
confirm.addEventListener('click', () => cleanup(true));
|
|
330
408
|
doc.addEventListener('keydown', onKeydown, true);
|
|
@@ -340,6 +418,10 @@
|
|
|
340
418
|
target.removeEventListener?.(type, listener, options);
|
|
341
419
|
}
|
|
342
420
|
instance.panelEl?.remove?.();
|
|
421
|
+
instance.launcherEl?.remove?.();
|
|
422
|
+
if (instance.storageChangeListener) {
|
|
423
|
+
instance.chromeApi?.storage?.onChanged?.removeListener?.(instance.storageChangeListener);
|
|
424
|
+
}
|
|
343
425
|
instance.document.documentElement.classList.remove(ROOT_CLASS, RESIZE_CLASS, COMPACT_CLASS);
|
|
344
426
|
}
|
|
345
427
|
|
|
@@ -347,6 +429,8 @@
|
|
|
347
429
|
create,
|
|
348
430
|
createConfirmController,
|
|
349
431
|
setVisible,
|
|
432
|
+
setLauncherVisible,
|
|
433
|
+
isLauncherVisible,
|
|
350
434
|
setBadge,
|
|
351
435
|
setWidth
|
|
352
436
|
};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
getSettingsPanelInstance: options.getSettingsPanelInstance || (() => null),
|
|
15
15
|
getSelectedModel: options.getSelectedModel || (() => ''),
|
|
16
16
|
getSelectedProviderId: options.getSelectedProviderId || (() => 'builtin'),
|
|
17
|
+
hasCurrentProject: options.hasCurrentProject || (() => true),
|
|
17
18
|
setSelectedProviderId: options.setSelectedProviderId || (() => {}),
|
|
18
19
|
confirmProviderSwitch: options.confirmProviderSwitch || (() => true),
|
|
19
20
|
clearSelectedModel: options.clearSelectedModel || (() => {}),
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
tx: instance.tx,
|
|
44
45
|
ProviderProfiles: Profiles,
|
|
45
46
|
callbacks: {
|
|
47
|
+
hasCurrentProject: instance.hasCurrentProject,
|
|
46
48
|
getCurrentProjectProviderId: () => instance.getSelectedProviderId() || 'builtin',
|
|
47
49
|
onTest: context => testProvider(instance, context),
|
|
48
50
|
onCancelTest: () => cancelProviderTest(instance),
|
|
@@ -208,6 +210,7 @@
|
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
async function prepareProviderActivation(instance, providerId) {
|
|
213
|
+
requireCurrentProject(instance);
|
|
211
214
|
const previousProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
212
215
|
const provider = instance.Profiles.getProviderById(instance.catalog, providerId);
|
|
213
216
|
if (providerId !== previousProviderId) {
|
|
@@ -301,6 +304,7 @@
|
|
|
301
304
|
async function saveProvider(instance, context, action = {}) {
|
|
302
305
|
instance.dialog.setBusy('saving', instance.tx('Saving provider…', '正在保存模型服务…'));
|
|
303
306
|
try {
|
|
307
|
+
if (action.activate === true) requireCurrentProject(instance);
|
|
304
308
|
const result = await request(instance, 'codex.providers.upsert', {
|
|
305
309
|
profileId: context.profileId,
|
|
306
310
|
expectedRevision: context.expectedRevision,
|
|
@@ -495,6 +499,12 @@
|
|
|
495
499
|
}
|
|
496
500
|
|
|
497
501
|
function updateSettingsSummary(instance, override = {}) {
|
|
502
|
+
if (!instance.hasCurrentProject()) {
|
|
503
|
+
instance.getSettingsPanelInstance()?.setProviderSummary?.({ summary: instance.tx(
|
|
504
|
+
'Shared model API profiles · open a project to select a provider',
|
|
505
|
+
'共享模型 API 配置 · 进入项目后选择使用的服务'), tone: 'info' });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
498
508
|
const selectedProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
499
509
|
const catalogProvider = instance.Profiles.getProviderById(
|
|
500
510
|
instance.catalog,
|
|
@@ -594,6 +604,11 @@
|
|
|
594
604
|
});
|
|
595
605
|
}
|
|
596
606
|
|
|
607
|
+
function requireCurrentProject(instance) {
|
|
608
|
+
if (!instance.hasCurrentProject()) throw createClientError(
|
|
609
|
+
'provider_project_required', instance.tx('Open an Overleaf project before selecting its provider.', '请先进入 Overleaf 项目,再选择该项目使用的模型服务。'));
|
|
610
|
+
}
|
|
611
|
+
|
|
597
612
|
function emptyCatalogChange() {
|
|
598
613
|
return { activeProviderChanged: false, activeModelCatalogChanged: false, changedProviderIds: [] };
|
|
599
614
|
}
|
|
@@ -47,12 +47,13 @@
|
|
|
47
47
|
if (instance.root) {
|
|
48
48
|
return instance.root;
|
|
49
49
|
}
|
|
50
|
-
const root = instance.document.createElement('
|
|
50
|
+
const root = instance.document.createElement('dialog');
|
|
51
51
|
root.className = 'codex-provider-dialog-root';
|
|
52
|
+
root.setAttribute('aria-labelledby', 'codex-provider-dialog-title');
|
|
52
53
|
root.hidden = true;
|
|
53
54
|
root.innerHTML = `
|
|
54
55
|
<div class="codex-provider-dialog-backdrop" data-provider-backdrop></div>
|
|
55
|
-
<section class="codex-provider-dialog"
|
|
56
|
+
<section class="codex-provider-dialog">
|
|
56
57
|
<header class="codex-provider-dialog-head">
|
|
57
58
|
<div class="codex-provider-dialog-heading">
|
|
58
59
|
<div class="codex-provider-dialog-titleline">
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
root.addEventListener('input', event => handleInput(instance, event));
|
|
80
81
|
root.addEventListener('change', event => handleInput(instance, event));
|
|
81
82
|
root.addEventListener('keydown', event => handleKeydown(instance, event));
|
|
83
|
+
root.addEventListener('cancel', event => { event.preventDefault(); requestClose(instance); });
|
|
82
84
|
root.addEventListener('mousedown', event => event.stopPropagation());
|
|
83
85
|
root.addEventListener('click', event => event.stopPropagation());
|
|
84
86
|
return root;
|
|
@@ -89,9 +91,12 @@
|
|
|
89
91
|
instance.root.hidden = false;
|
|
90
92
|
syncTheme(instance);
|
|
91
93
|
setCatalog(instance, catalog || instance.catalog);
|
|
94
|
+
if (!instance.root.open) instance.root.showModal?.();
|
|
92
95
|
queueMicrotask(() => {
|
|
93
|
-
instance.root.
|
|
94
|
-
|
|
96
|
+
if (!instance.root || instance.root.hidden) return;
|
|
97
|
+
const target = instance.root.querySelector('[data-provider-row][aria-current="true"]')
|
|
98
|
+
|| instance.root.querySelector('input, button, select, textarea');
|
|
99
|
+
target?.focus?.();
|
|
95
100
|
});
|
|
96
101
|
}
|
|
97
102
|
|
|
@@ -106,6 +111,7 @@
|
|
|
106
111
|
instance.callbacks.onCancelTest?.();
|
|
107
112
|
instance.busy = '';
|
|
108
113
|
}
|
|
114
|
+
instance.root.close?.();
|
|
109
115
|
instance.root.hidden = true;
|
|
110
116
|
instance.busy = '';
|
|
111
117
|
instance.returnFocus?.focus?.();
|
|
@@ -159,6 +165,9 @@
|
|
|
159
165
|
"Configure experimental third-party integrations used by this project's future Codex runs. Compatibility varies by provider and gateway.",
|
|
160
166
|
'配置当前项目后续 Codex 任务使用的实验性第三方集成;兼容性取决于具体模型服务与网关。'
|
|
161
167
|
);
|
|
168
|
+
if (instance.callbacks.hasCurrentProject?.() === false) instance.root.querySelector('[data-provider-dialog-subtitle]').textContent = tx(
|
|
169
|
+
'Manage shared model API profiles. Open a project to choose which provider its future turns use.',
|
|
170
|
+
'管理共享模型 API 配置。进入项目后,再选择该项目后续任务使用的服务。');
|
|
162
171
|
renderProviderList(instance);
|
|
163
172
|
renderDetail(instance);
|
|
164
173
|
renderFooter(instance);
|
|
@@ -408,8 +417,8 @@
|
|
|
408
417
|
<label class="codex-provider-disclosure">
|
|
409
418
|
<input type="checkbox" data-provider-disclosure ${disclosureSatisfied ? 'checked' : ''}>
|
|
410
419
|
<span data-provider-disclosure-text>${escapeHtml(tx(
|
|
411
|
-
`
|
|
412
|
-
|
|
420
|
+
`Projects using this shared provider may send task context to ${draft.baseUrl || 'this endpoint'}. Each project chooses its own provider.`,
|
|
421
|
+
`使用此共享服务配置的项目可能把任务上下文发送到 ${draft.baseUrl || '此端点'}。各项目独立选择使用的服务。`
|
|
413
422
|
))}</span>
|
|
414
423
|
</label>
|
|
415
424
|
<div class="codex-provider-test-row">
|
|
@@ -456,7 +465,7 @@
|
|
|
456
465
|
'Uses the authentication, model catalog, and provider configuration managed by the local Codex CLI.',
|
|
457
466
|
'使用本地 Codex CLI 管理的身份验证、模型目录和服务配置。'
|
|
458
467
|
))}</p>
|
|
459
|
-
${active
|
|
468
|
+
${instance.callbacks.hasCurrentProject?.() === false ? '' : active
|
|
460
469
|
? `<span class="codex-provider-active-badge">${escapeHtml(tx('Current project', '当前项目'))}</span>`
|
|
461
470
|
: `<button type="button" class="codex-provider-primary-button" data-provider-action="activate-builtin">${escapeHtml(tx('Use for this project', '用于当前项目'))}</button>`}
|
|
462
471
|
</div>
|
|
@@ -464,6 +473,7 @@
|
|
|
464
473
|
}
|
|
465
474
|
|
|
466
475
|
function getCurrentProjectProviderId(instance) {
|
|
476
|
+
if (instance.callbacks.hasCurrentProject?.() === false) return '';
|
|
467
477
|
const providerId = instance.callbacks.getCurrentProjectProviderId?.();
|
|
468
478
|
return typeof providerId === 'string' && providerId.trim()
|
|
469
479
|
? providerId.trim()
|
|
@@ -487,6 +497,7 @@
|
|
|
487
497
|
dirty: instance.dirty,
|
|
488
498
|
destinationChanged,
|
|
489
499
|
canSave: canSaveCurrentDraft(instance),
|
|
500
|
+
hasProject: instance.callbacks.hasCurrentProject?.() !== false,
|
|
490
501
|
canActivate: canActivateCurrentProvider(instance)
|
|
491
502
|
});
|
|
492
503
|
const saveDisabled = actionState.saveEnabled ? '' : ' disabled aria-disabled="true"';
|
|
@@ -508,8 +519,8 @@
|
|
|
508
519
|
const dirty = options.dirty === true;
|
|
509
520
|
return {
|
|
510
521
|
showSave: isNew || dirty,
|
|
511
|
-
showSaveAndUse: (isNew || dirty) && (!active || options.destinationChanged === true),
|
|
512
|
-
showUse: !isNew && !active && !dirty,
|
|
522
|
+
showSaveAndUse: options.hasProject !== false && (isNew || dirty) && (!active || options.destinationChanged === true),
|
|
523
|
+
showUse: options.hasProject !== false && !isNew && !active && !dirty,
|
|
513
524
|
saveEnabled: options.canSave === true,
|
|
514
525
|
useEnabled: options.canActivate === true
|
|
515
526
|
};
|
|
@@ -662,7 +673,7 @@
|
|
|
662
673
|
return;
|
|
663
674
|
}
|
|
664
675
|
const focusable = Array.from(instance.root.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'))
|
|
665
|
-
.filter(element => !element.closest('[hidden]'));
|
|
676
|
+
.filter(element => !element.closest('[hidden]') && (!element.getClientRects || element.getClientRects().length));
|
|
666
677
|
if (!focusable.length) {
|
|
667
678
|
return;
|
|
668
679
|
}
|
|
@@ -941,8 +952,8 @@
|
|
|
941
952
|
if (!text) return;
|
|
942
953
|
const baseUrl = getCurrentBaseUrl(instance) || instance.tx('this endpoint', '此端点');
|
|
943
954
|
text.textContent = instance.tx(
|
|
944
|
-
`
|
|
945
|
-
|
|
955
|
+
`Projects using this shared provider may send task context to ${baseUrl}. Each project chooses its own provider.`,
|
|
956
|
+
`使用此共享服务配置的项目可能把任务上下文发送到 ${baseUrl}。各项目独立选择使用的服务。`
|
|
946
957
|
);
|
|
947
958
|
}
|
|
948
959
|
|
|
@@ -984,10 +995,7 @@
|
|
|
984
995
|
if (!panel) {
|
|
985
996
|
return;
|
|
986
997
|
}
|
|
987
|
-
const style = instance.document.defaultView.getComputedStyle(panel);
|
|
988
998
|
instance.root.dataset.theme = panel.dataset.theme === 'light' ? 'light' : 'dark';
|
|
989
|
-
instance.root.style.setProperty('--provider-panel-bg', style.backgroundColor || '#151716');
|
|
990
|
-
instance.root.style.setProperty('--provider-panel-color', style.color || '#eceeea');
|
|
991
999
|
}
|
|
992
1000
|
|
|
993
1001
|
function escapeHtml(value) {
|
|
@@ -1011,6 +1019,7 @@
|
|
|
1011
1019
|
}
|
|
1012
1020
|
|
|
1013
1021
|
function destroy(instance) {
|
|
1022
|
+
instance.root?.close?.();
|
|
1014
1023
|
instance.root?.remove?.();
|
|
1015
1024
|
instance.root = null;
|
|
1016
1025
|
}
|
|
@@ -231,11 +231,46 @@
|
|
|
231
231
|
return String(content || '').trim().length > 0;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
function isRetryReplacementEligible(run = {}, projection = {}) {
|
|
235
|
+
if (!run.id || !['failed', 'interrupted'].includes(run.status)) return false;
|
|
236
|
+
if (run.trackedChangeStatus || run.undoStatus) return false;
|
|
237
|
+
if (['undoOperations', 'undoTrackedChanges', 'undoExpectedFiles', 'undoBaseFiles', 'appliedOperations']
|
|
238
|
+
.some(field => Array.isArray(run[field]) && run[field].length)) return false;
|
|
239
|
+
if (projection.canAccept || projection.canUndo || projection.changedDocument === true) return false;
|
|
240
|
+
if (projection.failureTerminalState === 'needs_review' || projection.terminalState === 'needs_review') return false;
|
|
241
|
+
return projection.changedDocument === false || run.mode === 'ask' || hasZeroWriteReport(run.events);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function hasZeroWriteReport(events = []) {
|
|
245
|
+
return (Array.isArray(events) ? events : []).some(event => {
|
|
246
|
+
const rows = Array.isArray(event?.detailStructured?.meta) ? event.detailStructured.meta : [];
|
|
247
|
+
return rows.some(row => row?.key === 'writeResult'
|
|
248
|
+
&& /(?:wrote|写入)\s*0\b[\s\S]*(?:skipped|跳过)\s*0\b/i.test(String(row.value || '')));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveRetryReplacement({ candidate, activeSessionId, runs, projectRunSettlement } = {}) {
|
|
253
|
+
if (!candidate?.runId || candidate.sessionId !== activeSessionId) return '';
|
|
254
|
+
const source = (Array.isArray(runs) ? runs : []).find(run => run?.id === candidate.runId);
|
|
255
|
+
if (!source) return '';
|
|
256
|
+
const projection = typeof projectRunSettlement === 'function' ? projectRunSettlement(source) : {};
|
|
257
|
+
return isRetryReplacementEligible(source, projection) ? source.id : '';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function replaceRunForRetry(runs, sourceRunId, nextRun, maxRuns = 20) {
|
|
261
|
+
const retained = (Array.isArray(runs) ? runs : [])
|
|
262
|
+
.filter(run => !sourceRunId || run?.id !== sourceRunId);
|
|
263
|
+
return [...retained, nextRun].slice(-Math.max(1, maxRuns));
|
|
264
|
+
}
|
|
265
|
+
|
|
234
266
|
return {
|
|
235
267
|
buildCodexRunParams,
|
|
236
268
|
canUseFocusedPartialSnapshot,
|
|
237
269
|
shouldRestrictWritebackToFocus,
|
|
238
270
|
buildSessionHistoryResult,
|
|
271
|
+
isRetryReplacementEligible,
|
|
272
|
+
replaceRunForRetry,
|
|
273
|
+
resolveRetryReplacement,
|
|
239
274
|
truncateSessionHistoryText,
|
|
240
275
|
normalizeComposerAttachments,
|
|
241
276
|
normalizeSkillInvocation
|
|
@@ -251,10 +251,12 @@
|
|
|
251
251
|
for (const selector of ['[data-governance-readonly-patterns]', '[data-governance-writable-patterns]', '[data-sensitive-check-enabled]', '[data-sensitive-confirm-allowed]', '[data-preload-project-context]', '[data-load-codex-local-skills]', '[data-load-codex-overleaf-skills]', '[data-theme-select]', '[data-language-select]']) {
|
|
252
252
|
const element = container.querySelector(selector);
|
|
253
253
|
element?.addEventListener?.('change', event => {
|
|
254
|
-
instance.callbacks.onInputChange?.(event)
|
|
255
|
-
|
|
254
|
+
Promise.resolve(instance.callbacks.onInputChange?.(event))
|
|
255
|
+
.then(() => flashSaved(instance, event)).catch(() => {});
|
|
256
|
+
});
|
|
257
|
+
element?.addEventListener?.('input', event => {
|
|
258
|
+
Promise.resolve(instance.callbacks.onInputChange?.(event)).catch(() => {});
|
|
256
259
|
});
|
|
257
|
-
element?.addEventListener?.('input', event => instance.callbacks.onInputChange?.(event));
|
|
258
260
|
}
|
|
259
261
|
// Guardrails: live, non-blocking analysis of the protection globs.
|
|
260
262
|
for (const selector of ['[data-governance-readonly-patterns]', '[data-governance-writable-patterns]']) {
|