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
@@ -1,5 +1,5 @@
1
1
  import { type Response } from 'express';
2
- import { DiffMode } from '../types/watch.js';
2
+ import { DiffMode, type WatchEvent } from '../types/watch.js';
3
3
  export declare class FileWatcherService {
4
4
  private subscriptions;
5
5
  private clients;
@@ -21,6 +21,7 @@ export declare class FileWatcherService {
21
21
  stop(): Promise<void>;
22
22
  addClient(res: Response): void;
23
23
  removeClient(res: Response): void;
24
+ broadcast(event: WatchEvent): void;
24
25
  private broadcastChange;
25
26
  private sendToClient;
26
27
  private determineChangeType;
@@ -198,6 +198,14 @@ export class FileWatcherService {
198
198
  this.clients.splice(index, 1);
199
199
  }
200
200
  }
201
+ broadcast(event) {
202
+ if (this.clients.length === 0) {
203
+ return;
204
+ }
205
+ this.clients.forEach((client) => {
206
+ this.sendToClient(client, event);
207
+ });
208
+ }
201
209
  broadcastChange() {
202
210
  if (this.clients.length === 0 || !this.config) {
203
211
  return;
@@ -210,9 +218,7 @@ export class FileWatcherService {
210
218
  timestamp: new Date().toISOString(),
211
219
  message: `Changes detected in ${this.config.diffMode} mode`,
212
220
  };
213
- this.clients.forEach((client) => {
214
- this.sendToClient(client, event);
215
- });
221
+ this.broadcast(event);
216
222
  }
217
223
  sendToClient(client, event) {
218
224
  try {
@@ -5,11 +5,16 @@ export declare class GitDiffParser {
5
5
  private readonly resolvedCommitCache;
6
6
  private static readonly RESOLVED_COMMIT_CACHE_TTL_MS;
7
7
  private static readonly GENERATED_HEADER_SCAN_BYTES;
8
+ private static readonly GITATTRIBUTES_CHECK_CHUNK_SIZE;
8
9
  constructor(repoPath?: string);
9
10
  private normalizeRepositoryRelativePath;
10
11
  private resolveBaseCommitish;
11
12
  parseDiff(selection: DiffSelection, ignoreWhitespace?: boolean, contextLines?: number): Promise<DiffResponse>;
12
13
  private parseUnifiedDiff;
14
+ private getGitattributesSourceArgs;
15
+ private parseGitattributesGeneratedOutput;
16
+ private getGitattributesGeneratedPaths;
17
+ private markGitattributesGeneratedFiles;
13
18
  private decodeGitPath;
14
19
  private extractPathFromLine;
15
20
  private parseDiffHeaderPaths;
@@ -9,6 +9,7 @@ export class GitDiffParser {
9
9
  resolvedCommitCache = new Map();
10
10
  static RESOLVED_COMMIT_CACHE_TTL_MS = 5_000;
11
11
  static GENERATED_HEADER_SCAN_BYTES = 4 * 1024;
12
+ static GITATTRIBUTES_CHECK_CHUNK_SIZE = 200;
12
13
  constructor(repoPath = process.cwd()) {
13
14
  this.repoPath = repoPath;
14
15
  this.git = simpleGit(repoPath);
@@ -51,6 +52,7 @@ export class GitDiffParser {
51
52
  let diffArgs;
52
53
  let resolvedBaseCommitish = effectiveBaseCommitish;
53
54
  let resolvedTargetCommitish = targetCommitish;
55
+ let attributesRef = targetCommitish;
54
56
  // Handle target special chars (base is always a regular commit)
55
57
  if (targetCommitish === 'working') {
56
58
  // Show unstaged changes (working vs staged)
@@ -78,6 +80,7 @@ export class GitDiffParser {
78
80
  resolvedCommit = createCommitRangeString(shortHash(baseHash), shortHash(targetHash));
79
81
  resolvedBaseCommitish = shortHash(baseHash);
80
82
  resolvedTargetCommitish = shortHash(targetHash);
83
+ attributesRef = targetHash;
81
84
  diffArgs = [baseHash, targetHash];
82
85
  }
83
86
  if (ignoreWhitespace) {
@@ -91,7 +94,7 @@ export class GitDiffParser {
91
94
  diffArgs.push('--no-ext-diff', '--color=never');
92
95
  // Single git invocation for better startup latency on large repositories.
93
96
  const diffRaw = await this.git.diff(diffArgs);
94
- const files = this.parseUnifiedDiff(diffRaw);
97
+ const files = await this.markGitattributesGeneratedFiles(this.parseUnifiedDiff(diffRaw), attributesRef);
95
98
  return {
96
99
  commit: resolvedCommit,
97
100
  files,
@@ -119,6 +122,63 @@ export class GitDiffParser {
119
122
  }
120
123
  return files;
121
124
  }
125
+ getGitattributesSourceArgs(ref) {
126
+ if (ref === 'working' || ref === '.') {
127
+ return [];
128
+ }
129
+ if (ref === 'staged') {
130
+ return ['--cached'];
131
+ }
132
+ return ['--source', ref];
133
+ }
134
+ parseGitattributesGeneratedOutput(output) {
135
+ const generatedPaths = new Set();
136
+ const fields = output.split('\0');
137
+ for (let i = 0; i + 2 < fields.length; i += 3) {
138
+ const [path, attribute, value] = fields.slice(i, i + 3);
139
+ if (attribute === 'linguist-generated' && value === 'true') {
140
+ generatedPaths.add(path);
141
+ }
142
+ }
143
+ return generatedPaths;
144
+ }
145
+ async getGitattributesGeneratedPaths(filepaths, ref) {
146
+ if (filepaths.length === 0) {
147
+ return new Set();
148
+ }
149
+ const generatedPaths = new Set();
150
+ try {
151
+ for (let i = 0; i < filepaths.length; i += GitDiffParser.GITATTRIBUTES_CHECK_CHUNK_SIZE) {
152
+ const chunk = filepaths.slice(i, i + GitDiffParser.GITATTRIBUTES_CHECK_CHUNK_SIZE);
153
+ const output = await this.git.raw([
154
+ 'check-attr',
155
+ '-z',
156
+ ...this.getGitattributesSourceArgs(ref),
157
+ 'linguist-generated',
158
+ '--',
159
+ ...chunk,
160
+ ]);
161
+ if (typeof output !== 'string') {
162
+ continue;
163
+ }
164
+ for (const path of this.parseGitattributesGeneratedOutput(output)) {
165
+ generatedPaths.add(path);
166
+ }
167
+ }
168
+ return generatedPaths;
169
+ }
170
+ catch {
171
+ return new Set();
172
+ }
173
+ }
174
+ async markGitattributesGeneratedFiles(files, ref) {
175
+ const candidates = files.filter((file) => !file.isGenerated).map((file) => file.path);
176
+ const generatedPaths = await this.getGitattributesGeneratedPaths(candidates, ref);
177
+ if (generatedPaths.size === 0) {
178
+ return files;
179
+ }
180
+ return files.map((file) => generatedPaths.has(file.path) ? { ...file, isGenerated: true } : file);
181
+ }
122
182
  decodeGitPath(rawPath) {
123
183
  if (typeof rawPath !== 'string') {
124
184
  return undefined;
@@ -454,6 +514,10 @@ export class GitDiffParser {
454
514
  if (pathResult.isGenerated) {
455
515
  return { isGenerated: true, source: 'path' };
456
516
  }
517
+ const gitattributesResult = await this.getGitattributesGeneratedPaths([filepath], ref);
518
+ if (gitattributesResult.has(filepath)) {
519
+ return { isGenerated: true, source: 'path' };
520
+ }
457
521
  try {
458
522
  const buffer = await this.getBlobContent(filepath, ref);
459
523
  const lines = this.extractHeaderLines(buffer);
@@ -985,6 +985,23 @@ index abc123..def456 100644
985
985
  expect(generatedStatus).toEqual({ isGenerated: true, source: 'path' });
986
986
  expect(getBlobContentSpy).not.toHaveBeenCalled();
987
987
  });
988
+ it('returns source=path for .gitattributes linguist-generated files without reading content', async () => {
989
+ const gitRaw = parser.git.raw;
990
+ gitRaw.mockResolvedValue('apps/app/web/src/api/index.tsx\0linguist-generated\0true\0');
991
+ const getBlobContentSpy = vi.spyOn(parser, 'getBlobContent');
992
+ const generatedStatus = await parser.getGeneratedStatus('apps/app/web/src/api/index.tsx', 'HEAD');
993
+ expect(gitRaw).toHaveBeenCalledWith([
994
+ 'check-attr',
995
+ '-z',
996
+ '--source',
997
+ 'HEAD',
998
+ 'linguist-generated',
999
+ '--',
1000
+ 'apps/app/web/src/api/index.tsx',
1001
+ ]);
1002
+ expect(generatedStatus).toEqual({ isGenerated: true, source: 'path' });
1003
+ expect(getBlobContentSpy).not.toHaveBeenCalled();
1004
+ });
988
1005
  it('returns false when content cannot be read for content-based generated detection', async () => {
989
1006
  const getBlobContentSpy = vi.spyOn(parser, 'getBlobContent');
990
1007
  getBlobContentSpy.mockRejectedValue(new Error('missing blob'));
@@ -1075,6 +1092,39 @@ index abc123..def456 100644
1075
1092
  });
1076
1093
  });
1077
1094
  describe('parseDiff', () => {
1095
+ it('marks files with .gitattributes linguist-generated=true as generated', async () => {
1096
+ const file = 'apps/app/web/src/api/index.tsx';
1097
+ const gitDiff = parser.git.diff;
1098
+ const gitRevparse = parser.git.revparse;
1099
+ const gitRaw = parser.git.raw;
1100
+ gitRevparse
1101
+ .mockResolvedValueOnce('1234567890abcdef1234567890abcdef12345678')
1102
+ .mockResolvedValueOnce('abcdef1234567890abcdef1234567890abcdef12');
1103
+ gitDiff.mockResolvedValue([
1104
+ `diff --git a/${file} b/${file}`,
1105
+ `index abc123..def456 100644`,
1106
+ `--- a/${file}`,
1107
+ `+++ b/${file}`,
1108
+ `@@ -1 +1 @@`,
1109
+ `-old`,
1110
+ `+new`,
1111
+ ].join('\n'));
1112
+ gitRaw.mockResolvedValue(`${file}\0linguist-generated\0true\0`);
1113
+ const response = await parser.parseDiff({
1114
+ targetCommitish: 'HEAD',
1115
+ baseCommitish: 'HEAD~1',
1116
+ });
1117
+ expect(gitRaw).toHaveBeenCalledWith([
1118
+ 'check-attr',
1119
+ '-z',
1120
+ '--source',
1121
+ '1234567890abcdef1234567890abcdef12345678',
1122
+ 'linguist-generated',
1123
+ '--',
1124
+ file,
1125
+ ]);
1126
+ expect(response.files[0].isGenerated).toBe(true);
1127
+ });
1078
1128
  it('passes context lines through to git diff', async () => {
1079
1129
  const gitDiff = parser.git.diff;
1080
1130
  const gitRevparse = parser.git.revparse;
@@ -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