diffing 0.17.0 → 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-qNwkSjxK.mjs → cli-agent-du9TS34N.mjs} +741 -13
- 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-DhynPewa.mjs → mcp-H7ytYScV.mjs} +397 -6
- 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-CEbp7K-k.mjs → server-CiE24vh-.mjs} +1567 -15
- package/dist/{setup-BnS_rSxK.mjs → setup-BwWZ9Fzq.mjs} +1 -1
- package/extensions/pi/index.ts +452 -5
- package/package.json +1 -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);
|
|
@@ -1744,6 +1744,1029 @@ var FilePlanStore = class {
|
|
|
1744
1744
|
}
|
|
1745
1745
|
};
|
|
1746
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());
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
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;
|
|
@@ -3199,17 +4222,57 @@ function collectPrAvatarUrls(session) {
|
|
|
3199
4222
|
function isCustomMode(opts) {
|
|
3200
4223
|
return opts.revisions.length > 0 || opts.pathspecs.length > 0 || opts.showMode;
|
|
3201
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
|
+
}
|
|
3202
4264
|
function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore, prSessionStore, prMode = false, security = {
|
|
3203
4265
|
bindHost: "127.0.0.1",
|
|
3204
4266
|
authToken: null,
|
|
3205
4267
|
insecureNoAuth: true
|
|
3206
|
-
}) {
|
|
4268
|
+
}, mockupStore) {
|
|
3207
4269
|
const app = new Hono();
|
|
3208
4270
|
app.use("*", createServerAuthMiddleware(security));
|
|
3209
4271
|
let diffOpts = { ...diffOptsInput };
|
|
3210
4272
|
const customMode = isCustomMode(diffOpts);
|
|
3211
4273
|
const store = commentStore ?? new FileCommentStore();
|
|
3212
4274
|
const plans = planStore ?? new FilePlanStore();
|
|
4275
|
+
const mockups = mockupStore ?? new FileMockupStore();
|
|
3213
4276
|
const prStore = prSessionStore ?? new FilePrSessionStore();
|
|
3214
4277
|
const agentDiffCache = new AgentDiffIndexCache();
|
|
3215
4278
|
const uiStateStore = new FileUiStateStore();
|
|
@@ -3233,17 +4296,37 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3233
4296
|
agents: agentsSnapshot()
|
|
3234
4297
|
})));
|
|
3235
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
|
+
};
|
|
3236
4318
|
let repoRoot;
|
|
3237
4319
|
try {
|
|
3238
4320
|
repoRoot = getRepoRoot();
|
|
3239
4321
|
} catch {
|
|
3240
4322
|
repoRoot = process.cwd();
|
|
3241
4323
|
}
|
|
3242
|
-
if (!commentStore && !planStore) try {
|
|
4324
|
+
if (!commentStore && !planStore && !mockupStore) try {
|
|
3243
4325
|
const storageDir = getProjectStorageDir();
|
|
3244
4326
|
mkdirSync(storageDir, { recursive: true });
|
|
3245
4327
|
let commentsDebounce = null;
|
|
3246
4328
|
let plansDebounce = null;
|
|
4329
|
+
let mockupsDebounce = null;
|
|
3247
4330
|
let prSessionDebounce = null;
|
|
3248
4331
|
watch(storageDir, (_eventType, filename) => {
|
|
3249
4332
|
if (!filename) return;
|
|
@@ -3253,6 +4336,9 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3253
4336
|
} else if (filename.startsWith("plans.json")) {
|
|
3254
4337
|
if (plansDebounce) clearTimeout(plansDebounce);
|
|
3255
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);
|
|
3256
4342
|
} else if (filename.startsWith("pr-session.json")) {
|
|
3257
4343
|
if (prSessionDebounce) clearTimeout(prSessionDebounce);
|
|
3258
4344
|
prSessionDebounce = setTimeout(() => broadcast("pr-session", Date.now().toString()), 120);
|
|
@@ -3263,7 +4349,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3263
4349
|
}
|
|
3264
4350
|
let debounceTimeout = null;
|
|
3265
4351
|
try {
|
|
3266
|
-
watch(repoRoot, { recursive: true }, (
|
|
4352
|
+
watch(repoRoot, { recursive: true }, (_eventType, filename) => {
|
|
3267
4353
|
if (!filename) return;
|
|
3268
4354
|
const parts = filename.split(/[/\\]/);
|
|
3269
4355
|
const isGit = parts.includes(".git");
|
|
@@ -4295,7 +5381,6 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4295
5381
|
const ref = typeof body?.ref === "string" ? body.ref : "";
|
|
4296
5382
|
if (!ref.trim()) return c.json({ error: "ref is required" }, 400);
|
|
4297
5383
|
try {
|
|
4298
|
-
parsePrRef(ref, await detectCwdRepo() ?? void 0);
|
|
4299
5384
|
const session = await buildPrSession(ref);
|
|
4300
5385
|
await prStore.set(session);
|
|
4301
5386
|
return c.json({
|
|
@@ -4477,7 +5562,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4477
5562
|
submittedAt: Date.now(),
|
|
4478
5563
|
submittedReviewId: result.reviewId,
|
|
4479
5564
|
submittedReviewUrl: result.reviewUrl,
|
|
4480
|
-
authSource: result.authSource
|
|
5565
|
+
authSource: result.authSource === "none" ? void 0 : result.authSource
|
|
4481
5566
|
};
|
|
4482
5567
|
await prStore.set(next);
|
|
4483
5568
|
}
|
|
@@ -4763,6 +5848,472 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4763
5848
|
app.get("/api/plan-review/status", (c) => {
|
|
4764
5849
|
return c.json(planReviewSession.snapshot());
|
|
4765
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
|
+
});
|
|
4766
6317
|
app.post("/api/attachments", async (c) => {
|
|
4767
6318
|
const file = (await c.req.parseBody())["file"];
|
|
4768
6319
|
if (!file || !(file instanceof File)) return c.json({ error: "No file uploaded" }, 400);
|
|
@@ -4839,6 +6390,7 @@ async function newestActivityMs(projectDir) {
|
|
|
4839
6390
|
for (const name of [
|
|
4840
6391
|
"comments.json",
|
|
4841
6392
|
"plans.json",
|
|
6393
|
+
"mockups.json",
|
|
4842
6394
|
"pr-session.json"
|
|
4843
6395
|
]) {
|
|
4844
6396
|
const file = join(projectDir, name);
|