difit 4.0.5 → 4.0.7

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 (176) 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-DX2p9X2Y.js} +1 -1
  14. package/dist/client/assets/architecture-YZFGNWBL-2zVtKbnG.js +1 -0
  15. package/dist/client/assets/architectureDiagram-Q4EWVU46-FixTWViB.js +36 -0
  16. package/dist/client/assets/{blockDiagram-DXYQGD6D-CtNJnEWN.js → blockDiagram-DXYQGD6D-CUAMgGr9.js} +1 -1
  17. package/dist/client/assets/{c4Diagram-AHTNJAMY-BqG-1m6C.js → c4Diagram-AHTNJAMY-BM_HNNZe.js} +1 -1
  18. package/dist/client/assets/channel-B_ddQhpW.js +1 -0
  19. package/dist/client/assets/{chunk-2KRD3SAO-DqP2NJNd.js → chunk-2KRD3SAO-DeT59g2K.js} +1 -1
  20. package/dist/client/assets/chunk-336JU56O-CaGvJA86.js +2 -0
  21. package/dist/client/assets/chunk-426QAEUC-CMTCMPn4.js +1 -0
  22. package/dist/client/assets/{chunk-4BX2VUAB-BT78EnQ6.js → chunk-4BX2VUAB-D9mNDl5f.js} +1 -1
  23. package/dist/client/assets/{chunk-4TB4RGXK-C4w_Bwzw.js → chunk-4TB4RGXK-Df3b4HEG.js} +2 -2
  24. package/dist/client/assets/{chunk-55IACEB6-z3MQSTaj.js → chunk-55IACEB6-dCWLe_n4.js} +1 -1
  25. package/dist/client/assets/{chunk-5FUZZQ4R-Chei69aj.js → chunk-5FUZZQ4R-EScvXcSN.js} +1 -1
  26. package/dist/client/assets/{chunk-5PVQY5BW-HgRiIs0X.js → chunk-5PVQY5BW-ail-oj89.js} +1 -1
  27. package/dist/client/assets/{chunk-67CJDMHE-B2q10-fp.js → chunk-67CJDMHE-CHeCIL1u.js} +1 -1
  28. package/dist/client/assets/{chunk-7N4EOEYR-DPgxysWq.js → chunk-7N4EOEYR-P0tNRVMZ.js} +1 -1
  29. package/dist/client/assets/{chunk-AA7GKIK3-BqmVmKLq.js → chunk-AA7GKIK3-DloBHWSo.js} +1 -1
  30. package/dist/client/assets/{chunk-BSJP7CBP-CaIgleFn.js → chunk-BSJP7CBP-CGLThsR8.js} +1 -1
  31. package/dist/client/assets/{chunk-CIAEETIT-ByD-tlNF.js → chunk-CIAEETIT-rCt2IEMp.js} +1 -1
  32. package/dist/client/assets/{chunk-EDXVE4YY-d3RUKKAj.js → chunk-EDXVE4YY-DIJEIKIq.js} +1 -1
  33. package/dist/client/assets/{chunk-ENJZ2VHE-CNq5Qmg9.js → chunk-ENJZ2VHE-CdrdxFfV.js} +1 -1
  34. package/dist/client/assets/{chunk-FMBD7UC4-DYfHJ6MV.js → chunk-FMBD7UC4-BH_GgR9u.js} +1 -1
  35. package/dist/client/assets/{chunk-FOC6F5B3-BRpSWlZj.js → chunk-FOC6F5B3-D71VljSN.js} +1 -1
  36. package/dist/client/assets/{chunk-ICPOFSXX-B_MThwG6.js → chunk-ICPOFSXX-2vcQKuhB.js} +2 -2
  37. package/dist/client/assets/{chunk-K5T4RW27-DmamW1Ds.js → chunk-K5T4RW27-BWIFd7pZ.js} +10 -10
  38. package/dist/client/assets/{chunk-KGLVRYIC-CRbg4c4z.js → chunk-KGLVRYIC-Ck8I8tdt.js} +1 -1
  39. package/dist/client/assets/{chunk-LIHQZDEY-CHQPSdB3.js → chunk-LIHQZDEY-Cc7TtI-w.js} +1 -1
  40. package/dist/client/assets/{chunk-ORNJ4GCN-CIsQ4Zi4.js → chunk-ORNJ4GCN-BMSqiphc.js} +1 -1
  41. package/dist/client/assets/{chunk-OYMX7WX6-Cxi0kdGg.js → chunk-OYMX7WX6-B5faFb53.js} +1 -1
  42. package/dist/client/assets/chunk-QZHKN3VN-B-G9G-FB.js +1 -0
  43. package/dist/client/assets/{chunk-U2HBQHQK-V_hneCfR.js → chunk-U2HBQHQK-BILTfRyq.js} +1 -1
  44. package/dist/client/assets/{chunk-X2U36JSP-De4pvO-I.js → chunk-X2U36JSP-D4-56gWx.js} +1 -1
  45. package/dist/client/assets/{chunk-XPW4576I-B_osXKp6.js → chunk-XPW4576I-SxB401Zg.js} +1 -1
  46. package/dist/client/assets/{chunk-YZCP3GAM-C_kqXssD.js → chunk-YZCP3GAM-CWXUVxFj.js} +1 -1
  47. package/dist/client/assets/{chunk-ZZ45TVLE-B_xtlma5.js → chunk-ZZ45TVLE-CXjZua4f.js} +1 -1
  48. package/dist/client/assets/classDiagram-6PBFFD2Q-DnUQ2iGN.js +1 -0
  49. package/dist/client/assets/classDiagram-v2-HSJHXN6E-Dwp5vuOB.js +1 -0
  50. package/dist/client/assets/clone-aWrl-obY.js +1 -0
  51. package/dist/client/assets/cose-bilkent-S5V4N54A-YToNpueF.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-QFYoTa0z.js} +1 -1
  54. package/dist/client/assets/{dagre-DU-XBdcU.js → dagre-tvaMpP4D.js} +1 -1
  55. package/dist/client/assets/{diagram-5BDNPKRD-DpUUhvWz.js → diagram-5BDNPKRD-DM0NNmEN.js} +1 -1
  56. package/dist/client/assets/{diagram-G4DWMVQ6-BJoTrUAx.js → diagram-G4DWMVQ6-TiLkMmwt.js} +1 -1
  57. package/dist/client/assets/{diagram-MMDJMWI5-CAk1GW5g.js → diagram-MMDJMWI5-DM1ykqrB.js} +1 -1
  58. package/dist/client/assets/{diagram-TYMM5635-Cct6g7FA.js → diagram-TYMM5635-BEOLX1wr.js} +1 -1
  59. package/dist/client/assets/{dist-61sCfOmN.js → dist-CCBhd9az.js} +1 -1
  60. package/dist/client/assets/{erDiagram-SMLLAGMA-DHs2bXUj.js → erDiagram-SMLLAGMA-DZcjZq6z.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-B1AVT9es.js} +1 -1
  63. package/dist/client/assets/ganttDiagram-T4ZO3ILL-BCEXws9V.js +292 -0
  64. package/dist/client/assets/gitGraph-7Q5UKJZL-BE3Mcr-v.js +1 -0
  65. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-Bc_rL3_k.js → gitGraphDiagram-UUTBAWPF-CVznBDOl.js} +1 -1
  66. package/dist/client/assets/{graphlib-BVMK0xYE.js → graphlib-C4fWcyt1.js} +1 -1
  67. package/dist/client/assets/index-6LShOAAb.js +79 -0
  68. package/dist/client/assets/index-C16wNcPQ.css +2 -0
  69. package/dist/client/assets/info-OMHHGYJF-CBpXVhw-.js +1 -0
  70. package/dist/client/assets/{infoDiagram-42DDH7IO-Cf8u4jgP.js → infoDiagram-42DDH7IO-D8Oxr-KJ.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-BE9KniVE.js} +1 -1
  73. package/dist/client/assets/{journeyDiagram-VCZTEJTY-BMkeQqJb.js → journeyDiagram-VCZTEJTY-B3lGcz06.js} +1 -1
  74. package/dist/client/assets/{kanban-definition-6JOO6SKY-B8KkeZLS.js → kanban-definition-6JOO6SKY-Bs1QdB0j.js} +1 -1
  75. package/dist/client/assets/{line-CVpcI6kj.js → line-CO4-KhEq.js} +1 -1
  76. package/dist/client/assets/{linear-DmhiOOKU.js → linear-CnaJKs0I.js} +1 -1
  77. package/dist/client/assets/mermaid-parser.core-CravK6bS.js +4 -0
  78. package/dist/client/assets/{mermaid.core-R7nXpPx-.js → mermaid.core-DTh9KJvF.js} +3 -3
  79. package/dist/client/assets/{mindmap-definition-QFDTVHPH-CwcHocMZ.js → mindmap-definition-QFDTVHPH-D2xU2hfX.js} +1 -1
  80. package/dist/client/assets/{ordinal-k--hYEme.js → ordinal-DIg8h6NI.js} +1 -1
  81. package/dist/client/assets/packet-4T2RLAQJ-abaJ3V5T.js +1 -0
  82. package/dist/client/assets/pie-ZZUOXDRM-B12dpA7V.js +1 -0
  83. package/dist/client/assets/{pieDiagram-DEJITSTG-BVAn8Lmr.js → pieDiagram-DEJITSTG-CRX6y4IQ.js} +1 -1
  84. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-C2XZ_zxa.js → quadrantDiagram-34T5L4WZ-K2HFp8O8.js} +1 -1
  85. package/dist/client/assets/radar-PYXPWWZC-BbBaJJN8.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-C-8AW0uI.js} +1 -1
  88. package/dist/client/assets/{sankeyDiagram-XADWPNL6-D_4_234M.js → sankeyDiagram-XADWPNL6-Bv-_ZFS5.js} +1 -1
  89. package/dist/client/assets/{sequenceDiagram-FGHM5R23-B-yHKMuK.js → sequenceDiagram-FGHM5R23-Bk4QYIPk.js} +1 -1
  90. package/dist/client/assets/src-XMuEuFcU.js +1 -0
  91. package/dist/client/assets/{stateDiagram-FHFEXIEX-BeG2di4I.js → stateDiagram-FHFEXIEX-CI1G7zGC.js} +1 -1
  92. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-DQ0U-oto.js +1 -0
  93. package/dist/client/assets/{timeline-definition-GMOUNBTQ-DhtnMGcE.js → timeline-definition-GMOUNBTQ-CnXv8xHg.js} +1 -1
  94. package/dist/client/assets/treeView-SZITEDCU-CM0rCBUc.js +1 -0
  95. package/dist/client/assets/treemap-W4RFUUIX-CXoNE_rL.js +1 -0
  96. package/dist/client/assets/{vennDiagram-DHZGUBPP-CBn69TcQ.js → vennDiagram-DHZGUBPP-M5x471Ar.js} +1 -1
  97. package/dist/client/assets/wardley-RL74JXVD-B_EtnvOk.js +1 -0
  98. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-CEoSJmN1.js → wardleyDiagram-NUSXRM2D-BG99uPNN.js} +1 -1
  99. package/dist/client/assets/{xychartDiagram-5P7HB3ND-BZ_X9tkn.js → xychartDiagram-5P7HB3ND-DO7Upr9G.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/manifest.json +1 -0
  106. package/dist/client/site-data/og-image.png +0 -0
  107. package/dist/client/site-data/snapshots/55f23a1...080c0e6.json +1 -0
  108. package/dist/client/site-data/snapshots/66ff7c6...e6977fe.json +1 -0
  109. package/dist/client/site-data/snapshots/7d40fd4...a72112f-comments.json +1 -0
  110. package/dist/client/site-data/snapshots/7d40fd4...a72112f.json +1 -0
  111. package/dist/server/file-watcher.d.ts +2 -1
  112. package/dist/server/file-watcher.js +9 -3
  113. package/dist/server/git-diff.d.ts +5 -0
  114. package/dist/server/git-diff.js +65 -1
  115. package/dist/server/git-diff.test.js +50 -0
  116. package/dist/server/server.js +265 -68
  117. package/dist/server/server.test.js +228 -0
  118. package/dist/tui/App.js +0 -1
  119. package/dist/types/diff.d.ts +15 -4
  120. package/dist/types/watch.d.ts +30 -1
  121. package/dist/utils/commentImports.d.ts +2 -0
  122. package/dist/utils/commentImports.js +119 -1
  123. package/dist/utils/editorOptions.d.ts +58 -35
  124. package/dist/utils/editorOptions.js +150 -24
  125. package/dist/utils/editorOptions.test.js +201 -9
  126. package/package.json +7 -4
  127. package/dist/client/assets/architecture-YZFGNWBL-Cs2Q6RQP.js +0 -1
  128. package/dist/client/assets/architectureDiagram-Q4EWVU46-BO4dVPUA.js +0 -36
  129. package/dist/client/assets/channel-_xDT1u3-.js +0 -1
  130. package/dist/client/assets/chunk-336JU56O-D1qa7Qzb.js +0 -2
  131. package/dist/client/assets/chunk-426QAEUC-6J_A_wvD.js +0 -1
  132. package/dist/client/assets/chunk-CFjPhJqf.js +0 -1
  133. package/dist/client/assets/chunk-QZHKN3VN-C0QzfgZ8.js +0 -1
  134. package/dist/client/assets/classDiagram-6PBFFD2Q-5XrS-DAQ.js +0 -1
  135. package/dist/client/assets/classDiagram-v2-HSJHXN6E-Covl2vKy.js +0 -1
  136. package/dist/client/assets/clone-rhRH8pyW.js +0 -1
  137. package/dist/client/assets/cose-bilkent-S5V4N54A-BvXFc7Rr.js +0 -1
  138. package/dist/client/assets/ganttDiagram-T4ZO3ILL-CMIzlKAR.js +0 -292
  139. package/dist/client/assets/gitGraph-7Q5UKJZL-A_wWsXju.js +0 -1
  140. package/dist/client/assets/index-BPoqJmrs.js +0 -79
  141. package/dist/client/assets/index-Cq_APK7Y.css +0 -2
  142. package/dist/client/assets/info-OMHHGYJF-Bv3kK2Bb.js +0 -1
  143. package/dist/client/assets/mermaid-parser.core-CnJ9Tv8l.js +0 -4
  144. package/dist/client/assets/packet-4T2RLAQJ-D2q3-9ae.js +0 -1
  145. package/dist/client/assets/pie-ZZUOXDRM-GivlQcUF.js +0 -1
  146. package/dist/client/assets/preload-helper-DSXbuxSR.js +0 -1
  147. package/dist/client/assets/radar-PYXPWWZC-C9pD6VNR.js +0 -1
  148. package/dist/client/assets/src-CjDs0_Ij.js +0 -1
  149. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-DvcSq7KE.js +0 -1
  150. package/dist/client/assets/treeView-SZITEDCU-BSNk8_yV.js +0 -1
  151. package/dist/client/assets/treemap-W4RFUUIX-ym4zQztE.js +0 -1
  152. package/dist/client/assets/wardley-RL74JXVD-B02H6ReJ.js +0 -1
  153. /package/dist/client/assets/{array-BNor45A1.js → array-DOVTz2Mq.js} +0 -0
  154. /package/dist/client/assets/{defaultLocale-DPzUsThw.js → defaultLocale-Ck2Xxk-C.js} +0 -0
  155. /package/dist/client/assets/{init-C0L3woqb.js → init-Bft5Ffpj.js} +0 -0
  156. /package/dist/client/assets/{katex-FOM3xZj7.js → katex-CeIlAR55.js} +0 -0
  157. /package/dist/client/assets/{path-sMK4d_s9.js → path-DfRbCp9y.js} +0 -0
  158. /package/dist/client/assets/{prism-bash-iQBez6et.js → prism-bash-CPkZUJMA.js} +0 -0
  159. /package/dist/client/assets/{prism-clojure-CTkJ-FW_.js → prism-clojure-BpoF2XhX.js} +0 -0
  160. /package/dist/client/assets/{prism-csharp-DAAROvjt.js → prism-csharp-BEk8D1-3.js} +0 -0
  161. /package/dist/client/assets/{prism-dart-CMjMHaBW.js → prism-dart-ByLYrdQB.js} +0 -0
  162. /package/dist/client/assets/{prism-elixir-B9cwzXs0.js → prism-elixir-BZtyIEab.js} +0 -0
  163. /package/dist/client/assets/{prism-haskell-Vgx7BCAm.js → prism-haskell-NAsbeo3V.js} +0 -0
  164. /package/dist/client/assets/{prism-hcl-Du4YC80h.js → prism-hcl-crnGqmVp.js} +0 -0
  165. /package/dist/client/assets/{prism-java-CWuFbfVD.js → prism-java-BovStacA.js} +0 -0
  166. /package/dist/client/assets/{prism-markup-templating-h9TC-ifW.js → prism-markup-templating-Cl8NiLjy.js} +0 -0
  167. /package/dist/client/assets/{prism-nix-CqauNIYa.js → prism-nix-BS_cm_1n.js} +0 -0
  168. /package/dist/client/assets/{prism-perl-DhcRwJzx.js → prism-perl-DGLVMq5H.js} +0 -0
  169. /package/dist/client/assets/{prism-php-DcBIrISj.js → prism-php-BskSwJN8.js} +0 -0
  170. /package/dist/client/assets/{prism-protobuf-DuPg7Jbg.js → prism-protobuf-DfbIYpO7.js} +0 -0
  171. /package/dist/client/assets/{prism-ruby-lhDmuasn.js → prism-ruby-FBVh1PRE.js} +0 -0
  172. /package/dist/client/assets/{prism-scala-YlPat9I4.js → prism-scala--9AfMHPY.js} +0 -0
  173. /package/dist/client/assets/{prism-solidity-C3nR0EVH.js → prism-solidity-BgJNkj1z.js} +0 -0
  174. /package/dist/client/assets/{prism-sql-Cz-8DmQS.js → prism-sql-C9Czmpov.js} +0 -0
  175. /package/dist/client/assets/{prism-vim-C3oukvmk.js → prism-vim-CzUNf0WQ.js} +0 -0
  176. /package/dist/client/assets/{rough.esm-DeLgKbOI.js → rough.esm-Bbn_-PMU.js} +0 -0
@@ -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]);
@@ -32,7 +32,7 @@ export interface ParsedDiff {
32
32
  chunks: DiffChunk[];
33
33
  }
