codex-overleaf-link 1.3.6 → 1.3.8

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1>Codex Overleaf Link</h1>
4
4
  <p><strong>Empower Overleaf with Codex.</strong></p>
5
5
  <p>
6
- <img src="https://img.shields.io/badge/version-1.3.6-blue" alt="version">
6
+ <img src="https://img.shields.io/badge/version-1.3.8-blue" alt="version">
7
7
  <img src="https://img.shields.io/badge/platform-macOS%20%2F%20Windows%20%2F%20Linux-lightgrey" alt="platform">
8
8
  <img src="https://img.shields.io/badge/chrome-MV3-green" alt="chrome manifest v3">
9
9
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node version">
@@ -38,14 +38,14 @@ One command installs the native host **and** sets up the extension: the script r
38
38
  macOS / Linux:
39
39
 
40
40
  ```bash
41
- CODEX_OVERLEAF_REF=v1.3.6 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.6/install.sh)"
41
+ CODEX_OVERLEAF_REF=v1.3.8 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.8/install.sh)"
42
42
  ```
43
43
 
44
44
  Windows PowerShell:
45
45
 
46
46
  ```powershell
47
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.6/install.ps1 -OutFile install.ps1
48
- $env:CODEX_OVERLEAF_REF='v1.3.6'
47
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.8/install.ps1 -OutFile install.ps1
48
+ $env:CODEX_OVERLEAF_REF='v1.3.8'
49
49
  powershell -ExecutionPolicy Bypass -File install.ps1
50
50
  ```
51
51
 
@@ -56,10 +56,10 @@ Then, in the `chrome://extensions` tab the script opened: enable **Developer mod
56
56
  `npm exec` installs and updates the **native host only** — it does not include the Chrome extension. Use it if you prefer a pinned npm package to a source checkout.
57
57
 
58
58
  ```bash
59
- npm exec --yes codex-overleaf-link@1.3.6 -- install-native
59
+ npm exec --yes codex-overleaf-link@1.3.8 -- install-native
60
60
  ```
61
61
 
62
- Then add the extension yourself: download `codex-overleaf-link-extension-v1.3.6.zip` from the [v1.3.6 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.3.6), unzip it to a stable folder, and in `chrome://extensions` enable **Developer mode**, click **Load unpacked**, and select that folder.
62
+ Then add the extension yourself: download `codex-overleaf-link-extension-v1.3.8.zip` from the [v1.3.8 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.3.8), unzip it to a stable folder, and in `chrome://extensions` enable **Developer mode**, click **Load unpacked**, and select that folder.
63
63
 
64
64
  ### Open Overleaf
65
65
 
@@ -86,9 +86,9 @@ npm installs, updates, uninstalls, and diagnoses the native host only. npm does
86
86
 
87
87
  | Action | Command |
88
88
  |--------|---------|
89
- | Install / update | `npm exec --yes codex-overleaf-link@1.3.6 -- install-native` |
90
- | Diagnose | `npm exec --yes codex-overleaf-link@1.3.6 -- doctor` |
91
- | Uninstall | `npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native` |
89
+ | Install / update | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` |
90
+ | Diagnose | `npm exec --yes codex-overleaf-link@1.3.8 -- doctor` |
91
+ | Uninstall | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` |
92
92
 
93
93
  Use `--extension-id <chrome-extension-id>` only for a custom/dev unpacked extension id that differs from the official bundled id.
94
94
 
