@hienlh/ppm 0.18.13 → 0.18.15

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 (542) hide show
  1. package/.rdtest2-cf.out +0 -0
  2. package/CHANGELOG.md +23 -0
  3. package/CLAUDE.md +3 -0
  4. package/assets/skills/ppm/SKILL.md +1 -1
  5. package/assets/skills/ppm/references/cli-reference.md +18 -0
  6. package/assets/skills/ppm/references/http-api.md +8 -1
  7. package/dist/web/assets/{adaptive-context-menu-CbgPYQmj.js → adaptive-context-menu-ChBZWLLq.js} +1 -1
  8. package/dist/web/assets/{ai-resource-editor-unM2HNmg.js → ai-resource-editor-C8aAY8Po.js} +1 -1
  9. package/dist/web/assets/{ai-settings-section-CdktdrwF.js → ai-settings-section-ERu1WiZc.js} +1 -1
  10. package/dist/web/assets/{angular-html-DcpbdcNG.js → angular-html-CXgHIQ2U.js} +1 -1
  11. package/dist/web/assets/{angular-ts-Bv_WIk34.js → angular-ts-C8renMV9.js} +1 -1
  12. package/dist/web/assets/api-client-WpZ2FDHg.js +1 -0
  13. package/dist/web/assets/api-settings-BgSEE6et.js +1 -0
  14. package/dist/web/assets/{api-settings-BdCtex9i.js → api-settings-R59c66DE.js} +1 -1
  15. package/dist/web/assets/{api-themes-pRRoLvgD.js → api-themes-DKxrwJda.js} +1 -1
  16. package/dist/web/assets/{apl-D5_lLszu.js → apl-DkGH8laI.js} +1 -1
  17. package/dist/web/assets/architecture-PBZL5I3N-CCalSKAt.js +1 -0
  18. package/dist/web/assets/{astro-CBHwddYL.js → astro-tHUd8Zlg.js} +1 -1
  19. package/dist/web/assets/{audio-preview-BD1iDovf.js → audio-preview-B_C6Dpvz.js} +1 -1
  20. package/dist/web/assets/{blade-DGe1d2Pq.js → blade-BbKpjIYK.js} +1 -1
  21. package/dist/web/assets/{bsl-2lyAzRJA.js → bsl-DA0UYto0.js} +1 -1
  22. package/dist/web/assets/{button-2WCAS8dI.js → button-DBSpDPS0.js} +1 -1
  23. package/dist/web/assets/{cairo-DjzZeJrq.js → cairo-79ayucbB.js} +1 -1
  24. package/dist/web/assets/chat-tab-BraiJsYG.js +15 -0
  25. package/dist/web/assets/{cobol-cC3XIuAJ.js → cobol-DSS2EOYo.js} +1 -1
  26. package/dist/web/assets/code-editor-lhmLNhXu.js +10 -0
  27. package/dist/web/assets/{coffee-4eGboVoU.js → coffee-DvSxq2iD.js} +1 -1
  28. package/dist/web/assets/{conflict-editor-DdJag4QK.js → conflict-editor-C4VCv51R.js} +1 -1
  29. package/dist/web/assets/{context-menu-BO1c9jKy.js → context-menu-BpwUny-o.js} +1 -1
  30. package/dist/web/assets/{cpp-DF9_G-OU.js → cpp-BhWZ-FSn.js} +1 -1
  31. package/dist/web/assets/{crystal-OavZlzcm.js → crystal-yP2OeF-f.js} +1 -1
  32. package/dist/web/assets/{csv-preview-DnXwouF3.js → csv-preview-D46W_icb.js} +1 -1
  33. package/dist/web/assets/{data-grid-overlay-editor-B9Gpjm3l.js → data-grid-overlay-editor-DeGkPYtA.js} +1 -1
  34. package/dist/web/assets/{database-viewer-Dk8oGyZx.js → database-viewer-DFI4YtrH.js} +1 -1
  35. package/dist/web/assets/{dialog-B-l2ZxVm.js → dialog-RIpeb87v.js} +1 -1
  36. package/dist/web/assets/diff-viewer-eaNOLcBL.js +4 -0
  37. package/dist/web/assets/{docx-preview-DAdHPlhZ.js → docx-preview-Cbj-pOK3.js} +1 -1
  38. package/dist/web/assets/{edge-TzYh8OlN.js → edge-D3K9cd-p.js} +1 -1
  39. package/dist/web/assets/{edit-diff-preview-DlwP3UIU.js → edit-diff-preview-DDqy3KLA.js} +1 -1
  40. package/dist/web/assets/{elixir-Cy3RhKKH.js → elixir-D4PBmsLp.js} +1 -1
  41. package/dist/web/assets/{elm-BTBZdweK.js → elm-DSpXjuaH.js} +1 -1
  42. package/dist/web/assets/{erb-dv1mI0y4.js → erb-CaYZMTJW.js} +1 -1
  43. package/dist/web/assets/{erlang-DjeiDrkl.js → erlang-DX5BFqJj.js} +1 -1
  44. package/dist/web/assets/{esm-FRwvcesu.js → esm-Cv7HsFnk.js} +1 -1
  45. package/dist/web/assets/explorer-body-BxbSttfl.js +2 -0
  46. package/dist/web/assets/explorer-window-content-DUqQCC7A.js +1 -0
  47. package/dist/web/assets/{extension-store-DUaZINyC.js → extension-store-B1EClUAr.js} +1 -1
  48. package/dist/web/assets/{extension-webview-W0qRfaAU.js → extension-webview-BaGvDk8v.js} +1 -1
  49. package/dist/web/assets/{file-store-yReVbR8q.js → file-store-m9crh6uq.js} +1 -1
  50. package/dist/web/assets/{fortran-fixed-form-BCH9DAIJ.js → fortran-fixed-form-B0bAufUe.js} +1 -1
  51. package/dist/web/assets/{fsharp-C93NYKYD.js → fsharp-C2AL2b1M.js} +1 -1
  52. package/dist/web/assets/{gdresource-BcIEE2eX.js → gdresource-DRQsDMNI.js} +1 -1
  53. package/dist/web/assets/{git-commit-BVjlXEy8.js → git-commit-GtBdnrzk.js} +1 -1
  54. package/dist/web/assets/{git-log-panel-2_4HuP47.js → git-log-panel-hwV1flSO.js} +1 -1
  55. package/dist/web/assets/{git-rebase-Bj_21B1V.js → git-rebase-h_0CGA6F.js} +1 -1
  56. package/dist/web/assets/gitGraph-HDMCJU4V-DAUrJCqi.js +1 -0
  57. package/dist/web/assets/glide-data-grid-YsN-xm6x.js +138 -0
  58. package/dist/web/assets/{glimmer-js-C20yj4Zv.js → glimmer-js-Bxwu93bS.js} +1 -1
  59. package/dist/web/assets/{glimmer-ts-ipbYC7kh.js → glimmer-ts-CIUYdR4E.js} +1 -1
  60. package/dist/web/assets/{glsl-Dovazu3t.js → glsl-CfxLI5jn.js} +1 -1
  61. package/dist/web/assets/{graphql-DpMlM7hC.js → graphql-CstqDPde.js} +1 -1
  62. package/dist/web/assets/group-chat-tab-B7mJO1gG.js +1 -0
  63. package/dist/web/assets/{hack-gc7LCBuV.js → hack-Bjqnssb5.js} +1 -1
  64. package/dist/web/assets/{haml-vtlFm9gm.js → haml-lVc-DfbM.js} +1 -1
  65. package/dist/web/assets/hand-x0ejq3fS.js +1 -0
  66. package/dist/web/assets/{handlebars-DGKhpCji.js → handlebars-Yy6fPQI_.js} +1 -1
  67. package/dist/web/assets/{html-derivative-C3oTSSCN.js → html-derivative-CchzCY1z.js} +1 -1
  68. package/dist/web/assets/{html-DvfhQbXb.js → html-lqtcy6-2.js} +1 -1
  69. package/dist/web/assets/{http-SG6zLTo7.js → http-ChclRrko.js} +1 -1
  70. package/dist/web/assets/{hurl-BWXsYlqW.js → hurl-CoOK7sXa.js} +1 -1
  71. package/dist/web/assets/{hxml-DUdMzaoh.js → hxml-fZSZ7Iyt.js} +1 -1
  72. package/dist/web/assets/{image-preview-DxgFOV1T.js → image-preview-ClAcqDlL.js} +1 -1
  73. package/dist/web/assets/index-CtJbMNNI.css +2 -0
  74. package/dist/web/assets/index-H_gScEOk.js +52 -0
  75. package/dist/web/assets/info-3K5VOQVL-Cx1ZfJbp.js +1 -0
  76. package/dist/web/assets/{input-DuAF-Stj.js → input-DXPgUtxi.js} +1 -1
  77. package/dist/web/assets/{jinja-955w9RKM.js → jinja-Y0w_KBdE.js} +1 -1
  78. package/dist/web/assets/{jison-WWfqPo50.js → jison-BILj3F2e.js} +1 -1
  79. package/dist/web/assets/{julia-BFrfnaNl.js → julia-DRfOPDts.js} +1 -1
  80. package/dist/web/assets/{just-BrZdyR_I.js → just-n3U_oODO.js} +1 -1
  81. package/dist/web/assets/keybindings-store-BqctreaI.js +1 -0
  82. package/dist/web/assets/{keybindings-store-0Wqfa7Qp.js → keybindings-store-D-CHdq-G.js} +1 -1
  83. package/dist/web/assets/keyboard-DBBaoQSu.js +1 -0
  84. package/dist/web/assets/{latex-CwtPPASD.js → latex-D6gztEOL.js} +1 -1
  85. package/dist/web/assets/{liquid-CYZuvDDd.js → liquid-DPqCEVb9.js} +1 -1
  86. package/dist/web/assets/{lua-BV095ZyY.js → lua-I40KPUm-.js} +1 -1
  87. package/dist/web/assets/markdown-renderer-BwOkiZC1.js +1 -0
  88. package/dist/web/assets/{markdown-renderer-CnRcKK3d.js → markdown-renderer-oJ9jPdQ2.js} +3 -3
  89. package/dist/web/assets/{marko-CNeljZ1C.js → marko-DMjsThSW.js} +1 -1
  90. package/dist/web/assets/{mdc-JfGapShy.js → mdc-CN2alsbo.js} +1 -1
  91. package/dist/web/assets/{media-url-Bm4PsduE.js → media-url-COUvasjn.js} +1 -1
  92. package/dist/web/assets/mobile-bottom-sheet-Cj1YBq-c.js +1 -0
  93. package/dist/web/assets/mobile-explorer-sheet-D1nM0Ik1.js +1 -0
  94. package/dist/web/assets/{nextflow-DD5C8nqX.js → nextflow-CGa4JeZ6.js} +1 -1
  95. package/dist/web/assets/{nginx-Ca7B__x_.js → nginx-CmqrIj84.js} +1 -1
  96. package/dist/web/assets/{nim-B8FogsDw.js → nim-CqhmTeMa.js} +1 -1
  97. package/dist/web/assets/notification-store-n-XEQ5pH.js +1 -0
  98. package/dist/web/assets/{number-overlay-editor-DG9xJwEG.js → number-overlay-editor-BEzJ1YeD.js} +1 -1
  99. package/dist/web/assets/{overview-panel-s53bnl0q.js → overview-panel-Bf9Gn8qV.js} +1 -1
  100. package/dist/web/assets/packet-RMMSAZCW-Bqwiy7dz.js +1 -0
  101. package/dist/web/assets/{panel-store-B4rijFKN.js → panel-store-UWCH6B-_.js} +1 -1
  102. package/dist/web/assets/{pdf-preview-DYt3aBzM.js → pdf-preview-BMHM5cR2.js} +1 -1
  103. package/dist/web/assets/{perl-BR78hS8R.js → perl-BOwpb6Tb.js} +1 -1
  104. package/dist/web/assets/{php-DXBOMC9z.js → php-wT9fVxxu.js} +1 -1
  105. package/dist/web/assets/pie-UPGHQEXC-BXSJ2God.js +1 -0
  106. package/dist/web/assets/{postgres-viewer-BvoWSuoZ.js → postgres-viewer-D8rfEY5b.js} +3 -3
  107. package/dist/web/assets/process-table-CsQ-hDJr.js +1 -0
  108. package/dist/web/assets/{project-store-BuY7rxn_.js → project-store-CKAOr23O.js} +1 -1
  109. package/dist/web/assets/{pug-CvTwVVKV.js → pug-DdlNnj-p.js} +1 -1
  110. package/dist/web/assets/{qml-CPSoZ1-B.js → qml-C5zBdydf.js} +1 -1
  111. package/dist/web/assets/radar-KQ55EAFF-D6XrM7HH.js +1 -0
  112. package/dist/web/assets/{razor-BUA1YKtF.js → razor-DqcJX4iq.js} +1 -1
  113. package/dist/web/assets/remote-desktop-mobile-sheet-CTiipn4k.js +2 -0
  114. package/dist/web/assets/remote-desktop-mobile-view-Cf5B0dKo.js +1 -0
  115. package/dist/web/assets/remote-desktop-stats-overlay-vhJBIW7s.js +1 -0
  116. package/dist/web/assets/remote-desktop-window-content-wCJUfI6G.js +1 -0
  117. package/dist/web/assets/{rst-BJsq7ISz.js → rst-DiMiMYUO.js} +1 -1
  118. package/dist/web/assets/{ruby-DSDMeYf5.js → ruby-DK62iflA.js} +1 -1
  119. package/dist/web/assets/{run-in-terminal-C9xpskFn.js → run-in-terminal-DJSL6nwZ.js} +1 -1
  120. package/dist/web/assets/{sas-B1nJOjbN.js → sas-6QPGJRa5.js} +1 -1
  121. package/dist/web/assets/{scroll-area-DBcG1fQo.js → scroll-area-DXGXeiRz.js} +1 -1
  122. package/dist/web/assets/{scss-BZFyRiQn.js → scss-DmUqh48_.js} +1 -1
  123. package/dist/web/assets/{send-to-chat-BIg70MUZ.js → send-to-chat-BOony0sO.js} +1 -1
  124. package/dist/web/assets/settings-store-BpcY5aYE.js +2 -0
  125. package/dist/web/assets/settings-tab-BVqRO01j.js +1 -0
  126. package/dist/web/assets/settings-tab-FH581A_i.js +3 -0
  127. package/dist/web/assets/{shaderlab-laINRXBo.js → shaderlab-DopFR1mi.js} +1 -1
  128. package/dist/web/assets/{shellsession-BpOOvWVR.js → shellsession-Bp_TGuXb.js} +1 -1
  129. package/dist/web/assets/{soy-BW4d91Sa.js → soy-DEB9KEFb.js} +1 -1
  130. package/dist/web/assets/{sparql-BsvvyoHn.js → sparql-C8lEwO1p.js} +1 -1
  131. package/dist/web/assets/{sql-query-editor-qQce5QJ6.js → sql-query-editor-MMfk0Kbz.js} +1 -1
  132. package/dist/web/assets/{sqlite-viewer-yhc0YNvu.js → sqlite-viewer-vglzrgnZ.js} +1 -1
  133. package/dist/web/assets/{stata-K-dZInUt.js → stata-DVep4apN.js} +1 -1
  134. package/dist/web/assets/{surrealql-CpNp2sgf.js → surrealql-fSlJApOA.js} +1 -1
  135. package/dist/web/assets/{svelte-BG6iBA8R.js → svelte-DezWBEDi.js} +1 -1
  136. package/dist/web/assets/{switch-_NcrktLe.js → switch-DqL_ZjB_.js} +1 -1
  137. package/dist/web/assets/system-monitor-body-BYUfyF8f.js +2 -0
  138. package/dist/web/assets/system-monitor-tab-CKGXB8lH.js +1 -0
  139. package/dist/web/assets/{system-monitor-window-content-CaOhD0AU.js → system-monitor-window-content-CiJ2yIsr.js} +1 -1
  140. package/dist/web/assets/tab-host-window-content-DYlXR0cg.js +1 -0
  141. package/dist/web/assets/{tab-store-tFnyPmm4.js → tab-store-D2thn4Vv.js} +1 -1
  142. package/dist/web/assets/team-member-sheet-Bnygu7Xw.js +2 -0
  143. package/dist/web/assets/{team-member-window-content-B1jkKkDi.js → team-member-window-content-qrgFTC96.js} +1 -1
  144. package/dist/web/assets/{templ-gxNKXmt3.js → templ-D0zB42a9.js} +1 -1
  145. package/dist/web/assets/{terminal-tab-UvwM0sty.js → terminal-tab-D_-R5rGm.js} +3 -3
  146. package/dist/web/assets/{tex-C0ON27g5.js → tex-BJiPijcH.js} +1 -1
  147. package/dist/web/assets/{tool-cards-jB7GT-wj.js → tool-cards-BW7Xc20J.js} +4 -4
  148. package/dist/web/assets/treemap-KZPCXAKY-BobWhcHN.js +1 -0
  149. package/dist/web/assets/{ts-tags-BX_CTVgQ.js → ts-tags-Dcr4u6y5.js} +1 -1
  150. package/dist/web/assets/{twig-CwL7XnU0.js → twig-B-SQU0bg.js} +1 -1
  151. package/dist/web/assets/{use-blob-url-DcRcjNTE.js → use-blob-url-BtPlqwLr.js} +1 -1
  152. package/dist/web/assets/use-monaco-theme-DQY1ZSaf.js +1 -0
  153. package/dist/web/assets/{use-open-team-member-1hRsHLKg.js → use-open-team-member-C5LQ_TCM.js} +1 -1
  154. package/dist/web/assets/use-visual-viewport-pVxLfE5B.js +1 -0
  155. package/dist/web/assets/{use-websocket-rxJLbFqy.js → use-websocket-l39Odhlp.js} +1 -1
  156. package/dist/web/assets/{vendor-mermaid-CHTa6D69.js → vendor-mermaid-CtDsSxbM.js} +1 -1
  157. package/dist/web/assets/{video-preview-DMhnf2Gi.js → video-preview-C5AhkIDl.js} +1 -1
  158. package/dist/web/assets/{vue-Bykm6wSS.js → vue-Cw10Sfbh.js} +1 -1
  159. package/dist/web/assets/{vue-html-CETdK_iV.js → vue-html-Cc5C_Z_w.js} +1 -1
  160. package/dist/web/assets/{vue-vine-DiJb6fO5.js → vue-vine-BuNMlnvu.js} +1 -1
  161. package/dist/web/assets/window-pip-registry-C7oz3Dfy.js +1 -0
  162. package/dist/web/assets/window-store-B5ZswdYg.js +1 -0
  163. package/dist/web/assets/{xml-BNw8J5YW.js → xml-DqT1wkky.js} +1 -1
  164. package/dist/web/assets/{xsl-CsG9L31b.js → xsl-DLkge3A2.js} +1 -1
  165. package/dist/web/index.html +45 -42
  166. package/dist/web/sw.js +1 -1
  167. package/docs/architecture/ai-chat-and-providers.md +3 -0
  168. package/docs/architecture/workspace-and-ui.md +8 -0
  169. package/docs/design-guidelines.md +3 -2
  170. package/package.json +1 -1
  171. package/src/cli/commands/backup-cmd.ts +119 -0
  172. package/src/index.ts +3 -0
  173. package/src/providers/claude-agent-sdk.ts +29 -1
  174. package/src/server/index.ts +42 -15
  175. package/src/server/routes/accounts.ts +30 -2
  176. package/src/server/routes/chat.ts +8 -1
  177. package/src/server/routes/named-tunnel.ts +5 -2
  178. package/src/server/routes/remote-desktop.ts +64 -0
  179. package/src/server/routes/settings.ts +1 -0
  180. package/src/server/ws/chat.ts +126 -7
  181. package/src/server/ws/remote-desktop.ts +86 -0
  182. package/src/services/account.service.ts +106 -23
  183. package/src/services/db-backup/db-backup-paths.ts +66 -0
  184. package/src/services/db-backup/db-backup-restore.ts +105 -0
  185. package/src/services/db-backup/db-backup-retention.ts +96 -0
  186. package/src/services/db-backup/db-backup-sync.ts +113 -0
  187. package/src/services/db-backup/db-backup.service.ts +61 -0
  188. package/src/services/db.service.ts +29 -1
  189. package/src/services/fs-credential-path-guard.ts +19 -1
  190. package/src/services/named-tunnel/cloudflared-login.service.ts +2 -2
  191. package/src/services/nested-subagent-spy.ts +206 -0
  192. package/src/services/remote-desktop/access-unit-assembler.ts +90 -0
  193. package/src/services/remote-desktop/avc1-codec-string.ts +18 -0
  194. package/src/services/remote-desktop/nal-splitter.ts +100 -0
  195. package/src/services/remote-desktop/remote-desktop-capture.ts +120 -0
  196. package/src/services/remote-desktop/remote-desktop-encoder-args.ts +42 -0
  197. package/src/services/remote-desktop/remote-desktop-flag.ts +10 -0
  198. package/src/services/remote-desktop/remote-desktop-input.ts +217 -0
  199. package/src/services/remote-desktop/remote-desktop-nonce.ts +38 -0
  200. package/src/services/remote-desktop/remote-desktop-session.ts +189 -0
  201. package/src/services/remote-desktop/remote-desktop-vk-map.ts +41 -0
  202. package/src/services/subagent-transcript-merger.ts +92 -75
  203. package/src/services/subprocess-retention.ts +29 -6
  204. package/src/services/supervisor.ts +58 -0
  205. package/src/services/team-member-activity/subagent-transcript-index.ts +56 -0
  206. package/src/types/chat.ts +2 -0
  207. package/src/web/app.tsx +8 -0
  208. package/src/web/components/chat/account-dialogs.tsx +11 -4
  209. package/src/web/components/chat/question-card.tsx +9 -2
  210. package/src/web/components/chat/usage-badge.tsx +17 -3
  211. package/src/web/components/floating-window/floating-window.tsx +8 -1
  212. package/src/web/components/floating-window/open-tab-in-pip.ts +78 -0
  213. package/src/web/components/floating-window/window-content-registry.ts +2 -0
  214. package/src/web/components/floating-window/window-pip-registry.ts +46 -0
  215. package/src/web/components/floating-window/window-store-types.ts +1 -1
  216. package/src/web/components/layout/mobile-drawer.tsx +38 -27
  217. package/src/web/components/layout/nav-section-rail.tsx +10 -2
  218. package/src/web/components/layout/tab-pop-out-menu-item.tsx +25 -2
  219. package/src/web/components/remote-desktop/open-remote-desktop.ts +26 -0
  220. package/src/web/components/remote-desktop/remote-desktop-coords.ts +98 -0
  221. package/src/web/components/remote-desktop/remote-desktop-frame-timestamp.ts +21 -0
  222. package/src/web/components/remote-desktop/remote-desktop-gesture-classifiers.ts +56 -0
  223. package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +131 -0
  224. package/src/web/components/remote-desktop/remote-desktop-mobile-sheet.tsx +43 -0
  225. package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +79 -0
  226. package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +190 -0
  227. package/src/web/components/remote-desktop/remote-desktop-stats-overlay.tsx +54 -0
  228. package/src/web/components/remote-desktop/remote-desktop-stats.ts +37 -0
  229. package/src/web/components/remote-desktop/remote-desktop-touch-gesture-tracker.ts +195 -0
  230. package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +72 -0
  231. package/src/web/components/remote-desktop/remote-desktop-ws-url.ts +22 -0
  232. package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +175 -0
  233. package/src/web/components/remote-desktop/use-remote-desktop-available.ts +29 -0
  234. package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +147 -0
  235. package/src/web/components/remote-desktop/use-remote-desktop-mobile-open-state.ts +18 -0
  236. package/src/web/components/remote-desktop/use-remote-desktop-touch.ts +224 -0
  237. package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +50 -0
  238. package/src/web/components/remote-desktop/use-remote-input-capture.ts +92 -0
  239. package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +59 -59
  240. package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +2 -2
  241. package/src/web/hooks/use-notification-badge.ts +19 -11
  242. package/src/web/lib/feature-badges.ts +2 -0
  243. package/src/web/stores/settings-store.ts +13 -0
  244. package/src/web/stores/streaming-store.ts +21 -2
  245. package/dist/web/assets/api-client-BogqsFna.js +0 -1
  246. package/dist/web/assets/api-settings-DfXViPKz.js +0 -1
  247. package/dist/web/assets/architecture-PBZL5I3N-BzFm9ELu.js +0 -1
  248. package/dist/web/assets/chat-tab-DzK8LGti.js +0 -15
  249. package/dist/web/assets/code-editor-CuMfPN9X.js +0 -10
  250. package/dist/web/assets/diff-viewer-eaUBjLCH.js +0 -4
  251. package/dist/web/assets/explorer-body-DkCkSVrN.js +0 -2
  252. package/dist/web/assets/explorer-window-content-MsnTVngu.js +0 -1
  253. package/dist/web/assets/gitGraph-HDMCJU4V-B72vcb2r.js +0 -1
  254. package/dist/web/assets/glide-data-grid-B0IY6n_Q.js +0 -138
  255. package/dist/web/assets/group-chat-tab-DRLDHOp6.js +0 -1
  256. package/dist/web/assets/index-CBn4SOoT.js +0 -52
  257. package/dist/web/assets/index-DWpNR3nB.css +0 -2
  258. package/dist/web/assets/info-3K5VOQVL-3irPxRK_.js +0 -1
  259. package/dist/web/assets/keybindings-store-d54pv2NK.js +0 -1
  260. package/dist/web/assets/markdown-renderer-BTS8ifdu.js +0 -1
  261. package/dist/web/assets/mobile-bottom-sheet-M5iZDX7y.js +0 -1
  262. package/dist/web/assets/mobile-explorer-sheet-BqK2-WuN.js +0 -1
  263. package/dist/web/assets/notification-store-Yvy1I8gd.js +0 -1
  264. package/dist/web/assets/packet-RMMSAZCW-Z5jWjFCV.js +0 -1
  265. package/dist/web/assets/pie-UPGHQEXC-Bq7UMi8P.js +0 -1
  266. package/dist/web/assets/process-table-BYwKigiD.js +0 -1
  267. package/dist/web/assets/radar-KQ55EAFF-BLo4BOTD.js +0 -1
  268. package/dist/web/assets/settings-store-GV5pkkke.js +0 -2
  269. package/dist/web/assets/settings-tab-BePvIcjd.js +0 -3
  270. package/dist/web/assets/settings-tab-Br-lJSPa.js +0 -1
  271. package/dist/web/assets/system-monitor-body-D8Z5QXBB.js +0 -2
  272. package/dist/web/assets/system-monitor-tab-BclYXFTe.js +0 -1
  273. package/dist/web/assets/tab-host-window-content-f_Rc5rJK.js +0 -1
  274. package/dist/web/assets/team-member-sheet-QDHoYCs0.js +0 -2
  275. package/dist/web/assets/treemap-KZPCXAKY-dyrE6SW2.js +0 -1
  276. package/dist/web/assets/use-monaco-theme--9tOOkDA.js +0 -1
  277. package/dist/web/assets/window-store-CQ1lghvv.js +0 -1
  278. /package/dist/web/assets/{abap-DGlrX_8G.js → abap-CKgBNk5K.js} +0 -0
  279. /package/dist/web/assets/{actionscript-3-CUqRtUJE.js → actionscript-3-oARhAY6c.js} +0 -0
  280. /package/dist/web/assets/{ada-CY3Spa33.js → ada-2vi8ASy3.js} +0 -0
  281. /package/dist/web/assets/{andromeeda-Lxs2TJn7.js → andromeeda-B3vMjPKd.js} +0 -0
  282. /package/dist/web/assets/{apache-DfxgGdgM.js → apache-DnIEr338.js} +0 -0
  283. /package/dist/web/assets/{apex-QVDF1dnI.js → apex-B7Qs5AY5.js} +0 -0
  284. /package/dist/web/assets/{api-client-oY1tC3tM.js → api-client-BkGKTjsq.js} +0 -0
  285. /package/dist/web/assets/{applescript-pwJ8QXh-.js → applescript-EbQNUCWb.js} +0 -0
  286. /package/dist/web/assets/{apply-theme-B_bMVL6a.js → apply-theme-Dd_cqGa-.js} +0 -0
  287. /package/dist/web/assets/{ara-mPjeX5YX.js → ara-CtHdhFRw.js} +0 -0
  288. /package/dist/web/assets/{asciidoc-Bf7LsDkI.js → asciidoc-BH6qin_O.js} +0 -0
  289. /package/dist/web/assets/{asm-rRKC1OUx.js → asm-D1XU1ACk.js} +0 -0
  290. /package/dist/web/assets/{aurora-x-DwQr0DkG.js → aurora-x-BkhcxkuS.js} +0 -0
  291. /package/dist/web/assets/{awk-Bpt768fN.js → awk-DijzB6o7.js} +0 -0
  292. /package/dist/web/assets/{ayu-dark-D9lwQHMs.js → ayu-dark-tysG77WC.js} +0 -0
  293. /package/dist/web/assets/{ayu-light-B4-ptrU5.js → ayu-light-Dg0WMETC.js} +0 -0
  294. /package/dist/web/assets/{ayu-mirage-DAyRzH5Q.js → ayu-mirage--RsJCjC4.js} +0 -0
  295. /package/dist/web/assets/{ballerina-Cgdpj1HC.js → ballerina-DT_5-l82.js} +0 -0
  296. /package/dist/web/assets/{bat-4FrN36S2.js → bat-BRpDlkv0.js} +0 -0
  297. /package/dist/web/assets/{beancount-DkcxQER3.js → beancount--8Df-WGG.js} +0 -0
  298. /package/dist/web/assets/{berry-Dnuut-Ml.js → berry-Bc9fo35J.js} +0 -0
  299. /package/dist/web/assets/{bibtex-CCDwmbuA.js → bibtex-CwyBpp9X.js} +0 -0
  300. /package/dist/web/assets/{bicep-BC0ryoGM.js → bicep-DiyH2Bs7.js} +0 -0
  301. /package/dist/web/assets/{bird2-C50V_A-0.js → bird2-CHgK4a2A.js} +0 -0
  302. /package/dist/web/assets/{c-Ddorhq0U.js → c-BM1qL6-f.js} +0 -0
  303. /package/dist/web/assets/{c3-DdIf5r9E.js → c3-D2oOjrP7.js} +0 -0
  304. /package/dist/web/assets/{cadence-s9pd1Wng.js → cadence-nS45wqF6.js} +0 -0
  305. /package/dist/web/assets/{can-open-in-ppm-C6hZV1OP.js → can-open-in-ppm-DwP9KU8o.js} +0 -0
  306. /package/dist/web/assets/{catppuccin-frappe-CzJNTyu-.js → catppuccin-frappe-BpnJHiCj.js} +0 -0
  307. /package/dist/web/assets/{catppuccin-latte-BNpt-m7s.js → catppuccin-latte-Bb95Wj3h.js} +0 -0
  308. /package/dist/web/assets/{catppuccin-macchiato-FYGkfsJ5.js → catppuccin-macchiato-Btx8c17W.js} +0 -0
  309. /package/dist/web/assets/{catppuccin-mocha-it2g5D1q.js → catppuccin-mocha-B4DZOUGI.js} +0 -0
  310. /package/dist/web/assets/{clarity-BVFBTjMe.js → clarity-1ps69jU6.js} +0 -0
  311. /package/dist/web/assets/{clipboard-BWznymz7.js → clipboard-CrHX8AYw.js} +0 -0
  312. /package/dist/web/assets/{clojure-BSYvXMI8.js → clojure-BklC0hKV.js} +0 -0
  313. /package/dist/web/assets/{cmake-DGEqH-Np.js → cmake-B573LYAQ.js} +0 -0
  314. /package/dist/web/assets/{codeowners-D8FCOxWg.js → codeowners-BoYeX89M.js} +0 -0
  315. /package/dist/web/assets/{codeql-COV0eluG.js → codeql-7Dhf5Udk.js} +0 -0
  316. /package/dist/web/assets/{common-lisp-xHnZUlgt.js → common-lisp-m1nxlthv.js} +0 -0
  317. /package/dist/web/assets/{coq-Cs_2i3Xg.js → coq-DNhGmd4a.js} +0 -0
  318. /package/dist/web/assets/{csharp-B1ONB4PT.js → csharp-P-nAurtS.js} +0 -0
  319. /package/dist/web/assets/{css-BgGQbvrZ.js → css-LYbSeh3-.js} +0 -0
  320. /package/dist/web/assets/{csv-CNHmQBzs.js → csv-DHZZZm7X.js} +0 -0
  321. /package/dist/web/assets/{csv-parser-DWR24J17.js → csv-parser-CY9Ggtg5.js} +0 -0
  322. /package/dist/web/assets/{cue-ZXAKzwIy.js → cue-D8yLUOle.js} +0 -0
  323. /package/dist/web/assets/{cypher-4PL5bmT5.js → cypher-9cbW8RYA.js} +0 -0
  324. /package/dist/web/assets/{d-CEsV3RT2.js → d-ZihzhxqI.js} +0 -0
  325. /package/dist/web/assets/{dark-plus-NtFnYSDz.js → dark-plus-C8xIA1u3.js} +0 -0
  326. /package/dist/web/assets/{dart-CKHn4mDf.js → dart-CBm6jKc7.js} +0 -0
  327. /package/dist/web/assets/{data-grid-types-CxU8oo-j.js → data-grid-types-DkEQ-Na9.js} +0 -0
  328. /package/dist/web/assets/{dax-Cmkj1I_M.js → dax-B3cx6YeV.js} +0 -0
  329. /package/dist/web/assets/{desktop-DJLalvPQ.js → desktop-C03SpXth.js} +0 -0
  330. /package/dist/web/assets/{diff-BnS2wpj6.js → diff-D8LnnvFY.js} +0 -0
  331. /package/dist/web/assets/{dist-uYjs3Wv6.js → dist-C4XoS_zo.js} +0 -0
  332. /package/dist/web/assets/{dist-CuIdRyUB.js → dist-Do3fljss.js} +0 -0
  333. /package/dist/web/assets/{docker-CovNy7ml.js → docker-DwFiOQWW.js} +0 -0
  334. /package/dist/web/assets/{dotenv-Gh4w3I6B.js → dotenv-CVJ_vFSQ.js} +0 -0
  335. /package/dist/web/assets/{dracula-NF0tAHXl.js → dracula-DMw8UACY.js} +0 -0
  336. /package/dist/web/assets/{dracula-soft-Cj_qBurK.js → dracula-soft-BnB3nDrA.js} +0 -0
  337. /package/dist/web/assets/{dream-maker-CRuZPcl7.js → dream-maker-2riXUQXo.js} +0 -0
  338. /package/dist/web/assets/{emacs-lisp-BACmC9qj.js → emacs-lisp-BQdYaiB5.js} +0 -0
  339. /package/dist/web/assets/{everforest-dark-Dd9av4QG.js → everforest-dark-C2FPIWdj.js} +0 -0
  340. /package/dist/web/assets/{everforest-light-C-jsss-B.js → everforest-light-DMOWf9sD.js} +0 -0
  341. /package/dist/web/assets/{fennel-BJA9XnL8.js → fennel-JtSHp0vW.js} +0 -0
  342. /package/dist/web/assets/{fish-CKlhNSc2.js → fish-CflDcRfe.js} +0 -0
  343. /package/dist/web/assets/{fluent-wfglRk5D.js → fluent-D4eu6Vac.js} +0 -0
  344. /package/dist/web/assets/{format-bytes-utwHMTry.js → format-bytes-DR3228bu.js} +0 -0
  345. /package/dist/web/assets/{fortran-free-form-BlXCdid3.js → fortran-free-form-hO4RuC9Z.js} +0 -0
  346. /package/dist/web/assets/{gdscript-BwAVG6Et.js → gdscript-D7n3a8VM.js} +0 -0
  347. /package/dist/web/assets/{gdshader-DPc_T7ib.js → gdshader-BhUmrlzG.js} +0 -0
  348. /package/dist/web/assets/{genie-B-zZSLCX.js → genie-VbyftqBp.js} +0 -0
  349. /package/dist/web/assets/{gherkin-CJ0ajpGs.js → gherkin-BZzhvUUb.js} +0 -0
  350. /package/dist/web/assets/{github-dark-Dz8ZkzSP.js → github-dark-C4q41woW.js} +0 -0
  351. /package/dist/web/assets/{github-dark-default-CNUYo_kp.js → github-dark-default-CMNi7dQX.js} +0 -0
  352. /package/dist/web/assets/{github-dark-dimmed-241z_Ycx.js → github-dark-dimmed-BRf_-LlY.js} +0 -0
  353. /package/dist/web/assets/{github-dark-high-contrast-7r6yZZYH.js → github-dark-high-contrast-xanKBsRI.js} +0 -0
  354. /package/dist/web/assets/{github-light-C8gmZn-D.js → github-light-BEQeNQCs.js} +0 -0
  355. /package/dist/web/assets/{github-light-default-GAEgq1eA.js → github-light-default-ceQrtdXg.js} +0 -0
  356. /package/dist/web/assets/{github-light-high-contrast-3z91352E.js → github-light-high-contrast-doKbXzgW.js} +0 -0
  357. /package/dist/web/assets/{gleam-C7vxW5zd.js → gleam-Chzqf4fT.js} +0 -0
  358. /package/dist/web/assets/{gn-CyJIXwHP.js → gn-CCN_-6fp.js} +0 -0
  359. /package/dist/web/assets/{gnuplot-CqLbGgPd.js → gnuplot-CIplgwwP.js} +0 -0
  360. /package/dist/web/assets/{go-DdDcK9aX.js → go-Cjj-9dRs.js} +0 -0
  361. /package/dist/web/assets/{groovy-UYHggEqH.js → groovy-PqD4K7X1.js} +0 -0
  362. /package/dist/web/assets/{gruvbox-dark-hard-B4ierkJn.js → gruvbox-dark-hard-BAbpUSF-.js} +0 -0
  363. /package/dist/web/assets/{gruvbox-dark-medium-DYwkDSZ6.js → gruvbox-dark-medium-BWcWXDBB.js} +0 -0
  364. /package/dist/web/assets/{gruvbox-dark-soft-CVL4P-Lz.js → gruvbox-dark-soft-Dkx1okkk.js} +0 -0
  365. /package/dist/web/assets/{gruvbox-light-hard-BJ0LewUZ.js → gruvbox-light-hard-WVsDK0MM.js} +0 -0
  366. /package/dist/web/assets/{gruvbox-light-medium-CaqqjeGJ.js → gruvbox-light-medium-tiR9t7gr.js} +0 -0
  367. /package/dist/web/assets/{gruvbox-light-soft-ByKMoVNr.js → gruvbox-light-soft-HzHohaX6.js} +0 -0
  368. /package/dist/web/assets/{haskell-BilYRVPp.js → haskell-B1eIZfr6.js} +0 -0
  369. /package/dist/web/assets/{haxe-Brs9Bj1s.js → haxe-BcDdi5xr.js} +0 -0
  370. /package/dist/web/assets/{hcl-ClrZpol-.js → hcl-Dq-HKVni.js} +0 -0
  371. /package/dist/web/assets/{hjson-9PvHnYMX.js → hjson-Dr9D9WVU.js} +0 -0
  372. /package/dist/web/assets/{hlsl-BbBk4vqV.js → hlsl-Bo8t2FuZ.js} +0 -0
  373. /package/dist/web/assets/{horizon-E5Py0cny.js → horizon-DWG5IXc-.js} +0 -0
  374. /package/dist/web/assets/{horizon-bright-DVCsNGEk.js → horizon-bright-DFr9wTTx.js} +0 -0
  375. /package/dist/web/assets/{houston-JdFEax9Y.js → houston-NAVNhkup.js} +0 -0
  376. /package/dist/web/assets/{hy-W_W3tLKT.js → hy-DnyvUk3B.js} +0 -0
  377. /package/dist/web/assets/{image-gallery-5IURd6db.js → image-gallery-DoFdNF9i.js} +0 -0
  378. /package/dist/web/assets/{imba-CfAqJz7T.js → imba-CPbR5W_A.js} +0 -0
  379. /package/dist/web/assets/{ini-C_3gvKWs.js → ini-lc8_Lt0y.js} +0 -0
  380. /package/dist/web/assets/{java-CKqtXZ-j.js → java-B0KCo2vK.js} +0 -0
  381. /package/dist/web/assets/{javascript-C7gSEfae.js → javascript-YqEbNRpd.js} +0 -0
  382. /package/dist/web/assets/{json-BoN6e11T.js → json-Co0WtOlh.js} +0 -0
  383. /package/dist/web/assets/{json5-CgE7C8HW.js → json5-2Jhb0QUN.js} +0 -0
  384. /package/dist/web/assets/{jsonc-DjyoattY.js → jsonc-e72F11nU.js} +0 -0
  385. /package/dist/web/assets/{jsonl-lI82wGSw.js → jsonl-Bff8IPye.js} +0 -0
  386. /package/dist/web/assets/{jsonnet-CszEI8va.js → jsonnet-DRCkb-_H.js} +0 -0
  387. /package/dist/web/assets/{jssm-CZXDlszb.js → jssm-C6UiwVEm.js} +0 -0
  388. /package/dist/web/assets/{jsx-BeRz0eEz.js → jsx-u1aZlnfA.js} +0 -0
  389. /package/dist/web/assets/{kanagawa-dragon-BOmnUiyd.js → kanagawa-dragon-De-RQFV3.js} +0 -0
  390. /package/dist/web/assets/{kanagawa-lotus-ffU_P5w-.js → kanagawa-lotus-K8elE4QU.js} +0 -0
  391. /package/dist/web/assets/{kanagawa-wave-DKTJZrMu.js → kanagawa-wave-BrStl_zx.js} +0 -0
  392. /package/dist/web/assets/{kdl-BWECJveH.js → kdl-CLCfMeZy.js} +0 -0
  393. /package/dist/web/assets/{kotlin-DpVwBCSt.js → kotlin-DEEC3pNh.js} +0 -0
  394. /package/dist/web/assets/{kusto-BgVqjUZA.js → kusto-DTqRhCup.js} +0 -0
  395. /package/dist/web/assets/{laserwave-CEVCi8em.js → laserwave-DsgpUSbA.js} +0 -0
  396. /package/dist/web/assets/{lean-CGyfRhLJ.js → lean-Dl9MI0Y3.js} +0 -0
  397. /package/dist/web/assets/{less-B0_H10Ku.js → less-Bg21rao6.js} +0 -0
  398. /package/dist/web/assets/{lib-hrC3bruJ.js → lib-CXUwxw1M.js} +0 -0
  399. /package/dist/web/assets/{libesm-DVpFmwJu.js → libesm-CVkncAQz.js} +0 -0
  400. /package/dist/web/assets/{light-plus-DqzMqPID.js → light-plus-DGNbbK1V.js} +0 -0
  401. /package/dist/web/assets/{llvm-9ucnfNiF.js → llvm-91_ZX3p-.js} +0 -0
  402. /package/dist/web/assets/{loader-circle-Dpqcv0eS.js → loader-circle-DouKgIXy.js} +0 -0
  403. /package/dist/web/assets/{lock-DldbT31x.js → lock-CNxUGpTk.js} +0 -0
  404. /package/dist/web/assets/{log-m6A2dOSj.js → log-DSXzbq8u.js} +0 -0
  405. /package/dist/web/assets/{logo-ByPcsC9P.js → logo-BQRNd4tc.js} +0 -0
  406. /package/dist/web/assets/{luau-DFCo8nBn.js → luau-BE-TMpiZ.js} +0 -0
  407. /package/dist/web/assets/{make-C90w3MeA.js → make-ePYGKTpc.js} +0 -0
  408. /package/dist/web/assets/{markdown-DT0uywZG.js → markdown-Cst3wgMW.js} +0 -0
  409. /package/dist/web/assets/{material-theme-BVUO93tJ.js → material-theme-9XyvnNLb.js} +0 -0
  410. /package/dist/web/assets/{material-theme-darker-xXgGDwnF.js → material-theme-darker-H1VN4MTS.js} +0 -0
  411. /package/dist/web/assets/{material-theme-lighter-BjStPzWB.js → material-theme-lighter-DAreBbuF.js} +0 -0
  412. /package/dist/web/assets/{material-theme-ocean-wFmSiKjy.js → material-theme-ocean-BpTR-3II.js} +0 -0
  413. /package/dist/web/assets/{material-theme-palenight-e-0CtRoe.js → material-theme-palenight-DUNzX711.js} +0 -0
  414. /package/dist/web/assets/{matlab-BNaNQAUH.js → matlab-BpYNufBU.js} +0 -0
  415. /package/dist/web/assets/{mdx-DfpVWAqO.js → mdx-BYjfe1DK.js} +0 -0
  416. /package/dist/web/assets/{mermaid-Bpq7wFCP.js → mermaid-DPCcIW9B.js} +0 -0
  417. /package/dist/web/assets/{min-dark-CL2Y0WH6.js → min-dark-DnGOAGCo.js} +0 -0
  418. /package/dist/web/assets/{min-light-DOgr_IdP.js → min-light-CwV-o31y.js} +0 -0
  419. /package/dist/web/assets/{mipsasm-C9ie2zlB.js → mipsasm-DdeCPC97.js} +0 -0
  420. /package/dist/web/assets/{mojo-Md2A2sJy.js → mojo-BmxxZ3iM.js} +0 -0
  421. /package/dist/web/assets/{monitor-4vWfqzuN.js → monitor-C835ucWH.js} +0 -0
  422. /package/dist/web/assets/{monokai-nbXIEySB.js → monokai-Cx6cQ-MA.js} +0 -0
  423. /package/dist/web/assets/{moonbit-CJ-T-s4A.js → moonbit-D2-Gj5vj.js} +0 -0
  424. /package/dist/web/assets/{move-DwEehLIK.js → move-DSWGoQa8.js} +0 -0
  425. /package/dist/web/assets/{narrat-CuF4sIKr.js → narrat-D9TnZSSu.js} +0 -0
  426. /package/dist/web/assets/{nextflow-groovy-1dYhfPfq.js → nextflow-groovy-BbL-8PwD.js} +0 -0
  427. /package/dist/web/assets/{night-owl-gZS5mfzY.js → night-owl-CZitKm5d.js} +0 -0
  428. /package/dist/web/assets/{night-owl-light-DDAster5.js → night-owl-light-DJVX1Pno.js} +0 -0
  429. /package/dist/web/assets/{nix-BlbEYs2c.js → nix-R6hr3oln.js} +0 -0
  430. /package/dist/web/assets/{nord-BRiPb8Vy.js → nord-ChGHya3l.js} +0 -0
  431. /package/dist/web/assets/{nushell-DgWicB1i.js → nushell-QQWB-GWx.js} +0 -0
  432. /package/dist/web/assets/{objective-c-D8mPZ42Y.js → objective-c-C0pZ6y73.js} +0 -0
  433. /package/dist/web/assets/{objective-cpp-B0pNXzuj.js → objective-cpp-BdzVM31a.js} +0 -0
  434. /package/dist/web/assets/{ocaml-lQd7aK5d.js → ocaml-D4KktNlS.js} +0 -0
  435. /package/dist/web/assets/{odin-CxtIOBjm.js → odin-Y-fypmGT.js} +0 -0
  436. /package/dist/web/assets/{one-dark-pro-T_UFnWfe.js → one-dark-pro-BwecIxQL.js} +0 -0
  437. /package/dist/web/assets/{one-light-LBVqwyV-.js → one-light-Cz7UVzLU.js} +0 -0
  438. /package/dist/web/assets/{openscad-C5dnS6J7.js → openscad-DtRe3qYD.js} +0 -0
  439. /package/dist/web/assets/{pascal-CmWCWQML.js → pascal-Ck6I1fGG.js} +0 -0
  440. /package/dist/web/assets/{pkl-BWWOtDQk.js → pkl-DdFK94VW.js} +0 -0
  441. /package/dist/web/assets/{plastic-ekhupckF.js → plastic-BQSAzZwR.js} +0 -0
  442. /package/dist/web/assets/{play-CpCAoQRZ.js → play-CJlWpVMw.js} +0 -0
  443. /package/dist/web/assets/{plsql-BPsDHXx4.js → plsql-DjXg_CP0.js} +0 -0
  444. /package/dist/web/assets/{po-BX2FiuW2.js → po-Bw2SuM4r.js} +0 -0
  445. /package/dist/web/assets/{poimandres-DzXOdZcN.js → poimandres-YMK6c5VN.js} +0 -0
  446. /package/dist/web/assets/{polar-DDRXgJzG.js → polar-BMu17-O0.js} +0 -0
  447. /package/dist/web/assets/{portal-container-context-a399Hd9F.js → portal-container-context-DSGdlXrJ.js} +0 -0
  448. /package/dist/web/assets/{postcss-BHW6XBhx.js → postcss-DWib10RZ.js} +0 -0
  449. /package/dist/web/assets/{powerquery-B89c2NBb.js → powerquery-Dht4CK0b.js} +0 -0
  450. /package/dist/web/assets/{powershell-Dvxrd3Tu.js → powershell-DvrCg8ji.js} +0 -0
  451. /package/dist/web/assets/{prisma-lMK8EFLU.js → prisma-DbodYvCM.js} +0 -0
  452. /package/dist/web/assets/{prolog-D0YrtMG4.js → prolog-DZ-m6ysB.js} +0 -0
  453. /package/dist/web/assets/{proto-J0mM25aw.js → proto-HHpG3tGO.js} +0 -0
  454. /package/dist/web/assets/{puppet-CUT0xkhh.js → puppet-BljHe_75.js} +0 -0
  455. /package/dist/web/assets/{purescript-CoJWSGRp.js → purescript-DzCiFR0B.js} +0 -0
  456. /package/dist/web/assets/{python-BBrmuz6P.js → python-B5BQ27cC.js} +0 -0
  457. /package/dist/web/assets/{qmldir-CUtzs4Q7.js → qmldir-CYlcXrnn.js} +0 -0
  458. /package/dist/web/assets/{qss-Dm1RZ8aL.js → qss-Ce7T_TEg.js} +0 -0
  459. /package/dist/web/assets/{r-BGP7qZTe.js → r-Bku-2Jp_.js} +0 -0
  460. /package/dist/web/assets/{racket-K_6U1lqJ.js → racket-DtfSL3WR.js} +0 -0
  461. /package/dist/web/assets/{raku-bNSiZeW_.js → raku-CnDLsBJr.js} +0 -0
  462. /package/dist/web/assets/{react-X4jUntEf.js → react-Dfio3PfK.js} +0 -0
  463. /package/dist/web/assets/{red-uVeIEF0f.js → red-vbqcE9b-.js} +0 -0
  464. /package/dist/web/assets/{refresh-cw-B9iv7csb.js → refresh-cw-84mz8aOE.js} +0 -0
  465. /package/dist/web/assets/{reg-DSm9GWjh.js → reg-DIDLT8-A.js} +0 -0
  466. /package/dist/web/assets/{regexp-CljRm_j_.js → regexp-CB4h-VPu.js} +0 -0
  467. /package/dist/web/assets/{rel-d3JcopZg.js → rel-CiQEnWXa.js} +0 -0
  468. /package/dist/web/assets/{resolve-theme-DLKmm-lE.js → resolve-theme-DPTQHAy1.js} +0 -0
  469. /package/dist/web/assets/{riscv-CJmaELjT.js → riscv-R78iq5Rf.js} +0 -0
  470. /package/dist/web/assets/{ron-D0g9B1rW.js → ron-DhYENxc4.js} +0 -0
  471. /package/dist/web/assets/{rose-pine-DOrD2BSH.js → rose-pine-5wBeyjfJ.js} +0 -0
  472. /package/dist/web/assets/{rose-pine-dawn-CzZQ-LCF.js → rose-pine-dawn-FpKtPCuH.js} +0 -0
  473. /package/dist/web/assets/{rose-pine-moon-CdmsYp7C.js → rose-pine-moon-BwcIYc7C.js} +0 -0
  474. /package/dist/web/assets/{rosmsg-CmDth6pO.js → rosmsg-DWIGAYMl.js} +0 -0
  475. /package/dist/web/assets/{rotate-ccw-DnuPzJRE.js → rotate-ccw-CNTk2J7l.js} +0 -0
  476. /package/dist/web/assets/{rust-DGdP6jiR.js → rust-DU_ofKgt.js} +0 -0
  477. /package/dist/web/assets/{sass-Dkgt2FKZ.js → sass-U9SUuYEy.js} +0 -0
  478. /package/dist/web/assets/{save-DxonD4y1.js → save-CtW9ojKk.js} +0 -0
  479. /package/dist/web/assets/{scala-BQLTON6C.js → scala-op4gjA6j.js} +0 -0
  480. /package/dist/web/assets/{scheme-DKlxUGCi.js → scheme-tEjkR8rL.js} +0 -0
  481. /package/dist/web/assets/{sdbl-C3iZJQeZ.js → sdbl-C6SSRmZg.js} +0 -0
  482. /package/dist/web/assets/{send-B-t5Mz26.js → send-C_sLFt56.js} +0 -0
  483. /package/dist/web/assets/{shallow-jwPzEunk.js → shallow-C1YoJQA-.js} +0 -0
  484. /package/dist/web/assets/{shellscript-BRu-ZQAx.js → shellscript-Bf2KiwQj.js} +0 -0
  485. /package/dist/web/assets/{shield-check-CdTiK1j2.js → shield-check-Du6HKYn9.js} +0 -0
  486. /package/dist/web/assets/{shield-off-DuetkXKM.js → shield-off-CWKb1MQL.js} +0 -0
  487. /package/dist/web/assets/{slack-dark-qkm_u2zz.js → slack-dark-oGgCey8r.js} +0 -0
  488. /package/dist/web/assets/{slack-ochin-BP8_8e4m.js → slack-ochin-CemlB8cE.js} +0 -0
  489. /package/dist/web/assets/{smalltalk-Bnz4gt07.js → smalltalk-CTE-6jrZ.js} +0 -0
  490. /package/dist/web/assets/{snazzy-light-fbPZg5io.js → snazzy-light-DoEIGnmz.js} +0 -0
  491. /package/dist/web/assets/{solarized-dark-CM0vTRoi.js → solarized-dark-CStiJyBg.js} +0 -0
  492. /package/dist/web/assets/{solarized-light-C4_W9mvs.js → solarized-light-CfuNycS9.js} +0 -0
  493. /package/dist/web/assets/{solidity-Bz6v1PoD.js → solidity-DL6sO5rd.js} +0 -0
  494. /package/dist/web/assets/{splunk-DyNxu-0i.js → splunk-yK4ZUFBB.js} +0 -0
  495. /package/dist/web/assets/{sql-DXRL1gw1.js → sql-Lw03eoN8.js} +0 -0
  496. /package/dist/web/assets/{ssh-config-2z1QHuOG.js → ssh-config-DeRp25TR.js} +0 -0
  497. /package/dist/web/assets/{stylus-D4I-VaeY.js → stylus-D8OMsPh-.js} +0 -0
  498. /package/dist/web/assets/{swift-C_2G-tIc.js → swift-nsl9stPV.js} +0 -0
  499. /package/dist/web/assets/{synthwave-84-osBjChRP.js → synthwave-84-exAgsgpf.js} +0 -0
  500. /package/dist/web/assets/{system-verilog-zQP2qPNU.js → system-verilog-DBWf00T6.js} +0 -0
  501. /package/dist/web/assets/{systemd-UGfjRSBM.js → systemd-nhu27Dw5.js} +0 -0
  502. /package/dist/web/assets/{table-FLU8ei8b.js → table-GAR4upb6.js} +0 -0
  503. /package/dist/web/assets/{talonscript-C1XrFPwo.js → talonscript-CNOEcGDc.js} +0 -0
  504. /package/dist/web/assets/{tasl-B5raVn43.js → tasl-CjYZenSH.js} +0 -0
  505. /package/dist/web/assets/{tcl-3-vbeMIc.js → tcl-BVA09RDz.js} +0 -0
  506. /package/dist/web/assets/{terraform-a8VXZDm9.js → terraform-C5EloH1q.js} +0 -0
  507. /package/dist/web/assets/{text-wrap-B05VGRa-.js → text-wrap-D71Jqz5U.js} +0 -0
  508. /package/dist/web/assets/{tokyo-night-4SRhf-Hz.js → tokyo-night-D764DdhO.js} +0 -0
  509. /package/dist/web/assets/{toml-B4N0i8wT.js → toml-CCDYblSB.js} +0 -0
  510. /package/dist/web/assets/{trash-2-CTXlTflF.js → trash-2-C08fhIg1.js} +0 -0
  511. /package/dist/web/assets/{triangle-alert-BRq0VAfd.js → triangle-alert-85BQgs3T.js} +0 -0
  512. /package/dist/web/assets/{tsv-DA5k9Lvw.js → tsv-BZ4sA05e.js} +0 -0
  513. /package/dist/web/assets/{tsx-D9SZ8owC.js → tsx-DPLBzAt5.js} +0 -0
  514. /package/dist/web/assets/{turtle-BTHTvyY_.js → turtle-CwGHCq40.js} +0 -0
  515. /package/dist/web/assets/{typescript-DYzV9p5L.js → typescript-CUH0vBAl.js} +0 -0
  516. /package/dist/web/assets/{typespec-BC15eh7F.js → typespec-Bq31Vq4u.js} +0 -0
  517. /package/dist/web/assets/{typst-BJUqzA6D.js → typst-MD9BR9W5.js} +0 -0
  518. /package/dist/web/assets/{use-is-mobile-DFWViA4S.js → use-is-mobile-DUuki97q.js} +0 -0
  519. /package/dist/web/assets/{utils-C9OjCrou.js → utils-u2fbYPrY.js} +0 -0
  520. /package/dist/web/assets/{v-50Lps6oA.js → v-DZV7IxIT.js} +0 -0
  521. /package/dist/web/assets/{vala-8wiUvvrD.js → vala-urd1zi9S.js} +0 -0
  522. /package/dist/web/assets/{vb-BlgPLt6B.js → vb-DIZU3zbJ.js} +0 -0
  523. /package/dist/web/assets/{vendor-xterm-BZOrwF_F.js → vendor-xterm-DCj0zgSC.js} +0 -0
  524. /package/dist/web/assets/{verilog-CD9pVW5p.js → verilog-Ndz_ciTA.js} +0 -0
  525. /package/dist/web/assets/{vesper-BrivW4PT.js → vesper-C86M7LJp.js} +0 -0
  526. /package/dist/web/assets/{vhdl-Bunxfa7N.js → vhdl-_klwDUjd.js} +0 -0
  527. /package/dist/web/assets/{viml-DndUb2qM.js → viml-DzRXWoQh.js} +0 -0
  528. /package/dist/web/assets/{vitesse-black-BurZSkxW.js → vitesse-black-TWZ20_3U.js} +0 -0
  529. /package/dist/web/assets/{vitesse-dark-8UShspzC.js → vitesse-dark-Dl7R6fy9.js} +0 -0
  530. /package/dist/web/assets/{vitesse-light-DghCAZ90.js → vitesse-light-Cs0Dt2_n.js} +0 -0
  531. /package/dist/web/assets/{vyper-b-Clr-8u.js → vyper-qnCVj8ia.js} +0 -0
  532. /package/dist/web/assets/{wasm-CpcKStkw.js → wasm-Ckx9jBPe.js} +0 -0
  533. /package/dist/web/assets/{wasm-C5v2T06Y.js → wasm-Dy-SJ3W9.js} +0 -0
  534. /package/dist/web/assets/{wenyan-Dz2srLZR.js → wenyan-DTEy2Lx-.js} +0 -0
  535. /package/dist/web/assets/{wgsl-CeD1-dRb.js → wgsl-DC7n63Sy.js} +0 -0
  536. /package/dist/web/assets/{wikitext-CyeS-9FH.js → wikitext-1zFoZiW6.js} +0 -0
  537. /package/dist/web/assets/{wit-Kr-q-qBQ.js → wit-DtTfCRph.js} +0 -0
  538. /package/dist/web/assets/{wolfram-BKm8bmln.js → wolfram-B2Bgu1KL.js} +0 -0
  539. /package/dist/web/assets/{x-YDu4m2Iq.js → x-CeN0Qybc.js} +0 -0
  540. /package/dist/web/assets/{yaml-2XSxomjM.js → yaml-B-Jxk0IQ.js} +0 -0
  541. /package/dist/web/assets/{zenscript-CKIS34vf.js → zenscript-BWRfvNER.js} +0 -0
  542. /package/dist/web/assets/{zig-DdHGNllX.js → zig-CkwuvETm.js} +0 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Pure(-ish) per-gesture accumulators for `use-remote-desktop-touch.ts` — kept out of the hook
