diffing 0.17.0 → 0.18.0

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