difit 4.0.4 → 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.
- package/README.ja.md +3 -2
- package/README.ko.md +3 -2
- package/README.md +3 -2
- package/README.zh.md +3 -2
- package/dist/cli/comment.d.ts +2 -0
- package/dist/cli/comment.js +91 -0
- package/dist/cli/comment.test.d.ts +1 -0
- package/dist/cli/comment.test.js +164 -0
- package/dist/cli/index.js +104 -13
- package/dist/cli/index.test.js +5 -5
- package/dist/cli/utils.d.ts +1 -0
- package/dist/cli/utils.js +7 -0
- package/dist/client/assets/{_baseFor-DKyA49xd.js → _baseFor-Dq1lbcoh.js} +1 -1
- package/dist/client/assets/{arc-COOp7iVe.js → arc-1g1LrDb3.js} +1 -1
- package/dist/client/assets/architecture-YZFGNWBL-MZfdAdY6.js +1 -0
- package/dist/client/assets/architectureDiagram-Q4EWVU46-D87-Rmwy.js +36 -0
- package/dist/client/assets/{blockDiagram-DXYQGD6D-CtNJnEWN.js → blockDiagram-DXYQGD6D-Cep-MIFv.js} +1 -1
- package/dist/client/assets/{c4Diagram-AHTNJAMY-BqG-1m6C.js → c4Diagram-AHTNJAMY-BQuH9Txx.js} +1 -1
- package/dist/client/assets/channel-CJqLEVLU.js +1 -0
- package/dist/client/assets/{chunk-2KRD3SAO-DqP2NJNd.js → chunk-2KRD3SAO-CpQQpmvx.js} +1 -1
- package/dist/client/assets/chunk-336JU56O-Ddk9EzgO.js +2 -0
- package/dist/client/assets/chunk-426QAEUC-2xhUznDE.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-BT78EnQ6.js → chunk-4BX2VUAB-Ca-N0Wd9.js} +1 -1
- package/dist/client/assets/{chunk-4TB4RGXK-C4w_Bwzw.js → chunk-4TB4RGXK-ZTWP_Onw.js} +2 -2
- package/dist/client/assets/{chunk-55IACEB6-z3MQSTaj.js → chunk-55IACEB6-Dub40zHG.js} +1 -1
- package/dist/client/assets/{chunk-5FUZZQ4R-Chei69aj.js → chunk-5FUZZQ4R-Cgda0gtZ.js} +1 -1
- package/dist/client/assets/{chunk-5PVQY5BW-HgRiIs0X.js → chunk-5PVQY5BW-D8JPH_tm.js} +1 -1
- package/dist/client/assets/{chunk-67CJDMHE-B2q10-fp.js → chunk-67CJDMHE-U1KyLHzG.js} +1 -1
- package/dist/client/assets/{chunk-7N4EOEYR-DPgxysWq.js → chunk-7N4EOEYR-WzOy51nD.js} +1 -1
- package/dist/client/assets/{chunk-AA7GKIK3-BqmVmKLq.js → chunk-AA7GKIK3-DlWOj4lr.js} +1 -1
- package/dist/client/assets/{chunk-BSJP7CBP-CaIgleFn.js → chunk-BSJP7CBP-CcZ0op08.js} +1 -1
- package/dist/client/assets/{chunk-CIAEETIT-ByD-tlNF.js → chunk-CIAEETIT-qVSphnw5.js} +1 -1
- package/dist/client/assets/{chunk-EDXVE4YY-d3RUKKAj.js → chunk-EDXVE4YY-76SPH4sf.js} +1 -1
- package/dist/client/assets/{chunk-ENJZ2VHE-CNq5Qmg9.js → chunk-ENJZ2VHE-CKULNIzL.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-DYfHJ6MV.js → chunk-FMBD7UC4-CvDPP3mb.js} +1 -1
- package/dist/client/assets/{chunk-FOC6F5B3-BRpSWlZj.js → chunk-FOC6F5B3-DceW0hWA.js} +1 -1
- package/dist/client/assets/{chunk-ICPOFSXX-B_MThwG6.js → chunk-ICPOFSXX-ChGBNZMk.js} +2 -2
- package/dist/client/assets/{chunk-K5T4RW27-DmamW1Ds.js → chunk-K5T4RW27-DBHdC4ln.js} +10 -10
- package/dist/client/assets/{chunk-KGLVRYIC-CRbg4c4z.js → chunk-KGLVRYIC-DRS7yiGQ.js} +1 -1
- package/dist/client/assets/{chunk-LIHQZDEY-CHQPSdB3.js → chunk-LIHQZDEY-KsE8dyJP.js} +1 -1
- package/dist/client/assets/{chunk-ORNJ4GCN-CIsQ4Zi4.js → chunk-ORNJ4GCN-Dnp4oHRD.js} +1 -1
- package/dist/client/assets/{chunk-OYMX7WX6-Cxi0kdGg.js → chunk-OYMX7WX6-CciaotDu.js} +1 -1
- package/dist/client/assets/chunk-QZHKN3VN-BiVE5u_E.js +1 -0
- package/dist/client/assets/{chunk-U2HBQHQK-V_hneCfR.js → chunk-U2HBQHQK-nbp7CjBP.js} +1 -1
- package/dist/client/assets/{chunk-X2U36JSP-De4pvO-I.js → chunk-X2U36JSP-Chs85loT.js} +1 -1
- package/dist/client/assets/{chunk-XPW4576I-B_osXKp6.js → chunk-XPW4576I-VtI9b561.js} +1 -1
- package/dist/client/assets/{chunk-YZCP3GAM-C_kqXssD.js → chunk-YZCP3GAM-sBsewSoO.js} +1 -1
- package/dist/client/assets/{chunk-ZZ45TVLE-B_xtlma5.js → chunk-ZZ45TVLE-TMgeW_px.js} +1 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-CfyHazmg.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-D7Rb-bnu.js +1 -0
- package/dist/client/assets/clone-8xC1huEg.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-5TzM3w9g.js +1 -0
- package/dist/client/assets/{cytoscape.esm-DRReFUEO.js → cytoscape.esm-DdcHPZAZ.js} +2 -2
- package/dist/client/assets/{dagre-KV5264BT-BWYGReXF.js → dagre-KV5264BT-xvyFOxd3.js} +1 -1
- package/dist/client/assets/{dagre-DU-XBdcU.js → dagre-sb6WtN4K.js} +1 -1
- package/dist/client/assets/{diagram-5BDNPKRD-DpUUhvWz.js → diagram-5BDNPKRD-ChRpAe5p.js} +1 -1
- package/dist/client/assets/{diagram-G4DWMVQ6-BJoTrUAx.js → diagram-G4DWMVQ6-C_8BED4A.js} +1 -1
- package/dist/client/assets/{diagram-MMDJMWI5-CAk1GW5g.js → diagram-MMDJMWI5-BMwXEou2.js} +1 -1
- package/dist/client/assets/{diagram-TYMM5635-Cct6g7FA.js → diagram-TYMM5635-CeAkx82D.js} +1 -1
- package/dist/client/assets/{dist-61sCfOmN.js → dist-CwC9dd2Z.js} +1 -1
- package/dist/client/assets/{erDiagram-SMLLAGMA-DHs2bXUj.js → erDiagram-SMLLAGMA-yGCTeXGt.js} +1 -1
- package/dist/client/assets/{flatten-mnWyE-RB.js → flatten-SRIRKgqP.js} +1 -1
- package/dist/client/assets/{flowDiagram-DWJPFMVM-DLu-6dfC.js → flowDiagram-DWJPFMVM-CugkvbmM.js} +1 -1
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-BXnlBFgK.js +292 -0
- package/dist/client/assets/gitGraph-7Q5UKJZL-BeTWkPrd.js +1 -0
- package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-Bc_rL3_k.js → gitGraphDiagram-UUTBAWPF-B61aCwwu.js} +1 -1
- package/dist/client/assets/{graphlib-BVMK0xYE.js → graphlib-BMWKz3zT.js} +1 -1
- package/dist/client/assets/index-D2Y8-unG.css +2 -0
- package/dist/client/assets/index-D9v_eYzS.js +79 -0
- package/dist/client/assets/info-OMHHGYJF-MUNR2tTt.js +1 -0
- package/dist/client/assets/{infoDiagram-42DDH7IO-Cf8u4jgP.js → infoDiagram-42DDH7IO-Bkh6nTL2.js} +1 -1
- package/dist/client/assets/{isEmpty-CiiIHfXR.js → isEmpty-CStpjy4G.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-7n7DvfEb.js → ishikawaDiagram-UXIWVN3A-D_fdVT6_.js} +1 -1
- package/dist/client/assets/{journeyDiagram-VCZTEJTY-BMkeQqJb.js → journeyDiagram-VCZTEJTY-DkXVokNF.js} +1 -1
- package/dist/client/assets/{kanban-definition-6JOO6SKY-B8KkeZLS.js → kanban-definition-6JOO6SKY-y8qq7qvL.js} +1 -1
- package/dist/client/assets/{line-CVpcI6kj.js → line-B0LcTqNY.js} +1 -1
- package/dist/client/assets/{linear-DmhiOOKU.js → linear-CqIjr2qp.js} +1 -1
- package/dist/client/assets/mermaid-parser.core-Du6QzpZO.js +4 -0
- package/dist/client/assets/{mermaid.core-R7nXpPx-.js → mermaid.core-CZBu-oKJ.js} +3 -3
- package/dist/client/assets/{mindmap-definition-QFDTVHPH-CwcHocMZ.js → mindmap-definition-QFDTVHPH-BJrRxSkM.js} +1 -1
- package/dist/client/assets/{ordinal-k--hYEme.js → ordinal-DIg8h6NI.js} +1 -1
- package/dist/client/assets/packet-4T2RLAQJ-Ci-Uu57s.js +1 -0
- package/dist/client/assets/pie-ZZUOXDRM-pm57XGIg.js +1 -0
- package/dist/client/assets/{pieDiagram-DEJITSTG-BVAn8Lmr.js → pieDiagram-DEJITSTG-Debmhc0u.js} +1 -1
- package/dist/client/assets/prism-clojure-BpoF2XhX.js +1 -0
- package/dist/client/assets/{quadrantDiagram-34T5L4WZ-C2XZ_zxa.js → quadrantDiagram-34T5L4WZ-SE3g2BC9.js} +1 -1
- package/dist/client/assets/radar-PYXPWWZC-CH-AuSDw.js +1 -0
- package/dist/client/assets/{reduce-BTlHjXna.js → reduce-CG4cgj93.js} +1 -1
- package/dist/client/assets/{requirementDiagram-MS252O5E-CfO16pkI.js → requirementDiagram-MS252O5E-1mv41puC.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-XADWPNL6-D_4_234M.js → sankeyDiagram-XADWPNL6-CLjPRtOP.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-FGHM5R23-B-yHKMuK.js → sequenceDiagram-FGHM5R23-Cs-P3AtR.js} +1 -1
- package/dist/client/assets/src-5XpQHeIJ.js +1 -0
- package/dist/client/assets/{stateDiagram-FHFEXIEX-BeG2di4I.js → stateDiagram-FHFEXIEX-CmB1fohY.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-D6jsrR-f.js +1 -0
- package/dist/client/assets/{timeline-definition-GMOUNBTQ-DhtnMGcE.js → timeline-definition-GMOUNBTQ-BMUafJOI.js} +1 -1
- package/dist/client/assets/treeView-SZITEDCU-BGsVMAdJ.js +1 -0
- package/dist/client/assets/treemap-W4RFUUIX-DXnhegXy.js +1 -0
- package/dist/client/assets/{vennDiagram-DHZGUBPP-CBn69TcQ.js → vennDiagram-DHZGUBPP-CpZ1Qhjz.js} +1 -1
- package/dist/client/assets/wardley-RL74JXVD-COd5nWj-.js +1 -0
- package/dist/client/assets/{wardleyDiagram-NUSXRM2D-CEoSJmN1.js → wardleyDiagram-NUSXRM2D-C-zH0lsd.js} +1 -1
- package/dist/client/assets/{xychartDiagram-5P7HB3ND-BZ_X9tkn.js → xychartDiagram-5P7HB3ND-SkLFuEHZ.js} +1 -1
- package/dist/client/index.html +2 -4
- package/dist/client/site-data/blobs/080c0e6/cHVibGljL3NpdGUtZGF0YS9vZy1pbWFnZS5wbmc.png +0 -0
- package/dist/client/site-data/blobs/55f23a1/bGFuZGluZy9wdWJsaWMvZGlmaXQvbG9nby5wbmc.png +0 -0
- package/dist/client/site-data/blobs/66ff7c6/cHVibGljL2xvZ28ucG5n.png +0 -0
- package/dist/client/site-data/blobs/e6977fe/cHVibGljL2xvZ28ucG5n.png +0 -0
- package/dist/client/site-data/og-image.png +0 -0
- package/dist/server/file-watcher.d.ts +2 -1
- package/dist/server/file-watcher.js +9 -3
- package/dist/server/git-diff.d.ts +5 -0
- package/dist/server/git-diff.js +65 -1
- package/dist/server/git-diff.test.js +50 -0
- package/dist/server/server.js +265 -68
- package/dist/server/server.test.js +228 -0
- package/dist/tui/App.js +0 -1
- package/dist/types/diff.d.ts +4 -4
- package/dist/types/watch.d.ts +30 -1
- package/dist/utils/commentImports.d.ts +2 -0
- package/dist/utils/commentImports.js +119 -1
- package/dist/utils/editorOptions.d.ts +58 -35
- package/dist/utils/editorOptions.js +150 -24
- package/dist/utils/editorOptions.test.js +201 -9
- package/package.json +9 -6
- package/dist/client/assets/architecture-YZFGNWBL-Cs2Q6RQP.js +0 -1
- package/dist/client/assets/architectureDiagram-Q4EWVU46-BO4dVPUA.js +0 -36
- package/dist/client/assets/channel-_xDT1u3-.js +0 -1
- package/dist/client/assets/chunk-336JU56O-D1qa7Qzb.js +0 -2
- package/dist/client/assets/chunk-426QAEUC-6J_A_wvD.js +0 -1
- package/dist/client/assets/chunk-CFjPhJqf.js +0 -1
- package/dist/client/assets/chunk-QZHKN3VN-C0QzfgZ8.js +0 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-5XrS-DAQ.js +0 -1
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-Covl2vKy.js +0 -1
- package/dist/client/assets/clone-rhRH8pyW.js +0 -1
- package/dist/client/assets/cose-bilkent-S5V4N54A-BvXFc7Rr.js +0 -1
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-CMIzlKAR.js +0 -292
- package/dist/client/assets/gitGraph-7Q5UKJZL-A_wWsXju.js +0 -1
- package/dist/client/assets/index-Cq_APK7Y.css +0 -2
- package/dist/client/assets/index-RcU838Ah.js +0 -79
- package/dist/client/assets/info-OMHHGYJF-Bv3kK2Bb.js +0 -1
- package/dist/client/assets/mermaid-parser.core-CnJ9Tv8l.js +0 -4
- package/dist/client/assets/packet-4T2RLAQJ-D2q3-9ae.js +0 -1
- package/dist/client/assets/pie-ZZUOXDRM-GivlQcUF.js +0 -1
- package/dist/client/assets/preload-helper-DSXbuxSR.js +0 -1
- package/dist/client/assets/radar-PYXPWWZC-C9pD6VNR.js +0 -1
- package/dist/client/assets/src-CjDs0_Ij.js +0 -1
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-DvcSq7KE.js +0 -1
- package/dist/client/assets/treeView-SZITEDCU-BSNk8_yV.js +0 -1
- package/dist/client/assets/treemap-W4RFUUIX-ym4zQztE.js +0 -1
- package/dist/client/assets/wardley-RL74JXVD-B02H6ReJ.js +0 -1
- /package/dist/client/assets/{array-BNor45A1.js → array-DOVTz2Mq.js} +0 -0
- /package/dist/client/assets/{defaultLocale-DPzUsThw.js → defaultLocale-Ck2Xxk-C.js} +0 -0
- /package/dist/client/assets/{init-C0L3woqb.js → init-Bft5Ffpj.js} +0 -0
- /package/dist/client/assets/{katex-FOM3xZj7.js → katex-CeIlAR55.js} +0 -0
- /package/dist/client/assets/{path-sMK4d_s9.js → path-DfRbCp9y.js} +0 -0
- /package/dist/client/assets/{prism-bash-iQBez6et.js → prism-bash-CPkZUJMA.js} +0 -0
- /package/dist/client/assets/{prism-csharp-C1RDHXRk.js → prism-csharp-BEk8D1-3.js} +0 -0
- /package/dist/client/assets/{prism-dart-nIH9vDUM.js → prism-dart-ByLYrdQB.js} +0 -0
- /package/dist/client/assets/{prism-elixir-DUMUOd7H.js → prism-elixir-BZtyIEab.js} +0 -0
- /package/dist/client/assets/{prism-haskell-BP3SRvzt.js → prism-haskell-NAsbeo3V.js} +0 -0
- /package/dist/client/assets/{prism-hcl-C-ZHJGEE.js → prism-hcl-crnGqmVp.js} +0 -0
- /package/dist/client/assets/{prism-java-scuShSv5.js → prism-java-BovStacA.js} +0 -0
- /package/dist/client/assets/{prism-markup-templating-BFXREXfb.js → prism-markup-templating-Cl8NiLjy.js} +0 -0
- /package/dist/client/assets/{prism-nix-CO4UPu3E.js → prism-nix-BS_cm_1n.js} +0 -0
- /package/dist/client/assets/{prism-perl-BBDKnHRR.js → prism-perl-DGLVMq5H.js} +0 -0
- /package/dist/client/assets/{prism-php-DjIafOi_.js → prism-php-BskSwJN8.js} +0 -0
- /package/dist/client/assets/{prism-protobuf-BE1MoFmZ.js → prism-protobuf-DfbIYpO7.js} +0 -0
- /package/dist/client/assets/{prism-ruby-CZ-lrXfL.js → prism-ruby-FBVh1PRE.js} +0 -0
- /package/dist/client/assets/{prism-scala-DgnxHuDn.js → prism-scala--9AfMHPY.js} +0 -0
- /package/dist/client/assets/{prism-solidity-5fSUcW9Y.js → prism-solidity-BgJNkj1z.js} +0 -0
- /package/dist/client/assets/{prism-sql-CKkohPI_.js → prism-sql-C9Czmpov.js} +0 -0
- /package/dist/client/assets/{prism-vim-CkRmxTmK.js → prism-vim-CzUNf0WQ.js} +0 -0
- /package/dist/client/assets/{rough.esm-DeLgKbOI.js → rough.esm-Bbn_-PMU.js} +0 -0
package/dist/server/server.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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:
|
|
415
|
+
id: threadId,
|
|
330
416
|
body: comment.body,
|
|
331
417
|
author: comment.author,
|
|
332
|
-
createdAt:
|
|
333
|
-
updatedAt:
|
|
418
|
+
createdAt: timestamp,
|
|
419
|
+
updatedAt: timestamp,
|
|
334
420
|
},
|
|
335
421
|
],
|
|
336
422
|
};
|
|
337
423
|
}
|
|
338
|
-
function
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
380
|
-
const output = formatCommentsOutput(
|
|
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
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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]);
|