codex-overleaf-link 2.2.1 → 2.3.1
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 +25 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +5 -71
- package/extension/src/background.js +67 -63
- package/extension/src/backgroundUpdateCoordinator.js +128 -66
- package/extension/src/content/applyResultFormatters.js +2 -3
- package/extension/src/content/composerPanel.js +0 -2
- package/extension/src/content/contentRuntime.js +757 -1558
- package/extension/src/content/contextTray.js +4 -6
- package/extension/src/content/diagnosticsController.js +1 -2
- package/extension/src/content/diffReviewPanel.js +14 -13
- package/extension/src/content/generated/content.bundle.js +41605 -0
- package/extension/src/content/generated/content.bundle.meta.json +103 -0
- package/extension/src/content/legacyGlobalRegistry.js +129 -0
- package/extension/src/content/markdownText.js +17 -17
- package/extension/src/content/mathText.js +155 -9
- package/extension/src/content/modelPicker.js +1 -1
- package/extension/src/content/modelPickerSupport.js +10 -5
- package/extension/src/content/moduleRegistryKernel.js +125 -0
- package/extension/src/content/nativeCompatibilityController.js +313 -0
- package/extension/src/content/otWarmMirror.js +8 -6
- package/extension/src/content/pageBridgeClient.js +224 -0
- package/extension/src/content/panelMaintenance.js +8 -3
- package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
- package/extension/src/content/projectProviderSelection.js +4 -12
- package/extension/src/content/projectSessionCleanup.js +3 -3
- package/extension/src/content/providerSettingsCoordinator.js +59 -26
- package/extension/src/content/providerSettingsDialog.js +30 -18
- package/extension/src/content/recentProjects.js +15 -23
- package/extension/src/content/runGuidanceController.js +1 -1
- package/extension/src/content/runQueueScheduler.js +129 -29
- package/extension/src/content/runSettlementPersistence.js +51 -0
- package/extension/src/content/runTimelineView.js +17 -8
- package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
- package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
- package/extension/src/content/scopedPersistencePanelState.js +111 -0
- package/extension/src/content/sessionManager.js +15 -12
- package/extension/src/content/sessionPersistence.js +67 -8
- package/extension/src/content/updateNotice.js +10 -4
- package/extension/src/content/writebackOrchestrator.js +89 -68
- package/extension/src/contentScript.js +5 -1
- package/extension/src/page/trackedChangesLifecycle.js +16 -5
- package/extension/src/page/writebackRouter.js +6 -2
- package/extension/src/pageBridge.js +47 -98
- package/extension/src/shared/agentTranscript.js +18 -8
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/governanceRules.js +1 -1
- package/extension/src/shared/i18n.js +2 -4
- package/extension/src/shared/managedUpdateProjection.js +337 -0
- package/extension/src/shared/nativeRequestIdentity.js +60 -0
- package/extension/src/shared/otText.js +0 -19
- package/extension/src/shared/pageRpcContract.js +136 -0
- package/extension/src/shared/runExecutionSnapshot.js +187 -0
- package/extension/src/shared/runExecutionSnapshotCodec.js +171 -0
- package/extension/src/shared/runInputQueue.js +81 -34
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
- package/extension/src/shared/scopedPersistenceQueuePolicy.js +200 -0
- package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
- package/extension/src/shared/scopedPersistenceTransaction.js +241 -0
- package/extension/src/shared/sessionState.js +110 -59
- package/extension/src/shared/settlementFacts.js +351 -0
- package/extension/src/shared/storageDb.js +25 -10
- package/extension/src/shared/storageMigration.js +275 -61
- package/extension/src/shared/storageSessionClaims.js +60 -0
- package/extension/src/shared/updateConsent.js +4 -0
- package/extension/src/shared/updateRevocationIntent.js +109 -0
- package/extension/src/shared/writebackEvidenceProjection.js +150 -0
- package/extension/src/shared/writebackOperationEvidence.js +153 -0
- package/extension/src/shared/writebackSettlement.js +727 -0
- package/extension/styles/panel.css +18 -8
- package/native-host/src/codexPromptAssembly.js +1 -1
- package/native-host/src/codexRuntimeIdentity.js +1 -1
- package/native-host/src/codexSessionRunner.js +7 -13
- package/native-host/src/index.js +3 -69
- package/native-host/src/managedInstall.js +50 -8
- package/native-host/src/nativeTransportEnvelope.js +148 -0
- package/native-host/src/taskRunnerRuntime.js +14 -84
- package/native-host/src/updateManager.js +36 -1
- package/package.json +13 -5
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
(function initCodexOverleafPageBridgeClient(root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.CodexOverleafPageBridgeClient = api;
|
|
5
|
+
})(typeof window !== 'undefined' ? window : globalThis, function pageBridgeClientFactory() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const PAGE_WORLD_SCRIPTS = Object.freeze([
|
|
9
|
+
['src/shared/reviewing.js', 'codex-overleaf-reviewing-script'],
|
|
10
|
+
['src/shared/projectFiles.js', 'codex-overleaf-project-files-script'],
|
|
11
|
+
['src/shared/compatibility.js', 'codex-overleaf-compatibility-page-script'],
|
|
12
|
+
['src/shared/pageRpcContract.js', 'codex-overleaf-page-rpc-contract-script'],
|
|
13
|
+
['src/shared/staleGuard.js', 'codex-overleaf-stale-guard-script'],
|
|
14
|
+
['src/shared/compileAdapter.js', 'codex-overleaf-compile-adapter-script'],
|
|
15
|
+
['src/shared/sensitiveScan.js', 'codex-overleaf-sensitive-scan-script'],
|
|
16
|
+
['src/shared/auditRecords.js', 'codex-overleaf-audit-records-script'],
|
|
17
|
+
['src/page/saveState.js', 'codex-overleaf-save-state-script', true],
|
|
18
|
+
['src/page/overleafCapabilities.js', 'codex-overleaf-capabilities-script'],
|
|
19
|
+
['src/page/compileBridge.js', 'codex-overleaf-compile-bridge-script'],
|
|
20
|
+
['src/page/overleafEditor.js', 'codex-overleaf-editor-script'],
|
|
21
|
+
['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
|
|
22
|
+
['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
|
|
23
|
+
['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
|
|
24
|
+
['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
|
|
25
|
+
['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
|
|
26
|
+
['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
|
|
27
|
+
['src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', true],
|
|
28
|
+
['src/pageBridge.js', 'codex-overleaf-page-bridge-script', true]
|
|
29
|
+
]);
|
|
30
|
+
const OPTIONAL_OT_SCRIPTS = Object.freeze([
|
|
31
|
+
['src/shared/otText.js', 'codex-overleaf-ot-text-script'],
|
|
32
|
+
['src/page/overleafRealtimeObserver.js', 'codex-overleaf-realtime-observer-script']
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function create(options = {}) {
|
|
36
|
+
const windowRef = options.window || globalThis.window;
|
|
37
|
+
const documentRef = options.document || windowRef?.document;
|
|
38
|
+
const chromeApi = options.chromeApi || globalThis.chrome;
|
|
39
|
+
const cryptoImpl = options.crypto || globalThis.crypto;
|
|
40
|
+
const runtimeRoot = options.root || windowRef;
|
|
41
|
+
const Contract = options.contract;
|
|
42
|
+
const Compatibility = options.compatibility;
|
|
43
|
+
const timeoutOverrides = { ...(options.timeoutOverrides || {}) };
|
|
44
|
+
const revision = Contract.REVISION;
|
|
45
|
+
const capability = createCapability(cryptoImpl);
|
|
46
|
+
const activeCancellationHandlers = new Map();
|
|
47
|
+
let readyPromise = null;
|
|
48
|
+
|
|
49
|
+
function start() {
|
|
50
|
+
if (!readyPromise) readyPromise = injectPageBridge();
|
|
51
|
+
return readyPromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function call(method, params) {
|
|
55
|
+
try {
|
|
56
|
+
await start();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { ok: false, error: `Page bridge unavailable: ${error.message}` };
|
|
59
|
+
}
|
|
60
|
+
return send(method, params, Contract.resolveDispatchPolicy(method, timeoutOverrides));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function send(method, params, policy = {}) {
|
|
64
|
+
const id = cryptoImpl.randomUUID();
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const timeoutMs = Number.isFinite(Number(policy.timeoutMs))
|
|
67
|
+
? Number(policy.timeoutMs)
|
|
68
|
+
: Contract.resolveTimeoutMs(method, timeoutOverrides);
|
|
69
|
+
const cancellable = policy.cancellation === 'content_abort';
|
|
70
|
+
let settled = false;
|
|
71
|
+
let timeout = null;
|
|
72
|
+
function cleanup() {
|
|
73
|
+
if (timeout !== null) windowRef.clearTimeout(timeout);
|
|
74
|
+
windowRef.removeEventListener('message', onMessage);
|
|
75
|
+
if (cancellable) activeCancellationHandlers.delete(id);
|
|
76
|
+
}
|
|
77
|
+
function resolveOnce(value) {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
cleanup();
|
|
81
|
+
resolve(value);
|
|
82
|
+
}
|
|
83
|
+
function rejectOnce(error) {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
cleanup();
|
|
87
|
+
reject(error);
|
|
88
|
+
}
|
|
89
|
+
function cancelRequest() {
|
|
90
|
+
const error = new Error('Codex run was cancelled by the user');
|
|
91
|
+
error.code = 'codex_cancelled';
|
|
92
|
+
error.cancelled = true;
|
|
93
|
+
rejectOnce(error);
|
|
94
|
+
}
|
|
95
|
+
timeout = windowRef.setTimeout(() => {
|
|
96
|
+
resolveOnce({ ok: false, error: 'Page bridge timed out' });
|
|
97
|
+
}, timeoutMs);
|
|
98
|
+
function onMessage(event) {
|
|
99
|
+
if (event.source !== windowRef
|
|
100
|
+
|| event.origin !== windowRef.location.origin
|
|
101
|
+
|| event.data?.source !== 'codex-overleaf/page'
|
|
102
|
+
|| event.data?.pageBridgeVersion !== Compatibility?.BUILD_TARGET_VERSION
|
|
103
|
+
|| event.data?.pageBridgeRevision !== revision
|
|
104
|
+
|| event.data.id !== id) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
resolveOnce(event.data.result);
|
|
108
|
+
}
|
|
109
|
+
if (cancellable) {
|
|
110
|
+
activeCancellationHandlers.set(id, cancelRequest);
|
|
111
|
+
if (options.isCancellationRequested?.()) windowRef.queueMicrotask(cancelRequest);
|
|
112
|
+
}
|
|
113
|
+
windowRef.addEventListener('message', onMessage);
|
|
114
|
+
// Every low-level request remains one attempt. Higher layers must opt
|
|
115
|
+
// into replay so a document mutation is never duplicated implicitly.
|
|
116
|
+
windowRef.postMessage({
|
|
117
|
+
source: 'codex-overleaf/content',
|
|
118
|
+
id,
|
|
119
|
+
method,
|
|
120
|
+
params,
|
|
121
|
+
capability
|
|
122
|
+
}, windowRef.location.origin);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cancelActiveRequests() {
|
|
127
|
+
const handlers = Array.from(activeCancellationHandlers.values());
|
|
128
|
+
activeCancellationHandlers.clear();
|
|
129
|
+
handlers.forEach(cancel => cancel());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveTimeoutMs(method) {
|
|
133
|
+
return Contract.resolveTimeoutMs(method, timeoutOverrides);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function injectPageBridge() {
|
|
137
|
+
const splitAt = PAGE_WORLD_SCRIPTS.findIndex(([src]) =>
|
|
138
|
+
src === 'src/page/pageBridgeCapability.js'
|
|
139
|
+
);
|
|
140
|
+
for (const [src, id, force] of PAGE_WORLD_SCRIPTS.slice(0, splitAt)) {
|
|
141
|
+
await injectScriptOnce(src, id, { force });
|
|
142
|
+
}
|
|
143
|
+
await injectOptionalOtDependencies();
|
|
144
|
+
for (const [src, id, force] of PAGE_WORLD_SCRIPTS.slice(splitAt)) {
|
|
145
|
+
await injectScriptOnce(src, id, { force });
|
|
146
|
+
}
|
|
147
|
+
await initializeCapability();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function injectOptionalOtDependencies() {
|
|
151
|
+
try {
|
|
152
|
+
for (const [src, id] of OPTIONAL_OT_SCRIPTS) await injectScriptOnce(src, id);
|
|
153
|
+
} catch (_error) {
|
|
154
|
+
// The page bridge keeps its read-only unavailable fallback.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function injectScriptOnce(src, id, injectOptions = {}) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const existing = documentRef.getElementById(id);
|
|
161
|
+
if (existing && injectOptions.force !== true) {
|
|
162
|
+
resolve();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
existing?.remove?.();
|
|
166
|
+
const script = documentRef.createElement('script');
|
|
167
|
+
const timeout = windowRef.setTimeout(() => {
|
|
168
|
+
cleanup();
|
|
169
|
+
reject(new Error(`Timed out loading ${src}`));
|
|
170
|
+
}, 8000);
|
|
171
|
+
function cleanup() {
|
|
172
|
+
windowRef.clearTimeout(timeout);
|
|
173
|
+
script.onload = null;
|
|
174
|
+
script.onerror = null;
|
|
175
|
+
script.remove();
|
|
176
|
+
}
|
|
177
|
+
script.id = id;
|
|
178
|
+
const runtimePrefix = String(runtimeRoot.__CODEX_OVERLEAF_RUNTIME_PREFIX__ || '');
|
|
179
|
+
script.src = chromeApi.runtime.getURL(runtimePrefix + src);
|
|
180
|
+
script.onload = () => {
|
|
181
|
+
cleanup();
|
|
182
|
+
resolve();
|
|
183
|
+
};
|
|
184
|
+
script.onerror = () => {
|
|
185
|
+
cleanup();
|
|
186
|
+
reject(new Error(`Failed to load ${src}`));
|
|
187
|
+
};
|
|
188
|
+
(documentRef.head || documentRef.documentElement).append(script);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function initializeCapability() {
|
|
193
|
+
const result = await send(
|
|
194
|
+
'initializeCapability',
|
|
195
|
+
{},
|
|
196
|
+
Contract.resolveDispatchPolicy('initializeCapability', timeoutOverrides)
|
|
197
|
+
);
|
|
198
|
+
if (!result?.ok) {
|
|
199
|
+
throw new Error(result?.error || result?.reason || 'Page bridge capability initialization failed');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
call,
|
|
205
|
+
cancelActiveRequests,
|
|
206
|
+
resolveTimeoutMs,
|
|
207
|
+
send,
|
|
208
|
+
start
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createCapability(cryptoImpl) {
|
|
213
|
+
if (cryptoImpl?.randomUUID) return cryptoImpl.randomUUID();
|
|
214
|
+
const bytes = new Uint8Array(24);
|
|
215
|
+
cryptoImpl.getRandomValues(bytes);
|
|
216
|
+
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Object.freeze({
|
|
220
|
+
OPTIONAL_OT_SCRIPTS,
|
|
221
|
+
PAGE_WORLD_SCRIPTS,
|
|
222
|
+
create
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
setState,
|
|
30
30
|
getCurrentRunView,
|
|
31
31
|
getSettingsPanelInstance,
|
|
32
|
+
StorageDb
|
|
32
33
|
} = deps;
|
|
33
34
|
|
|
34
35
|
// --- Change history (v1.7.5): the audit log finally gets a read path. ---
|
|
@@ -48,7 +49,6 @@
|
|
|
48
49
|
list.textContent = tx('Loading\u2026', '正在加载…');
|
|
49
50
|
let records = [];
|
|
50
51
|
try {
|
|
51
|
-
const StorageDb = window.CodexOverleafStorageDb;
|
|
52
52
|
records = StorageDb?.getAllByIndex
|
|
53
53
|
? await StorageDb.getAllByIndex('auditLogs', 'projectId', getCurrentProjectId())
|
|
54
54
|
: [];
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
try {
|
|
174
|
-
await
|
|
174
|
+
await StorageDb?.clearAllStores?.();
|
|
175
175
|
// v1.8.1: the dashboard's project-name cache lives in
|
|
176
176
|
// chrome.storage.local, outside the IndexedDB stores — clear it too so
|
|
177
177
|
// no (potentially sensitive) project titles outlive the wipe.
|
|
@@ -244,7 +244,12 @@
|
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
246
|
try {
|
|
247
|
-
await callPageBridge('jumpToPosition', {
|
|
247
|
+
await callPageBridge('jumpToPosition', {
|
|
248
|
+
path: target,
|
|
249
|
+
from: 0,
|
|
250
|
+
to: 0,
|
|
251
|
+
runProjectId: getCurrentProjectId()
|
|
252
|
+
});
|
|
248
253
|
} catch (error) {
|
|
249
254
|
showPluginToast(tx(`Could not open ${target}: ${error.message}`, `无法打开 ${target}:${error.message}`));
|
|
250
255
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
(function initPostNavigationSettlementPersistence(root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
4
|
+
if (root) root.CodexOverleafPostNavigationSettlementPersistence = api;
|
|
5
|
+
})(typeof window !== 'undefined' ? window : globalThis, function postNavigationSettlementPersistenceFactory() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
async function persist(options = {}) {
|
|
9
|
+
if (options.__insideScopedTransaction !== true) {
|
|
10
|
+
const accountScopeId = String(options.accountScopeId || '').trim();
|
|
11
|
+
const projectId = String(options.projectId || '').trim();
|
|
12
|
+
if (!accountScopeId) {
|
|
13
|
+
return { ok: false, reason: 'account_scope_unavailable' };
|
|
14
|
+
}
|
|
15
|
+
if (!projectId) {
|
|
16
|
+
return { ok: false, reason: 'project_scope_unavailable' };
|
|
17
|
+
}
|
|
18
|
+
if (!options.PersistenceCoordinator
|
|
19
|
+
|| typeof options.PersistenceCoordinator.commitDetached !== 'function') {
|
|
20
|
+
return { ok: false, reason: 'scoped_persistence_unavailable' };
|
|
21
|
+
}
|
|
22
|
+
const committed = await options.PersistenceCoordinator.commitDetached(
|
|
23
|
+
projectId,
|
|
24
|
+
accountScopeId,
|
|
25
|
+
{ rebase: true },
|
|
26
|
+
() => persist({
|
|
27
|
+
...options,
|
|
28
|
+
__insideScopedTransaction: true
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
if (!committed || typeof committed !== 'object') {
|
|
32
|
+
return { ok: false, reason: 'scoped_persistence_invalid_result' };
|
|
33
|
+
}
|
|
34
|
+
if (committed.ok !== true) return committed;
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(committed, 'value')) return committed;
|
|
36
|
+
return committed.value && typeof committed.value === 'object'
|
|
37
|
+
? committed.value
|
|
38
|
+
: { ok: false, reason: 'scoped_persistence_action_result_unavailable' };
|
|
39
|
+
}
|
|
40
|
+
const {
|
|
41
|
+
StorageDb,
|
|
42
|
+
WritebackSettlement,
|
|
43
|
+
sessionId,
|
|
44
|
+
runId,
|
|
45
|
+
status,
|
|
46
|
+
statusText,
|
|
47
|
+
finishedAt,
|
|
48
|
+
settlementResult
|
|
49
|
+
} = options;
|
|
50
|
+
if (!StorageDb || !WritebackSettlement || !sessionId || !runId || !status) {
|
|
51
|
+
return { ok: false, reason: 'invalid_post_navigation_settlement' };
|
|
52
|
+
}
|
|
53
|
+
const record = await StorageDb.getRecord('sessions', sessionId);
|
|
54
|
+
if (!record) return { ok: false, reason: 'session_not_found' };
|
|
55
|
+
if (String(record.accountScopeId || '').trim() !== String(options.accountScopeId || '').trim()) {
|
|
56
|
+
return { ok: false, reason: 'account_scope_mismatch' };
|
|
57
|
+
}
|
|
58
|
+
if (String(record.projectId || '').trim() !== String(options.projectId || '').trim()) {
|
|
59
|
+
return { ok: false, reason: 'project_scope_mismatch' };
|
|
60
|
+
}
|
|
61
|
+
const runs = Array.isArray(record.runs) ? [...record.runs] : [];
|
|
62
|
+
const runIndex = runs.findIndex(run => run && run.id === runId);
|
|
63
|
+
if (runIndex < 0) return { ok: false, reason: 'run_not_found' };
|
|
64
|
+
|
|
65
|
+
let transitioned = settlementResult
|
|
66
|
+
? WritebackSettlement.applySettlementTransition(runs[runIndex], settlementResult)
|
|
67
|
+
: { ...runs[runIndex] };
|
|
68
|
+
transitioned = WritebackSettlement.applySettlementTransition(
|
|
69
|
+
transitioned,
|
|
70
|
+
WritebackSettlement.transitionPostNavigationStatus({
|
|
71
|
+
status,
|
|
72
|
+
statusText,
|
|
73
|
+
finishedAt: finishedAt || transitioned.finishedAt || new Date().toISOString()
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
runs[runIndex] = transitioned;
|
|
77
|
+
|
|
78
|
+
const now = options.now || new Date().toISOString();
|
|
79
|
+
record.runs = runs;
|
|
80
|
+
record.lastActivityAt = now;
|
|
81
|
+
record.updatedAt = now;
|
|
82
|
+
await StorageDb.putRecord('sessions', record);
|
|
83
|
+
return { ok: true, run: transitioned };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function persistRequired(options = {}) {
|
|
87
|
+
const result = await persist(options);
|
|
88
|
+
if (result?.ok === true) return result;
|
|
89
|
+
const reason = String(result?.reason || 'unknown_failure');
|
|
90
|
+
throw Object.assign(
|
|
91
|
+
new Error(`Post-navigation settlement persistence failed: ${reason}`),
|
|
92
|
+
{
|
|
93
|
+
code: 'post_navigation_settlement_persistence_failed',
|
|
94
|
+
reason
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Object.freeze({ persist, persistRequired });
|
|
100
|
+
});
|
|
@@ -30,24 +30,16 @@
|
|
|
30
30
|
const nextSpeedTier = deps.readSelectedSpeed?.() || state.speedTier;
|
|
31
31
|
const runSelection = deps.getRunSelection?.(nextProviderId);
|
|
32
32
|
const nextProviderRevision = Number(runSelection?.providerRevision || 0);
|
|
33
|
+
const providerConfigurationChanged = nextProviderId === (state.providerId || 'builtin')
|
|
34
|
+
&& nextProviderRevision !== Number(state.providerRevision || 0);
|
|
35
|
+
const resetProviderThread = providerChanged || providerConfigurationChanged;
|
|
33
36
|
const sessions = (state.sessions || []).map(session => ({
|
|
34
37
|
...session,
|
|
35
38
|
providerId: nextProviderId,
|
|
36
39
|
model: nextModel,
|
|
37
40
|
reasoningEffort: nextReasoningEffort,
|
|
38
41
|
speedTier: nextSpeedTier,
|
|
39
|
-
codexThreadId:
|
|
40
|
-
pendingInputs: (session.pendingInputs || []).map(item => ({
|
|
41
|
-
...item,
|
|
42
|
-
payload: {
|
|
43
|
-
...(item.payload || {}),
|
|
44
|
-
providerId: nextProviderId,
|
|
45
|
-
providerRevision: nextProviderRevision,
|
|
46
|
-
model: nextModel,
|
|
47
|
-
reasoningEffort: nextReasoningEffort,
|
|
48
|
-
speedTier: nextSpeedTier
|
|
49
|
-
}
|
|
50
|
-
}))
|
|
42
|
+
codexThreadId: resetProviderThread ? '' : session.codexThreadId
|
|
51
43
|
}));
|
|
52
44
|
deps.setState?.(deps.normalizeState({
|
|
53
45
|
...state,
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
async function clearProjectSessions(projectId, projectName) {
|
|
26
|
-
const StorageDb =
|
|
27
|
-
const SessionState =
|
|
26
|
+
const StorageDb = deps.StorageDb;
|
|
27
|
+
const SessionState = deps.SessionState;
|
|
28
28
|
if (!StorageDb || !SessionState) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
if (!approved) {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
const Migration =
|
|
52
|
+
const Migration = deps.StorageMigration;
|
|
53
53
|
if (Migration?.addSessionTombstones) {
|
|
54
54
|
await Migration.addSessionTombstones(projectId, records.map(record => record.id));
|
|
55
55
|
}
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
function create(options = {}) {
|
|
5
|
-
const Profiles =
|
|
5
|
+
const Profiles = options.ProviderProfiles;
|
|
6
|
+
const Dialog = options.ProviderSettingsDialog;
|
|
7
|
+
if (!Profiles || !Dialog?.create) {
|
|
8
|
+
throw new Error('Provider settings require explicit profile and dialog dependencies.');
|
|
9
|
+
}
|
|
6
10
|
const instance = {
|
|
11
|
+
Profiles,
|
|
7
12
|
tx: options.tx || ((english) => english),
|
|
8
13
|
sendBackgroundNative: options.sendBackgroundNative,
|
|
9
14
|
getSettingsPanelInstance: options.getSettingsPanelInstance || (() => null),
|
|
@@ -21,12 +26,24 @@
|
|
|
21
26
|
channel: null,
|
|
22
27
|
dialog: null
|
|
23
28
|
};
|
|
24
|
-
|
|
29
|
+
const projectProviderChanged = options.onProviderChanged
|
|
25
30
|
|| ((catalog, change) => reconcileProviderSelection(instance, catalog, change));
|
|
26
|
-
instance.
|
|
31
|
+
instance.onProviderChanged = async (catalog, change) => {
|
|
32
|
+
const previousProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
33
|
+
const result = await projectProviderChanged(catalog, change);
|
|
34
|
+
const nextProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
35
|
+
updateSettingsSummary(instance);
|
|
36
|
+
if (previousProviderId !== nextProviderId && instance.dialog?.isOpen?.()) {
|
|
37
|
+
instance.dialog.setCatalog(instance.catalog, nextProviderId);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
instance.dialog = Dialog.create({
|
|
27
42
|
document: options.document || document,
|
|
28
43
|
tx: instance.tx,
|
|
44
|
+
ProviderProfiles: Profiles,
|
|
29
45
|
callbacks: {
|
|
46
|
+
getCurrentProjectProviderId: () => instance.getSelectedProviderId() || 'builtin',
|
|
30
47
|
onTest: context => testProvider(instance, context),
|
|
31
48
|
onCancelTest: () => cancelProviderTest(instance),
|
|
32
49
|
onSave: (context, action) => saveProvider(instance, context, action),
|
|
@@ -135,7 +152,7 @@
|
|
|
135
152
|
await ensureLoadedWithRetry(instance);
|
|
136
153
|
} catch (_error) {
|
|
137
154
|
return instance.refreshModelOptions(
|
|
138
|
-
|
|
155
|
+
instance.Profiles.buildRunSelection(instance.catalog, instance.getSelectedProviderId())
|
|
139
156
|
);
|
|
140
157
|
}
|
|
141
158
|
return reconcileProviderSelection(instance, instance.catalog, { forceSessionRefresh: true });
|
|
@@ -143,12 +160,18 @@
|
|
|
143
160
|
|
|
144
161
|
async function reconcileProviderSelection(instance, catalog, change = {}) {
|
|
145
162
|
const previousProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
146
|
-
|
|
163
|
+
let requestedProviderId = typeof change.sessionProviderId === 'string' && change.sessionProviderId
|
|
147
164
|
? change.sessionProviderId
|
|
148
165
|
: previousProviderId;
|
|
166
|
+
const requestedProviderAvailable = requestedProviderId === 'builtin'
|
|
167
|
+
|| Boolean(instance.Profiles.getProviderById(catalog, requestedProviderId));
|
|
168
|
+
const selectedProviderWasRemoved = !requestedProviderAvailable;
|
|
169
|
+
if (selectedProviderWasRemoved) {
|
|
170
|
+
requestedProviderId = 'builtin';
|
|
171
|
+
}
|
|
149
172
|
const projectProviderChanged = requestedProviderId !== previousProviderId;
|
|
150
|
-
if (projectProviderChanged && change.providerSwitchApproved !== true) {
|
|
151
|
-
const requestedProvider =
|
|
173
|
+
if (projectProviderChanged && !selectedProviderWasRemoved && change.providerSwitchApproved !== true) {
|
|
174
|
+
const requestedProvider = instance.Profiles.getProviderById(catalog, requestedProviderId);
|
|
152
175
|
const approved = await instance.confirmProviderSwitch({
|
|
153
176
|
providerId: requestedProviderId,
|
|
154
177
|
providerName: requestedProvider?.name || requestedProviderId
|
|
@@ -158,13 +181,14 @@
|
|
|
158
181
|
}
|
|
159
182
|
}
|
|
160
183
|
const changedProviderIds = Array.isArray(change.changedProviderIds) ? change.changedProviderIds : [];
|
|
161
|
-
|
|
184
|
+
const providerConfigurationChanged = changedProviderIds.includes(requestedProviderId);
|
|
185
|
+
if (!change.forceSessionRefresh && !projectProviderChanged && !providerConfigurationChanged) {
|
|
162
186
|
return;
|
|
163
187
|
}
|
|
164
188
|
const loadResult = change.modelsPreloaded === true
|
|
165
189
|
? { stale: false, selectedModel: change.preloadedModelId || '' }
|
|
166
190
|
: await instance.refreshModelOptions(
|
|
167
|
-
|
|
191
|
+
instance.Profiles.buildRunSelection(catalog, requestedProviderId),
|
|
168
192
|
{ persist: false }
|
|
169
193
|
);
|
|
170
194
|
if (loadResult?.stale || loadResult?.error) {
|
|
@@ -175,7 +199,7 @@
|
|
|
175
199
|
error.details = loadResult?.error || {};
|
|
176
200
|
throw error;
|
|
177
201
|
}
|
|
178
|
-
if (projectProviderChanged) {
|
|
202
|
+
if (projectProviderChanged || providerConfigurationChanged) {
|
|
179
203
|
await instance.setSelectedProviderId(requestedProviderId, loadResult?.selectedModel || '');
|
|
180
204
|
}
|
|
181
205
|
await instance.persistInputs();
|
|
@@ -185,7 +209,7 @@
|
|
|
185
209
|
|
|
186
210
|
async function prepareProviderActivation(instance, providerId) {
|
|
187
211
|
const previousProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
188
|
-
const provider =
|
|
212
|
+
const provider = instance.Profiles.getProviderById(instance.catalog, providerId);
|
|
189
213
|
if (providerId !== previousProviderId) {
|
|
190
214
|
const approved = await instance.confirmProviderSwitch({
|
|
191
215
|
providerId,
|
|
@@ -196,13 +220,13 @@
|
|
|
196
220
|
}
|
|
197
221
|
}
|
|
198
222
|
const result = await instance.refreshModelOptions(
|
|
199
|
-
|
|
223
|
+
instance.Profiles.buildRunSelection(instance.catalog, providerId),
|
|
200
224
|
{ persist: false }
|
|
201
225
|
);
|
|
202
226
|
if (result?.stale || result?.error) {
|
|
203
227
|
if (providerId !== previousProviderId) {
|
|
204
228
|
await instance.refreshModelOptions(
|
|
205
|
-
|
|
229
|
+
instance.Profiles.buildRunSelection(instance.catalog, previousProviderId),
|
|
206
230
|
{ persist: false }
|
|
207
231
|
).catch(() => {});
|
|
208
232
|
}
|
|
@@ -293,7 +317,11 @@
|
|
|
293
317
|
selectedId: result.savedProviderId
|
|
294
318
|
});
|
|
295
319
|
await notifyChanged(instance, applied.change, action.activate === true
|
|
296
|
-
? {
|
|
320
|
+
? {
|
|
321
|
+
sessionProviderId: result.savedProviderId,
|
|
322
|
+
providerSwitchApproved: true,
|
|
323
|
+
requireProjection: true
|
|
324
|
+
}
|
|
297
325
|
: {});
|
|
298
326
|
instance.dialog.setBusy('', '');
|
|
299
327
|
const fellBackToBuiltin = previousCatalog.activeProviderId === result.savedProviderId
|
|
@@ -389,7 +417,7 @@
|
|
|
389
417
|
} catch (error) {
|
|
390
418
|
if (preparation?.previousProviderId && preparation.previousProviderId !== context.profileId) {
|
|
391
419
|
await instance.refreshModelOptions(
|
|
392
|
-
|
|
420
|
+
instance.Profiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
|
|
393
421
|
{ persist: false }
|
|
394
422
|
).catch(() => {});
|
|
395
423
|
}
|
|
@@ -423,7 +451,7 @@
|
|
|
423
451
|
} catch (error) {
|
|
424
452
|
if (preparation?.previousProviderId && preparation.previousProviderId !== 'builtin') {
|
|
425
453
|
await instance.refreshModelOptions(
|
|
426
|
-
|
|
454
|
+
instance.Profiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
|
|
427
455
|
{ persist: false }
|
|
428
456
|
).catch(() => {});
|
|
429
457
|
}
|
|
@@ -448,7 +476,7 @@
|
|
|
448
476
|
}
|
|
449
477
|
|
|
450
478
|
function applyCatalog(instance, result, options = {}) {
|
|
451
|
-
const nextCatalog =
|
|
479
|
+
const nextCatalog = instance.Profiles.normalizeCatalog(result);
|
|
452
480
|
if (instance.loaded && nextCatalog.storeRevision < instance.catalog.storeRevision) {
|
|
453
481
|
return { applied: false, change: emptyCatalogChange() };
|
|
454
482
|
}
|
|
@@ -463,19 +491,19 @@
|
|
|
463
491
|
instance.dialog.setCatalog(instance.catalog, options.selectedId);
|
|
464
492
|
}
|
|
465
493
|
}
|
|
466
|
-
return { applied: true, change: describeCatalogChange(previousCatalog, nextCatalog) };
|
|
494
|
+
return { applied: true, change: describeCatalogChange(instance, previousCatalog, nextCatalog) };
|
|
467
495
|
}
|
|
468
496
|
|
|
469
497
|
function updateSettingsSummary(instance, override = {}) {
|
|
470
498
|
const selectedProviderId = instance.getSelectedProviderId() || 'builtin';
|
|
471
|
-
const catalogProvider =
|
|
499
|
+
const catalogProvider = instance.Profiles.getProviderById(
|
|
472
500
|
instance.catalog,
|
|
473
501
|
selectedProviderId
|
|
474
502
|
);
|
|
475
503
|
const selectedProvider = catalogProvider?.id === selectedProviderId
|
|
476
504
|
? catalogProvider
|
|
477
505
|
: (selectedProviderId === 'builtin'
|
|
478
|
-
?
|
|
506
|
+
? instance.Profiles.getActiveProvider(instance.catalog)
|
|
479
507
|
: {
|
|
480
508
|
id: selectedProviderId,
|
|
481
509
|
name: selectedProviderId,
|
|
@@ -528,16 +556,16 @@
|
|
|
528
556
|
}
|
|
529
557
|
}
|
|
530
558
|
|
|
531
|
-
function describeCatalogChange(previous, next) {
|
|
532
|
-
const previousActive =
|
|
533
|
-
const nextActive =
|
|
559
|
+
function describeCatalogChange(instance, previous, next) {
|
|
560
|
+
const previousActive = instance.Profiles.getActiveProvider(previous);
|
|
561
|
+
const nextActive = instance.Profiles.getActiveProvider(next);
|
|
534
562
|
const providerIds = new Set([
|
|
535
563
|
...(previous.providers || []).map(provider => provider.id),
|
|
536
564
|
...(next.providers || []).map(provider => provider.id)
|
|
537
565
|
]);
|
|
538
566
|
const changedProviderIds = Array.from(providerIds).filter(providerId => {
|
|
539
|
-
const previousProvider =
|
|
540
|
-
const nextProvider =
|
|
567
|
+
const previousProvider = instance.Profiles.getProviderById(previous, providerId);
|
|
568
|
+
const nextProvider = instance.Profiles.getProviderById(next, providerId);
|
|
541
569
|
return modelCatalogSignature(previousProvider) !== modelCatalogSignature(nextProvider);
|
|
542
570
|
});
|
|
543
571
|
return {
|
|
@@ -548,8 +576,13 @@
|
|
|
548
576
|
}
|
|
549
577
|
|
|
550
578
|
function modelCatalogSignature(provider = {}) {
|
|
579
|
+
provider = provider || {};
|
|
551
580
|
return JSON.stringify({
|
|
552
581
|
id: provider.id || '',
|
|
582
|
+
revision: Number(provider.revision || 0),
|
|
583
|
+
baseUrl: provider.baseUrl || '',
|
|
584
|
+
wireApiPreference: provider.wireApiPreference || '',
|
|
585
|
+
resolvedWireApi: provider.resolvedWireApi || '',
|
|
553
586
|
defaultModelId: provider.defaultModelId || '',
|
|
554
587
|
reasoningAdapter: provider.reasoningAdapter || '',
|
|
555
588
|
reasoningCapability: provider.reasoningCapability || '',
|
|
@@ -567,7 +600,7 @@
|
|
|
567
600
|
|
|
568
601
|
function getProviderSnapshot(instance, providerId) {
|
|
569
602
|
const requestedId = providerId || instance.getSelectedProviderId() || 'builtin';
|
|
570
|
-
const provider =
|
|
603
|
+
const provider = instance.Profiles.getProviderById(instance.catalog, requestedId);
|
|
571
604
|
if (!provider) {
|
|
572
605
|
return {
|
|
573
606
|
providerId: requestedId,
|