codex-overleaf-link 2.6.0 → 2.6.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 +9 -9
- package/README.zh-CN.md +9 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +70 -0
- package/extension/src/content/composerAttachments.js +35 -8
- package/extension/src/content/contentRuntime.js +200 -51
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +129 -42
- package/extension/src/content/generated/content.bundle.js +1283 -331
- package/extension/src/content/generated/content.bundle.meta.json +3 -3
- package/extension/src/content/localSkillsPanel.js +4 -2
- package/extension/src/content/markdownText.js +11 -9
- package/extension/src/content/modelPicker.js +28 -19
- package/extension/src/content/nativeCompatibilityController.js +23 -9
- package/extension/src/content/pageBridgeClient.js +5 -5
- package/extension/src/content/panelMaintenance.js +4 -0
- package/extension/src/content/panelRenderer.js +4 -1
- package/extension/src/content/providerSettingsDialog.js +26 -18
- package/extension/src/content/recentProjects.js +26 -16
- package/extension/src/content/runQueueScheduler.js +19 -9
- package/extension/src/content/runResultActions.js +51 -4
- package/extension/src/content/runReviewController.js +9 -3
- package/extension/src/content/scopedPersistencePanelState.js +34 -25
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sharedSessionViewSync.js +9 -1
- package/extension/src/content/writebackOrchestrator.js +266 -50
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +152 -40
- package/extension/src/pageBridge.js +17 -7
- package/extension/src/shared/agentTranscript.js +23 -2
- package/extension/src/shared/auditRecords.js +1 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/composerAttachmentDraft.js +22 -7
- package/extension/src/shared/failureReasons.js +15 -0
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/staleGuard.js +58 -0
- package/extension/src/shared/storageMigration.js +104 -1
- package/extension/styles/panel.css +191 -63
- package/native-host/src/codexSessionRunner.js +1 -0
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +11 -3
- package/native-host/src/updateManager.js +24 -6
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +1 -1
|
@@ -136,6 +136,11 @@
|
|
|
136
136
|
fallbackUserMessage: 'The file changed while Codex was working.',
|
|
137
137
|
fallbackNextAction: 'Review the current file, then rerun the task.'
|
|
138
138
|
},
|
|
139
|
+
document_identity_mismatch: {
|
|
140
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
141
|
+
fallbackUserMessage: 'Codex could not confirm that the editor content belongs to the target file, so it was not used.',
|
|
142
|
+
fallbackNextAction: 'Wait for the target file to open and settle, then retry. Reload Overleaf if this continues.'
|
|
143
|
+
},
|
|
139
144
|
patch_anchor_not_found: {
|
|
140
145
|
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
141
146
|
fallbackUserMessage: 'The edit anchor no longer matches current Overleaf content.',
|
|
@@ -358,6 +363,11 @@
|
|
|
358
363
|
fallbackUserMessage: 'Native host request failed.',
|
|
359
364
|
fallbackNextAction: 'Open diagnostics and retry.'
|
|
360
365
|
},
|
|
366
|
+
native_request_timeout: {
|
|
367
|
+
stage: 'native', severity: 'error', defaultRetryable: true,
|
|
368
|
+
fallbackUserMessage: 'The local service did not respond.',
|
|
369
|
+
fallbackNextAction: 'Retry, or open Diagnostics to check the local connection.'
|
|
370
|
+
},
|
|
361
371
|
native_protocol_incompatible: {
|
|
362
372
|
stage: 'native', severity: 'blocked', defaultRetryable: true,
|
|
363
373
|
fallbackUserMessage: 'Native host version or capabilities do not match the extension.',
|
|
@@ -428,6 +438,11 @@
|
|
|
428
438
|
fallbackUserMessage: 'The provider rate limit was reached.',
|
|
429
439
|
fallbackNextAction: 'Wait for the rate-limit window to recover and retry.'
|
|
430
440
|
},
|
|
441
|
+
provider_quota_exceeded: {
|
|
442
|
+
stage: 'codex', severity: 'error', defaultRetryable: true,
|
|
443
|
+
fallbackUserMessage: 'The model provider quota or billing limit was reached.',
|
|
444
|
+
fallbackNextAction: 'Check the provider account and plan, or switch model or provider before retrying.'
|
|
445
|
+
},
|
|
431
446
|
provider_connection_timeout: {
|
|
432
447
|
stage: 'codex', severity: 'error', defaultRetryable: true,
|
|
433
448
|
fallbackUserMessage: 'The provider connection timed out.',
|
|
@@ -151,6 +151,8 @@
|
|
|
151
151
|
failureReason_provider_request_rejected_next: "Review the model ID, reasoning controls, tool support, and request overrides.",
|
|
152
152
|
failureReason_provider_rate_limited_user: "The provider rate limit was reached.",
|
|
153
153
|
failureReason_provider_rate_limited_next: "Wait for the rate-limit window to recover and retry.",
|
|
154
|
+
failureReason_provider_quota_exceeded_user: "The model provider quota or billing limit was reached.",
|
|
155
|
+
failureReason_provider_quota_exceeded_next: "Check the provider account and plan, or switch model or provider before retrying.",
|
|
154
156
|
failureReason_provider_connection_timeout_user: "The provider connection timed out.",
|
|
155
157
|
failureReason_provider_connection_timeout_next: "Check the endpoint and network, then retry.",
|
|
156
158
|
failureReason_provider_dns_failed_user: "The provider hostname could not be resolved.",
|
|
@@ -639,6 +641,7 @@
|
|
|
639
641
|
queuedInputResume: 'Resume',
|
|
640
642
|
queuedInputRemove: 'Remove',
|
|
641
643
|
queuedInputAttachmentsUnsupported: 'Queued follow-ups currently support text and @context. Remove binary attachments before queuing.',
|
|
644
|
+
attachmentsLoading: 'Attachments are still loading',
|
|
642
645
|
queuedInputError_empty_input: 'Enter a follow-up first.',
|
|
643
646
|
queuedInputError_input_too_long: 'This follow-up is too long to queue.',
|
|
644
647
|
queuedInputError_queue_full: 'The follow-up queue is full.',
|
|
@@ -759,6 +762,8 @@
|
|
|
759
762
|
failureReason_accept_replay_failed_next: 'Inspect the file in Overleaf, then retry.',
|
|
760
763
|
failureReason_native_request_failed_user: 'The request to the local Codex bridge failed.',
|
|
761
764
|
failureReason_native_request_failed_next: 'Open Diagnostics to check the bridge, then retry.',
|
|
765
|
+
failureReason_native_request_timeout_user: 'The local service did not respond.',
|
|
766
|
+
failureReason_native_request_timeout_next: 'Retry, or open Diagnostics to check the local connection.',
|
|
762
767
|
failureReason_panel_reload_interrupted_user: 'This run was interrupted by a page refresh.',
|
|
763
768
|
failureReason_panel_reload_interrupted_next: 'Edit and resend the task to continue.',
|
|
764
769
|
failureReason_sensitive_content_blocked_user: 'Sensitive content was found before sending, so Codex did not receive this request.',
|
|
@@ -785,6 +790,8 @@
|
|
|
785
790
|
failureReason_target_editor_not_ready_next: 'Wait for Overleaf to finish loading, then retry.',
|
|
786
791
|
failureReason_stale_source_changed_user: '{file} changed while Codex was working, so Codex did not overwrite it.',
|
|
787
792
|
failureReason_stale_source_changed_next: 'Review the current file, then rerun the task.',
|
|
793
|
+
failureReason_document_identity_mismatch_user: 'Codex could not confirm that the editor content belongs to the target file, so it was not used.',
|
|
794
|
+
failureReason_document_identity_mismatch_next: 'Wait for the target file to open and settle, then retry. Reload Overleaf if this continues.',
|
|
788
795
|
failureReason_patch_anchor_not_found_user: 'The edit anchor for {file} no longer matches current Overleaf content.',
|
|
789
796
|
failureReason_patch_anchor_not_found_next: 'Rerun the task against the current document.',
|
|
790
797
|
failureReason_write_observed_mismatch_user: 'Codex attempted to write {file}, but the content read back from Overleaf did not match the approved change.',
|
|
@@ -851,6 +858,7 @@
|
|
|
851
858
|
recentProjects_previewUnavailableHint: 'Saved conversation could not be loaded.',
|
|
852
859
|
recentProjects_conversationGone: 'This conversation is no longer available. Choose another conversation from the refreshed list.',
|
|
853
860
|
recentProjects_continueFailed: 'Could not open the conversation. Please try again.',
|
|
861
|
+
recentProjects_renameFailed: 'Could not rename the conversation. Your title is still available to edit or copy.',
|
|
854
862
|
recentProjects_welcome_subtitle: 'Conversations saved in this browser',
|
|
855
863
|
recentProjects_empty: 'No conversations saved in this browser yet. Open an Overleaf project and start a Codex conversation.',
|
|
856
864
|
recentProjects_sessions_empty: 'No saved conversations in this project',
|
|
@@ -1035,6 +1043,8 @@
|
|
|
1035
1043
|
failureReason_provider_request_rejected_next: "请检查模型标识、推理设置、工具支持及自定义请求参数。",
|
|
1036
1044
|
failureReason_provider_rate_limited_user: "模型服务已达到请求频率限制。",
|
|
1037
1045
|
failureReason_provider_rate_limited_next: "请等待限流窗口恢复后重试。",
|
|
1046
|
+
failureReason_provider_quota_exceeded_user: "模型服务的配额或账单限额已达到。",
|
|
1047
|
+
failureReason_provider_quota_exceeded_next: "请检查模型服务账户和套餐,或切换模型或模型服务后重试。",
|
|
1038
1048
|
failureReason_provider_connection_timeout_user: "模型服务连接超时。",
|
|
1039
1049
|
failureReason_provider_connection_timeout_next: "请检查服务端点和网络,然后重试。",
|
|
1040
1050
|
failureReason_provider_dns_failed_user: "无法解析模型服务的主机名。",
|
|
@@ -1523,6 +1533,7 @@
|
|
|
1523
1533
|
queuedInputResume: '继续',
|
|
1524
1534
|
queuedInputRemove: '删除',
|
|
1525
1535
|
queuedInputAttachmentsUnsupported: '当前排队消息支持文本和 @context;请先移除二进制附件。',
|
|
1536
|
+
attachmentsLoading: '附件读取中',
|
|
1526
1537
|
queuedInputError_empty_input: '请先输入后续任务。',
|
|
1527
1538
|
queuedInputError_input_too_long: '这条后续任务太长,无法排队。',
|
|
1528
1539
|
queuedInputError_queue_full: '后续任务队列已满。',
|
|
@@ -1642,6 +1653,8 @@
|
|
|
1642
1653
|
failureReason_accept_replay_failed_next: '请在 Overleaf 中检查该文件后重试。',
|
|
1643
1654
|
failureReason_native_request_failed_user: '向本地 Codex bridge 的请求失败。',
|
|
1644
1655
|
failureReason_native_request_failed_next: '打开「诊断」检查 bridge 后重试。',
|
|
1656
|
+
failureReason_native_request_timeout_user: '本机服务没有响应。',
|
|
1657
|
+
failureReason_native_request_timeout_next: '请重试,或打开「诊断」检查本机连接。',
|
|
1645
1658
|
failureReason_panel_reload_interrupted_user: '页面刷新导致本轮中断。',
|
|
1646
1659
|
failureReason_panel_reload_interrupted_next: '可编辑后重发任务以继续。',
|
|
1647
1660
|
failureReason_sensitive_content_blocked_user: '发送前发现敏感内容,Codex 没有收到这次请求。',
|
|
@@ -1668,6 +1681,8 @@
|
|
|
1668
1681
|
failureReason_target_editor_not_ready_next: '请等 Overleaf 加载完成后再重试。',
|
|
1669
1682
|
failureReason_stale_source_changed_user: '{file} 在任务执行期间被修改过,Codex 没有覆盖它。',
|
|
1670
1683
|
failureReason_stale_source_changed_next: '请先查看当前文件内容,再重新运行本轮任务。',
|
|
1684
|
+
failureReason_document_identity_mismatch_user: 'Codex 无法确认编辑器内容属于目标文件,因此没有使用这些内容。',
|
|
1685
|
+
failureReason_document_identity_mismatch_next: '请等目标文件打开并稳定后重试;如果问题持续,请重新加载 Overleaf。',
|
|
1671
1686
|
failureReason_patch_anchor_not_found_user: '{file} 的修改锚点已经无法和当前 Overleaf 内容对齐。',
|
|
1672
1687
|
failureReason_patch_anchor_not_found_next: '请基于当前文档重新运行任务。',
|
|
1673
1688
|
failureReason_write_observed_mismatch_user: 'Codex 尝试写入 {file},但写入后从 Overleaf 读回的内容与批准的改动不一致。',
|
|
@@ -1734,6 +1749,7 @@
|
|
|
1734
1749
|
recentProjects_previewUnavailableHint: '已保存的对话暂时无法读取。',
|
|
1735
1750
|
recentProjects_conversationGone: '该对话已不可用,请从更新后的列表中选择其他对话。',
|
|
1736
1751
|
recentProjects_continueFailed: '暂时无法打开对话,请重试。',
|
|
1752
|
+
recentProjects_renameFailed: '暂时无法重命名对话,输入的标题仍保留,可继续编辑或复制。',
|
|
1737
1753
|
recentProjects_welcome_subtitle: '当前浏览器保存的对话',
|
|
1738
1754
|
recentProjects_empty: '当前浏览器还没有保存的对话。打开一个 Overleaf 项目,与 Codex 开始对话即可。',
|
|
1739
1755
|
recentProjects_sessions_empty: '该项目还没有保存的对话',
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this), function () {
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
var REVISION = '2026-10-
|
|
8
|
+
var REVISION = '2026-10-29-identity-v54';
|
|
9
9
|
var METHODS = freezeCatalog({
|
|
10
10
|
initializeCapability: entry('bridge.initialize', 'control', 'none', 'default', false, 'none'),
|
|
11
11
|
probe: entry('bridge.probe', 'read', 'none', 'default', false, 'safe'),
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
14
|
var META_PREFIX = 'codex-overleaf-scoped-persistence-v1:';
|
|
15
|
+
var MIRROR_FENCE_PREFIX = 'codex-overleaf-mirror-reuse-fence-v1:';
|
|
15
16
|
var fallbackMeta = new Map();
|
|
16
17
|
var cleanPart = QueuePolicy.cleanPart;
|
|
17
18
|
var normalizeMeta = QueuePolicy.normalizeMeta;
|
|
@@ -74,7 +75,40 @@
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
async function getMirrorReuseFences(scope, chromeApi) {
|
|
79
|
+
chromeApi = chromeApi || (typeof chrome !== 'undefined' ? chrome : null);
|
|
80
|
+
if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local) throw new Error('Mirror fence storage is unavailable.');
|
|
81
|
+
var prefix = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ':';
|
|
82
|
+
var stored = await chromeApi.storage.local.get(null);
|
|
83
|
+
return Object.keys(stored || {}).filter(key => key.startsWith(prefix)).map(key => {
|
|
84
|
+
var generation = key.slice(prefix.length);
|
|
85
|
+
if (!/^[a-zA-Z0-9_-]{1,160}$/.test(generation) || stored[key]?.v !== 1 || stored[key].generation !== generation) {
|
|
86
|
+
throw new Error('Mirror fence storage contains an unreadable generation.');
|
|
87
|
+
}
|
|
88
|
+
return generation;
|
|
89
|
+
}).sort();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function setMirrorReuseFence(scope, generation, chromeApi) {
|
|
93
|
+
chromeApi = chromeApi || (typeof chrome !== 'undefined' ? chrome : null);
|
|
94
|
+
if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local
|
|
95
|
+
|| !/^[a-zA-Z0-9_-]{1,160}$/.test(generation)) throw new Error('Mirror fence storage is unavailable.');
|
|
96
|
+
var key = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ':' + generation;
|
|
97
|
+
await chromeApi.storage.local.set({ [key]: { v: 1, generation } });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function clearMirrorReuseFences(scope, generations, chromeApi) {
|
|
101
|
+
chromeApi = chromeApi || (typeof chrome !== 'undefined' ? chrome : null);
|
|
102
|
+
if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local || !Array.isArray(generations)
|
|
103
|
+
|| generations.some(generation => !/^[a-zA-Z0-9_-]{1,160}$/.test(generation))) throw new Error('Mirror fence storage is unavailable.');
|
|
104
|
+
var prefix = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ':';
|
|
105
|
+
await chromeApi.storage.local.remove(generations.map(generation => prefix + generation));
|
|
106
|
+
}
|
|
107
|
+
|
|
77
108
|
return Object.freeze({
|
|
109
|
+
getMirrorReuseFences: getMirrorReuseFences,
|
|
110
|
+
setMirrorReuseFence: setMirrorReuseFence,
|
|
111
|
+
clearMirrorReuseFences: clearMirrorReuseFences,
|
|
78
112
|
createBrowserAdapter: createBrowserAdapter,
|
|
79
113
|
normalizeScope: normalizeScope,
|
|
80
114
|
scopeKey: scopeKey
|
|
@@ -7,6 +7,63 @@
|
|
|
7
7
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function staleGuardFactory() {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
const DOCUMENT_PROVENANCE_OTHER_MIN = 0.9;
|
|
11
|
+
const DOCUMENT_PROVENANCE_CLAIMED_MAX = 0.5;
|
|
12
|
+
const DOCUMENT_PROVENANCE_EDGE_LIMIT = 64 * 1024;
|
|
13
|
+
|
|
14
|
+
function assessDocumentProvenance(input = {}) {
|
|
15
|
+
const { path, projectId, documentIdentity } = input || {};
|
|
16
|
+
if (documentIdentity !== undefined) {
|
|
17
|
+
const valid = documentIdentity && typeof documentIdentity === 'object' && !Array.isArray(documentIdentity)
|
|
18
|
+
&& documentIdentity.version === 1 && documentIdentity.source === 'cm-ranges-v1'
|
|
19
|
+
&& typeof projectId === 'string' && projectId.length > 0 && documentIdentity.projectId === projectId
|
|
20
|
+
&& typeof path === 'string' && path.length > 0 && documentIdentity.path === path
|
|
21
|
+
&& typeof documentIdentity.documentId === 'string' && typeof documentIdentity.targetDocumentId === 'string'
|
|
22
|
+
&& /^[a-f0-9]{24}$/.test(documentIdentity.documentId)
|
|
23
|
+
&& /^[a-f0-9]{24}$/.test(documentIdentity.targetDocumentId)
|
|
24
|
+
&& documentIdentity.documentId === documentIdentity.targetDocumentId;
|
|
25
|
+
return valid ? { ok: true, identityState: 'match' }
|
|
26
|
+
: { ok: false, code: 'document_identity_mismatch', identityState: 'mismatch' };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const { path, text, references } = input || {};
|
|
30
|
+
if (typeof text !== 'string' || !references) return { ok: true, identityState: 'fallback' };
|
|
31
|
+
const entries = references instanceof Map ? references : Object.entries(references);
|
|
32
|
+
const claimed = references instanceof Map ? references.get(path)
|
|
33
|
+
: Object.prototype.hasOwnProperty.call(references, path) ? references[path] : undefined;
|
|
34
|
+
if (text === claimed) return { ok: true, identityState: 'fallback' };
|
|
35
|
+
let matchedPath, bestScore = -1;
|
|
36
|
+
for (const [otherPath, reference] of entries) {
|
|
37
|
+
if (otherPath === path || typeof reference !== 'string') continue;
|
|
38
|
+
if (text === reference) return { ok: false, code: 'document_identity_mismatch', identityState: 'fallback', matchedPath: otherPath };
|
|
39
|
+
const score = documentSimilarity(text, reference);
|
|
40
|
+
if (score > bestScore) { bestScore = score; matchedPath = otherPath; }
|
|
41
|
+
}
|
|
42
|
+
if (bestScore >= DOCUMENT_PROVENANCE_OTHER_MIN
|
|
43
|
+
&& (typeof claimed !== 'string' || documentSimilarity(text, claimed) < DOCUMENT_PROVENANCE_CLAIMED_MAX)) {
|
|
44
|
+
return { ok: false, code: 'document_identity_mismatch', identityState: 'fallback', matchedPath };
|
|
45
|
+
}
|
|
46
|
+
} catch (_error) {
|
|
47
|
+
// Unavailable references supply no contradictory evidence.
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, identityState: 'fallback' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Shared prefix + non-overlapping suffix / longer length. For large files,
|
|
53
|
+
// sample at most 64 Ki UTF-16 units at each edge and cap the denominator at
|
|
54
|
+
// twice that limit. Exact equality above still compares the complete text.
|
|
55
|
+
// No token arrays or quadratic diff: total work is O(total reference text),
|
|
56
|
+
// with bounded per-reference similarity work and constant auxiliary memory.
|
|
57
|
+
function documentSimilarity(left, right) {
|
|
58
|
+
const length = Math.min(left.length, right.length);
|
|
59
|
+
let prefix = 0, suffix = 0;
|
|
60
|
+
while (prefix < Math.min(length, DOCUMENT_PROVENANCE_EDGE_LIMIT) && left[prefix] === right[prefix]) prefix++;
|
|
61
|
+
while (suffix < Math.min(length - prefix, DOCUMENT_PROVENANCE_EDGE_LIMIT)
|
|
62
|
+
&& left[left.length - suffix - 1] === right[right.length - suffix - 1]) suffix++;
|
|
63
|
+
const denominator = Math.min(Math.max(left.length, right.length), 2 * DOCUMENT_PROVENANCE_EDGE_LIMIT);
|
|
64
|
+
return denominator ? (prefix + suffix) / denominator : 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
10
67
|
function buildBaseFileLookup(files) {
|
|
11
68
|
if (!Array.isArray(files) || files.length === 0) {
|
|
12
69
|
return null;
|
|
@@ -262,6 +319,7 @@
|
|
|
262
319
|
}
|
|
263
320
|
|
|
264
321
|
return {
|
|
322
|
+
assessDocumentProvenance,
|
|
265
323
|
buildBaseFileLookup,
|
|
266
324
|
checkOperationFreshness,
|
|
267
325
|
prepareOperationPatches,
|
|
@@ -23,6 +23,101 @@
|
|
|
23
23
|
'selectedLocalSkillIdsByProject'
|
|
24
24
|
];
|
|
25
25
|
var projectPreferenceLockTails = {};
|
|
26
|
+
var projectPreferenceBaselines = new Map();
|
|
27
|
+
var projectPreferenceIntents = new Map();
|
|
28
|
+
var projectPreferenceBaselineVersions = new WeakMap();
|
|
29
|
+
|
|
30
|
+
function getProjectPreferenceBaseline(accountScopeId, projectId) {
|
|
31
|
+
var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
|
|
32
|
+
var baseline = copyPreferenceValue(projectPreferenceBaselines.get(key));
|
|
33
|
+
var intents = projectPreferenceIntents.get(key);
|
|
34
|
+
if (baseline && intents) projectPreferenceBaselineVersions.set(baseline, {
|
|
35
|
+
owner: intents, versions: Object.assign({}, intents.acknowledged)
|
|
36
|
+
});
|
|
37
|
+
return baseline;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function adoptProjectPreferenceBaseline(prefs, accountScopeId, projectId) {
|
|
41
|
+
var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
|
|
42
|
+
if (key) {
|
|
43
|
+
projectPreferenceBaselines.set(key, copyPreferenceValue(prefs));
|
|
44
|
+
projectPreferenceIntents.set(key, { revision: 0, fields: {}, acknowledged: {} });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function captureProjectPreferenceWrite(input) {
|
|
49
|
+
var projectId = input.projectId;
|
|
50
|
+
var key = buildScopedProjectPreferenceKey(input.accountScopeId, projectId);
|
|
51
|
+
if (!key) return null;
|
|
52
|
+
var intents = projectPreferenceIntents.get(key);
|
|
53
|
+
if (!intents) {
|
|
54
|
+
intents = { revision: 0, fields: {}, acknowledged: {} };
|
|
55
|
+
projectPreferenceIntents.set(key, intents);
|
|
56
|
+
}
|
|
57
|
+
function values(prefs) {
|
|
58
|
+
return {
|
|
59
|
+
experimentalOtByProject: input.normalizeExperimentalOtByProject(prefs.experimentalOtByProject)[projectId] === true,
|
|
60
|
+
reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject?.[projectId],
|
|
61
|
+
governanceRulesByProject: input.normalizeGovernanceRulesByProject({
|
|
62
|
+
[projectId]: prefs.governanceRulesByProject?.[projectId] || {}
|
|
63
|
+
})[projectId],
|
|
64
|
+
customInstructionsByProject: input.normalizeCustomInstructionsByProject({
|
|
65
|
+
[projectId]: prefs.customInstructionsByProject?.[projectId] || ''
|
|
66
|
+
})[projectId]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var baseline = input.baseline || getProjectPreferenceBaseline(input.accountScopeId, projectId);
|
|
70
|
+
var captured = projectPreferenceBaselineVersions.get(baseline);
|
|
71
|
+
var versions = captured?.owner === intents ? captured.versions : {};
|
|
72
|
+
var previous = values(baseline || {});
|
|
73
|
+
var current = values(input.state);
|
|
74
|
+
var token = { key: key, owner: intents, projectId: projectId, fields: {}, versions: {}, written: [] };
|
|
75
|
+
for (var name of Object.keys(current)) {
|
|
76
|
+
var latest = intents.fields[name];
|
|
77
|
+
// A pending or later local intent supersedes the older captured read.
|
|
78
|
+
// Peer adoption alone does not turn an unchanged queued snapshot into an edit.
|
|
79
|
+
var reference = latest && latest.revision > (versions[name] || 0) ? latest.value : previous[name];
|
|
80
|
+
if (JSON.stringify(current[name]) !== JSON.stringify(reference)) {
|
|
81
|
+
latest = { revision: ++intents.revision, value: copyPreferenceValue(current[name]), dirty: true };
|
|
82
|
+
intents.fields[name] = latest;
|
|
83
|
+
}
|
|
84
|
+
token.fields[name] = latest && JSON.stringify(current[name]) === JSON.stringify(latest.value) ? latest : null;
|
|
85
|
+
token.versions[name] = latest?.revision || 0;
|
|
86
|
+
}
|
|
87
|
+
return token;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function projectPreferencePatch(token, accountScopeId, projectId) {
|
|
91
|
+
if (token.key !== buildScopedProjectPreferenceKey(accountScopeId, projectId)) {
|
|
92
|
+
throw Object.assign(new Error('Scoped project preferences require account and project identity.'), {
|
|
93
|
+
code: 'account_scope_unavailable'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
var current = projectPreferenceIntents.get(token.key) === token.owner;
|
|
97
|
+
var patch = {};
|
|
98
|
+
token.written = [];
|
|
99
|
+
for (var name of Object.keys(token.fields)) {
|
|
100
|
+
var intent = token.fields[name];
|
|
101
|
+
var changed = current && intent?.dirty && token.owner.fields[name] === intent;
|
|
102
|
+
patch[name] = changed ? { [token.projectId]: copyPreferenceValue(intent.value) } : {};
|
|
103
|
+
if (changed) token.written.push(name);
|
|
104
|
+
}
|
|
105
|
+
return patch;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function completeProjectPreferenceWrite(token, prefs) {
|
|
109
|
+
if (projectPreferenceIntents.get(token.key) !== token.owner) return [];
|
|
110
|
+
for (var name of token.written) {
|
|
111
|
+
var intent = token.fields[name];
|
|
112
|
+
intent.dirty = false;
|
|
113
|
+
token.owner.acknowledged[name] = Math.max(token.owner.acknowledged[name] || 0, intent.revision);
|
|
114
|
+
}
|
|
115
|
+
projectPreferenceBaselines.set(token.key, copyPreferenceValue(prefs));
|
|
116
|
+
return Object.keys(token.fields).filter(function (name) {
|
|
117
|
+
var latest = token.owner.fields[name];
|
|
118
|
+
return (latest?.revision || 0) === token.versions[name] && !latest?.dirty;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
26
121
|
|
|
27
122
|
function buildScopedProjectPreferenceKey(accountScopeId, projectId) {
|
|
28
123
|
var account = compactScopePart(accountScopeId);
|
|
@@ -170,6 +265,7 @@
|
|
|
170
265
|
var visible = await StorageDb.claimSessionsForAccount(projectId, account, deleted);
|
|
171
266
|
function result(currentPrefs, sessions, migrated) {
|
|
172
267
|
var wanted = currentPrefs.activeSessionByProject?.[projectId] || '';
|
|
268
|
+
adoptProjectPreferenceBaseline(currentPrefs, account, projectId);
|
|
173
269
|
return { prefs: currentPrefs, sessions: sessions,
|
|
174
270
|
activeSessionId: sessions.some(session => session.id === wanted) ? wanted : sessions.at(-1)?.id || '',
|
|
175
271
|
migrated: migrated };
|
|
@@ -260,7 +356,9 @@
|
|
|
260
356
|
return chrome.storage.local.get([PREFS_KEY]).then(function (stored) {
|
|
261
357
|
var claimed = claimLegacyProjectPreferences(stored[PREFS_KEY] || {}, account, project);
|
|
262
358
|
var next = mergeScopedProjectPreferences(claimed.prefs, prefs, account, project);
|
|
263
|
-
return chrome.storage.local.set({ [PREFS_KEY]: next })
|
|
359
|
+
return chrome.storage.local.set({ [PREFS_KEY]: next }).then(function () {
|
|
360
|
+
return buildProjectPreferenceView(next, account, project);
|
|
361
|
+
});
|
|
264
362
|
});
|
|
265
363
|
});
|
|
266
364
|
}
|
|
@@ -464,6 +562,11 @@
|
|
|
464
562
|
runMigrationIfNeeded: runMigrationIfNeeded,
|
|
465
563
|
savePrefs: savePrefs,
|
|
466
564
|
loadPrefs: loadPrefs,
|
|
565
|
+
getProjectPreferenceBaseline: getProjectPreferenceBaseline,
|
|
566
|
+
adoptProjectPreferenceBaseline: adoptProjectPreferenceBaseline,
|
|
567
|
+
captureProjectPreferenceWrite: captureProjectPreferenceWrite,
|
|
568
|
+
projectPreferencePatch: projectPreferencePatch,
|
|
569
|
+
completeProjectPreferenceWrite: completeProjectPreferenceWrite,
|
|
467
570
|
loadSessionTombstones: loadSessionTombstones,
|
|
468
571
|
addSessionTombstones: addSessionTombstones,
|
|
469
572
|
getDeletedSessionIds: getDeletedSessionIds,
|