@@ -98,13 +98,13 @@ To update, re-run any of the [native host installers](#install) — they install
98
98
 
99
99
  ## GitHub Release Artifacts
100
100
 
101
- The v1.3.6 GitHub Release contains:
101
+ The v1.3.8 GitHub Release contains:
102
102
 
103
- - `codex-overleaf-link-extension-v1.3.6.zip`: loadable Chrome extension package for manual unpacked installation.
104
- - `codex-overleaf-native-host-v1.3.6.tar.gz`: native host runtime files used by the installer and release verification.
105
- - `codex-overleaf-link-1.3.6.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
106
- - `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.3.6` when run directly from the release artifact.
107
- - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.3.6` when run directly from the release artifact.
103
+ - `codex-overleaf-link-extension-v1.3.8.zip`: loadable Chrome extension package for manual unpacked installation.
104
+ - `codex-overleaf-native-host-v1.3.8.tar.gz`: native host runtime files used by the installer and release verification.
105
+ - `codex-overleaf-link-1.3.8.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
106
+ - `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.3.8` when run directly from the release artifact.
107
+ - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.3.8` when run directly from the release artifact.
108
108
  - `uninstall-native-host.mjs`: native host uninstaller that removes the Chrome Native Messaging manifest, bridge executable, and runtime copy.
109
109
  - `nativeHostPlatform.js`, `manifest.js`, `runtimeInstaller.js`: helper files required by the loose uninstaller asset.
110
110
  - `SHA256SUMS` and `release-manifest.json`: checksum and artifact metadata for release verification.
@@ -115,7 +115,7 @@ The v1.3.6 GitHub Release contains:
115
115
  Remove the native host (use `--browser chromium` on Linux Chromium):
116
116
 
117
117
  ```bash
118
- npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native
118
+ npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native
119
119
  ```
120
120
 
121
121
  The same command works on Windows PowerShell. If you installed from a manual checkout or source installer, you can also run `npm run uninstall:native` inside the repo, use `node ~/.codex-overleaf/source/scripts/uninstall-native-host.mjs` on macOS / Linux, or use `node $env:LOCALAPPDATA\CodexOverleaf\source\scripts\uninstall-native-host.mjs` on Windows PowerShell.
@@ -150,13 +150,13 @@ Then remove the extension from `chrome://extensions`. To delete local data: on m
150
150
  Linux Chromium install or update:
151
151
 
152
152
  ```bash
153
- CODEX_OVERLEAF_REF=v1.3.6 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.6/install.sh)" -- --browser chromium
153
+ CODEX_OVERLEAF_REF=v1.3.8 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.8/install.sh)" -- --browser chromium
154
154
  ```
155
155
 
156
156
  Linux Chromium uninstall:
157
157
 
158
158
  ```bash
159
- npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native --browser chromium
159
+ npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native --browser chromium
160
160
  ```
161
161
 
162
162
  ## Features
@@ -167,6 +167,7 @@ npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native --browser chromium
167
167
  - **Diff review** — per-file diff view before accepting changes.
168
168
  - **Undo checkpoint** — one-click revert of browser writes.
169
169
  - **Track Changes integration** — optionally enables Overleaf Reviewing before writing.
170
+ - **Accept / Undo per run** — when a run wrote in Reviewing mode, accept or revert all of its tracked changes from the run card in one click. Accept replays the run's edits as untracked text via Overleaf's native undo path, with stable-Editing waits and an automatic rollback if Overleaf reintroduces tracked changes during the replay.
170
171
  - **Auto-recompile** — triggers Overleaf recompile after writeback; logs compile errors as context.
171
172
  - **@ context** — attach specific files, `@compile-log`, or `@current-section` to the prompt.
172
173
  - **Composer attachments and binary writeback** — paste or drop PDFs, images, and files into the composer as turn-scoped Codex context, and review Codex-created assets before creating or replacing them in Overleaf.
@@ -288,7 +289,7 @@ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per
288
289
  Re-run any [native host installer](#install), reload the extension in `chrome://extensions`, then refresh the Overleaf tab. This also fixes extension/native version mismatch and native protocol mismatch.
289
290
 
290
291
  ```bash
291
- npm exec --yes codex-overleaf-link@1.3.6 -- install-native
292
+ npm exec --yes codex-overleaf-link@1.3.8 -- install-native
292
293
  ```
293
294
 
294
295
  **The Windows popup or panel shows a Bash recovery command**
@@ -337,8 +338,8 @@ Use this matrix for release-candidate signoff and compatibility reports. Record
337
338
  | Browser/channel/version | Google Chrome channel and version. | Google Chrome channel and version. | Google Chrome channel and version. | Chromium channel/package and version. |
338
339
  | Install mode | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout; native host installed with `--browser chromium`. |
339
340
  | Extension id | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. |
340
- | Installer/update command | `npm exec --yes codex-overleaf-link@1.3.6 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.6 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.6 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.6 -- install-native --browser chromium` |
341
- | Uninstall command | `npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.6 -- uninstall-native --browser chromium` |
341
+ | Installer/update command | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native --browser chromium` |
342
+ | Uninstall command | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native --browser chromium` |
342
343
  | Manifest/registry path | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.codex.overleaf.json` | `HKCU\Software\Google\Chrome\NativeMessagingHosts\com.codex.overleaf` -> `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime\com.codex.overleaf.json` | `~/.config/google-chrome/NativeMessagingHosts/com.codex.overleaf.json` | `~/.config/chromium/NativeMessagingHosts/com.codex.overleaf.json` |
343
344
  | Bridge/runtime/source path | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `%LOCALAPPDATA%\CodexOverleaf\codex-overleaf-bridge.cmd`; runtime `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime`; source `%LOCALAPPDATA%\CodexOverleaf\source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. |
344
345
  | Node/Git/Codex/TeX | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. |
@@ -1,12 +1,27 @@
1
1
  (function initAgentTranscript(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory({
4
+ getFailureReasons: function getFailureReasonsCjs() { return require('./failureReasons.js'); },
5
+ getI18n: function getI18nCjs() { return require('./i18n.js'); }
6
+ });
4
7
  } else {
5
- root.CodexOverleafAgentTranscript = factory();
8
+ // Browser script-tag load order is determined by manifest.json content_scripts;
9
+ // resolve dependencies lazily so this module can be listed before its deps.
10
+ root.CodexOverleafAgentTranscript = factory({
11
+ getFailureReasons: function getFailureReasonsWindow() { return root.CodexOverleafFailureReasons || null; },
12
+ getI18n: function getI18nWindow() { return root.CodexOverleafI18n || null; }
13
+ });
6
14
  }
7
- })(typeof globalThis !== 'undefined' ? globalThis : window, function agentTranscriptFactory() {
15
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function agentTranscriptFactory(deps) {
8
16
  'use strict';
9
17
 
18
+ const getFailureReasonsModule = (deps && deps.getFailureReasons) instanceof Function
19
+ ? deps.getFailureReasons
20
+ : function noFailureReasons() { return null; };
21
+ const getI18nModule = (deps && deps.getI18n) instanceof Function
22
+ ? deps.getI18n
23
+ : function noI18n() { return null; };
24
+
10
25
  const TECHNICAL_EVENT_PATTERNS = [
11
26
  /^agent\.command\./,
12
27
  /^native\.task\./,
@@ -523,6 +538,16 @@
523
538
  nextStep: textFor(locale, '请确认终端里可以运行 `codex`,然后重新安装 native host 或刷新扩展后重试。', 'Confirm `codex` works in Terminal, then reinstall the native host or reload the extension.')
524
539
  };
525
540
  }
541
+ if (/project_locked|currently in use by codex\.run/i.test(text)) {
542
+ return {
543
+ conclusion: textFor(locale,
544
+ '这轮没有启动:同一个 Overleaf 项目里已经有一轮 Codex 任务正在运行。',
545
+ 'This run did not start: another Codex task is already running for this Overleaf project.'),
546
+ nextStep: textFor(locale,
547
+ '请等待当前任务完成,或先取消当前任务后再重试。',
548
+ 'Wait for the current task to finish, or cancel it before retrying.')
549
+ };
550
+ }
526
551
  if (/unsupported[_ ]parameter/i.test(text) && /reasoning\.summary|summary/i.test(text)) {
527
552
  return {
528
553
  conclusion: textFor(locale, '这轮没有继续:当前 Codex 模型不支持插件请求的推理摘要参数。', 'This run did not continue: the selected Codex model does not support the requested reasoning summary parameter.'),
@@ -548,6 +573,21 @@
548
573
  };
549
574
  }
550
575
 
576
+ // When the caller signals Codex DID produce an answer (assistantMessage
577
+ // arrived on the stream) but an unrelated exception still escaped to the
578
+ // outer catch — the original 'no usable result' copy is wrong-by-design
579
+ // because the user can see Codex's answer in chat. Surface the real
580
+ // shape: Codex returned, post-processing failed, the answer is preserved.
581
+ if (context.codexReturned) {
582
+ return {
583
+ conclusion: textFor(locale,
584
+ 'Codex 已经返回了结果,但本地处理这一轮时出错了。',
585
+ 'Codex returned a result, but local post-processing of this run failed.'),
586
+ nextStep: textFor(locale,
587
+ '请打开技术详情查看错误。Codex 的回答仍保留在会话中。',
588
+ 'Open Technical Details to inspect the error. Codex\'s answer is preserved in the conversation.')
589
+ };
590
+ }
551
591
  return {
552
592
  conclusion: context.mode === 'ask'
553
593
  ? textFor(locale, '这轮只问不改没有完成:本地 Codex 没有正常完成,因此没有生成最终说明。', 'This Ask run did not complete: local Codex did not finish normally, so no final answer was generated.')
@@ -582,10 +622,88 @@
582
622
  return {
583
623
  title: textFor(locale, '本轮完成报告', 'Task report'),
584
624
  status: failed ? 'failed' : 'completed',
585
- text: formatHumanReport(report, locale)
625
+ text: formatHumanReport(report, locale),
626
+ structured: buildStructuredHumanReport(report, locale)
586
627
  };
587
628
  }
588
629
 
630
+ /**
631
+ * Thin wrapper that renders just the fallback final-report text for a
632
+ * writeback `apply` payload. Used by tests and by callers that only need the
633
+ * skipped-block formatting without the full completion-report envelope.
634
+ * @param {{ apply: { applied?: any[], skipped?: any[] }, locale?: string, includeWriteResult?: boolean, status?: string }} input
635
+ * @returns {string}
636
+ */
637
+ function formatFallbackFinalReport(input = {}) {
638
+ const locale = normalizeLocale(input);
639
+ const apply = (input && input.apply) || {};
640
+ const report = buildHumanCompletionReport({
641
+ locale,
642
+ status: input.status || 'failed',
643
+ operations: [],
644
+ applyResults: [apply],
645
+ includeWriteResult: input.includeWriteResult !== false,
646
+ undoCount: 0
647
+ });
648
+ return report.text;
649
+ }
650
+
651
+ /**
652
+ * Splits the human report into a conclusion (the human-language outcome),
653
+ * a body of list-style content sections (checked / findings / planned /
654
+ * changes / skipped), and a meta block of run-metadata key/value rows
655
+ * (unchanged reason, write result, undo, next). The renderer uses this to
656
+ * visually demote the meta block — it is system info about the run, not
657
+ * part of Codex's answer. `formatHumanReport` retains the legacy flat-text
658
+ * shape for transcripts, storage fallback, and existing assertions.
659
+ */
660
+ function buildStructuredHumanReport(report = {}, locale = 'zh') {
661
+ const conclusion = cleanVisibleMarkdownText(report.conclusion || '');
662
+
663
+ const bodySections = [];
664
+ addListSection(bodySections, textFor(locale, '检查范围', 'Checked'), report.checked, locale);
665
+ addListSection(bodySections, textFor(locale, '发现', 'Findings'), report.findings, locale);
666
+ addListSection(bodySections, textFor(locale, '计划修改', 'Planned changes'), report.plannedChanges, locale);
667
+ addListSection(bodySections, textFor(locale, '修改', 'Changes'), report.appliedChanges, locale);
668
+ addStructuredListSection(bodySections, textFor(locale, '跳过原因', 'Skipped'), report.skippedChanges, locale);
669
+
670
+ const meta = [];
671
+ const unchangedReason = localizeVisibleReason(report.unchangedReason || '', locale);
672
+ if (unchangedReason) {
673
+ meta.push({
674
+ key: 'unchangedReason',
675
+ label: textFor(locale, '未修改原因', 'Why nothing changed'),
676
+ value: unchangedReason
677
+ });
678
+ }
679
+ const writeResult = cleanVisibleText(report.writeResult || '');
680
+ if (writeResult) {
681
+ meta.push({
682
+ key: 'writeResult',
683
+ label: textFor(locale, '写入结果', 'Write result'),
684
+ value: writeResult
685
+ });
686
+ }
687
+ const undo = cleanVisibleText(report.undo || '');
688
+ if (undo) {
689
+ meta.push({
690
+ key: 'undo',
691
+ label: textFor(locale, '可撤销', 'Undo'),
692
+ value: undo
693
+ });
694
+ }
695
+ const nextStep = cleanVisibleText(report.nextStep || '');
696
+ if (nextStep) {
697
+ meta.push({
698
+ key: 'nextStep',
699
+ label: textFor(locale, '下一步', 'Next'),
700
+ value: nextStep
701
+ });
702
+ }
703
+
704
+ return { conclusion, body: bodySections.join('\n\n'), meta };
705
+ }
706
+
589
707
  function formatHumanReport(report = {}, locale = 'zh') {
590
708
  const sections = [];
591
709
  const conclusion = cleanVisibleMarkdownText(report.conclusion || '');
@@ -604,7 +722,7 @@
604
722
  if (writeResult) {
605
723
  sections.push(textFor(locale, `写入结果:${writeResult}`, `Write result: ${writeResult}`));
606
724
  }
607
- addListSection(sections, textFor(locale, '跳过原因', 'Skipped'), report.skippedChanges, locale);
725
+ addStructuredListSection(sections, textFor(locale, '跳过原因', 'Skipped'), report.skippedChanges, locale);
608
726
  const undo = cleanVisibleText(report.undo || '');
609
727
  if (undo) {
610
728
  sections.push(textFor(locale, `可撤销:${undo}`, `Undo: ${undo}`));
@@ -640,10 +758,36 @@
640
758
  ? formatWriteResult(counts.appliedCount, counts.skippedCount, locale)
641
759
  : ''),
642
760
  undo: input.undo,
643
- nextStep: input.nextStep || counts.translatedError?.nextStep || formatFallbackNextStep(input, counts.skippedCount, affectedFiles, locale)
761
+ nextStep: input.nextStep
762
+ || counts.translatedError?.nextStep
763
+ || formatPrimaryFailureNextStep(input.applyResults, locale)
764
+ || formatFallbackNextStep(input, counts.skippedCount, affectedFiles, locale)
644
765
  };
645
766
  }
646
767
 
768
+ /**
769
+ * Derive the run-level next-step from the highest-priority skipped failure
770
+ * via `selectPrimaryFailure`. Returns '' when no usable primary failure
771
+ * exists, so callers can fall through to the generic copy.
772
+ */
773
+ function formatPrimaryFailureNextStep(applyResults, locale) {
774
+ const failureReasons = getFailureReasonsModule();
775
+ if (!failureReasons || !failureReasons.selectPrimaryFailure || !failureReasons.normalizeFailureReason) {
776
+ return '';
777
+ }
778
+ const skipped = collectSkippedOperations(applyResults);
779
+ if (!skipped.length) {
780
+ return '';
781
+ }
782
+ const failures = skipped.map(item =>
783
+ failureReasons.normalizeFailureReason(item.result, item.operation || {}, { locale })
784
+ ).filter(Boolean);
785
+ const primary = failureReasons.selectPrimaryFailure(failures);
786
+ if (!primary) return '';
787
+ const localized = failureReasons.localizeFailureReason(primary, locale, failureI18nLookup(locale));
788
+ return localized.nextAction || primary.nextAction || '';
789
+ }
790
+
647
791
  function formatWriteResult(appliedCount, skippedCount, locale = 'zh') {
648
792
  return textFor(
649
793
  locale,
@@ -919,6 +1063,27 @@
919
1063
  sections.push(`${label}${locale === 'en' ? ':' : ':'}\n${items.map(item => `- ${item}`).join('\n')}`);
920
1064
  }
921
1065
 
1066
+ /**
1067
+ * Same as `addListSection` but preserves item-internal newlines so that
1068
+ * structured FailureReason blocks (Reason/Stage/Code/Next) survive into the
1069
+ * rendered report. Per-item first line gets the `- ` bullet; subsequent
1070
+ * lines pass through verbatim (the formatter already indents them).
1071
+ */
1072
+ function addStructuredListSection(sections, label, values, locale = 'zh') {
1073
+ const items = normalizeMultilineStringList(values);
1074
+ if (!items.length) {
1075
+ return;
1076
+ }
1077
+ sections.push(`${label}${locale === 'en' ? ':' : ':'}\n${items.map(item => `- ${item}`).join('\n')}`);
1078
+ }
1079
+
1080
+ function normalizeMultilineStringList(value) {
1081
+ const values = Array.isArray(value) ? value : (value ? [value] : []);
1082
+ return values
1083
+ .map(item => cleanVisibleMarkdownText(item))
1084
+ .filter(Boolean);
1085
+ }
1086
+
922
1087
  function normalizeStringList(value) {
923
1088
  const values = Array.isArray(value) ? value : (value ? [value] : []);
924
1089
  return values
@@ -1034,12 +1199,107 @@
1034
1199
  const operation = item?.operation || {};
1035
1200
  const labels = OPERATION_LABELS[locale] || OPERATION_LABELS.zh;
1036
1201
  const label = labels[operation.type] || operation.type || textFor(locale, '处理', 'process');
1037
- const filePath = operation.path || operation.from || operation.to || textFor(locale, '未知文件', 'unknown file');
1038
1202
  const result = item?.result || {};
1039
- const reason = formatSkippedReason(result, operation, locale);
1203
+ const filePath = operation.path || operation.from || operation.to || (
1204
+ result?.failure?.stage === 'write' || result?.code === 'editor_project_id_unavailable' || result?.code === 'aborted_project_changed'
1205
+ ? textFor(locale, '写入流程', 'writeback process')
1206
+ : textFor(locale, '未知文件', 'unknown file')
1207
+ );
1208
+ const headerLine = locale === 'en'
1209
+ ? `${filePath}: ${label} was not written`
1210
+ : `${filePath}:${label}没有写入`;
1211
+ const block = formatFailureBlockForResult(result, operation, locale);
1212
+ if (block) {
1213
+ return `${headerLine}\n${block}`;
1214
+ }
1215
+ // Last-resort fallback: legacy parenthesized form, only when neither
1216
+ // a structured failure nor the normalizer surfaces a usable record.
1217
+ const legacyReason = formatSkippedReason(result, operation, locale);
1040
1218
  return locale === 'en'
1041
- ? `${filePath}: ${label} was not written (${reason})`
1042
- : `${filePath}:${label}没有写入(${reason})`;
1219
+ ? `${headerLine} (${legacyReason})`
1220
+ : `${headerLine}(${legacyReason})`;
1221
+ }
1222
+
1223
+ /**
1224
+ * Render a `FailureReason` (already localized) as a four-line indented block:
1225
+ * <indent>Reason: <userMessage>
1226
+ * <indent>Stage: <stage>
1227
+ * <indent>Code: <code>
1228
+ * <indent>Next: <nextAction> (only when present)
1229
+ * The `Next` line is omitted when the failure has no `nextAction`.
1230
+ * @param {{ userMessage: string, stage: string, code: string, nextAction?: string }} failure
1231
+ * @param {string} locale - 'en' or 'zh'.
1232
+ * @param {string} [indent=' '] - String prefix applied to every line.
1233
+ * @returns {string}
1234
+ */
1235
+ function formatFailureBlock(failure, locale, indent) {
1236
+ const pad = indent === undefined ? ' ' : indent;
1237
+ const i18nModule = getI18nModule();
1238
+ const headingKey = label => (
1239
+ i18nModule && i18nModule.t ? i18nModule.t(locale, label) : null
1240
+ ) || defaultSectionHeading(label, locale);
1241
+ const sectionHeading = headingKey('failureReason_section_heading');
1242
+ const sectionStage = headingKey('failureReason_section_stage');
1243
+ const sectionCode = headingKey('failureReason_section_code');
1244
+ const sectionNext = headingKey('failureReason_section_next');
1245
+ const lines = [];
1246
+ lines.push(`${pad}${sectionHeading}: ${failure.userMessage || ''}`);
1247
+ lines.push(`${pad}${sectionStage}: ${failure.stage || ''}`);
1248
+ lines.push(`${pad}${sectionCode}: ${failure.code || ''}`);
1249
+ if (failure.nextAction) {
1250
+ lines.push(`${pad}${sectionNext}: ${failure.nextAction}`);
1251
+ }
1252
+ return lines.join('\n');
1253
+ }
1254
+
1255
+ function defaultSectionHeading(key, locale) {
1256
+ const en = {
1257
+ failureReason_section_heading: 'Reason',
1258
+ failureReason_section_stage: 'Stage',
1259
+ failureReason_section_code: 'Code',
1260
+ failureReason_section_next: 'Next'
1261
+ };
1262
+ const zh = {
1263
+ failureReason_section_heading: '原因',
1264
+ failureReason_section_stage: '阶段',
1265
+ failureReason_section_code: '代码',
1266
+ failureReason_section_next: '下一步'
1267
+ };
1268
+ const dict = locale === 'zh' ? zh : en;
1269
+ return dict[key] || key;
1270
+ }
1271
+
1272
+ /**
1273
+ * Build the localized FailureReason block for a skipped writeback item.
1274
+ * Returns '' when the failureReasons module is unavailable, which makes
1275
+ * callers fall back to the legacy parenthesized form.
1276
+ */
1277
+ function formatFailureBlockForResult(result, operation, locale) {
1278
+ const failureReasons = getFailureReasonsModule();
1279
+ if (!failureReasons || !failureReasons.normalizeFailureReason) {
1280
+ return '';
1281
+ }
1282
+ const failure = failureReasons.normalizeFailureReason(result, operation || {}, { locale });
1283
+ if (!failure || !failure.code) {
1284
+ return '';
1285
+ }
1286
+ const localized = failureReasons.localizeFailureReason(failure, locale, failureI18nLookup(locale));
1287
+ const rendered = Object.assign({}, failure, {
1288
+ userMessage: localized.userMessage || failure.userMessage,
1289
+ nextAction: localized.nextAction || failure.nextAction
1290
+ });
1291
+ return formatFailureBlock(rendered, locale, ' ');
1292
+ }
1293
+
1294
+ function failureI18nLookup(locale) {
1295
+ return function lookup(key) {
1296
+ const i18nModule = getI18nModule();
1297
+ if (!i18nModule || !i18nModule.t) return undefined;
1298
+ const localized = i18nModule.t(locale, key);
1299
+ // i18n.t returns the key itself on miss; treat that as miss so the
1300
+ // catalog fallback wins for codes without bespoke localization.
1301
+ return localized && localized !== key ? localized : undefined;
1302
+ };
1043
1303
  }
1044
1304
 
1045
1305
  function formatSkippedReason(result = {}, operation = {}, locale = 'zh') {
@@ -1204,6 +1464,9 @@
1204
1464
 
1205
1465
  return {
1206
1466
  buildHumanCompletionReport,
1467
+ buildStructuredHumanReport,
1468
+ formatFallbackFinalReport,
1469
+ formatFailureBlock,
1207
1470
  formatHumanReport,
1208
1471
  mapAgentEventToActivity,
1209
1472
  translateRawError
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '1.3.6';
15
+ const BUILD_TARGET_VERSION = '1.3.8';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',