codex-overleaf-link 1.8.5 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +45 -21
  2. package/bin/codex-overleaf-link.mjs +58 -0
  3. package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
  4. package/extension/assets/icons/codex-overleaf-icon.png +0 -0
  5. package/extension/assets/icons/icon128.png +0 -0
  6. package/extension/assets/icons/icon16.png +0 -0
  7. package/extension/assets/icons/icon32.png +0 -0
  8. package/extension/assets/icons/icon48.png +0 -0
  9. package/extension/bootstrap/background.js +404 -0
  10. package/extension/bootstrap/manifest.template.json +51 -0
  11. package/extension/bootstrap/popup.css +26 -0
  12. package/extension/bootstrap/popup.html +38 -0
  13. package/extension/bootstrap/popup.js +44 -0
  14. package/extension/bootstrap/runtimeContext.js +2 -0
  15. package/extension/runtime-manifest.json +60 -0
  16. package/extension/src/background.js +532 -0
  17. package/extension/src/content/applyResultFormatters.js +387 -0
  18. package/extension/src/content/composerAttachments.js +522 -0
  19. package/extension/src/content/composerPanel.js +254 -0
  20. package/extension/src/content/contentRuntime.js +8895 -0
  21. package/extension/src/content/contextTray.js +686 -0
  22. package/extension/src/content/diagnosticsController.js +659 -0
  23. package/extension/src/content/diagnosticsPanel.js +342 -0
  24. package/extension/src/content/diffReviewPanel.js +797 -0
  25. package/extension/src/content/localSkillsPanel.js +280 -0
  26. package/extension/src/content/markdownText.js +606 -0
  27. package/extension/src/content/mirrorHealth.js +92 -0
  28. package/extension/src/content/modelPicker.js +489 -0
  29. package/extension/src/content/nativeChannel.js +62 -0
  30. package/extension/src/content/otWarmMirror.js +964 -0
  31. package/extension/src/content/otWarmMirrorController.js +174 -0
  32. package/extension/src/content/panelMaintenance.js +367 -0
  33. package/extension/src/content/panelRenderer.js +253 -0
  34. package/extension/src/content/recentProjects.js +1063 -0
  35. package/extension/src/content/reviewHunks.js +247 -0
  36. package/extension/src/content/runController.js +242 -0
  37. package/extension/src/content/runTimelineView.js +1126 -0
  38. package/extension/src/content/sessionManager.js +487 -0
  39. package/extension/src/content/sessionPanel.js +263 -0
  40. package/extension/src/content/settingsPanel.js +527 -0
  41. package/extension/src/content/themeController.js +91 -0
  42. package/extension/src/content/updateIdle.js +67 -0
  43. package/extension/src/content/writebackController.js +300 -0
  44. package/extension/src/content/writebackOrchestrator.js +850 -0
  45. package/extension/src/contentScript.js +8 -0
  46. package/extension/src/page/compileBridge.js +460 -0
  47. package/extension/src/page/overleafCapabilities.js +100 -0
  48. package/extension/src/page/overleafEditor.js +282 -0
  49. package/extension/src/page/overleafProjectSnapshot.js +234 -0
  50. package/extension/src/page/overleafRealtimeObserver.js +491 -0
  51. package/extension/src/page/pageBridgeCapability.js +71 -0
  52. package/extension/src/page/snapshotRouter.js +948 -0
  53. package/extension/src/page/trackedChangesLifecycle.js +1506 -0
  54. package/extension/src/page/treeOperations.js +1584 -0
  55. package/extension/src/page/writeGuard.js +142 -0
  56. package/extension/src/page/writebackRouter.js +1934 -0
  57. package/extension/src/pageBridge.js +2224 -0
  58. package/extension/src/popup.js +247 -0
  59. package/extension/src/shared/compatibility.js +1 -1
  60. package/extension/styles/panel.css +4048 -0
  61. package/native-host/src/index.js +9 -2
  62. package/native-host/src/launcher.js +2 -0
  63. package/native-host/src/managedInstall.js +393 -0
  64. package/native-host/src/managedLauncherRuntime.js +71 -0
  65. package/native-host/src/nativeEnvironment.js +4 -0
  66. package/native-host/src/nativeHostPlatform.js +16 -0
  67. package/native-host/src/runtimeInstaller.js +6 -1
  68. package/native-host/src/taskRunnerRuntime.js +8 -0
  69. package/native-host/src/updateArchive.js +253 -0
  70. package/native-host/src/updateManager.js +548 -0
  71. package/native-host/src/updateTrust.js +156 -0
  72. package/package.json +11 -2
  73. package/scripts/install-managed.mjs +64 -0
  74. package/scripts/uninstall-managed.mjs +48 -0
