difit 4.0.5 → 4.0.6

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 (171) hide show
  1. package/README.ja.md +1 -0
  2. package/README.ko.md +1 -0
  3. package/README.md +1 -0
  4. package/README.zh.md +1 -0
  5. package/dist/cli/comment.d.ts +2 -0
  6. package/dist/cli/comment.js +91 -0
  7. package/dist/cli/comment.test.d.ts +1 -0
  8. package/dist/cli/comment.test.js +164 -0
  9. package/dist/cli/index.js +103 -12
  10. package/dist/cli/utils.d.ts +1 -0
  11. package/dist/cli/utils.js +7 -0
  12. package/dist/client/assets/{_baseFor-DKyA49xd.js → _baseFor-Dq1lbcoh.js} +1 -1
  13. package/dist/client/assets/{arc-COOp7iVe.js → arc-1g1LrDb3.js} +1 -1
  14. package/dist/client/assets/architecture-YZFGNWBL-MZfdAdY6.js +1 -0
  15. package/dist/client/assets/architectureDiagram-Q4EWVU46-D87-Rmwy.js +36 -0
  16. package/dist/client/assets/{blockDiagram-DXYQGD6D-CtNJnEWN.js → blockDiagram-DXYQGD6D-Cep-MIFv.js} +1 -1
  17. package/dist/client/assets/{c4Diagram-AHTNJAMY-BqG-1m6C.js → c4Diagram-AHTNJAMY-BQuH9Txx.js} +1 -1
  18. package/dist/client/assets/channel-CJqLEVLU.js +1 -0
  19. package/dist/client/assets/{chunk-2KRD3SAO-DqP2NJNd.js → chunk-2KRD3SAO-CpQQpmvx.js} +1 -1
  20. package/dist/client/assets/chunk-336JU56O-Ddk9EzgO.js +2 -0
  21. package/dist/client/assets/chunk-426QAEUC-2xhUznDE.js +1 -0
  22. package/dist/client/assets/{chunk-4BX2VUAB-BT78EnQ6.js → chunk-4BX2VUAB-Ca-N0Wd9.js} +1 -1
  23. package/dist/client/assets/{chunk-4TB4RGXK-C4w_Bwzw.js → chunk-4TB4RGXK-ZTWP_Onw.js} +2 -2
  24. package/dist/client/assets/{chunk-55IACEB6-z3MQSTaj.js → chunk-55IACEB6-Dub40zHG.js} +1 -1
  25. package/dist/client/assets/{chunk-5FUZZQ4R-Chei69aj.js → chunk-5FUZZQ4R-Cgda0gtZ.js} +1 -1
  26. package/dist/client/assets/{chunk-5PVQY5BW-HgRiIs0X.js → chunk-5PVQY5BW-D8JPH_tm.js} +1 -1
  27. package/dist/client/assets/{chunk-67CJDMHE-B2q10-fp.js → chunk-67CJDMHE-U1KyLHzG.js} +1 -1
  28. package/dist/client/assets/{chunk-7N4EOEYR-DPgxysWq.js → chunk-7N4EOEYR-WzOy51nD.js} +1 -1
  29. package/dist/client/assets/{chunk-AA7GKIK3-BqmVmKLq.js → chunk-AA7GKIK3-DlWOj4lr.js} +1 -1
  30. package/dist/client/assets/{chunk-BSJP7CBP-CaIgleFn.js → chunk-BSJP7CBP-CcZ0op08.js} +1 -1
  31. package/dist/client/assets/{chunk-CIAEETIT-ByD-tlNF.js → chunk-CIAEETIT-qVSphnw5.js} +1 -1
  32. package/dist/client/assets/{chunk-EDXVE4YY-d3RUKKAj.js → chunk-EDXVE4YY-76SPH4sf.js} +1 -1
  33. package/dist/client/assets/{chunk-ENJZ2VHE-CNq5Qmg9.js → chunk-ENJZ2VHE-CKULNIzL.js} +1 -1
  34. package/dist/client/assets/{chunk-FMBD7UC4-DYfHJ6MV.js → chunk-FMBD7UC4-CvDPP3mb.js} +1 -1
  35. package/dist/client/assets/{chunk-FOC6F5B3-BRpSWlZj.js → chunk-FOC6F5B3-DceW0hWA.js} +1 -1
  36. package/dist/client/assets/{chunk-ICPOFSXX-B_MThwG6.js → chunk-ICPOFSXX-ChGBNZMk.js} +2 -2
  37. package/dist/client/assets/{chunk-K5T4RW27-DmamW1Ds.js → chunk-K5T4RW27-DBHdC4ln.js} +10 -10
  38. package/dist/client/assets/{chunk-KGLVRYIC-CRbg4c4z.js → chunk-KGLVRYIC-DRS7yiGQ.js} +1 -1
  39. package/dist/client/assets/{chunk-LIHQZDEY-CHQPSdB3.js → chunk-LIHQZDEY-KsE8dyJP.js} +1 -1
  40. package/dist/client/assets/{chunk-ORNJ4GCN-CIsQ4Zi4.js → chunk-ORNJ4GCN-Dnp4oHRD.js} +1 -1
  41. package/dist/client/assets/{chunk-OYMX7WX6-Cxi0kdGg.js → chunk-OYMX7WX6-CciaotDu.js} +1 -1
  42. package/dist/client/assets/chunk-QZHKN3VN-BiVE5u_E.js +1 -0
  43. package/dist/client/assets/{chunk-U2HBQHQK-V_hneCfR.js → chunk-U2HBQHQK-nbp7CjBP.js} +1 -1
  44. package/dist/client/assets/{chunk-X2U36JSP-De4pvO-I.js → chunk-X2U36JSP-Chs85loT.js} +1 -1
  45. package/dist/client/assets/{chunk-XPW4576I-B_osXKp6.js → chunk-XPW4576I-VtI9b561.js} +1 -1
  46. package/dist/client/assets/{chunk-YZCP3GAM-C_kqXssD.js → chunk-YZCP3GAM-sBsewSoO.js} +1 -1
  47. package/dist/client/assets/{chunk-ZZ45TVLE-B_xtlma5.js → chunk-ZZ45TVLE-TMgeW_px.js} +1 -1
  48. package/dist/client/assets/classDiagram-6PBFFD2Q-CfyHazmg.js +1 -0
  49. package/dist/client/assets/classDiagram-v2-HSJHXN6E-D7Rb-bnu.js +1 -0
  50. package/dist/client/assets/clone-8xC1huEg.js +1 -0
  51. package/dist/client/assets/cose-bilkent-S5V4N54A-5TzM3w9g.js +1 -0
  52. package/dist/client/assets/{cytoscape.esm-DRReFUEO.js → cytoscape.esm-DdcHPZAZ.js} +2 -2
  53. package/dist/client/assets/{dagre-KV5264BT-BWYGReXF.js → dagre-KV5264BT-xvyFOxd3.js} +1 -1
  54. package/dist/client/assets/{dagre-DU-XBdcU.js → dagre-sb6WtN4K.js} +1 -1
  55. package/dist/client/assets/{diagram-5BDNPKRD-DpUUhvWz.js → diagram-5BDNPKRD-ChRpAe5p.js} +1 -1
  56. package/dist/client/assets/{diagram-G4DWMVQ6-BJoTrUAx.js → diagram-G4DWMVQ6-C_8BED4A.js} +1 -1
  57. package/dist/client/assets/{diagram-MMDJMWI5-CAk1GW5g.js → diagram-MMDJMWI5-BMwXEou2.js} +1 -1
  58. package/dist/client/assets/{diagram-TYMM5635-Cct6g7FA.js → diagram-TYMM5635-CeAkx82D.js} +1 -1
  59. package/dist/client/assets/{dist-61sCfOmN.js → dist-CwC9dd2Z.js} +1 -1
  60. package/dist/client/assets/{erDiagram-SMLLAGMA-DHs2bXUj.js → erDiagram-SMLLAGMA-yGCTeXGt.js} +1 -1
  61. package/dist/client/assets/{flatten-mnWyE-RB.js → flatten-SRIRKgqP.js} +1 -1
  62. package/dist/client/assets/{flowDiagram-DWJPFMVM-DLu-6dfC.js → flowDiagram-DWJPFMVM-CugkvbmM.js} +1 -1
  63. package/dist/client/assets/ganttDiagram-T4ZO3ILL-BXnlBFgK.js +292 -0
  64. package/dist/client/assets/gitGraph-7Q5UKJZL-BeTWkPrd.js +1 -0
  65. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-Bc_rL3_k.js → gitGraphDiagram-UUTBAWPF-B61aCwwu.js} +1 -1
  66. package/dist/client/assets/{graphlib-BVMK0xYE.js → graphlib-BMWKz3zT.js} +1 -1
  67. package/dist/client/assets/index-D2Y8-unG.css +2 -0
  68. package/dist/client/assets/index-D9v_eYzS.js +79 -0
  69. package/dist/client/assets/info-OMHHGYJF-MUNR2tTt.js +1 -0
  70. package/dist/client/assets/{infoDiagram-42DDH7IO-Cf8u4jgP.js → infoDiagram-42DDH7IO-Bkh6nTL2.js} +1 -1
  71. package/dist/client/assets/{isEmpty-CiiIHfXR.js → isEmpty-CStpjy4G.js} +1 -1
  72. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-7n7DvfEb.js → ishikawaDiagram-UXIWVN3A-D_fdVT6_.js} +1 -1
  73. package/dist/client/assets/{journeyDiagram-VCZTEJTY-BMkeQqJb.js → journeyDiagram-VCZTEJTY-DkXVokNF.js} +1 -1
  74. package/dist/client/assets/{kanban-definition-6JOO6SKY-B8KkeZLS.js → kanban-definition-6JOO6SKY-y8qq7qvL.js} +1 -1
  75. package/dist/client/assets/{line-CVpcI6kj.js → line-B0LcTqNY.js} +1 -1
  76. package/dist/client/assets/{linear-DmhiOOKU.js → linear-CqIjr2qp.js} +1 -1
  77. package/dist/client/assets/mermaid-parser.core-Du6QzpZO.js +4 -0
  78. package/dist/client/assets/{mermaid.core-R7nXpPx-.js → mermaid.core-CZBu-oKJ.js} +3 -3
  79. package/dist/client/assets/{mindmap-definition-QFDTVHPH-CwcHocMZ.js → mindmap-definition-QFDTVHPH-BJrRxSkM.js} +1 -1
  80. package/dist/client/assets/{ordinal-k--hYEme.js → ordinal-DIg8h6NI.js} +1 -1
  81. package/dist/client/assets/packet-4T2RLAQJ-Ci-Uu57s.js +1 -0
  82. package/dist/client/assets/pie-ZZUOXDRM-pm57XGIg.js +1 -0
  83. package/dist/client/assets/{pieDiagram-DEJITSTG-BVAn8Lmr.js → pieDiagram-DEJITSTG-Debmhc0u.js} +1 -1
  84. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-C2XZ_zxa.js → quadrantDiagram-34T5L4WZ-SE3g2BC9.js} +1 -1
  85. package/dist/client/assets/radar-PYXPWWZC-CH-AuSDw.js +1 -0
  86. package/dist/client/assets/{reduce-BTlHjXna.js → reduce-CG4cgj93.js} +1 -1
  87. package/dist/client/assets/{requirementDiagram-MS252O5E-CfO16pkI.js → requirementDiagram-MS252O5E-1mv41puC.js} +1 -1
  88. package/dist/client/assets/{sankeyDiagram-XADWPNL6-D_4_234M.js → sankeyDiagram-XADWPNL6-CLjPRtOP.js} +1 -1
  89. package/dist/client/assets/{sequenceDiagram-FGHM5R23-B-yHKMuK.js → sequenceDiagram-FGHM5R23-Cs-P3AtR.js} +1 -1
  90. package/dist/client/assets/src-5XpQHeIJ.js +1 -0
  91. package/dist/client/assets/{stateDiagram-FHFEXIEX-BeG2di4I.js → stateDiagram-FHFEXIEX-CmB1fohY.js} +1 -1
  92. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-D6jsrR-f.js +1 -0
  93. package/dist/client/assets/{timeline-definition-GMOUNBTQ-DhtnMGcE.js → timeline-definition-GMOUNBTQ-BMUafJOI.js} +1 -1
  94. package/dist/client/assets/treeView-SZITEDCU-BGsVMAdJ.js +1 -0
  95. package/dist/client/assets/treemap-W4RFUUIX-DXnhegXy.js +1 -0
  96. package/dist/client/assets/{vennDiagram-DHZGUBPP-CBn69TcQ.js → vennDiagram-DHZGUBPP-CpZ1Qhjz.js} +1 -1
  97. package/dist/client/assets/wardley-RL74JXVD-COd5nWj-.js +1 -0
  98. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-CEoSJmN1.js → wardleyDiagram-NUSXRM2D-C-zH0lsd.js} +1 -1
  99. package/dist/client/assets/{xychartDiagram-5P7HB3ND-BZ_X9tkn.js → xychartDiagram-5P7HB3ND-SkLFuEHZ.js} +1 -1
  100. package/dist/client/index.html +2 -4
  101. package/dist/client/site-data/blobs/080c0e6/cHVibGljL3NpdGUtZGF0YS9vZy1pbWFnZS5wbmc.png +0 -0
  102. package/dist/client/site-data/blobs/55f23a1/bGFuZGluZy9wdWJsaWMvZGlmaXQvbG9nby5wbmc.png +0 -0
  103. package/dist/client/site-data/blobs/66ff7c6/cHVibGljL2xvZ28ucG5n.png +0 -0
  104. package/dist/client/site-data/blobs/e6977fe/cHVibGljL2xvZ28ucG5n.png +0 -0
  105. package/dist/client/site-data/og-image.png +0 -0
  106. package/dist/server/file-watcher.d.ts +2 -1
  107. package/dist/server/file-watcher.js +9 -3
  108. package/dist/server/git-diff.d.ts +5 -0
  109. package/dist/server/git-diff.js +65 -1
  110. package/dist/server/git-diff.test.js +50 -0
  111. package/dist/server/server.js +265 -68
  112. package/dist/server/server.test.js +228 -0
  113. package/dist/tui/App.js +0 -1
  114. package/dist/types/diff.d.ts +4 -4
  115. package/dist/types/watch.d.ts +30 -1
  116. package/dist/utils/commentImports.d.ts +2 -0
  117. package/dist/utils/commentImports.js +119 -1
  118. package/dist/utils/editorOptions.d.ts +58 -35
  119. package/dist/utils/editorOptions.js +150 -24
  120. package/dist/utils/editorOptions.test.js +201 -9
  121. package/package.json +7 -4
  122. package/dist/client/assets/architecture-YZFGNWBL-Cs2Q6RQP.js +0 -1
  123. package/dist/client/assets/architectureDiagram-Q4EWVU46-BO4dVPUA.js +0 -36
  124. package/dist/client/assets/channel-_xDT1u3-.js +0 -1
  125. package/dist/client/assets/chunk-336JU56O-D1qa7Qzb.js +0 -2
  126. package/dist/client/assets/chunk-426QAEUC-6J_A_wvD.js +0 -1
  127. package/dist/client/assets/chunk-CFjPhJqf.js +0 -1
  128. package/dist/client/assets/chunk-QZHKN3VN-C0QzfgZ8.js +0 -1
  129. package/dist/client/assets/classDiagram-6PBFFD2Q-5XrS-DAQ.js +0 -1
  130. package/dist/client/assets/classDiagram-v2-HSJHXN6E-Covl2vKy.js +0 -1
  131. package/dist/client/assets/clone-rhRH8pyW.js +0 -1
  132. package/dist/client/assets/cose-bilkent-S5V4N54A-BvXFc7Rr.js +0 -1
  133. package/dist/client/assets/ganttDiagram-T4ZO3ILL-CMIzlKAR.js +0 -292
  134. package/dist/client/assets/gitGraph-7Q5UKJZL-A_wWsXju.js +0 -1
  135. package/dist/client/assets/index-BPoqJmrs.js +0 -79
  136. package/dist/client/assets/index-Cq_APK7Y.css +0 -2
  137. package/dist/client/assets/info-OMHHGYJF-Bv3kK2Bb.js +0 -1
  138. package/dist/client/assets/mermaid-parser.core-CnJ9Tv8l.js +0 -4
  139. package/dist/client/assets/packet-4T2RLAQJ-D2q3-9ae.js +0 -1
  140. package/dist/client/assets/pie-ZZUOXDRM-GivlQcUF.js +0 -1
  141. package/dist/client/assets/preload-helper-DSXbuxSR.js +0 -1
  142. package/dist/client/assets/radar-PYXPWWZC-C9pD6VNR.js +0 -1
  143. package/dist/client/assets/src-CjDs0_Ij.js +0 -1
  144. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-DvcSq7KE.js +0 -1
  145. package/dist/client/assets/treeView-SZITEDCU-BSNk8_yV.js +0 -1
  146. package/dist/client/assets/treemap-W4RFUUIX-ym4zQztE.js +0 -1
  147. package/dist/client/assets/wardley-RL74JXVD-B02H6ReJ.js +0 -1
  148. /package/dist/client/assets/{array-BNor45A1.js → array-DOVTz2Mq.js} +0 -0
  149. /package/dist/client/assets/{defaultLocale-DPzUsThw.js → defaultLocale-Ck2Xxk-C.js} +0 -0
  150. /package/dist/client/assets/{init-C0L3woqb.js → init-Bft5Ffpj.js} +0 -0
  151. /package/dist/client/assets/{katex-FOM3xZj7.js → katex-CeIlAR55.js} +0 -0
  152. /package/dist/client/assets/{path-sMK4d_s9.js → path-DfRbCp9y.js} +0 -0
  153. /package/dist/client/assets/{prism-bash-iQBez6et.js → prism-bash-CPkZUJMA.js} +0 -0
  154. /package/dist/client/assets/{prism-clojure-CTkJ-FW_.js → prism-clojure-BpoF2XhX.js} +0 -0
  155. /package/dist/client/assets/{prism-csharp-DAAROvjt.js → prism-csharp-BEk8D1-3.js} +0 -0
  156. /package/dist/client/assets/{prism-dart-CMjMHaBW.js → prism-dart-ByLYrdQB.js} +0 -0
  157. /package/dist/client/assets/{prism-elixir-B9cwzXs0.js → prism-elixir-BZtyIEab.js} +0 -0
  158. /package/dist/client/assets/{prism-haskell-Vgx7BCAm.js → prism-haskell-NAsbeo3V.js} +0 -0
  159. /package/dist/client/assets/{prism-hcl-Du4YC80h.js → prism-hcl-crnGqmVp.js} +0 -0
  160. /package/dist/client/assets/{prism-java-CWuFbfVD.js → prism-java-BovStacA.js} +0 -0
  161. /package/dist/client/assets/{prism-markup-templating-h9TC-ifW.js → prism-markup-templating-Cl8NiLjy.js} +0 -0
  162. /package/dist/client/assets/{prism-nix-CqauNIYa.js → prism-nix-BS_cm_1n.js} +0 -0
  163. /package/dist/client/assets/{prism-perl-DhcRwJzx.js → prism-perl-DGLVMq5H.js} +0 -0
  164. /package/dist/client/assets/{prism-php-DcBIrISj.js → prism-php-BskSwJN8.js} +0 -0
  165. /package/dist/client/assets/{prism-protobuf-DuPg7Jbg.js → prism-protobuf-DfbIYpO7.js} +0 -0
  166. /package/dist/client/assets/{prism-ruby-lhDmuasn.js → prism-ruby-FBVh1PRE.js} +0 -0
  167. /package/dist/client/assets/{prism-scala-YlPat9I4.js → prism-scala--9AfMHPY.js} +0 -0
  168. /package/dist/client/assets/{prism-solidity-C3nR0EVH.js → prism-solidity-BgJNkj1z.js} +0 -0
  169. /package/dist/client/assets/{prism-sql-Cz-8DmQS.js → prism-sql-C9Czmpov.js} +0 -0
  170. /package/dist/client/assets/{prism-vim-C3oukvmk.js → prism-vim-CzUNf0WQ.js} +0 -0
  171. /package/dist/client/assets/{rough.esm-DeLgKbOI.js → rough.esm-Bbn_-PMU.js} +0 -0
