diffing 0.16.1 → 0.18.0

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