@@ -0,0 +1,797 @@
1
+ (function initCodexOverleafDiffReviewPanel(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafDiffReviewPanel = factory();
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function diffReviewPanelFactory() {
8
+ 'use strict';
9
+
10
+ const MAX_INITIAL_REVIEW_HUNKS = 20;
11
+ const MAX_INITIAL_HUNK_LINES = 80;
12
+
13
+ function createDiffReviewPanelController(deps = {}) {
14
+ const root = deps.root || (typeof window !== 'undefined' ? window : globalThis);
15
+ const document = deps.document || root.document || (typeof globalThis !== 'undefined' ? globalThis.document : null);
16
+ const tr = typeof deps.tr === 'function' ? deps.tr : (key) => key;
17
+ const callPageBridge = typeof deps.callPageBridge === 'function'
18
+ ? deps.callPageBridge
19
+ : () => Promise.resolve({ ok: false, reason: tr('diffHunkJumpFailed') });
20
+ const getRunEvents = typeof deps.getRunEvents === 'function' ? deps.getRunEvents : () => null;
21
+ const appendRunEvent = typeof deps.appendRunEvent === 'function' ? deps.appendRunEvent : () => {};
22
+ const scrollLogToBottom = typeof deps.scrollLogToBottom === 'function' ? deps.scrollLogToBottom : () => {};
23
+
24
+ function getReviewHunks() {
25
+ return deps.reviewHunks ||
26
+ root.CodexOverleafReviewHunks ||
27
+ (typeof window !== 'undefined' ? window.CodexOverleafReviewHunks : null);
28
+ }
29
+
30
+ function createDiffReviewElement(syncChanges, options = {}) {
31
+ const readonly = Boolean(options.readonly);
32
+ const container = document.createElement('div');
33
+ container.className = 'codex-diff-review';
34
+ container.tabIndex = 0;
35
+ container.setAttribute('role', 'region');
36
+ container.setAttribute('data-diff-review-container', '');
37
+ if (readonly) {
38
+ container.dataset.readonly = 'true';
39
+ }
40
+ const reviewHunks = options.reviewHunks ||
41
+ deps.reviewHunks ||
42
+ root.CodexOverleafReviewHunks ||
43
+ (typeof window !== 'undefined' ? window.CodexOverleafReviewHunks : null);
44
+ const reviewModel = reviewHunks?.buildReviewModel?.(syncChanges) || { files: [], hunks: [] };
45
+ const reviewFilesByIndex = new Map(reviewModel.files.map(file => [file.index, file]));
46
+ const fileStates = new Map();
47
+ const hunkStates = new Map();
48
+ const fileViews = new Map();
49
+ const hunkViews = new Map();
50
+ const decisionListeners = new Set();
51
+ let focusedHunkKey = null;
52
+ let remainingInitialReviewHunks = MAX_INITIAL_REVIEW_HUNKS;
53
+
54
+ function notifyDecisionChanged() {
55
+ for (const listener of decisionListeners) {
56
+ listener();
57
+ }
58
+ }
59
+
60
+ function setDecisionStatus(actions, accepted) {
61
+ const status = document.createElement('span');
62
+ status.className = 'codex-diff-decision-label';
63
+ status.textContent = accepted ? tr('diffAccepted') : tr('diffRejected');
64
+ actions.replaceChildren(status);
65
+ }
66
+
67
+ function setHunkDecisionStatus(actions, accepted, hunk) {
68
+ const status = document.createElement('span');
69
+ status.className = 'codex-diff-hunk-status';
70
+ status.setAttribute('data-diff-hunk-status', '');
71
+ status.textContent = accepted ? tr('diffHunkAccepted') : tr('diffHunkRejected');
72
+ actions.replaceChildren(status);
73
+ // A mis-click must not be permanent: every decision stays reversible
74
+ // until the review is applied.
75
+ if (!readonly && hunk) {
76
+ const revert = document.createElement('button');
77
+ revert.type = 'button';
78
+ revert.setAttribute('data-diff-hunk-revert', '');
79
+ revert.textContent = tr('diffHunkRevert');
80
+ revert.title = tr('diffHunkRevert');
81
+ revert.setAttribute('aria-label', tr('diffHunkRevert'));
82
+ revert.addEventListener('click', () => revertHunkDecision(hunk));
83
+ actions.append(revert);
84
+ }
85
+ }
86
+
87
+ function revertHunkDecision(hunk) {
88
+ if (readonly || hunkStates.get(hunk.decisionKey) === null) {
89
+ return;
90
+ }
91
+ hunkStates.set(hunk.decisionKey, null);
92
+ const view = hunkViews.get(hunk.decisionKey);
93
+ if (view) {
94
+ view.hunkEl.dataset.decision = 'pending';
95
+ setHunkCollapsed(view, false);
96
+ renderPendingHunkActions(view.actions, hunk);
97
+ }
98
+ const fileView = fileViews.get(hunk.path);
99
+ const fileModel = reviewModel.files.find(file => file.path === hunk.path);
100
+ if (fileView && fileModel) {
101
+ updateReviewableFileDecision(fileView, fileModel);
102
+ }
103
+ notifyDecisionChanged();
104
+ }
105
+
106
+ function renderPendingHunkActions(actions, reviewHunk) {
107
+ const acceptHunkBtn = document.createElement('button');
108
+ acceptHunkBtn.type = 'button';
109
+ acceptHunkBtn.setAttribute('data-diff-hunk-accept', '');
110
+ acceptHunkBtn.textContent = '✓';
111
+ acceptHunkBtn.title = tr('diffHunkAccept');
112
+ acceptHunkBtn.setAttribute('aria-label', tr('diffHunkAccept'));
113
+ const rejectHunkBtn = document.createElement('button');
114
+ rejectHunkBtn.type = 'button';
115
+ rejectHunkBtn.setAttribute('data-diff-hunk-reject', '');
116
+ rejectHunkBtn.textContent = '✗';
117
+ rejectHunkBtn.title = tr('diffHunkReject');
118
+ rejectHunkBtn.setAttribute('aria-label', tr('diffHunkReject'));
119
+ const jumpHunkBtn = document.createElement('button');
120
+ jumpHunkBtn.type = 'button';
121
+ jumpHunkBtn.setAttribute('data-diff-hunk-jump', '');
122
+ jumpHunkBtn.textContent = '↗';
123
+ jumpHunkBtn.title = tr('diffHunkJump');
124
+ jumpHunkBtn.setAttribute('aria-label', tr('diffHunkJump'));
125
+ acceptHunkBtn.addEventListener('click', () => decideHunkChange(reviewHunk, true));
126
+ rejectHunkBtn.addEventListener('click', () => decideHunkChange(reviewHunk, false));
127
+ jumpHunkBtn.addEventListener('click', () => jumpToHunk(reviewHunk));
128
+ actions.replaceChildren(acceptHunkBtn, rejectHunkBtn, jumpHunkBtn);
129
+ }
130
+
131
+ function setHunkCollapsed(view, collapsed) {
132
+ if (view?.hunkEl) {
133
+ view.hunkEl.dataset.collapsed = collapsed ? 'true' : 'false';
134
+ }
135
+ }
136
+
137
+ function setHunkDecisionView(hunk, accepted, options = {}) {
138
+ const hunkView = hunkViews.get(hunk.decisionKey);
139
+ if (!hunkView) {
140
+ return;
141
+ }
142
+ hunkView.hunkEl.dataset.decision = accepted ? 'accepted' : 'rejected';
143
+ setHunkDecisionStatus(hunkView.actions, accepted, hunk);
144
+ if (options.collapse) {
145
+ setHunkCollapsed(hunkView, true);
146
+ }
147
+ }
148
+
149
+ function setHunkJumpStatus(view, status, message = '') {
150
+ if (!view) {
151
+ return;
152
+ }
153
+ view.hunkEl.dataset.jumpStatus = status;
154
+ let statusEl = view.hunkEl.querySelector?.('[data-diff-hunk-jump-status]');
155
+ if (!statusEl) {
156
+ statusEl = document.createElement('span');
157
+ statusEl.className = 'codex-diff-hunk-jump-status';
158
+ statusEl.setAttribute('data-diff-hunk-jump-status', '');
159
+ view.actions.append(statusEl);
160
+ }
161
+ statusEl.textContent = message;
162
+ }
163
+
164
+ function setHunkFocused(hunkEl, focused) {
165
+ hunkEl.dataset.focused = focused ? 'true' : 'false';
166
+ const baseClass = 'codex-diff-hunk';
167
+ hunkEl.className = focused ? `${baseClass} codex-diff-hunk-focused` : baseClass;
168
+ }
169
+
170
+ function getFocusableHunks() {
171
+ return Array.from(hunkViews.values()).filter(view => view.reviewHunk);
172
+ }
173
+
174
+ function focusHunkByIndex(index) {
175
+ const hunks = getFocusableHunks();
176
+ if (!hunks.length) {
177
+ return false;
178
+ }
179
+ const nextIndex = Math.max(0, Math.min(index, hunks.length - 1));
180
+ const next = hunks[nextIndex];
181
+ if (focusedHunkKey && focusedHunkKey !== next.reviewHunk.decisionKey) {
182
+ const previous = hunkViews.get(focusedHunkKey);
183
+ if (previous) {
184
+ setHunkFocused(previous.hunkEl, false);
185
+ }
186
+ }
187
+ focusedHunkKey = next.reviewHunk.decisionKey;
188
+ setHunkFocused(next.hunkEl, true);
189
+ return true;
190
+ }
191
+
192
+ function moveFocusedHunk(delta) {
193
+ const hunks = getFocusableHunks();
194
+ if (!hunks.length) {
195
+ return false;
196
+ }
197
+ const currentIndex = Math.max(0, hunks.findIndex(view => view.reviewHunk.decisionKey === focusedHunkKey));
198
+ return focusHunkByIndex(currentIndex + delta);
199
+ }
200
+
201
+ function getFocusedHunk() {
202
+ if (!focusedHunkKey) {
203
+ focusHunkByIndex(0);
204
+ }
205
+ return hunkViews.get(focusedHunkKey)?.reviewHunk || null;
206
+ }
207
+
208
+ function focusNextPendingHunkAfter(decisionKey) {
209
+ const hunks = getFocusableHunks();
210
+ if (!hunks.length) {
211
+ return false;
212
+ }
213
+ const startIndex = hunks.findIndex(view => view.reviewHunk.decisionKey === decisionKey);
214
+ const anchorIndex = startIndex >= 0 ? startIndex : -1;
215
+ for (let offset = 1; offset <= hunks.length; offset += 1) {
216
+ const index = (anchorIndex + offset) % hunks.length;
217
+ const view = hunks[index];
218
+ if (hunkStates.get(view.reviewHunk.decisionKey) === null) {
219
+ const focused = focusHunkByIndex(index);
220
+ view.hunkEl.scrollIntoView?.({ block: 'nearest' });
221
+ return focused;
222
+ }
223
+ }
224
+ if (focusedHunkKey) {
225
+ const previous = hunkViews.get(focusedHunkKey);
226
+ if (previous) {
227
+ setHunkFocused(previous.hunkEl, false);
228
+ }
229
+ focusedHunkKey = null;
230
+ }
231
+ return false;
232
+ }
233
+
234
+ function getHunkJumpRange(reviewHunk) {
235
+ const from = Number(reviewHunk?.startOffset);
236
+ const to = Number(reviewHunk?.endOffset);
237
+ return {
238
+ path: reviewHunk?.path,
239
+ from: Number.isInteger(from) && from >= 0 ? from : 0,
240
+ to: Number.isInteger(to) && to >= 0 ? to : (Number.isInteger(from) && from >= 0 ? from : 0)
241
+ };
242
+ }
243
+
244
+ async function jumpToHunk(reviewHunk) {
245
+ if (!reviewHunk) {
246
+ return;
247
+ }
248
+ const { path, from, to } = getHunkJumpRange(reviewHunk);
249
+ const view = hunkViews.get(reviewHunk.decisionKey);
250
+ setHunkJumpStatus(view, 'pending', '');
251
+ try {
252
+ const result = await callPageBridge('jumpToPosition', { path, from, to });
253
+ if (result?.ok === false) {
254
+ setHunkJumpStatus(view, 'failed', result.reason || tr('diffHunkJumpFailed'));
255
+ } else {
256
+ setHunkJumpStatus(view, 'done', '');
257
+ }
258
+ } catch (error) {
259
+ setHunkJumpStatus(view, 'failed', error?.message || tr('diffHunkJumpFailed'));
260
+ }
261
+ }
262
+
263
+ function isDiffReviewEditableTarget(target) {
264
+ const tagName = String(target?.tagName || '').toUpperCase();
265
+ if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' ||
266
+ tagName === 'BUTTON' || tagName === 'SUMMARY') {
267
+ return true;
268
+ }
269
+ if (tagName === 'A' && target?.getAttribute?.('href')) {
270
+ return true;
271
+ }
272
+ const role = String(target?.getAttribute?.('role') || '').toLowerCase();
273
+ if (['button', 'link', 'menuitem', 'checkbox', 'radio', 'switch', 'tab'].includes(role)) {
274
+ return true;
275
+ }
276
+ if (target?.isContentEditable || target?.getAttribute?.('contenteditable') === 'true') {
277
+ return true;
278
+ }
279
+ return Boolean(target?.closest?.(
280
+ 'input, textarea, select, button, summary, a[href], [contenteditable="true"], [role="button"], [role="link"], [role="menuitem"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"]'
281
+ ));
282
+ }
283
+
284
+ function handleDiffReviewKeydown(event) {
285
+ if (isDiffReviewEditableTarget(event.target)) {
286
+ return;
287
+ }
288
+ let handled = false;
289
+ switch (event.key) {
290
+ case 'j':
291
+ handled = moveFocusedHunk(1);
292
+ break;
293
+ case 'k':
294
+ handled = moveFocusedHunk(-1);
295
+ break;
296
+ case 'a': {
297
+ const hunk = getFocusedHunk();
298
+ if (hunk && !readonly) {
299
+ decideHunkChange(hunk, true);
300
+ handled = true;
301
+ }
302
+ break;
303
+ }
304
+ case 'r': {
305
+ const hunk = getFocusedHunk();
306
+ if (hunk && !readonly) {
307
+ decideHunkChange(hunk, false);
308
+ handled = true;
309
+ }
310
+ break;
311
+ }
312
+ case 'Enter': {
313
+ const hunk = getFocusedHunk();
314
+ if (hunk) {
315
+ jumpToHunk(hunk);
316
+ handled = true;
317
+ }
318
+ break;
319
+ }
320
+ case 'Escape':
321
+ if (focusedHunkKey) {
322
+ const previous = hunkViews.get(focusedHunkKey);
323
+ if (previous) {
324
+ setHunkFocused(previous.hunkEl, false);
325
+ }
326
+ focusedHunkKey = null;
327
+ }
328
+ container.blur?.();
329
+ handled = true;
330
+ break;
331
+ default:
332
+ break;
333
+ }
334
+ if (handled) {
335
+ event.preventDefault?.();
336
+ }
337
+ }
338
+
339
+ function updateReviewableFileDecision(view, fileModel) {
340
+ const decisions = fileModel.hunks.map(hunk => hunkStates.get(hunk.decisionKey));
341
+ const allAccepted = decisions.every(value => value === true);
342
+ const allRejected = decisions.every(value => value === false);
343
+ if (allAccepted || allRejected) {
344
+ view.card.dataset.accepted = allAccepted ? 'true' : 'false';
345
+ view.card.dataset.decision = allAccepted ? 'accepted' : 'rejected';
346
+ setDecisionStatus(view.actions, allAccepted);
347
+ return;
348
+ }
349
+ view.card.dataset.decision = 'pending';
350
+ delete view.card.dataset.accepted;
351
+ }
352
+
353
+ function decideHunkChange(hunk, accepted) {
354
+ if (readonly || hunkStates.get(hunk.decisionKey) !== null) {
355
+ return;
356
+ }
357
+ hunkStates.set(hunk.decisionKey, accepted);
358
+ setHunkDecisionView(hunk, accepted, { collapse: true });
359
+ focusNextPendingHunkAfter(hunk.decisionKey);
360
+ const fileView = fileViews.get(hunk.path);
361
+ const fileModel = reviewModel.files.find(file => file.path === hunk.path);
362
+ if (fileView && fileModel) {
363
+ updateReviewableFileDecision(fileView, fileModel);
364
+ }
365
+ notifyDecisionChanged();
366
+ }
367
+
368
+ function decideReviewableFilePendingHunks(fileModel, accepted) {
369
+ let changed = false;
370
+ for (const hunk of fileModel.hunks) {
371
+ if (hunkStates.get(hunk.decisionKey) !== null) {
372
+ continue;
373
+ }
374
+ hunkStates.set(hunk.decisionKey, accepted);
375
+ setHunkDecisionView(hunk, accepted, { collapse: true });
376
+ changed = true;
377
+ }
378
+ return changed;
379
+ }
380
+
381
+ function decidePendingChanges(accepted) {
382
+ if (readonly) {
383
+ return false;
384
+ }
385
+ let changed = false;
386
+ for (const [path, value] of fileStates.entries()) {
387
+ if (value !== null) {
388
+ continue;
389
+ }
390
+ fileStates.set(path, accepted);
391
+ const view = fileViews.get(path);
392
+ if (view) {
393
+ view.card.dataset.accepted = accepted ? 'true' : 'false';
394
+ view.card.dataset.decision = accepted ? 'accepted' : 'rejected';
395
+ setDecisionStatus(view.actions, accepted);
396
+ }
397
+ changed = true;
398
+ }
399
+ for (const fileModel of reviewModel.files) {
400
+ if (!fileModel.reviewable) {
401
+ continue;
402
+ }
403
+ changed = decideReviewableFilePendingHunks(fileModel, accepted) || changed;
404
+ const fileView = fileViews.get(fileModel.path);
405
+ if (fileView) {
406
+ updateReviewableFileDecision(fileView, fileModel);
407
+ }
408
+ }
409
+ focusNextPendingHunkAfter(focusedHunkKey);
410
+ if (changed) {
411
+ notifyDecisionChanged();
412
+ }
413
+ return changed;
414
+ }
415
+
416
+ function decideFileChange(path, accepted) {
417
+ if (readonly) {
418
+ return;
419
+ }
420
+ const view = fileViews.get(path);
421
+ if (!view) {
422
+ return;
423
+ }
424
+ const fileModel = view.fileModel;
425
+ if (fileModel?.reviewable) {
426
+ const changed = decideReviewableFilePendingHunks(fileModel, accepted);
427
+ updateReviewableFileDecision(view, fileModel);
428
+ focusNextPendingHunkAfter(focusedHunkKey);
429
+ if (changed) {
430
+ notifyDecisionChanged();
431
+ }
432
+ return;
433
+ }
434
+ if (fileStates.get(path) !== null) {
435
+ return;
436
+ }
437
+ fileStates.set(path, accepted);
438
+ view.card.dataset.accepted = accepted ? 'true' : 'false';
439
+ view.card.dataset.decision = accepted ? 'accepted' : 'rejected';
440
+ setDecisionStatus(view.actions, accepted);
441
+ notifyDecisionChanged();
442
+ }
443
+
444
+ function getPendingCount() {
445
+ let count = 0;
446
+ for (const value of fileStates.values()) {
447
+ if (value === null) {
448
+ count += 1;
449
+ }
450
+ }
451
+ for (const value of hunkStates.values()) {
452
+ if (value === null) {
453
+ count += 1;
454
+ }
455
+ }
456
+ return count;
457
+ }
458
+
459
+ function getDecisions() {
460
+ const decisions = {};
461
+ for (const [path, value] of fileStates.entries()) {
462
+ decisions[reviewHunks?.normalizeReviewDecisionKey?.(path) || `${path}::file`] = value;
463
+ }
464
+ for (const [key, value] of hunkStates.entries()) {
465
+ decisions[key] = value;
466
+ }
467
+ return decisions;
468
+ }
469
+
470
+ function getAcceptedChanges() {
471
+ const decisions = getDecisions();
472
+ if (reviewHunks?.buildAcceptedSyncChanges) {
473
+ return reviewHunks.buildAcceptedSyncChanges(syncChanges, decisions);
474
+ }
475
+ return syncChanges.filter(change => fileStates.get(change.path) === true);
476
+ }
477
+
478
+ function createDiffLineElement(line) {
479
+ const lineEl = document.createElement('div');
480
+ lineEl.className = 'codex-diff-line';
481
+ lineEl.dataset.type = line.type;
482
+ lineEl.setAttribute('data-diff-line', '');
483
+ lineEl.textContent = line.text;
484
+ return lineEl;
485
+ }
486
+
487
+ function createPatchDiffLines(patch = {}) {
488
+ const lines = [];
489
+ appendPatchDiffText(lines, 'remove', patch.expected);
490
+ appendPatchDiffText(lines, 'add', patch.insert);
491
+ return lines;
492
+ }
493
+
494
+ function appendPatchDiffText(lines, type, text) {
495
+ const value = String(text ?? '');
496
+ if (!value) {
497
+ return;
498
+ }
499
+ for (const line of value.split('\n')) {
500
+ lines.push({ type, text: line });
501
+ }
502
+ }
503
+
504
+ function getReviewDisplayHunk(change, diffHunks, fileModel, hunkIndex) {
505
+ const diffHunk = diffHunks[hunkIndex] || { lines: [] };
506
+ if (!fileModel?.reviewable) {
507
+ return diffHunk;
508
+ }
509
+ if (diffHunks.length === fileModel.hunks.length && Array.isArray(diffHunk.lines) && diffHunk.lines.length) {
510
+ return diffHunk;
511
+ }
512
+ const patchIndex = fileModel.hunks[hunkIndex]?.patchIndexes?.[0] ?? hunkIndex;
513
+ const patch = Array.isArray(change.patches) ? change.patches[patchIndex] : null;
514
+ const patchLines = createPatchDiffLines(patch);
515
+ return patchLines.length ? { ...diffHunk, lines: patchLines } : diffHunk;
516
+ }
517
+
518
+ function appendHunkLines(hunkEl, lines = []) {
519
+ const lineWrap = document.createElement('div');
520
+ lineWrap.className = 'codex-diff-hunk-lines';
521
+ const normalizedLines = Array.isArray(lines) ? lines : [];
522
+
523
+ function renderLines(expanded) {
524
+ const visibleLines = expanded ? normalizedLines : normalizedLines.slice(0, MAX_INITIAL_HUNK_LINES);
525
+ const children = visibleLines.map(createDiffLineElement);
526
+ if (!expanded && normalizedLines.length > MAX_INITIAL_HUNK_LINES) {
527
+ const expandBtn = document.createElement('button');
528
+ expandBtn.type = 'button';
529
+ expandBtn.className = 'codex-diff-hunk-expand';
530
+ expandBtn.setAttribute('data-diff-hunk-expand', '');
531
+ expandBtn.textContent = tr('diffHunkExpandLines', {
532
+ count: normalizedLines.length - MAX_INITIAL_HUNK_LINES
533
+ });
534
+ expandBtn.addEventListener('click', () => renderLines(true));
535
+ children.push(expandBtn);
536
+ }
537
+ lineWrap.replaceChildren(...children);
538
+ }
539
+
540
+ renderLines(false);
541
+ hunkEl.append(lineWrap);
542
+ }
543
+
544
+ function createDiffHunkElement(hunk, reviewHunk, fileModel, hunkIndex) {
545
+ const hunkEl = document.createElement('div');
546
+ hunkEl.className = 'codex-diff-hunk';
547
+ hunkEl.setAttribute('data-diff-review-hunk', '');
548
+ hunkEl.dataset.hunkIndex = String(hunkIndex);
549
+ if (reviewHunk) {
550
+ // Orientation header: reviewers need "what changed where" — the
551
+ // start line was always computed (reviewHunks) but never shown.
552
+ if (Number.isFinite(Number(reviewHunk.startLine)) && Number(reviewHunk.startLine) > 0) {
553
+ const location = document.createElement('div');
554
+ location.className = 'codex-diff-hunk-location';
555
+ location.textContent = tr('diffHunkLocation', {
556
+ line: String(reviewHunk.startLine),
557
+ count: String(reviewHunk.lineCount || 1)
558
+ });
559
+ hunkEl.append(location);
560
+ }
561
+ const hunkDecision = hunkStates.get(reviewHunk.decisionKey);
562
+ hunkEl.dataset.decision = readonly ? 'accepted' : (hunkDecision === true ? 'accepted' : hunkDecision === false ? 'rejected' : 'pending');
563
+ if (!readonly && (hunkDecision === true || hunkDecision === false)) {
564
+ hunkEl.dataset.collapsed = 'true';
565
+ }
566
+ setHunkFocused(hunkEl, focusedHunkKey === reviewHunk.decisionKey);
567
+ const hunkActions = document.createElement('div');
568
+ hunkActions.className = 'codex-diff-hunk-actions';
569
+ if (readonly || hunkDecision === true || hunkDecision === false) {
570
+ setHunkDecisionStatus(hunkActions, readonly ? true : hunkDecision, readonly ? null : reviewHunk);
571
+ } else {
572
+ renderPendingHunkActions(hunkActions, reviewHunk);
573
+ }
574
+ hunkEl.append(hunkActions);
575
+ hunkViews.set(reviewHunk.decisionKey, { hunkEl, actions: hunkActions, reviewHunk });
576
+ } else if (!readonly && fileModel && !fileModel.reviewable && hunkIndex === 0) {
577
+ const fallback = document.createElement('div');
578
+ fallback.className = 'codex-diff-fallback-label';
579
+ fallback.textContent = tr('diffFallbackFileOnly');
580
+ hunkEl.append(fallback);
581
+ }
582
+ appendHunkLines(hunkEl, hunk.lines);
583
+ return hunkEl;
584
+ }
585
+
586
+ for (const [changeIndex, change] of syncChanges.entries()) {
587
+ const fileModel = reviewFilesByIndex.get(changeIndex);
588
+ if (fileModel?.reviewable) {
589
+ for (const hunk of fileModel.hunks) {
590
+ hunkStates.set(hunk.decisionKey, readonly ? true : null);
591
+ }
592
+ } else {
593
+ fileStates.set(change.path, readonly ? true : null);
594
+ }
595
+ const card = document.createElement('div');
596
+ card.className = 'codex-diff-file';
597
+ card.dataset.path = change.path;
598
+ card.dataset.decision = readonly ? 'accepted' : 'pending';
599
+ if (readonly) {
600
+ card.dataset.accepted = 'true';
601
+ }
602
+
603
+ const header = document.createElement('div');
604
+ header.className = 'codex-diff-file-header';
605
+ const pathEl = document.createElement('span');
606
+ pathEl.className = 'codex-diff-file-path';
607
+ pathEl.textContent = change.type === 'delete' ? `[delete] ${change.path}` : change.path;
608
+ pathEl.title = change.path;
609
+ const actions = document.createElement('div');
610
+ actions.className = 'codex-diff-file-actions';
611
+
612
+ if (readonly) {
613
+ const status = document.createElement('span');
614
+ status.className = 'codex-diff-readonly-label';
615
+ status.textContent = tr('diffWritten');
616
+ actions.append(status);
617
+ } else {
618
+ const acceptBtn = document.createElement('button');
619
+ acceptBtn.type = 'button';
620
+ acceptBtn.dataset.diffAccept = '';
621
+ acceptBtn.textContent = '✓';
622
+ acceptBtn.title = tr('diffAccept');
623
+ acceptBtn.setAttribute('aria-label', tr('diffAccept'));
624
+ const rejectBtn = document.createElement('button');
625
+ rejectBtn.type = 'button';
626
+ rejectBtn.dataset.diffReject = '';
627
+ rejectBtn.textContent = '✗';
628
+ rejectBtn.title = tr('diffReject');
629
+ rejectBtn.setAttribute('aria-label', tr('diffReject'));
630
+
631
+ acceptBtn.addEventListener('click', () => decideFileChange(change.path, true));
632
+ rejectBtn.addEventListener('click', () => decideFileChange(change.path, false));
633
+
634
+ actions.append(acceptBtn, rejectBtn);
635
+ }
636
+
637
+ header.append(pathEl, actions);
638
+ card.append(header);
639
+
640
+ const diffHunks = Array.isArray(change.diff) ? change.diff : [];
641
+ if (diffHunks.length || fileModel?.reviewable) {
642
+ const body = document.createElement('div');
643
+ body.className = 'codex-diff-body';
644
+ const hunkCount = fileModel?.reviewable ? Math.max(diffHunks.length, fileModel.hunks.length) : diffHunks.length;
645
+ const initialHunkCount = Math.min(hunkCount, remainingInitialReviewHunks);
646
+ remainingInitialReviewHunks -= initialHunkCount;
647
+ function renderHunks(visibleCount) {
648
+ const children = [];
649
+ for (let hunkIndex = 0; hunkIndex < visibleCount; hunkIndex += 1) {
650
+ const reviewHunk = fileModel?.reviewable ? fileModel.hunks[hunkIndex] : null;
651
+ const hunk = getReviewDisplayHunk(change, diffHunks, fileModel, hunkIndex);
652
+ children.push(createDiffHunkElement(hunk, reviewHunk, fileModel, hunkIndex));
653
+ }
654
+ if (visibleCount < hunkCount) {
655
+ const showMoreBtn = document.createElement('button');
656
+ showMoreBtn.type = 'button';
657
+ showMoreBtn.className = 'codex-diff-show-more-hunks';
658
+ showMoreBtn.setAttribute('data-diff-show-more-hunks', '');
659
+ showMoreBtn.textContent = tr('diffShowMoreHunks', {
660
+ count: hunkCount - visibleCount
661
+ });
662
+ showMoreBtn.addEventListener('click', () => renderHunks(hunkCount));
663
+ children.push(showMoreBtn);
664
+ }
665
+ body.replaceChildren(...children);
666
+ }
667
+ renderHunks(initialHunkCount);
668
+ card.append(body);
669
+ }
670
+
671
+ container.append(card);
672
+ fileViews.set(change.path, { card, actions, fileModel });
673
+ }
674
+
675
+ container.addEventListener('keydown', handleDiffReviewKeydown);
676
+
677
+ return {
678
+ container,
679
+ fileStates,
680
+ decideFileChange,
681
+ decidePendingChanges,
682
+ getPendingCount,
683
+ getDecisions,
684
+ getAcceptedChanges,
685
+ onDecision(callback) {
686
+ decisionListeners.add(callback);
687
+ return () => decisionListeners.delete(callback);
688
+ }
689
+ };
690
+ }
691
+
692
+ function renderDiffReview(syncChanges) {
693
+ return new Promise(resolve => {
694
+ const review = createDiffReviewElement(syncChanges);
695
+ const { container } = review;
696
+ let finished = false;
697
+
698
+ const toolbar = document.createElement('div');
699
+ toolbar.className = 'codex-diff-toolbar';
700
+ const summary = document.createElement('span');
701
+ summary.className = 'codex-diff-toolbar-summary';
702
+ const rejectAllBtn = document.createElement('button');
703
+ rejectAllBtn.type = 'button';
704
+ rejectAllBtn.textContent = tr('diffRejectAll');
705
+ const acceptAllBtn = document.createElement('button');
706
+ acceptAllBtn.type = 'button';
707
+ acceptAllBtn.dataset.diffAcceptAll = '';
708
+ acceptAllBtn.textContent = tr('diffAcceptAll');
709
+
710
+ function finish(accepted) {
711
+ if (finished) {
712
+ return;
713
+ }
714
+ finished = true;
715
+ container.remove();
716
+ resolve(accepted);
717
+ }
718
+
719
+ function updateSummary() {
720
+ const pending = review.getPendingCount();
721
+ summary.textContent = pending ? tr('diffPendingCount', { count: pending }) : tr('diffSelectionDone');
722
+ }
723
+
724
+ function finishIfAllDecided() {
725
+ updateSummary();
726
+ if (review.getPendingCount() === 0) {
727
+ const reviewHunks = getReviewHunks();
728
+ // Report what was rejected BEFORE resolving: the run's completion
729
+ // report uses these summaries for the "redo rejected changes"
730
+ // action. Failure here must never block the accept path.
731
+ if (typeof deps.onRejectedHunks === 'function' && reviewHunks?.buildRejectedHunkSummaries) {
732
+ try {
733
+ deps.onRejectedHunks(reviewHunks.buildRejectedHunkSummaries(syncChanges, review.getDecisions()));
734
+ } catch (error) {
735
+ // Summaries are best-effort decoration on the report.
736
+ }
737
+ }
738
+ finish(reviewHunks?.buildAcceptedSyncChanges
739
+ ? reviewHunks.buildAcceptedSyncChanges(syncChanges, review.getDecisions())
740
+ : review.getAcceptedChanges());
741
+ }
742
+ }
743
+
744
+ acceptAllBtn.addEventListener('click', () => {
745
+ review.decidePendingChanges(true);
746
+ finishIfAllDecided();
747
+ });
748
+ rejectAllBtn.addEventListener('click', () => {
749
+ review.decidePendingChanges(false);
750
+ finishIfAllDecided();
751
+ });
752
+ review.onDecision(finishIfAllDecided);
753
+
754
+ updateSummary();
755
+ toolbar.append(summary, rejectAllBtn, acceptAllBtn);
756
+ container.append(toolbar);
757
+
758
+ const events = getRunEvents();
759
+ if (events) {
760
+ events.append(container);
761
+ scrollLogToBottom();
762
+ }
763
+ });
764
+ }
765
+
766
+ function renderReadOnlyDiffReview(syncChanges, title = tr('diffWrittenChangesTitle')) {
767
+ const visibleChanges = (syncChanges || []).filter(change => change?.diff?.length);
768
+ let events = getRunEvents();
769
+ if (!visibleChanges.length || !events) {
770
+ return;
771
+ }
772
+
773
+ appendRunEvent({
774
+ title,
775
+ status: 'completed'
776
+ });
777
+ const { container } = createDiffReviewElement(visibleChanges, { readonly: true });
778
+ events = getRunEvents() || events;
779
+ events.append(container);
780
+ scrollLogToBottom();
781
+ }
782
+
783
+ return {
784
+ createDiffReviewElement,
785
+ renderDiffReview,
786
+ renderReadOnlyDiffReview
787
+ };
788
+ }
789
+
790
+ return {
791
+ createDiffReviewPanelController,
792
+ _private: {
793
+ MAX_INITIAL_REVIEW_HUNKS,
794
+ MAX_INITIAL_HUNK_LINES
795
+ }
796
+ };
797
+ });