34
34
  export type DiffViewMode = 'split' | 'unified';
35
- export type LegacyDiffViewMode = 'side-by-side' | 'inline';
35
+ type LegacyDiffViewMode = 'side-by-side' | 'inline';
36
36
  export type DiffSide = 'old' | 'new';
37
37
  export type DiffLineRange = number | {
38
38
  start: number;
@@ -138,6 +138,17 @@ export interface ViewedFileRecord {
138
138
  viewedAt: string;
139
139
  diffContentHash: string;
140
140
  }
141
+ export interface ViewedHashIndexEntry {
142
+ filePath: string;
143
+ diffContentHash: string;
144
+ hashVersion: 1;
145
+ viewedAt: string;
146
+ }
147
+ export interface ViewedHashIndex {
148
+ version: 1;
149
+ lastModifiedAt: string;
150
+ entries: ViewedHashIndexEntry[];
151
+ }
141
152
  export interface LegacyDiffContextStorage {
142
153
  version: 1;
143
154
  baseCommitish: string;
@@ -168,11 +179,11 @@ export interface CommentThread {
168
179
  codeContent?: string;
169
180
  messages: DiffCommentMessage[];
170
181
  }
171
- export interface RevisionOption {
182
+ interface RevisionOption {
172
183
  value: string;
173
184
  label: string;
174
185
  }
175
- export interface BranchInfo {
186
+ interface BranchInfo {
176
187
  name: string;
177
188
  current: boolean;
178
189
  }
@@ -199,7 +210,7 @@ export interface FileExpandedState {
199
210
  oldTotalLines?: number;
200
211
  newTotalLines?: number;
201
212
  }
202
- export interface ExpandedRange {
213
+ interface ExpandedRange {
203
214
  chunkIndex: number;
204
215
  direction: 'up' | 'down';
205
216
  count: number;
@@ -5,12 +5,41 @@ export declare enum DiffMode {
5
5
  DOT = "dot",// HEAD vs working (all changes)
6
6
  SPECIFIC = "specific"
7
7
  }
8
+ type WatchChangeType = 'file' | 'commit' | 'staging';
9
+ interface ConnectedWatchEvent {
10
+ type: 'connected';
11
+ diffMode: DiffMode;
12
+ changeType: WatchChangeType;
13
+ timestamp: string;
14
+ message?: string;
15
+ }
16
+ interface ReloadWatchEvent {
17
+ type: 'reload';
18
+ diffMode: DiffMode;
19
+ changeType: WatchChangeType;
20
+ timestamp: string;
21
+ message?: string;
22
+ }
23
+ interface ErrorWatchEvent {
24
+ type: 'error';
25
+ diffMode: DiffMode;
26
+ changeType: WatchChangeType;
27
+ timestamp: string;
28
+ message?: string;
29
+ }
30
+ interface CommentsChangedWatchEvent {
31
+ type: 'commentsChanged';
32
+ version: number;
33
+ timestamp: string;
34
+ }
35
+ export type WatchEvent = ConnectedWatchEvent | ReloadWatchEvent | ErrorWatchEvent | CommentsChangedWatchEvent;
8
36
  export interface ClientWatchState {
9
37
  isWatchEnabled: boolean;
10
38
  diffMode: DiffMode;
11
39
  shouldReload: boolean;
12
40
  isReloading: boolean;
13
41
  lastChangeTime: Date | null;
14
- lastChangeType: 'file' | 'commit' | 'staging' | null;
42
+ lastChangeType: WatchChangeType | null;
15
43
  connectionStatus: 'connected' | 'disconnected' | 'reconnecting';
16
44
  }
45
+ export {};
@@ -3,7 +3,9 @@ interface MergeCommentImportsResult {
3
3
  threads: DiffCommentThread[];
4
4
  warnings: string[];
5
5
  }
6
+ export declare function normalizeCommentImports(input: unknown): CommentImport[];
6
7
  export declare function parseCommentImportValue(value: string): CommentImport[];
8
+ export declare function mergeCommentThreads(existingThreads: DiffCommentThread[], incomingThreads: DiffCommentThread[]): MergeCommentImportsResult;
7
9
  export declare function serializeCommentImports(commentImports: CommentImport[]): string;
8
10
  export declare function mergeCommentImports(existingThreads: DiffCommentThread[], commentImports: CommentImport[]): MergeCommentImportsResult;
9
11
  export {};
@@ -105,7 +105,7 @@ function normalizeCommentImportEntry(value) {
105
105
  };
106
106
  return normalized;
107
107
  }
108
- function normalizeCommentImports(input) {
108
+ export function normalizeCommentImports(input) {
109
109
  if (Array.isArray(input)) {
110
110
  return input.map((entry) => normalizeCommentImportEntry(entry));
111
111
  }
@@ -204,6 +204,124 @@ function createImportedReply(commentImport, now) {
204
204
  updatedAt,
205
205
  };
206
206
  }
207
+ function cloneLineRange(line) {
208
+ if (typeof line === 'number') {
209
+ return line;
210
+ }
211
+ return {
212
+ start: line.start,
213
+ end: line.end,
214
+ };
215
+ }
216
+ function clonePosition(position) {
217
+ return {
218
+ side: position.side,
219
+ line: cloneLineRange(position.line),
220
+ };
221
+ }
222
+ function cloneCodeSnapshot(snapshot) {
223
+ if (!snapshot) {
224
+ return undefined;
225
+ }
226
+ return {
227
+ content: snapshot.content,
228
+ language: snapshot.language,
229
+ };
230
+ }
231
+ function cloneMessage(message) {
232
+ return {
233
+ id: message.id,
234
+ body: message.body,
235
+ author: message.author,
236
+ createdAt: message.createdAt,
237
+ updatedAt: message.updatedAt,
238
+ };
239
+ }
240
+ function cloneThread(thread) {
241
+ return {
242
+ id: thread.id,
243
+ filePath: thread.filePath,
244
+ createdAt: thread.createdAt,
245
+ updatedAt: thread.updatedAt,
246
+ position: clonePosition(thread.position),
247
+ codeSnapshot: cloneCodeSnapshot(thread.codeSnapshot),
248
+ messages: thread.messages.map(cloneMessage),
249
+ };
250
+ }
251
+ function messagesMatch(left, right) {
252
+ if (left.id === right.id) {
253
+ return true;
254
+ }
255
+ if (normalizeAuthor(left.author) !== normalizeAuthor(right.author)) {
256
+ return false;
257
+ }
258
+ return left.body === right.body && left.createdAt === right.createdAt;
259
+ }
260
+ function threadsMatch(left, right) {
261
+ if (left.id === right.id) {
262
+ return true;
263
+ }
264
+ if (left.filePath !== right.filePath || !positionsMatch(left.position, right.position)) {
265
+ return false;
266
+ }
267
+ const leftRoot = left.messages[0];
268
+ const rightRoot = right.messages[0];
269
+ if (!leftRoot || !rightRoot) {
270
+ return false;
271
+ }
272
+ return messagesMatch(leftRoot, rightRoot);
273
+ }
274
+ function sortReplies(left, right) {
275
+ const createdAtOrder = left.createdAt.localeCompare(right.createdAt);
276
+ if (createdAtOrder !== 0) {
277
+ return createdAtOrder;
278
+ }
279
+ return left.id.localeCompare(right.id);
280
+ }
281
+ function pickNewerMessage(existingMessage, incomingMessage) {
282
+ if (incomingMessage.updatedAt.localeCompare(existingMessage.updatedAt) >= 0) {
283
+ return cloneMessage(incomingMessage);
284
+ }
285
+ return cloneMessage(existingMessage);
286
+ }
287
+ function mergeThread(existingThread, incomingThread) {
288
+ const mergedMessages = existingThread.messages.map(cloneMessage);
289
+ for (const incomingMessage of incomingThread.messages) {
290
+ const matchIndex = mergedMessages.findIndex((message) => messagesMatch(message, incomingMessage));
291
+ if (matchIndex >= 0) {
292
+ mergedMessages[matchIndex] = pickNewerMessage(mergedMessages[matchIndex], incomingMessage);
293
+ continue;
294
+ }
295
+ mergedMessages.push(cloneMessage(incomingMessage));
296
+ }
297
+ const [rootMessage, ...replyMessages] = mergedMessages;
298
+ const orderedMessages = rootMessage
299
+ ? [rootMessage, ...replyMessages.sort(sortReplies)]
300
+ : replyMessages.sort(sortReplies);
301
+ const updatedAt = orderedMessages.reduce((latest, message) => maxIsoTimestamp(latest, message.updatedAt), maxIsoTimestamp(existingThread.updatedAt, incomingThread.updatedAt));
302
+ return {
303
+ ...cloneThread(existingThread),
304
+ createdAt: existingThread.createdAt.localeCompare(incomingThread.createdAt) <= 0
305
+ ? existingThread.createdAt
306
+ : incomingThread.createdAt,
307
+ updatedAt,
308
+ position: clonePosition(existingThread.position),
309
+ codeSnapshot: cloneCodeSnapshot(incomingThread.codeSnapshot ?? existingThread.codeSnapshot),
310
+ messages: orderedMessages,
311
+ };
312
+ }
313
+ export function mergeCommentThreads(existingThreads, incomingThreads) {
314
+ const threads = existingThreads.map(cloneThread);
315
+ for (const incomingThread of incomingThreads) {
316
+ const existingIndex = threads.findIndex((thread) => threadsMatch(thread, incomingThread));
317
+ if (existingIndex < 0) {
318
+ threads.push(cloneThread(incomingThread));
319
+ continue;
320
+ }
321
+ threads[existingIndex] = mergeThread(threads[existingIndex], incomingThread);
322
+ }
323
+ return { threads, warnings: [] };
324
+ }
207
325
  function serializeLineRange(line) {
208
326
  if (typeof line === 'number') {
209
327
  return line;
@@ -1,37 +1,60 @@
1
- export declare const EDITOR_OPTIONS: readonly [{
2
- readonly id: "vscode";
3
- readonly label: "VS Code";
4
- readonly protocol: "vscode";
5
- readonly cliCommand: "code";
6
- readonly cliArgs: readonly [];
7
- readonly lineFormat: "goto-flag";
8
- readonly aliases: readonly ["vscode", "code"];
9
- }, {
10
- readonly id: "cursor";
11
- readonly label: "Cursor";
12
- readonly protocol: "cursor";
13
- readonly cliCommand: "cursor";
14
- readonly cliArgs: readonly [];
15
- readonly lineFormat: "goto-flag";
16
- readonly aliases: readonly ["cursor"];
17
- }, {
18
- readonly id: "zed";
19
- readonly label: "Zed";
20
- readonly protocol: "zed";
21
- readonly cliCommand: "zed";
22
- readonly cliArgs: readonly [];
23
- readonly lineFormat: "path-suffix";
24
- readonly aliases: readonly ["zed"];
25
- }, {
26
- readonly id: "none";
27
- readonly label: "Hide “Open in editor” button";
28
- readonly protocol: null;
29
- readonly cliCommand: null;
30
- readonly cliArgs: readonly [];
31
- readonly lineFormat: "goto-flag";
32
- readonly aliases: readonly ["none", "disabled", "off"];
33
- }];
34
- export type EditorOption = (typeof EDITOR_OPTIONS)[number];
35
- export type EditorOptionId = EditorOption['id'];
1
+ export type EditorOptionId = 'vscode' | 'cursor' | 'zed' | 'textmate' | 'bbedit' | 'emacs' | 'macvim' | 'sublime' | 'nova' | 'custom' | 'none';
2
+ interface EditorOption {
3
+ readonly id: EditorOptionId;
4
+ readonly label: string;
5
+ /**
6
+ * Executable to run. Empty string for the `custom` placeholder (filled in by
7
+ * the user) and for `none` (open-in-editor disabled).
8
+ */
9
+ readonly command: string;
10
+ /**
11
+ * Whitespace-separated argument template. Supports the `%file` and `%line`
12
+ * placeholders. Parsed just before spawning so quoted segments survive:
13
+ * e.g. `'-l %line "%file"'`.
14
+ */
15
+ readonly argsTemplate: string;
16
+ }
17
+ export declare const CUSTOM_EDITOR_ID: EditorOptionId;
18
+ export declare const NONE_EDITOR_ID: EditorOptionId;
36
19
  export declare const DEFAULT_EDITOR_ID: EditorOptionId;
20
+ export declare const EDITOR_OPTIONS: readonly [EditorOption, ...EditorOption[]];
21
+ export declare const DEFAULT_EDITOR_OPTION: EditorOption;
22
+ /**
23
+ * Case-insensitive lookup by id. Used mostly for the `DIFIT_EDITOR` / `EDITOR`
24
+ * environment-variable fallback on the server; the browser UI passes the full
25
+ * `{id, command, argsTemplate}` shape so it does not rely on this helper.
26
+ */
37
27
  export declare const resolveEditorOption: (input?: string) => EditorOption;
28
+ /**
29
+ * Tokenise a user-supplied arguments template string into individual CLI
30
+ * arguments. Supports single and double quoted segments so paths and flags
31
+ * containing spaces can be preserved.
32
+ *
33
+ * Inside double-quoted segments `\"` decodes to a literal `"` and `\\` to a
34
+ * literal `\`, mirroring common shell intuition. Other `\x` sequences pass
35
+ * through untouched so Lisp/regex fragments like `\s` or `\$` stay intact.
36
+ * Single-quoted segments are fully literal. The template is handed to
37
+ * `spawn` as argv directly – no shell is involved.
38
+ */
39
+ export declare const parseEditorArgsTemplate: (template: string | undefined | null) => string[];
40
+ interface EditorSpawnSpec {
41
+ readonly command: string;
42
+ readonly args: readonly string[];
43
+ }
44
+ interface BuildEditorSpawnSpecInput {
45
+ readonly command: string;
46
+ readonly argsTemplate: string;
47
+ readonly filePath: string;
48
+ readonly lineNumber: number | null;
49
+ }
50
+ /**
51
+ * Build the final `{ command, args }` tuple to hand to `child_process.spawn`.
52
+ * Returns `null` when the command is empty so callers can surface a clear
53
+ * "not configured" error instead of spawning an empty process.
54
+ *
55
+ * `%file` and `%line` are substituted as-is inside each token, so composite
56
+ * tokens such as `"%file:%line"` become `"/abs/file.ts:42"`. When no line
57
+ * number is available the substitution falls back to `1`.
58
+ */
59
+ export declare const buildEditorSpawnSpec: (input: BuildEditorSpawnSpecInput) => EditorSpawnSpec | null;
60
+ export {};