diffing 0.16.1 → 0.18.0
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/dist/{cli-agent-Dk0yRtrA.mjs → cli-agent-du9TS34N.mjs} +763 -20
- package/dist/{cli-gh-BvoDabKK.mjs → cli-gh-2NEumKQB.mjs} +1 -1
- package/dist/cli.mjs +5 -4
- package/dist/client/assets/{angular-html-ByeJB0rh.js → angular-html-Des_xD-2.js} +1 -1
- package/dist/client/assets/{angular-ts-CiwZGajG.js → angular-ts-FXZjKR0Q.js} +1 -1
- package/dist/client/assets/{apl-C8k5a5fk.js → apl-CU0GH5rh.js} +1 -1
- package/dist/client/assets/{arc-B8VMoqVr.js → arc-DnvfnvVv.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-C-ng2xNL.js → architectureDiagram-3BPJPVTR-Cb41y9QS.js} +1 -1
- package/dist/client/assets/{astro-BE-h-qpp.js → astro-CUWNJBWq.js} +1 -1
- package/dist/client/assets/{blade-D-0BV1wW.js → blade-2PPtLT0e.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-DyAQuONU.js → blockDiagram-GPEHLZMM-BYxsiST2.js} +1 -1
- package/dist/client/assets/{c-Dg_Gx29H.js → c-D3K42QQV.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-k5uKTHvm.js → c4Diagram-AAUBKEIU-BU7I7J1w.js} +1 -1
- package/dist/client/assets/channel-Cx6nXkw1.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Dod_VwYj.js → chunk-2J33WTMH-DQnKNewn.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-B8HMjry-.js → chunk-3OPIFGDE-BE6bYzdw.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-TvkJVIZw.js → chunk-4BX2VUAB-CTw-Pjas.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-BsXuKAKc.js → chunk-55IACEB6-BBCsXXSw.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-BI1cj2cZ.js → chunk-5ZQYHXKU-BrM_4KH1.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-yWQ27Wk0.js → chunk-727SXJPM-B_Vz3jyP.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-B2BBCtTr.js → chunk-AQP2D5EJ-CHSGbbjn.js} +1 -1
- package/dist/client/assets/{chunk-BSJP7CBP-CXXvPXhK.js → chunk-BSJP7CBP-D6jlDOzg.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-jYiloqyi.js → chunk-CSCIHK7Q-D_02XyF0.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-DZzqIT32.js → chunk-FMBD7UC4-DxbqnfDH.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-krdCf8iG.js → chunk-KSCS5N6A-_Pc1IHYq.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-0uuTmeZV.js → chunk-L5ZTLDWV-Dj8OTMjE.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-BBfUgD06.js → chunk-LZXEDZCA-DQn_LdxZ.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-jkir4ZKE.js → chunk-ND2GUHAM-BYbHwrXp.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-DMWZNibJ.js → chunk-NZK2D7GU-Bvgyac_1.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-B1e1HGaX.js → chunk-O5CBEL6O-DeRRUqxP.js} +1 -1
- package/dist/client/assets/chunk-QZHKN3VN-BkYYlBcO.js +1 -0
- package/dist/client/assets/chunk-WU5MYG2G-DyGKxcFl.js +1 -0
- package/dist/client/assets/{chunk-XPW4576I-AvbCAUav.js → chunk-XPW4576I-BrtDtomN.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-DwPqBLeK.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Mxl80axQ.js +1 -0
- package/dist/client/assets/{cobol-DQnGB_GV.js → cobol-hL9_ibhX.js} +1 -1
- package/dist/client/assets/{coffee-CDV5a89x.js → coffee-DFRt5qt1.js} +1 -1
- package/dist/client/assets/{cose-bilkent-S5V4N54A-t-mDNRF2.js → cose-bilkent-S5V4N54A-DHCJqbho.js} +1 -1
- package/dist/client/assets/{cpp-8G6b-siI.js → cpp-3A0aT-dF.js} +1 -1
- package/dist/client/assets/{crystal-CGYCC6FM.js → crystal-BzBxs8w6.js} +1 -1
- package/dist/client/assets/{css-nez4R8_Z.js → css-C5lLpDbg.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG--azesYIg.js → dagre-BM42HDAG-DOuf0bNE.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-4WJ7nKhA.js → diagram-2AECGRRQ-Bd5Jiq1y.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-C7ZGhLXn.js → diagram-5GNKFQAL-DvYLKuEY.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BobK_rbl.js → diagram-KO2AKTUF-BVvqGAtk.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-BOc-OkIS.js → diagram-LMA3HP47-QqHAHEnc.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BEtLgm86.js → diagram-OG6HWLK6-CkG5SOks.js} +1 -1
- package/dist/client/assets/{dist-DPuBwToc.js → dist-Dh8NC3tY.js} +1 -1
- package/dist/client/assets/{edge-Z5RMbJQv.js → edge-9n_aVc7U.js} +1 -1
- package/dist/client/assets/{edit-CFxbgVPV.js → edit-DlkvWCkd.js} +1 -1
- package/dist/client/assets/{elixir-d3C79FQc.js → elixir-C7ssdzaK.js} +1 -1
- package/dist/client/assets/{elm-CDAf6Uyn.js → elm-CWs6E90H.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D-pR2GY9.js → erDiagram-TEJ5UH35-CfW0k0ip.js} +1 -1
- package/dist/client/assets/{erb-CTJk9FKH.js → erb-CdXoY37D.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-BwMCy2QV.js → flowDiagram-I6XJVG4X-CBKpQpqx.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-BI1WLJDi.js → ganttDiagram-6RSMTGT7-CS13EpgX.js} +1 -1
- package/dist/client/assets/{git-rebase-Dr0-MVoW.js → git-rebase-BSoXZvcc.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CUfW0k1f.js → gitGraphDiagram-PVQCEYII-BN0i9pDL.js} +1 -1
- package/dist/client/assets/{glimmer-js-CXhLhyW-.js → glimmer-js-BZMH6HKs.js} +1 -1
- package/dist/client/assets/{glimmer-ts-BFAFT-VD.js → glimmer-ts-DXSo27g6.js} +1 -1
- package/dist/client/assets/{glsl-D7yanhgY.js → glsl-Cl8JUVz8.js} +1 -1
- package/dist/client/assets/{graphql-CpoDxrok.js → graphql-CNR_SNqJ.js} +1 -1
- package/dist/client/assets/{hack-BL5fmC4q.js → hack-iAxNUz76.js} +1 -1
- package/dist/client/assets/{haml-ClAE6RVM.js → haml-D11gKVlW.js} +1 -1
- package/dist/client/assets/{handlebars-nHw91uOv.js → handlebars-BAAs9kjt.js} +1 -1
- package/dist/client/assets/{html-BIiOKqgG.js → html-C-zWst2_.js} +1 -1
- package/dist/client/assets/{html-derivative-D1nPnuOK.js → html-derivative-Bu6k8r8J.js} +1 -1
- package/dist/client/assets/{http-B_QcRZjt.js → http-B_xQ7YQ1.js} +1 -1
- package/dist/client/assets/{hurl-CZE0DsD3.js → hurl-B7IYz3qL.js} +1 -1
- package/dist/client/assets/index-1GDpp-wO.css +1 -0
- package/dist/client/assets/{index-Erqhm7rW.js → index-D_eRtjau.js} +49 -49
- package/dist/client/assets/{infoDiagram-5YYISTIA-w2M_OHua.js → infoDiagram-5YYISTIA-Choq89cB.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CW_l9PGx.js → ishikawaDiagram-YF4QCWOH-CZY99mk-.js} +1 -1
- package/dist/client/assets/{java-CXTqX09w.js → java-BPJVzio0.js} +1 -1
- package/dist/client/assets/{javascript-9z9ABBep.js → javascript-DiOMtf_A.js} +1 -1
- package/dist/client/assets/{jinja-Cluf_5Bk.js → jinja-l7JM-8PK.js} +1 -1
- package/dist/client/assets/{jison-CqCEUtc4.js → jison-CAc6lM-g.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-D-YwusBa.js → journeyDiagram-JHISSGLW-CDbADAMs.js} +1 -1
- package/dist/client/assets/{json-BMq_bPP8.js → json-DqZO-AEh.js} +1 -1
- package/dist/client/assets/{jsx-Djij59b9.js → jsx-UqnCpKTP.js} +1 -1
- package/dist/client/assets/{julia-DybzCMnh.js → julia-DlbfYJz3.js} +1 -1
- package/dist/client/assets/{just-4ChcAr9-.js → just-9ze8Wi6d.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Brvoukmm.js → kanban-definition-UN3LZRKU-DgrkeisF.js} +1 -1
- package/dist/client/assets/{latex-CS2TtFXG.js → latex-_9Fzq-E3.js} +1 -1
- package/dist/client/assets/{line-CqxM-stA.js → line-b-Rcrqbg.js} +1 -1
- package/dist/client/assets/{linear-C7T82059.js → linear-C_RFbWEw.js} +1 -1
- package/dist/client/assets/{liquid-84AtYenm.js → liquid-CMiBoRv6.js} +1 -1
- package/dist/client/assets/{lua-DJzFx3Cf.js → lua-BzfULkCh.js} +1 -1
- package/dist/client/assets/{marko-BLw9BV9E.js → marko-BfcVqnF5.js} +1 -1
- package/dist/client/assets/{mdc-BjZejN3b.js → mdc-BKqkdcKC.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-BdiEBAsd.js → mermaid-parser.core-DiJRF2JX.js} +1 -1
- package/dist/client/assets/{mermaid.core-BsnKhnrM.js → mermaid.core-2WkwaCuC.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-CBaIFnbc.js → mindmap-definition-RKZ34NQL-BYxdNWsh.js} +1 -1
- package/dist/client/assets/{nginx-DuamCwvX.js → nginx-DiNLSsbI.js} +1 -1
- package/dist/client/assets/{nim-Bt8c9-ge.js → nim-CohF0tPX.js} +1 -1
- package/dist/client/assets/{perl-D52AfmwC.js → perl-Q-a6BHYR.js} +1 -1
- package/dist/client/assets/{php-BBMlJg0_.js → php-DEj4odG7.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BlmPajcA.js → pieDiagram-4H26LBE5-DIMAUXZJ.js} +1 -1
- package/dist/client/assets/{pug-DFHmDqEE.js → pug-vqq1Jo-6.js} +1 -1
- package/dist/client/assets/{qml-C0333o-g.js → qml-DkOoIH_j.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Bqlm_zZk.js → quadrantDiagram-W4KKPZXB-DKoe-k2E.js} +1 -1
- package/dist/client/assets/{r-DZFmvIgR.js → r-Cen6c1F0.js} +1 -1
- package/dist/client/assets/{razor-BIGLkWzR.js → razor-SY8AQA9t.js} +1 -1
- package/dist/client/assets/{regexp-BW6jpgf9.js → regexp-CjphwwlN.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-B1PrtUeD.js → requirementDiagram-4Y6WPE33-Du9fS-Dw.js} +1 -1
- package/dist/client/assets/{review-export-BlgD3bdw.js → review-export-FIT7B1uP.js} +1 -1
- package/dist/client/assets/{rst-Cj51ZJaa.js → rst-B79v9bjn.js} +1 -1
- package/dist/client/assets/{ruby-lMhGjwpf.js → ruby-Dia4QaOO.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-D2HtUNVF.js → sankeyDiagram-5OEKKPKP-CsezsM2Z.js} +1 -1
- package/dist/client/assets/{sas-BWSQku7l.js → sas-CYRTwHiQ.js} +1 -1
- package/dist/client/assets/{scss-CdRPuhLN.js → scss-DNWCy5-q.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-D9SQy0XI.js → sequenceDiagram-3UESZ5HK-CNovbaFC.js} +1 -1
- package/dist/client/assets/{shellscript-DOfOBXcj.js → shellscript-BSQLDkAe.js} +1 -1
- package/dist/client/assets/{shellsession-7XAOCVni.js → shellsession-CfvHV3oR.js} +1 -1
- package/dist/client/assets/{soy-DDnzVRIw.js → soy-DLwu5RtN.js} +1 -1
- package/dist/client/assets/{sql-Ba34agKe.js → sql-DlyzTy0P.js} +1 -1
- package/dist/client/assets/{src-BO-qT_v_.js → src-DNJJnOd8.js} +1 -1
- package/dist/client/assets/{stata-BQlYGHQU.js → stata-Djh-WYVT.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-7IEYjFRo.js → stateDiagram-AJRCARHV-cV6R_6CN.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-jmBy204V.js +1 -0
- package/dist/client/assets/{surrealql-BET1l7SW.js → surrealql-Cu-G0NVH.js} +1 -1
- package/dist/client/assets/{svelte-Bd7ZOeQI.js → svelte-CDA2MakI.js} +1 -1
- package/dist/client/assets/{templ-BYvRQdOw.js → templ-H2xc1tDE.js} +1 -1
- package/dist/client/assets/{tex-CT8DpKxX.js → tex-DJ6mQsYC.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-CurQYWmi.js → timeline-definition-PNZ67QCA-BlCMASdx.js} +1 -1
- package/dist/client/assets/{ts-tags-D4iPPwau.js → ts-tags-bYEWgmJO.js} +1 -1
- package/dist/client/assets/{tsx-Dgk_Aw-B.js → tsx-CtTBiWFd.js} +1 -1
- package/dist/client/assets/{twig-abRNWeh3.js → twig-CgXr9wNF.js} +1 -1
- package/dist/client/assets/{typescript-BgR1JqdC.js → typescript-Na7Lj9Vl.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-DbrRBL5S.js → vennDiagram-CIIHVFJN-CvcrIycD.js} +1 -1
- package/dist/client/assets/{vue-DSkhKq4Q.js → vue-BtOd_AfK.js} +1 -1
- package/dist/client/assets/{vue-html-J0jWoD9X.js → vue-html-CVlcjzDD.js} +1 -1
- package/dist/client/assets/{vue-vine-BgX5MC8-.js → vue-vine-k7M6KtfE.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DIFXQsoP.js → wardleyDiagram-YWT4CUSO-BUDJvh59.js} +1 -1
- package/dist/client/assets/{xml-Ca53xGLB.js → xml-Brz46P3k.js} +1 -1
- package/dist/client/assets/{xsl-CNnXclYV.js → xsl-Bh5vdfXi.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-CmfInPqa.js → xychartDiagram-2RQKCTM6-T2Bb0lF_.js} +1 -1
- package/dist/client/assets/{yaml-CjjyUhfg.js → yaml-CRsmB7sL.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/{completions-7YSX1neu.mjs → completions-2aHVTmiJ.mjs} +17 -0
- package/dist/completions-CYa5usLb.mjs +2 -0
- package/dist/{mcp-BgrRjzn3.mjs → mcp-H7ytYScV.mjs} +430 -23
- package/dist/mockup-format-CM3ivHHX.mjs +322 -0
- package/dist/native/tui-darwin-arm64/diffing-tui +0 -0
- package/dist/native/tui-darwin-x64/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-musl/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-musl/diffing-tui +0 -0
- package/dist/native/tui-win32-x64-msvc/diffing-tui.exe +0 -0
- package/dist/{review-export-GW4KNsGN.mjs → review-export-DoST2oPt.mjs} +1 -1
- package/dist/{server-Ct9QuV1l.mjs → server-CiE24vh-.mjs} +1842 -53
- package/dist/{setup-BnS_rSxK.mjs → setup-BwWZ9Fzq.mjs} +1 -1
- package/extensions/pi/index.ts +453 -5
- package/package.json +2 -1
- package/dist/client/assets/channel-DsuXeLi4.js +0 -1
- package/dist/client/assets/chunk-QZHKN3VN-D5b3mBJ2.js +0 -1
- package/dist/client/assets/chunk-WU5MYG2G-C1Pa4Y04.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-DCnVfYt-.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BPa1Jkt-.js +0 -1
- package/dist/client/assets/index-BR6EGJ-g.css +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DP6swlL9.js +0 -1
- package/dist/completions-5rtfoxB9.mjs +0 -2
- package/dist/plan-format-BwJh5JSE.mjs +0 -144
- /package/dist/{comment-format-B85NHZnG.mjs → comment-format-D4mCqr7T.mjs} +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { C as listRepoFiles, D as toSafeRelativePath, E as isSafePath, S as isGitRepo, T as revertHunk, _ as getShowDiff, a as getCommitSeriesSummary, b as getUntrackedFilePathsAsync, d as getMergeStatus, f as getProjectStorageDir, h as getRepoRoot, l as getGitDiffAsync, m as getRepoMetadataAsync, o as getCustomGitDiffAsync, s as getFileContent, u as getHunkHistory, x as gitAddFile, y as getTabSizeForFilesAsync } from "./git-BGj31GX-.mjs";
|
|
2
2
|
import { a as searchFiles, i as searchContent, o as searchSymbols, r as searchAll, s as trackSelection, t as getSearchStatus } from "./search-3R2E5xxr.mjs";
|
|
3
3
|
import { i as loadSettings, s as saveSettings } from "./settings-DkPDB_hS.mjs";
|
|
4
|
-
import { t as
|
|
5
|
-
import {
|
|
4
|
+
import { a as MOCKUP_MAX_SCREEN_BYTES, c as defaultScreenLabel, d as normalizeSubmitScreens, i as sectionTitleForLine, l as isMockupViewport, n as extractPlanLines, o as MOCKUP_SCREEN_ID_RE, r as formatPlanReview, s as commentViewport, t as formatMockupReview, u as normalizeMockupViewport } from "./mockup-format-CM3ivHHX.mjs";
|
|
5
|
+
import { t as formatComments } from "./comment-format-D4mCqr7T.mjs";
|
|
6
6
|
import { g as createServerAuthMiddleware, h as buildSessionTokenSetCookieValue, v as injectSessionTokenIntoHtml } from "./server-lock-CdaUNZyX.mjs";
|
|
7
|
-
import { D as setPrReviewThreadResolved, E as resolvedFromSession, O as submitReview,
|
|
7
|
+
import { D as setPrReviewThreadResolved, E as resolvedFromSession, O as submitReview, i as deletePrReviewComment, k as updatePrReviewComment, l as fetchExistingCommentsViaGh, p as fetchPrFileContentViaGh, t as buildPrSession, u as fetchExistingReviewsViaGh, w as refreshPrSession } from "./github-CToLsvPu.mjs";
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { dirname, extname, join, resolve } from "node:path";
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, watch } from "node:fs";
|
|
13
13
|
import { parseArgs } from "node:util";
|
|
14
|
-
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { Hono } from "hono";
|
|
17
17
|
import { streamSSE } from "hono/streaming";
|
|
@@ -1446,7 +1446,7 @@ function isReviewCommentSide(value) {
|
|
|
1446
1446
|
function planSourceFilePath(storageDir, planId) {
|
|
1447
1447
|
return join(storageDir, "plan-sources", `${planId}.md`);
|
|
1448
1448
|
}
|
|
1449
|
-
function newId() {
|
|
1449
|
+
function newId$1() {
|
|
1450
1450
|
return crypto.randomUUID();
|
|
1451
1451
|
}
|
|
1452
1452
|
/**
|
|
@@ -1486,7 +1486,7 @@ function markBackfilled(plan) {
|
|
|
1486
1486
|
plan.__backfilledVersions = true;
|
|
1487
1487
|
}
|
|
1488
1488
|
/** Shared mutation helpers so both stores behave identically. */
|
|
1489
|
-
function applyUpsert(plans, input, now) {
|
|
1489
|
+
function applyUpsert$1(plans, input, now) {
|
|
1490
1490
|
if (input.id) {
|
|
1491
1491
|
const existing = plans.find((p) => p.id === input.id);
|
|
1492
1492
|
if (existing) {
|
|
@@ -1512,7 +1512,7 @@ function applyUpsert(plans, input, now) {
|
|
|
1512
1512
|
}
|
|
1513
1513
|
}
|
|
1514
1514
|
const plan = {
|
|
1515
|
-
id: input.id || newId(),
|
|
1515
|
+
id: input.id || newId$1(),
|
|
1516
1516
|
title: input.title,
|
|
1517
1517
|
body: input.body,
|
|
1518
1518
|
source: input.source,
|
|
@@ -1539,7 +1539,7 @@ function applyUpsert(plans, input, now) {
|
|
|
1539
1539
|
* fields after a non-resubmit edit (PUT). Doesn't add a new entry — that only
|
|
1540
1540
|
* happens via `upsert` (the resubmit flow).
|
|
1541
1541
|
*/
|
|
1542
|
-
function syncCurrentVersion(plan) {
|
|
1542
|
+
function syncCurrentVersion$1(plan) {
|
|
1543
1543
|
if (!plan.versions || plan.versions.length === 0) plan.versions = [];
|
|
1544
1544
|
if (plan.versions.length === 0) {
|
|
1545
1545
|
plan.versions.push({
|
|
@@ -1638,7 +1638,7 @@ var FilePlanStore = class {
|
|
|
1638
1638
|
}
|
|
1639
1639
|
async upsert(input) {
|
|
1640
1640
|
const plans = await this.getAll();
|
|
1641
|
-
const plan = applyUpsert(plans, input, Date.now());
|
|
1641
|
+
const plan = applyUpsert$1(plans, input, Date.now());
|
|
1642
1642
|
backfillPlan(plan);
|
|
1643
1643
|
await this.writeSourceMirror(plan);
|
|
1644
1644
|
await this.save(plans);
|
|
@@ -1653,7 +1653,7 @@ var FilePlanStore = class {
|
|
|
1653
1653
|
if (fields.source !== void 0) plan.source = fields.source;
|
|
1654
1654
|
if (fields.model !== void 0) plan.model = fields.model;
|
|
1655
1655
|
plan.updatedAt = Date.now();
|
|
1656
|
-
syncCurrentVersion(plan);
|
|
1656
|
+
syncCurrentVersion$1(plan);
|
|
1657
1657
|
backfillPlan(plan);
|
|
1658
1658
|
if (fields.body !== void 0) await this.writeSourceMirror(plan);
|
|
1659
1659
|
await this.save(plans);
|
|
@@ -1732,18 +1732,1041 @@ var FilePlanStore = class {
|
|
|
1732
1732
|
return true;
|
|
1733
1733
|
});
|
|
1734
1734
|
}
|
|
1735
|
-
async updateReply(planId, commentId, replyId, body) {
|
|
1736
|
-
return this.mutate(planId, (plan) => {
|
|
1737
|
-
const c = plan.comments.find((x) => x.id === commentId);
|
|
1738
|
-
if (!c) return false;
|
|
1739
|
-
const reply = c.replies.find((r) => r.id === replyId);
|
|
1740
|
-
if (!reply) return false;
|
|
1741
|
-
reply.body = body;
|
|
1742
|
-
return true;
|
|
1743
|
-
});
|
|
1735
|
+
async updateReply(planId, commentId, replyId, body) {
|
|
1736
|
+
return this.mutate(planId, (plan) => {
|
|
1737
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1738
|
+
if (!c) return false;
|
|
1739
|
+
const reply = c.replies.find((r) => r.id === replyId);
|
|
1740
|
+
if (!reply) return false;
|
|
1741
|
+
reply.body = body;
|
|
1742
|
+
return true;
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
//#endregion
|
|
1747
|
+
//#region src/lib/mockups.ts
|
|
1748
|
+
function mockupSourceDir(storageDir, mockupId) {
|
|
1749
|
+
return join(storageDir, "mockup-sources", mockupId);
|
|
1750
|
+
}
|
|
1751
|
+
function newId() {
|
|
1752
|
+
return crypto.randomUUID();
|
|
1753
|
+
}
|
|
1754
|
+
function snapshotScreens(screens) {
|
|
1755
|
+
return screens.map((s) => ({
|
|
1756
|
+
id: s.id,
|
|
1757
|
+
label: s.label,
|
|
1758
|
+
html: s.html
|
|
1759
|
+
}));
|
|
1760
|
+
}
|
|
1761
|
+
function backfillMockup(mockup) {
|
|
1762
|
+
const currentVersion = mockup.version ?? 1;
|
|
1763
|
+
const have = Array.isArray(mockup.versions) ? mockup.versions.length : 0;
|
|
1764
|
+
if (!mockup.versions || have === 0 || have < currentVersion) {
|
|
1765
|
+
const createdAt = mockup.updatedAt ?? mockup.createdAt ?? Date.now();
|
|
1766
|
+
const haveEntries = Array.isArray(mockup.versions) ? mockup.versions : [];
|
|
1767
|
+
const next = [];
|
|
1768
|
+
for (let i = 1; i <= currentVersion; i++) {
|
|
1769
|
+
const existing = haveEntries.find((v) => v.version === i);
|
|
1770
|
+
next.push(existing ?? {
|
|
1771
|
+
version: i,
|
|
1772
|
+
title: mockup.title,
|
|
1773
|
+
screens: snapshotScreens(mockup.screens ?? []),
|
|
1774
|
+
source: mockup.source,
|
|
1775
|
+
model: mockup.model,
|
|
1776
|
+
createdAt
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
mockup.versions = next.filter((v) => v.version >= 1 && v.version <= currentVersion);
|
|
1780
|
+
}
|
|
1781
|
+
if (!mockup.screens) mockup.screens = [];
|
|
1782
|
+
if (!mockup.comments) mockup.comments = [];
|
|
1783
|
+
for (const c of mockup.comments) {
|
|
1784
|
+
if (typeof c.createdAtMockupVersion !== "number") c.createdAtMockupVersion = mockup.version ?? 1;
|
|
1785
|
+
if (!c.replies) c.replies = [];
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function applyUpsert(mockups, input, now) {
|
|
1789
|
+
if (input.id) {
|
|
1790
|
+
const existing = mockups.find((m) => m.id === input.id);
|
|
1791
|
+
if (existing) {
|
|
1792
|
+
existing.title = input.title;
|
|
1793
|
+
existing.screens = snapshotScreens(input.screens);
|
|
1794
|
+
if (input.source !== void 0) existing.source = input.source;
|
|
1795
|
+
if (input.model !== void 0) existing.model = input.model;
|
|
1796
|
+
existing.version += 1;
|
|
1797
|
+
existing.updatedAt = now;
|
|
1798
|
+
if (!existing.versions) existing.versions = [];
|
|
1799
|
+
existing.versions.push({
|
|
1800
|
+
version: existing.version,
|
|
1801
|
+
title: existing.title,
|
|
1802
|
+
screens: snapshotScreens(existing.screens),
|
|
1803
|
+
source: existing.source,
|
|
1804
|
+
model: existing.model,
|
|
1805
|
+
createdAt: now
|
|
1806
|
+
});
|
|
1807
|
+
existing.decision = "pending";
|
|
1808
|
+
existing.decisionComment = void 0;
|
|
1809
|
+
existing.decidedAt = void 0;
|
|
1810
|
+
return existing;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
const mockup = {
|
|
1814
|
+
id: input.id || newId(),
|
|
1815
|
+
title: input.title,
|
|
1816
|
+
screens: snapshotScreens(input.screens),
|
|
1817
|
+
source: input.source,
|
|
1818
|
+
model: input.model,
|
|
1819
|
+
createdAt: now,
|
|
1820
|
+
updatedAt: now,
|
|
1821
|
+
version: 1,
|
|
1822
|
+
decision: "pending",
|
|
1823
|
+
comments: [],
|
|
1824
|
+
versions: [{
|
|
1825
|
+
version: 1,
|
|
1826
|
+
title: input.title,
|
|
1827
|
+
screens: snapshotScreens(input.screens),
|
|
1828
|
+
source: input.source,
|
|
1829
|
+
model: input.model,
|
|
1830
|
+
createdAt: now
|
|
1831
|
+
}]
|
|
1832
|
+
};
|
|
1833
|
+
mockups.push(mockup);
|
|
1834
|
+
return mockup;
|
|
1835
|
+
}
|
|
1836
|
+
function syncCurrentVersion(mockup) {
|
|
1837
|
+
if (!mockup.versions || mockup.versions.length === 0) mockup.versions = [];
|
|
1838
|
+
if (mockup.versions.length === 0) {
|
|
1839
|
+
mockup.versions.push({
|
|
1840
|
+
version: mockup.version,
|
|
1841
|
+
title: mockup.title,
|
|
1842
|
+
screens: snapshotScreens(mockup.screens),
|
|
1843
|
+
source: mockup.source,
|
|
1844
|
+
model: mockup.model,
|
|
1845
|
+
createdAt: mockup.updatedAt
|
|
1846
|
+
});
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const last = mockup.versions[mockup.versions.length - 1];
|
|
1850
|
+
last.title = mockup.title;
|
|
1851
|
+
last.screens = snapshotScreens(mockup.screens);
|
|
1852
|
+
last.source = mockup.source;
|
|
1853
|
+
last.model = mockup.model;
|
|
1854
|
+
}
|
|
1855
|
+
function versionMismatch(expected, current) {
|
|
1856
|
+
if (expected === void 0) return void 0;
|
|
1857
|
+
if (expected !== current) return {
|
|
1858
|
+
expectedVersion: expected,
|
|
1859
|
+
currentVersion: current
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function bumpMockupVersion(mockup, now) {
|
|
1863
|
+
mockup.version += 1;
|
|
1864
|
+
mockup.updatedAt = now;
|
|
1865
|
+
if (!mockup.versions) mockup.versions = [];
|
|
1866
|
+
mockup.versions.push({
|
|
1867
|
+
version: mockup.version,
|
|
1868
|
+
title: mockup.title,
|
|
1869
|
+
screens: snapshotScreens(mockup.screens),
|
|
1870
|
+
source: mockup.source,
|
|
1871
|
+
model: mockup.model,
|
|
1872
|
+
createdAt: now
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
function applyScreenUpsert(mockup, screen, now) {
|
|
1876
|
+
const html = screen.html.replace(/\r\n/g, "\n");
|
|
1877
|
+
const existing = mockup.screens.find((s) => s.id === screen.id);
|
|
1878
|
+
if (existing) {
|
|
1879
|
+
existing.html = html;
|
|
1880
|
+
if (screen.label?.trim()) existing.label = screen.label.trim();
|
|
1881
|
+
} else mockup.screens.push({
|
|
1882
|
+
id: screen.id,
|
|
1883
|
+
label: screen.label?.trim() || defaultScreenLabel(screen.id),
|
|
1884
|
+
html
|
|
1885
|
+
});
|
|
1886
|
+
bumpMockupVersion(mockup, now);
|
|
1887
|
+
}
|
|
1888
|
+
function applyScreenPatch(screen, patch) {
|
|
1889
|
+
const idx = screen.html.indexOf(patch.expectedText);
|
|
1890
|
+
if (idx === -1) return 0;
|
|
1891
|
+
const count = screen.html.split(patch.expectedText).length - 1;
|
|
1892
|
+
screen.html = screen.html.slice(0, idx) + patch.replacement + screen.html.slice(idx + patch.expectedText.length);
|
|
1893
|
+
return count;
|
|
1894
|
+
}
|
|
1895
|
+
/** Validate every op against the mockup before applying any (all-or-nothing). */
|
|
1896
|
+
function validateThreadBatch(mockup, ops) {
|
|
1897
|
+
const comments = mockup.comments ?? [];
|
|
1898
|
+
for (let i = 0; i < ops.length; i++) {
|
|
1899
|
+
const item = ops[i];
|
|
1900
|
+
const c = comments.find((x) => x.id === item.commentId);
|
|
1901
|
+
if (!c) return `operations[${i}]: comment ${item.commentId} not found`;
|
|
1902
|
+
if ((item.op === "edit" || item.op === "delete") && item.replyId && !(c.replies ?? []).some((r) => r.id === item.replyId)) return `operations[${i}]: reply ${item.replyId} not found on comment ${item.commentId}`;
|
|
1903
|
+
}
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
/** Apply ops after validation passed — every lookup is guaranteed to hit. */
|
|
1907
|
+
function applyThreadOps(mockup, ops, now) {
|
|
1908
|
+
const results = [];
|
|
1909
|
+
for (const item of ops) {
|
|
1910
|
+
const c = mockup.comments.find((x) => x.id === item.commentId);
|
|
1911
|
+
switch (item.op) {
|
|
1912
|
+
case "reply": {
|
|
1913
|
+
const reply = {
|
|
1914
|
+
id: crypto.randomUUID(),
|
|
1915
|
+
body: item.body,
|
|
1916
|
+
createdAt: now,
|
|
1917
|
+
role: item.role === "agent" ? "agent" : item.role === "user" ? "user" : item.model ? "agent" : "user",
|
|
1918
|
+
model: item.model
|
|
1919
|
+
};
|
|
1920
|
+
if (typeof c.createdAtMockupVersion === "number") reply.createdAtPlanVersion = c.createdAtMockupVersion;
|
|
1921
|
+
if (!c.replies) c.replies = [];
|
|
1922
|
+
c.replies.push(reply);
|
|
1923
|
+
results.push({
|
|
1924
|
+
op: "reply",
|
|
1925
|
+
commentId: item.commentId,
|
|
1926
|
+
replyId: reply.id,
|
|
1927
|
+
ok: true
|
|
1928
|
+
});
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
case "edit":
|
|
1932
|
+
if (item.replyId) {
|
|
1933
|
+
const r = c.replies.find((x) => x.id === item.replyId);
|
|
1934
|
+
r.body = item.body;
|
|
1935
|
+
} else c.body = item.body;
|
|
1936
|
+
results.push({
|
|
1937
|
+
op: "edit",
|
|
1938
|
+
commentId: item.commentId,
|
|
1939
|
+
replyId: item.replyId,
|
|
1940
|
+
ok: true
|
|
1941
|
+
});
|
|
1942
|
+
break;
|
|
1943
|
+
case "delete":
|
|
1944
|
+
if (item.replyId) {
|
|
1945
|
+
const idx = c.replies.findIndex((x) => x.id === item.replyId);
|
|
1946
|
+
c.replies.splice(idx, 1);
|
|
1947
|
+
results.push({
|
|
1948
|
+
op: "delete",
|
|
1949
|
+
commentId: item.commentId,
|
|
1950
|
+
replyId: item.replyId,
|
|
1951
|
+
ok: true
|
|
1952
|
+
});
|
|
1953
|
+
} else {
|
|
1954
|
+
const idx = mockup.comments.findIndex((x) => x.id === item.commentId);
|
|
1955
|
+
mockup.comments.splice(idx, 1);
|
|
1956
|
+
results.push({
|
|
1957
|
+
op: "delete",
|
|
1958
|
+
commentId: item.commentId,
|
|
1959
|
+
ok: true
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
break;
|
|
1963
|
+
case "resolve":
|
|
1964
|
+
c.status = "resolved";
|
|
1965
|
+
results.push({
|
|
1966
|
+
op: "resolve",
|
|
1967
|
+
commentId: item.commentId,
|
|
1968
|
+
ok: true
|
|
1969
|
+
});
|
|
1970
|
+
break;
|
|
1971
|
+
case "unresolve":
|
|
1972
|
+
c.status = "open";
|
|
1973
|
+
results.push({
|
|
1974
|
+
op: "unresolve",
|
|
1975
|
+
commentId: item.commentId,
|
|
1976
|
+
ok: true
|
|
1977
|
+
});
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return results;
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Shared HTTP/CLI/MCP shape for the atomic thread batch. Validates every op's
|
|
1985
|
+
* shape before any store mutation; errors carry the offending op index.
|
|
1986
|
+
*/
|
|
1987
|
+
function normalizeThreadOperations(raw) {
|
|
1988
|
+
if (!Array.isArray(raw) || raw.length === 0) return {
|
|
1989
|
+
ok: false,
|
|
1990
|
+
error: "operations[] is required"
|
|
1991
|
+
};
|
|
1992
|
+
const ops = [];
|
|
1993
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1994
|
+
const item = raw[i];
|
|
1995
|
+
if (!item || typeof item !== "object") return {
|
|
1996
|
+
ok: false,
|
|
1997
|
+
error: `operations[${i}] must be an object`,
|
|
1998
|
+
index: i
|
|
1999
|
+
};
|
|
2000
|
+
const op = item.op;
|
|
2001
|
+
const commentId = typeof item.commentId === "string" ? item.commentId : "";
|
|
2002
|
+
if (!commentId) return {
|
|
2003
|
+
ok: false,
|
|
2004
|
+
error: `operations[${i}].commentId is required`,
|
|
2005
|
+
index: i
|
|
2006
|
+
};
|
|
2007
|
+
switch (op) {
|
|
2008
|
+
case "reply": {
|
|
2009
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2010
|
+
if (!body.trim()) return {
|
|
2011
|
+
ok: false,
|
|
2012
|
+
error: `operations[${i}].body is required`,
|
|
2013
|
+
index: i
|
|
2014
|
+
};
|
|
2015
|
+
ops.push({
|
|
2016
|
+
op: "reply",
|
|
2017
|
+
commentId,
|
|
2018
|
+
body,
|
|
2019
|
+
role: typeof item.role === "string" ? item.role : void 0,
|
|
2020
|
+
model: typeof item.model === "string" ? item.model : void 0
|
|
2021
|
+
});
|
|
2022
|
+
break;
|
|
2023
|
+
}
|
|
2024
|
+
case "edit": {
|
|
2025
|
+
const replyId = typeof item.replyId === "string" ? item.replyId : "";
|
|
2026
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2027
|
+
if (!body.trim()) return {
|
|
2028
|
+
ok: false,
|
|
2029
|
+
error: `operations[${i}].body is required`,
|
|
2030
|
+
index: i
|
|
2031
|
+
};
|
|
2032
|
+
ops.push({
|
|
2033
|
+
op: "edit",
|
|
2034
|
+
commentId,
|
|
2035
|
+
replyId: replyId || void 0,
|
|
2036
|
+
body
|
|
2037
|
+
});
|
|
2038
|
+
break;
|
|
2039
|
+
}
|
|
2040
|
+
case "delete": {
|
|
2041
|
+
const replyId = typeof item.replyId === "string" ? item.replyId : void 0;
|
|
2042
|
+
ops.push({
|
|
2043
|
+
op: "delete",
|
|
2044
|
+
commentId,
|
|
2045
|
+
replyId
|
|
2046
|
+
});
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
case "resolve":
|
|
2050
|
+
case "unresolve":
|
|
2051
|
+
ops.push({
|
|
2052
|
+
op,
|
|
2053
|
+
commentId
|
|
2054
|
+
});
|
|
2055
|
+
break;
|
|
2056
|
+
default: return {
|
|
2057
|
+
ok: false,
|
|
2058
|
+
error: `operations[${i}].op must be reply|edit|delete|resolve|unresolve`,
|
|
2059
|
+
index: i
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return {
|
|
2064
|
+
ok: true,
|
|
2065
|
+
ops
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
var FileMockupStore = class {
|
|
2069
|
+
dirPath;
|
|
2070
|
+
filePath;
|
|
2071
|
+
constructor(storageDir) {
|
|
2072
|
+
this.dirPath = storageDir ?? getProjectStorageDir();
|
|
2073
|
+
this.filePath = join(this.dirPath, "mockups.json");
|
|
2074
|
+
}
|
|
2075
|
+
async getAll() {
|
|
2076
|
+
try {
|
|
2077
|
+
const data = await readFile(this.filePath, "utf-8");
|
|
2078
|
+
const mockups = JSON.parse(data);
|
|
2079
|
+
let anyHealed = false;
|
|
2080
|
+
for (const m of mockups) {
|
|
2081
|
+
backfillMockup(m);
|
|
2082
|
+
if (!m.sourcePath) {
|
|
2083
|
+
m.sourcePath = mockupSourceDir(this.dirPath, m.id);
|
|
2084
|
+
anyHealed = true;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (anyHealed) try {
|
|
2088
|
+
await this.save(mockups);
|
|
2089
|
+
} catch {}
|
|
2090
|
+
return mockups;
|
|
2091
|
+
} catch {
|
|
2092
|
+
return [];
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
async get(id) {
|
|
2096
|
+
return (await this.getAll()).find((m) => m.id === id) ?? null;
|
|
2097
|
+
}
|
|
2098
|
+
async getVersion(id, version) {
|
|
2099
|
+
const mockup = await this.get(id);
|
|
2100
|
+
if (!mockup) return null;
|
|
2101
|
+
return mockup.versions.find((v) => v.version === version) ?? null;
|
|
2102
|
+
}
|
|
2103
|
+
async save(mockups) {
|
|
2104
|
+
try {
|
|
2105
|
+
await mkdir(this.dirPath, { recursive: true });
|
|
2106
|
+
try {
|
|
2107
|
+
const repoRoot = getRepoRoot();
|
|
2108
|
+
await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
|
|
2109
|
+
} catch {}
|
|
2110
|
+
await writeFile(this.filePath, JSON.stringify(mockups, null, 2), "utf-8");
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
console.error("Failed to save mockups to file:", err);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
async writeSourceMirror(mockup) {
|
|
2116
|
+
try {
|
|
2117
|
+
const dir = mockupSourceDir(this.dirPath, mockup.id);
|
|
2118
|
+
await mkdir(dir, { recursive: true });
|
|
2119
|
+
for (const screen of mockup.screens) await writeFile(join(dir, `${screen.id}.html`), screen.html, "utf-8");
|
|
2120
|
+
mockup.sourcePath = dir;
|
|
2121
|
+
} catch (err) {
|
|
2122
|
+
console.error("Failed to write mockup source mirror:", err);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
async upsert(input) {
|
|
2126
|
+
const mockups = await this.getAll();
|
|
2127
|
+
const mockup = applyUpsert(mockups, input, Date.now());
|
|
2128
|
+
backfillMockup(mockup);
|
|
2129
|
+
await this.writeSourceMirror(mockup);
|
|
2130
|
+
await this.save(mockups);
|
|
2131
|
+
return mockup;
|
|
2132
|
+
}
|
|
2133
|
+
async update(id, fields) {
|
|
2134
|
+
const mockups = await this.getAll();
|
|
2135
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2136
|
+
if (!mockup) return null;
|
|
2137
|
+
if (fields.title !== void 0) mockup.title = fields.title;
|
|
2138
|
+
if (fields.screens !== void 0) mockup.screens = snapshotScreens(fields.screens);
|
|
2139
|
+
if (fields.source !== void 0) mockup.source = fields.source;
|
|
2140
|
+
if (fields.model !== void 0) mockup.model = fields.model;
|
|
2141
|
+
mockup.updatedAt = Date.now();
|
|
2142
|
+
syncCurrentVersion(mockup);
|
|
2143
|
+
backfillMockup(mockup);
|
|
2144
|
+
if (fields.screens !== void 0) await this.writeSourceMirror(mockup);
|
|
2145
|
+
await this.save(mockups);
|
|
2146
|
+
return mockup;
|
|
2147
|
+
}
|
|
2148
|
+
async remove(id) {
|
|
2149
|
+
const mockups = await this.getAll();
|
|
2150
|
+
const idx = mockups.findIndex((m) => m.id === id);
|
|
2151
|
+
if (idx === -1) return false;
|
|
2152
|
+
mockups.splice(idx, 1);
|
|
2153
|
+
await this.save(mockups);
|
|
2154
|
+
try {
|
|
2155
|
+
await rm(mockupSourceDir(this.dirPath, id), {
|
|
2156
|
+
recursive: true,
|
|
2157
|
+
force: true
|
|
2158
|
+
});
|
|
2159
|
+
} catch {}
|
|
2160
|
+
return true;
|
|
2161
|
+
}
|
|
2162
|
+
async setDecision(id, decision, decisionComment) {
|
|
2163
|
+
const mockups = await this.getAll();
|
|
2164
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2165
|
+
if (!mockup) return null;
|
|
2166
|
+
mockup.decision = decision;
|
|
2167
|
+
mockup.decisionComment = decisionComment?.trim() || void 0;
|
|
2168
|
+
mockup.decidedAt = Date.now();
|
|
2169
|
+
mockup.updatedAt = mockup.decidedAt;
|
|
2170
|
+
await this.save(mockups);
|
|
2171
|
+
return mockup;
|
|
2172
|
+
}
|
|
2173
|
+
async mutate(mockupId, fn) {
|
|
2174
|
+
const mockups = await this.getAll();
|
|
2175
|
+
const mockup = mockups.find((m) => m.id === mockupId);
|
|
2176
|
+
if (!mockup) return null;
|
|
2177
|
+
if (!mockup.comments) mockup.comments = [];
|
|
2178
|
+
if (!fn(mockup)) return null;
|
|
2179
|
+
backfillMockup(mockup);
|
|
2180
|
+
await this.save(mockups);
|
|
2181
|
+
return mockup;
|
|
2182
|
+
}
|
|
2183
|
+
async addComment(mockupId, comment) {
|
|
2184
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2185
|
+
if (typeof comment.createdAtMockupVersion !== "number") comment.createdAtMockupVersion = mockup.version ?? 1;
|
|
2186
|
+
mockup.comments.push(comment);
|
|
2187
|
+
return true;
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
async updateComment(mockupId, commentId, fields) {
|
|
2191
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2192
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2193
|
+
if (!c) return false;
|
|
2194
|
+
if (fields.body !== void 0) c.body = fields.body;
|
|
2195
|
+
if (fields.status !== void 0) c.status = fields.status;
|
|
2196
|
+
return true;
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
async removeComment(mockupId, commentId) {
|
|
2200
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2201
|
+
const idx = mockup.comments.findIndex((x) => x.id === commentId);
|
|
2202
|
+
if (idx === -1) return false;
|
|
2203
|
+
mockup.comments.splice(idx, 1);
|
|
2204
|
+
return true;
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
async addReply(mockupId, commentId, reply) {
|
|
2208
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2209
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2210
|
+
if (!c) return false;
|
|
2211
|
+
if (typeof c.createdAtMockupVersion === "number" && typeof reply.createdAtPlanVersion !== "number") reply.createdAtPlanVersion = c.createdAtMockupVersion;
|
|
2212
|
+
if (!c.replies) c.replies = [];
|
|
2213
|
+
c.replies.push(reply);
|
|
2214
|
+
return true;
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
async removeReply(mockupId, commentId, replyId) {
|
|
2218
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2219
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2220
|
+
if (!c) return false;
|
|
2221
|
+
const idx = c.replies.findIndex((r) => r.id === replyId);
|
|
2222
|
+
if (idx === -1) return false;
|
|
2223
|
+
c.replies.splice(idx, 1);
|
|
2224
|
+
return true;
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
async updateReply(mockupId, commentId, replyId, body) {
|
|
2228
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2229
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2230
|
+
if (!c) return false;
|
|
2231
|
+
const reply = c.replies.find((r) => r.id === replyId);
|
|
2232
|
+
if (!reply) return false;
|
|
2233
|
+
reply.body = body;
|
|
2234
|
+
return true;
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
async upsertScreen(id, screen, opts) {
|
|
2238
|
+
const mockups = await this.getAll();
|
|
2239
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2240
|
+
if (!mockup) return {
|
|
2241
|
+
mockup: null,
|
|
2242
|
+
error: "Mockup not found"
|
|
2243
|
+
};
|
|
2244
|
+
const vm = versionMismatch(opts?.expectedVersion, mockup.version);
|
|
2245
|
+
if (vm) return {
|
|
2246
|
+
mockup: null,
|
|
2247
|
+
versionMismatch: vm
|
|
2248
|
+
};
|
|
2249
|
+
applyScreenUpsert(mockup, screen, Date.now());
|
|
2250
|
+
await this.writeSourceMirror(mockup);
|
|
2251
|
+
await this.save(mockups);
|
|
2252
|
+
return { mockup };
|
|
2253
|
+
}
|
|
2254
|
+
async removeScreen(id, screenId, opts) {
|
|
2255
|
+
const mockups = await this.getAll();
|
|
2256
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2257
|
+
if (!mockup) return {
|
|
2258
|
+
mockup: null,
|
|
2259
|
+
error: "Mockup not found"
|
|
2260
|
+
};
|
|
2261
|
+
const vm = versionMismatch(opts?.expectedVersion, mockup.version);
|
|
2262
|
+
if (vm) return {
|
|
2263
|
+
mockup: null,
|
|
2264
|
+
versionMismatch: vm
|
|
2265
|
+
};
|
|
2266
|
+
const idx = mockup.screens.findIndex((s) => s.id === screenId);
|
|
2267
|
+
if (idx === -1) return {
|
|
2268
|
+
mockup: null,
|
|
2269
|
+
error: `Screen ${screenId} not found`
|
|
2270
|
+
};
|
|
2271
|
+
if (mockup.screens.length <= 1) return {
|
|
2272
|
+
mockup: null,
|
|
2273
|
+
error: "Cannot remove the last screen"
|
|
2274
|
+
};
|
|
2275
|
+
mockup.screens.splice(idx, 1);
|
|
2276
|
+
bumpMockupVersion(mockup, Date.now());
|
|
2277
|
+
try {
|
|
2278
|
+
await unlink(join(mockupSourceDir(this.dirPath, id), `${screenId}.html`));
|
|
2279
|
+
} catch {}
|
|
2280
|
+
await this.save(mockups);
|
|
2281
|
+
return { mockup };
|
|
2282
|
+
}
|
|
2283
|
+
async patchScreen(id, screenId, patch, opts) {
|
|
2284
|
+
const mockups = await this.getAll();
|
|
2285
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2286
|
+
if (!mockup) return {
|
|
2287
|
+
mockup: null,
|
|
2288
|
+
error: "Mockup not found"
|
|
2289
|
+
};
|
|
2290
|
+
const vm = versionMismatch(opts?.expectedVersion, mockup.version);
|
|
2291
|
+
if (vm) return {
|
|
2292
|
+
mockup: null,
|
|
2293
|
+
versionMismatch: vm
|
|
2294
|
+
};
|
|
2295
|
+
const screen = mockup.screens.find((s) => s.id === screenId);
|
|
2296
|
+
if (!screen) return {
|
|
2297
|
+
mockup: null,
|
|
2298
|
+
error: `Screen ${screenId} not found`
|
|
2299
|
+
};
|
|
2300
|
+
if (!patch.expectedText) return {
|
|
2301
|
+
mockup: null,
|
|
2302
|
+
error: "expectedText is required"
|
|
2303
|
+
};
|
|
2304
|
+
if (patch.replacement === void 0) return {
|
|
2305
|
+
mockup: null,
|
|
2306
|
+
error: "replacement is required"
|
|
2307
|
+
};
|
|
2308
|
+
const occurrences = applyScreenPatch(screen, patch);
|
|
2309
|
+
if (occurrences === 0) return {
|
|
2310
|
+
mockup: null,
|
|
2311
|
+
error: "Exact text not found"
|
|
2312
|
+
};
|
|
2313
|
+
bumpMockupVersion(mockup, Date.now());
|
|
2314
|
+
await this.writeSourceMirror(mockup);
|
|
2315
|
+
await this.save(mockups);
|
|
2316
|
+
return {
|
|
2317
|
+
mockup,
|
|
2318
|
+
occurrences
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
async applyThreadBatch(mockupId, ops) {
|
|
2322
|
+
const mockups = await this.getAll();
|
|
2323
|
+
const mockup = mockups.find((m) => m.id === mockupId);
|
|
2324
|
+
if (!mockup) return {
|
|
2325
|
+
mockup: null,
|
|
2326
|
+
error: "Mockup not found",
|
|
2327
|
+
results: []
|
|
2328
|
+
};
|
|
2329
|
+
backfillMockup(mockup);
|
|
2330
|
+
const error = validateThreadBatch(mockup, ops);
|
|
2331
|
+
if (error) return {
|
|
2332
|
+
mockup: null,
|
|
2333
|
+
error,
|
|
2334
|
+
results: []
|
|
2335
|
+
};
|
|
2336
|
+
const results = applyThreadOps(mockup, ops, Date.now());
|
|
2337
|
+
await this.save(mockups);
|
|
2338
|
+
return {
|
|
2339
|
+
mockup,
|
|
2340
|
+
results
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
2344
|
+
/** Shared by HTTP/CLI so submit shapes stay one path. */
|
|
2345
|
+
function screensFromSubmitBody(body) {
|
|
2346
|
+
return normalizeSubmitScreens({
|
|
2347
|
+
html: typeof body.html === "string" ? body.html : void 0,
|
|
2348
|
+
screens: Array.isArray(body.screens) ? body.screens.map((item) => {
|
|
2349
|
+
const rec = item;
|
|
2350
|
+
return {
|
|
2351
|
+
id: typeof rec.id === "string" ? rec.id : void 0,
|
|
2352
|
+
label: typeof rec.label === "string" ? rec.label : void 0,
|
|
2353
|
+
html: typeof rec.html === "string" ? rec.html : ""
|
|
2354
|
+
};
|
|
2355
|
+
}) : void 0
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
//#endregion
|
|
2359
|
+
//#region src/lib/mockup-review-session.ts
|
|
2360
|
+
var MockupReviewSession = class {
|
|
2361
|
+
round = 0;
|
|
2362
|
+
lastDecidedAt = null;
|
|
2363
|
+
lastPayload = null;
|
|
2364
|
+
waiters = /* @__PURE__ */ new Set();
|
|
2365
|
+
onStatusChange;
|
|
2366
|
+
constructor(onStatusChange) {
|
|
2367
|
+
this.onStatusChange = onStatusChange;
|
|
2368
|
+
}
|
|
2369
|
+
await(opts) {
|
|
2370
|
+
const { sinceRound, timeoutMs, signal } = opts;
|
|
2371
|
+
if (sinceRound !== void 0 && sinceRound < this.round && this.lastPayload) return Promise.resolve({
|
|
2372
|
+
status: "released",
|
|
2373
|
+
payload: this.lastPayload
|
|
2374
|
+
});
|
|
2375
|
+
if (signal?.aborted) return Promise.resolve({
|
|
2376
|
+
status: "keep-waiting",
|
|
2377
|
+
round: this.round
|
|
2378
|
+
});
|
|
2379
|
+
return new Promise((resolve) => {
|
|
2380
|
+
const waiter = {
|
|
2381
|
+
resolve,
|
|
2382
|
+
timer: setTimeout(() => {
|
|
2383
|
+
this.removeWaiter(waiter);
|
|
2384
|
+
resolve({
|
|
2385
|
+
status: "keep-waiting",
|
|
2386
|
+
round: this.round
|
|
2387
|
+
});
|
|
2388
|
+
}, timeoutMs),
|
|
2389
|
+
cleanup: () => {}
|
|
2390
|
+
};
|
|
2391
|
+
if (signal) {
|
|
2392
|
+
const onAbort = () => {
|
|
2393
|
+
this.removeWaiter(waiter);
|
|
2394
|
+
resolve({
|
|
2395
|
+
status: "keep-waiting",
|
|
2396
|
+
round: this.round
|
|
2397
|
+
});
|
|
2398
|
+
};
|
|
2399
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2400
|
+
waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
2401
|
+
}
|
|
2402
|
+
this.waiters.add(waiter);
|
|
2403
|
+
this.emitStatus();
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
decide(input) {
|
|
2407
|
+
this.round += 1;
|
|
2408
|
+
this.lastDecidedAt = Date.now();
|
|
2409
|
+
const payload = {
|
|
2410
|
+
round: this.round,
|
|
2411
|
+
sentAt: this.lastDecidedAt,
|
|
2412
|
+
mockupId: input.mockup.id,
|
|
2413
|
+
decision: input.decision,
|
|
2414
|
+
decisionComment: input.decisionComment,
|
|
2415
|
+
reviewXml: input.reviewXml,
|
|
2416
|
+
openCommentCount: input.openCommentCount,
|
|
2417
|
+
mockup: input.mockup,
|
|
2418
|
+
mode: input.mode ?? "standard"
|
|
2419
|
+
};
|
|
2420
|
+
this.lastPayload = payload;
|
|
2421
|
+
for (const waiter of [...this.waiters]) {
|
|
2422
|
+
this.removeWaiter(waiter);
|
|
2423
|
+
waiter.resolve({
|
|
2424
|
+
status: "released",
|
|
2425
|
+
payload
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
this.emitStatus();
|
|
2429
|
+
return payload;
|
|
2430
|
+
}
|
|
2431
|
+
snapshot() {
|
|
2432
|
+
return {
|
|
2433
|
+
round: this.round,
|
|
2434
|
+
waiters: this.waiters.size,
|
|
2435
|
+
lastDecidedAt: this.lastDecidedAt
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
removeWaiter(waiter) {
|
|
2439
|
+
clearTimeout(waiter.timer);
|
|
2440
|
+
waiter.cleanup();
|
|
2441
|
+
this.waiters.delete(waiter);
|
|
2442
|
+
this.emitStatus();
|
|
2443
|
+
}
|
|
2444
|
+
emitStatus() {
|
|
2445
|
+
this.onStatusChange?.(this.snapshot());
|
|
1744
2446
|
}
|
|
1745
2447
|
};
|
|
1746
2448
|
//#endregion
|
|
2449
|
+
//#region src/lib/mockup-document.ts
|
|
2450
|
+
/**
|
|
2451
|
+
* Inject a selection probe into stored mockup HTML.
|
|
2452
|
+
* The stored source is never mutated — only the served document.
|
|
2453
|
+
*/
|
|
2454
|
+
const MOCKUP_PROBE_FLAG = "data-diffing-probe";
|
|
2455
|
+
/**
|
|
2456
|
+
* Build the probe script for one served document. The script is parameterized
|
|
2457
|
+
* with the document nonce + viewport so every posted event carries them.
|
|
2458
|
+
*/
|
|
2459
|
+
function buildMockupProbeScript(opts = {}) {
|
|
2460
|
+
const nonce = opts.nonce ?? "";
|
|
2461
|
+
const viewport = opts.viewport ?? "desktop";
|
|
2462
|
+
return `(() => {
|
|
2463
|
+
if (window.__diffingMockupProbe) return;
|
|
2464
|
+
window.__diffingMockupProbe = true;
|
|
2465
|
+
const NONCE = ${JSON.stringify(nonce)};
|
|
2466
|
+
const VIEWPORT = ${JSON.stringify(viewport)};
|
|
2467
|
+
const BLOCK = 'H1,H2,H3,H4,H5,H6,BUTTON,A,INPUT,TEXTAREA,SELECT,IMG,LI,P,SECTION,ARTICLE,HEADER,TR,TD,LABEL,NAV,ASIDE,FOOTER,FIGURE';
|
|
2468
|
+
let tool = 'block';
|
|
2469
|
+
let drag = null;
|
|
2470
|
+
let shield = null;
|
|
2471
|
+
|
|
2472
|
+
function post(event, extra) {
|
|
2473
|
+
try {
|
|
2474
|
+
parent.postMessage(Object.assign({ type: 'diffing-mockup', event: event, nonce: NONCE, viewport: VIEWPORT }, extra || {}), '*');
|
|
2475
|
+
} catch (_) {}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
function pctRect(el) {
|
|
2479
|
+
const r = el.getBoundingClientRect();
|
|
2480
|
+
const w = Math.max(document.documentElement.clientWidth, 1);
|
|
2481
|
+
const h = Math.max(document.documentElement.clientHeight, 1);
|
|
2482
|
+
return {
|
|
2483
|
+
x: (r.left / w) * 100,
|
|
2484
|
+
y: (r.top / h) * 100,
|
|
2485
|
+
w: (r.width / w) * 100,
|
|
2486
|
+
h: (r.height / h) * 100,
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
function pointPct(ev) {
|
|
2491
|
+
const w = Math.max(document.documentElement.clientWidth, 1);
|
|
2492
|
+
const h = Math.max(document.documentElement.clientHeight, 1);
|
|
2493
|
+
return { x: (ev.clientX / w) * 100, y: (ev.clientY / h) * 100 };
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
function cssEscape(s) {
|
|
2497
|
+
if (window.CSS && CSS.escape) return CSS.escape(s);
|
|
2498
|
+
return String(s).replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
function shortSelector(el) {
|
|
2502
|
+
if (!el || el === document.documentElement || el === document.body) return 'body';
|
|
2503
|
+
const parts = [];
|
|
2504
|
+
let node = el;
|
|
2505
|
+
for (let i = 0; i < 6 && node && node !== document.body; i++) {
|
|
2506
|
+
if (node.getAttribute && node.getAttribute('data-diffing')) {
|
|
2507
|
+
parts.unshift('[data-diffing="' + node.getAttribute('data-diffing') + '"]');
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
if (node.id) { parts.unshift('#' + cssEscape(node.id)); break; }
|
|
2511
|
+
const tag = node.tagName.toLowerCase();
|
|
2512
|
+
const parent = node.parentElement;
|
|
2513
|
+
if (!parent) { parts.unshift(tag); break; }
|
|
2514
|
+
const cls = (node.className && typeof node.className === 'string')
|
|
2515
|
+
? node.className.trim().split(/\\s+/).filter(Boolean)[0]
|
|
2516
|
+
: '';
|
|
2517
|
+
const same = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
|
|
2518
|
+
const idx = same.indexOf(node) + 1;
|
|
2519
|
+
const bit = cls ? tag + '.' + cssEscape(cls) : (same.length > 1 ? tag + ':nth-of-type(' + idx + ')' : tag);
|
|
2520
|
+
parts.unshift(bit);
|
|
2521
|
+
node = parent;
|
|
2522
|
+
}
|
|
2523
|
+
return parts.join('>');
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// Stable selector for a block measured from its nearest named section root:
|
|
2527
|
+
// '[data-diffing="hero"] > div.row > button.pay:nth-of-type(2)'. Nodes above
|
|
2528
|
+
// the section never appear, so edits elsewhere in the document cannot change it.
|
|
2529
|
+
function sectionRelativeSelector(el, section) {
|
|
2530
|
+
const name = section.getAttribute('data-diffing');
|
|
2531
|
+
const root = '[data-diffing="' + name + '"]';
|
|
2532
|
+
if (!el || el === section) return root;
|
|
2533
|
+
const parts = [];
|
|
2534
|
+
let node = el;
|
|
2535
|
+
while (node && node !== section) {
|
|
2536
|
+
const tag = node.tagName.toLowerCase();
|
|
2537
|
+
const parent = node.parentElement;
|
|
2538
|
+
let bit = tag;
|
|
2539
|
+
if (node.id) {
|
|
2540
|
+
bit += '#' + cssEscape(node.id);
|
|
2541
|
+
} else {
|
|
2542
|
+
const cls = (node.className && typeof node.className === 'string')
|
|
2543
|
+
? node.className.trim().split(/\\s+/).filter(Boolean)[0]
|
|
2544
|
+
: '';
|
|
2545
|
+
if (cls) bit += '.' + cssEscape(cls);
|
|
2546
|
+
if (parent) {
|
|
2547
|
+
const same = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
|
|
2548
|
+
if (same.length > 1) bit += ':nth-of-type(' + (same.indexOf(node) + 1) + ')';
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
parts.unshift(bit);
|
|
2552
|
+
node = parent;
|
|
2553
|
+
}
|
|
2554
|
+
return root + ' > ' + parts.join(' > ');
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// Stable digest of a DOM path. Purely structural — class renames on the
|
|
2558
|
+
// block itself change it, but viewport/layout shifts do not.
|
|
2559
|
+
function fingerprintOf(path) {
|
|
2560
|
+
let h = 5381;
|
|
2561
|
+
for (let i = 0; i < path.length; i++) {
|
|
2562
|
+
h = ((h << 5) + h + path.charCodeAt(i)) | 0;
|
|
2563
|
+
}
|
|
2564
|
+
return 'fp:' + (h >>> 0).toString(16);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function clipHtml(el, max) {
|
|
2568
|
+
if (!el || !el.cloneNode) return undefined;
|
|
2569
|
+
const clone = el.cloneNode(true);
|
|
2570
|
+
if (clone.querySelectorAll) {
|
|
2571
|
+
clone.querySelectorAll('script').forEach(function (s) { s.remove(); });
|
|
2572
|
+
}
|
|
2573
|
+
let html = clone.outerHTML || '';
|
|
2574
|
+
if (html.length > max) html = html.slice(0, max) + '\u2026';
|
|
2575
|
+
return html;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
function isBlock(el) {
|
|
2579
|
+
if (!el || el === document.body || el === document.documentElement) return false;
|
|
2580
|
+
if (el.hasAttribute('data-diffing')) return true;
|
|
2581
|
+
if (el.id || (el.className && String(el.className).trim())) return true;
|
|
2582
|
+
if (el.getAttribute('role')) return true;
|
|
2583
|
+
return BLOCK.split(',').includes(el.tagName);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
function deepestBlock(start) {
|
|
2587
|
+
let el = start;
|
|
2588
|
+
while (el && el !== document.body && !isBlock(el)) el = el.parentElement;
|
|
2589
|
+
return el && el !== document.body ? el : start;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
function payloadFor(el, ev, kindOverride) {
|
|
2593
|
+
// Nearest named [data-diffing] ancestor only — sections never nest deeper.
|
|
2594
|
+
const section = el && el.closest ? el.closest('[data-diffing]') : null;
|
|
2595
|
+
const kind = kindOverride || (tool === 'pin' ? 'point' : tool === 'section' && section ? 'section' : 'block');
|
|
2596
|
+
const hitEl = deepestBlock(el);
|
|
2597
|
+
const outlineEl = kind === 'section' && section ? section : hitEl;
|
|
2598
|
+
const pt = ev ? pointPct(ev) : { x: 0, y: 0 };
|
|
2599
|
+
let sectionX, sectionY;
|
|
2600
|
+
if (section && ev) {
|
|
2601
|
+
const sr = section.getBoundingClientRect();
|
|
2602
|
+
if (sr.width > 0 && sr.height > 0) {
|
|
2603
|
+
sectionX = Math.round(((ev.clientX - sr.left) / sr.width) * 1000) / 10;
|
|
2604
|
+
sectionY = Math.round(((ev.clientY - sr.top) / sr.height) * 1000) / 10;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const base = {
|
|
2608
|
+
kind: kind,
|
|
2609
|
+
target: section ? section.getAttribute('data-diffing') : undefined,
|
|
2610
|
+
x: Math.round(pt.x * 10) / 10,
|
|
2611
|
+
y: Math.round(pt.y * 10) / 10,
|
|
2612
|
+
sectionX: sectionX,
|
|
2613
|
+
sectionY: sectionY,
|
|
2614
|
+
rect: outlineEl ? pctRect(outlineEl) : undefined,
|
|
2615
|
+
};
|
|
2616
|
+
if (kind === 'section' && section) {
|
|
2617
|
+
const sel = '[data-diffing="' + section.getAttribute('data-diffing') + '"]';
|
|
2618
|
+
return Object.assign(base, {
|
|
2619
|
+
selector: sel,
|
|
2620
|
+
fingerprint: fingerprintOf(sel),
|
|
2621
|
+
html: clipHtml(section, 2000),
|
|
2622
|
+
contextHtml: clipHtml(section, 4000),
|
|
2623
|
+
snapshot: String(section.innerText || '').trim().slice(0, 240),
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
if (kind === 'point') {
|
|
2627
|
+
// Pin: coordinate + nearby context. The coordinates are the locator;
|
|
2628
|
+
// no deep selector/fingerprint is recorded.
|
|
2629
|
+
return Object.assign(base, {
|
|
2630
|
+
html: clipHtml(hitEl, 2000),
|
|
2631
|
+
contextHtml: clipHtml(section || (hitEl && hitEl.parentElement) || hitEl, 4000),
|
|
2632
|
+
snapshot: hitEl ? String(hitEl.innerText || '').trim().slice(0, 240) : undefined,
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
// block
|
|
2636
|
+
if (section) {
|
|
2637
|
+
const sel = sectionRelativeSelector(hitEl, section);
|
|
2638
|
+
return Object.assign(base, {
|
|
2639
|
+
selector: sel,
|
|
2640
|
+
fingerprint: fingerprintOf(sel),
|
|
2641
|
+
html: clipHtml(hitEl, 2000),
|
|
2642
|
+
contextHtml: clipHtml(section, 4000),
|
|
2643
|
+
snapshot: hitEl ? String(hitEl.innerText || '').trim().slice(0, 240) : undefined,
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
const sel = hitEl ? shortSelector(hitEl) : undefined;
|
|
2647
|
+
return Object.assign(base, {
|
|
2648
|
+
selector: sel,
|
|
2649
|
+
fingerprint: sel ? fingerprintOf(sel) : undefined,
|
|
2650
|
+
html: clipHtml(hitEl, 2000),
|
|
2651
|
+
contextHtml: clipHtml(hitEl && hitEl.parentElement, 4000),
|
|
2652
|
+
snapshot: hitEl ? String(hitEl.innerText || '').trim().slice(0, 240) : undefined,
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
function sections() {
|
|
2657
|
+
return Array.from(document.querySelectorAll('[data-diffing]')).map((el) => ({
|
|
2658
|
+
name: el.getAttribute('data-diffing'),
|
|
2659
|
+
rect: pctRect(el),
|
|
2660
|
+
}));
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
window.addEventListener('message', (ev) => {
|
|
2664
|
+
const d = ev.data;
|
|
2665
|
+
if (!d || d.type !== 'diffing-mockup') return;
|
|
2666
|
+
if (d.event === 'set-tool' && d.tool) tool = d.tool;
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
function targetAt(ev) {
|
|
2670
|
+
if (!shield) return document.body;
|
|
2671
|
+
shield.style.pointerEvents = 'none';
|
|
2672
|
+
try {
|
|
2673
|
+
return document.elementFromPoint(ev.clientX, ev.clientY) || document.body;
|
|
2674
|
+
} finally {
|
|
2675
|
+
shield.style.pointerEvents = 'auto';
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function installShield() {
|
|
2680
|
+
if (shield || !document.body) return;
|
|
2681
|
+
const stabilityStyle = document.createElement('style');
|
|
2682
|
+
stabilityStyle.setAttribute('data-diffing-stability', '1');
|
|
2683
|
+
stabilityStyle.textContent = 'html { scrollbar-gutter: stable; }';
|
|
2684
|
+
(document.head || document.documentElement).appendChild(stabilityStyle);
|
|
2685
|
+
|
|
2686
|
+
shield = document.createElement('div');
|
|
2687
|
+
shield.setAttribute('data-diffing-inspection-shield', '1');
|
|
2688
|
+
shield.setAttribute('aria-hidden', 'true');
|
|
2689
|
+
shield.style.cssText = 'position:fixed;inset:0;background:transparent;z-index:2147483647;pointer-events:auto;cursor:crosshair;';
|
|
2690
|
+
document.body.appendChild(shield);
|
|
2691
|
+
|
|
2692
|
+
shield.addEventListener('mousemove', (ev) => {
|
|
2693
|
+
const target = targetAt(ev);
|
|
2694
|
+
if (tool === 'pin') {
|
|
2695
|
+
post('hover', Object.assign(payloadFor(target, ev, 'point'), { kind: 'point' }));
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (tool === 'section') {
|
|
2699
|
+
const section = target && target.closest ? target.closest('[data-diffing]') : null;
|
|
2700
|
+
if (!section) {
|
|
2701
|
+
post('hover-miss');
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
post('hover', payloadFor(section, ev, 'section'));
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
post('hover', payloadFor(target, ev));
|
|
2708
|
+
}, true);
|
|
2709
|
+
|
|
2710
|
+
shield.addEventListener('mousedown', (ev) => {
|
|
2711
|
+
if (ev.button !== 0) return;
|
|
2712
|
+
drag = { x: ev.clientX, y: ev.clientY, target: targetAt(ev) };
|
|
2713
|
+
ev.preventDefault();
|
|
2714
|
+
}, true);
|
|
2715
|
+
|
|
2716
|
+
shield.addEventListener('mouseup', (ev) => {
|
|
2717
|
+
if (ev.button !== 0 || !drag) return;
|
|
2718
|
+
const target = targetAt(ev);
|
|
2719
|
+
const dx = ev.clientX - drag.x;
|
|
2720
|
+
const dy = ev.clientY - drag.y;
|
|
2721
|
+
const dist = Math.hypot(dx, dy);
|
|
2722
|
+
drag = null;
|
|
2723
|
+
if (tool === 'pin') {
|
|
2724
|
+
post('click', payloadFor(target, ev, 'point'));
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (tool === 'section') {
|
|
2728
|
+
const section = target && target.closest ? target.closest('[data-diffing]') : null;
|
|
2729
|
+
if (!section) {
|
|
2730
|
+
post('section-miss');
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
post(dist >= 8 ? 'dragend' : 'click', payloadFor(section, ev, 'section'));
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
post(dist >= 8 ? 'dragend' : 'click', payloadFor(target, ev));
|
|
2737
|
+
}, true);
|
|
2738
|
+
|
|
2739
|
+
shield.addEventListener('click', (ev) => {
|
|
2740
|
+
ev.preventDefault();
|
|
2741
|
+
ev.stopPropagation();
|
|
2742
|
+
}, true);
|
|
2743
|
+
post('ready', { sections: sections() });
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
if (document.readyState === 'loading') {
|
|
2747
|
+
document.addEventListener('DOMContentLoaded', installShield, { once: true });
|
|
2748
|
+
} else {
|
|
2749
|
+
installShield();
|
|
2750
|
+
}
|
|
2751
|
+
})();`;
|
|
2752
|
+
}
|
|
2753
|
+
buildMockupProbeScript();
|
|
2754
|
+
function injectMockupProbe(html, opts = {}) {
|
|
2755
|
+
const script = `<script ${MOCKUP_PROBE_FLAG}="1">${buildMockupProbeScript(opts)}<\/script>`;
|
|
2756
|
+
const lower = html.toLowerCase();
|
|
2757
|
+
const headIdx = lower.indexOf("<head");
|
|
2758
|
+
if (headIdx !== -1) {
|
|
2759
|
+
const close = html.indexOf(">", headIdx);
|
|
2760
|
+
if (close !== -1) return html.slice(0, close + 1) + script + html.slice(close + 1);
|
|
2761
|
+
}
|
|
2762
|
+
const htmlIdx = lower.indexOf("<html");
|
|
2763
|
+
if (htmlIdx !== -1) {
|
|
2764
|
+
const close = html.indexOf(">", htmlIdx);
|
|
2765
|
+
if (close !== -1) return html.slice(0, close + 1) + script + html.slice(close + 1);
|
|
2766
|
+
}
|
|
2767
|
+
return script + html;
|
|
2768
|
+
}
|
|
2769
|
+
//#endregion
|
|
1747
2770
|
//#region src/lib/state.ts
|
|
1748
2771
|
var FileUiStateStore = class {
|
|
1749
2772
|
dirPath;
|
|
@@ -2201,6 +3224,113 @@ var FilePrSessionStore = class {
|
|
|
2201
3224
|
} catch {}
|
|
2202
3225
|
}
|
|
2203
3226
|
};
|
|
3227
|
+
const PATH_MATCH_CAP = 20;
|
|
3228
|
+
function compilePathspecGlob(pattern) {
|
|
3229
|
+
const source = pattern.trim();
|
|
3230
|
+
if (!source) return {
|
|
3231
|
+
error: "invalid path glob: empty pattern",
|
|
3232
|
+
status: 400,
|
|
3233
|
+
path: pattern
|
|
3234
|
+
};
|
|
3235
|
+
try {
|
|
3236
|
+
return {
|
|
3237
|
+
source,
|
|
3238
|
+
test: compileGlobTester(source)
|
|
3239
|
+
};
|
|
3240
|
+
} catch (error) {
|
|
3241
|
+
return {
|
|
3242
|
+
error: error instanceof Error ? error.message : "invalid path glob",
|
|
3243
|
+
status: 400,
|
|
3244
|
+
path: pattern
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
function fileMatchesPath(matcher, oldPath, newPath) {
|
|
3249
|
+
if (newPath && matcher.test(newPath)) return true;
|
|
3250
|
+
if (oldPath && matcher.test(oldPath)) return true;
|
|
3251
|
+
return false;
|
|
3252
|
+
}
|
|
3253
|
+
function parseExcludeList(raw) {
|
|
3254
|
+
if (raw == null) return [];
|
|
3255
|
+
const values = (Array.isArray(raw) ? raw : raw.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
3256
|
+
for (const value of values) if (value !== "lockfiles") return {
|
|
3257
|
+
error: `unknown exclude: ${value}`,
|
|
3258
|
+
status: 400
|
|
3259
|
+
};
|
|
3260
|
+
return [...new Set(values)];
|
|
3261
|
+
}
|
|
3262
|
+
function isLockfileNoise(path) {
|
|
3263
|
+
const base = path.split("/").pop() ?? path;
|
|
3264
|
+
if (base === "package-lock.json" || base === "pnpm-lock.yaml") return true;
|
|
3265
|
+
if (base.endsWith(".lock")) return true;
|
|
3266
|
+
if (base.endsWith(".min.js")) return true;
|
|
3267
|
+
return false;
|
|
3268
|
+
}
|
|
3269
|
+
function displayPath(oldPath, newPath) {
|
|
3270
|
+
return newPath ?? oldPath ?? "";
|
|
3271
|
+
}
|
|
3272
|
+
function capPathMatches(matches) {
|
|
3273
|
+
return matches.slice(0, PATH_MATCH_CAP);
|
|
3274
|
+
}
|
|
3275
|
+
function compileGlobTester(pattern) {
|
|
3276
|
+
const parts = (pattern.includes("/") ? pattern : `**/${pattern}`).split("/");
|
|
3277
|
+
for (const part of parts) {
|
|
3278
|
+
if (part !== "**" && part.includes("**")) throw new Error(`invalid path glob: ${pattern}`);
|
|
3279
|
+
validateGlobSegment(part, pattern);
|
|
3280
|
+
}
|
|
3281
|
+
return (path) => matchParts(parts, path.split("/"));
|
|
3282
|
+
}
|
|
3283
|
+
function matchParts(pat, path) {
|
|
3284
|
+
if (pat.length === 0) return path.length === 0;
|
|
3285
|
+
if (pat[0] === "**") return matchParts(pat.slice(1), path) || path.length > 0 && matchParts(pat, path.slice(1));
|
|
3286
|
+
if (path.length === 0) return false;
|
|
3287
|
+
return globSegment(pat[0], path[0]) && matchParts(pat.slice(1), path.slice(1));
|
|
3288
|
+
}
|
|
3289
|
+
function validateGlobSegment(segment, pattern) {
|
|
3290
|
+
for (let i = 0; i < segment.length; i++) {
|
|
3291
|
+
if (segment[i] === "[") {
|
|
3292
|
+
const close = segment.indexOf("]", i + 1);
|
|
3293
|
+
if (close < 0 || close === i + 1) throw new Error(`invalid path glob: ${pattern}`);
|
|
3294
|
+
i = close;
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3297
|
+
if (segment[i] === "\\") {
|
|
3298
|
+
if (segment[i + 1] == null) throw new Error(`invalid path glob: ${pattern}`);
|
|
3299
|
+
i++;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
function globSegment(pattern, value) {
|
|
3304
|
+
return globChars([...pattern], [...value]);
|
|
3305
|
+
}
|
|
3306
|
+
function globChars(pattern, value) {
|
|
3307
|
+
if (pattern.length === 0) return value.length === 0;
|
|
3308
|
+
const [head, ...rest] = pattern;
|
|
3309
|
+
if (head === "*") return globChars(rest, value) || value.length > 0 && globChars(pattern, value.slice(1));
|
|
3310
|
+
if (head === "?") return value.length > 0 && globChars(rest, value.slice(1));
|
|
3311
|
+
if (head === "[") {
|
|
3312
|
+
const close = rest.indexOf("]");
|
|
3313
|
+
if (close < 0 || value.length === 0) return false;
|
|
3314
|
+
const classBody = rest.slice(0, close);
|
|
3315
|
+
const negated = classBody[0] === "!" || classBody[0] === "^";
|
|
3316
|
+
if (classMatches(negated ? classBody.slice(1) : classBody, value[0]) === negated) return false;
|
|
3317
|
+
return globChars(rest.slice(close + 1), value.slice(1));
|
|
3318
|
+
}
|
|
3319
|
+
if (head === "\\") return rest.length > 0 && value.length > 0 && rest[0] === value[0] && globChars(rest.slice(1), value.slice(1));
|
|
3320
|
+
return value.length > 0 && head === value[0] && globChars(rest, value.slice(1));
|
|
3321
|
+
}
|
|
3322
|
+
function classMatches(body, value) {
|
|
3323
|
+
for (let i = 0; i < body.length;) {
|
|
3324
|
+
if (i + 2 < body.length && body[i + 1] === "-") {
|
|
3325
|
+
if (body[i] <= value && value <= body[i + 2]) return true;
|
|
3326
|
+
i += 3;
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
if (body[i] === value) return true;
|
|
3330
|
+
i++;
|
|
3331
|
+
}
|
|
3332
|
+
return false;
|
|
3333
|
+
}
|
|
2204
3334
|
//#endregion
|
|
2205
3335
|
//#region src/lib/agent-diff-index.ts
|
|
2206
3336
|
/**
|
|
@@ -2429,33 +3559,140 @@ function buildAgentDiffIndex(patch, generation = nextGeneration++) {
|
|
|
2429
3559
|
patchBytes
|
|
2430
3560
|
};
|
|
2431
3561
|
}
|
|
2432
|
-
|
|
3562
|
+
const DIRECTORY_CAP = 20;
|
|
3563
|
+
function indexSummary(index, excludeRaw) {
|
|
3564
|
+
const exclude = parseExcludeList(excludeRaw);
|
|
3565
|
+
if ("error" in exclude) return exclude;
|
|
3566
|
+
const counted = exclude.includes("lockfiles") ? index.files.filter((file) => !isLockfileNoise(displayPath(file.oldPath, file.newPath))) : index.files;
|
|
2433
3567
|
const changes = {};
|
|
2434
|
-
|
|
2435
|
-
|
|
3568
|
+
let hunks = 0;
|
|
3569
|
+
let rows = 0;
|
|
3570
|
+
let additions = 0;
|
|
3571
|
+
let deletions = 0;
|
|
3572
|
+
for (const file of counted) {
|
|
3573
|
+
changes[file.kind] = (changes[file.kind] ?? 0) + 1;
|
|
3574
|
+
hunks += file.hunks.length;
|
|
3575
|
+
rows += file.rowCount;
|
|
3576
|
+
additions += file.additions;
|
|
3577
|
+
deletions += file.deletions;
|
|
3578
|
+
}
|
|
3579
|
+
const summary = {
|
|
2436
3580
|
generation: index.generation,
|
|
2437
3581
|
complete: index.complete,
|
|
2438
|
-
files:
|
|
2439
|
-
hunks
|
|
2440
|
-
rows
|
|
2441
|
-
additions
|
|
2442
|
-
deletions
|
|
3582
|
+
files: counted.length,
|
|
3583
|
+
hunks,
|
|
3584
|
+
rows,
|
|
3585
|
+
additions,
|
|
3586
|
+
deletions,
|
|
2443
3587
|
patchBytes: index.patchBytes,
|
|
2444
3588
|
changes,
|
|
3589
|
+
directories: summarizeDirectories(counted),
|
|
2445
3590
|
next: [
|
|
2446
3591
|
"diff_files",
|
|
2447
3592
|
"diff_search",
|
|
2448
3593
|
"diff_slice"
|
|
2449
3594
|
]
|
|
2450
3595
|
};
|
|
3596
|
+
if (exclude.length > 0) summary.exclude = exclude;
|
|
3597
|
+
return summary;
|
|
3598
|
+
}
|
|
3599
|
+
function summarizeDirectories(files) {
|
|
3600
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3601
|
+
for (const file of files) {
|
|
3602
|
+
const path = displayPath(file.oldPath, file.newPath);
|
|
3603
|
+
const slash = path.indexOf("/");
|
|
3604
|
+
const dir = slash < 0 ? "." : path.slice(0, slash);
|
|
3605
|
+
const bucket = buckets.get(dir) ?? {
|
|
3606
|
+
path: dir,
|
|
3607
|
+
files: 0,
|
|
3608
|
+
hunks: 0,
|
|
3609
|
+
additions: 0,
|
|
3610
|
+
deletions: 0
|
|
3611
|
+
};
|
|
3612
|
+
bucket.files += 1;
|
|
3613
|
+
bucket.hunks += file.hunks.length;
|
|
3614
|
+
bucket.additions += file.additions;
|
|
3615
|
+
bucket.deletions += file.deletions;
|
|
3616
|
+
buckets.set(dir, bucket);
|
|
3617
|
+
}
|
|
3618
|
+
const ranked = [...buckets.values()].sort((a, b) => {
|
|
3619
|
+
if (b.files !== a.files) return b.files - a.files;
|
|
3620
|
+
return b.additions + b.deletions - (a.additions + a.deletions);
|
|
3621
|
+
});
|
|
3622
|
+
if (ranked.length <= DIRECTORY_CAP) return ranked;
|
|
3623
|
+
const head = ranked.slice(0, DIRECTORY_CAP);
|
|
3624
|
+
const rest = ranked.slice(DIRECTORY_CAP);
|
|
3625
|
+
const other = {
|
|
3626
|
+
path: "+other",
|
|
3627
|
+
files: 0,
|
|
3628
|
+
hunks: 0,
|
|
3629
|
+
additions: 0,
|
|
3630
|
+
deletions: 0
|
|
3631
|
+
};
|
|
3632
|
+
for (const bucket of rest) {
|
|
3633
|
+
other.files += bucket.files;
|
|
3634
|
+
other.hunks += bucket.hunks;
|
|
3635
|
+
other.additions += bucket.additions;
|
|
3636
|
+
other.deletions += bucket.deletions;
|
|
3637
|
+
}
|
|
3638
|
+
head.push(other);
|
|
3639
|
+
return head;
|
|
3640
|
+
}
|
|
3641
|
+
function scopedFiles(index, path) {
|
|
3642
|
+
if (path == null || path === "") return { entries: index.files.map((file, i) => ({
|
|
3643
|
+
index: i,
|
|
3644
|
+
file
|
|
3645
|
+
})) };
|
|
3646
|
+
const matcher = compilePathspecGlob(path);
|
|
3647
|
+
if ("error" in matcher) return matcher;
|
|
3648
|
+
return {
|
|
3649
|
+
matcher,
|
|
3650
|
+
entries: index.files.flatMap((file, i) => fileMatchesPath(matcher, file.oldPath, file.newPath) ? [{
|
|
3651
|
+
index: i,
|
|
3652
|
+
file
|
|
3653
|
+
}] : [])
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
function resolveInspectFile(index, file, path) {
|
|
3657
|
+
const hasFile = file != null;
|
|
3658
|
+
const hasPath = path != null && path !== "";
|
|
3659
|
+
if (hasFile && hasPath) return {
|
|
3660
|
+
error: "path and file are mutually exclusive",
|
|
3661
|
+
status: 400,
|
|
3662
|
+
path
|
|
3663
|
+
};
|
|
3664
|
+
if (!hasFile && !hasPath) return {
|
|
3665
|
+
error: "file or path is required",
|
|
3666
|
+
status: 400
|
|
3667
|
+
};
|
|
3668
|
+
if (hasFile) return { fileIndex: file };
|
|
3669
|
+
const scoped = scopedFiles(index, path);
|
|
3670
|
+
if ("error" in scoped) return scoped;
|
|
3671
|
+
if (scoped.entries.length === 0) return {
|
|
3672
|
+
error: "path matched no files",
|
|
3673
|
+
status: 404,
|
|
3674
|
+
path
|
|
3675
|
+
};
|
|
3676
|
+
if (scoped.entries.length > 1) return {
|
|
3677
|
+
error: "path matched multiple files; narrow the glob or pass file",
|
|
3678
|
+
status: 409,
|
|
3679
|
+
path,
|
|
3680
|
+
matches: capPathMatches(scoped.entries.map(({ index, file }) => ({
|
|
3681
|
+
index,
|
|
3682
|
+
path: displayPath(file.oldPath, file.newPath)
|
|
3683
|
+
})))
|
|
3684
|
+
};
|
|
3685
|
+
return { fileIndex: scoped.entries[0].index };
|
|
2451
3686
|
}
|
|
2452
|
-
function indexFiles(index, cursor = 0, limit = 100) {
|
|
3687
|
+
function indexFiles(index, cursor = 0, limit = 100, path) {
|
|
3688
|
+
const scoped = scopedFiles(index, path);
|
|
3689
|
+
if ("error" in scoped) return scoped;
|
|
2453
3690
|
const safeLimit = clamp(limit, 1, MAX_PAGE_LINES);
|
|
2454
3691
|
const start = Math.max(0, cursor);
|
|
2455
|
-
const end = Math.min(
|
|
2456
|
-
const files =
|
|
2457
|
-
index
|
|
2458
|
-
path: file.
|
|
3692
|
+
const end = Math.min(scoped.entries.length, start + safeLimit);
|
|
3693
|
+
const files = scoped.entries.slice(start, end).map(({ index, file }) => ({
|
|
3694
|
+
index,
|
|
3695
|
+
path: displayPath(file.oldPath, file.newPath),
|
|
2459
3696
|
oldPath: file.oldPath,
|
|
2460
3697
|
newPath: file.newPath,
|
|
2461
3698
|
kind: file.kind,
|
|
@@ -2465,13 +3702,16 @@ function indexFiles(index, cursor = 0, limit = 100) {
|
|
|
2465
3702
|
additions: file.additions,
|
|
2466
3703
|
deletions: file.deletions
|
|
2467
3704
|
}));
|
|
2468
|
-
|
|
3705
|
+
const page = {
|
|
2469
3706
|
generation: index.generation,
|
|
2470
3707
|
returned: files.length,
|
|
2471
3708
|
total: index.files.length,
|
|
2472
|
-
|
|
3709
|
+
matched: scoped.entries.length,
|
|
3710
|
+
nextCursor: end < scoped.entries.length ? end : null,
|
|
2473
3711
|
files
|
|
2474
3712
|
};
|
|
3713
|
+
if (path) page.path = path;
|
|
3714
|
+
return page;
|
|
2475
3715
|
}
|
|
2476
3716
|
function indexHunks(index, fileIndex, cursor = 0, limit = 100, generation) {
|
|
2477
3717
|
if (generation !== void 0 && generation !== index.generation) return {
|
|
@@ -2542,11 +3782,17 @@ function indexSlice(index, fileIndex, startRow = 0, maxLines = DEFAULT_SLICE_LIN
|
|
|
2542
3782
|
rows
|
|
2543
3783
|
};
|
|
2544
3784
|
}
|
|
2545
|
-
function indexSearch(index, query, fileStart = 0, rowStart = 0, limit = 100, maxBytes = DEFAULT_MAX_BYTES, generation) {
|
|
3785
|
+
function indexSearch(index, query, fileStart = 0, rowStart = 0, limit = 100, maxBytes = DEFAULT_MAX_BYTES, generation, path) {
|
|
2546
3786
|
if (generation !== void 0 && generation !== index.generation) return {
|
|
2547
3787
|
error: `stale generation ${generation}; current generation is ${index.generation}`,
|
|
2548
3788
|
status: 409
|
|
2549
3789
|
};
|
|
3790
|
+
let matcher;
|
|
3791
|
+
if (path) {
|
|
3792
|
+
const compiled = compilePathspecGlob(path);
|
|
3793
|
+
if ("error" in compiled) return compiled;
|
|
3794
|
+
matcher = compiled;
|
|
3795
|
+
}
|
|
2550
3796
|
const q = query.toLowerCase();
|
|
2551
3797
|
if (!q) return {
|
|
2552
3798
|
generation: index.generation,
|
|
@@ -2565,6 +3811,7 @@ function indexSearch(index, query, fileStart = 0, rowStart = 0, limit = 100, max
|
|
|
2565
3811
|
let nextRow = null;
|
|
2566
3812
|
outer: for (let fi = Math.max(0, fileStart); fi < index.files.length; fi++) {
|
|
2567
3813
|
const file = index.files[fi];
|
|
3814
|
+
if (matcher && !fileMatchesPath(matcher, file.oldPath, file.newPath)) continue;
|
|
2568
3815
|
const path = file.newPath ?? file.oldPath ?? "";
|
|
2569
3816
|
const pathMatch = path.toLowerCase().includes(q);
|
|
2570
3817
|
const rowBegin = fi === fileStart ? Math.max(0, rowStart) : 0;
|
|
@@ -2975,17 +4222,57 @@ function collectPrAvatarUrls(session) {
|
|
|
2975
4222
|
function isCustomMode(opts) {
|
|
2976
4223
|
return opts.revisions.length > 0 || opts.pathspecs.length > 0 || opts.showMode;
|
|
2977
4224
|
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Compact comment serializer for bounded inspect views. `context` controls how
|
|
4227
|
+
* much anchor data is included: none → metadata only; anchor → locator fields
|
|
4228
|
+
* (target/selector/fingerprint/coords/html/snapshot); source → + contextHtml.
|
|
4229
|
+
*/
|
|
4230
|
+
function serializeMockupCommentSummary(comment, context, opts = {}) {
|
|
4231
|
+
const out = {
|
|
4232
|
+
id: comment.id,
|
|
4233
|
+
screenId: comment.screenId,
|
|
4234
|
+
viewport: commentViewport(comment),
|
|
4235
|
+
kind: comment.kind,
|
|
4236
|
+
status: comment.status,
|
|
4237
|
+
createdAt: comment.createdAt,
|
|
4238
|
+
createdAtMockupVersion: comment.createdAtMockupVersion
|
|
4239
|
+
};
|
|
4240
|
+
out.body = opts.truncateBody && comment.body.length > opts.truncateBody ? comment.body.slice(0, opts.truncateBody) + "…" : comment.body;
|
|
4241
|
+
if (comment.severity && comment.severity !== "none") out.severity = comment.severity;
|
|
4242
|
+
if (comment.threadKind) out.threadKind = comment.threadKind;
|
|
4243
|
+
if (context === "none") return out;
|
|
4244
|
+
if (comment.target) out.target = comment.target;
|
|
4245
|
+
if (comment.selector) out.selector = comment.selector;
|
|
4246
|
+
if (comment.fingerprint) out.fingerprint = comment.fingerprint;
|
|
4247
|
+
if (comment.x !== void 0) out.x = comment.x;
|
|
4248
|
+
if (comment.y !== void 0) out.y = comment.y;
|
|
4249
|
+
if (comment.sectionX !== void 0) out.sectionX = comment.sectionX;
|
|
4250
|
+
if (comment.sectionY !== void 0) out.sectionY = comment.sectionY;
|
|
4251
|
+
if (comment.html) out.html = comment.html;
|
|
4252
|
+
if (comment.snapshot) out.snapshot = comment.snapshot;
|
|
4253
|
+
out.replies = (comment.replies ?? []).map((r) => ({
|
|
4254
|
+
id: r.id,
|
|
4255
|
+
body: r.body,
|
|
4256
|
+
createdAt: r.createdAt,
|
|
4257
|
+
role: r.role,
|
|
4258
|
+
model: r.model
|
|
4259
|
+
}));
|
|
4260
|
+
if (context === "anchor") return out;
|
|
4261
|
+
if (comment.contextHtml) out.contextHtml = comment.contextHtml;
|
|
4262
|
+
return out;
|
|
4263
|
+
}
|
|
2978
4264
|
function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore, prSessionStore, prMode = false, security = {
|
|
2979
4265
|
bindHost: "127.0.0.1",
|
|
2980
4266
|
authToken: null,
|
|
2981
4267
|
insecureNoAuth: true
|
|
2982
|
-
}) {
|
|
4268
|
+
}, mockupStore) {
|
|
2983
4269
|
const app = new Hono();
|
|
2984
4270
|
app.use("*", createServerAuthMiddleware(security));
|
|
2985
4271
|
let diffOpts = { ...diffOptsInput };
|
|
2986
4272
|
const customMode = isCustomMode(diffOpts);
|
|
2987
4273
|
const store = commentStore ?? new FileCommentStore();
|
|
2988
4274
|
const plans = planStore ?? new FilePlanStore();
|
|
4275
|
+
const mockups = mockupStore ?? new FileMockupStore();
|
|
2989
4276
|
const prStore = prSessionStore ?? new FilePrSessionStore();
|
|
2990
4277
|
const agentDiffCache = new AgentDiffIndexCache();
|
|
2991
4278
|
const uiStateStore = new FileUiStateStore();
|
|
@@ -3009,17 +4296,37 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3009
4296
|
agents: agentsSnapshot()
|
|
3010
4297
|
})));
|
|
3011
4298
|
const planReviewSession = new PlanReviewSession((snapshot) => broadcast("plan-review-status", JSON.stringify(snapshot)));
|
|
4299
|
+
const mockupReviewSession = new MockupReviewSession((snapshot) => broadcast("mockup-review-status", JSON.stringify(snapshot)));
|
|
4300
|
+
const NONCE_TTL_MS = 3600 * 1e3;
|
|
4301
|
+
const mockupNonces = /* @__PURE__ */ new Map();
|
|
4302
|
+
const registerMockupNonce = (nonce, entry) => {
|
|
4303
|
+
const now = Date.now();
|
|
4304
|
+
if (mockupNonces.size > 500) {
|
|
4305
|
+
for (const [key, value] of mockupNonces) if (value.expiresAt < now) mockupNonces.delete(key);
|
|
4306
|
+
}
|
|
4307
|
+
mockupNonces.set(nonce, {
|
|
4308
|
+
...entry,
|
|
4309
|
+
expiresAt: now + NONCE_TTL_MS
|
|
4310
|
+
});
|
|
4311
|
+
};
|
|
4312
|
+
const validateMockupNonce = (nonce, mockupId, screenId) => {
|
|
4313
|
+
const entry = mockupNonces.get(nonce);
|
|
4314
|
+
if (!entry || entry.expiresAt < Date.now()) return "Stale mockup document (nonce expired or unknown) — reload the screen and retry.";
|
|
4315
|
+
if (entry.mockupId !== mockupId || entry.screenId !== screenId) return "Mockup nonce does not match this mockup/screen — reload the screen and retry.";
|
|
4316
|
+
return null;
|
|
4317
|
+
};
|
|
3012
4318
|
let repoRoot;
|
|
3013
4319
|
try {
|
|
3014
4320
|
repoRoot = getRepoRoot();
|
|
3015
4321
|
} catch {
|
|
3016
4322
|
repoRoot = process.cwd();
|
|
3017
4323
|
}
|
|
3018
|
-
if (!commentStore && !planStore) try {
|
|
4324
|
+
if (!commentStore && !planStore && !mockupStore) try {
|
|
3019
4325
|
const storageDir = getProjectStorageDir();
|
|
3020
4326
|
mkdirSync(storageDir, { recursive: true });
|
|
3021
4327
|
let commentsDebounce = null;
|
|
3022
4328
|
let plansDebounce = null;
|
|
4329
|
+
let mockupsDebounce = null;
|
|
3023
4330
|
let prSessionDebounce = null;
|
|
3024
4331
|
watch(storageDir, (_eventType, filename) => {
|
|
3025
4332
|
if (!filename) return;
|
|
@@ -3029,6 +4336,9 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3029
4336
|
} else if (filename.startsWith("plans.json")) {
|
|
3030
4337
|
if (plansDebounce) clearTimeout(plansDebounce);
|
|
3031
4338
|
plansDebounce = setTimeout(() => broadcast("plans", Date.now().toString()), 120);
|
|
4339
|
+
} else if (filename.startsWith("mockups.json")) {
|
|
4340
|
+
if (mockupsDebounce) clearTimeout(mockupsDebounce);
|
|
4341
|
+
mockupsDebounce = setTimeout(() => broadcast("mockups", Date.now().toString()), 120);
|
|
3032
4342
|
} else if (filename.startsWith("pr-session.json")) {
|
|
3033
4343
|
if (prSessionDebounce) clearTimeout(prSessionDebounce);
|
|
3034
4344
|
prSessionDebounce = setTimeout(() => broadcast("pr-session", Date.now().toString()), 120);
|
|
@@ -3039,7 +4349,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3039
4349
|
}
|
|
3040
4350
|
let debounceTimeout = null;
|
|
3041
4351
|
try {
|
|
3042
|
-
watch(repoRoot, { recursive: true }, (
|
|
4352
|
+
watch(repoRoot, { recursive: true }, (_eventType, filename) => {
|
|
3043
4353
|
if (!filename) return;
|
|
3044
4354
|
const parts = filename.split(/[/\\]/);
|
|
3045
4355
|
const isGit = parts.includes(".git");
|
|
@@ -3169,9 +4479,21 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3169
4479
|
const n = Number(value);
|
|
3170
4480
|
return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : fallback;
|
|
3171
4481
|
}
|
|
4482
|
+
function optionalUInt(value) {
|
|
4483
|
+
if (value == null || value === "") return void 0;
|
|
4484
|
+
const n = Number(value);
|
|
4485
|
+
return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : void 0;
|
|
4486
|
+
}
|
|
4487
|
+
function inspectError(c, result) {
|
|
4488
|
+
const body = { error: result.error };
|
|
4489
|
+
if (result.path != null) body.path = result.path;
|
|
4490
|
+
if (result.matches != null) body.matches = result.matches;
|
|
4491
|
+
return c.json(body, result.status);
|
|
4492
|
+
}
|
|
3172
4493
|
app.get("/api/diff/summary", async (c) => {
|
|
3173
4494
|
const patch = await resolveAgentPatch();
|
|
3174
|
-
const summary = indexSummary(agentDiffCache.getOrBuild(patch));
|
|
4495
|
+
const summary = indexSummary(agentDiffCache.getOrBuild(patch), c.req.query("exclude"));
|
|
4496
|
+
if ("status" in summary) return inspectError(c, summary);
|
|
3175
4497
|
if (!prMode) return c.json(summary);
|
|
3176
4498
|
const session = await prStore.get();
|
|
3177
4499
|
if (!session) return c.json(summary);
|
|
@@ -3189,32 +4511,33 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3189
4511
|
});
|
|
3190
4512
|
app.get("/api/diff/files", async (c) => {
|
|
3191
4513
|
const patch = await resolveAgentPatch();
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
return c.json(indexFiles(index, cursor, limit));
|
|
4514
|
+
const result = indexFiles(agentDiffCache.getOrBuild(patch), parseUInt(c.req.query("cursor"), 0), parseUInt(c.req.query("limit"), 100), c.req.query("path"));
|
|
4515
|
+
if ("status" in result) return inspectError(c, result);
|
|
4516
|
+
return c.json(result);
|
|
3196
4517
|
});
|
|
3197
4518
|
app.get("/api/diff/hunks", async (c) => {
|
|
3198
4519
|
const patch = await resolveAgentPatch();
|
|
3199
4520
|
const index = agentDiffCache.getOrBuild(patch);
|
|
3200
|
-
const
|
|
4521
|
+
const resolved = resolveInspectFile(index, optionalUInt(c.req.query("file")), c.req.query("path"));
|
|
4522
|
+
if ("status" in resolved) return inspectError(c, resolved);
|
|
3201
4523
|
const cursor = parseUInt(c.req.query("cursor"), 0);
|
|
3202
4524
|
const limit = parseUInt(c.req.query("limit"), 100);
|
|
3203
4525
|
const generationRaw = c.req.query("generation");
|
|
3204
|
-
const result = indexHunks(index,
|
|
3205
|
-
if ("status" in result) return c
|
|
4526
|
+
const result = indexHunks(index, resolved.fileIndex, cursor, limit, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0);
|
|
4527
|
+
if ("status" in result) return inspectError(c, result);
|
|
3206
4528
|
return c.json(result);
|
|
3207
4529
|
});
|
|
3208
4530
|
app.get("/api/diff/slice", async (c) => {
|
|
3209
4531
|
const patch = await resolveAgentPatch();
|
|
3210
4532
|
const index = agentDiffCache.getOrBuild(patch);
|
|
3211
|
-
const
|
|
4533
|
+
const resolved = resolveInspectFile(index, optionalUInt(c.req.query("file")), c.req.query("path"));
|
|
4534
|
+
if ("status" in resolved) return inspectError(c, resolved);
|
|
3212
4535
|
const start = parseUInt(c.req.query("start"), 0);
|
|
3213
4536
|
const maxLines = parseUInt(c.req.query("maxLines"), 120);
|
|
3214
4537
|
const maxBytes = parseUInt(c.req.query("maxBytes"), 256 * 1024);
|
|
3215
4538
|
const generationRaw = c.req.query("generation");
|
|
3216
|
-
const result = indexSlice(index,
|
|
3217
|
-
if ("status" in result) return c
|
|
4539
|
+
const result = indexSlice(index, resolved.fileIndex, start, maxLines, maxBytes, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0);
|
|
4540
|
+
if ("status" in result) return inspectError(c, result);
|
|
3218
4541
|
return c.json(result);
|
|
3219
4542
|
});
|
|
3220
4543
|
app.get("/api/diff/search", async (c) => {
|
|
@@ -3226,8 +4549,8 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3226
4549
|
const limit = parseUInt(c.req.query("limit"), 100);
|
|
3227
4550
|
const maxBytes = parseUInt(c.req.query("maxBytes"), 256 * 1024);
|
|
3228
4551
|
const generationRaw = c.req.query("generation");
|
|
3229
|
-
const result = indexSearch(index, q, file, row, limit, maxBytes, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0);
|
|
3230
|
-
if ("status" in result) return c
|
|
4552
|
+
const result = indexSearch(index, q, file, row, limit, maxBytes, generationRaw != null && generationRaw !== "" ? parseUInt(generationRaw, 0) : void 0, c.req.query("path"));
|
|
4553
|
+
if ("status" in result) return inspectError(c, result);
|
|
3231
4554
|
return c.json(result);
|
|
3232
4555
|
});
|
|
3233
4556
|
const resolveFileVersion = async (path, version) => {
|
|
@@ -4058,7 +5381,6 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4058
5381
|
const ref = typeof body?.ref === "string" ? body.ref : "";
|
|
4059
5382
|
if (!ref.trim()) return c.json({ error: "ref is required" }, 400);
|
|
4060
5383
|
try {
|
|
4061
|
-
parsePrRef(ref, await detectCwdRepo() ?? void 0);
|
|
4062
5384
|
const session = await buildPrSession(ref);
|
|
4063
5385
|
await prStore.set(session);
|
|
4064
5386
|
return c.json({
|
|
@@ -4240,7 +5562,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4240
5562
|
submittedAt: Date.now(),
|
|
4241
5563
|
submittedReviewId: result.reviewId,
|
|
4242
5564
|
submittedReviewUrl: result.reviewUrl,
|
|
4243
|
-
authSource: result.authSource
|
|
5565
|
+
authSource: result.authSource === "none" ? void 0 : result.authSource
|
|
4244
5566
|
};
|
|
4245
5567
|
await prStore.set(next);
|
|
4246
5568
|
}
|
|
@@ -4526,6 +5848,472 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4526
5848
|
app.get("/api/plan-review/status", (c) => {
|
|
4527
5849
|
return c.json(planReviewSession.snapshot());
|
|
4528
5850
|
});
|
|
5851
|
+
const summarizeMockup = (mockup, includeComments = false) => ({
|
|
5852
|
+
id: mockup.id,
|
|
5853
|
+
title: mockup.title,
|
|
5854
|
+
screens: mockup.screens.map(({ id, label }) => ({
|
|
5855
|
+
id,
|
|
5856
|
+
label
|
|
5857
|
+
})),
|
|
5858
|
+
source: mockup.source,
|
|
5859
|
+
model: mockup.model,
|
|
5860
|
+
createdAt: mockup.createdAt,
|
|
5861
|
+
updatedAt: mockup.updatedAt,
|
|
5862
|
+
version: mockup.version,
|
|
5863
|
+
decision: mockup.decision,
|
|
5864
|
+
decidedAt: mockup.decidedAt,
|
|
5865
|
+
versionCount: mockup.versions.length,
|
|
5866
|
+
commentCounts: {
|
|
5867
|
+
total: mockup.comments.length,
|
|
5868
|
+
open: mockup.comments.filter((comment) => comment.status === "open").length,
|
|
5869
|
+
resolved: mockup.comments.filter((comment) => comment.status === "resolved").length
|
|
5870
|
+
},
|
|
5871
|
+
...includeComments ? { comments: mockup.comments } : {}
|
|
5872
|
+
});
|
|
5873
|
+
app.get("/api/mockups", async (c) => {
|
|
5874
|
+
const all = await mockups.getAll();
|
|
5875
|
+
if (c.req.query("include") === "full") return c.json(all);
|
|
5876
|
+
const includeComments = c.req.query("include") === "comments";
|
|
5877
|
+
return c.json(all.map((mockup) => summarizeMockup(mockup, includeComments)));
|
|
5878
|
+
});
|
|
5879
|
+
app.get("/api/mockups/:id", async (c) => {
|
|
5880
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
5881
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
5882
|
+
return c.json(mockup);
|
|
5883
|
+
});
|
|
5884
|
+
app.get("/api/mockups/:id/versions", async (c) => {
|
|
5885
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
5886
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
5887
|
+
return c.json((mockup.versions ?? []).map((version) => ({
|
|
5888
|
+
version: version.version,
|
|
5889
|
+
title: version.title,
|
|
5890
|
+
source: version.source,
|
|
5891
|
+
model: version.model,
|
|
5892
|
+
createdAt: version.createdAt,
|
|
5893
|
+
screens: version.screens.map(({ id, label }) => ({
|
|
5894
|
+
id,
|
|
5895
|
+
label
|
|
5896
|
+
}))
|
|
5897
|
+
})));
|
|
5898
|
+
});
|
|
5899
|
+
app.get("/api/mockups/:id/versions/:n", async (c) => {
|
|
5900
|
+
const id = c.req.param("id");
|
|
5901
|
+
const n = Number(c.req.param("n"));
|
|
5902
|
+
if (!Number.isFinite(n) || n < 1) return c.json({ error: "version must be a positive integer" }, 400);
|
|
5903
|
+
const mockup = await mockups.get(id);
|
|
5904
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
5905
|
+
const version = (mockup.versions ?? []).find((v) => v.version === n);
|
|
5906
|
+
if (!version) return c.json({ error: `Version ${n} not found` }, 404);
|
|
5907
|
+
return c.json({
|
|
5908
|
+
version,
|
|
5909
|
+
mockup: {
|
|
5910
|
+
id: mockup.id,
|
|
5911
|
+
title: mockup.title,
|
|
5912
|
+
decision: mockup.decision,
|
|
5913
|
+
currentVersion: mockup.version
|
|
5914
|
+
}
|
|
5915
|
+
});
|
|
5916
|
+
});
|
|
5917
|
+
app.get("/api/mockups/:id/screens/:screenId/document", async (c) => {
|
|
5918
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
5919
|
+
if (!mockup) return c.text("Mockup not found", 404);
|
|
5920
|
+
const versionRaw = c.req.query("version");
|
|
5921
|
+
const versionN = versionRaw ? Number(versionRaw) : mockup.version;
|
|
5922
|
+
const snap = versionN === mockup.version ? mockup : (mockup.versions ?? []).find((v) => v.version === versionN);
|
|
5923
|
+
if (!snap) return c.text("Version not found", 404);
|
|
5924
|
+
const screen = snap.screens.find((s) => s.id === c.req.param("screenId"));
|
|
5925
|
+
if (!screen) return c.text("Screen not found", 404);
|
|
5926
|
+
const viewport = normalizeMockupViewport(c.req.query("viewport"));
|
|
5927
|
+
const nonce = crypto.randomUUID();
|
|
5928
|
+
registerMockupNonce(nonce, {
|
|
5929
|
+
mockupId: mockup.id,
|
|
5930
|
+
screenId: screen.id,
|
|
5931
|
+
version: versionN,
|
|
5932
|
+
viewport
|
|
5933
|
+
});
|
|
5934
|
+
return c.html(injectMockupProbe(screen.html, {
|
|
5935
|
+
nonce,
|
|
5936
|
+
viewport
|
|
5937
|
+
}), 200, {
|
|
5938
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline' https: data:; img-src data: blob: https:; font-src https: data:; script-src 'unsafe-inline';",
|
|
5939
|
+
"X-Content-Type-Options": "nosniff",
|
|
5940
|
+
"X-Diffing-Mockup-Nonce": nonce,
|
|
5941
|
+
"X-Diffing-Mockup-Viewport": viewport
|
|
5942
|
+
});
|
|
5943
|
+
});
|
|
5944
|
+
app.get("/api/mockups/:id/inspect", async (c) => {
|
|
5945
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
5946
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
5947
|
+
const view = c.req.query("view") || "summary";
|
|
5948
|
+
if (view !== "summary" && view !== "comments" && view !== "comment" && view !== "screen") return c.json({ error: "view must be summary|comments|comment|screen" }, 400);
|
|
5949
|
+
const status = c.req.query("status");
|
|
5950
|
+
if (status !== void 0 && status !== "open" && status !== "resolved") return c.json({ error: "status must be open or resolved" }, 400);
|
|
5951
|
+
const screenFilter = c.req.query("screen");
|
|
5952
|
+
const viewportRaw = c.req.query("viewport");
|
|
5953
|
+
if (viewportRaw !== void 0 && !isMockupViewport(viewportRaw)) return c.json({ error: "viewport must be desktop|tablet|mobile" }, 400);
|
|
5954
|
+
const versionRaw = c.req.query("version");
|
|
5955
|
+
if (versionRaw !== void 0 && (!Number.isFinite(Number(versionRaw)) || Number(versionRaw) < 1)) return c.json({ error: "version must be a positive integer" }, 400);
|
|
5956
|
+
const contextRaw = c.req.query("context") || "anchor";
|
|
5957
|
+
if (contextRaw !== "none" && contextRaw !== "anchor" && contextRaw !== "source") return c.json({ error: "context must be none|anchor|source" }, 400);
|
|
5958
|
+
const context = contextRaw;
|
|
5959
|
+
const cursorRaw = c.req.query("cursor");
|
|
5960
|
+
const cursor = cursorRaw === void 0 ? 0 : Number(cursorRaw);
|
|
5961
|
+
if (!Number.isFinite(cursor) || cursor < 0) return c.json({ error: "cursor must be a non-negative integer" }, 400);
|
|
5962
|
+
const limitRaw = c.req.query("limit");
|
|
5963
|
+
const limit = limitRaw === void 0 ? 50 : Math.min(Math.max(Number(limitRaw) || 50, 1), 200);
|
|
5964
|
+
const allComments = mockup.comments ?? [];
|
|
5965
|
+
if (view === "summary") {
|
|
5966
|
+
const byViewport = {
|
|
5967
|
+
desktop: 0,
|
|
5968
|
+
tablet: 0,
|
|
5969
|
+
mobile: 0
|
|
5970
|
+
};
|
|
5971
|
+
for (const c2 of allComments) byViewport[commentViewport(c2)] += 1;
|
|
5972
|
+
return c.json({
|
|
5973
|
+
view: "summary",
|
|
5974
|
+
id: mockup.id,
|
|
5975
|
+
title: mockup.title,
|
|
5976
|
+
version: mockup.version,
|
|
5977
|
+
decision: mockup.decision,
|
|
5978
|
+
decidedAt: mockup.decidedAt ?? null,
|
|
5979
|
+
createdAt: mockup.createdAt,
|
|
5980
|
+
updatedAt: mockup.updatedAt,
|
|
5981
|
+
source: mockup.source,
|
|
5982
|
+
model: mockup.model,
|
|
5983
|
+
screens: mockup.screens.map((s) => ({
|
|
5984
|
+
id: s.id,
|
|
5985
|
+
label: s.label
|
|
5986
|
+
})),
|
|
5987
|
+
versions: (mockup.versions ?? []).length,
|
|
5988
|
+
commentCounts: {
|
|
5989
|
+
total: allComments.length,
|
|
5990
|
+
open: allComments.filter((c2) => c2.status === "open").length,
|
|
5991
|
+
resolved: allComments.filter((c2) => c2.status === "resolved").length,
|
|
5992
|
+
byViewport
|
|
5993
|
+
}
|
|
5994
|
+
});
|
|
5995
|
+
}
|
|
5996
|
+
if (view === "comment") {
|
|
5997
|
+
const id = c.req.query("id");
|
|
5998
|
+
if (!id) return c.json({ error: "id is required for view=comment" }, 400);
|
|
5999
|
+
const comment = allComments.find((c2) => c2.id === id);
|
|
6000
|
+
if (!comment) return c.json({ error: `Comment ${id} not found` }, 404);
|
|
6001
|
+
return c.json({
|
|
6002
|
+
view: "comment",
|
|
6003
|
+
mockupId: mockup.id,
|
|
6004
|
+
version: mockup.version,
|
|
6005
|
+
comment: serializeMockupCommentSummary(comment, context)
|
|
6006
|
+
});
|
|
6007
|
+
}
|
|
6008
|
+
if (view === "screen") {
|
|
6009
|
+
const requestedVersion = versionRaw === void 0 ? mockup.version : Number(versionRaw);
|
|
6010
|
+
const snapshot = requestedVersion === mockup.version ? mockup : mockup.versions.find((version) => version.version === requestedVersion);
|
|
6011
|
+
if (!snapshot) return c.json({ error: `Version ${requestedVersion} not found` }, 404);
|
|
6012
|
+
if (screenFilter) {
|
|
6013
|
+
const screen = snapshot.screens.find((item) => item.id === screenFilter);
|
|
6014
|
+
if (!screen) return c.json({ error: "Screen not found" }, 404);
|
|
6015
|
+
return c.json({
|
|
6016
|
+
view: "screen",
|
|
6017
|
+
mockupId: mockup.id,
|
|
6018
|
+
version: requestedVersion,
|
|
6019
|
+
screen: {
|
|
6020
|
+
id: screen.id,
|
|
6021
|
+
label: screen.label,
|
|
6022
|
+
htmlBytes: Buffer.byteLength(screen.html, "utf8"),
|
|
6023
|
+
...context === "source" ? { html: screen.html } : {}
|
|
6024
|
+
}
|
|
6025
|
+
});
|
|
6026
|
+
}
|
|
6027
|
+
const start = Math.min(cursor, snapshot.screens.length);
|
|
6028
|
+
const page = snapshot.screens.slice(start, start + limit);
|
|
6029
|
+
return c.json({
|
|
6030
|
+
view: "screen",
|
|
6031
|
+
mockupId: mockup.id,
|
|
6032
|
+
version: requestedVersion,
|
|
6033
|
+
cursor: start,
|
|
6034
|
+
limit,
|
|
6035
|
+
nextCursor: start + page.length < snapshot.screens.length ? start + page.length : null,
|
|
6036
|
+
screens: page.map((screen) => ({
|
|
6037
|
+
id: screen.id,
|
|
6038
|
+
label: screen.label,
|
|
6039
|
+
htmlBytes: Buffer.byteLength(screen.html, "utf8")
|
|
6040
|
+
}))
|
|
6041
|
+
});
|
|
6042
|
+
}
|
|
6043
|
+
const versionN = versionRaw === void 0 ? void 0 : Number(versionRaw);
|
|
6044
|
+
const filtered = allComments.filter((c2) => {
|
|
6045
|
+
if (status !== void 0 && c2.status !== status) return false;
|
|
6046
|
+
if (screenFilter !== void 0 && c2.screenId !== screenFilter) return false;
|
|
6047
|
+
if (viewportRaw !== void 0 && commentViewport(c2) !== viewportRaw) return false;
|
|
6048
|
+
if (versionN !== void 0 && c2.createdAtMockupVersion !== versionN) return false;
|
|
6049
|
+
return true;
|
|
6050
|
+
});
|
|
6051
|
+
const start = Math.min(cursor, filtered.length);
|
|
6052
|
+
const page = filtered.slice(start, start + limit);
|
|
6053
|
+
return c.json({
|
|
6054
|
+
view: "comments",
|
|
6055
|
+
mockupId: mockup.id,
|
|
6056
|
+
version: mockup.version,
|
|
6057
|
+
cursor: start,
|
|
6058
|
+
limit,
|
|
6059
|
+
nextCursor: start + page.length < filtered.length ? start + page.length : null,
|
|
6060
|
+
comments: page.map((c2) => serializeMockupCommentSummary(c2, context, { truncateBody: 400 }))
|
|
6061
|
+
});
|
|
6062
|
+
});
|
|
6063
|
+
const screenOpError = (c, result) => {
|
|
6064
|
+
if (result.versionMismatch) return c.json({
|
|
6065
|
+
error: `Mockup is at version ${result.versionMismatch.currentVersion}, expected ${result.versionMismatch.expectedVersion} — pass the current version and retry.`,
|
|
6066
|
+
code: "version-mismatch",
|
|
6067
|
+
currentVersion: result.versionMismatch.currentVersion,
|
|
6068
|
+
expectedVersion: result.versionMismatch.expectedVersion
|
|
6069
|
+
}, 409);
|
|
6070
|
+
if (result.error === "Exact text not found") return c.json({
|
|
6071
|
+
error: result.error,
|
|
6072
|
+
code: "exact-text-not-found"
|
|
6073
|
+
}, 409);
|
|
6074
|
+
if (result.error === "Mockup not found" || result.error?.endsWith("not found")) return c.json({ error: result.error }, 404);
|
|
6075
|
+
return c.json({ error: result.error ?? "Screen operation failed" }, 400);
|
|
6076
|
+
};
|
|
6077
|
+
app.put("/api/mockups/:id/screens/:screenId", async (c) => {
|
|
6078
|
+
const mockupId = c.req.param("id");
|
|
6079
|
+
const screenId = c.req.param("screenId");
|
|
6080
|
+
if (!MOCKUP_SCREEN_ID_RE.test(screenId)) return c.json({ error: `Invalid screen id "${screenId}"` }, 400);
|
|
6081
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6082
|
+
if (typeof body.html !== "string" || !body.html.trim()) return c.json({ error: "html is required" }, 400);
|
|
6083
|
+
if (Buffer.byteLength(body.html, "utf8") > 1572864) return c.json({ error: `Each screen must be ≤ ${MOCKUP_MAX_SCREEN_BYTES} bytes` }, 400);
|
|
6084
|
+
const expectedVersion = Number.isFinite(body.expectedVersion) ? Number(body.expectedVersion) : void 0;
|
|
6085
|
+
const label = typeof body.label === "string" && body.label.trim() ? body.label.trim() : void 0;
|
|
6086
|
+
const result = await mockups.upsertScreen(mockupId, {
|
|
6087
|
+
id: screenId,
|
|
6088
|
+
label,
|
|
6089
|
+
html: body.html
|
|
6090
|
+
}, { expectedVersion });
|
|
6091
|
+
if (!result.mockup) return screenOpError(c, result);
|
|
6092
|
+
return c.json(result.mockup, 200);
|
|
6093
|
+
});
|
|
6094
|
+
app.patch("/api/mockups/:id/screens/:screenId", async (c) => {
|
|
6095
|
+
const mockupId = c.req.param("id");
|
|
6096
|
+
const screenId = c.req.param("screenId");
|
|
6097
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6098
|
+
const expectedText = typeof body.expectedText === "string" ? body.expectedText : "";
|
|
6099
|
+
const replacement = typeof body.replacement === "string" ? body.replacement : "";
|
|
6100
|
+
if (!expectedText) return c.json({ error: "expectedText is required" }, 400);
|
|
6101
|
+
if (typeof body.replacement !== "string") return c.json({ error: "replacement is required" }, 400);
|
|
6102
|
+
const expectedVersion = Number.isFinite(body.expectedVersion) ? Number(body.expectedVersion) : void 0;
|
|
6103
|
+
const result = await mockups.patchScreen(mockupId, screenId, {
|
|
6104
|
+
expectedText,
|
|
6105
|
+
replacement
|
|
6106
|
+
}, { expectedVersion });
|
|
6107
|
+
if (!result.mockup) return screenOpError(c, result);
|
|
6108
|
+
return c.json({
|
|
6109
|
+
mockup: result.mockup,
|
|
6110
|
+
occurrences: result.occurrences
|
|
6111
|
+
}, 200);
|
|
6112
|
+
});
|
|
6113
|
+
app.delete("/api/mockups/:id/screens/:screenId", async (c) => {
|
|
6114
|
+
const mockupId = c.req.param("id");
|
|
6115
|
+
const screenId = c.req.param("screenId");
|
|
6116
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6117
|
+
const expectedVersion = Number.isFinite(body.expectedVersion) ? Number(body.expectedVersion) : c.req.query("expectedVersion") !== void 0 ? Number(c.req.query("expectedVersion")) : void 0;
|
|
6118
|
+
const result = await mockups.removeScreen(mockupId, screenId, { expectedVersion });
|
|
6119
|
+
if (!result.mockup) return screenOpError(c, result);
|
|
6120
|
+
return c.json(result.mockup, 200);
|
|
6121
|
+
});
|
|
6122
|
+
app.post("/api/mockups/:id/threads/batch", async (c) => {
|
|
6123
|
+
const mockupId = c.req.param("id");
|
|
6124
|
+
const normalized = normalizeThreadOperations((await c.req.json().catch(() => ({}))).operations);
|
|
6125
|
+
if (!normalized.ok) return c.json({
|
|
6126
|
+
error: normalized.error,
|
|
6127
|
+
index: normalized.index
|
|
6128
|
+
}, 400);
|
|
6129
|
+
const result = await mockups.applyThreadBatch(mockupId, normalized.ops);
|
|
6130
|
+
if (!result.mockup) return c.json({
|
|
6131
|
+
error: result.error ?? "Thread batch failed",
|
|
6132
|
+
results: result.results
|
|
6133
|
+
}, result.error === "Mockup not found" ? 404 : 409);
|
|
6134
|
+
return c.json({
|
|
6135
|
+
ok: true,
|
|
6136
|
+
applied: result.results.length,
|
|
6137
|
+
results: result.results,
|
|
6138
|
+
mockup: result.mockup
|
|
6139
|
+
});
|
|
6140
|
+
});
|
|
6141
|
+
app.post("/api/mockups", async (c) => {
|
|
6142
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6143
|
+
const normalized = screensFromSubmitBody(body);
|
|
6144
|
+
if (!normalized.ok) return c.json({ error: normalized.error }, 400);
|
|
6145
|
+
const title = typeof body.title === "string" && body.title.trim() ? body.title.trim() : "Untitled mockup";
|
|
6146
|
+
const mockup = await mockups.upsert({
|
|
6147
|
+
id: typeof body.id === "string" && body.id ? body.id : void 0,
|
|
6148
|
+
title,
|
|
6149
|
+
screens: normalized.screens,
|
|
6150
|
+
source: typeof body.source === "string" ? body.source : void 0,
|
|
6151
|
+
model: typeof body.model === "string" ? body.model : void 0
|
|
6152
|
+
});
|
|
6153
|
+
return c.json(mockup, 201);
|
|
6154
|
+
});
|
|
6155
|
+
app.put("/api/mockups/:id", async (c) => {
|
|
6156
|
+
const id = c.req.param("id");
|
|
6157
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6158
|
+
let screens;
|
|
6159
|
+
if (body.html !== void 0 || body.screens !== void 0) {
|
|
6160
|
+
const normalized = screensFromSubmitBody(body);
|
|
6161
|
+
if (!normalized.ok) return c.json({ error: normalized.error }, 400);
|
|
6162
|
+
screens = normalized.screens;
|
|
6163
|
+
}
|
|
6164
|
+
const updated = await mockups.update(id, {
|
|
6165
|
+
title: typeof body.title === "string" ? body.title : void 0,
|
|
6166
|
+
screens,
|
|
6167
|
+
source: typeof body.source === "string" ? body.source : void 0,
|
|
6168
|
+
model: typeof body.model === "string" ? body.model : void 0
|
|
6169
|
+
});
|
|
6170
|
+
if (!updated) return c.json({ error: "Mockup not found" }, 404);
|
|
6171
|
+
return c.json(updated);
|
|
6172
|
+
});
|
|
6173
|
+
app.delete("/api/mockups/:id", async (c) => {
|
|
6174
|
+
if (!await mockups.remove(c.req.param("id"))) return c.json({ error: "Mockup not found" }, 404);
|
|
6175
|
+
return c.json({ ok: true });
|
|
6176
|
+
});
|
|
6177
|
+
app.post("/api/mockups/:id/comments", async (c) => {
|
|
6178
|
+
const mockupId = c.req.param("id");
|
|
6179
|
+
const mockup = await mockups.get(mockupId);
|
|
6180
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
6181
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6182
|
+
const kind = body.kind;
|
|
6183
|
+
if (kind !== "section" && kind !== "block" && kind !== "point") return c.json({ error: "kind must be section, block, or point" }, 400);
|
|
6184
|
+
const screenId = typeof body.screenId === "string" ? body.screenId : "";
|
|
6185
|
+
if (!mockup.screens.some((s) => s.id === screenId)) return c.json({ error: "Unknown screenId" }, 400);
|
|
6186
|
+
if (typeof body.nonce === "string" && body.nonce) {
|
|
6187
|
+
const nonceError = validateMockupNonce(body.nonce, mockupId, screenId);
|
|
6188
|
+
if (nonceError) return c.json({
|
|
6189
|
+
error: nonceError,
|
|
6190
|
+
code: "invalid-nonce"
|
|
6191
|
+
}, 409);
|
|
6192
|
+
}
|
|
6193
|
+
if (typeof body.body !== "string" || !body.body.trim()) return c.json({ error: "body is required" }, 400);
|
|
6194
|
+
const viewport = normalizeMockupViewport(body.viewport);
|
|
6195
|
+
const severityRaw = body.severity;
|
|
6196
|
+
const severity = severityRaw === "blocking" || severityRaw === "nit" || severityRaw === "question" || severityRaw === "praise" || severityRaw === "none" ? severityRaw : void 0;
|
|
6197
|
+
const rect = body.rect && typeof body.rect === "object" ? body.rect : void 0;
|
|
6198
|
+
const comment = {
|
|
6199
|
+
id: crypto.randomUUID(),
|
|
6200
|
+
screenId,
|
|
6201
|
+
kind,
|
|
6202
|
+
target: typeof body.target === "string" ? body.target : void 0,
|
|
6203
|
+
selector: typeof body.selector === "string" ? body.selector : void 0,
|
|
6204
|
+
fingerprint: typeof body.fingerprint === "string" ? body.fingerprint : void 0,
|
|
6205
|
+
html: typeof body.html === "string" ? body.html : void 0,
|
|
6206
|
+
contextHtml: typeof body.contextHtml === "string" ? body.contextHtml : void 0,
|
|
6207
|
+
x: Number.isFinite(body.x) ? Number(body.x) : void 0,
|
|
6208
|
+
y: Number.isFinite(body.y) ? Number(body.y) : void 0,
|
|
6209
|
+
sectionX: Number.isFinite(body.sectionX) ? Number(body.sectionX) : void 0,
|
|
6210
|
+
sectionY: Number.isFinite(body.sectionY) ? Number(body.sectionY) : void 0,
|
|
6211
|
+
snapshot: typeof body.snapshot === "string" ? body.snapshot : void 0,
|
|
6212
|
+
rect: rect && Number.isFinite(rect.x) && Number.isFinite(rect.y) && Number.isFinite(rect.w) && Number.isFinite(rect.h) ? {
|
|
6213
|
+
x: Number(rect.x),
|
|
6214
|
+
y: Number(rect.y),
|
|
6215
|
+
w: Number(rect.w),
|
|
6216
|
+
h: Number(rect.h)
|
|
6217
|
+
} : void 0,
|
|
6218
|
+
body: String(body.body),
|
|
6219
|
+
status: "open",
|
|
6220
|
+
createdAt: Date.now(),
|
|
6221
|
+
createdAtMockupVersion: Number.isFinite(body.createdAtMockupVersion) ? Number(body.createdAtMockupVersion) : mockup.version,
|
|
6222
|
+
viewport,
|
|
6223
|
+
replies: [],
|
|
6224
|
+
...severity && severity !== "none" ? { severity } : {}
|
|
6225
|
+
};
|
|
6226
|
+
const updated = await mockups.addComment(mockupId, comment);
|
|
6227
|
+
if (!updated) return c.json({ error: "Mockup not found" }, 404);
|
|
6228
|
+
return c.json(updated, 201);
|
|
6229
|
+
});
|
|
6230
|
+
app.put("/api/mockups/:id/comments/:commentId", async (c) => {
|
|
6231
|
+
const { body, status } = await c.req.json();
|
|
6232
|
+
const updated = await mockups.updateComment(c.req.param("id"), c.req.param("commentId"), {
|
|
6233
|
+
body,
|
|
6234
|
+
status
|
|
6235
|
+
});
|
|
6236
|
+
if (!updated) return c.json({ error: "Mockup or comment not found" }, 404);
|
|
6237
|
+
return c.json(updated);
|
|
6238
|
+
});
|
|
6239
|
+
app.delete("/api/mockups/:id/comments/:commentId", async (c) => {
|
|
6240
|
+
const updated = await mockups.removeComment(c.req.param("id"), c.req.param("commentId"));
|
|
6241
|
+
if (!updated) return c.json({ error: "Mockup or comment not found" }, 404);
|
|
6242
|
+
return c.json(updated);
|
|
6243
|
+
});
|
|
6244
|
+
app.post("/api/mockups/:id/comments/:commentId/replies", async (c) => {
|
|
6245
|
+
const { body, role, model } = await c.req.json();
|
|
6246
|
+
const reply = {
|
|
6247
|
+
id: crypto.randomUUID(),
|
|
6248
|
+
body,
|
|
6249
|
+
createdAt: Date.now(),
|
|
6250
|
+
role: role || (model ? "agent" : "user"),
|
|
6251
|
+
model: model || void 0
|
|
6252
|
+
};
|
|
6253
|
+
const updated = await mockups.addReply(c.req.param("id"), c.req.param("commentId"), reply);
|
|
6254
|
+
if (!updated) return c.json({ error: "Mockup or comment not found" }, 404);
|
|
6255
|
+
return c.json(updated);
|
|
6256
|
+
});
|
|
6257
|
+
app.put("/api/mockups/:id/comments/:commentId/replies/:replyId", async (c) => {
|
|
6258
|
+
const { body } = await c.req.json();
|
|
6259
|
+
if (!body) return c.json({ error: "Body is required" }, 400);
|
|
6260
|
+
const updated = await mockups.updateReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"), body);
|
|
6261
|
+
if (!updated) return c.json({ error: "Mockup, comment, or reply not found" }, 404);
|
|
6262
|
+
return c.json(updated);
|
|
6263
|
+
});
|
|
6264
|
+
app.delete("/api/mockups/:id/comments/:commentId/replies/:replyId", async (c) => {
|
|
6265
|
+
const updated = await mockups.removeReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"));
|
|
6266
|
+
if (!updated) return c.json({ error: "Mockup, comment, or reply not found" }, 404);
|
|
6267
|
+
return c.json(updated);
|
|
6268
|
+
});
|
|
6269
|
+
app.post("/api/mockups/:id/decision", async (c) => {
|
|
6270
|
+
const mockupId = c.req.param("id");
|
|
6271
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6272
|
+
const decision = body.decision;
|
|
6273
|
+
if (decision !== "approved" && decision !== "rejected" && decision !== "changes-requested" && decision !== "comment-only") return c.json({ error: "decision must be one of: approved, rejected, changes-requested, comment-only" }, 400);
|
|
6274
|
+
const decisionComment = typeof body.decisionComment === "string" ? body.decisionComment : void 0;
|
|
6275
|
+
const mode = body?.mode === "comment-only" || body?.mode === "standard" ? body.mode : "standard";
|
|
6276
|
+
const focusedScreen = typeof body.screen === "string" ? body.screen : void 0;
|
|
6277
|
+
const focusedViewport = isMockupViewport(body.viewport) ? body.viewport : void 0;
|
|
6278
|
+
const mockup = await mockups.setDecision(mockupId, decision, decisionComment);
|
|
6279
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
6280
|
+
const openCommentCount = (mockup.comments ?? []).filter((x) => x.status === "open").length;
|
|
6281
|
+
const payload = mockupReviewSession.decide({
|
|
6282
|
+
mockup,
|
|
6283
|
+
decision,
|
|
6284
|
+
decisionComment: mockup.decisionComment,
|
|
6285
|
+
reviewXml: formatMockupReview(mockup, {
|
|
6286
|
+
mode,
|
|
6287
|
+
focusedScreen,
|
|
6288
|
+
focusedViewport
|
|
6289
|
+
}),
|
|
6290
|
+
openCommentCount,
|
|
6291
|
+
mode
|
|
6292
|
+
});
|
|
6293
|
+
return c.json({
|
|
6294
|
+
ok: true,
|
|
6295
|
+
round: payload.round,
|
|
6296
|
+
decision,
|
|
6297
|
+
mode: payload.mode,
|
|
6298
|
+
openCommentCount,
|
|
6299
|
+
waiters: mockupReviewSession.snapshot().waiters
|
|
6300
|
+
});
|
|
6301
|
+
});
|
|
6302
|
+
app.get("/api/mockup-review/await", async (c) => {
|
|
6303
|
+
const sinceRaw = c.req.query("sinceRound");
|
|
6304
|
+
const sinceRound = sinceRaw !== void 0 && sinceRaw !== "" ? Number(sinceRaw) : void 0;
|
|
6305
|
+
const requested = Number(c.req.query("timeoutMs")) || 25e3;
|
|
6306
|
+
const timeoutMs = Math.min(Math.max(requested, 1e3), 5e4);
|
|
6307
|
+
const result = await mockupReviewSession.await({
|
|
6308
|
+
sinceRound: Number.isNaN(sinceRound) ? void 0 : sinceRound,
|
|
6309
|
+
timeoutMs,
|
|
6310
|
+
signal: c.req.raw.signal
|
|
6311
|
+
});
|
|
6312
|
+
return c.json(result);
|
|
6313
|
+
});
|
|
6314
|
+
app.get("/api/mockup-review/status", (c) => {
|
|
6315
|
+
return c.json(mockupReviewSession.snapshot());
|
|
6316
|
+
});
|
|
4529
6317
|
app.post("/api/attachments", async (c) => {
|
|
4530
6318
|
const file = (await c.req.parseBody())["file"];
|
|
4531
6319
|
if (!file || !(file instanceof File)) return c.json({ error: "No file uploaded" }, 400);
|
|
@@ -4602,6 +6390,7 @@ async function newestActivityMs(projectDir) {
|
|
|
4602
6390
|
for (const name of [
|
|
4603
6391
|
"comments.json",
|
|
4604
6392
|
"plans.json",
|
|
6393
|
+
"mockups.json",
|
|
4605
6394
|
"pr-session.json"
|
|
4606
6395
|
]) {
|
|
4607
6396
|
const file = join(projectDir, name);
|