farming-code 2.2.23 → 2.2.28

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 (264) hide show
  1. package/README.md +9 -2
  2. package/README.zh_cn.md +9 -2
  3. package/THIRD_PARTY_NOTICES.md +3 -0
  4. package/backend/acp-runtime.js +15 -1
  5. package/backend/agent-manager.js +1054 -94
  6. package/backend/atomic-json-store.js +37 -0
  7. package/backend/cli-agents.js +9 -7
  8. package/backend/config-manager.js +167 -58
  9. package/backend/control-api.js +2 -1
  10. package/backend/farming-agent-bootstrap.js +49 -0
  11. package/backend/farming-agent-bootstrap.zh_cn.md +5 -0
  12. package/backend/farming-app-cli.js +9 -2
  13. package/backend/farming-cli.js +55 -0
  14. package/backend/farming-session-store.js +92 -89
  15. package/backend/main-agent-skills.js +17 -0
  16. package/backend/native-pty-host-client.js +99 -7
  17. package/backend/native-pty-host-path.js +20 -0
  18. package/backend/native-pty-host.js +6 -5
  19. package/backend/preview-session-manager.js +68 -0
  20. package/backend/provider-adapters.js +44 -5
  21. package/backend/review-session-store.js +32 -25
  22. package/backend/review-state-store.js +13 -14
  23. package/backend/run-history-store.js +13 -16
  24. package/backend/server.js +103 -78
  25. package/backend/storage-layout.js +20 -0
  26. package/backend/workspace-file-router.js +171 -6
  27. package/backend/workspace-file-service.js +19 -0
  28. package/backend/workspace-root-registry.js +6 -4
  29. package/dist/assets/App-CM7Cy-l4.css +1 -0
  30. package/dist/assets/App-CPG4WcjO.js +658 -0
  31. package/dist/assets/{FileEditorMarkdownPreview-KbnE8VC8.js → FileEditorMarkdownPreview-DYOMm-b1.js} +5 -5
  32. package/dist/assets/FileEditorPane-DXXq82aM.js +2 -0
  33. package/dist/assets/IconGlyphs-DrdPjYXr.js +1 -0
  34. package/dist/assets/ProjectFilesSection-CP66XVPl.js +12 -0
  35. package/dist/assets/{ReviewPage-L1vh1Ggs.js → ReviewPage-w21MEPD6.js} +1 -1
  36. package/dist/assets/{abnfDiagram-VRR7QNED-nnglrXFi.js → abnfDiagram-VRR7QNED-BY1qwq8A.js} +1 -1
  37. package/dist/assets/api-w2x5QfxB.js +1 -0
  38. package/dist/assets/{arc-CWoVlxJw.js → arc-DgZRAlwj.js} +1 -1
  39. package/dist/assets/{architectureDiagram-ZJ3FMSHR-BEXMaFfY.js → architectureDiagram-ZJ3FMSHR-jFn144od.js} +1 -1
  40. package/dist/assets/{blockDiagram-677ZJIJ3-1mCezSBk.js → blockDiagram-677ZJIJ3-BmRjlV0H.js} +1 -1
  41. package/dist/assets/{c4Diagram-LMCZKHZV-DhTiI6Uv.js → c4Diagram-LMCZKHZV-DUKul6tT.js} +1 -1
  42. package/dist/assets/{workspace-editor-monaco-DLlws5Ig.js → channel-CI9j3QFD.js} +1 -1
  43. package/dist/assets/chunk-2Q5K7J3B-87UnWaoh.js +1 -0
  44. package/dist/assets/{chunk-32BRIVSS-CCdsKd73.js → chunk-32BRIVSS-rGVu23o5.js} +1 -1
  45. package/dist/assets/{chunk-52WLFC77-Gncjk4LF.js → chunk-52WLFC77-CvFDKgbB.js} +1 -1
  46. package/dist/assets/{chunk-5VM5RSS4-Dlw7AXWp.js → chunk-5VM5RSS4-CFgnRW1C.js} +1 -1
  47. package/dist/assets/{chunk-7BUUIJ7U-ZBRhjLOh.js → chunk-7BUUIJ7U-Cp7bqW6s.js} +1 -1
  48. package/dist/assets/{chunk-C7G6YPKG-CiDVUP82.js → chunk-C7G6YPKG-BIS1gjZO.js} +1 -1
  49. package/dist/assets/{chunk-EX3LRPZG-z6gXP2_N.js → chunk-EX3LRPZG-BKG-cR9j.js} +1 -1
  50. package/dist/assets/{chunk-FWX5IMBZ-DMZBccQa.js → chunk-FWX5IMBZ-B8PsTr8a.js} +2 -2
  51. package/dist/assets/{chunk-HOUHSVGY-D8tIeoPk.js → chunk-HOUHSVGY-BHFOAiXT.js} +1 -1
  52. package/dist/assets/{chunk-ICXQ74PX-b6oQ2Q17.js → chunk-ICXQ74PX-CvLE1J9X.js} +1 -1
  53. package/dist/assets/{chunk-JWPE2WC7-DSyBRaRG.js → chunk-JWPE2WC7-D8misks7.js} +1 -1
  54. package/dist/assets/{chunk-MOJQB5TN-DAVH_p2a.js → chunk-MOJQB5TN-D-oNgSNH.js} +1 -1
  55. package/dist/assets/{chunk-OGEWGWER-DxoiMCI-.js → chunk-OGEWGWER-C2N-la9e.js} +1 -1
  56. package/dist/assets/{chunk-PUDLZKDR-NsOWh7HM.js → chunk-PUDLZKDR-BwI3Jz8q.js} +1 -1
  57. package/dist/assets/{chunk-Q4XR5HBZ-Di4H7L21.js → chunk-Q4XR5HBZ-BuqGfsXA.js} +1 -1
  58. package/dist/assets/{chunk-RYQCIY6F-CZA9pvFX.js → chunk-RYQCIY6F-Bq4XZcGt.js} +1 -1
  59. package/dist/assets/{chunk-V7JOEXUC-DttpZg9Y.js → chunk-V7JOEXUC-QZLzdTTJ.js} +1 -1
  60. package/dist/assets/chunk-VAUOI2AC-Cb0-kQ9g.js +1 -0
  61. package/dist/assets/{chunk-VR4S4FIN-D-Fmtd4Z.js → chunk-VR4S4FIN-DcZTpML_.js} +1 -1
  62. package/dist/assets/{chunk-WYO6CB5R-CUxMtfK2.js → chunk-WYO6CB5R-DCU8wuny.js} +2 -2
  63. package/dist/assets/chunk-XXDRQBXY-BZAC1Krv.js +1 -0
  64. package/dist/assets/{chunk-ZGVPDNZ5-CoV2QEl0.js → chunk-ZGVPDNZ5-B7r85QbY.js} +1 -1
  65. package/dist/assets/{chunk-ZIRB5QZD-DncwlQQc.js → chunk-ZIRB5QZD-BgEnDWhQ.js} +1 -1
  66. package/dist/assets/classDiagram-OUVF2IWQ-DcvGOuvi.js +1 -0
  67. package/dist/assets/classDiagram-v2-EOCWNBFH-DDYUSofQ.js +1 -0
  68. package/dist/assets/code-dark-BDjwj7Um.css +1 -0
  69. package/dist/assets/{cose-bilkent-JH36ORCC-BNIAz0nN.js → cose-bilkent-JH36ORCC-CThXS2qG.js} +1 -1
  70. package/dist/assets/{cssMode-DDWxTPm_.js → cssMode-Bdg5RpcJ.js} +1 -1
  71. package/dist/assets/{cynefinDiagram-TSTJHNR4-0xPI92Lg.js → cynefinDiagram-TSTJHNR4-DxBgzw25.js} +1 -1
  72. package/dist/assets/{dagre-DXlcHgr3.js → dagre-BM-GvGUZ.js} +1 -1
  73. package/dist/assets/{dagre-VKFMJZFB-DfuMG4gU.js → dagre-VKFMJZFB-B7UCRmxR.js} +1 -1
  74. package/dist/assets/{diagram-FQU43EPY-CT00bLZ6.js → diagram-FQU43EPY-CeL2DwKx.js} +1 -1
  75. package/dist/assets/{diagram-G47NLZAW-Xbh7zXUx.js → diagram-G47NLZAW-BEirglCC.js} +1 -1
  76. package/dist/assets/{diagram-NH7WQ7WH-SRDVPfn7.js → diagram-NH7WQ7WH-C6wcI-Rl.js} +1 -1
  77. package/dist/assets/{diagram-OA4YK3LP-CQRli4Np.js → diagram-OA4YK3LP-DpM-XXtJ.js} +1 -1
  78. package/dist/assets/{diagram-WEI45ONY-DMf17WQi.js → diagram-WEI45ONY-BQyaut9k.js} +1 -1
  79. package/dist/assets/{ebnfDiagram-CCIWWBDH-DhpFkgJ5.js → ebnfDiagram-CCIWWBDH-UmAXsUhr.js} +1 -1
  80. package/dist/assets/{erDiagram-Q63AITRT-1IwxWmJo.js → erDiagram-Q63AITRT-CdYLEOch.js} +1 -1
  81. package/dist/assets/file-icons-BOlcJ0ZF.js +1 -0
  82. package/dist/assets/flowDiagram-23GEKE2U-CO-z9-tq.js +1 -0
  83. package/dist/assets/{freemarker2-QBsTERH-.js → freemarker2-DfMDJfnE.js} +1 -1
  84. package/dist/assets/{ganttDiagram-NO4QXBWP-BNCdfyr1.js → ganttDiagram-NO4QXBWP-DgSUNdUB.js} +1 -1
  85. package/dist/assets/{gitGraphDiagram-IHSO6WYX-pbyWZuiX.js → gitGraphDiagram-IHSO6WYX-DjwjtRtF.js} +1 -1
  86. package/dist/assets/{handlebars-CuoxPo5A.js → handlebars-BEdPtzMT.js} +1 -1
  87. package/dist/assets/{html-gX5PEu0Q.js → html-B1fPb1QD.js} +1 -1
  88. package/dist/assets/html2canvas-DsczWxBa.js +5 -0
  89. package/dist/assets/{htmlMode-DIWUbUxd.js → htmlMode-CY2ZjHxH.js} +1 -1
  90. package/dist/assets/{index-DhTUO8DA.js → index-BzQO3pUr.js} +2 -2
  91. package/dist/assets/{infoDiagram-FWYZ7A6U-BcTOgDbe.js → infoDiagram-FWYZ7A6U-Bi9076l2.js} +1 -1
  92. package/dist/assets/{ishikawaDiagram-FXEZZL3T-DCdvGKrn.js → ishikawaDiagram-FXEZZL3T-DaMaNZN7.js} +1 -1
  93. package/dist/assets/{javascript-BYKGIUGs.js → javascript-BV2OsWA0.js} +1 -1
  94. package/dist/assets/{journeyDiagram-5HDEW3XC-BZ-1q_IT.js → journeyDiagram-5HDEW3XC-CfDy69EZ.js} +1 -1
  95. package/dist/assets/{jsonMode-AFmQQP6u.js → jsonMode-zd07aeuX.js} +1 -1
  96. package/dist/assets/{kanban-definition-HUTT4EX6-BIgejCjj.js → kanban-definition-HUTT4EX6-XFvZ722B.js} +1 -1
  97. package/dist/assets/{line-CfptFMmX.js → line-BXK9PKLn.js} +1 -1
  98. package/dist/assets/{linear-Dld3_ERu.js → linear-CULvmq2Z.js} +1 -1
  99. package/dist/assets/{liquid-DBIl-f0z.js → liquid-BParI24X.js} +1 -1
  100. package/dist/assets/{lspLanguageFeatures-DK6tqicV.js → lspLanguageFeatures-BzredkoV.js} +1 -1
  101. package/dist/assets/main-BlVUXfxa.css +1 -0
  102. package/dist/assets/{map-LZWWoeWa.js → map-C9Qw9eWh.js} +1 -1
  103. package/dist/assets/{mdx-DS-bVtZN.js → mdx-vGzUJN98.js} +1 -1
  104. package/dist/assets/{mermaid.core-gSpX30pV.js → mermaid.core-CzazaIKr.js} +3 -3
  105. package/dist/assets/{mindmap-definition-LN4V7U3C-BKBaI4LW.js → mindmap-definition-LN4V7U3C-Uoff7hZ3.js} +1 -1
  106. package/dist/assets/{monaco.contribution-BXz-lfFB.js → monaco.contribution-BmaeBQEc.js} +2 -2
  107. package/dist/assets/{ordinal-BLrOss5K.js → ordinal-r6pIwRdZ.js} +1 -1
  108. package/dist/assets/{pegDiagram-2B236MQR-sBvDY3VX.js → pegDiagram-2B236MQR-CCBiHQjk.js} +1 -1
  109. package/dist/assets/{pieDiagram-ENE6RG2P-DWlWcu08.js → pieDiagram-ENE6RG2P-D61s7l75.js} +1 -1
  110. package/dist/assets/{python-C93q3xXz.js → python-CUwdLgsI.js} +1 -1
  111. package/dist/assets/{quadrantDiagram-ABIIQ3AL-DZutUd3J.js → quadrantDiagram-ABIIQ3AL-D_CWk2DQ.js} +1 -1
  112. package/dist/assets/{railroadDiagram-RFXS5EU6-DQb3lXGM.js → railroadDiagram-RFXS5EU6-CdlCHtxN.js} +1 -1
  113. package/dist/assets/{razor-CBifNGXO.js → razor-BScebUxU.js} +1 -1
  114. package/dist/assets/{requirementDiagram-TGXJPOKE-DqFGXu2e.js → requirementDiagram-TGXJPOKE-BLpHtkoC.js} +1 -1
  115. package/dist/assets/{sankeyDiagram-HTMAVEWB-d7ECahnO.js → sankeyDiagram-HTMAVEWB-CkhYZN2j.js} +1 -1
  116. package/dist/assets/{sequenceDiagram-DBY2YBRQ-DdsbKqct.js → sequenceDiagram-DBY2YBRQ-8bploKJl.js} +1 -1
  117. package/dist/assets/{sizeCapture-X5ZJPWSS-daTB6OcF.js → sizeCapture-X5ZJPWSS-DtSdkS0d.js} +1 -1
  118. package/dist/assets/{src-WBW1QxJt.js → src-6yeq6WBA.js} +1 -1
  119. package/dist/assets/{stateDiagram-2N3HPSRC-P6iSrgwR.js → stateDiagram-2N3HPSRC-DfmVGpfA.js} +1 -1
  120. package/dist/assets/stateDiagram-v2-6OUMAXLB-B2c9CQ-J.js +1 -0
  121. package/dist/assets/{swimlanes-5IMT3BWC-BGnZJx1M.js → swimlanes-5IMT3BWC-DRLk2zRF.js} +2 -2
  122. package/dist/assets/swimlanesDiagram-G3AALYLV-B7_APh_h.js +8 -0
  123. package/dist/assets/{timeline-definition-FHXFAJF6-rA-r3aUR.js → timeline-definition-FHXFAJF6-Cngfy588.js} +1 -1
  124. package/dist/assets/{tsMode-B0oUTcXQ.js → tsMode-DzUAibXK.js} +1 -1
  125. package/dist/assets/{typescript-KJKfYO7K.js → typescript-D9EOeM7N.js} +1 -1
  126. package/dist/assets/{vennDiagram-L72KCM5P-DiVKUjA9.js → vennDiagram-L72KCM5P-DeJn63Lq.js} +1 -1
  127. package/dist/assets/{wardleyDiagram-EHGQE667-Cfxh7UOn.js → wardleyDiagram-EHGQE667-BH1nzy77.js} +1 -1
  128. package/dist/assets/{workers-BSQRIkqa.js → workers-DAiomYZM.js} +1 -1
  129. package/dist/assets/workspace-editor-model-CSGCl21x.js +1 -0
  130. package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +1 -0
  131. package/dist/assets/{workspace-editor-monaco-BIpqcWf1.js → workspace-editor-monaco-tXA-udGx.js} +3 -3
  132. package/dist/assets/workspace-view-state-BS_V8JVs.js +7 -0
  133. package/dist/assets/{xml-C8FAcN6j.js → xml-wWl1dhkQ.js} +1 -1
  134. package/dist/assets/{xychartDiagram-FW5EYKEG--hmryQeL.js → xychartDiagram-FW5EYKEG-SkO2prCU.js} +1 -1
  135. package/dist/assets/{yaml-BQ4NrRhG.js → yaml-Dc2WDaOZ.js} +1 -1
  136. package/dist/crt-markdown-renderer.js +46 -46
  137. package/dist/index.html +1 -1
  138. package/extensions/browser/backend/agent-capability.js +55 -0
  139. package/extensions/browser/backend/browser-mcp-server.js +269 -0
  140. package/extensions/browser/backend/browser-resource-manager.js +524 -0
  141. package/extensions/browser/backend/browser-resource-store.js +149 -0
  142. package/extensions/browser/backend/browser-router.js +128 -0
  143. package/extensions/browser/backend/cdp-browser-runtime.js +648 -0
  144. package/extensions/browser/backend/cdp-client.js +167 -0
  145. package/extensions/browser/backend/executable-discovery.js +91 -0
  146. package/extensions/browser/backend/farming-browser-client.js +83 -0
  147. package/extensions/browser/backend/index.js +7 -0
  148. package/extensions/browser/bin/farming-browser +117 -0
  149. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +362 -0
  150. package/extensions/browser/frontend/BrowserViewer.tsx +329 -0
  151. package/extensions/browser/frontend/browser-resource-state.ts +60 -0
  152. package/extensions/browser/frontend/browser.css +516 -0
  153. package/extensions/browser/frontend/types.ts +25 -0
  154. package/extensions/browser/frontend/useBrowserResources.ts +151 -0
  155. package/frontend/session-bridge.js +7 -6
  156. package/frontend/skins/crt/app.js +64 -4
  157. package/package.json +18 -13
  158. package/shared/browser-protocol.js +1 -1
  159. package/dist/assets/App-BbNBDA__.js +0 -210
  160. package/dist/assets/App-BrP-ENHg.css +0 -1
  161. package/dist/assets/FileEditorPane-5_HUejjd.js +0 -2
  162. package/dist/assets/IconGlyphs-C2WFcoJS.js +0 -1
  163. package/dist/assets/ProjectFilesSection-C1RAJWOE.js +0 -12
  164. package/dist/assets/api-D8nyOEbz.js +0 -1
  165. package/dist/assets/channel-BVWNatUB.js +0 -1
  166. package/dist/assets/chunk-2Q5K7J3B-d5WWh8jo.js +0 -1
  167. package/dist/assets/chunk-VAUOI2AC-UHNr_yhX.js +0 -1
  168. package/dist/assets/chunk-XXDRQBXY-BBXwjhUx.js +0 -1
  169. package/dist/assets/classDiagram-OUVF2IWQ-Db0ib5iE.js +0 -1
  170. package/dist/assets/classDiagram-v2-EOCWNBFH-Bkf69XF9.js +0 -1
  171. package/dist/assets/code-dark-BKtvf0Xc.css +0 -1
  172. package/dist/assets/file-icons-CHWt5nSy.js +0 -1
  173. package/dist/assets/flowDiagram-23GEKE2U-DYFo2xmB.js +0 -1
  174. package/dist/assets/main-XMXgyJJt.css +0 -1
  175. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIYSf_qh.js +0 -1
  176. package/dist/assets/swimlanesDiagram-G3AALYLV-BIkrlKrZ.js +0 -8
  177. package/dist/assets/workspace-editor-model-D0Im34S5.js +0 -1
  178. package/dist/assets/workspace-view-state-BqB4HM7r.js +0 -7
  179. /package/dist/assets/{abap-Cc5D5h5i.js → abap-CtX5nWyX.js} +0 -0
  180. /package/dist/assets/{apex-BKopwLmx.js → apex-DQJRAHwc.js} +0 -0
  181. /package/dist/assets/{array-HeX70jSN.js → array-BTwzYU0x.js} +0 -0
  182. /package/dist/assets/{azcli-DI-C34WA.js → azcli-9SMrOQ4q.js} +0 -0
  183. /package/dist/assets/{bat-Cbp6u42z.js → bat-CBGLUsw1.js} +0 -0
  184. /package/dist/assets/{bicep-4QA1jTCM.js → bicep-pDx8da1v.js} +0 -0
  185. /package/dist/assets/{cameligo-Dda_adPs.js → cameligo-hZV3vQ6_.js} +0 -0
  186. /package/dist/assets/{chunk-Y2CYZVJY-Di0sYzi1.js → chunk-Y2CYZVJY-BYi0W5ea.js} +0 -0
  187. /package/dist/assets/{clojure-CvY8A83L.js → clojure-BhCwU3gq.js} +0 -0
  188. /package/dist/assets/{coffee-4LX2Sv8A.js → coffee-BVB1PsgH.js} +0 -0
  189. /package/dist/assets/{core-B9MwRjDE.js → core-vPkV8P_m.js} +0 -0
  190. /package/dist/assets/{cpp-D4AzSV28.js → cpp-eTjuiNPq.js} +0 -0
  191. /package/dist/assets/{csharp-C4nWN4Yd.js → csharp-1kP-kKZT.js} +0 -0
  192. /package/dist/assets/{csp-CiEaaS5Z.js → csp-C_iQqIes.js} +0 -0
  193. /package/dist/assets/{css-DXxBRsbk.js → css-CxOs7IfP.js} +0 -0
  194. /package/dist/assets/{cypher-DOMzqyFA.js → cypher-Cd-a9Y82.js} +0 -0
  195. /package/dist/assets/{cytoscape.esm-Cl8iC3D7.js → cytoscape.esm-OLojuicg.js} +0 -0
  196. /package/dist/assets/{dart-BWS7MXt_.js → dart-whhXpexn.js} +0 -0
  197. /package/dist/assets/{defaultLocale-GJwWH1Jr.js → defaultLocale-CPimVhfE.js} +0 -0
  198. /package/dist/assets/{dist-ClAL-Tv5.js → dist-CLPYXg1u.js} +0 -0
  199. /package/dist/assets/{dockerfile-Baf4U3xT.js → dockerfile-DibBi8b6.js} +0 -0
  200. /package/dist/assets/{ecl-CT7udlkQ.js → ecl-DlF1KdoW.js} +0 -0
  201. /package/dist/assets/{editor.api2-B9h6iFPK.js → editor.api2-D74ufkbr.js} +0 -0
  202. /package/dist/assets/{elixir-CIbAZw5l.js → elixir-CHQ8uFT3.js} +0 -0
  203. /package/dist/assets/{flow9-Dqiw45WV.js → flow9-8nUnVKk1.js} +0 -0
  204. /package/dist/assets/{fsharp-E8E2L9jU.js → fsharp-DNZNZyEE.js} +0 -0
  205. /package/dist/assets/{go-CouVSN4D.js → go-_kPJjNc_.js} +0 -0
  206. /package/dist/assets/{graphlib-CbSvZcZU.js → graphlib-CBGF9gA9.js} +0 -0
  207. /package/dist/assets/{graphql-a6h6h5-t.js → graphql-4HCNVt5g.js} +0 -0
  208. /package/dist/assets/{hcl-vso3e6N-.js → hcl-C87ymY0o.js} +0 -0
  209. /package/dist/assets/{ini-SwZZkotR.js → ini-BYrrGNTc.js} +0 -0
  210. /package/dist/assets/{init-BPLPMQ3Y.js → init-8_PCa3LO.js} +0 -0
  211. /package/dist/assets/{java-CtRLC4_3.js → java-B211rJbk.js} +0 -0
  212. /package/dist/assets/{julia-CuJWFKQH.js → julia-DVPsRtBm.js} +0 -0
  213. /package/dist/assets/{katex-C8DPyI6s.js → katex-C2iYad0m.js} +0 -0
  214. /package/dist/assets/{kotlin-B_kBhsre.js → kotlin-BYyNerjp.js} +0 -0
  215. /package/dist/assets/{less-CtrHIfz3.js → less-EC9UblGE.js} +0 -0
  216. /package/dist/assets/{lexon-yVt1pJLu.js → lexon-B1yuUdqW.js} +0 -0
  217. /package/dist/assets/{lua-prOwgJ4U.js → lua-Bo6GFzxT.js} +0 -0
  218. /package/dist/assets/{m3-DhrdTQ84.js → m3-C-Y9cBVI.js} +0 -0
  219. /package/dist/assets/{markdown-DoI5HwSM.js → markdown-BHu-wLbe.js} +0 -0
  220. /package/dist/assets/{mermaid-parser.core-BQtepoLd.js → mermaid-parser.core-IxcGtNYG.js} +0 -0
  221. /package/dist/assets/{mips-Dye5jsBU.js → mips-Dkds26rX.js} +0 -0
  222. /package/dist/assets/{msdax-TkNOyIL8.js → msdax-PU4g-8_g.js} +0 -0
  223. /package/dist/assets/{mysql-CEc-RKZF.js → mysql-fPpuGpS7.js} +0 -0
  224. /package/dist/assets/{objective-c-CqqCsQz0.js → objective-c-Wy1e5JUf.js} +0 -0
  225. /package/dist/assets/{pascal-DO_pj8yg.js → pascal-BIXwvrqU.js} +0 -0
  226. /package/dist/assets/{pascaligo-1cbdZdF3.js → pascaligo-DwR0AtL_.js} +0 -0
  227. /package/dist/assets/{path-YdFzr2W6.js → path-BxQuywGl.js} +0 -0
  228. /package/dist/assets/{perl-B_N3PYfC.js → perl-CGHAl8my.js} +0 -0
  229. /package/dist/assets/{pgsql-9iYk1FQ7.js → pgsql-B91M73E2.js} +0 -0
  230. /package/dist/assets/{php-rO2sgoUM.js → php-C70ttrCx.js} +0 -0
  231. /package/dist/assets/{pla-Cbgv60kq.js → pla-BNrXaRh0.js} +0 -0
  232. /package/dist/assets/{postiats-y6xrhy_M.js → postiats-Dv2IAZR9.js} +0 -0
  233. /package/dist/assets/{powerquery-BaD8gWNE.js → powerquery-CkY5Wq7J.js} +0 -0
  234. /package/dist/assets/{powershell-BMhG4JVe.js → powershell-uGIYLU3C.js} +0 -0
  235. /package/dist/assets/{protobuf-CdOP5kN3.js → protobuf-CavfmJu-.js} +0 -0
  236. /package/dist/assets/{pug-Ck60hNF9.js → pug-Drqnyitx.js} +0 -0
  237. /package/dist/assets/{qrcode-DV-_rjU8.js → qrcode-7uGsTEmi.js} +0 -0
  238. /package/dist/assets/{qsharp-BcaKFfP-.js → qsharp-C7yF-Dti.js} +0 -0
  239. /package/dist/assets/{r-rFNes0iK.js → r-HXLGP0_-.js} +0 -0
  240. /package/dist/assets/{redis-DkpQ3K6G.js → redis-C4y6mf2p.js} +0 -0
  241. /package/dist/assets/{redshift-Di9N5TI3.js → redshift-DdQrFPrv.js} +0 -0
  242. /package/dist/assets/{restructuredtext-By5ZzLt6.js → restructuredtext-CBrf2PNj.js} +0 -0
  243. /package/dist/assets/{rough.esm-c4PR5shF.js → rough.esm-6CnTHTkH.js} +0 -0
  244. /package/dist/assets/{ruby-fF5zGiLp.js → ruby-CVtMoX2B.js} +0 -0
  245. /package/dist/assets/{rust-C5b3AYY6.js → rust-Ckq4sbMP.js} +0 -0
  246. /package/dist/assets/{sb-CD07UNRs.js → sb-er66OpE3.js} +0 -0
  247. /package/dist/assets/{scala-jtn1ip9G.js → scala-BdWZz-hH.js} +0 -0
  248. /package/dist/assets/{scheme-C4vqMkn_.js → scheme-kTV_s2kP.js} +0 -0
  249. /package/dist/assets/{scss-I4Glxjbz.js → scss-Bymy2VJQ.js} +0 -0
  250. /package/dist/assets/{shell-DubUcuns.js → shell-bwEP1LQ1.js} +0 -0
  251. /package/dist/assets/{solidity-CNy2KSl2.js → solidity-DCddeBgL.js} +0 -0
  252. /package/dist/assets/{sophia-D9_URt84.js → sophia-BMN3AQig.js} +0 -0
  253. /package/dist/assets/{sparql-DviXpm4C.js → sparql-CSXOLjzi.js} +0 -0
  254. /package/dist/assets/{sql-vDRbAZgK.js → sql-COg8ewmK.js} +0 -0
  255. /package/dist/assets/{st-CnpnRqA7.js → st-CIaZtBS_.js} +0 -0
  256. /package/dist/assets/{swift-D_g8Ganz.js → swift-DXiOhUHu.js} +0 -0
  257. /package/dist/assets/{systemverilog-OuZa5h1M.js → systemverilog-BTDE6Dr2.js} +0 -0
  258. /package/dist/assets/{tcl-DBm9ucsV.js → tcl-B0zy9NvV.js} +0 -0
  259. /package/dist/assets/{twig-BRzEqX5Y.js → twig-D594l4Z1.js} +0 -0
  260. /package/dist/assets/{typespec-D4ZgGeX8.js → typespec-XKVjaPqo.js} +0 -0
  261. /package/dist/assets/{useWorkspaceMenuKeyboard-Brws6Ar9.js → useWorkspaceMenuKeyboard-Bpy_s69Z.js} +0 -0
  262. /package/dist/assets/{vb-DcP4s6WS.js → vb-B2wYoKRV.js} +0 -0
  263. /package/dist/assets/{wgsl-CnRJ_QUf.js → wgsl-D0Yf2LbK.js} +0 -0
  264. /package/dist/assets/{workspace-working-copy-D8-s_Sgh.js → workspace-working-copy-C3FSR3N6.js} +0 -0
