diffing 0.17.0 → 0.18.1
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-Cf6SAnH4.mjs} +744 -15
- 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-BV3gPi7L.js} +1 -1
- package/dist/client/assets/{angular-ts-CiwZGajG.js → angular-ts-DuuSfKJK.js} +1 -1
- package/dist/client/assets/{apl-C8k5a5fk.js → apl-BMc69egk.js} +1 -1
- package/dist/client/assets/{arc-B8VMoqVr.js → arc-CEC1cz1S.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-C-ng2xNL.js → architectureDiagram-3BPJPVTR-BmJ5OHgd.js} +1 -1
- package/dist/client/assets/{astro-BE-h-qpp.js → astro-Bg_CYwmB.js} +1 -1
- package/dist/client/assets/{blade-D-0BV1wW.js → blade-Mca0XoRC.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-DyAQuONU.js → blockDiagram-GPEHLZMM--XrThRLW.js} +1 -1
- package/dist/client/assets/{c-Dg_Gx29H.js → c-B2Uqi6oI.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-k5uKTHvm.js → c4Diagram-AAUBKEIU-W8w5oiRi.js} +1 -1
- package/dist/client/assets/channel-C9URWRe4.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Dod_VwYj.js → chunk-2J33WTMH-Bpr3MqKf.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-B8HMjry-.js → chunk-3OPIFGDE-DdMV_3DM.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-TvkJVIZw.js → chunk-4BX2VUAB-BkfeLLsz.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-BsXuKAKc.js → chunk-55IACEB6-pCwXbxR0.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-BI1cj2cZ.js → chunk-5ZQYHXKU-BGFzCgvs.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-yWQ27Wk0.js → chunk-727SXJPM-DU7oVWfJ.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-B2BBCtTr.js → chunk-AQP2D5EJ-DaVYG1qm.js} +1 -1
- package/dist/client/assets/{chunk-BSJP7CBP-CXXvPXhK.js → chunk-BSJP7CBP-DAvnzgjY.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-jYiloqyi.js → chunk-CSCIHK7Q-D6ZDvMsi.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-DZzqIT32.js → chunk-FMBD7UC4-BFDCYCqF.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-krdCf8iG.js → chunk-KSCS5N6A-BmD6XY52.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-0uuTmeZV.js → chunk-L5ZTLDWV-Dl9iUIBf.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-BBfUgD06.js → chunk-LZXEDZCA-rOOJDxdB.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-jkir4ZKE.js → chunk-ND2GUHAM-DBDu-JeD.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-DMWZNibJ.js → chunk-NZK2D7GU-CRyk4F3R.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-B1e1HGaX.js → chunk-O5CBEL6O-DcKB07iP.js} +1 -1
- package/dist/client/assets/chunk-QZHKN3VN-BBNO2-yv.js +1 -0
- package/dist/client/assets/chunk-WU5MYG2G-HP2Om5f9.js +1 -0
- package/dist/client/assets/{chunk-XPW4576I-AvbCAUav.js → chunk-XPW4576I-WuzWMSrp.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BW_ZPHe7.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DOd628kk.js +1 -0
- package/dist/client/assets/{cobol-DQnGB_GV.js → cobol-oQ1i-YtO.js} +1 -1
- package/dist/client/assets/{coffee-CDV5a89x.js → coffee-s9XlmHbB.js} +1 -1
- package/dist/client/assets/{cose-bilkent-S5V4N54A-t-mDNRF2.js → cose-bilkent-S5V4N54A-CymUm7fn.js} +1 -1
- package/dist/client/assets/{cpp-8G6b-siI.js → cpp-D60LxTox.js} +1 -1
- package/dist/client/assets/{crystal-CGYCC6FM.js → crystal-CHrCczDb.js} +1 -1
- package/dist/client/assets/{css-nez4R8_Z.js → css-1PQO32Hw.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG--azesYIg.js → dagre-BM42HDAG-DOHZc0tO.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-4WJ7nKhA.js → diagram-2AECGRRQ-D8O2cleS.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-C7ZGhLXn.js → diagram-5GNKFQAL-CO-Ej4UD.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BobK_rbl.js → diagram-KO2AKTUF-DCUr4wzS.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-BOc-OkIS.js → diagram-LMA3HP47-D9HnA0Qg.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BEtLgm86.js → diagram-OG6HWLK6-BxAitO2n.js} +1 -1
- package/dist/client/assets/{dist-DPuBwToc.js → dist-N-ROBnkL.js} +1 -1
- package/dist/client/assets/{edge-Z5RMbJQv.js → edge-Ck54Fn7y.js} +1 -1
- package/dist/client/assets/{edit-CFxbgVPV.js → edit-B8RwtrDs.js} +1 -1
- package/dist/client/assets/{elixir-d3C79FQc.js → elixir-xXxdfsfU.js} +1 -1
- package/dist/client/assets/{elm-CDAf6Uyn.js → elm-g0vLRWbF.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D-pR2GY9.js → erDiagram-TEJ5UH35-1ZMg7g98.js} +1 -1
- package/dist/client/assets/{erb-CTJk9FKH.js → erb-CGFnxOST.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-BwMCy2QV.js → flowDiagram-I6XJVG4X-D5u9lhJ-.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-BI1WLJDi.js → ganttDiagram-6RSMTGT7-BbVKEzs8.js} +1 -1
- package/dist/client/assets/{git-rebase-Dr0-MVoW.js → git-rebase-B3s6bE5_.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CUfW0k1f.js → gitGraphDiagram-PVQCEYII-sAottiiz.js} +1 -1
- package/dist/client/assets/{glimmer-js-CXhLhyW-.js → glimmer-js-C2Xff18_.js} +1 -1
- package/dist/client/assets/{glimmer-ts-BFAFT-VD.js → glimmer-ts-CRBS8ftd.js} +1 -1
- package/dist/client/assets/{glsl-D7yanhgY.js → glsl-aOY_t6dG.js} +1 -1
- package/dist/client/assets/{graphql-CpoDxrok.js → graphql-Cucpdpc6.js} +1 -1
- package/dist/client/assets/{hack-BL5fmC4q.js → hack-Bw0KkVza.js} +1 -1
- package/dist/client/assets/{haml-ClAE6RVM.js → haml-DnyaPoa2.js} +1 -1
- package/dist/client/assets/{handlebars-nHw91uOv.js → handlebars-AedEof-m.js} +1 -1
- package/dist/client/assets/{html-BIiOKqgG.js → html-Bu5h_Kgf.js} +1 -1
- package/dist/client/assets/{html-derivative-D1nPnuOK.js → html-derivative-CNiVLld1.js} +1 -1
- package/dist/client/assets/{http-B_QcRZjt.js → http-B9-oOYPw.js} +1 -1
- package/dist/client/assets/{hurl-CZE0DsD3.js → hurl-Bo6NUnnd.js} +1 -1
- package/dist/client/assets/{index-Erqhm7rW.js → index-BkwV-g6J.js} +91 -91
- package/dist/client/assets/index-C73yr1tm.css +1 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-w2M_OHua.js → infoDiagram-5YYISTIA-CfVELF4R.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CW_l9PGx.js → ishikawaDiagram-YF4QCWOH-DztjIGfb.js} +1 -1
- package/dist/client/assets/{java-CXTqX09w.js → java-JJ3dl_jz.js} +1 -1
- package/dist/client/assets/{javascript-9z9ABBep.js → javascript-QgUuQNTw.js} +1 -1
- package/dist/client/assets/{jinja-Cluf_5Bk.js → jinja-GbeEm2-u.js} +1 -1
- package/dist/client/assets/{jison-CqCEUtc4.js → jison-BXZbHsO3.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-D-YwusBa.js → journeyDiagram-JHISSGLW-Lg8CkxeI.js} +1 -1
- package/dist/client/assets/{json-BMq_bPP8.js → json-D02YvQSa.js} +1 -1
- package/dist/client/assets/{jsx-Djij59b9.js → jsx-eZde6NZS.js} +1 -1
- package/dist/client/assets/{julia-DybzCMnh.js → julia-CfcuUD-A.js} +1 -1
- package/dist/client/assets/{just-4ChcAr9-.js → just-hQr8WcQp.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Brvoukmm.js → kanban-definition-UN3LZRKU-BWIeZIf6.js} +1 -1
- package/dist/client/assets/{latex-CS2TtFXG.js → latex-BxffZwv_.js} +1 -1
- package/dist/client/assets/{line-CqxM-stA.js → line-Cy1AFxf0.js} +1 -1
- package/dist/client/assets/{linear-C7T82059.js → linear-Kk5IbGHr.js} +1 -1
- package/dist/client/assets/{liquid-84AtYenm.js → liquid-BvKnpUyR.js} +1 -1
- package/dist/client/assets/{lua-DJzFx3Cf.js → lua-Bcds2vY9.js} +1 -1
- package/dist/client/assets/{marko-BLw9BV9E.js → marko-CZJQLjYc.js} +1 -1
- package/dist/client/assets/{mdc-BjZejN3b.js → mdc-BRigtixg.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-BdiEBAsd.js → mermaid-parser.core-Dh2uTVxt.js} +1 -1
- package/dist/client/assets/{mermaid.core-BsnKhnrM.js → mermaid.core-CXIGVQ74.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-CBaIFnbc.js → mindmap-definition-RKZ34NQL-DQNQJuAB.js} +1 -1
- package/dist/client/assets/{nginx-DuamCwvX.js → nginx-BI3-LI9U.js} +1 -1
- package/dist/client/assets/{nim-Bt8c9-ge.js → nim-PEd8ZOME.js} +1 -1
- package/dist/client/assets/{perl-D52AfmwC.js → perl-BPTGdP4p.js} +1 -1
- package/dist/client/assets/{php-BBMlJg0_.js → php-C8BKL0yL.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BlmPajcA.js → pieDiagram-4H26LBE5-Dz7gE0t5.js} +1 -1
- package/dist/client/assets/{pug-DFHmDqEE.js → pug-H5z9rtbz.js} +1 -1
- package/dist/client/assets/{qml-C0333o-g.js → qml-EzSpo2aM.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Bqlm_zZk.js → quadrantDiagram-W4KKPZXB-pTnmYdEJ.js} +1 -1
- package/dist/client/assets/{r-DZFmvIgR.js → r-VMeUwMIt.js} +1 -1
- package/dist/client/assets/{razor-BIGLkWzR.js → razor-Ne05zcaj.js} +1 -1
- package/dist/client/assets/{regexp-BW6jpgf9.js → regexp-i5_jUZAj.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-B1PrtUeD.js → requirementDiagram-4Y6WPE33-GVw3NOxE.js} +1 -1
- package/dist/client/assets/{review-export-BlgD3bdw.js → review-export-B4TOUDUE.js} +1 -1
- package/dist/client/assets/{rst-Cj51ZJaa.js → rst-BZWuei_6.js} +1 -1
- package/dist/client/assets/{ruby-lMhGjwpf.js → ruby-CApICFTD.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-D2HtUNVF.js → sankeyDiagram-5OEKKPKP-CwWzwmqj.js} +1 -1
- package/dist/client/assets/{sas-BWSQku7l.js → sas-BzEPX6GL.js} +1 -1
- package/dist/client/assets/{scss-CdRPuhLN.js → scss-B6Ld2NRm.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-D9SQy0XI.js → sequenceDiagram-3UESZ5HK-Bz_Q76Kt.js} +1 -1
- package/dist/client/assets/{shellscript-DOfOBXcj.js → shellscript-KmoqGVVu.js} +1 -1
- package/dist/client/assets/{shellsession-7XAOCVni.js → shellsession-CsSh_KTo.js} +1 -1
- package/dist/client/assets/{soy-DDnzVRIw.js → soy-CLgOu1If.js} +1 -1
- package/dist/client/assets/{sql-Ba34agKe.js → sql-BdCh56kU.js} +1 -1
- package/dist/client/assets/{src-BO-qT_v_.js → src-D7QQkGEc.js} +1 -1
- package/dist/client/assets/{stata-BQlYGHQU.js → stata-C3fo8EYS.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-7IEYjFRo.js → stateDiagram-AJRCARHV-D0pZtzLu.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-P0vTqsyH.js +1 -0
- package/dist/client/assets/{surrealql-BET1l7SW.js → surrealql-qzihsJw_.js} +1 -1
- package/dist/client/assets/{svelte-Bd7ZOeQI.js → svelte-B4tbn_KI.js} +1 -1
- package/dist/client/assets/{templ-BYvRQdOw.js → templ-Ddev3BvY.js} +1 -1
- package/dist/client/assets/{tex-CT8DpKxX.js → tex-DJLtgNQ6.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-CurQYWmi.js → timeline-definition-PNZ67QCA-CWTWgYi0.js} +1 -1
- package/dist/client/assets/{ts-tags-D4iPPwau.js → ts-tags-qqxigRtr.js} +1 -1
- package/dist/client/assets/{tsx-Dgk_Aw-B.js → tsx-Pkf9BaPv.js} +1 -1
- package/dist/client/assets/{twig-abRNWeh3.js → twig-BiScb10p.js} +1 -1
- package/dist/client/assets/{typescript-BgR1JqdC.js → typescript-Bhy3UXig.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-DbrRBL5S.js → vennDiagram-CIIHVFJN-DwB89v9I.js} +1 -1
- package/dist/client/assets/{vue-DSkhKq4Q.js → vue-CANMWBRv.js} +1 -1
- package/dist/client/assets/{vue-html-J0jWoD9X.js → vue-html-DtS6f62r.js} +1 -1
- package/dist/client/assets/{vue-vine-BgX5MC8-.js → vue-vine-BpQ9kwSB.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DIFXQsoP.js → wardleyDiagram-YWT4CUSO-DNwwPk4s.js} +1 -1
- package/dist/client/assets/{xml-Ca53xGLB.js → xml-CD5eIXsm.js} +1 -1
- package/dist/client/assets/{xsl-CNnXclYV.js → xsl-DGNZQAUu.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-CmfInPqa.js → xychartDiagram-2RQKCTM6-L7M4YSvw.js} +1 -1
- package/dist/client/assets/{yaml-CjjyUhfg.js → yaml-Cm8mwIB7.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-DoNBucix.mjs} +403 -6
- package/dist/mockup-format-D4Ln0WxJ.mjs +327 -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-BRQdb64n.mjs} +1727 -49
- 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-D4Ln0WxJ.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);
|
|
@@ -1704,45 +1704,1181 @@ var FilePlanStore = class {
|
|
|
1704
1704
|
return true;
|
|
1705
1705
|
});
|
|
1706
1706
|
}
|
|
1707
|
-
async removeComment(planId, commentId) {
|
|
1708
|
-
return this.mutate(planId, (plan) => {
|
|
1709
|
-
const idx = plan.comments.findIndex((x) => x.id === commentId);
|
|
1710
|
-
if (idx === -1) return false;
|
|
1711
|
-
plan.comments.splice(idx, 1);
|
|
1712
|
-
return true;
|
|
1713
|
-
});
|
|
1707
|
+
async removeComment(planId, commentId) {
|
|
1708
|
+
return this.mutate(planId, (plan) => {
|
|
1709
|
+
const idx = plan.comments.findIndex((x) => x.id === commentId);
|
|
1710
|
+
if (idx === -1) return false;
|
|
1711
|
+
plan.comments.splice(idx, 1);
|
|
1712
|
+
return true;
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
async addReply(planId, commentId, reply) {
|
|
1716
|
+
return this.mutate(planId, (plan) => {
|
|
1717
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1718
|
+
if (!c) return false;
|
|
1719
|
+
if (typeof c.createdAtPlanVersion === "number" && typeof reply.createdAtPlanVersion !== "number") reply.createdAtPlanVersion = c.createdAtPlanVersion;
|
|
1720
|
+
if (!c.replies) c.replies = [];
|
|
1721
|
+
c.replies.push(reply);
|
|
1722
|
+
return true;
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
async removeReply(planId, commentId, replyId) {
|
|
1726
|
+
return this.mutate(planId, (plan) => {
|
|
1727
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1728
|
+
if (!c) return false;
|
|
1729
|
+
const idx = c.replies.findIndex((r) => r.id === replyId);
|
|
1730
|
+
if (idx === -1) return false;
|
|
1731
|
+
c.replies.splice(idx, 1);
|
|
1732
|
+
return true;
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
async updateReply(planId, commentId, replyId, body) {
|
|
1736
|
+
return this.mutate(planId, (plan) => {
|
|
1737
|
+
const c = plan.comments.find((x) => x.id === commentId);
|
|
1738
|
+
if (!c) return false;
|
|
1739
|
+
const reply = c.replies.find((r) => r.id === replyId);
|
|
1740
|
+
if (!reply) return false;
|
|
1741
|
+
reply.body = body;
|
|
1742
|
+
return true;
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
//#endregion
|
|
1747
|
+
//#region src/lib/mockups.ts
|
|
1748
|
+
function mockupSourceDir(storageDir, mockupId) {
|
|
1749
|
+
return join(storageDir, "mockup-sources", mockupId);
|
|
1750
|
+
}
|
|
1751
|
+
function newId() {
|
|
1752
|
+
return crypto.randomUUID();
|
|
1753
|
+
}
|
|
1754
|
+
function snapshotScreens(screens) {
|
|
1755
|
+
return screens.map((s) => ({
|
|
1756
|
+
id: s.id,
|
|
1757
|
+
label: s.label,
|
|
1758
|
+
html: s.html
|
|
1759
|
+
}));
|
|
1760
|
+
}
|
|
1761
|
+
function backfillMockup(mockup) {
|
|
1762
|
+
const currentVersion = mockup.version ?? 1;
|
|
1763
|
+
const have = Array.isArray(mockup.versions) ? mockup.versions.length : 0;
|
|
1764
|
+
if (!mockup.versions || have === 0 || have < currentVersion) {
|
|
1765
|
+
const createdAt = mockup.updatedAt ?? mockup.createdAt ?? Date.now();
|
|
1766
|
+
const haveEntries = Array.isArray(mockup.versions) ? mockup.versions : [];
|
|
1767
|
+
const next = [];
|
|
1768
|
+
for (let i = 1; i <= currentVersion; i++) {
|
|
1769
|
+
const existing = haveEntries.find((v) => v.version === i);
|
|
1770
|
+
next.push(existing ?? {
|
|
1771
|
+
version: i,
|
|
1772
|
+
title: mockup.title,
|
|
1773
|
+
screens: snapshotScreens(mockup.screens ?? []),
|
|
1774
|
+
source: mockup.source,
|
|
1775
|
+
model: mockup.model,
|
|
1776
|
+
createdAt
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
mockup.versions = next.filter((v) => v.version >= 1 && v.version <= currentVersion);
|
|
1780
|
+
}
|
|
1781
|
+
if (!mockup.screens) mockup.screens = [];
|
|
1782
|
+
if (!mockup.comments) mockup.comments = [];
|
|
1783
|
+
for (const c of mockup.comments) {
|
|
1784
|
+
if (typeof c.createdAtMockupVersion !== "number") c.createdAtMockupVersion = mockup.version ?? 1;
|
|
1785
|
+
if (!c.replies) c.replies = [];
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function applyUpsert(mockups, input, now) {
|
|
1789
|
+
if (input.id) {
|
|
1790
|
+
const existing = mockups.find((m) => m.id === input.id);
|
|
1791
|
+
if (existing) {
|
|
1792
|
+
existing.title = input.title;
|
|
1793
|
+
existing.screens = snapshotScreens(input.screens);
|
|
1794
|
+
if (input.source !== void 0) existing.source = input.source;
|
|
1795
|
+
if (input.model !== void 0) existing.model = input.model;
|
|
1796
|
+
existing.version += 1;
|
|
1797
|
+
existing.updatedAt = now;
|
|
1798
|
+
if (!existing.versions) existing.versions = [];
|
|
1799
|
+
existing.versions.push({
|
|
1800
|
+
version: existing.version,
|
|
1801
|
+
title: existing.title,
|
|
1802
|
+
screens: snapshotScreens(existing.screens),
|
|
1803
|
+
source: existing.source,
|
|
1804
|
+
model: existing.model,
|
|
1805
|
+
createdAt: now
|
|
1806
|
+
});
|
|
1807
|
+
existing.decision = "pending";
|
|
1808
|
+
existing.decisionComment = void 0;
|
|
1809
|
+
existing.decidedAt = void 0;
|
|
1810
|
+
return existing;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
const mockup = {
|
|
1814
|
+
id: input.id || newId(),
|
|
1815
|
+
title: input.title,
|
|
1816
|
+
screens: snapshotScreens(input.screens),
|
|
1817
|
+
source: input.source,
|
|
1818
|
+
model: input.model,
|
|
1819
|
+
createdAt: now,
|
|
1820
|
+
updatedAt: now,
|
|
1821
|
+
version: 1,
|
|
1822
|
+
decision: "pending",
|
|
1823
|
+
comments: [],
|
|
1824
|
+
versions: [{
|
|
1825
|
+
version: 1,
|
|
1826
|
+
title: input.title,
|
|
1827
|
+
screens: snapshotScreens(input.screens),
|
|
1828
|
+
source: input.source,
|
|
1829
|
+
model: input.model,
|
|
1830
|
+
createdAt: now
|
|
1831
|
+
}]
|
|
1832
|
+
};
|
|
1833
|
+
mockups.push(mockup);
|
|
1834
|
+
return mockup;
|
|
1835
|
+
}
|
|
1836
|
+
function syncCurrentVersion(mockup) {
|
|
1837
|
+
if (!mockup.versions || mockup.versions.length === 0) mockup.versions = [];
|
|
1838
|
+
if (mockup.versions.length === 0) {
|
|
1839
|
+
mockup.versions.push({
|
|
1840
|
+
version: mockup.version,
|
|
1841
|
+
title: mockup.title,
|
|
1842
|
+
screens: snapshotScreens(mockup.screens),
|
|
1843
|
+
source: mockup.source,
|
|
1844
|
+
model: mockup.model,
|
|
1845
|
+
createdAt: mockup.updatedAt
|
|
1846
|
+
});
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const last = mockup.versions[mockup.versions.length - 1];
|
|
1850
|
+
last.title = mockup.title;
|
|
1851
|
+
last.screens = snapshotScreens(mockup.screens);
|
|
1852
|
+
last.source = mockup.source;
|
|
1853
|
+
last.model = mockup.model;
|
|
1854
|
+
}
|
|
1855
|
+
function versionMismatch(expected, current) {
|
|
1856
|
+
if (expected === void 0) return void 0;
|
|
1857
|
+
if (expected !== current) return {
|
|
1858
|
+
expectedVersion: expected,
|
|
1859
|
+
currentVersion: current
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function bumpMockupVersion(mockup, now) {
|
|
1863
|
+
mockup.version += 1;
|
|
1864
|
+
mockup.updatedAt = now;
|
|
1865
|
+
if (!mockup.versions) mockup.versions = [];
|
|
1866
|
+
mockup.versions.push({
|
|
1867
|
+
version: mockup.version,
|
|
1868
|
+
title: mockup.title,
|
|
1869
|
+
screens: snapshotScreens(mockup.screens),
|
|
1870
|
+
source: mockup.source,
|
|
1871
|
+
model: mockup.model,
|
|
1872
|
+
createdAt: now
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
function applyScreenUpsert(mockup, screen, now) {
|
|
1876
|
+
const html = screen.html.replace(/\r\n/g, "\n");
|
|
1877
|
+
const existing = mockup.screens.find((s) => s.id === screen.id);
|
|
1878
|
+
if (existing) {
|
|
1879
|
+
existing.html = html;
|
|
1880
|
+
if (screen.label?.trim()) existing.label = screen.label.trim();
|
|
1881
|
+
} else mockup.screens.push({
|
|
1882
|
+
id: screen.id,
|
|
1883
|
+
label: screen.label?.trim() || defaultScreenLabel(screen.id),
|
|
1884
|
+
html
|
|
1885
|
+
});
|
|
1886
|
+
bumpMockupVersion(mockup, now);
|
|
1887
|
+
}
|
|
1888
|
+
function applyScreenPatch(screen, patch) {
|
|
1889
|
+
const idx = screen.html.indexOf(patch.expectedText);
|
|
1890
|
+
if (idx === -1) return 0;
|
|
1891
|
+
const count = screen.html.split(patch.expectedText).length - 1;
|
|
1892
|
+
screen.html = screen.html.slice(0, idx) + patch.replacement + screen.html.slice(idx + patch.expectedText.length);
|
|
1893
|
+
return count;
|
|
1894
|
+
}
|
|
1895
|
+
/** Validate every op against the mockup before applying any (all-or-nothing). */
|
|
1896
|
+
function validateThreadBatch(mockup, ops) {
|
|
1897
|
+
const comments = mockup.comments ?? [];
|
|
1898
|
+
for (let i = 0; i < ops.length; i++) {
|
|
1899
|
+
const item = ops[i];
|
|
1900
|
+
const c = comments.find((x) => x.id === item.commentId);
|
|
1901
|
+
if (!c) return `operations[${i}]: comment ${item.commentId} not found`;
|
|
1902
|
+
if ((item.op === "edit" || item.op === "delete") && item.replyId && !(c.replies ?? []).some((r) => r.id === item.replyId)) return `operations[${i}]: reply ${item.replyId} not found on comment ${item.commentId}`;
|
|
1903
|
+
}
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
/** Apply ops after validation passed — every lookup is guaranteed to hit. */
|
|
1907
|
+
function applyThreadOps(mockup, ops, now) {
|
|
1908
|
+
const results = [];
|
|
1909
|
+
for (const item of ops) {
|
|
1910
|
+
const c = mockup.comments.find((x) => x.id === item.commentId);
|
|
1911
|
+
switch (item.op) {
|
|
1912
|
+
case "reply": {
|
|
1913
|
+
const reply = {
|
|
1914
|
+
id: crypto.randomUUID(),
|
|
1915
|
+
body: item.body,
|
|
1916
|
+
createdAt: now,
|
|
1917
|
+
role: item.role === "agent" ? "agent" : item.role === "user" ? "user" : item.model ? "agent" : "user",
|
|
1918
|
+
model: item.model
|
|
1919
|
+
};
|
|
1920
|
+
if (typeof c.createdAtMockupVersion === "number") reply.createdAtPlanVersion = c.createdAtMockupVersion;
|
|
1921
|
+
if (!c.replies) c.replies = [];
|
|
1922
|
+
c.replies.push(reply);
|
|
1923
|
+
results.push({
|
|
1924
|
+
op: "reply",
|
|
1925
|
+
commentId: item.commentId,
|
|
1926
|
+
replyId: reply.id,
|
|
1927
|
+
ok: true
|
|
1928
|
+
});
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
case "edit":
|
|
1932
|
+
if (item.replyId) {
|
|
1933
|
+
const r = c.replies.find((x) => x.id === item.replyId);
|
|
1934
|
+
r.body = item.body;
|
|
1935
|
+
} else c.body = item.body;
|
|
1936
|
+
results.push({
|
|
1937
|
+
op: "edit",
|
|
1938
|
+
commentId: item.commentId,
|
|
1939
|
+
replyId: item.replyId,
|
|
1940
|
+
ok: true
|
|
1941
|
+
});
|
|
1942
|
+
break;
|
|
1943
|
+
case "delete":
|
|
1944
|
+
if (item.replyId) {
|
|
1945
|
+
const idx = c.replies.findIndex((x) => x.id === item.replyId);
|
|
1946
|
+
c.replies.splice(idx, 1);
|
|
1947
|
+
results.push({
|
|
1948
|
+
op: "delete",
|
|
1949
|
+
commentId: item.commentId,
|
|
1950
|
+
replyId: item.replyId,
|
|
1951
|
+
ok: true
|
|
1952
|
+
});
|
|
1953
|
+
} else {
|
|
1954
|
+
const idx = mockup.comments.findIndex((x) => x.id === item.commentId);
|
|
1955
|
+
mockup.comments.splice(idx, 1);
|
|
1956
|
+
results.push({
|
|
1957
|
+
op: "delete",
|
|
1958
|
+
commentId: item.commentId,
|
|
1959
|
+
ok: true
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
break;
|
|
1963
|
+
case "resolve":
|
|
1964
|
+
c.status = "resolved";
|
|
1965
|
+
results.push({
|
|
1966
|
+
op: "resolve",
|
|
1967
|
+
commentId: item.commentId,
|
|
1968
|
+
ok: true
|
|
1969
|
+
});
|
|
1970
|
+
break;
|
|
1971
|
+
case "unresolve":
|
|
1972
|
+
c.status = "open";
|
|
1973
|
+
results.push({
|
|
1974
|
+
op: "unresolve",
|
|
1975
|
+
commentId: item.commentId,
|
|
1976
|
+
ok: true
|
|
1977
|
+
});
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return results;
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Shared HTTP/CLI/MCP shape for the atomic thread batch. Validates every op's
|
|
1985
|
+
* shape before any store mutation; errors carry the offending op index.
|
|
1986
|
+
*/
|
|
1987
|
+
function normalizeThreadOperations(raw) {
|
|
1988
|
+
if (!Array.isArray(raw) || raw.length === 0) return {
|
|
1989
|
+
ok: false,
|
|
1990
|
+
error: "operations[] is required"
|
|
1991
|
+
};
|
|
1992
|
+
const ops = [];
|
|
1993
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1994
|
+
const item = raw[i];
|
|
1995
|
+
if (!item || typeof item !== "object") return {
|
|
1996
|
+
ok: false,
|
|
1997
|
+
error: `operations[${i}] must be an object`,
|
|
1998
|
+
index: i
|
|
1999
|
+
};
|
|
2000
|
+
const op = item.op;
|
|
2001
|
+
const commentId = typeof item.commentId === "string" ? item.commentId : "";
|
|
2002
|
+
if (!commentId) return {
|
|
2003
|
+
ok: false,
|
|
2004
|
+
error: `operations[${i}].commentId is required`,
|
|
2005
|
+
index: i
|
|
2006
|
+
};
|
|
2007
|
+
switch (op) {
|
|
2008
|
+
case "reply": {
|
|
2009
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2010
|
+
if (!body.trim()) return {
|
|
2011
|
+
ok: false,
|
|
2012
|
+
error: `operations[${i}].body is required`,
|
|
2013
|
+
index: i
|
|
2014
|
+
};
|
|
2015
|
+
ops.push({
|
|
2016
|
+
op: "reply",
|
|
2017
|
+
commentId,
|
|
2018
|
+
body,
|
|
2019
|
+
role: typeof item.role === "string" ? item.role : void 0,
|
|
2020
|
+
model: typeof item.model === "string" ? item.model : void 0
|
|
2021
|
+
});
|
|
2022
|
+
break;
|
|
2023
|
+
}
|
|
2024
|
+
case "edit": {
|
|
2025
|
+
const replyId = typeof item.replyId === "string" ? item.replyId : "";
|
|
2026
|
+
const body = typeof item.body === "string" ? item.body : "";
|
|
2027
|
+
if (!body.trim()) return {
|
|
2028
|
+
ok: false,
|
|
2029
|
+
error: `operations[${i}].body is required`,
|
|
2030
|
+
index: i
|
|
2031
|
+
};
|
|
2032
|
+
ops.push({
|
|
2033
|
+
op: "edit",
|
|
2034
|
+
commentId,
|
|
2035
|
+
replyId: replyId || void 0,
|
|
2036
|
+
body
|
|
2037
|
+
});
|
|
2038
|
+
break;
|
|
2039
|
+
}
|
|
2040
|
+
case "delete": {
|
|
2041
|
+
const replyId = typeof item.replyId === "string" ? item.replyId : void 0;
|
|
2042
|
+
ops.push({
|
|
2043
|
+
op: "delete",
|
|
2044
|
+
commentId,
|
|
2045
|
+
replyId
|
|
2046
|
+
});
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
case "resolve":
|
|
2050
|
+
case "unresolve":
|
|
2051
|
+
ops.push({
|
|
2052
|
+
op,
|
|
2053
|
+
commentId
|
|
2054
|
+
});
|
|
2055
|
+
break;
|
|
2056
|
+
default: return {
|
|
2057
|
+
ok: false,
|
|
2058
|
+
error: `operations[${i}].op must be reply|edit|delete|resolve|unresolve`,
|
|
2059
|
+
index: i
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return {
|
|
2064
|
+
ok: true,
|
|
2065
|
+
ops
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
var FileMockupStore = class {
|
|
2069
|
+
dirPath;
|
|
2070
|
+
filePath;
|
|
2071
|
+
constructor(storageDir) {
|
|
2072
|
+
this.dirPath = storageDir ?? getProjectStorageDir();
|
|
2073
|
+
this.filePath = join(this.dirPath, "mockups.json");
|
|
2074
|
+
}
|
|
2075
|
+
async getAll() {
|
|
2076
|
+
try {
|
|
2077
|
+
const data = await readFile(this.filePath, "utf-8");
|
|
2078
|
+
const mockups = JSON.parse(data);
|
|
2079
|
+
let anyHealed = false;
|
|
2080
|
+
for (const m of mockups) {
|
|
2081
|
+
backfillMockup(m);
|
|
2082
|
+
if (!m.sourcePath) {
|
|
2083
|
+
m.sourcePath = mockupSourceDir(this.dirPath, m.id);
|
|
2084
|
+
anyHealed = true;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (anyHealed) try {
|
|
2088
|
+
await this.save(mockups);
|
|
2089
|
+
} catch {}
|
|
2090
|
+
return mockups;
|
|
2091
|
+
} catch {
|
|
2092
|
+
return [];
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
async get(id) {
|
|
2096
|
+
return (await this.getAll()).find((m) => m.id === id) ?? null;
|
|
2097
|
+
}
|
|
2098
|
+
async getVersion(id, version) {
|
|
2099
|
+
const mockup = await this.get(id);
|
|
2100
|
+
if (!mockup) return null;
|
|
2101
|
+
return mockup.versions.find((v) => v.version === version) ?? null;
|
|
2102
|
+
}
|
|
2103
|
+
async save(mockups) {
|
|
2104
|
+
try {
|
|
2105
|
+
await mkdir(this.dirPath, { recursive: true });
|
|
2106
|
+
try {
|
|
2107
|
+
const repoRoot = getRepoRoot();
|
|
2108
|
+
await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
|
|
2109
|
+
} catch {}
|
|
2110
|
+
await writeFile(this.filePath, JSON.stringify(mockups, null, 2), "utf-8");
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
console.error("Failed to save mockups to file:", err);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
async writeSourceMirror(mockup) {
|
|
2116
|
+
try {
|
|
2117
|
+
const dir = mockupSourceDir(this.dirPath, mockup.id);
|
|
2118
|
+
await mkdir(dir, { recursive: true });
|
|
2119
|
+
for (const screen of mockup.screens) await writeFile(join(dir, `${screen.id}.html`), screen.html, "utf-8");
|
|
2120
|
+
mockup.sourcePath = dir;
|
|
2121
|
+
} catch (err) {
|
|
2122
|
+
console.error("Failed to write mockup source mirror:", err);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
async upsert(input) {
|
|
2126
|
+
const mockups = await this.getAll();
|
|
2127
|
+
const mockup = applyUpsert(mockups, input, Date.now());
|
|
2128
|
+
backfillMockup(mockup);
|
|
2129
|
+
await this.writeSourceMirror(mockup);
|
|
2130
|
+
await this.save(mockups);
|
|
2131
|
+
return mockup;
|
|
2132
|
+
}
|
|
2133
|
+
async update(id, fields) {
|
|
2134
|
+
const mockups = await this.getAll();
|
|
2135
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2136
|
+
if (!mockup) return null;
|
|
2137
|
+
if (fields.title !== void 0) mockup.title = fields.title;
|
|
2138
|
+
if (fields.screens !== void 0) mockup.screens = snapshotScreens(fields.screens);
|
|
2139
|
+
if (fields.source !== void 0) mockup.source = fields.source;
|
|
2140
|
+
if (fields.model !== void 0) mockup.model = fields.model;
|
|
2141
|
+
mockup.updatedAt = Date.now();
|
|
2142
|
+
syncCurrentVersion(mockup);
|
|
2143
|
+
backfillMockup(mockup);
|
|
2144
|
+
if (fields.screens !== void 0) await this.writeSourceMirror(mockup);
|
|
2145
|
+
await this.save(mockups);
|
|
2146
|
+
return mockup;
|
|
2147
|
+
}
|
|
2148
|
+
async remove(id) {
|
|
2149
|
+
const mockups = await this.getAll();
|
|
2150
|
+
const idx = mockups.findIndex((m) => m.id === id);
|
|
2151
|
+
if (idx === -1) return false;
|
|
2152
|
+
mockups.splice(idx, 1);
|
|
2153
|
+
await this.save(mockups);
|
|
2154
|
+
try {
|
|
2155
|
+
await rm(mockupSourceDir(this.dirPath, id), {
|
|
2156
|
+
recursive: true,
|
|
2157
|
+
force: true
|
|
2158
|
+
});
|
|
2159
|
+
} catch {}
|
|
2160
|
+
return true;
|
|
2161
|
+
}
|
|
2162
|
+
async setDecision(id, decision, decisionComment) {
|
|
2163
|
+
const mockups = await this.getAll();
|
|
2164
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2165
|
+
if (!mockup) return null;
|
|
2166
|
+
mockup.decision = decision;
|
|
2167
|
+
mockup.decisionComment = decisionComment?.trim() || void 0;
|
|
2168
|
+
mockup.decidedAt = Date.now();
|
|
2169
|
+
mockup.updatedAt = mockup.decidedAt;
|
|
2170
|
+
await this.save(mockups);
|
|
2171
|
+
return mockup;
|
|
2172
|
+
}
|
|
2173
|
+
async mutate(mockupId, fn) {
|
|
2174
|
+
const mockups = await this.getAll();
|
|
2175
|
+
const mockup = mockups.find((m) => m.id === mockupId);
|
|
2176
|
+
if (!mockup) return null;
|
|
2177
|
+
if (!mockup.comments) mockup.comments = [];
|
|
2178
|
+
if (!fn(mockup)) return null;
|
|
2179
|
+
backfillMockup(mockup);
|
|
2180
|
+
await this.save(mockups);
|
|
2181
|
+
return mockup;
|
|
2182
|
+
}
|
|
2183
|
+
async addComment(mockupId, comment) {
|
|
2184
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2185
|
+
if (typeof comment.createdAtMockupVersion !== "number") comment.createdAtMockupVersion = mockup.version ?? 1;
|
|
2186
|
+
mockup.comments.push(comment);
|
|
2187
|
+
return true;
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
async updateComment(mockupId, commentId, fields) {
|
|
2191
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2192
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2193
|
+
if (!c) return false;
|
|
2194
|
+
if (fields.body !== void 0) c.body = fields.body;
|
|
2195
|
+
if (fields.status !== void 0) c.status = fields.status;
|
|
2196
|
+
return true;
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
async removeComment(mockupId, commentId) {
|
|
2200
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2201
|
+
const idx = mockup.comments.findIndex((x) => x.id === commentId);
|
|
2202
|
+
if (idx === -1) return false;
|
|
2203
|
+
mockup.comments.splice(idx, 1);
|
|
2204
|
+
return true;
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
async addReply(mockupId, commentId, reply) {
|
|
2208
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2209
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2210
|
+
if (!c) return false;
|
|
2211
|
+
if (typeof c.createdAtMockupVersion === "number" && typeof reply.createdAtPlanVersion !== "number") reply.createdAtPlanVersion = c.createdAtMockupVersion;
|
|
2212
|
+
if (!c.replies) c.replies = [];
|
|
2213
|
+
c.replies.push(reply);
|
|
2214
|
+
return true;
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
async removeReply(mockupId, commentId, replyId) {
|
|
2218
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2219
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2220
|
+
if (!c) return false;
|
|
2221
|
+
const idx = c.replies.findIndex((r) => r.id === replyId);
|
|
2222
|
+
if (idx === -1) return false;
|
|
2223
|
+
c.replies.splice(idx, 1);
|
|
2224
|
+
return true;
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
async updateReply(mockupId, commentId, replyId, body) {
|
|
2228
|
+
return this.mutate(mockupId, (mockup) => {
|
|
2229
|
+
const c = mockup.comments.find((x) => x.id === commentId);
|
|
2230
|
+
if (!c) return false;
|
|
2231
|
+
const reply = c.replies.find((r) => r.id === replyId);
|
|
2232
|
+
if (!reply) return false;
|
|
2233
|
+
reply.body = body;
|
|
2234
|
+
return true;
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
async upsertScreen(id, screen, opts) {
|
|
2238
|
+
const mockups = await this.getAll();
|
|
2239
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2240
|
+
if (!mockup) return {
|
|
2241
|
+
mockup: null,
|
|
2242
|
+
error: "Mockup not found"
|
|
2243
|
+
};
|
|
2244
|
+
const vm = versionMismatch(opts?.expectedVersion, mockup.version);
|
|
2245
|
+
if (vm) return {
|
|
2246
|
+
mockup: null,
|
|
2247
|
+
versionMismatch: vm
|
|
2248
|
+
};
|
|
2249
|
+
applyScreenUpsert(mockup, screen, Date.now());
|
|
2250
|
+
await this.writeSourceMirror(mockup);
|
|
2251
|
+
await this.save(mockups);
|
|
2252
|
+
return { mockup };
|
|
2253
|
+
}
|
|
2254
|
+
async removeScreen(id, screenId, opts) {
|
|
2255
|
+
const mockups = await this.getAll();
|
|
2256
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2257
|
+
if (!mockup) return {
|
|
2258
|
+
mockup: null,
|
|
2259
|
+
error: "Mockup not found"
|
|
2260
|
+
};
|
|
2261
|
+
const vm = versionMismatch(opts?.expectedVersion, mockup.version);
|
|
2262
|
+
if (vm) return {
|
|
2263
|
+
mockup: null,
|
|
2264
|
+
versionMismatch: vm
|
|
2265
|
+
};
|
|
2266
|
+
const idx = mockup.screens.findIndex((s) => s.id === screenId);
|
|
2267
|
+
if (idx === -1) return {
|
|
2268
|
+
mockup: null,
|
|
2269
|
+
error: `Screen ${screenId} not found`
|
|
2270
|
+
};
|
|
2271
|
+
if (mockup.screens.length <= 1) return {
|
|
2272
|
+
mockup: null,
|
|
2273
|
+
error: "Cannot remove the last screen"
|
|
2274
|
+
};
|
|
2275
|
+
mockup.screens.splice(idx, 1);
|
|
2276
|
+
bumpMockupVersion(mockup, Date.now());
|
|
2277
|
+
try {
|
|
2278
|
+
await unlink(join(mockupSourceDir(this.dirPath, id), `${screenId}.html`));
|
|
2279
|
+
} catch {}
|
|
2280
|
+
await this.save(mockups);
|
|
2281
|
+
return { mockup };
|
|
2282
|
+
}
|
|
2283
|
+
async patchScreen(id, screenId, patch, opts) {
|
|
2284
|
+
const mockups = await this.getAll();
|
|
2285
|
+
const mockup = mockups.find((m) => m.id === id);
|
|
2286
|
+
if (!mockup) return {
|
|
2287
|
+
mockup: null,
|
|
2288
|
+
error: "Mockup not found"
|
|
2289
|
+
};
|
|
2290
|
+
const vm = versionMismatch(opts?.expectedVersion, mockup.version);
|
|
2291
|
+
if (vm) return {
|
|
2292
|
+
mockup: null,
|
|
2293
|
+
versionMismatch: vm
|
|
2294
|
+
};
|
|
2295
|
+
const screen = mockup.screens.find((s) => s.id === screenId);
|
|
2296
|
+
if (!screen) return {
|
|
2297
|
+
mockup: null,
|
|
2298
|
+
error: `Screen ${screenId} not found`
|
|
2299
|
+
};
|
|
2300
|
+
if (!patch.expectedText) return {
|
|
2301
|
+
mockup: null,
|
|
2302
|
+
error: "expectedText is required"
|
|
2303
|
+
};
|
|
2304
|
+
if (patch.replacement === void 0) return {
|
|
2305
|
+
mockup: null,
|
|
2306
|
+
error: "replacement is required"
|
|
2307
|
+
};
|
|
2308
|
+
const occurrences = applyScreenPatch(screen, patch);
|
|
2309
|
+
if (occurrences === 0) return {
|
|
2310
|
+
mockup: null,
|
|
2311
|
+
error: "Exact text not found"
|
|
2312
|
+
};
|
|
2313
|
+
bumpMockupVersion(mockup, Date.now());
|
|
2314
|
+
await this.writeSourceMirror(mockup);
|
|
2315
|
+
await this.save(mockups);
|
|
2316
|
+
return {
|
|
2317
|
+
mockup,
|
|
2318
|
+
occurrences
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
async applyThreadBatch(mockupId, ops) {
|
|
2322
|
+
const mockups = await this.getAll();
|
|
2323
|
+
const mockup = mockups.find((m) => m.id === mockupId);
|
|
2324
|
+
if (!mockup) return {
|
|
2325
|
+
mockup: null,
|
|
2326
|
+
error: "Mockup not found",
|
|
2327
|
+
results: []
|
|
2328
|
+
};
|
|
2329
|
+
backfillMockup(mockup);
|
|
2330
|
+
const error = validateThreadBatch(mockup, ops);
|
|
2331
|
+
if (error) return {
|
|
2332
|
+
mockup: null,
|
|
2333
|
+
error,
|
|
2334
|
+
results: []
|
|
2335
|
+
};
|
|
2336
|
+
const results = applyThreadOps(mockup, ops, Date.now());
|
|
2337
|
+
await this.save(mockups);
|
|
2338
|
+
return {
|
|
2339
|
+
mockup,
|
|
2340
|
+
results
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
2344
|
+
/** Shared by HTTP/CLI so submit shapes stay one path. */
|
|
2345
|
+
function screensFromSubmitBody(body) {
|
|
2346
|
+
return normalizeSubmitScreens({
|
|
2347
|
+
html: typeof body.html === "string" ? body.html : void 0,
|
|
2348
|
+
screens: Array.isArray(body.screens) ? body.screens.map((item) => {
|
|
2349
|
+
const rec = item;
|
|
2350
|
+
return {
|
|
2351
|
+
id: typeof rec.id === "string" ? rec.id : void 0,
|
|
2352
|
+
label: typeof rec.label === "string" ? rec.label : void 0,
|
|
2353
|
+
html: typeof rec.html === "string" ? rec.html : ""
|
|
2354
|
+
};
|
|
2355
|
+
}) : void 0
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
//#endregion
|
|
2359
|
+
//#region src/lib/mockup-review-session.ts
|
|
2360
|
+
var MockupReviewSession = class {
|
|
2361
|
+
round = 0;
|
|
2362
|
+
lastDecidedAt = null;
|
|
2363
|
+
lastPayload = null;
|
|
2364
|
+
waiters = /* @__PURE__ */ new Set();
|
|
2365
|
+
onStatusChange;
|
|
2366
|
+
constructor(onStatusChange) {
|
|
2367
|
+
this.onStatusChange = onStatusChange;
|
|
2368
|
+
}
|
|
2369
|
+
await(opts) {
|
|
2370
|
+
const { sinceRound, timeoutMs, signal } = opts;
|
|
2371
|
+
if (sinceRound !== void 0 && sinceRound < this.round && this.lastPayload) return Promise.resolve({
|
|
2372
|
+
status: "released",
|
|
2373
|
+
payload: this.lastPayload
|
|
2374
|
+
});
|
|
2375
|
+
if (signal?.aborted) return Promise.resolve({
|
|
2376
|
+
status: "keep-waiting",
|
|
2377
|
+
round: this.round
|
|
2378
|
+
});
|
|
2379
|
+
return new Promise((resolve) => {
|
|
2380
|
+
const waiter = {
|
|
2381
|
+
resolve,
|
|
2382
|
+
timer: setTimeout(() => {
|
|
2383
|
+
this.removeWaiter(waiter);
|
|
2384
|
+
resolve({
|
|
2385
|
+
status: "keep-waiting",
|
|
2386
|
+
round: this.round
|
|
2387
|
+
});
|
|
2388
|
+
}, timeoutMs),
|
|
2389
|
+
cleanup: () => {}
|
|
2390
|
+
};
|
|
2391
|
+
if (signal) {
|
|
2392
|
+
const onAbort = () => {
|
|
2393
|
+
this.removeWaiter(waiter);
|
|
2394
|
+
resolve({
|
|
2395
|
+
status: "keep-waiting",
|
|
2396
|
+
round: this.round
|
|
2397
|
+
});
|
|
2398
|
+
};
|
|
2399
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2400
|
+
waiter.cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
2401
|
+
}
|
|
2402
|
+
this.waiters.add(waiter);
|
|
2403
|
+
this.emitStatus();
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
decide(input) {
|
|
2407
|
+
this.round += 1;
|
|
2408
|
+
this.lastDecidedAt = Date.now();
|
|
2409
|
+
const payload = {
|
|
2410
|
+
round: this.round,
|
|
2411
|
+
sentAt: this.lastDecidedAt,
|
|
2412
|
+
mockupId: input.mockup.id,
|
|
2413
|
+
decision: input.decision,
|
|
2414
|
+
decisionComment: input.decisionComment,
|
|
2415
|
+
reviewXml: input.reviewXml,
|
|
2416
|
+
openCommentCount: input.openCommentCount,
|
|
2417
|
+
mockup: input.mockup,
|
|
2418
|
+
mode: input.mode ?? "standard"
|
|
2419
|
+
};
|
|
2420
|
+
this.lastPayload = payload;
|
|
2421
|
+
for (const waiter of [...this.waiters]) {
|
|
2422
|
+
this.removeWaiter(waiter);
|
|
2423
|
+
waiter.resolve({
|
|
2424
|
+
status: "released",
|
|
2425
|
+
payload
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
this.emitStatus();
|
|
2429
|
+
return payload;
|
|
2430
|
+
}
|
|
2431
|
+
snapshot() {
|
|
2432
|
+
return {
|
|
2433
|
+
round: this.round,
|
|
2434
|
+
waiters: this.waiters.size,
|
|
2435
|
+
lastDecidedAt: this.lastDecidedAt
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
removeWaiter(waiter) {
|
|
2439
|
+
clearTimeout(waiter.timer);
|
|
2440
|
+
waiter.cleanup();
|
|
2441
|
+
this.waiters.delete(waiter);
|
|
2442
|
+
this.emitStatus();
|
|
2443
|
+
}
|
|
2444
|
+
emitStatus() {
|
|
2445
|
+
this.onStatusChange?.(this.snapshot());
|
|
1714
2446
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
+
const passive = opts.passive === true;
|
|
2463
|
+
return `(() => {
|
|
2464
|
+
if (window.__diffingMockupProbe) return;
|
|
2465
|
+
window.__diffingMockupProbe = true;
|
|
2466
|
+
const NONCE = ${JSON.stringify(nonce)};
|
|
2467
|
+
const VIEWPORT = ${JSON.stringify(viewport)};
|
|
2468
|
+
const PASSIVE = ${passive};
|
|
2469
|
+
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';
|
|
2470
|
+
let tool = 'block';
|
|
2471
|
+
let drag = null;
|
|
2472
|
+
let shield = null;
|
|
2473
|
+
|
|
2474
|
+
function post(event, extra) {
|
|
2475
|
+
try {
|
|
2476
|
+
parent.postMessage(Object.assign({ type: 'diffing-mockup', event: event, nonce: NONCE, viewport: VIEWPORT }, extra || {}), '*');
|
|
2477
|
+
} catch (_) {}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function pctRect(el) {
|
|
2481
|
+
const r = el.getBoundingClientRect();
|
|
2482
|
+
const w = Math.max(document.documentElement.clientWidth, 1);
|
|
2483
|
+
const h = Math.max(document.documentElement.clientHeight, 1);
|
|
2484
|
+
return {
|
|
2485
|
+
x: (r.left / w) * 100,
|
|
2486
|
+
y: (r.top / h) * 100,
|
|
2487
|
+
w: (r.width / w) * 100,
|
|
2488
|
+
h: (r.height / h) * 100,
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function pointPct(ev) {
|
|
2493
|
+
const w = Math.max(document.documentElement.clientWidth, 1);
|
|
2494
|
+
const h = Math.max(document.documentElement.clientHeight, 1);
|
|
2495
|
+
return { x: (ev.clientX / w) * 100, y: (ev.clientY / h) * 100 };
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function cssEscape(s) {
|
|
2499
|
+
if (window.CSS && CSS.escape) return CSS.escape(s);
|
|
2500
|
+
return String(s).replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
function shortSelector(el) {
|
|
2504
|
+
if (!el || el === document.documentElement || el === document.body) return 'body';
|
|
2505
|
+
const parts = [];
|
|
2506
|
+
let node = el;
|
|
2507
|
+
for (let i = 0; i < 6 && node && node !== document.body; i++) {
|
|
2508
|
+
if (node.getAttribute && node.getAttribute('data-diffing')) {
|
|
2509
|
+
parts.unshift('[data-diffing="' + node.getAttribute('data-diffing') + '"]');
|
|
2510
|
+
break;
|
|
2511
|
+
}
|
|
2512
|
+
if (node.id) { parts.unshift('#' + cssEscape(node.id)); break; }
|
|
2513
|
+
const tag = node.tagName.toLowerCase();
|
|
2514
|
+
const parent = node.parentElement;
|
|
2515
|
+
if (!parent) { parts.unshift(tag); break; }
|
|
2516
|
+
const cls = (node.className && typeof node.className === 'string')
|
|
2517
|
+
? node.className.trim().split(/\\s+/).filter(Boolean)[0]
|
|
2518
|
+
: '';
|
|
2519
|
+
const same = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
|
|
2520
|
+
const idx = same.indexOf(node) + 1;
|
|
2521
|
+
const bit = cls ? tag + '.' + cssEscape(cls) : (same.length > 1 ? tag + ':nth-of-type(' + idx + ')' : tag);
|
|
2522
|
+
parts.unshift(bit);
|
|
2523
|
+
node = parent;
|
|
2524
|
+
}
|
|
2525
|
+
return parts.join('>');
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// Stable selector for a block measured from its nearest named section root:
|
|
2529
|
+
// '[data-diffing="hero"] > div.row > button.pay:nth-of-type(2)'. Nodes above
|
|
2530
|
+
// the section never appear, so edits elsewhere in the document cannot change it.
|
|
2531
|
+
function sectionRelativeSelector(el, section) {
|
|
2532
|
+
const name = section.getAttribute('data-diffing');
|
|
2533
|
+
const root = '[data-diffing="' + name + '"]';
|
|
2534
|
+
if (!el || el === section) return root;
|
|
2535
|
+
const parts = [];
|
|
2536
|
+
let node = el;
|
|
2537
|
+
while (node && node !== section) {
|
|
2538
|
+
const tag = node.tagName.toLowerCase();
|
|
2539
|
+
const parent = node.parentElement;
|
|
2540
|
+
let bit = tag;
|
|
2541
|
+
if (node.id) {
|
|
2542
|
+
bit += '#' + cssEscape(node.id);
|
|
2543
|
+
} else {
|
|
2544
|
+
const cls = (node.className && typeof node.className === 'string')
|
|
2545
|
+
? node.className.trim().split(/\\s+/).filter(Boolean)[0]
|
|
2546
|
+
: '';
|
|
2547
|
+
if (cls) bit += '.' + cssEscape(cls);
|
|
2548
|
+
if (parent) {
|
|
2549
|
+
const same = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
|
|
2550
|
+
if (same.length > 1) bit += ':nth-of-type(' + (same.indexOf(node) + 1) + ')';
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
parts.unshift(bit);
|
|
2554
|
+
node = parent;
|
|
2555
|
+
}
|
|
2556
|
+
return root + ' > ' + parts.join(' > ');
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// Stable digest of a DOM path. Purely structural — class renames on the
|
|
2560
|
+
// block itself change it, but viewport/layout shifts do not.
|
|
2561
|
+
function fingerprintOf(path) {
|
|
2562
|
+
let h = 5381;
|
|
2563
|
+
for (let i = 0; i < path.length; i++) {
|
|
2564
|
+
h = ((h << 5) + h + path.charCodeAt(i)) | 0;
|
|
2565
|
+
}
|
|
2566
|
+
return 'fp:' + (h >>> 0).toString(16);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function clipHtml(el, max) {
|
|
2570
|
+
if (!el || !el.cloneNode) return undefined;
|
|
2571
|
+
const clone = el.cloneNode(true);
|
|
2572
|
+
if (clone.querySelectorAll) {
|
|
2573
|
+
clone.querySelectorAll('script').forEach(function (s) { s.remove(); });
|
|
2574
|
+
}
|
|
2575
|
+
let html = clone.outerHTML || '';
|
|
2576
|
+
if (html.length > max) html = html.slice(0, max) + '\u2026';
|
|
2577
|
+
return html;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
function isBlock(el) {
|
|
2581
|
+
if (!el || el === document.body || el === document.documentElement) return false;
|
|
2582
|
+
if (el.hasAttribute('data-diffing')) return true;
|
|
2583
|
+
if (el.id || (el.className && String(el.className).trim())) return true;
|
|
2584
|
+
if (el.getAttribute('role')) return true;
|
|
2585
|
+
return BLOCK.split(',').includes(el.tagName);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function deepestBlock(start) {
|
|
2589
|
+
let el = start;
|
|
2590
|
+
while (el && el !== document.body && !isBlock(el)) el = el.parentElement;
|
|
2591
|
+
return el && el !== document.body ? el : start;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
function payloadFor(el, ev, kindOverride) {
|
|
2595
|
+
// Nearest named [data-diffing] ancestor only — sections never nest deeper.
|
|
2596
|
+
const section = el && el.closest ? el.closest('[data-diffing]') : null;
|
|
2597
|
+
const kind = kindOverride || (tool === 'pin' ? 'point' : tool === 'section' && section ? 'section' : 'block');
|
|
2598
|
+
const hitEl = deepestBlock(el);
|
|
2599
|
+
const outlineEl = kind === 'section' && section ? section : hitEl;
|
|
2600
|
+
const pt = ev ? pointPct(ev) : { x: 0, y: 0 };
|
|
2601
|
+
let sectionX, sectionY;
|
|
2602
|
+
if (section && ev) {
|
|
2603
|
+
const sr = section.getBoundingClientRect();
|
|
2604
|
+
if (sr.width > 0 && sr.height > 0) {
|
|
2605
|
+
sectionX = Math.round(((ev.clientX - sr.left) / sr.width) * 1000) / 10;
|
|
2606
|
+
sectionY = Math.round(((ev.clientY - sr.top) / sr.height) * 1000) / 10;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
const base = {
|
|
2610
|
+
kind: kind,
|
|
2611
|
+
target: section ? section.getAttribute('data-diffing') : undefined,
|
|
2612
|
+
x: Math.round(pt.x * 10) / 10,
|
|
2613
|
+
y: Math.round(pt.y * 10) / 10,
|
|
2614
|
+
sectionX: sectionX,
|
|
2615
|
+
sectionY: sectionY,
|
|
2616
|
+
rect: outlineEl ? pctRect(outlineEl) : undefined,
|
|
2617
|
+
};
|
|
2618
|
+
if (kind === 'section' && section) {
|
|
2619
|
+
const sel = '[data-diffing="' + section.getAttribute('data-diffing') + '"]';
|
|
2620
|
+
return Object.assign(base, {
|
|
2621
|
+
selector: sel,
|
|
2622
|
+
fingerprint: fingerprintOf(sel),
|
|
2623
|
+
html: clipHtml(section, 2000),
|
|
2624
|
+
contextHtml: clipHtml(section, 4000),
|
|
2625
|
+
snapshot: String(section.innerText || '').trim().slice(0, 240),
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
if (kind === 'point') {
|
|
2629
|
+
// Pin: coordinate + nearby context. The coordinates are the locator;
|
|
2630
|
+
// no deep selector/fingerprint is recorded.
|
|
2631
|
+
return Object.assign(base, {
|
|
2632
|
+
html: clipHtml(hitEl, 2000),
|
|
2633
|
+
contextHtml: clipHtml(section || (hitEl && hitEl.parentElement) || hitEl, 4000),
|
|
2634
|
+
snapshot: hitEl ? String(hitEl.innerText || '').trim().slice(0, 240) : undefined,
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
// block
|
|
2638
|
+
if (section) {
|
|
2639
|
+
const sel = sectionRelativeSelector(hitEl, section);
|
|
2640
|
+
return Object.assign(base, {
|
|
2641
|
+
selector: sel,
|
|
2642
|
+
fingerprint: fingerprintOf(sel),
|
|
2643
|
+
html: clipHtml(hitEl, 2000),
|
|
2644
|
+
contextHtml: clipHtml(section, 4000),
|
|
2645
|
+
snapshot: hitEl ? String(hitEl.innerText || '').trim().slice(0, 240) : undefined,
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
const sel = hitEl ? shortSelector(hitEl) : undefined;
|
|
2649
|
+
return Object.assign(base, {
|
|
2650
|
+
selector: sel,
|
|
2651
|
+
fingerprint: sel ? fingerprintOf(sel) : undefined,
|
|
2652
|
+
html: clipHtml(hitEl, 2000),
|
|
2653
|
+
contextHtml: clipHtml(hitEl && hitEl.parentElement, 4000),
|
|
2654
|
+
snapshot: hitEl ? String(hitEl.innerText || '').trim().slice(0, 240) : undefined,
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
function sections() {
|
|
2659
|
+
return Array.from(document.querySelectorAll('[data-diffing]')).map((el) => ({
|
|
2660
|
+
name: el.getAttribute('data-diffing'),
|
|
2661
|
+
rect: pctRect(el),
|
|
2662
|
+
}));
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
window.addEventListener('message', (ev) => {
|
|
2666
|
+
const d = ev.data;
|
|
2667
|
+
if (!d || d.type !== 'diffing-mockup') return;
|
|
2668
|
+
if (d.event === 'set-tool' && d.tool) tool = d.tool;
|
|
2669
|
+
if (d.event === 'check-anchors' && Array.isArray(d.anchors)) {
|
|
2670
|
+
const results = d.anchors.map(function (a) {
|
|
2671
|
+
let present = true;
|
|
2672
|
+
if (a && a.selector) {
|
|
2673
|
+
try { present = !!document.querySelector(a.selector); }
|
|
2674
|
+
catch (_) { present = false; }
|
|
2675
|
+
}
|
|
2676
|
+
return { id: a ? a.id : null, present: present };
|
|
2677
|
+
});
|
|
2678
|
+
post('anchors', { results: results });
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
function targetAt(ev) {
|
|
2684
|
+
if (!shield) return document.body;
|
|
2685
|
+
shield.style.pointerEvents = 'none';
|
|
2686
|
+
try {
|
|
2687
|
+
return document.elementFromPoint(ev.clientX, ev.clientY) || document.body;
|
|
2688
|
+
} finally {
|
|
2689
|
+
shield.style.pointerEvents = 'auto';
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function installShield() {
|
|
2694
|
+
if (shield || !document.body) return;
|
|
2695
|
+
const stabilityStyle = document.createElement('style');
|
|
2696
|
+
stabilityStyle.setAttribute('data-diffing-stability', '1');
|
|
2697
|
+
stabilityStyle.textContent = 'html { scrollbar-gutter: stable; }';
|
|
2698
|
+
(document.head || document.documentElement).appendChild(stabilityStyle);
|
|
2699
|
+
|
|
2700
|
+
shield = document.createElement('div');
|
|
2701
|
+
shield.setAttribute('data-diffing-inspection-shield', '1');
|
|
2702
|
+
shield.setAttribute('aria-hidden', 'true');
|
|
2703
|
+
shield.style.cssText = 'position:fixed;inset:0;background:transparent;z-index:2147483647;pointer-events:auto;cursor:crosshair;';
|
|
2704
|
+
document.body.appendChild(shield);
|
|
2705
|
+
|
|
2706
|
+
shield.addEventListener('mousemove', (ev) => {
|
|
2707
|
+
const target = targetAt(ev);
|
|
2708
|
+
if (tool === 'pin') {
|
|
2709
|
+
post('hover', Object.assign(payloadFor(target, ev, 'point'), { kind: 'point' }));
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
if (tool === 'section') {
|
|
2713
|
+
const section = target && target.closest ? target.closest('[data-diffing]') : null;
|
|
2714
|
+
if (!section) {
|
|
2715
|
+
post('hover-miss');
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
post('hover', payloadFor(section, ev, 'section'));
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
post('hover', payloadFor(target, ev));
|
|
2722
|
+
}, true);
|
|
2723
|
+
|
|
2724
|
+
shield.addEventListener('mousedown', (ev) => {
|
|
2725
|
+
if (ev.button !== 0) return;
|
|
2726
|
+
drag = { x: ev.clientX, y: ev.clientY, target: targetAt(ev) };
|
|
2727
|
+
ev.preventDefault();
|
|
2728
|
+
}, true);
|
|
2729
|
+
|
|
2730
|
+
shield.addEventListener('mouseup', (ev) => {
|
|
2731
|
+
if (ev.button !== 0 || !drag) return;
|
|
2732
|
+
const target = targetAt(ev);
|
|
2733
|
+
const dx = ev.clientX - drag.x;
|
|
2734
|
+
const dy = ev.clientY - drag.y;
|
|
2735
|
+
const dist = Math.hypot(dx, dy);
|
|
2736
|
+
drag = null;
|
|
2737
|
+
if (tool === 'pin') {
|
|
2738
|
+
post('click', payloadFor(target, ev, 'point'));
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (tool === 'section') {
|
|
2742
|
+
const section = target && target.closest ? target.closest('[data-diffing]') : null;
|
|
2743
|
+
if (!section) {
|
|
2744
|
+
post('section-miss');
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
post(dist >= 8 ? 'dragend' : 'click', payloadFor(section, ev, 'section'));
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
post(dist >= 8 ? 'dragend' : 'click', payloadFor(target, ev));
|
|
2751
|
+
}, true);
|
|
2752
|
+
|
|
2753
|
+
shield.addEventListener('click', (ev) => {
|
|
2754
|
+
ev.preventDefault();
|
|
2755
|
+
ev.stopPropagation();
|
|
2756
|
+
}, true);
|
|
2757
|
+
post('ready', { sections: sections() });
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function announce() {
|
|
2761
|
+
post('ready', { sections: sections() });
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
const boot = PASSIVE ? announce : installShield;
|
|
2765
|
+
if (document.readyState === 'loading') {
|
|
2766
|
+
document.addEventListener('DOMContentLoaded', boot, { once: true });
|
|
2767
|
+
} else {
|
|
2768
|
+
boot();
|
|
2769
|
+
}
|
|
2770
|
+
})();`;
|
|
2771
|
+
}
|
|
2772
|
+
buildMockupProbeScript();
|
|
2773
|
+
function injectMockupProbe(html, opts = {}) {
|
|
2774
|
+
const script = `<script ${MOCKUP_PROBE_FLAG}="1">${buildMockupProbeScript(opts)}<\/script>`;
|
|
2775
|
+
const lower = html.toLowerCase();
|
|
2776
|
+
const headIdx = lower.indexOf("<head");
|
|
2777
|
+
if (headIdx !== -1) {
|
|
2778
|
+
const close = html.indexOf(">", headIdx);
|
|
2779
|
+
if (close !== -1) return html.slice(0, close + 1) + script + html.slice(close + 1);
|
|
1724
2780
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
const idx = c.replies.findIndex((r) => r.id === replyId);
|
|
1730
|
-
if (idx === -1) return false;
|
|
1731
|
-
c.replies.splice(idx, 1);
|
|
1732
|
-
return true;
|
|
1733
|
-
});
|
|
2781
|
+
const htmlIdx = lower.indexOf("<html");
|
|
2782
|
+
if (htmlIdx !== -1) {
|
|
2783
|
+
const close = html.indexOf(">", htmlIdx);
|
|
2784
|
+
if (close !== -1) return html.slice(0, close + 1) + script + html.slice(close + 1);
|
|
1734
2785
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2786
|
+
return script + html;
|
|
2787
|
+
}
|
|
2788
|
+
//#endregion
|
|
2789
|
+
//#region src/lib/mockup-lint.ts
|
|
2790
|
+
const RULES$1 = [
|
|
2791
|
+
{
|
|
2792
|
+
label: "tabs",
|
|
2793
|
+
re: /role\s*=\s*["']tab(list|panel)?["']/i
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2796
|
+
label: "tabs",
|
|
2797
|
+
re: /aria-selected\s*=/i
|
|
2798
|
+
},
|
|
2799
|
+
{
|
|
2800
|
+
label: "tabs",
|
|
2801
|
+
re: /data-(?:bs-)?toggle\s*=\s*["'](?:tab|pill)["']/i
|
|
2802
|
+
},
|
|
2803
|
+
{
|
|
2804
|
+
label: "tabs",
|
|
2805
|
+
re: /\bdata-tabs?\s*=/i
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
label: "accordion",
|
|
2809
|
+
re: /<details[\s>]/i
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
label: "accordion",
|
|
2813
|
+
re: /data-(?:bs-)?toggle\s*=\s*["']collapse["']/i
|
|
2814
|
+
},
|
|
2815
|
+
{
|
|
2816
|
+
label: "accordion",
|
|
2817
|
+
re: /class\s*=\s*["'][^"']*\baccordion\b/i
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
label: "modal",
|
|
2821
|
+
re: /role\s*=\s*["'](?:alert)?dialog["']/i
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
label: "modal",
|
|
2825
|
+
re: /data-(?:bs-)?toggle\s*=\s*["']modal["']/i
|
|
2826
|
+
},
|
|
2827
|
+
{
|
|
2828
|
+
label: "modal",
|
|
2829
|
+
re: /class\s*=\s*["'][^"']*\bmodal\b/i
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
label: "dropdown",
|
|
2833
|
+
re: /role\s*=\s*["']menu(?:bar)?["']/i
|
|
2834
|
+
},
|
|
2835
|
+
{
|
|
2836
|
+
label: "dropdown",
|
|
2837
|
+
re: /data-(?:bs-)?toggle\s*=\s*["']dropdown["']/i
|
|
2838
|
+
},
|
|
2839
|
+
{
|
|
2840
|
+
label: "dropdown",
|
|
2841
|
+
re: /class\s*=\s*["'][^"']*\bdropdown\b/i
|
|
2842
|
+
},
|
|
2843
|
+
{
|
|
2844
|
+
label: "toggle",
|
|
2845
|
+
re: /role\s*=\s*["']switch["']/i
|
|
2846
|
+
},
|
|
2847
|
+
{
|
|
2848
|
+
label: "toggle",
|
|
2849
|
+
re: /class\s*=\s*["'][^"']*\b(?:toggle|switch)\b/i
|
|
2850
|
+
}
|
|
2851
|
+
];
|
|
2852
|
+
const ORDERED_LABELS = [
|
|
2853
|
+
"tabs",
|
|
2854
|
+
"accordion",
|
|
2855
|
+
"modal",
|
|
2856
|
+
"dropdown",
|
|
2857
|
+
"toggle"
|
|
2858
|
+
];
|
|
2859
|
+
/** Find the in-page state UI patterns present in one screen's HTML. */
|
|
2860
|
+
function detectInPageState(html) {
|
|
2861
|
+
const found = /* @__PURE__ */ new Set();
|
|
2862
|
+
for (const rule of RULES$1) {
|
|
2863
|
+
if (found.has(rule.label)) continue;
|
|
2864
|
+
if (rule.re.test(html)) found.add(rule.label);
|
|
2865
|
+
}
|
|
2866
|
+
return ORDERED_LABELS.filter((label) => found.has(label));
|
|
2867
|
+
}
|
|
2868
|
+
/** Build submit-time hints for a set of screens (empty when all clean). */
|
|
2869
|
+
function lintMockupScreens(screens) {
|
|
2870
|
+
const hints = [];
|
|
2871
|
+
for (const screen of screens) {
|
|
2872
|
+
const patterns = detectInPageState(screen.html);
|
|
2873
|
+
if (patterns.length === 0) continue;
|
|
2874
|
+
hints.push({
|
|
2875
|
+
screenId: screen.id,
|
|
2876
|
+
patterns,
|
|
2877
|
+
message: `Screen "${screen.id}" contains in-page state UI (${patterns.join(", ")}). Split each state into its own screen instead.`
|
|
1743
2878
|
});
|
|
1744
2879
|
}
|
|
1745
|
-
|
|
2880
|
+
return hints;
|
|
2881
|
+
}
|
|
1746
2882
|
//#endregion
|
|
1747
2883
|
//#region src/lib/state.ts
|
|
1748
2884
|
var FileUiStateStore = class {
|
|
@@ -3199,17 +4335,57 @@ function collectPrAvatarUrls(session) {
|
|
|
3199
4335
|
function isCustomMode(opts) {
|
|
3200
4336
|
return opts.revisions.length > 0 || opts.pathspecs.length > 0 || opts.showMode;
|
|
3201
4337
|
}
|
|
4338
|
+
/**
|
|
4339
|
+
* Compact comment serializer for bounded inspect views. `context` controls how
|
|
4340
|
+
* much anchor data is included: none → metadata only; anchor → locator fields
|
|
4341
|
+
* (target/selector/fingerprint/coords/html/snapshot); source → + contextHtml.
|
|
4342
|
+
*/
|
|
4343
|
+
function serializeMockupCommentSummary(comment, context, opts = {}) {
|
|
4344
|
+
const out = {
|
|
4345
|
+
id: comment.id,
|
|
4346
|
+
screenId: comment.screenId,
|
|
4347
|
+
viewport: commentViewport(comment),
|
|
4348
|
+
kind: comment.kind,
|
|
4349
|
+
status: comment.status,
|
|
4350
|
+
createdAt: comment.createdAt,
|
|
4351
|
+
createdAtMockupVersion: comment.createdAtMockupVersion
|
|
4352
|
+
};
|
|
4353
|
+
out.body = opts.truncateBody && comment.body.length > opts.truncateBody ? comment.body.slice(0, opts.truncateBody) + "…" : comment.body;
|
|
4354
|
+
if (comment.severity && comment.severity !== "none") out.severity = comment.severity;
|
|
4355
|
+
if (comment.threadKind) out.threadKind = comment.threadKind;
|
|
4356
|
+
if (context === "none") return out;
|
|
4357
|
+
if (comment.target) out.target = comment.target;
|
|
4358
|
+
if (comment.selector) out.selector = comment.selector;
|
|
4359
|
+
if (comment.fingerprint) out.fingerprint = comment.fingerprint;
|
|
4360
|
+
if (comment.x !== void 0) out.x = comment.x;
|
|
4361
|
+
if (comment.y !== void 0) out.y = comment.y;
|
|
4362
|
+
if (comment.sectionX !== void 0) out.sectionX = comment.sectionX;
|
|
4363
|
+
if (comment.sectionY !== void 0) out.sectionY = comment.sectionY;
|
|
4364
|
+
if (comment.html) out.html = comment.html;
|
|
4365
|
+
if (comment.snapshot) out.snapshot = comment.snapshot;
|
|
4366
|
+
out.replies = (comment.replies ?? []).map((r) => ({
|
|
4367
|
+
id: r.id,
|
|
4368
|
+
body: r.body,
|
|
4369
|
+
createdAt: r.createdAt,
|
|
4370
|
+
role: r.role,
|
|
4371
|
+
model: r.model
|
|
4372
|
+
}));
|
|
4373
|
+
if (context === "anchor") return out;
|
|
4374
|
+
if (comment.contextHtml) out.contextHtml = comment.contextHtml;
|
|
4375
|
+
return out;
|
|
4376
|
+
}
|
|
3202
4377
|
function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore, prSessionStore, prMode = false, security = {
|
|
3203
4378
|
bindHost: "127.0.0.1",
|
|
3204
4379
|
authToken: null,
|
|
3205
4380
|
insecureNoAuth: true
|
|
3206
|
-
}) {
|
|
4381
|
+
}, mockupStore) {
|
|
3207
4382
|
const app = new Hono();
|
|
3208
4383
|
app.use("*", createServerAuthMiddleware(security));
|
|
3209
4384
|
let diffOpts = { ...diffOptsInput };
|
|
3210
4385
|
const customMode = isCustomMode(diffOpts);
|
|
3211
4386
|
const store = commentStore ?? new FileCommentStore();
|
|
3212
4387
|
const plans = planStore ?? new FilePlanStore();
|
|
4388
|
+
const mockups = mockupStore ?? new FileMockupStore();
|
|
3213
4389
|
const prStore = prSessionStore ?? new FilePrSessionStore();
|
|
3214
4390
|
const agentDiffCache = new AgentDiffIndexCache();
|
|
3215
4391
|
const uiStateStore = new FileUiStateStore();
|
|
@@ -3233,17 +4409,37 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3233
4409
|
agents: agentsSnapshot()
|
|
3234
4410
|
})));
|
|
3235
4411
|
const planReviewSession = new PlanReviewSession((snapshot) => broadcast("plan-review-status", JSON.stringify(snapshot)));
|
|
4412
|
+
const mockupReviewSession = new MockupReviewSession((snapshot) => broadcast("mockup-review-status", JSON.stringify(snapshot)));
|
|
4413
|
+
const NONCE_TTL_MS = 3600 * 1e3;
|
|
4414
|
+
const mockupNonces = /* @__PURE__ */ new Map();
|
|
4415
|
+
const registerMockupNonce = (nonce, entry) => {
|
|
4416
|
+
const now = Date.now();
|
|
4417
|
+
if (mockupNonces.size > 500) {
|
|
4418
|
+
for (const [key, value] of mockupNonces) if (value.expiresAt < now) mockupNonces.delete(key);
|
|
4419
|
+
}
|
|
4420
|
+
mockupNonces.set(nonce, {
|
|
4421
|
+
...entry,
|
|
4422
|
+
expiresAt: now + NONCE_TTL_MS
|
|
4423
|
+
});
|
|
4424
|
+
};
|
|
4425
|
+
const validateMockupNonce = (nonce, mockupId, screenId) => {
|
|
4426
|
+
const entry = mockupNonces.get(nonce);
|
|
4427
|
+
if (!entry || entry.expiresAt < Date.now()) return "Stale mockup document (nonce expired or unknown) — reload the screen and retry.";
|
|
4428
|
+
if (entry.mockupId !== mockupId || entry.screenId !== screenId) return "Mockup nonce does not match this mockup/screen — reload the screen and retry.";
|
|
4429
|
+
return null;
|
|
4430
|
+
};
|
|
3236
4431
|
let repoRoot;
|
|
3237
4432
|
try {
|
|
3238
4433
|
repoRoot = getRepoRoot();
|
|
3239
4434
|
} catch {
|
|
3240
4435
|
repoRoot = process.cwd();
|
|
3241
4436
|
}
|
|
3242
|
-
if (!commentStore && !planStore) try {
|
|
4437
|
+
if (!commentStore && !planStore && !mockupStore) try {
|
|
3243
4438
|
const storageDir = getProjectStorageDir();
|
|
3244
4439
|
mkdirSync(storageDir, { recursive: true });
|
|
3245
4440
|
let commentsDebounce = null;
|
|
3246
4441
|
let plansDebounce = null;
|
|
4442
|
+
let mockupsDebounce = null;
|
|
3247
4443
|
let prSessionDebounce = null;
|
|
3248
4444
|
watch(storageDir, (_eventType, filename) => {
|
|
3249
4445
|
if (!filename) return;
|
|
@@ -3253,6 +4449,9 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3253
4449
|
} else if (filename.startsWith("plans.json")) {
|
|
3254
4450
|
if (plansDebounce) clearTimeout(plansDebounce);
|
|
3255
4451
|
plansDebounce = setTimeout(() => broadcast("plans", Date.now().toString()), 120);
|
|
4452
|
+
} else if (filename.startsWith("mockups.json")) {
|
|
4453
|
+
if (mockupsDebounce) clearTimeout(mockupsDebounce);
|
|
4454
|
+
mockupsDebounce = setTimeout(() => broadcast("mockups", Date.now().toString()), 120);
|
|
3256
4455
|
} else if (filename.startsWith("pr-session.json")) {
|
|
3257
4456
|
if (prSessionDebounce) clearTimeout(prSessionDebounce);
|
|
3258
4457
|
prSessionDebounce = setTimeout(() => broadcast("pr-session", Date.now().toString()), 120);
|
|
@@ -3263,7 +4462,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3263
4462
|
}
|
|
3264
4463
|
let debounceTimeout = null;
|
|
3265
4464
|
try {
|
|
3266
|
-
watch(repoRoot, { recursive: true }, (
|
|
4465
|
+
watch(repoRoot, { recursive: true }, (_eventType, filename) => {
|
|
3267
4466
|
if (!filename) return;
|
|
3268
4467
|
const parts = filename.split(/[/\\]/);
|
|
3269
4468
|
const isGit = parts.includes(".git");
|
|
@@ -4295,7 +5494,6 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4295
5494
|
const ref = typeof body?.ref === "string" ? body.ref : "";
|
|
4296
5495
|
if (!ref.trim()) return c.json({ error: "ref is required" }, 400);
|
|
4297
5496
|
try {
|
|
4298
|
-
parsePrRef(ref, await detectCwdRepo() ?? void 0);
|
|
4299
5497
|
const session = await buildPrSession(ref);
|
|
4300
5498
|
await prStore.set(session);
|
|
4301
5499
|
return c.json({
|
|
@@ -4477,7 +5675,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4477
5675
|
submittedAt: Date.now(),
|
|
4478
5676
|
submittedReviewId: result.reviewId,
|
|
4479
5677
|
submittedReviewUrl: result.reviewUrl,
|
|
4480
|
-
authSource: result.authSource
|
|
5678
|
+
authSource: result.authSource === "none" ? void 0 : result.authSource
|
|
4481
5679
|
};
|
|
4482
5680
|
await prStore.set(next);
|
|
4483
5681
|
}
|
|
@@ -4763,6 +5961,485 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4763
5961
|
app.get("/api/plan-review/status", (c) => {
|
|
4764
5962
|
return c.json(planReviewSession.snapshot());
|
|
4765
5963
|
});
|
|
5964
|
+
const summarizeMockup = (mockup, includeComments = false) => ({
|
|
5965
|
+
id: mockup.id,
|
|
5966
|
+
title: mockup.title,
|
|
5967
|
+
screens: mockup.screens.map(({ id, label }) => ({
|
|
5968
|
+
id,
|
|
5969
|
+
label
|
|
5970
|
+
})),
|
|
5971
|
+
source: mockup.source,
|
|
5972
|
+
model: mockup.model,
|
|
5973
|
+
createdAt: mockup.createdAt,
|
|
5974
|
+
updatedAt: mockup.updatedAt,
|
|
5975
|
+
version: mockup.version,
|
|
5976
|
+
decision: mockup.decision,
|
|
5977
|
+
decidedAt: mockup.decidedAt,
|
|
5978
|
+
versionCount: mockup.versions.length,
|
|
5979
|
+
commentCounts: {
|
|
5980
|
+
total: mockup.comments.length,
|
|
5981
|
+
open: mockup.comments.filter((comment) => comment.status === "open").length,
|
|
5982
|
+
resolved: mockup.comments.filter((comment) => comment.status === "resolved").length
|
|
5983
|
+
},
|
|
5984
|
+
...includeComments ? { comments: mockup.comments } : {}
|
|
5985
|
+
});
|
|
5986
|
+
app.get("/api/mockups", async (c) => {
|
|
5987
|
+
const all = await mockups.getAll();
|
|
5988
|
+
if (c.req.query("include") === "full") return c.json(all);
|
|
5989
|
+
const includeComments = c.req.query("include") === "comments";
|
|
5990
|
+
return c.json(all.map((mockup) => summarizeMockup(mockup, includeComments)));
|
|
5991
|
+
});
|
|
5992
|
+
app.get("/api/mockups/:id", async (c) => {
|
|
5993
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
5994
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
5995
|
+
return c.json(mockup);
|
|
5996
|
+
});
|
|
5997
|
+
app.get("/api/mockups/:id/versions", async (c) => {
|
|
5998
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
5999
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
6000
|
+
return c.json((mockup.versions ?? []).map((version) => ({
|
|
6001
|
+
version: version.version,
|
|
6002
|
+
title: version.title,
|
|
6003
|
+
source: version.source,
|
|
6004
|
+
model: version.model,
|
|
6005
|
+
createdAt: version.createdAt,
|
|
6006
|
+
screens: version.screens.map(({ id, label }) => ({
|
|
6007
|
+
id,
|
|
6008
|
+
label
|
|
6009
|
+
}))
|
|
6010
|
+
})));
|
|
6011
|
+
});
|
|
6012
|
+
app.get("/api/mockups/:id/versions/:n", async (c) => {
|
|
6013
|
+
const id = c.req.param("id");
|
|
6014
|
+
const n = Number(c.req.param("n"));
|
|
6015
|
+
if (!Number.isFinite(n) || n < 1) return c.json({ error: "version must be a positive integer" }, 400);
|
|
6016
|
+
const mockup = await mockups.get(id);
|
|
6017
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
6018
|
+
const version = (mockup.versions ?? []).find((v) => v.version === n);
|
|
6019
|
+
if (!version) return c.json({ error: `Version ${n} not found` }, 404);
|
|
6020
|
+
return c.json({
|
|
6021
|
+
version,
|
|
6022
|
+
mockup: {
|
|
6023
|
+
id: mockup.id,
|
|
6024
|
+
title: mockup.title,
|
|
6025
|
+
decision: mockup.decision,
|
|
6026
|
+
currentVersion: mockup.version
|
|
6027
|
+
}
|
|
6028
|
+
});
|
|
6029
|
+
});
|
|
6030
|
+
app.get("/api/mockups/:id/screens/:screenId/document", async (c) => {
|
|
6031
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
6032
|
+
if (!mockup) return c.text("Mockup not found", 404);
|
|
6033
|
+
const versionRaw = c.req.query("version");
|
|
6034
|
+
const versionN = versionRaw ? Number(versionRaw) : mockup.version;
|
|
6035
|
+
const snap = versionN === mockup.version ? mockup : (mockup.versions ?? []).find((v) => v.version === versionN);
|
|
6036
|
+
if (!snap) return c.text("Version not found", 404);
|
|
6037
|
+
const screen = snap.screens.find((s) => s.id === c.req.param("screenId"));
|
|
6038
|
+
if (!screen) return c.text("Screen not found", 404);
|
|
6039
|
+
const viewport = normalizeMockupViewport(c.req.query("viewport"));
|
|
6040
|
+
const passive = c.req.query("mode") === "view";
|
|
6041
|
+
const nonce = crypto.randomUUID();
|
|
6042
|
+
registerMockupNonce(nonce, {
|
|
6043
|
+
mockupId: mockup.id,
|
|
6044
|
+
screenId: screen.id,
|
|
6045
|
+
version: versionN,
|
|
6046
|
+
viewport
|
|
6047
|
+
});
|
|
6048
|
+
return c.html(injectMockupProbe(screen.html, {
|
|
6049
|
+
nonce,
|
|
6050
|
+
viewport,
|
|
6051
|
+
passive
|
|
6052
|
+
}), 200, {
|
|
6053
|
+
"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';",
|
|
6054
|
+
"X-Content-Type-Options": "nosniff",
|
|
6055
|
+
"X-Diffing-Mockup-Nonce": nonce,
|
|
6056
|
+
"X-Diffing-Mockup-Viewport": viewport
|
|
6057
|
+
});
|
|
6058
|
+
});
|
|
6059
|
+
app.get("/api/mockups/:id/inspect", async (c) => {
|
|
6060
|
+
const mockup = await mockups.get(c.req.param("id"));
|
|
6061
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
6062
|
+
const view = c.req.query("view") || "summary";
|
|
6063
|
+
if (view !== "summary" && view !== "comments" && view !== "comment" && view !== "screen") return c.json({ error: "view must be summary|comments|comment|screen" }, 400);
|
|
6064
|
+
const status = c.req.query("status");
|
|
6065
|
+
if (status !== void 0 && status !== "open" && status !== "resolved") return c.json({ error: "status must be open or resolved" }, 400);
|
|
6066
|
+
const screenFilter = c.req.query("screen");
|
|
6067
|
+
const viewportRaw = c.req.query("viewport");
|
|
6068
|
+
if (viewportRaw !== void 0 && !isMockupViewport(viewportRaw)) return c.json({ error: "viewport must be desktop|tablet|mobile" }, 400);
|
|
6069
|
+
const versionRaw = c.req.query("version");
|
|
6070
|
+
if (versionRaw !== void 0 && (!Number.isFinite(Number(versionRaw)) || Number(versionRaw) < 1)) return c.json({ error: "version must be a positive integer" }, 400);
|
|
6071
|
+
const contextRaw = c.req.query("context") || "anchor";
|
|
6072
|
+
if (contextRaw !== "none" && contextRaw !== "anchor" && contextRaw !== "source") return c.json({ error: "context must be none|anchor|source" }, 400);
|
|
6073
|
+
const context = contextRaw;
|
|
6074
|
+
const cursorRaw = c.req.query("cursor");
|
|
6075
|
+
const cursor = cursorRaw === void 0 ? 0 : Number(cursorRaw);
|
|
6076
|
+
if (!Number.isFinite(cursor) || cursor < 0) return c.json({ error: "cursor must be a non-negative integer" }, 400);
|
|
6077
|
+
const limitRaw = c.req.query("limit");
|
|
6078
|
+
const limit = limitRaw === void 0 ? 50 : Math.min(Math.max(Number(limitRaw) || 50, 1), 200);
|
|
6079
|
+
const allComments = mockup.comments ?? [];
|
|
6080
|
+
if (view === "summary") {
|
|
6081
|
+
const byViewport = {
|
|
6082
|
+
desktop: 0,
|
|
6083
|
+
tablet: 0,
|
|
6084
|
+
mobile: 0
|
|
6085
|
+
};
|
|
6086
|
+
for (const c2 of allComments) byViewport[commentViewport(c2)] += 1;
|
|
6087
|
+
return c.json({
|
|
6088
|
+
view: "summary",
|
|
6089
|
+
id: mockup.id,
|
|
6090
|
+
title: mockup.title,
|
|
6091
|
+
version: mockup.version,
|
|
6092
|
+
decision: mockup.decision,
|
|
6093
|
+
decidedAt: mockup.decidedAt ?? null,
|
|
6094
|
+
createdAt: mockup.createdAt,
|
|
6095
|
+
updatedAt: mockup.updatedAt,
|
|
6096
|
+
source: mockup.source,
|
|
6097
|
+
model: mockup.model,
|
|
6098
|
+
screens: mockup.screens.map((s) => ({
|
|
6099
|
+
id: s.id,
|
|
6100
|
+
label: s.label
|
|
6101
|
+
})),
|
|
6102
|
+
versions: (mockup.versions ?? []).length,
|
|
6103
|
+
commentCounts: {
|
|
6104
|
+
total: allComments.length,
|
|
6105
|
+
open: allComments.filter((c2) => c2.status === "open").length,
|
|
6106
|
+
resolved: allComments.filter((c2) => c2.status === "resolved").length,
|
|
6107
|
+
byViewport
|
|
6108
|
+
}
|
|
6109
|
+
});
|
|
6110
|
+
}
|
|
6111
|
+
if (view === "comment") {
|
|
6112
|
+
const id = c.req.query("id");
|
|
6113
|
+
if (!id) return c.json({ error: "id is required for view=comment" }, 400);
|
|
6114
|
+
const comment = allComments.find((c2) => c2.id === id);
|
|
6115
|
+
if (!comment) return c.json({ error: `Comment ${id} not found` }, 404);
|
|
6116
|
+
return c.json({
|
|
6117
|
+
view: "comment",
|
|
6118
|
+
mockupId: mockup.id,
|
|
6119
|
+
version: mockup.version,
|
|
6120
|
+
comment: serializeMockupCommentSummary(comment, context)
|
|
6121
|
+
});
|
|
6122
|
+
}
|
|
6123
|
+
if (view === "screen") {
|
|
6124
|
+
const requestedVersion = versionRaw === void 0 ? mockup.version : Number(versionRaw);
|
|
6125
|
+
const snapshot = requestedVersion === mockup.version ? mockup : mockup.versions.find((version) => version.version === requestedVersion);
|
|
6126
|
+
if (!snapshot) return c.json({ error: `Version ${requestedVersion} not found` }, 404);
|
|
6127
|
+
if (screenFilter) {
|
|
6128
|
+
const screen = snapshot.screens.find((item) => item.id === screenFilter);
|
|
6129
|
+
if (!screen) return c.json({ error: "Screen not found" }, 404);
|
|
6130
|
+
return c.json({
|
|
6131
|
+
view: "screen",
|
|
6132
|
+
mockupId: mockup.id,
|
|
6133
|
+
version: requestedVersion,
|
|
6134
|
+
screen: {
|
|
6135
|
+
id: screen.id,
|
|
6136
|
+
label: screen.label,
|
|
6137
|
+
htmlBytes: Buffer.byteLength(screen.html, "utf8"),
|
|
6138
|
+
...context === "source" ? { html: screen.html } : {}
|
|
6139
|
+
}
|
|
6140
|
+
});
|
|
6141
|
+
}
|
|
6142
|
+
const start = Math.min(cursor, snapshot.screens.length);
|
|
6143
|
+
const page = snapshot.screens.slice(start, start + limit);
|
|
6144
|
+
return c.json({
|
|
6145
|
+
view: "screen",
|
|
6146
|
+
mockupId: mockup.id,
|
|
6147
|
+
version: requestedVersion,
|
|
6148
|
+
cursor: start,
|
|
6149
|
+
limit,
|
|
6150
|
+
nextCursor: start + page.length < snapshot.screens.length ? start + page.length : null,
|
|
6151
|
+
screens: page.map((screen) => ({
|
|
6152
|
+
id: screen.id,
|
|
6153
|
+
label: screen.label,
|
|
6154
|
+
htmlBytes: Buffer.byteLength(screen.html, "utf8")
|
|
6155
|
+
}))
|
|
6156
|
+
});
|
|
6157
|
+
}
|
|
6158
|
+
const versionN = versionRaw === void 0 ? void 0 : Number(versionRaw);
|
|
6159
|
+
const filtered = allComments.filter((c2) => {
|
|
6160
|
+
if (status !== void 0 && c2.status !== status) return false;
|
|
6161
|
+
if (screenFilter !== void 0 && c2.screenId !== screenFilter) return false;
|
|
6162
|
+
if (viewportRaw !== void 0 && commentViewport(c2) !== viewportRaw) return false;
|
|
6163
|
+
if (versionN !== void 0 && c2.createdAtMockupVersion !== versionN) return false;
|
|
6164
|
+
return true;
|
|
6165
|
+
});
|
|
6166
|
+
const start = Math.min(cursor, filtered.length);
|
|
6167
|
+
const page = filtered.slice(start, start + limit);
|
|
6168
|
+
return c.json({
|
|
6169
|
+
view: "comments",
|
|
6170
|
+
mockupId: mockup.id,
|
|
6171
|
+
version: mockup.version,
|
|
6172
|
+
cursor: start,
|
|
6173
|
+
limit,
|
|
6174
|
+
nextCursor: start + page.length < filtered.length ? start + page.length : null,
|
|
6175
|
+
comments: page.map((c2) => serializeMockupCommentSummary(c2, context, { truncateBody: 400 }))
|
|
6176
|
+
});
|
|
6177
|
+
});
|
|
6178
|
+
const screenOpError = (c, result) => {
|
|
6179
|
+
if (result.versionMismatch) return c.json({
|
|
6180
|
+
error: `Mockup is at version ${result.versionMismatch.currentVersion}, expected ${result.versionMismatch.expectedVersion} — pass the current version and retry.`,
|
|
6181
|
+
code: "version-mismatch",
|
|
6182
|
+
currentVersion: result.versionMismatch.currentVersion,
|
|
6183
|
+
expectedVersion: result.versionMismatch.expectedVersion
|
|
6184
|
+
}, 409);
|
|
6185
|
+
if (result.error === "Exact text not found") return c.json({
|
|
6186
|
+
error: result.error,
|
|
6187
|
+
code: "exact-text-not-found"
|
|
6188
|
+
}, 409);
|
|
6189
|
+
if (result.error === "Mockup not found" || result.error?.endsWith("not found")) return c.json({ error: result.error }, 404);
|
|
6190
|
+
return c.json({ error: result.error ?? "Screen operation failed" }, 400);
|
|
6191
|
+
};
|
|
6192
|
+
app.put("/api/mockups/:id/screens/:screenId", async (c) => {
|
|
6193
|
+
const mockupId = c.req.param("id");
|
|
6194
|
+
const screenId = c.req.param("screenId");
|
|
6195
|
+
if (!MOCKUP_SCREEN_ID_RE.test(screenId)) return c.json({ error: `Invalid screen id "${screenId}"` }, 400);
|
|
6196
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6197
|
+
if (typeof body.html !== "string" || !body.html.trim()) return c.json({ error: "html is required" }, 400);
|
|
6198
|
+
if (Buffer.byteLength(body.html, "utf8") > 1572864) return c.json({ error: `Each screen must be ≤ ${MOCKUP_MAX_SCREEN_BYTES} bytes` }, 400);
|
|
6199
|
+
const expectedVersion = Number.isFinite(body.expectedVersion) ? Number(body.expectedVersion) : void 0;
|
|
6200
|
+
const label = typeof body.label === "string" && body.label.trim() ? body.label.trim() : void 0;
|
|
6201
|
+
const result = await mockups.upsertScreen(mockupId, {
|
|
6202
|
+
id: screenId,
|
|
6203
|
+
label,
|
|
6204
|
+
html: body.html
|
|
6205
|
+
}, { expectedVersion });
|
|
6206
|
+
if (!result.mockup) return screenOpError(c, result);
|
|
6207
|
+
const hints = lintMockupScreens([{
|
|
6208
|
+
id: screenId,
|
|
6209
|
+
html: body.html
|
|
6210
|
+
}]);
|
|
6211
|
+
return c.json(hints.length > 0 ? {
|
|
6212
|
+
...result.mockup,
|
|
6213
|
+
hints
|
|
6214
|
+
} : result.mockup, 200);
|
|
6215
|
+
});
|
|
6216
|
+
app.patch("/api/mockups/:id/screens/:screenId", async (c) => {
|
|
6217
|
+
const mockupId = c.req.param("id");
|
|
6218
|
+
const screenId = c.req.param("screenId");
|
|
6219
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6220
|
+
const expectedText = typeof body.expectedText === "string" ? body.expectedText : "";
|
|
6221
|
+
const replacement = typeof body.replacement === "string" ? body.replacement : "";
|
|
6222
|
+
if (!expectedText) return c.json({ error: "expectedText is required" }, 400);
|
|
6223
|
+
if (typeof body.replacement !== "string") return c.json({ error: "replacement is required" }, 400);
|
|
6224
|
+
const expectedVersion = Number.isFinite(body.expectedVersion) ? Number(body.expectedVersion) : void 0;
|
|
6225
|
+
const result = await mockups.patchScreen(mockupId, screenId, {
|
|
6226
|
+
expectedText,
|
|
6227
|
+
replacement
|
|
6228
|
+
}, { expectedVersion });
|
|
6229
|
+
if (!result.mockup) return screenOpError(c, result);
|
|
6230
|
+
return c.json({
|
|
6231
|
+
mockup: result.mockup,
|
|
6232
|
+
occurrences: result.occurrences
|
|
6233
|
+
}, 200);
|
|
6234
|
+
});
|
|
6235
|
+
app.delete("/api/mockups/:id/screens/:screenId", async (c) => {
|
|
6236
|
+
const mockupId = c.req.param("id");
|
|
6237
|
+
const screenId = c.req.param("screenId");
|
|
6238
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6239
|
+
const expectedVersion = Number.isFinite(body.expectedVersion) ? Number(body.expectedVersion) : c.req.query("expectedVersion") === void 0 ? void 0 : Number(c.req.query("expectedVersion"));
|
|
6240
|
+
const result = await mockups.removeScreen(mockupId, screenId, { expectedVersion });
|
|
6241
|
+
if (!result.mockup) return screenOpError(c, result);
|
|
6242
|
+
return c.json(result.mockup, 200);
|
|
6243
|
+
});
|
|
6244
|
+
app.post("/api/mockups/:id/threads/batch", async (c) => {
|
|
6245
|
+
const mockupId = c.req.param("id");
|
|
6246
|
+
const normalized = normalizeThreadOperations((await c.req.json().catch(() => ({}))).operations);
|
|
6247
|
+
if (!normalized.ok) return c.json({
|
|
6248
|
+
error: normalized.error,
|
|
6249
|
+
index: normalized.index
|
|
6250
|
+
}, 400);
|
|
6251
|
+
const result = await mockups.applyThreadBatch(mockupId, normalized.ops);
|
|
6252
|
+
if (!result.mockup) return c.json({
|
|
6253
|
+
error: result.error ?? "Thread batch failed",
|
|
6254
|
+
results: result.results
|
|
6255
|
+
}, result.error === "Mockup not found" ? 404 : 409);
|
|
6256
|
+
return c.json({
|
|
6257
|
+
ok: true,
|
|
6258
|
+
applied: result.results.length,
|
|
6259
|
+
results: result.results,
|
|
6260
|
+
mockup: result.mockup
|
|
6261
|
+
});
|
|
6262
|
+
});
|
|
6263
|
+
app.post("/api/mockups", async (c) => {
|
|
6264
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6265
|
+
const normalized = screensFromSubmitBody(body);
|
|
6266
|
+
if (!normalized.ok) return c.json({ error: normalized.error }, 400);
|
|
6267
|
+
const title = typeof body.title === "string" && body.title.trim() ? body.title.trim() : "Untitled mockup";
|
|
6268
|
+
const mockup = await mockups.upsert({
|
|
6269
|
+
id: typeof body.id === "string" && body.id ? body.id : void 0,
|
|
6270
|
+
title,
|
|
6271
|
+
screens: normalized.screens,
|
|
6272
|
+
source: typeof body.source === "string" ? body.source : void 0,
|
|
6273
|
+
model: typeof body.model === "string" ? body.model : void 0
|
|
6274
|
+
});
|
|
6275
|
+
const hints = lintMockupScreens(normalized.screens);
|
|
6276
|
+
return c.json(hints.length > 0 ? {
|
|
6277
|
+
...mockup,
|
|
6278
|
+
hints
|
|
6279
|
+
} : mockup, 201);
|
|
6280
|
+
});
|
|
6281
|
+
app.put("/api/mockups/:id", async (c) => {
|
|
6282
|
+
const id = c.req.param("id");
|
|
6283
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6284
|
+
let screens;
|
|
6285
|
+
if (body.html !== void 0 || body.screens !== void 0) {
|
|
6286
|
+
const normalized = screensFromSubmitBody(body);
|
|
6287
|
+
if (!normalized.ok) return c.json({ error: normalized.error }, 400);
|
|
6288
|
+
screens = normalized.screens;
|
|
6289
|
+
}
|
|
6290
|
+
const updated = await mockups.update(id, {
|
|
6291
|
+
title: typeof body.title === "string" ? body.title : void 0,
|
|
6292
|
+
screens,
|
|
6293
|
+
source: typeof body.source === "string" ? body.source : void 0,
|
|
6294
|
+
model: typeof body.model === "string" ? body.model : void 0
|
|
6295
|
+
});
|
|
6296
|
+
if (!updated) return c.json({ error: "Mockup not found" }, 404);
|
|
6297
|
+
return c.json(updated);
|
|
6298
|
+
});
|
|
6299
|
+
app.delete("/api/mockups/:id", async (c) => {
|
|
6300
|
+
if (!await mockups.remove(c.req.param("id"))) return c.json({ error: "Mockup not found" }, 404);
|
|
6301
|
+
return c.json({ ok: true });
|
|
6302
|
+
});
|
|
6303
|
+
app.post("/api/mockups/:id/comments", async (c) => {
|
|
6304
|
+
const mockupId = c.req.param("id");
|
|
6305
|
+
const mockup = await mockups.get(mockupId);
|
|
6306
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
6307
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6308
|
+
const kind = body.kind;
|
|
6309
|
+
if (kind !== "section" && kind !== "block" && kind !== "point") return c.json({ error: "kind must be section, block, or point" }, 400);
|
|
6310
|
+
const screenId = typeof body.screenId === "string" ? body.screenId : "";
|
|
6311
|
+
if (!mockup.screens.some((s) => s.id === screenId)) return c.json({ error: "Unknown screenId" }, 400);
|
|
6312
|
+
if (typeof body.nonce === "string" && body.nonce) {
|
|
6313
|
+
const nonceError = validateMockupNonce(body.nonce, mockupId, screenId);
|
|
6314
|
+
if (nonceError) return c.json({
|
|
6315
|
+
error: nonceError,
|
|
6316
|
+
code: "invalid-nonce"
|
|
6317
|
+
}, 409);
|
|
6318
|
+
}
|
|
6319
|
+
if (typeof body.body !== "string" || !body.body.trim()) return c.json({ error: "body is required" }, 400);
|
|
6320
|
+
const viewport = normalizeMockupViewport(body.viewport);
|
|
6321
|
+
const severityRaw = body.severity;
|
|
6322
|
+
const severity = severityRaw === "blocking" || severityRaw === "nit" || severityRaw === "question" || severityRaw === "praise" || severityRaw === "none" ? severityRaw : void 0;
|
|
6323
|
+
const rect = body.rect && typeof body.rect === "object" ? body.rect : void 0;
|
|
6324
|
+
const comment = {
|
|
6325
|
+
id: crypto.randomUUID(),
|
|
6326
|
+
screenId,
|
|
6327
|
+
kind,
|
|
6328
|
+
target: typeof body.target === "string" ? body.target : void 0,
|
|
6329
|
+
selector: typeof body.selector === "string" ? body.selector : void 0,
|
|
6330
|
+
fingerprint: typeof body.fingerprint === "string" ? body.fingerprint : void 0,
|
|
6331
|
+
html: typeof body.html === "string" ? body.html : void 0,
|
|
6332
|
+
contextHtml: typeof body.contextHtml === "string" ? body.contextHtml : void 0,
|
|
6333
|
+
x: Number.isFinite(body.x) ? Number(body.x) : void 0,
|
|
6334
|
+
y: Number.isFinite(body.y) ? Number(body.y) : void 0,
|
|
6335
|
+
sectionX: Number.isFinite(body.sectionX) ? Number(body.sectionX) : void 0,
|
|
6336
|
+
sectionY: Number.isFinite(body.sectionY) ? Number(body.sectionY) : void 0,
|
|
6337
|
+
snapshot: typeof body.snapshot === "string" ? body.snapshot : void 0,
|
|
6338
|
+
rect: rect && Number.isFinite(rect.x) && Number.isFinite(rect.y) && Number.isFinite(rect.w) && Number.isFinite(rect.h) ? {
|
|
6339
|
+
x: Number(rect.x),
|
|
6340
|
+
y: Number(rect.y),
|
|
6341
|
+
w: Number(rect.w),
|
|
6342
|
+
h: Number(rect.h)
|
|
6343
|
+
} : void 0,
|
|
6344
|
+
body: String(body.body),
|
|
6345
|
+
status: "open",
|
|
6346
|
+
createdAt: Date.now(),
|
|
6347
|
+
createdAtMockupVersion: Number.isFinite(body.createdAtMockupVersion) ? Number(body.createdAtMockupVersion) : mockup.version,
|
|
6348
|
+
viewport,
|
|
6349
|
+
replies: [],
|
|
6350
|
+
...severity && severity !== "none" ? { severity } : {}
|
|
6351
|
+
};
|
|
6352
|
+
const updated = await mockups.addComment(mockupId, comment);
|
|
6353
|
+
if (!updated) return c.json({ error: "Mockup not found" }, 404);
|
|
6354
|
+
return c.json(updated, 201);
|
|
6355
|
+
});
|
|
6356
|
+
app.put("/api/mockups/:id/comments/:commentId", async (c) => {
|
|
6357
|
+
const { body, status } = await c.req.json();
|
|
6358
|
+
const updated = await mockups.updateComment(c.req.param("id"), c.req.param("commentId"), {
|
|
6359
|
+
body,
|
|
6360
|
+
status
|
|
6361
|
+
});
|
|
6362
|
+
if (!updated) return c.json({ error: "Mockup or comment not found" }, 404);
|
|
6363
|
+
return c.json(updated);
|
|
6364
|
+
});
|
|
6365
|
+
app.delete("/api/mockups/:id/comments/:commentId", async (c) => {
|
|
6366
|
+
const updated = await mockups.removeComment(c.req.param("id"), c.req.param("commentId"));
|
|
6367
|
+
if (!updated) return c.json({ error: "Mockup or comment not found" }, 404);
|
|
6368
|
+
return c.json(updated);
|
|
6369
|
+
});
|
|
6370
|
+
app.post("/api/mockups/:id/comments/:commentId/replies", async (c) => {
|
|
6371
|
+
const { body, role, model } = await c.req.json();
|
|
6372
|
+
const reply = {
|
|
6373
|
+
id: crypto.randomUUID(),
|
|
6374
|
+
body,
|
|
6375
|
+
createdAt: Date.now(),
|
|
6376
|
+
role: role || (model ? "agent" : "user"),
|
|
6377
|
+
model: model || void 0
|
|
6378
|
+
};
|
|
6379
|
+
const updated = await mockups.addReply(c.req.param("id"), c.req.param("commentId"), reply);
|
|
6380
|
+
if (!updated) return c.json({ error: "Mockup or comment not found" }, 404);
|
|
6381
|
+
return c.json(updated);
|
|
6382
|
+
});
|
|
6383
|
+
app.put("/api/mockups/:id/comments/:commentId/replies/:replyId", async (c) => {
|
|
6384
|
+
const { body } = await c.req.json();
|
|
6385
|
+
if (!body) return c.json({ error: "Body is required" }, 400);
|
|
6386
|
+
const updated = await mockups.updateReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"), body);
|
|
6387
|
+
if (!updated) return c.json({ error: "Mockup, comment, or reply not found" }, 404);
|
|
6388
|
+
return c.json(updated);
|
|
6389
|
+
});
|
|
6390
|
+
app.delete("/api/mockups/:id/comments/:commentId/replies/:replyId", async (c) => {
|
|
6391
|
+
const updated = await mockups.removeReply(c.req.param("id"), c.req.param("commentId"), c.req.param("replyId"));
|
|
6392
|
+
if (!updated) return c.json({ error: "Mockup, comment, or reply not found" }, 404);
|
|
6393
|
+
return c.json(updated);
|
|
6394
|
+
});
|
|
6395
|
+
app.post("/api/mockups/:id/decision", async (c) => {
|
|
6396
|
+
const mockupId = c.req.param("id");
|
|
6397
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6398
|
+
const decision = body.decision;
|
|
6399
|
+
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);
|
|
6400
|
+
const decisionComment = typeof body.decisionComment === "string" ? body.decisionComment : void 0;
|
|
6401
|
+
const mode = body?.mode === "comment-only" || body?.mode === "standard" ? body.mode : "standard";
|
|
6402
|
+
const focusedScreen = typeof body.screen === "string" ? body.screen : void 0;
|
|
6403
|
+
const focusedViewport = isMockupViewport(body.viewport) ? body.viewport : void 0;
|
|
6404
|
+
const mockup = await mockups.setDecision(mockupId, decision, decisionComment);
|
|
6405
|
+
if (!mockup) return c.json({ error: "Mockup not found" }, 404);
|
|
6406
|
+
const openCommentCount = (mockup.comments ?? []).filter((x) => x.status === "open").length;
|
|
6407
|
+
const payload = mockupReviewSession.decide({
|
|
6408
|
+
mockup,
|
|
6409
|
+
decision,
|
|
6410
|
+
decisionComment: mockup.decisionComment,
|
|
6411
|
+
reviewXml: formatMockupReview(mockup, {
|
|
6412
|
+
mode,
|
|
6413
|
+
focusedScreen,
|
|
6414
|
+
focusedViewport
|
|
6415
|
+
}),
|
|
6416
|
+
openCommentCount,
|
|
6417
|
+
mode
|
|
6418
|
+
});
|
|
6419
|
+
return c.json({
|
|
6420
|
+
ok: true,
|
|
6421
|
+
round: payload.round,
|
|
6422
|
+
decision,
|
|
6423
|
+
mode: payload.mode,
|
|
6424
|
+
openCommentCount,
|
|
6425
|
+
waiters: mockupReviewSession.snapshot().waiters
|
|
6426
|
+
});
|
|
6427
|
+
});
|
|
6428
|
+
app.get("/api/mockup-review/await", async (c) => {
|
|
6429
|
+
const sinceRaw = c.req.query("sinceRound");
|
|
6430
|
+
const sinceRound = sinceRaw !== void 0 && sinceRaw !== "" ? Number(sinceRaw) : void 0;
|
|
6431
|
+
const requested = Number(c.req.query("timeoutMs")) || 25e3;
|
|
6432
|
+
const timeoutMs = Math.min(Math.max(requested, 1e3), 5e4);
|
|
6433
|
+
const result = await mockupReviewSession.await({
|
|
6434
|
+
sinceRound: Number.isNaN(sinceRound) ? void 0 : sinceRound,
|
|
6435
|
+
timeoutMs,
|
|
6436
|
+
signal: c.req.raw.signal
|
|
6437
|
+
});
|
|
6438
|
+
return c.json(result);
|
|
6439
|
+
});
|
|
6440
|
+
app.get("/api/mockup-review/status", (c) => {
|
|
6441
|
+
return c.json(mockupReviewSession.snapshot());
|
|
6442
|
+
});
|
|
4766
6443
|
app.post("/api/attachments", async (c) => {
|
|
4767
6444
|
const file = (await c.req.parseBody())["file"];
|
|
4768
6445
|
if (!file || !(file instanceof File)) return c.json({ error: "No file uploaded" }, 400);
|
|
@@ -4839,6 +6516,7 @@ async function newestActivityMs(projectDir) {
|
|
|
4839
6516
|
for (const name of [
|
|
4840
6517
|
"comments.json",
|
|
4841
6518
|
"plans.json",
|
|
6519
|
+
"mockups.json",
|
|
4842
6520
|
"pr-session.json"
|
|
4843
6521
|
]) {
|
|
4844
6522
|
const file = join(projectDir, name);
|