@@ -7,9 +7,9 @@ import open from 'open';
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
9
  import { formatCommentsOutput } from '../utils/commentFormatting.js';
10
- import { serializeCommentImports } from '../utils/commentImports.js';
10
+ import { mergeCommentImports, normalizeCommentImports, serializeCommentImports, } from '../utils/commentImports.js';
11
11
  import { normalizeDiffViewMode } from '../utils/diffMode.js';
12
- import { resolveEditorOption } from '../utils/editorOptions.js';
12
+ import { buildEditorSpawnSpec, CUSTOM_EDITOR_ID, NONE_EDITOR_ID, resolveEditorOption, } from '../utils/editorOptions.js';
13
13
  import { getFileExtension } from '../utils/fileUtils.js';
14
14
  import { FileWatcherService } from './file-watcher.js';
15
15
  import { GitDiffParser } from './git-diff.js';
@@ -42,6 +42,15 @@ function setCachedDiffResponse(cache, key, value) {
42
42
  cache.delete(oldestKey);
43
43
  }
44
44
  }
45
+ function createResolvedCommentSelection(responseDiffData, fallbackSelection, stdinDiff) {
46
+ const baseCommitish = responseDiffData.baseCommitish ?? (stdinDiff ? 'stdin' : fallbackSelection.baseCommitish);
47
+ const targetCommitish = responseDiffData.targetCommitish ?? (stdinDiff ? 'stdin' : fallbackSelection.targetCommitish);
48
+ const baseMode = responseDiffData.requestedBaseMode ?? fallbackSelection.baseMode;
49
+ return createDiffSelection(baseCommitish, targetCommitish, baseMode);
50
+ }
51
+ function createCommentSessionKey(selection) {
52
+ return getDiffSelectionKey(selection);
53
+ }
45
54
  export async function startServer(options) {
46
55
  const app = express();
47
56
  const repositoryPath = resolve(options.repoPath ?? process.cwd());
@@ -96,6 +105,7 @@ export async function startServer(options) {
96
105
  };
97
106
  // Track current revisions for cache invalidation
98
107
  let currentSelection = initialSelection;
108
+ let currentCommentSelection = createResolvedCommentSelection(initialDiffData, initialSelection, Boolean(options.stdinDiff));
99
109
  function parseRepositoryRelativePath(filepath) {
100
110
  if (typeof filepath !== 'string' || filepath.length === 0) {
101
111
  return { ok: false, error: 'Invalid file path' };
@@ -111,6 +121,51 @@ export async function startServer(options) {
111
121
  }
112
122
  return { ok: true, path: normalizedFilepath };
113
123
  }
124
+ function parseEditorRequest(value) {
125
+ if (!value || typeof value !== 'object') {
126
+ return { id: undefined, command: undefined, argsTemplate: undefined };
127
+ }
128
+ const candidate = value;
129
+ return {
130
+ id: typeof candidate.id === 'string' ? candidate.id : undefined,
131
+ command: typeof candidate.command === 'string' ? candidate.command : undefined,
132
+ argsTemplate: typeof candidate.argsTemplate === 'string' ? candidate.argsTemplate : undefined,
133
+ };
134
+ }
135
+ const commentSessions = new Map();
136
+ const initialCommentThreads = mergeCommentImports([], initialCommentImports).threads;
137
+ if (initialCommentThreads.length > 0) {
138
+ commentSessions.set(createCommentSessionKey(currentCommentSelection), {
139
+ threads: initialCommentThreads,
140
+ version: 1,
141
+ });
142
+ }
143
+ function getCommentSelectionFromQuery(query) {
144
+ const hasBase = typeof query.base === 'string';
145
+ const hasTarget = typeof query.target === 'string';
146
+ const hasBaseMode = typeof query.baseMode === 'string';
147
+ if (!hasBase && !hasTarget && !hasBaseMode) {
148
+ return currentCommentSelection;
149
+ }
150
+ return createDiffSelection(hasBase ? query.base : currentCommentSelection.baseCommitish, hasTarget ? query.target : currentCommentSelection.targetCommitish, hasBaseMode
151
+ ? parseBaseMode(query.baseMode)
152
+ : hasBase || hasTarget
153
+ ? undefined
154
+ : currentCommentSelection.baseMode);
155
+ }
156
+ function getOrCreateCommentSession(selection) {
157
+ const key = createCommentSessionKey(selection);
158
+ const existing = commentSessions.get(key);
159
+ if (existing) {
160
+ return existing;
161
+ }
162
+ const nextSession = {
163
+ threads: [],
164
+ version: 0,
165
+ };
166
+ commentSessions.set(key, nextSession);
167
+ return nextSession;
168
+ }
114
169
  app.get('/api/diff', async (req, res) => {
115
170
  const ignoreWhitespace = req.query.ignoreWhitespace === 'true';
116
171
  const hasBase = typeof req.query.base === 'string';
@@ -137,6 +192,7 @@ export async function startServer(options) {
137
192
  generatedStatusCache.clear();
138
193
  }
139
194
  }
195
+ currentCommentSelection = createResolvedCommentSelection(responseDiffData, requestedSelection, Boolean(options.stdinDiff));
140
196
  const baseCommitish = responseDiffData.baseCommitish ?? (options.stdinDiff ? 'stdin' : undefined);
141
197
  const targetCommitish = responseDiffData.targetCommitish ?? (options.stdinDiff ? 'stdin' : undefined);
142
198
  const requestedBaseCommitish = responseDiffData.requestedBaseCommitish ??
@@ -314,31 +370,58 @@ export async function startServer(options) {
314
370
  res.status(404).json({ error: 'File not found' });
315
371
  }
316
372
  });
317
- let finalThreads = [];
373
+ function normalizeLineValue(line) {
374
+ if (Array.isArray(line) && line.length === 2) {
375
+ const start = line[0];
376
+ const end = line[1];
377
+ if (typeof start === 'number' &&
378
+ typeof end === 'number' &&
379
+ Number.isInteger(start) &&
380
+ Number.isInteger(end) &&
381
+ start > 0 &&
382
+ end > 0 &&
383
+ start <= end) {
384
+ return { start, end };
385
+ }
386
+ }
387
+ if (typeof line === 'number' && Number.isInteger(line) && line > 0) {
388
+ return line;
389
+ }
390
+ return 1;
391
+ }
318
392
  function normalizeComment(comment) {
393
+ const now = new Date().toISOString();
394
+ const timestamp = typeof comment.timestamp === 'string' ? comment.timestamp : now;
395
+ const threadId = typeof comment.id === 'string' && comment.id.length > 0
396
+ ? comment.id
397
+ : createHash('sha256').update(JSON.stringify(comment)).digest('hex').slice(0, 12);
398
+ const filePath = typeof comment.file === 'string' && comment.file.length > 0 ? comment.file : '<unknown file>';
319
399
  return {
320
- id: comment.id,
321
- file: comment.file,
322
- line: comment.line,
323
- side: comment.side,
324
- createdAt: comment.timestamp,
325
- updatedAt: comment.timestamp,
326
- codeContent: comment.codeContent,
400
+ id: threadId,
401
+ filePath,
402
+ createdAt: timestamp,
403
+ updatedAt: timestamp,
404
+ position: {
405
+ side: comment.side ?? 'new',
406
+ line: normalizeLineValue(comment.line),
407
+ },
408
+ codeSnapshot: typeof comment.codeContent === 'string'
409
+ ? {
410
+ content: comment.codeContent,
411
+ }
412
+ : undefined,
327
413
  messages: [
328
414
  {
329
- id: comment.id,
415
+ id: threadId,
330
416
  body: comment.body,
331
417
  author: comment.author,
332
- createdAt: comment.timestamp,
333
- updatedAt: comment.timestamp,
418
+ createdAt: timestamp,
419
+ updatedAt: timestamp,
334
420
  },
335
421
  ],
336
422
  };
337
423
  }
338
- function normalizeThreadPayload(thread) {
339
- if ('file' in thread && 'line' in thread) {
340
- return thread;
341
- }
424
+ function toCommentThread(thread) {
342
425
  return {
343
426
  id: thread.id,
344
427
  file: thread.filePath,
@@ -352,6 +435,51 @@ export async function startServer(options) {
352
435
  messages: thread.messages,
353
436
  };
354
437
  }
438
+ function normalizeThreadPayload(thread) {
439
+ if ('filePath' in thread && 'position' in thread) {
440
+ return thread;
441
+ }
442
+ const threadId = typeof thread.id === 'string' && thread.id.length > 0
443
+ ? thread.id
444
+ : createHash('sha256').update(JSON.stringify(thread)).digest('hex').slice(0, 12);
445
+ const now = new Date().toISOString();
446
+ const messages = Array.isArray(thread.messages) && thread.messages.length > 0
447
+ ? thread.messages.map((message, index) => ({
448
+ id: typeof message.id === 'string' && message.id.length > 0
449
+ ? message.id
450
+ : `${threadId}:${index}`,
451
+ body: message.body,
452
+ author: message.author,
453
+ createdAt: message.createdAt || thread.createdAt || now,
454
+ updatedAt: message.updatedAt || message.createdAt || thread.updatedAt || now,
455
+ }))
456
+ : [
457
+ {
458
+ id: threadId,
459
+ body: '',
460
+ createdAt: thread.createdAt || now,
461
+ updatedAt: thread.updatedAt || thread.createdAt || now,
462
+ },
463
+ ];
464
+ const firstMessage = messages[0];
465
+ const lastMessage = messages[messages.length - 1];
466
+ return {
467
+ id: threadId,
468
+ filePath: typeof thread.file === 'string' && thread.file.length > 0 ? thread.file : '<unknown file>',
469
+ createdAt: thread.createdAt || firstMessage?.createdAt || now,
470
+ updatedAt: thread.updatedAt || lastMessage?.updatedAt || thread.createdAt || now,
471
+ position: {
472
+ side: thread.side ?? 'new',
473
+ line: normalizeLineValue(thread.line),
474
+ },
475
+ codeSnapshot: typeof thread.codeContent === 'string'
476
+ ? {
477
+ content: thread.codeContent,
478
+ }
479
+ : undefined,
480
+ messages,
481
+ };
482
+ }
355
483
  function parseCommentsPayload(body) {
356
484
  const payload = typeof body === 'string'
357
485
  ? JSON.parse(body)
@@ -364,9 +492,33 @@ export async function startServer(options) {
364
492
  }
365
493
  return [];
366
494
  }
495
+ function parseCommentImportsPayload(body) {
496
+ if (typeof body === 'string') {
497
+ return normalizeCommentImports(JSON.parse(body));
498
+ }
499
+ return normalizeCommentImports(body);
500
+ }
501
+ function updateCommentSession(selection, nextThreads) {
502
+ const session = getOrCreateCommentSession(selection);
503
+ const previous = JSON.stringify(session.threads);
504
+ const next = JSON.stringify(nextThreads);
505
+ session.threads = nextThreads;
506
+ if (previous === next) {
507
+ return false;
508
+ }
509
+ session.version += 1;
510
+ fileWatcher.broadcast({
511
+ type: 'commentsChanged',
512
+ version: session.version,
513
+ timestamp: new Date().toISOString(),
514
+ });
515
+ return true;
516
+ }
367
517
  app.post('/api/comments', (req, res) => {
368
518
  try {
369
- finalThreads = parseCommentsPayload(req.body);
519
+ const selection = getCommentSelectionFromQuery(req.query);
520
+ const nextThreads = parseCommentsPayload(req.body);
521
+ updateCommentSession(selection, nextThreads);
370
522
  res.json({ success: true });
371
523
  }
372
524
  catch (error) {
@@ -374,10 +526,43 @@ export async function startServer(options) {
374
526
  res.status(400).json({ error: 'Invalid comment data' });
375
527
  }
376
528
  });
377
- app.get('/api/comments-output', (_req, res) => {
529
+ app.post('/api/comment-imports', (req, res) => {
530
+ try {
531
+ const selection = getCommentSelectionFromQuery(req.query);
532
+ const session = getOrCreateCommentSession(selection);
533
+ const commentImports = parseCommentImportsPayload(req.body);
534
+ const importId = createHash('sha256')
535
+ .update(serializeCommentImports(commentImports))
536
+ .digest('hex');
537
+ const merged = mergeCommentImports(session.threads, commentImports);
538
+ const changed = updateCommentSession(selection, merged.threads);
539
+ res.json({
540
+ success: true,
541
+ changed,
542
+ count: commentImports.length,
543
+ importId,
544
+ warnings: merged.warnings,
545
+ });
546
+ }
547
+ catch (error) {
548
+ console.error('Error parsing comment imports:', error);
549
+ res.status(400).json({ error: 'Invalid comment import data' });
550
+ }
551
+ });
552
+ app.get('/api/comments-json', (req, res) => {
553
+ const selection = getCommentSelectionFromQuery(req.query);
554
+ const session = getOrCreateCommentSession(selection);
555
+ res.json({
556
+ version: session.version,
557
+ threads: session.threads,
558
+ });
559
+ });
560
+ app.get('/api/comments-output', (req, res) => {
561
+ const selection = getCommentSelectionFromQuery(req.query);
562
+ const session = getOrCreateCommentSession(selection);
378
563
  res.type('text/plain');
379
- if (finalThreads.length > 0) {
380
- const output = formatCommentsOutput(finalThreads);
564
+ if (session.threads.length > 0) {
565
+ const output = formatCommentsOutput(session.threads.map(toCommentThread));
381
566
  res.send(output);
382
567
  }
383
568
  else {
@@ -400,67 +585,79 @@ export async function startServer(options) {
400
585
  return;
401
586
  }
402
587
  const resolvedPath = resolve(repositoryPath, filepathResult.path);
403
- const editorInput = typeof editor === 'string' ? editor : (process.env.DIFIT_EDITOR ?? process.env.EDITOR);
404
- const resolvedEditor = resolveEditorOption(editorInput);
405
- if (resolvedEditor.protocol === null) {
588
+ const editorRequest = parseEditorRequest(editor);
589
+ const editorId = editorRequest.id ?? process.env.DIFIT_EDITOR ?? process.env.EDITOR ?? undefined;
590
+ if (editorId?.toLowerCase() === NONE_EDITOR_ID) {
406
591
  res.status(400).json({ error: 'Open in editor is disabled' });
407
592
  return;
408
593
  }
594
+ // The browser always sends command + argsTemplate in the body, so we use
595
+ // those directly. We only fall back to the preset table when neither is
596
+ // provided (for example, when DIFIT_EDITOR is set and there's no body).
597
+ let command;
598
+ let argsTemplate;
599
+ if (editorRequest.command !== undefined || editorRequest.argsTemplate !== undefined) {
600
+ command = (editorRequest.command ?? '').trim();
601
+ argsTemplate = (editorRequest.argsTemplate ?? '').trim();
602
+ }
603
+ else {
604
+ const preset = resolveEditorOption(editorId);
605
+ command = preset.command;
606
+ argsTemplate = preset.argsTemplate;
607
+ }
608
+ if (!command || !argsTemplate) {
609
+ const isCustom = editorId?.toLowerCase() === CUSTOM_EDITOR_ID;
610
+ res.status(400).json({
611
+ error: isCustom
612
+ ? 'Custom editor is not configured. Set a command and arguments in Settings > System.'
613
+ : 'Open in editor is not configured',
614
+ });
615
+ return;
616
+ }
409
617
  const lineNumber = (() => {
410
618
  const parsed = Number.parseInt(String(line ?? ''), 10);
411
619
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
412
620
  })();
413
- const tryOpenWithCli = async () => {
414
- if (!resolvedEditor.cliCommand)
415
- return false;
416
- const args = [...resolvedEditor.cliArgs];
417
- if (lineNumber !== null) {
418
- const fileWithLine = `${resolvedPath}:${lineNumber}`;
419
- if (resolvedEditor.lineFormat === 'goto-flag') {
420
- args.push('-g', fileWithLine);
421
- }
422
- else {
423
- args.push(fileWithLine);
621
+ const spawnSpec = buildEditorSpawnSpec({
622
+ command,
623
+ argsTemplate,
624
+ filePath: resolvedPath,
625
+ lineNumber,
626
+ });
627
+ if (!spawnSpec) {
628
+ res.status(500).json({ error: 'Invalid editor configuration' });
629
+ return;
630
+ }
631
+ const launched = await new Promise((resolvePromise) => {
632
+ const child = spawn(spawnSpec.command, [...spawnSpec.args], {
633
+ stdio: 'ignore',
634
+ detached: true,
635
+ });
636
+ child.once('error', (error) => {
637
+ const code = error.code;
638
+ if (code && code !== 'ENOENT') {
639
+ console.error('Failed to launch editor CLI:', error);
424
640
  }
425
- }
426
- else {
427
- args.push(resolvedPath);
428
- }
429
- args.push(repositoryPath);
430
- return await new Promise((resolvePromise) => {
431
- const child = spawn(resolvedEditor.cliCommand, args, { stdio: 'ignore', detached: true });
432
- child.once('error', (error) => {
433
- const code = error.code;
434
- if (code && code !== 'ENOENT') {
435
- console.error('Failed to launch editor CLI:', error);
436
- }
437
- resolvePromise(false);
438
- });
439
- child.once('spawn', () => {
440
- child.unref();
441
- resolvePromise(true);
442
- });
641
+ resolvePromise(false);
642
+ });
643
+ child.once('spawn', () => {
644
+ child.unref();
645
+ resolvePromise(true);
646
+ });
647
+ });
648
+ if (!launched) {
649
+ res.status(500).json({
650
+ error: `Failed to launch editor: command "${spawnSpec.command}" is not available on PATH`,
443
651
  });
444
- };
445
- if (await tryOpenWithCli()) {
446
- res.json({ success: true });
447
652
  return;
448
653
  }
449
- const lineSuffix = lineNumber !== null ? `:${lineNumber}` : '';
450
- const fileUri = `${resolvedEditor.protocol}://file${encodeURI(resolvedPath)}${lineSuffix}`;
451
- try {
452
- await open(fileUri);
453
- res.json({ success: true });
454
- }
455
- catch (error) {
456
- console.error('Failed to open file in editor:', error);
457
- res.status(500).json({ error: 'Failed to open file in editor' });
458
- }
654
+ res.json({ success: true });
459
655
  });
460
656
  // Function to output comments when server shuts down
461
657
  function outputFinalComments() {
462
- if (finalThreads.length > 0) {
463
- console.log(formatCommentsOutput(finalThreads));
658
+ const session = getOrCreateCommentSession(currentCommentSelection);
659
+ if (session.threads.length > 0) {
660
+ console.log(formatCommentsOutput(session.threads.map(toCommentThread)));
464
661
  }
465
662
  }
466
663
  // SSE endpoint for file watching
@@ -526,6 +526,234 @@ describe('Server Integration Tests', () => {
526
526
  expect(output).toContain('Multi-line comment');
527
527
  expect(output).toContain('Total comments: 2');
528
528
  });
529
+ it('POST /api/comment-imports accepts valid comment imports', async () => {
530
+ const imports = [
531
+ {
532
+ type: 'thread',
533
+ filePath: 'src/example.ts',
534
+ position: { side: 'new', line: 10 },
535
+ body: 'Review comment',
536
+ },
537
+ ];
538
+ const response = await fetch(`http://localhost:${port}/api/comment-imports`, {
539
+ method: 'POST',
540
+ headers: { 'Content-Type': 'application/json' },
541
+ body: JSON.stringify(imports),
542
+ });
543
+ expect(response.ok).toBe(true);
544
+ const data = (await response.json());
545
+ expect(data.success).toBe(true);
546
+ expect(data.importId).toEqual(expect.any(String));
547
+ expect(data.count).toBe(1);
548
+ });
549
+ it('POST /api/comment-imports accepts a single object', async () => {
550
+ const singleImport = {
551
+ type: 'thread',
552
+ filePath: 'src/example.ts',
553
+ position: { side: 'new', line: 5 },
554
+ body: 'Single object import',
555
+ };
556
+ const response = await fetch(`http://localhost:${port}/api/comment-imports`, {
557
+ method: 'POST',
558
+ headers: { 'Content-Type': 'application/json' },
559
+ body: JSON.stringify(singleImport),
560
+ });
561
+ expect(response.ok).toBe(true);
562
+ const data = (await response.json());
563
+ expect(data.success).toBe(true);
564
+ expect(data.count).toBe(1);
565
+ });
566
+ it('POST /api/comment-imports rejects invalid data', async () => {
567
+ const response = await fetch(`http://localhost:${port}/api/comment-imports`, {
568
+ method: 'POST',
569
+ headers: { 'Content-Type': 'application/json' },
570
+ body: JSON.stringify({ invalid: true }),
571
+ });
572
+ expect(response.status).toBe(400);
573
+ const data = (await response.json());
574
+ expect(data).toHaveProperty('error');
575
+ });
576
+ it('GET /api/comments-json returns empty threads by default', async () => {
577
+ const response = await fetch(`http://localhost:${port}/api/comments-json`);
578
+ expect(response.ok).toBe(true);
579
+ const data = (await response.json());
580
+ expect(data).toHaveProperty('threads');
581
+ expect(data.threads).toEqual([]);
582
+ });
583
+ it('GET /api/comments-json returns threads after posting comments', async () => {
584
+ const comments = [{ file: 'test.js', line: 10, body: 'JSON test comment' }];
585
+ await fetch(`http://localhost:${port}/api/comments`, {
586
+ method: 'POST',
587
+ headers: { 'Content-Type': 'application/json' },
588
+ body: JSON.stringify({ comments }),
589
+ });
590
+ const response = await fetch(`http://localhost:${port}/api/comments-json`);
591
+ expect(response.ok).toBe(true);
592
+ const data = (await response.json());
593
+ expect(data.threads).toHaveLength(1);
594
+ expect(data.threads[0].messages[0].body).toBe('JSON test comment');
595
+ });
596
+ it('POST /api/comment-imports merges into server-side threads for comments-output', async () => {
597
+ const imports = [
598
+ {
599
+ type: 'thread',
600
+ filePath: 'src/example.ts',
601
+ position: { side: 'new', line: 42 },
602
+ body: 'Merged server-side comment',
603
+ },
604
+ ];
605
+ await fetch(`http://localhost:${port}/api/comment-imports`, {
606
+ method: 'POST',
607
+ headers: { 'Content-Type': 'application/json' },
608
+ body: JSON.stringify(imports),
609
+ });
610
+ const outputResponse = await fetch(`http://localhost:${port}/api/comments-output`);
611
+ const output = await outputResponse.text();
612
+ expect(output).toContain('src/example.ts:L42');
613
+ expect(output).toContain('Merged server-side comment');
614
+ });
615
+ it('POST /api/comment-imports merges reply into existing thread', async () => {
616
+ // First add a thread
617
+ await fetch(`http://localhost:${port}/api/comment-imports`, {
618
+ method: 'POST',
619
+ headers: { 'Content-Type': 'application/json' },
620
+ body: JSON.stringify([
621
+ {
622
+ type: 'thread',
623
+ filePath: 'src/reply-test.ts',
624
+ position: { side: 'new', line: 5 },
625
+ body: 'Original comment',
626
+ author: 'User',
627
+ },
628
+ ]),
629
+ });
630
+ // Then add a reply
631
+ await fetch(`http://localhost:${port}/api/comment-imports`, {
632
+ method: 'POST',
633
+ headers: { 'Content-Type': 'application/json' },
634
+ body: JSON.stringify([
635
+ {
636
+ type: 'reply',
637
+ filePath: 'src/reply-test.ts',
638
+ position: { side: 'new', line: 5 },
639
+ body: 'Reply to comment',
640
+ author: 'AI',
641
+ },
642
+ ]),
643
+ });
644
+ const jsonResponse = await fetch(`http://localhost:${port}/api/comments-json`);
645
+ const data = (await jsonResponse.json());
646
+ const thread = data.threads.find((t) => t.filePath === 'src/reply-test.ts');
647
+ expect(thread).toBeDefined();
648
+ expect(thread.messages).toHaveLength(2);
649
+ expect(thread.messages[0].body).toBe('Original comment');
650
+ expect(thread.messages[1].body).toBe('Reply to comment');
651
+ });
652
+ it('POST /api/comment-imports deduplicates identical imports', async () => {
653
+ const imports = [
654
+ {
655
+ type: 'thread',
656
+ filePath: 'src/dedup.ts',
657
+ position: { side: 'new', line: 1 },
658
+ body: 'Unique comment',
659
+ },
660
+ ];
661
+ // Send the same import twice
662
+ await fetch(`http://localhost:${port}/api/comment-imports`, {
663
+ method: 'POST',
664
+ headers: { 'Content-Type': 'application/json' },
665
+ body: JSON.stringify(imports),
666
+ });
667
+ await fetch(`http://localhost:${port}/api/comment-imports`, {
668
+ method: 'POST',
669
+ headers: { 'Content-Type': 'application/json' },
670
+ body: JSON.stringify(imports),
671
+ });
672
+ const jsonResponse = await fetch(`http://localhost:${port}/api/comments-json`);
673
+ const data = (await jsonResponse.json());
674
+ const threads = data.threads.filter((t) => t.filePath === 'src/dedup.ts');
675
+ expect(threads).toHaveLength(1);
676
+ });
677
+ it('isolates comment sessions between different diff selections', async () => {
678
+ const importServer = await startServer({
679
+ selection: { targetCommitish: 'HEAD', baseCommitish: 'HEAD^' },
680
+ preferredPort: 9039,
681
+ commentImports: [
682
+ {
683
+ type: 'thread',
684
+ filePath: 'src/cli/comment.test.ts',
685
+ position: { side: 'new', line: 10 },
686
+ body: 'Startup comment',
687
+ },
688
+ ],
689
+ });
690
+ servers.push(importServer.server);
691
+ const parser = parserInstances.at(-1);
692
+ parser?.parseDiff.mockImplementation(async (selection) => ({
693
+ targetCommit: 'abc123',
694
+ baseCommit: 'def456',
695
+ baseCommitish: selection.baseCommitish === 'HEAD^' ? 'def4567' : selection.baseCommitish,
696
+ targetCommitish: selection.targetCommitish === 'HEAD' ? 'abc1234' : selection.targetCommitish,
697
+ requestedBaseCommitish: selection.baseCommitish,
698
+ requestedTargetCommitish: selection.targetCommitish,
699
+ requestedBaseMode: selection.baseMode,
700
+ targetMessage: 'Test commit',
701
+ baseMessage: 'Previous commit',
702
+ files: [
703
+ {
704
+ path: 'src/cli/comment.test.ts',
705
+ additions: 10,
706
+ deletions: 5,
707
+ chunks: [],
708
+ },
709
+ ],
710
+ stats: { additions: 10, deletions: 5 },
711
+ isEmpty: false,
712
+ }));
713
+ await fetch(`http://localhost:${importServer.port}/api/comment-imports`, {
714
+ method: 'POST',
715
+ headers: { 'Content-Type': 'application/json' },
716
+ body: JSON.stringify([
717
+ {
718
+ type: 'thread',
719
+ filePath: 'src/cli/comment.test.ts',
720
+ position: { side: 'new', line: 20 },
721
+ body: 'API comment',
722
+ },
723
+ ]),
724
+ });
725
+ let response = await fetch(`http://localhost:${importServer.port}/api/comments-output`);
726
+ let output = await response.text();
727
+ expect(output).toContain('Startup comment');
728
+ expect(output).toContain('API comment');
729
+ await fetch(`http://localhost:${importServer.port}/api/diff?base=feat%2F292-comment-read-write&target=codex%2Fcomment-session-state`);
730
+ response = await fetch(`http://localhost:${importServer.port}/api/comments-output`);
731
+ output = await response.text();
732
+ expect(output).toBe('');
733
+ await fetch(`http://localhost:${importServer.port}/api/comment-imports`, {
734
+ method: 'POST',
735
+ headers: { 'Content-Type': 'application/json' },
736
+ body: JSON.stringify([
737
+ {
738
+ type: 'thread',
739
+ filePath: 'src/cli/comment.test.ts',
740
+ position: { side: 'new', line: 30 },
741
+ body: 'Other diff comment',
742
+ },
743
+ ]),
744
+ });
745
+ response = await fetch(`http://localhost:${importServer.port}/api/comments-output`);
746
+ output = await response.text();
747
+ expect(output).toContain('Other diff comment');
748
+ expect(output).not.toContain('Startup comment');
749
+ expect(output).not.toContain('API comment');
750
+ await fetch(`http://localhost:${importServer.port}/api/diff?base=HEAD%5E&target=HEAD`);
751
+ response = await fetch(`http://localhost:${importServer.port}/api/comments-output`);
752
+ output = await response.text();
753
+ expect(output).toContain('Startup comment');
754
+ expect(output).toContain('API comment');
755
+ expect(output).not.toContain('Other diff comment');
756
+ });
529
757
  it.skip('GET /api/heartbeat returns SSE headers', async () => {
530
758
  // Skipped due to connection reset issues in test environment
531
759
  // SSE endpoint functionality is verified through manual testing
package/dist/tui/App.js CHANGED
@@ -28,7 +28,6 @@ const App = ({ selection, mode, repoPath, contextLines }) => {
28
28
  }
29
29
  };
30
30
  useEffect(() => {
31
- // oxlint-disable-next-line react-hooks-js/set-state-in-effect -- intentional: trigger initial diff load when revisions change
32
31
  void loadDiff();
33
32
  // oxlint-disable-next-line react/exhaustive-deps
34
33
  }, [baseCommitish, targetCommitish]);