@@ -0,0 +1,362 @@
1
+ import { createPortal } from 'react-dom'
2
+ import { useCallback, useEffect, useLayoutEffect, useState } from 'react'
3
+ import {
4
+ ChevronDownGlyph,
5
+ ChevronRightGlyph,
6
+ CloseGlyph,
7
+ PencilGlyph,
8
+ PlusGlyph,
9
+ SquareGlyph,
10
+ } from '@/components/IconGlyphs'
11
+ import type { ProjectGroup } from '@/components/code/types'
12
+ import type { UiPreferences } from '@/lib/ui-preferences'
13
+ import type { BrowserResource } from './types'
14
+ import type { BrowserResourcesController } from './useBrowserResources'
15
+
16
+ const COLLAPSED_KEY = 'farming.code.browserSectionsCollapsed.v1'
17
+
18
+ function readCollapsed() {
19
+ try {
20
+ const parsed = JSON.parse(localStorage.getItem(COLLAPSED_KEY) || '[]')
21
+ return new Set(Array.isArray(parsed) ? parsed.filter(value => typeof value === 'string') : [])
22
+ } catch {
23
+ return new Set<string>()
24
+ }
25
+ }
26
+
27
+ function writeCollapsed(values: Set<string>) {
28
+ try {
29
+ localStorage.setItem(COLLAPSED_KEY, JSON.stringify([...values]))
30
+ } catch {
31
+ // In-memory collapse state still works.
32
+ }
33
+ }
34
+
35
+ function browserCopy(language: UiPreferences['language']) {
36
+ const zh = language === 'zh'
37
+ return {
38
+ browsers: zh ? '浏览器' : 'Browsers',
39
+ newBrowser: zh ? '新建浏览器' : 'New Browser',
40
+ createBrowser: zh ? '新建浏览器' : 'Create browser',
41
+ browserName: zh ? '浏览器名称' : 'Browser name',
42
+ renameBrowser: zh ? '重命名浏览器' : 'Rename Browser',
43
+ startBrowser: zh ? '启动浏览器' : 'Start Browser',
44
+ stopBrowser: zh ? '停止浏览器' : 'Stop Browser',
45
+ deleteBrowser: zh ? '删除浏览器' : 'Delete Browser',
46
+ deleteConfirm: (name: string) => zh
47
+ ? `删除浏览器“${name}”及其独立资料目录?`
48
+ : `Delete Browser “${name}” and its isolated profile?`,
49
+ failed: zh ? '浏览器失败' : 'Browser failed',
50
+ starting: zh ? '启动中…' : 'Starting…',
51
+ stopping: zh ? '停止中…' : 'Stopping…',
52
+ stopped: zh ? '已停止' : 'Stopped',
53
+ renameFailed: zh ? '浏览器重命名失败' : 'Failed to rename Browser',
54
+ transitionFailed: zh ? '浏览器状态切换失败' : 'Browser transition failed',
55
+ deleteFailed: zh ? '浏览器删除失败' : 'Failed to delete Browser',
56
+ createFailed: zh ? '浏览器创建失败' : 'Failed to create Browser',
57
+ }
58
+ }
59
+
60
+ type BrowserCopy = ReturnType<typeof browserCopy>
61
+
62
+ function BrowserPlayGlyph() {
63
+ return (
64
+ <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
65
+ <path d="M4.5 2.6a.75.75 0 0 1 1.15-.63l7 4.9a.75.75 0 0 1 0 1.23l-7 4.9A.75.75 0 0 1 4.5 12.4V2.6Z" />
66
+ </svg>
67
+ )
68
+ }
69
+
70
+ function resourceStatusLabel(resource: BrowserResource, copy: BrowserCopy) {
71
+ if (resource.status === 'running') return resource.url.replace(/^https?:\/\//, '') || 'about:blank'
72
+ if (resource.status === 'failed') return resource.error || copy.failed
73
+ if (resource.status === 'starting') return copy.starting
74
+ if (resource.status === 'stopping') return copy.stopping
75
+ return copy.stopped
76
+ }
77
+
78
+ function BrowserRow({
79
+ resource,
80
+ active,
81
+ controller,
82
+ copy,
83
+ onOpen,
84
+ }: {
85
+ resource: BrowserResource
86
+ active: boolean
87
+ controller: BrowserResourcesController
88
+ copy: BrowserCopy
89
+ onOpen: (resource: BrowserResource) => void
90
+ }) {
91
+ const [renaming, setRenaming] = useState(false)
92
+ const [name, setName] = useState(resource.name)
93
+ const busy = resource.status === 'starting' || resource.status === 'stopping'
94
+ const submitRename = async () => {
95
+ const next = name.trim()
96
+ setRenaming(false)
97
+ if (!next || next === resource.name) {
98
+ setName(resource.name)
99
+ return
100
+ }
101
+ await controller.rename(resource.id, next).catch(error => {
102
+ setName(resource.name)
103
+ window.alert(error instanceof Error ? error.message : copy.renameFailed)
104
+ })
105
+ }
106
+ return (
107
+ <div
108
+ className={`farming-browser-row ${active ? 'active' : ''}`}
109
+ data-testid="farming-browser-row"
110
+ data-browser-id={resource.id}
111
+ role="button"
112
+ tabIndex={0}
113
+ onClick={() => onOpen(resource)}
114
+ onKeyDown={event => {
115
+ if (event.key === 'Enter' || event.key === ' ') {
116
+ event.preventDefault()
117
+ onOpen(resource)
118
+ }
119
+ }}
120
+ >
121
+ <span className={`farming-browser-status ${resource.status}`} aria-hidden="true" />
122
+ <span className="farming-browser-row-copy">
123
+ {renaming ? (
124
+ <input
125
+ autoFocus
126
+ value={name}
127
+ aria-label={copy.browserName}
128
+ onClick={event => event.stopPropagation()}
129
+ onChange={event => setName(event.currentTarget.value)}
130
+ onBlur={() => void submitRename()}
131
+ onKeyDown={event => {
132
+ event.stopPropagation()
133
+ if (event.key === 'Enter') void submitRename()
134
+ if (event.key === 'Escape') {
135
+ setName(resource.name)
136
+ setRenaming(false)
137
+ }
138
+ }}
139
+ />
140
+ ) : (
141
+ <span className="farming-browser-row-name">{resource.name}</span>
142
+ )}
143
+ <span className="farming-browser-row-detail" title={resource.error || resource.url}>
144
+ {resourceStatusLabel(resource, copy)}
145
+ </span>
146
+ </span>
147
+ <span className="farming-browser-row-actions">
148
+ <button
149
+ type="button"
150
+ title={copy.renameBrowser}
151
+ aria-label={copy.renameBrowser}
152
+ onClick={event => {
153
+ event.stopPropagation()
154
+ setRenaming(true)
155
+ }}
156
+ >
157
+ <PencilGlyph />
158
+ </button>
159
+ <button
160
+ type="button"
161
+ disabled={busy}
162
+ title={resource.status === 'running' ? copy.stopBrowser : copy.startBrowser}
163
+ aria-label={resource.status === 'running' ? copy.stopBrowser : copy.startBrowser}
164
+ onClick={event => {
165
+ event.stopPropagation()
166
+ const operation = resource.status === 'running' ? controller.stop(resource.id) : controller.start(resource.id)
167
+ void operation.catch(error => window.alert(error instanceof Error ? error.message : copy.transitionFailed))
168
+ }}
169
+ >
170
+ {resource.status === 'running' ? <SquareGlyph /> : <BrowserPlayGlyph />}
171
+ </button>
172
+ <button
173
+ type="button"
174
+ title={copy.deleteBrowser}
175
+ aria-label={copy.deleteBrowser}
176
+ onClick={event => {
177
+ event.stopPropagation()
178
+ if (!window.confirm(copy.deleteConfirm(resource.name))) return
179
+ void controller.remove(resource.id).catch(error => {
180
+ window.alert(error instanceof Error ? error.message : copy.deleteFailed)
181
+ })
182
+ }}
183
+ >
184
+ <CloseGlyph />
185
+ </button>
186
+ </span>
187
+ </div>
188
+ )
189
+ }
190
+
191
+ function BrowserSection({
192
+ project,
193
+ resources,
194
+ activeBrowserId,
195
+ controller,
196
+ copy,
197
+ collapsed,
198
+ onToggle,
199
+ onOpen,
200
+ }: {
201
+ project: ProjectGroup
202
+ resources: BrowserResource[]
203
+ activeBrowserId: string | null
204
+ controller: BrowserResourcesController
205
+ copy: BrowserCopy
206
+ collapsed: boolean
207
+ onToggle: () => void
208
+ onOpen: (resource: BrowserResource) => void
209
+ }) {
210
+ const createBrowser = async () => {
211
+ try {
212
+ const resource = await controller.create(project.workspace)
213
+ const running = await controller.start(resource.id)
214
+ onOpen(running)
215
+ } catch (error) {
216
+ window.alert(error instanceof Error ? error.message : copy.createFailed)
217
+ }
218
+ }
219
+ return (
220
+ <section className="farming-browser-section" data-testid="farming-browser-section">
221
+ <header>
222
+ <button
223
+ type="button"
224
+ className="farming-browser-section-toggle"
225
+ aria-expanded={!collapsed}
226
+ onClick={onToggle}
227
+ >
228
+ <span className="farming-browser-section-chevron" aria-hidden="true">
229
+ {collapsed ? <ChevronRightGlyph /> : <ChevronDownGlyph />}
230
+ </span>
231
+ <span>{copy.browsers}</span>
232
+ <small>{resources.length}</small>
233
+ </button>
234
+ <button
235
+ type="button"
236
+ className="farming-browser-new"
237
+ title={copy.newBrowser}
238
+ aria-label={copy.newBrowser}
239
+ onClick={() => void createBrowser()}
240
+ >
241
+ <PlusGlyph />
242
+ </button>
243
+ </header>
244
+ {!collapsed && (
245
+ <div className="farming-browser-list">
246
+ {resources.map(resource => (
247
+ <BrowserRow
248
+ key={resource.id}
249
+ resource={resource}
250
+ active={resource.id === activeBrowserId}
251
+ controller={controller}
252
+ copy={copy}
253
+ onOpen={onOpen}
254
+ />
255
+ ))}
256
+ {resources.length === 0 && (
257
+ <button
258
+ type="button"
259
+ className="farming-browser-empty"
260
+ onClick={() => void createBrowser()}
261
+ >
262
+ <PlusGlyph />
263
+ <span>{copy.createBrowser}</span>
264
+ </button>
265
+ )}
266
+ </div>
267
+ )}
268
+ </section>
269
+ )
270
+ }
271
+
272
+ function findProjectExpandedElement(projectId: string) {
273
+ const titles = document.querySelectorAll<HTMLElement>('[data-testid="code-project-title"]')
274
+ for (const title of titles) {
275
+ if (title.dataset.projectId !== projectId) continue
276
+ return title.closest<HTMLElement>('[data-testid="code-project-group"]')
277
+ ?.querySelector<HTMLElement>('.code-project-expanded') ?? null
278
+ }
279
+ return null
280
+ }
281
+
282
+ export function BrowserSidebarPortals({
283
+ projects,
284
+ collapsedProjectIds,
285
+ activeBrowserId,
286
+ controller,
287
+ language,
288
+ onOpen,
289
+ }: {
290
+ projects: ProjectGroup[]
291
+ collapsedProjectIds: ReadonlySet<string>
292
+ activeBrowserId: string | null
293
+ controller: BrowserResourcesController
294
+ language: UiPreferences['language']
295
+ onOpen: (resource: BrowserResource) => void
296
+ }) {
297
+ const copy = browserCopy(language)
298
+ const available = controller.loading === false && controller.capability?.available === true
299
+ const [targets, setTargets] = useState(new Map<string, HTMLElement>())
300
+ const [collapsed, setCollapsed] = useState(readCollapsed)
301
+ const refreshTargets = useCallback(() => {
302
+ const next = new Map<string, HTMLElement>()
303
+ for (const project of projects) {
304
+ if (collapsedProjectIds.has(project.id)) continue
305
+ const target = findProjectExpandedElement(project.id)
306
+ if (target) next.set(project.id, target)
307
+ }
308
+ setTargets(current => {
309
+ if (current.size === next.size && [...next].every(([id, element]) => current.get(id) === element)) return current
310
+ return next
311
+ })
312
+ }, [collapsedProjectIds, projects])
313
+
314
+ useLayoutEffect(() => {
315
+ if (available) {
316
+ refreshTargets()
317
+ return
318
+ }
319
+ setTargets(current => current.size === 0 ? current : new Map())
320
+ }, [available, refreshTargets])
321
+ useEffect(() => {
322
+ if (!available) return undefined
323
+ const observer = new MutationObserver(refreshTargets)
324
+ observer.observe(document.body, { childList: true, subtree: true })
325
+ return () => observer.disconnect()
326
+ }, [available, refreshTargets])
327
+
328
+ const toggle = (projectId: string) => {
329
+ setCollapsed(current => {
330
+ const next = new Set(current)
331
+ if (next.has(projectId)) next.delete(projectId)
332
+ else next.add(projectId)
333
+ writeCollapsed(next)
334
+ return next
335
+ })
336
+ }
337
+
338
+ if (!available) return null
339
+
340
+ return (
341
+ <>
342
+ {projects.map(project => {
343
+ const target = targets.get(project.id)
344
+ if (!target || !project.workspace) return null
345
+ return createPortal(
346
+ <BrowserSection
347
+ project={project}
348
+ resources={controller.byWorkspace.get(project.workspace) ?? []}
349
+ activeBrowserId={activeBrowserId}
350
+ controller={controller}
351
+ copy={copy}
352
+ collapsed={collapsed.has(project.id)}
353
+ onToggle={() => toggle(project.id)}
354
+ onOpen={onOpen}
355
+ />,
356
+ target,
357
+ `browser-section:${project.id}`,
358
+ )
359
+ })}
360
+ </>
361
+ )
362
+ }
@@ -0,0 +1,329 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { ArrowLeftGlyph, ArrowRightGlyph, CopyGlyph, SquareGlyph } from '@/components/IconGlyphs'
3
+ import { appPath } from '@/lib/base-path'
4
+ import type { UiPreferences } from '@/lib/ui-preferences'
5
+ import type { BrowserResource } from './types'
6
+ import type { BrowserResourcesController } from './useBrowserResources'
7
+
8
+ function viewerWebSocketUrl(resource: BrowserResource) {
9
+ const url = new URL(appPath(`/api/browsers/${encodeURIComponent(resource.id)}/viewer`), window.location.href)
10
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
11
+ return url.href
12
+ }
13
+
14
+ function viewerCopy(language: UiPreferences['language']) {
15
+ const zh = language === 'zh'
16
+ return {
17
+ back: zh ? '后退' : 'Back',
18
+ forward: zh ? '前进' : 'Forward',
19
+ reload: zh ? '重新加载' : 'Reload',
20
+ address: zh ? '浏览器地址' : 'Browser address',
21
+ connected: zh ? 'Viewer 已连接' : 'Viewer connected',
22
+ disconnected: zh ? 'Viewer 未连接' : 'Viewer disconnected',
23
+ copyLink: zh ? '复制链接' : 'Copy link',
24
+ start: zh ? '启动' : 'Start',
25
+ stop: zh ? '停止' : 'Stop',
26
+ startBrowser: zh ? '启动浏览器' : 'Start Browser',
27
+ stoppedTitle: zh ? '浏览器已停止' : 'Browser stopped',
28
+ failedTitle: zh ? '浏览器失败' : 'Browser failed',
29
+ stoppedHint: zh ? '启动后,用户和 Agent 将操作同一个页面。' : 'Start it to share one page between the user and Agent.',
30
+ pageLabel: (name: string) => zh ? `${name} 浏览器页面` : `${name} browser page`,
31
+ textInput: zh ? '浏览器文本输入' : 'Browser text input',
32
+ viewerFailed: zh ? 'Browser Viewer 失败' : 'Browser Viewer failed',
33
+ connectionFailed: zh ? 'Browser Viewer 连接失败' : 'Browser Viewer connection failed',
34
+ navigationFailed: zh ? '导航失败' : 'Navigation failed',
35
+ transitionFailed: zh ? '浏览器状态切换失败' : 'Browser transition failed',
36
+ }
37
+ }
38
+
39
+ function ReloadGlyph() {
40
+ return (
41
+ <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
42
+ <path d="M12.8 3.8V1.5a.5.5 0 0 1 1 0v3.6a.5.5 0 0 1-.5.5H9.7a.5.5 0 0 1 0-1h2.36A5.5 5.5 0 1 0 13.5 8a.5.5 0 0 1 1 0 6.5 6.5 0 1 1-1.7-4.2Z" />
43
+ </svg>
44
+ )
45
+ }
46
+
47
+ function PlayGlyph() {
48
+ return (
49
+ <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
50
+ <path d="M4.5 2.6a.75.75 0 0 1 1.15-.63l7 4.9a.75.75 0 0 1 0 1.23l-7 4.9A.75.75 0 0 1 4.5 12.4V2.6Z" />
51
+ </svg>
52
+ )
53
+ }
54
+
55
+ export function BrowserViewer({
56
+ resource,
57
+ controller,
58
+ language,
59
+ onResource,
60
+ }: {
61
+ resource: BrowserResource
62
+ controller: BrowserResourcesController
63
+ language: UiPreferences['language']
64
+ onResource: (resource: BrowserResource) => void
65
+ }) {
66
+ const copy = viewerCopy(language)
67
+ const canvasRef = useRef<HTMLCanvasElement>(null)
68
+ const textInputRef = useRef<HTMLTextAreaElement>(null)
69
+ const composingTextRef = useRef(false)
70
+ const socketRef = useRef<WebSocket | null>(null)
71
+ const imageSequenceRef = useRef(0)
72
+ const paintFrameRef = useRef<number | null>(null)
73
+ const [address, setAddress] = useState(resource.url)
74
+ const [connected, setConnected] = useState(false)
75
+ const [viewerError, setViewerError] = useState('')
76
+
77
+ useEffect(() => setAddress(resource.url), [resource.url])
78
+
79
+ useEffect(() => {
80
+ if (resource.status !== 'running') {
81
+ setConnected(false)
82
+ socketRef.current?.close()
83
+ socketRef.current = null
84
+ return undefined
85
+ }
86
+ let cancelled = false
87
+ let reconnectTimer = 0
88
+ const connect = () => {
89
+ if (cancelled) return
90
+ const socket = new WebSocket(viewerWebSocketUrl(resource))
91
+ socketRef.current = socket
92
+ socket.onopen = () => {
93
+ setConnected(true)
94
+ setViewerError('')
95
+ }
96
+ socket.onmessage = event => {
97
+ const message = JSON.parse(String(event.data)) as {
98
+ type: string
99
+ data?: string
100
+ resource?: BrowserResource
101
+ message?: string
102
+ }
103
+ if (message.type === 'browser-state' && message.resource) {
104
+ onResource(message.resource)
105
+ return
106
+ }
107
+ if (message.type === 'browser-error') {
108
+ setViewerError(message.message || copy.viewerFailed)
109
+ return
110
+ }
111
+ if (message.type !== 'browser-frame' || !message.data) return
112
+ const sequence = ++imageSequenceRef.current
113
+ const image = new Image()
114
+ image.onload = () => {
115
+ if (sequence !== imageSequenceRef.current) return
116
+ if (paintFrameRef.current !== null) window.cancelAnimationFrame(paintFrameRef.current)
117
+ paintFrameRef.current = window.requestAnimationFrame(() => {
118
+ paintFrameRef.current = null
119
+ if (sequence !== imageSequenceRef.current) return
120
+ const canvas = canvasRef.current
121
+ const context = canvas?.getContext('2d')
122
+ if (!canvas || !context) return
123
+ canvas.width = image.naturalWidth
124
+ canvas.height = image.naturalHeight
125
+ context.drawImage(image, 0, 0)
126
+ })
127
+ }
128
+ image.src = `data:image/jpeg;base64,${message.data}`
129
+ }
130
+ socket.onclose = () => {
131
+ if (socketRef.current === socket) socketRef.current = null
132
+ setConnected(false)
133
+ if (!cancelled) reconnectTimer = window.setTimeout(connect, 1_000)
134
+ }
135
+ socket.onerror = () => setViewerError(copy.connectionFailed)
136
+ }
137
+ connect()
138
+ return () => {
139
+ cancelled = true
140
+ window.clearTimeout(reconnectTimer)
141
+ if (paintFrameRef.current !== null) window.cancelAnimationFrame(paintFrameRef.current)
142
+ paintFrameRef.current = null
143
+ socketRef.current?.close()
144
+ socketRef.current = null
145
+ }
146
+ }, [copy.connectionFailed, copy.viewerFailed, onResource, resource.id, resource.status])
147
+
148
+ const send = useCallback((message: Record<string, unknown>) => {
149
+ const socket = socketRef.current
150
+ if (!socket || socket.readyState !== WebSocket.OPEN) return
151
+ socket.send(JSON.stringify({ ...message, generation: resource.generation }))
152
+ }, [resource.generation])
153
+
154
+ const point = (event: {
155
+ currentTarget: HTMLCanvasElement
156
+ clientX: number
157
+ clientY: number
158
+ }) => {
159
+ const canvas = event.currentTarget
160
+ const bounds = canvas.getBoundingClientRect()
161
+ return {
162
+ x: (event.clientX - bounds.left) * canvas.width / Math.max(1, bounds.width),
163
+ y: (event.clientY - bounds.top) * canvas.height / Math.max(1, bounds.height),
164
+ }
165
+ }
166
+ const navigate = async () => {
167
+ try {
168
+ const normalized = /^https?:\/\//i.test(address) || address === 'about:blank' ? address : `http://${address}`
169
+ const response = await fetch(appPath(`/api/browsers/${encodeURIComponent(resource.id)}/navigate`), {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
172
+ body: JSON.stringify({ url: normalized }),
173
+ })
174
+ const next = await response.json() as BrowserResource & { error?: string }
175
+ if (!response.ok) throw new Error(next.error || copy.navigationFailed)
176
+ onResource(next)
177
+ } catch (error) {
178
+ setViewerError(error instanceof Error ? error.message : copy.navigationFailed)
179
+ }
180
+ }
181
+ const browserAction = async (kind: 'back' | 'forward' | 'reload') => {
182
+ try {
183
+ const response = await fetch(appPath(`/api/browsers/${encodeURIComponent(resource.id)}/action`), {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
186
+ body: JSON.stringify({ kind }),
187
+ })
188
+ const next = await response.json() as BrowserResource & { error?: string }
189
+ if (!response.ok) throw new Error(next.error || `Browser ${kind} failed`)
190
+ onResource(next)
191
+ } catch (error) {
192
+ setViewerError(error instanceof Error ? error.message : `Browser ${kind} failed`)
193
+ }
194
+ }
195
+
196
+ return (
197
+ <section className="farming-browser-viewer" data-testid="farming-browser-viewer">
198
+ <header className="farming-browser-toolbar">
199
+ <button type="button" className="farming-browser-toolbar-icon" aria-label={copy.back} title={copy.back} disabled={resource.status !== 'running'} onClick={() => void browserAction('back')}><ArrowLeftGlyph /></button>
200
+ <button type="button" className="farming-browser-toolbar-icon" aria-label={copy.forward} title={copy.forward} disabled={resource.status !== 'running'} onClick={() => void browserAction('forward')}><ArrowRightGlyph /></button>
201
+ <button type="button" className="farming-browser-toolbar-icon" aria-label={copy.reload} title={copy.reload} disabled={resource.status !== 'running'} onClick={() => void browserAction('reload')}><ReloadGlyph /></button>
202
+ <form onSubmit={event => {
203
+ event.preventDefault()
204
+ void navigate()
205
+ }}>
206
+ <input
207
+ value={address}
208
+ aria-label={copy.address}
209
+ disabled={resource.status !== 'running'}
210
+ onChange={event => setAddress(event.currentTarget.value)}
211
+ />
212
+ </form>
213
+ <span className={`farming-browser-connection ${connected ? 'connected' : ''}`} title={connected ? copy.connected : copy.disconnected} />
214
+ <button
215
+ type="button"
216
+ className="farming-browser-toolbar-action farming-browser-copy-link"
217
+ onClick={() => {
218
+ const stableUrl = new URL(window.location.href)
219
+ stableUrl.searchParams.set('browser', resource.id)
220
+ void navigator.clipboard.writeText(stableUrl.href)
221
+ }}
222
+ >
223
+ <CopyGlyph />
224
+ <span>{copy.copyLink}</span>
225
+ </button>
226
+ <button
227
+ type="button"
228
+ className="farming-browser-toolbar-action"
229
+ disabled={resource.status === 'starting' || resource.status === 'stopping'}
230
+ onClick={() => {
231
+ const transition = resource.status === 'running'
232
+ ? controller.stop(resource.id)
233
+ : controller.start(resource.id)
234
+ void transition.catch(error => setViewerError(error instanceof Error ? error.message : copy.transitionFailed))
235
+ }}
236
+ >
237
+ {resource.status === 'running' ? <SquareGlyph /> : <PlayGlyph />}
238
+ <span>{resource.status === 'running' ? copy.stop : copy.start}</span>
239
+ </button>
240
+ </header>
241
+ <div className="farming-browser-viewport">
242
+ {resource.status === 'running' ? (
243
+ <canvas
244
+ ref={canvasRef}
245
+ tabIndex={0}
246
+ aria-label={copy.pageLabel(resource.name)}
247
+ onPointerMove={event => {
248
+ const position = point(event)
249
+ send({ type: 'pointer', action: 'move', ...position })
250
+ }}
251
+ onPointerDown={event => {
252
+ event.currentTarget.focus()
253
+ event.currentTarget.setPointerCapture(event.pointerId)
254
+ const position = point(event)
255
+ send({ type: 'pointer', action: 'down', button: event.button === 2 ? 'right' : event.button === 1 ? 'middle' : 'left', ...position })
256
+ if (event.pointerType !== 'mouse') {
257
+ window.requestAnimationFrame(() => textInputRef.current?.focus({ preventScroll: true }))
258
+ }
259
+ }}
260
+ onPointerUp={event => {
261
+ const position = point(event)
262
+ send({ type: 'pointer', action: 'up', button: event.button === 2 ? 'right' : event.button === 1 ? 'middle' : 'left', ...position })
263
+ }}
264
+ onContextMenu={event => event.preventDefault()}
265
+ onWheel={event => {
266
+ event.preventDefault()
267
+ const position = point(event)
268
+ send({ type: 'wheel', deltaX: event.deltaX, deltaY: event.deltaY, ...position })
269
+ }}
270
+ onKeyDown={event => {
271
+ event.preventDefault()
272
+ const modifiers = (event.altKey ? 1 : 0)
273
+ | (event.ctrlKey ? 2 : 0)
274
+ | (event.metaKey ? 4 : 0)
275
+ | (event.shiftKey ? 8 : 0)
276
+ if (event.key.length === 1 && (modifiers & ~8) === 0) {
277
+ send({ type: 'text', text: event.key })
278
+ } else {
279
+ send({ type: 'key', key: event.key, code: event.code, modifiers })
280
+ }
281
+ }}
282
+ onPaste={event => {
283
+ const text = event.clipboardData.getData('text/plain')
284
+ if (!text) return
285
+ event.preventDefault()
286
+ send({ type: 'text', text })
287
+ }}
288
+ />
289
+ ) : (
290
+ <div className="farming-browser-placeholder">
291
+ <strong>{resource.status === 'failed' ? copy.failedTitle : copy.stoppedTitle}</strong>
292
+ <p>{resource.error || copy.stoppedHint}</p>
293
+ <button type="button" onClick={() => void controller.start(resource.id).catch(error => setViewerError(error.message))}>
294
+ <PlayGlyph />
295
+ <span>{copy.startBrowser}</span>
296
+ </button>
297
+ </div>
298
+ )}
299
+ <textarea
300
+ ref={textInputRef}
301
+ className="farming-browser-text-input-proxy"
302
+ aria-label={copy.textInput}
303
+ autoCapitalize="none"
304
+ autoCorrect="off"
305
+ spellCheck={false}
306
+ onCompositionStart={() => {
307
+ composingTextRef.current = true
308
+ }}
309
+ onCompositionEnd={() => {
310
+ composingTextRef.current = false
311
+ }}
312
+ onInput={event => {
313
+ if (composingTextRef.current) return
314
+ const text = event.currentTarget.value
315
+ if (text) send({ type: 'text', text })
316
+ event.currentTarget.value = ''
317
+ }}
318
+ onKeyDown={event => {
319
+ if (composingTextRef.current) return
320
+ if (!['Backspace', 'Delete', 'Enter', 'Tab', 'Escape', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown'].includes(event.key)) return
321
+ event.preventDefault()
322
+ send({ type: 'key', key: event.key, code: event.code })
323
+ }}
324
+ />
325
+ </div>
326
+ {viewerError && <div className="farming-browser-viewer-error" role="alert">{viewerError}</div>}
327
+ </section>
328
+ )
329
+ }