3
+ * so the DOM-wiring there stays readable and these decision rules live in one place. Each
4
+ * `track` object is created on touchstart and mutated by the `advance*`/`resolve*` functions as
5
+ * touchmove/touchend events arrive; nothing here touches the DOM or sends messages directly,
6
+ * the hook does that with whatever these functions return.
7
+ */
8
+ import { classifyTwoFingerGesture, isTapGesture, touchDistance, touchMidpoint, type PointerSample } from "./remote-desktop-gesture-classifiers";
9
+ import { fractionFromZoomedPoint, type ZoomPanTransform, type Fraction, type Rect } from "./remote-desktop-coords";
10
+
11
+ export type RemoteDesktopInputMode = "touch" | "mouse";
12
+
13
+ export const TAP_MOVE_THRESHOLD_PX = 10;
14
+ export const TAP_MAX_DURATION_MS = 300;
15
+ export const TWO_FINGER_THRESHOLD_PX = 15;
16
+ export const SCALE_MIN = 1;
17
+ export const SCALE_MAX = 4;
18
+
19
+ function clamp(n: number, min: number, max: number): number {
20
+ return Math.min(Math.max(n, min), max);
21
+ }
22
+
23
+ export function clickMessages(xFrac: number, yFrac: number, button: "left" | "right"): Record<string, unknown>[] {
24
+ return [
25
+ { type: "pointer", xFrac, yFrac, button, down: true },
26
+ { type: "pointer", xFrac, yFrac, button, down: false },
27
+ ];
28
+ }
29
+
30
+ /* ------------------------------------------------------------------ */
31
+ /* Single-finger tracking */
32
+ /* ------------------------------------------------------------------ */
33
+
34
+ export interface SingleTrack {
35
+ start: PointerSample;
36
+ last: { x: number; y: number };
37
+ moved: boolean;
38
+ /** The displayed video's rect (post letterbox) at gesture start — cached because it does not
39
+ * change mid-drag. */
40
+ rect: Rect;
41
+ }
42
+
43
+ export function beginSingleTouch(x: number, y: number, rect: Rect): SingleTrack {
44
+ return { start: { x, y, t: performance.now() }, last: { x, y }, moved: false, rect };
45
+ }
46
+
47
+ export interface SingleMoveResult {
48
+ /** Pointer message to send immediately (touch mode: drag-start once, then drag-move). */
49
+ message?: Record<string, unknown>;
50
+ /** New virtual-cursor fraction (mouse mode only). */
51
+ cursor?: Fraction;
52
+ sendCursorMove?: boolean;
53
+ }
54
+
55
+ export function advanceSingleTouch(
56
+ track: SingleTrack,
57
+ x: number,
58
+ y: number,
59
+ mode: RemoteDesktopInputMode,
60
+ transform: ZoomPanTransform,
61
+ cursor: Fraction,
62
+ ): SingleMoveResult {
63
+ const dist = Math.hypot(x - track.start.x, y - track.start.y);
64
+ if (mode === "touch") {
65
+ if (!track.moved && dist > TAP_MOVE_THRESHOLD_PX) {
66
+ track.moved = true;
67
+ const start = fractionFromZoomedPoint(track.start.x, track.start.y, track.rect, transform);
68
+ track.last = { x, y };
69
+ return { message: { type: "pointer", xFrac: start.xFrac, yFrac: start.yFrac, button: "left", down: true } };
70
+ }
71
+ track.last = { x, y };
72
+ if (!track.moved) return {};
73
+ const frac = fractionFromZoomedPoint(x, y, track.rect, transform);
74
+ return { message: { type: "pointer", xFrac: frac.xFrac, yFrac: frac.yFrac, button: null, down: null } };
75
+ }
76
+
77
+ // Mouse mode: relative drag of the client-side virtual cursor, scaled down by zoom so panning
78
+ // while zoomed in still moves the cursor the same *host* distance.
79
+ const dx = (x - track.last.x) / transform.scale;
80
+ const dy = (y - track.last.y) / transform.scale;
81
+ if (dist > TAP_MOVE_THRESHOLD_PX) track.moved = true;
82
+ track.last = { x, y };
83
+ const next: Fraction = {
84
+ xFrac: clamp(cursor.xFrac + dx / track.rect.width, 0, 1),
85
+ yFrac: clamp(cursor.yFrac + dy / track.rect.height, 0, 1),
86
+ };
87
+ return { cursor: next, sendCursorMove: true };
88
+ }
89
+
90
+ export function resolveSingleTouchEnd(
91
+ track: SingleTrack,
92
+ x: number,
93
+ y: number,
94
+ mode: RemoteDesktopInputMode,
95
+ transform: ZoomPanTransform,
96
+ cursor: Fraction,
97
+ ): Record<string, unknown>[] {
98
+ const end: PointerSample = { x, y, t: performance.now() };
99
+ const tapped = isTapGesture(track.start, end, TAP_MOVE_THRESHOLD_PX, TAP_MAX_DURATION_MS);
100
+ if (mode === "touch") {
101
+ const frac = fractionFromZoomedPoint(x, y, track.rect, transform);
102
+ if (!track.moved && tapped) return clickMessages(frac.xFrac, frac.yFrac, "left");
103
+ if (track.moved) return [{ type: "pointer", xFrac: frac.xFrac, yFrac: frac.yFrac, button: "left", down: false }];
104
+ return [];
105
+ }
106
+ if (!track.moved && tapped) return clickMessages(cursor.xFrac, cursor.yFrac, "left");
107
+ return [];
108
+ }
109
+
110
+ /* ------------------------------------------------------------------ */
111
+ /* Two-finger tracking */
112
+ /* ------------------------------------------------------------------ */
113
+
114
+ export interface TwoFingerTrack {
115
+ startDistance: number;
116
+ startMid: { x: number; y: number };
117
+ lastMid: { x: number; y: number };
118
+ startScale: number;
119
+ startTime: number;
120
+ gesture: "pinch" | "scroll" | null;
121
+ }
122
+
123
+ export function beginTwoFingerTouch(
124
+ pa: { x: number; y: number },
125
+ pb: { x: number; y: number },
126
+ currentScale: number,
127
+ ): TwoFingerTrack {
128
+ const mid = touchMidpoint(pa, pb);
129
+ return {
130
+ startDistance: touchDistance(pa, pb),
131
+ startMid: mid,
132
+ lastMid: mid,
133
+ startScale: currentScale,
134
+ startTime: performance.now(),
135
+ gesture: null,
136
+ };
137
+ }
138
+
139
+ export interface TwoFingerMoveResult {
140
+ transform?: ZoomPanTransform;
141
+ wheelDeltaPx?: number;
142
+ }
143
+
144
+ /** Locks in "pinch" vs "scroll" the first time either signal (finger spread, or midpoint
145
+ * translation) crosses `TWO_FINGER_THRESHOLD_PX`, then keeps applying that gesture for the
146
+ * rest of the touch — see `remote-desktop-gesture-classifiers.ts` for why. */
147
+ export function advanceTwoFingerTouch(
148
+ track: TwoFingerTrack,
149
+ pa: { x: number; y: number },
150
+ pb: { x: number; y: number },
151
+ currentTransform: ZoomPanTransform,
152
+ ): TwoFingerMoveResult {
153
+ const currentDistance = touchDistance(pa, pb);
154
+ const currentMid = touchMidpoint(pa, pb);
155
+
156
+ if (track.gesture === null) {
157
+ const distDiff = Math.abs(currentDistance - track.startDistance);
158
+ const midDist = touchDistance(track.startMid, currentMid);
159
+ if (distDiff > TWO_FINGER_THRESHOLD_PX) {
160
+ track.gesture = classifyTwoFingerGesture(track.startDistance, currentDistance, TWO_FINGER_THRESHOLD_PX);
161
+ } else if (midDist > TWO_FINGER_THRESHOLD_PX) {
162
+ track.gesture = "scroll";
163
+ } else {
164
+ track.lastMid = currentMid;
165
+ return {}; // still ambiguous — wait for more movement before committing
166
+ }
167
+ }
168
+
169
+ let result: TwoFingerMoveResult;
170
+ if (track.gesture === "pinch") {
171
+ const scale = clamp(track.startScale * (currentDistance / track.startDistance), SCALE_MIN, SCALE_MAX);
172
+ result = { transform: { scale, panX: currentTransform.panX, panY: currentTransform.panY } };
173
+ } else if (currentTransform.scale > 1.01) {
174
+ // Panning only makes sense once zoomed in — at scale 1 the same drag scrolls instead.
175
+ const dx = currentMid.x - track.lastMid.x;
176
+ const dy = currentMid.y - track.lastMid.y;
177
+ result = { transform: { scale: currentTransform.scale, panX: currentTransform.panX + dx, panY: currentTransform.panY + dy } };
178
+ } else {
179
+ result = { wheelDeltaPx: currentMid.y - track.lastMid.y };
180
+ }
181
+ track.lastMid = currentMid;
182
+ return result;
183
+ }
184
+
185
+ export function resolveTwoFingerEnd(
186
+ track: TwoFingerTrack,
187
+ rect: Rect,
188
+ transform: ZoomPanTransform,
189
+ ): Record<string, unknown>[] {
190
+ if (track.gesture === null && performance.now() - track.startTime <= TAP_MAX_DURATION_MS) {
191
+ const frac = fractionFromZoomedPoint(track.lastMid.x, track.lastMid.y, rect, transform);
192
+ return clickMessages(frac.xFrac, frac.yFrac, "right");
193
+ }
194
+ return [];
195
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Remote Desktop window body: thin wrapper around `useRemoteDesktopConnection` (WS/nonce/ping/
3
+ * decode, shared with the mobile viewer) that renders the canvas, overlay states, and wires
4
+ * pointer/keyboard capture over the connection's `sendMessage`.
5
+ */
6
+ import { useRef } from "react";
7
+ import { RotateCw, MonitorX, Gauge } from "lucide-react";
8
+ import type { WindowContentProps } from "@/components/floating-window/window-content-registry";
9
+ import { useSettingsStore } from "@/stores/settings-store";
10
+ import { cn } from "@/lib/utils";
11
+ import { useRemoteDesktopConnection } from "./use-remote-desktop-connection";
12
+ import { useRemoteInputCapture } from "./use-remote-input-capture";
13
+ import { RemoteDesktopStatsOverlay } from "./remote-desktop-stats-overlay";
14
+
15
+ export default function RemoteDesktopWindowContent(_props: WindowContentProps) {
16
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
17
+ const { connState, errorMessage, decoderStatus, decoderErrorMessage, sendMessage, reconnect, getFrameCount, getTotalBytes } =
18
+ useRemoteDesktopConnection(canvasRef);
19
+ const statsVisible = useSettingsStore((s) => s.remoteDesktopStatsVisible);
20
+ const toggleStats = useSettingsStore((s) => s.toggleRemoteDesktopStatsVisible);
21
+
22
+ useRemoteInputCapture(canvasRef, sendMessage, connState === "streaming");
23
+
24
+ const overlayMessage = decoderStatus === "unsupported"
25
+ ? decoderErrorMessage
26
+ : connState === "error"
27
+ ? errorMessage
28
+ : connState === "closed"
29
+ ? "Disconnected"
30
+ : connState === "connecting"
31
+ ? "Connecting…"
32
+ : null;
33
+
34
+ return (
35
+ <div
36
+ className="relative flex h-full w-full items-center justify-center bg-black"
37
+ data-testid="remote-desktop-window"
38
+ data-conn-state={connState}
39
+ >
40
+ <canvas ref={canvasRef} data-testid="remote-desktop-canvas" className="max-h-full max-w-full outline-none" />
41
+ <RemoteDesktopStatsOverlay canvasRef={canvasRef} getFrameCount={getFrameCount} getTotalBytes={getTotalBytes} />
42
+ {/* Small corner toggle for the stats overlay above — top-right so it never collides with
43
+ the overlay itself (top-left) or the window's own title bar/controls above this body. */}
44
+ <button
45
+ type="button"
46
+ onClick={toggleStats}
47
+ aria-label="Toggle stats overlay"
48
+ aria-pressed={statsVisible}
49
+ className={cn(
50
+ "absolute right-1 top-1 z-40 flex size-6 items-center justify-center rounded bg-black/40 text-white/70 hover:bg-black/60 hover:text-white",
51
+ statsVisible && "bg-primary/70 text-primary-foreground hover:bg-primary/80",
52
+ )}
53
+ >
54
+ <Gauge className="size-3.5" />
55
+ </button>
56
+ {overlayMessage && (
57
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/70 text-sm text-white">
58
+ <MonitorX className="size-6 opacity-70" />
59
+ <span className="max-w-sm text-center px-4">{overlayMessage}</span>
60
+ {(connState === "error" || connState === "closed") && decoderStatus !== "unsupported" && (
61
+ <button
62
+ onClick={reconnect}
63
+ className="flex items-center gap-1.5 rounded-md bg-white/10 px-3 py-1.5 hover:bg-white/20"
64
+ >
65
+ <RotateCw className="size-3.5" /> Reconnect
66
+ </button>
67
+ )}
68
+ </div>
69
+ )}
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,22 @@
1
+ /** Pure URL builder — mirrors `ws-client.ts`'s dev-port bypass so it's unit-testable without
2
+ * mounting a real WebSocket or window.location. */
3
+ import { withWsAuth } from "@/lib/ws-auth";
4
+
5
+ export interface LocationParts {
6
+ protocol: string;
7
+ hostname: string;
8
+ host: string;
9
+ }
10
+
11
+ export function resolveRemoteDesktopWsUrl(loc: LocationParts, isDev: boolean, devPort = "8081"): string {
12
+ const authed = withWsAuth("/ws/remote-desktop");
13
+ const wsProtocol = loc.protocol === "https:" ? "wss:" : "ws:";
14
+ if (isDev && loc.protocol !== "https:") {
15
+ // Same bypass as ws-client.ts: Vite's dev proxy has unreliable WS upgrade handling.
16
+ // Port defaults to 8081 (the normal `bun dev:server` port) but is overridable via
17
+ // `VITE_DEV_API_PORT` for an alt-port dev stack (see `PPM_DEV_API` in vite.config.ts) —
18
+ // without this, a second dev stack on e.g. 8082 would always connect to the wrong server.
19
+ return `ws://${loc.hostname}:${devPort}${authed}`;
20
+ }
21
+ return `${wsProtocol}//${loc.host}${authed}`;
22
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * WebCodecs `VideoDecoder` wrapper — Annex-B H.264 in, `<canvas>` frames out.
3
+ *
4
+ * Two config traps this guards against: (1) the codec string must come from the real
5
+ * bitstream (`avc1-codec-string.ts` on the server derives it from the actual SPS) — a
6
+ * hardcoded guess can mismatch the encoder's actual profile/level and make `configure()`
7
+ * throw; (2) no `description` field is passed, which is what tells WebCodecs to expect
8
+ * Annex-B (start-code-prefixed) input rather than length-prefixed AVCC — passing a
9
+ * `description` here would silently break decode of every chunk this session sends.
10
+ *
11
+ * Self-healing on decode error: a mobile hardware decoder (confirmed: works on desktop Chrome,
12
+ * dies on mobile Chromium after ~one GOP) can throw a hard decode error and never recover on its
13
+ * own — WebCodecs offers no "just skip this frame" API once that happens, the decoder is dead.
14
+ * Rather than freeze the stream forever, a decode error tears down the errored `VideoDecoder`
15
+ * and creates a fresh one with the same cached codec string, then waits for the next server
16
+ * keyframe before feeding it anything (same guard the very first connect already uses) — the
17
+ * server sends one roughly every second (30-frame GOP @ 30fps), so recovery is bounded to about
18
+ * that long instead of permanent.
19
+ */
20
+ import { useCallback, useRef, useState } from "react";
21
+ import { frameTimestampMicros } from "./remote-desktop-frame-timestamp";
22
+
23
+ export type DecoderStatus = "idle" | "unsupported" | "ready" | "error";
24
+
25
+ /**
26
+ * Whether an access unit should be fed to the decoder right now. Decode cannot start on a delta
27
+ * chunk — true both on the very first connect (before any keyframe has arrived) and after an
28
+ * auto-recovered decode error, since a freshly (re)created `VideoDecoder` has no prior reference
29
+ * frame either way. A keyframe is always decodable on its own. Pure so this resync rule is
30
+ * testable without a real `VideoDecoder`.
31
+ */
32
+ export function shouldDecodeAccessUnit(hasSeenKeyframeThisInstance: boolean, isKey: boolean): boolean {
33
+ return isKey || hasSeenKeyframeThisInstance;
34
+ }
35
+
36
+ export interface UseH264CanvasDecoderResult {
37
+ status: DecoderStatus;
38
+ errorMessage: string | null;
39
+ /** Configure (or reconfigure) the decoder once the server's `{type:"config"}` message
40
+ * arrives with a codec string derived from the real SPS. */
41
+ configure: (codec: string) => Promise<void>;
42
+ /** Feed one access unit (header byte already stripped by the caller) to the decoder. */
43
+ decodeAccessUnit: (bytes: Uint8Array, isKey: boolean) => void;
44
+ reset: () => void;
45
+ /** Total frames the decoder has handed to `output()` (and drawn, or attempted to) since the
46
+ * last `reset()`. Ref-backed, not React state — read it from a poll (e.g. the stats overlay),
47
+ * not a render dependency, so every decoded frame doesn't force a re-render. */
48
+ getFrameCount: () => number;
49
+ }
50
+
51
+ export function useH264CanvasDecoder(
52
+ canvasRef: React.RefObject<HTMLCanvasElement | null>,
53
+ ): UseH264CanvasDecoderResult {
54
+ const [status, setStatus] = useState<DecoderStatus>("idle");
55
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
56
+ const decoderRef = useRef<VideoDecoder | null>(null);
57
+ const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
58
+ const decodedAnyKeyRef = useRef(false);
59
+ const frameCountRef = useRef(0);
60
+ const frameIndexRef = useRef(0); // per-decoder-instance timestamp counter, see decodeAccessUnit
61
+ const lastCodecRef = useRef<string | null>(null);
62
+ const activeRef = useRef(false); // false once reset()/unmount has torn this session down
63
+
64
+ const fail = useCallback((message: string) => {
65
+ setStatus("error");
66
+ setErrorMessage(message);
67
+ }, []);
68
+
69
+ const configure = useCallback(async (codec: string) => {
70
+ lastCodecRef.current = codec;
71
+ if (typeof VideoDecoder === "undefined") {
72
+ setStatus("unsupported");
73
+ setErrorMessage("This browser has no WebCodecs VideoDecoder — try Chrome, Edge, or Safari 16.4+.");
74
+ return;
75
+ }
76
+ const config: VideoDecoderConfig = { codec, optimizeForLatency: true };
77
+ try {
78
+ const support = await VideoDecoder.isConfigSupported(config);
79
+ if (!support.supported) {
80
+ setStatus("unsupported");
81
+ setErrorMessage(`This browser's decoder does not support ${codec}.`);
82
+ return;
83
+ }
84
+ } catch (e) {
85
+ fail((e as Error).message);
86
+ return;
87
+ }
88
+
89
+ decoderRef.current?.close();
90
+ decodedAnyKeyRef.current = false;
91
+ frameIndexRef.current = 0;
92
+ const decoder = new VideoDecoder({
93
+ output: (frame) => {
94
+ frameCountRef.current += 1;
95
+ const canvas = canvasRef.current;
96
+ if (canvas) {
97
+ if (canvas.width !== frame.displayWidth || canvas.height !== frame.displayHeight) {
98
+ canvas.width = frame.displayWidth;
99
+ canvas.height = frame.displayHeight;
100
+ }
101
+ if (!ctxRef.current) ctxRef.current = canvas.getContext("2d");
102
+ ctxRef.current?.drawImage(frame, 0, 0, canvas.width, canvas.height);
103
+ }
104
+ frame.close();
105
+ },
106
+ error: (e) => recoverOrFail(e.message),
107
+ });
108
+ try {
109
+ decoder.configure(config);
110
+ } catch (e) {
111
+ fail((e as Error).message);
112
+ return;
113
+ }
114
+ decoderRef.current = decoder;
115
+ activeRef.current = true;
116
+ setStatus("ready");
117
+ setErrorMessage(null);
118
+ }, [canvasRef, fail]);
119
+
120
+ /** Shared by the decoder's async `error` callback and `decodeAccessUnit`'s synchronous catch
121
+ * (a `.decode()` call can also throw directly) — both are "this decoder just broke", handled
122
+ * identically: recreate rather than give up, unless the session has already been torn down
123
+ * (`reset()`/unmount) or there is no cached codec to recreate with (should not happen — codec
124
+ * is cached before any decode is possible). */
125
+ const recoverOrFail = useCallback((message: string) => {
126
+ if (!activeRef.current) return; // torn down already — nothing to recover into
127
+ const codec = lastCodecRef.current;
128
+ if (!codec) { fail(message); return; }
129
+ try { decoderRef.current?.close(); } catch { /* discarding the errored instance regardless */ }
130
+ decoderRef.current = null;
131
+ void configure(codec); // recreate; configure() itself resets decodedAnyKeyRef, so decode
132
+ // waits for the next server keyframe instead of feeding a fresh decoder mid-GOP deltas.
133
+ }, [configure, fail]);
134
+
135
+ const decodeAccessUnit = useCallback((bytes: Uint8Array, isKey: boolean) => {
136
+ const decoder = decoderRef.current;
137
+ if (!decoder || decoder.state !== "configured") return;
138
+ if (!shouldDecodeAccessUnit(decodedAnyKeyRef.current, isKey)) return;
139
+ if (isKey) decodedAnyKeyRef.current = true;
140
+ try {
141
+ decoder.decode(new EncodedVideoChunk({
142
+ type: isKey ? "key" : "delta",
143
+ timestamp: frameTimestampMicros(frameIndexRef.current++),
144
+ data: bytes,
145
+ }));
146
+ } catch (e) {
147
+ recoverOrFail((e as Error).message);
148
+ }
149
+ }, [recoverOrFail]);
150
+
151
+ const reset = useCallback(() => {
152
+ activeRef.current = false;
153
+ // Closing an already-errored/half-configured decoder can throw in some browsers; this runs
154
+ // from a React effect cleanup, and an uncaught throw there crashes the whole tree (no error
155
+ // boundary catches effect-cleanup errors) — swallow it, we're discarding the decoder anyway.
156
+ try { decoderRef.current?.close(); } catch { /* discarding regardless */ }
157
+ decoderRef.current = null;
158
+ decodedAnyKeyRef.current = false;
159
+ frameCountRef.current = 0;
160
+ frameIndexRef.current = 0;
161
+ setStatus("idle");
162
+ setErrorMessage(null);
163
+ }, []);
164
+
165
+ const getFrameCount = useCallback(() => frameCountRef.current, []);
166
+
167
+ return {
168
+ status,
169
+ errorMessage,
170
+ configure,
171
+ decodeAccessUnit,
172
+ reset,
173
+ getFrameCount,
174
+ };
175
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Fetches remote-desktop capabilities once and reports whether the feature should surface in
3
+ * the UI. Returns `available=false` when the server feature flag is off (the endpoint 404s) or
4
+ * video capture is unavailable, so the nav entry stays hidden unless `REMOTE_DESKTOP_ENABLED`
5
+ * is set and ffmpeg can capture.
6
+ */
7
+ import { useEffect, useState } from "react";
8
+ import { api } from "@/lib/api-client";
9
+
10
+ interface Capabilities {
11
+ ffmpegAvailable: boolean;
12
+ videoAvailable: boolean;
13
+ inputAvailable: boolean;
14
+ authRequired: boolean;
15
+ }
16
+
17
+ export function useRemoteDesktopAvailable(): { available: boolean; authRequired: boolean } {
18
+ const [caps, setCaps] = useState<Capabilities | null>(null);
19
+
20
+ useEffect(() => {
21
+ let cancelled = false;
22
+ api.get<Capabilities>("/api/remote-desktop/capabilities")
23
+ .then((c) => { if (!cancelled) setCaps(c); })
24
+ .catch(() => { if (!cancelled) setCaps(null); }); // 404 (flag off) or network error — stay hidden
25
+ return () => { cancelled = true; };
26
+ }, []);
27
+
28
+ return { available: !!caps?.videoAvailable, authRequired: !!caps?.authRequired };
29
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Owns one remote-desktop WS connection end to end: mints a session nonce, opens the socket,
3
+ * feeds binary access units to the H.264 decoder, and answers the server's heartbeat ping.
4
+ * Factored out of the desktop floating-window body so the mobile full-screen viewer can share
5
+ * the exact same connection logic instead of a parallel implementation — see
6
+ * `remote-desktop-window-content.tsx` (desktop) and `remote-desktop-mobile-view.tsx` (mobile),
7
+ * both now thin callers of this hook.
8
+ *
9
+ * Connection is one-shot — a lost connection reports `connState: "closed"`/`"error"` and waits
10
+ * for the caller to bump `reconnect()`. We deliberately do NOT auto-reconnect on every close (a
11
+ * flaky link reconnecting in a loop would fight the server's "one session per host" eviction).
12
+ * The one exception is the tab becoming visible again: browsers throttle the ping
13
+ * `setInterval` in a backgrounded tab, so a user who glances away can trip the server's
14
+ * heartbeat timeout — on return we re-ping (or, if the session already died, reconnect once).
15
+ * That's a discrete user-driven event, not a loop.
16
+ */
17
+ import { useCallback, useEffect, useRef, useState } from "react";
18
+ import { api } from "@/lib/api-client";
19
+ import { resolveRemoteDesktopWsUrl } from "./remote-desktop-ws-url";
20
+ import { useH264CanvasDecoder, type DecoderStatus } from "./use-h264-canvas-decoder";
21
+
22
+ export type RemoteDesktopConnState = "connecting" | "streaming" | "error" | "closed";
23
+
24
+ const PING_INTERVAL_MS = 5_000;
25
+
26
+ export interface UseRemoteDesktopConnectionResult {
27
+ connState: RemoteDesktopConnState;
28
+ errorMessage: string | null;
29
+ decoderStatus: DecoderStatus;
30
+ decoderErrorMessage: string | null;
31
+ sendMessage: (msg: Record<string, unknown>) => void;
32
+ /** Force a fresh connection (e.g. a manual "Reconnect" button). */
33
+ reconnect: () => void;
34
+ /** Total bytes of binary (access-unit) WS messages received since the last (re)connect.
35
+ * Ref-backed — poll it (e.g. the stats overlay), don't treat it as a render dependency. Feeds
36
+ * the KB/s stat; frame count below feeds fps. */
37
+ getTotalBytes: () => number;
38
+ /** Forwards the decoder's own frame counter (see `use-h264-canvas-decoder.ts`). */
39
+ getFrameCount: () => number;
40
+ }
41
+
42
+ export function useRemoteDesktopConnection(
43
+ canvasRef: React.RefObject<HTMLCanvasElement | null>,
44
+ ): UseRemoteDesktopConnectionResult {
45
+ const wsRef = useRef<WebSocket | null>(null);
46
+ const totalBytesRef = useRef(0);
47
+ const [connState, setConnState] = useState<RemoteDesktopConnState>("connecting");
48
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
49
+ const [generation, setGeneration] = useState(0); // bump to force a manual reconnect
50
+
51
+ const decoder = useH264CanvasDecoder(canvasRef);
52
+
53
+ const sendMessage = useCallback((msg: Record<string, unknown>) => {
54
+ const ws = wsRef.current;
55
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
56
+ }, []);
57
+
58
+ useEffect(() => {
59
+ let cancelled = false;
60
+ let pingTimer: ReturnType<typeof setInterval> | null = null;
61
+ setConnState("connecting");
62
+ setErrorMessage(null);
63
+ totalBytesRef.current = 0;
64
+ decoder.reset();
65
+
66
+ (async () => {
67
+ let nonce: string;
68
+ try {
69
+ // `wsPath` in the response is always "/ws/remote-desktop" — the client already knows
70
+ // that path; the response is only consumed for the nonce.
71
+ const res = await api.post<{ nonce: string; wsPath: string }>("/api/remote-desktop/session");
72
+ nonce = res.nonce;
73
+ } catch (e) {
74
+ if (!cancelled) { setConnState("error"); setErrorMessage((e as Error).message); }
75
+ return;
76
+ }
77
+ if (cancelled) return;
78
+
79
+ const url = resolveRemoteDesktopWsUrl(window.location, import.meta.env.DEV, import.meta.env.VITE_DEV_API_PORT);
80
+ const ws = new WebSocket(url);
81
+ ws.binaryType = "arraybuffer";
82
+ wsRef.current = ws;
83
+
84
+ ws.onopen = () => {
85
+ ws.send(JSON.stringify({ type: "auth", nonce }));
86
+ pingTimer = setInterval(() => sendMessage({ type: "ping" }), PING_INTERVAL_MS);
87
+ };
88
+ ws.onmessage = (event) => {
89
+ if (typeof event.data === "string") {
90
+ let msg: Record<string, unknown>;
91
+ try { msg = JSON.parse(event.data); } catch { return; }
92
+ if (msg.type === "config" && typeof msg.codec === "string") {
93
+ decoder.configure(msg.codec).then(() => { if (!cancelled) setConnState("streaming"); });
94
+ } else if (msg.type === "error") {
95
+ setConnState("error");
96
+ setErrorMessage(typeof msg.message === "string" ? msg.message : "Server error");
97
+ }
98
+ return;
99
+ }
100
+ const bytes = new Uint8Array(event.data as ArrayBuffer);
101
+ if (bytes.length < 1) return;
102
+ totalBytesRef.current += bytes.length;
103
+ decoder.decodeAccessUnit(bytes.subarray(1), bytes[0] === 1);
104
+ };
105
+ ws.onerror = () => { if (!cancelled) { setConnState("error"); setErrorMessage("WebSocket error"); } };
106
+ ws.onclose = () => { if (!cancelled) setConnState((s) => (s === "error" ? s : "closed")); };
107
+ })();
108
+
109
+ // A backgrounded tab throttles the ping interval, which can trip the server heartbeat. When
110
+ // the tab is visible again, re-ping immediately; if the session already died, reconnect once
111
+ // (bumping generation re-runs this effect). Fires only on the visible transition — not a loop.
112
+ const onVisibility = () => {
113
+ if (document.hidden || cancelled) return;
114
+ const ws = wsRef.current;
115
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
116
+ else setGeneration((g) => g + 1);
117
+ };
118
+ document.addEventListener("visibilitychange", onVisibility);
119
+
120
+ return () => {
121
+ cancelled = true;
122
+ document.removeEventListener("visibilitychange", onVisibility);
123
+ if (pingTimer) clearInterval(pingTimer);
124
+ // A throw here (e.g. closing a socket/decoder mid-teardown) runs inside an effect cleanup,
125
+ // which no error boundary catches — left unguarded it can blank the entire app on unmount,
126
+ // not just this component (this is what closing the mobile sheet used to do).
127
+ try { wsRef.current?.close(); } catch { /* tearing down regardless */ }
128
+ wsRef.current = null;
129
+ try { decoder.reset(); } catch { /* tearing down regardless */ }
130
+ };
131
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- reconnect is driven by `generation`
132
+ }, [generation]);
133
+
134
+ const reconnect = useCallback(() => setGeneration((g) => g + 1), []);
135
+ const getTotalBytes = useCallback(() => totalBytesRef.current, []);
136
+
137
+ return {
138
+ connState,
139
+ errorMessage,
140
+ decoderStatus: decoder.status,
141
+ decoderErrorMessage: decoder.errorMessage,
142
+ sendMessage,
143
+ reconnect,
144
+ getTotalBytes,
145
+ getFrameCount: decoder.getFrameCount,
146
+ };
147
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Global open/close state for the single mobile remote-desktop sheet. There is exactly one
3
+ * host to control (same as the desktop floating window, which also dedupes to one instance),
4
+ * so — unlike the mobile explorer's per-path slices — this is just an open flag.
5
+ */
6
+ import { create } from "zustand";
7
+
8
+ interface RemoteDesktopMobileOpenState {
9
+ isOpen: boolean;
10
+ open(): void;
11
+ close(): void;
12
+ }
13
+
14
+ export const useRemoteDesktopMobileOpenState = create<RemoteDesktopMobileOpenState>((set) => ({
15
+ isOpen: false,
16
+ open: () => set({ isOpen: true }),
17
+ close: () => set({ isOpen: false }),
18
+ }));