@hienlh/ppm 0.18.14 → 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 (539) hide show
  1. package/.rdtest2-cf.out +0 -0
  2. package/CHANGELOG.md +18 -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 +22 -4
  6. package/assets/skills/ppm/references/http-api.md +8 -1
  7. package/dist/web/assets/{adaptive-context-menu-DCZtgOL7.js → adaptive-context-menu-ChBZWLLq.js} +1 -1
  8. package/dist/web/assets/{ai-resource-editor-D5CPBg06.js → ai-resource-editor-C8aAY8Po.js} +1 -1
  9. package/dist/web/assets/{ai-settings-section-DY-t1bK8.js → ai-settings-section-ERu1WiZc.js} +1 -1
  10. package/dist/web/assets/{angular-html-CTvM4ulF.js → angular-html-CXgHIQ2U.js} +1 -1
  11. package/dist/web/assets/{angular-ts-B_vSkjco.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-B2iNXQ0c.js → api-settings-R59c66DE.js} +1 -1
  15. package/dist/web/assets/{api-themes-DrWnJbhs.js → api-themes-DKxrwJda.js} +1 -1
  16. package/dist/web/assets/{apl-DywzTry0.js → apl-DkGH8laI.js} +1 -1
  17. package/dist/web/assets/architecture-PBZL5I3N-CCalSKAt.js +1 -0
  18. package/dist/web/assets/{astro-1JaRgaSb.js → astro-tHUd8Zlg.js} +1 -1
  19. package/dist/web/assets/{audio-preview-DAuGvOiK.js → audio-preview-B_C6Dpvz.js} +1 -1
  20. package/dist/web/assets/{blade-CDeeJWBb.js → blade-BbKpjIYK.js} +1 -1
  21. package/dist/web/assets/{bsl-BfgoPVYR.js → bsl-DA0UYto0.js} +1 -1
  22. package/dist/web/assets/{button-tNy0ba3y.js → button-DBSpDPS0.js} +1 -1
  23. package/dist/web/assets/{cairo-C26gNwWW.js → cairo-79ayucbB.js} +1 -1
  24. package/dist/web/assets/chat-tab-BraiJsYG.js +15 -0
  25. package/dist/web/assets/{cobol-D92nBnON.js → cobol-DSS2EOYo.js} +1 -1
  26. package/dist/web/assets/code-editor-lhmLNhXu.js +10 -0
  27. package/dist/web/assets/{coffee-CIWH40R1.js → coffee-DvSxq2iD.js} +1 -1
  28. package/dist/web/assets/{conflict-editor-CDp0I3mD.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-DOrBqXCn.js → cpp-BhWZ-FSn.js} +1 -1
  31. package/dist/web/assets/{crystal-BNKqmHux.js → crystal-yP2OeF-f.js} +1 -1
  32. package/dist/web/assets/{csv-preview-D4svNDAq.js → csv-preview-D46W_icb.js} +1 -1
  33. package/dist/web/assets/{data-grid-overlay-editor-BqV0AxZg.js → data-grid-overlay-editor-DeGkPYtA.js} +1 -1
  34. package/dist/web/assets/{database-viewer-BCvv_Bwf.js → database-viewer-DFI4YtrH.js} +1 -1
  35. package/dist/web/assets/{dialog-DcLGDwaX.js → dialog-RIpeb87v.js} +1 -1
  36. package/dist/web/assets/{diff-viewer-D41fCFAi.js → diff-viewer-eaNOLcBL.js} +1 -1
  37. package/dist/web/assets/{docx-preview-DfaQn9v7.js → docx-preview-Cbj-pOK3.js} +1 -1
  38. package/dist/web/assets/{edge-Dbjp42Pt.js → edge-D3K9cd-p.js} +1 -1
  39. package/dist/web/assets/{edit-diff-preview-BqcUuFCc.js → edit-diff-preview-DDqy3KLA.js} +1 -1
  40. package/dist/web/assets/{elixir-na3b1pYy.js → elixir-D4PBmsLp.js} +1 -1
  41. package/dist/web/assets/{elm-_Nalil1R.js → elm-DSpXjuaH.js} +1 -1
  42. package/dist/web/assets/{erb-CW_7dubf.js → erb-CaYZMTJW.js} +1 -1
  43. package/dist/web/assets/{erlang-DmZ7Zzb0.js → erlang-DX5BFqJj.js} +1 -1
  44. package/dist/web/assets/{esm-CwyBetO1.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-BW_mccb3.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-DB2A8L4-.js → fortran-fixed-form-B0bAufUe.js} +1 -1
  51. package/dist/web/assets/{fsharp-BYHrti7z.js → fsharp-C2AL2b1M.js} +1 -1
  52. package/dist/web/assets/{gdresource-D5GBin40.js → gdresource-DRQsDMNI.js} +1 -1
  53. package/dist/web/assets/{git-commit-CUiOBoIL.js → git-commit-GtBdnrzk.js} +1 -1
  54. package/dist/web/assets/{git-log-panel-C1QDkmgl.js → git-log-panel-hwV1flSO.js} +1 -1
  55. package/dist/web/assets/{git-rebase-DOxGnpdw.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-Begy4UT7.js → glimmer-js-Bxwu93bS.js} +1 -1
  59. package/dist/web/assets/{glimmer-ts-rPApRY8N.js → glimmer-ts-CIUYdR4E.js} +1 -1
  60. package/dist/web/assets/{glsl-wrbPvrBL.js → glsl-CfxLI5jn.js} +1 -1
  61. package/dist/web/assets/{graphql-Cu4-NR__.js → graphql-CstqDPde.js} +1 -1
  62. package/dist/web/assets/group-chat-tab-B7mJO1gG.js +1 -0
  63. package/dist/web/assets/{hack-Ddo93XKD.js → hack-Bjqnssb5.js} +1 -1
  64. package/dist/web/assets/{haml-BFX_1i8j.js → haml-lVc-DfbM.js} +1 -1
  65. package/dist/web/assets/hand-x0ejq3fS.js +1 -0
  66. package/dist/web/assets/{handlebars-5gFKA4H5.js → handlebars-Yy6fPQI_.js} +1 -1
  67. package/dist/web/assets/{html-derivative-e5JLAFZF.js → html-derivative-CchzCY1z.js} +1 -1
  68. package/dist/web/assets/{html-C06I-zeO.js → html-lqtcy6-2.js} +1 -1
  69. package/dist/web/assets/{http-CrAAhSU7.js → http-ChclRrko.js} +1 -1
  70. package/dist/web/assets/{hurl-T1V3SX9T.js → hurl-CoOK7sXa.js} +1 -1
  71. package/dist/web/assets/{hxml-B056LLKN.js → hxml-fZSZ7Iyt.js} +1 -1
  72. package/dist/web/assets/{image-preview-DWyuCYT3.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-DouJ9Zvt.js → input-DXPgUtxi.js} +1 -1
  77. package/dist/web/assets/{jinja-DGdtRh43.js → jinja-Y0w_KBdE.js} +1 -1
  78. package/dist/web/assets/{jison-Ah6QobPS.js → jison-BILj3F2e.js} +1 -1
  79. package/dist/web/assets/{julia-BJeGOK0K.js → julia-DRfOPDts.js} +1 -1
  80. package/dist/web/assets/{just-iw9oRs7j.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-DHjj5hyN.js → latex-D6gztEOL.js} +1 -1
  85. package/dist/web/assets/{liquid-B-FFecDc.js → liquid-DPqCEVb9.js} +1 -1
  86. package/dist/web/assets/{lua-DDianJp_.js → lua-I40KPUm-.js} +1 -1
  87. package/dist/web/assets/markdown-renderer-BwOkiZC1.js +1 -0
  88. package/dist/web/assets/{markdown-renderer-BHiUhQN8.js → markdown-renderer-oJ9jPdQ2.js} +3 -3
  89. package/dist/web/assets/{marko-BW5SBVng.js → marko-DMjsThSW.js} +1 -1
  90. package/dist/web/assets/{mdc-B9gL9bjl.js → mdc-CN2alsbo.js} +1 -1
  91. package/dist/web/assets/{media-url-DUK-k0bB.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-dTluY6qy.js → nextflow-CGa4JeZ6.js} +1 -1
  95. package/dist/web/assets/{nginx-DlwghGI-.js → nginx-CmqrIj84.js} +1 -1
  96. package/dist/web/assets/{nim-N9jddb5D.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-BVXM7aAs.js → number-overlay-editor-BEzJ1YeD.js} +1 -1
  99. package/dist/web/assets/{overview-panel-DCCDmtCQ.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-D7yLmAZW.js → pdf-preview-BMHM5cR2.js} +1 -1
  103. package/dist/web/assets/{perl-CkmpYKa_.js → perl-BOwpb6Tb.js} +1 -1
  104. package/dist/web/assets/{php-BwZ3KDPo.js → php-wT9fVxxu.js} +1 -1
  105. package/dist/web/assets/pie-UPGHQEXC-BXSJ2God.js +1 -0
  106. package/dist/web/assets/{postgres-viewer--CRN1fvO.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-B3rBPqQo.js → pug-DdlNnj-p.js} +1 -1
  110. package/dist/web/assets/{qml-BfDbWFmg.js → qml-C5zBdydf.js} +1 -1
  111. package/dist/web/assets/radar-KQ55EAFF-D6XrM7HH.js +1 -0
  112. package/dist/web/assets/{razor-B1ww1r38.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-C0MSq6m-.js → rst-DiMiMYUO.js} +1 -1
  118. package/dist/web/assets/{ruby-ClIZ1wb3.js → ruby-DK62iflA.js} +1 -1
  119. package/dist/web/assets/{run-in-terminal-4efbW2Xv.js → run-in-terminal-DJSL6nwZ.js} +1 -1
  120. package/dist/web/assets/{sas-DR0j3o4_.js → sas-6QPGJRa5.js} +1 -1
  121. package/dist/web/assets/{scroll-area-ZP8wcwTI.js → scroll-area-DXGXeiRz.js} +1 -1
  122. package/dist/web/assets/{scss-Vh3JLayR.js → scss-DmUqh48_.js} +1 -1
  123. package/dist/web/assets/{send-to-chat-BNrEpAyC.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-0rHRnQ18.js → shaderlab-DopFR1mi.js} +1 -1
  128. package/dist/web/assets/{shellsession-Dxdw6D8c.js → shellsession-Bp_TGuXb.js} +1 -1
  129. package/dist/web/assets/{soy-BHlSNj1y.js → soy-DEB9KEFb.js} +1 -1
  130. package/dist/web/assets/{sparql-B2QaPpni.js → sparql-C8lEwO1p.js} +1 -1
  131. package/dist/web/assets/{sql-query-editor-R_iKjPP9.js → sql-query-editor-MMfk0Kbz.js} +1 -1
  132. package/dist/web/assets/{sqlite-viewer-b2onA0SP.js → sqlite-viewer-vglzrgnZ.js} +1 -1
  133. package/dist/web/assets/{stata-CwIxjsEJ.js → stata-DVep4apN.js} +1 -1
  134. package/dist/web/assets/{surrealql-C6vo-rp-.js → surrealql-fSlJApOA.js} +1 -1
  135. package/dist/web/assets/{svelte-LLdJ6rlH.js → svelte-DezWBEDi.js} +1 -1
  136. package/dist/web/assets/{switch-DV-BDjyk.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-BPif0azo.js → system-monitor-window-content-CiJ2yIsr.js} +1 -1
  140. package/dist/web/assets/{tab-host-window-content-BnAN6m4K.js → tab-host-window-content-DYlXR0cg.js} +1 -1
  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-BSyU-5Uc.js → team-member-window-content-qrgFTC96.js} +1 -1
  144. package/dist/web/assets/{templ-CJGvhEP5.js → templ-D0zB42a9.js} +1 -1
  145. package/dist/web/assets/{terminal-tab-Ncor0Rfu.js → terminal-tab-D_-R5rGm.js} +3 -3
  146. package/dist/web/assets/{tex-psXWhc4t.js → tex-BJiPijcH.js} +1 -1
  147. package/dist/web/assets/{tool-cards-DdVP1wrD.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-D5Ar_ZCO.js → ts-tags-Dcr4u6y5.js} +1 -1
  150. package/dist/web/assets/{twig-vJgGC-qY.js → twig-B-SQU0bg.js} +1 -1
  151. package/dist/web/assets/{use-blob-url-BOVGWg9F.js → use-blob-url-BtPlqwLr.js} +1 -1
  152. package/dist/web/assets/{use-monaco-theme-BshnLXoY.js → use-monaco-theme-DQY1ZSaf.js} +1 -1
  153. package/dist/web/assets/{use-open-team-member-CY3wOdWL.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-B7xjNsyD.js → use-websocket-l39Odhlp.js} +1 -1
  156. package/dist/web/assets/{vendor-mermaid-999ksD45.js → vendor-mermaid-CtDsSxbM.js} +1 -1
  157. package/dist/web/assets/{video-preview-7F72sRwf.js → video-preview-C5AhkIDl.js} +1 -1
  158. package/dist/web/assets/{vue-BWngFyav.js → vue-Cw10Sfbh.js} +1 -1
  159. package/dist/web/assets/{vue-html-BN5ZLu-H.js → vue-html-Cc5C_Z_w.js} +1 -1
  160. package/dist/web/assets/{vue-vine-BZHnO8yV.js → vue-vine-BuNMlnvu.js} +1 -1
  161. package/dist/web/assets/{window-pip-registry-BnYU83LE.js → window-pip-registry-C7oz3Dfy.js} +1 -1
  162. package/dist/web/assets/window-store-B5ZswdYg.js +1 -0
  163. package/dist/web/assets/{xml-DS2beZHp.js → xml-DqT1wkky.js} +1 -1
  164. package/dist/web/assets/{xsl-Ciiku1Bq.js → xsl-DLkge3A2.js} +1 -1
  165. package/dist/web/index.html +45 -43
  166. package/dist/web/sw.js +1 -1
  167. package/docs/architecture/ai-chat-and-providers.md +3 -0
  168. package/package.json +1 -1
  169. package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
  170. package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
  171. package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
  172. package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
  173. package/src/cli/commands/backup-cmd.ts +119 -0
  174. package/src/index.ts +3 -0
  175. package/src/providers/claude-agent-sdk.ts +29 -1
  176. package/src/server/index.ts +42 -15
  177. package/src/server/routes/accounts.ts +30 -2
  178. package/src/server/routes/chat.ts +8 -1
  179. package/src/server/routes/named-tunnel.ts +5 -2
  180. package/src/server/routes/remote-desktop.ts +64 -0
  181. package/src/server/routes/settings.ts +1 -0
  182. package/src/server/ws/chat.ts +126 -7
  183. package/src/server/ws/remote-desktop.ts +86 -0
  184. package/src/services/account.service.ts +106 -23
  185. package/src/services/db-backup/db-backup-paths.ts +66 -0
  186. package/src/services/db-backup/db-backup-restore.ts +105 -0
  187. package/src/services/db-backup/db-backup-retention.ts +96 -0
  188. package/src/services/db-backup/db-backup-sync.ts +113 -0
  189. package/src/services/db-backup/db-backup.service.ts +61 -0
  190. package/src/services/db.service.ts +29 -1
  191. package/src/services/edge-forwarder.ts +191 -191
  192. package/src/services/fs-credential-path-guard.ts +19 -1
  193. package/src/services/named-tunnel/cloudflared-login.service.ts +2 -2
  194. package/src/services/nested-subagent-spy.ts +206 -0
  195. package/src/services/remote-desktop/access-unit-assembler.ts +90 -0
  196. package/src/services/remote-desktop/avc1-codec-string.ts +18 -0
  197. package/src/services/remote-desktop/nal-splitter.ts +100 -0
  198. package/src/services/remote-desktop/remote-desktop-capture.ts +120 -0
  199. package/src/services/remote-desktop/remote-desktop-encoder-args.ts +42 -0
  200. package/src/services/remote-desktop/remote-desktop-flag.ts +10 -0
  201. package/src/services/remote-desktop/remote-desktop-input.ts +217 -0
  202. package/src/services/remote-desktop/remote-desktop-nonce.ts +38 -0
  203. package/src/services/remote-desktop/remote-desktop-session.ts +189 -0
  204. package/src/services/remote-desktop/remote-desktop-vk-map.ts +41 -0
  205. package/src/services/subagent-transcript-merger.ts +92 -75
  206. package/src/services/subprocess-retention.ts +29 -6
  207. package/src/services/supervisor.ts +58 -0
  208. package/src/services/team-member-activity/subagent-transcript-index.ts +56 -0
  209. package/src/services/windows-process-tree.ts +351 -351
  210. package/src/types/chat.ts +2 -0
  211. package/src/web/app.tsx +8 -0
  212. package/src/web/components/chat/account-dialogs.tsx +11 -4
  213. package/src/web/components/chat/question-card.tsx +9 -2
  214. package/src/web/components/chat/usage-badge.tsx +17 -3
  215. package/src/web/components/floating-window/window-content-registry.ts +2 -0
  216. package/src/web/components/floating-window/window-store-types.ts +1 -1
  217. package/src/web/components/layout/mobile-drawer.tsx +38 -27
  218. package/src/web/components/layout/nav-section-rail.tsx +10 -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-DRF9oYkn.js +0 -1
  247. package/dist/web/assets/architecture-PBZL5I3N-nw5-j_I_.js +0 -1
  248. package/dist/web/assets/chat-tab-IcnU3nqQ.js +0 -15
  249. package/dist/web/assets/code-editor-Dno5Srkm.js +0 -10
  250. package/dist/web/assets/explorer-body-DB1L30pa.js +0 -2
  251. package/dist/web/assets/explorer-window-content-Cwlt3C3I.js +0 -1
  252. package/dist/web/assets/gitGraph-HDMCJU4V-C1xf87gN.js +0 -1
  253. package/dist/web/assets/glide-data-grid-Dz8lx6n2.js +0 -138
  254. package/dist/web/assets/group-chat-tab-DFwr-pig.js +0 -1
  255. package/dist/web/assets/index-DWpNR3nB.css +0 -2
  256. package/dist/web/assets/index-oUKV5nIA.js +0 -52
  257. package/dist/web/assets/info-3K5VOQVL-C2z7FTDe.js +0 -1
  258. package/dist/web/assets/keybindings-store-d54pv2NK.js +0 -1
  259. package/dist/web/assets/markdown-renderer-CnE36_gd.js +0 -1
  260. package/dist/web/assets/mobile-bottom-sheet-CJ7ApOkI.js +0 -1
  261. package/dist/web/assets/mobile-explorer-sheet-BzjlS1Xn.js +0 -1
  262. package/dist/web/assets/notification-store-C7UBqxYP.js +0 -1
  263. package/dist/web/assets/packet-RMMSAZCW-DQUxN0wU.js +0 -1
  264. package/dist/web/assets/pie-UPGHQEXC-BOr9ek4Q.js +0 -1
  265. package/dist/web/assets/process-table-CXC10GSy.js +0 -1
  266. package/dist/web/assets/radar-KQ55EAFF-DSErxZFb.js +0 -1
  267. package/dist/web/assets/settings-store-OLJE7dPH.js +0 -2
  268. package/dist/web/assets/settings-tab-B1rvHFc6.js +0 -3
  269. package/dist/web/assets/settings-tab-BUlmPJ2w.js +0 -1
  270. package/dist/web/assets/system-monitor-body-DPYQlBEt.js +0 -2
  271. package/dist/web/assets/system-monitor-tab-CPQdWv27.js +0 -1
  272. package/dist/web/assets/team-member-sheet-Bu2wG1sm.js +0 -2
  273. package/dist/web/assets/treemap-KZPCXAKY-C17p0LQw.js +0 -1
  274. package/dist/web/assets/window-store-CQ1lghvv.js +0 -1
  275. /package/dist/web/assets/{abap-CjzrhAS2.js → abap-CKgBNk5K.js} +0 -0
  276. /package/dist/web/assets/{actionscript-3-B59Kbl-9.js → actionscript-3-oARhAY6c.js} +0 -0
  277. /package/dist/web/assets/{ada-B7gItKAE.js → ada-2vi8ASy3.js} +0 -0
  278. /package/dist/web/assets/{andromeeda-Dy5zcy0Q.js → andromeeda-B3vMjPKd.js} +0 -0
  279. /package/dist/web/assets/{apache-B6T9jrc0.js → apache-DnIEr338.js} +0 -0
  280. /package/dist/web/assets/{apex-CTsIxDQw.js → apex-B7Qs5AY5.js} +0 -0
  281. /package/dist/web/assets/{api-client-oY1tC3tM.js → api-client-BkGKTjsq.js} +0 -0
  282. /package/dist/web/assets/{applescript-DuycKmmb.js → applescript-EbQNUCWb.js} +0 -0
  283. /package/dist/web/assets/{apply-theme-r8tKDrCR.js → apply-theme-Dd_cqGa-.js} +0 -0
  284. /package/dist/web/assets/{ara-CISL3599.js → ara-CtHdhFRw.js} +0 -0
  285. /package/dist/web/assets/{asciidoc-BXyE2Lpr.js → asciidoc-BH6qin_O.js} +0 -0
  286. /package/dist/web/assets/{asm-y258phtP.js → asm-D1XU1ACk.js} +0 -0
  287. /package/dist/web/assets/{aurora-x-Dq3Jc78M.js → aurora-x-BkhcxkuS.js} +0 -0
  288. /package/dist/web/assets/{awk-DxL6XSJv.js → awk-DijzB6o7.js} +0 -0
  289. /package/dist/web/assets/{ayu-dark-DjoK6Evo.js → ayu-dark-tysG77WC.js} +0 -0
  290. /package/dist/web/assets/{ayu-light-CKKXBLMI.js → ayu-light-Dg0WMETC.js} +0 -0
  291. /package/dist/web/assets/{ayu-mirage-DGXSrrXB.js → ayu-mirage--RsJCjC4.js} +0 -0
  292. /package/dist/web/assets/{ballerina-DZSXFmUf.js → ballerina-DT_5-l82.js} +0 -0
  293. /package/dist/web/assets/{bat-BNsZk6nY.js → bat-BRpDlkv0.js} +0 -0
  294. /package/dist/web/assets/{beancount-cPl4gJct.js → beancount--8Df-WGG.js} +0 -0
  295. /package/dist/web/assets/{berry-DGAodERk.js → berry-Bc9fo35J.js} +0 -0
  296. /package/dist/web/assets/{bibtex-DftVoRif.js → bibtex-CwyBpp9X.js} +0 -0
  297. /package/dist/web/assets/{bicep-Suioyyzh.js → bicep-DiyH2Bs7.js} +0 -0
  298. /package/dist/web/assets/{bird2-DjU24I2P.js → bird2-CHgK4a2A.js} +0 -0
  299. /package/dist/web/assets/{c-DFi8Y_nN.js → c-BM1qL6-f.js} +0 -0
  300. /package/dist/web/assets/{c3-1eSHnH5V.js → c3-D2oOjrP7.js} +0 -0
  301. /package/dist/web/assets/{cadence-DLLsFwQP.js → cadence-nS45wqF6.js} +0 -0
  302. /package/dist/web/assets/{can-open-in-ppm-in727eFQ.js → can-open-in-ppm-DwP9KU8o.js} +0 -0
  303. /package/dist/web/assets/{catppuccin-frappe-DDj0JEiY.js → catppuccin-frappe-BpnJHiCj.js} +0 -0
  304. /package/dist/web/assets/{catppuccin-latte-DPyANHyR.js → catppuccin-latte-Bb95Wj3h.js} +0 -0
  305. /package/dist/web/assets/{catppuccin-macchiato-CO-h8VTT.js → catppuccin-macchiato-Btx8c17W.js} +0 -0
  306. /package/dist/web/assets/{catppuccin-mocha-BCmbN2IZ.js → catppuccin-mocha-B4DZOUGI.js} +0 -0
  307. /package/dist/web/assets/{clarity-C0pYsZjs.js → clarity-1ps69jU6.js} +0 -0
  308. /package/dist/web/assets/{clipboard-DBLGKMrH.js → clipboard-CrHX8AYw.js} +0 -0
  309. /package/dist/web/assets/{clojure-JFr6s8iZ.js → clojure-BklC0hKV.js} +0 -0
  310. /package/dist/web/assets/{cmake-EEcLEgVy.js → cmake-B573LYAQ.js} +0 -0
  311. /package/dist/web/assets/{codeowners-Deh3Lx0u.js → codeowners-BoYeX89M.js} +0 -0
  312. /package/dist/web/assets/{codeql-C317igT5.js → codeql-7Dhf5Udk.js} +0 -0
  313. /package/dist/web/assets/{common-lisp-DepbATZs.js → common-lisp-m1nxlthv.js} +0 -0
  314. /package/dist/web/assets/{coq-MxgxU5ya.js → coq-DNhGmd4a.js} +0 -0
  315. /package/dist/web/assets/{csharp-eEFXdsAq.js → csharp-P-nAurtS.js} +0 -0
  316. /package/dist/web/assets/{css-Db-syWW9.js → css-LYbSeh3-.js} +0 -0
  317. /package/dist/web/assets/{csv-DHeYcbaq.js → csv-DHZZZm7X.js} +0 -0
  318. /package/dist/web/assets/{csv-parser-_JWMLGl3.js → csv-parser-CY9Ggtg5.js} +0 -0
  319. /package/dist/web/assets/{cue-BmUNowrt.js → cue-D8yLUOle.js} +0 -0
  320. /package/dist/web/assets/{cypher-XjXHgCNS.js → cypher-9cbW8RYA.js} +0 -0
  321. /package/dist/web/assets/{d-BWJnZtig.js → d-ZihzhxqI.js} +0 -0
  322. /package/dist/web/assets/{dark-plus-Db7umas1.js → dark-plus-C8xIA1u3.js} +0 -0
  323. /package/dist/web/assets/{dart-DSI4HYh1.js → dart-CBm6jKc7.js} +0 -0
  324. /package/dist/web/assets/{data-grid-types-CfVHh9gv.js → data-grid-types-DkEQ-Na9.js} +0 -0
  325. /package/dist/web/assets/{dax-yw1JsAni.js → dax-B3cx6YeV.js} +0 -0
  326. /package/dist/web/assets/{desktop-C2QO4RvD.js → desktop-C03SpXth.js} +0 -0
  327. /package/dist/web/assets/{diff-Drt8aEyp.js → diff-D8LnnvFY.js} +0 -0
  328. /package/dist/web/assets/{dist-DNRvhVdz.js → dist-C4XoS_zo.js} +0 -0
  329. /package/dist/web/assets/{dist-VBxOHLmN.js → dist-Do3fljss.js} +0 -0
  330. /package/dist/web/assets/{docker-ByDKbmLl.js → docker-DwFiOQWW.js} +0 -0
  331. /package/dist/web/assets/{dotenv-BkfOLytR.js → dotenv-CVJ_vFSQ.js} +0 -0
  332. /package/dist/web/assets/{dracula-DAmrm-Un.js → dracula-DMw8UACY.js} +0 -0
  333. /package/dist/web/assets/{dracula-soft-BUOEnKqR.js → dracula-soft-BnB3nDrA.js} +0 -0
  334. /package/dist/web/assets/{dream-maker-DQBIfSiO.js → dream-maker-2riXUQXo.js} +0 -0
  335. /package/dist/web/assets/{emacs-lisp-Blh5kBD3.js → emacs-lisp-BQdYaiB5.js} +0 -0
  336. /package/dist/web/assets/{everforest-dark-CEzhDxHq.js → everforest-dark-C2FPIWdj.js} +0 -0
  337. /package/dist/web/assets/{everforest-light-CPk_5Khm.js → everforest-light-DMOWf9sD.js} +0 -0
  338. /package/dist/web/assets/{fennel-B1MT_EBZ.js → fennel-JtSHp0vW.js} +0 -0
  339. /package/dist/web/assets/{fish-C_HGcewt.js → fish-CflDcRfe.js} +0 -0
  340. /package/dist/web/assets/{fluent-Dp1Iclmx.js → fluent-D4eu6Vac.js} +0 -0
  341. /package/dist/web/assets/{format-bytes-53WBtyi2.js → format-bytes-DR3228bu.js} +0 -0
  342. /package/dist/web/assets/{fortran-free-form-B_-UA4XC.js → fortran-free-form-hO4RuC9Z.js} +0 -0
  343. /package/dist/web/assets/{gdscript-Bm_JpjS_.js → gdscript-D7n3a8VM.js} +0 -0
  344. /package/dist/web/assets/{gdshader-C_PNmfYU.js → gdshader-BhUmrlzG.js} +0 -0
  345. /package/dist/web/assets/{genie-B5rmR2TF.js → genie-VbyftqBp.js} +0 -0
  346. /package/dist/web/assets/{gherkin-BTJ34TJ9.js → gherkin-BZzhvUUb.js} +0 -0
  347. /package/dist/web/assets/{github-dark-DOkNVj9H.js → github-dark-C4q41woW.js} +0 -0
  348. /package/dist/web/assets/{github-dark-default-DTsmiyVo.js → github-dark-default-CMNi7dQX.js} +0 -0
  349. /package/dist/web/assets/{github-dark-dimmed-CjbxAe9s.js → github-dark-dimmed-BRf_-LlY.js} +0 -0
  350. /package/dist/web/assets/{github-dark-high-contrast-r_M8F_k7.js → github-dark-high-contrast-xanKBsRI.js} +0 -0
  351. /package/dist/web/assets/{github-light-BzfxyMyb.js → github-light-BEQeNQCs.js} +0 -0
  352. /package/dist/web/assets/{github-light-default-BOqiBIy5.js → github-light-default-ceQrtdXg.js} +0 -0
  353. /package/dist/web/assets/{github-light-high-contrast-DQzBrDM4.js → github-light-high-contrast-doKbXzgW.js} +0 -0
  354. /package/dist/web/assets/{gleam-BWmuAtD8.js → gleam-Chzqf4fT.js} +0 -0
  355. /package/dist/web/assets/{gn-CNQPjiZW.js → gn-CCN_-6fp.js} +0 -0
  356. /package/dist/web/assets/{gnuplot-BlHjcPMe.js → gnuplot-CIplgwwP.js} +0 -0
  357. /package/dist/web/assets/{go-Bn8Ba9uz.js → go-Cjj-9dRs.js} +0 -0
  358. /package/dist/web/assets/{groovy-C1Y1_F4V.js → groovy-PqD4K7X1.js} +0 -0
  359. /package/dist/web/assets/{gruvbox-dark-hard-DueOsHav.js → gruvbox-dark-hard-BAbpUSF-.js} +0 -0
  360. /package/dist/web/assets/{gruvbox-dark-medium-DaQjD538.js → gruvbox-dark-medium-BWcWXDBB.js} +0 -0
  361. /package/dist/web/assets/{gruvbox-dark-soft-CbbX2WOm.js → gruvbox-dark-soft-Dkx1okkk.js} +0 -0
  362. /package/dist/web/assets/{gruvbox-light-hard-E2M70ljZ.js → gruvbox-light-hard-WVsDK0MM.js} +0 -0
  363. /package/dist/web/assets/{gruvbox-light-medium-B_DDLY6h.js → gruvbox-light-medium-tiR9t7gr.js} +0 -0
  364. /package/dist/web/assets/{gruvbox-light-soft-2SZmyC1F.js → gruvbox-light-soft-HzHohaX6.js} +0 -0
  365. /package/dist/web/assets/{haskell-D8DeoXtG.js → haskell-B1eIZfr6.js} +0 -0
  366. /package/dist/web/assets/{haxe-BmZm7B0W.js → haxe-BcDdi5xr.js} +0 -0
  367. /package/dist/web/assets/{hcl-CKnBM3ng.js → hcl-Dq-HKVni.js} +0 -0
  368. /package/dist/web/assets/{hjson-Ct9t4-ci.js → hjson-Dr9D9WVU.js} +0 -0
  369. /package/dist/web/assets/{hlsl-BZSASBqS.js → hlsl-Bo8t2FuZ.js} +0 -0
  370. /package/dist/web/assets/{horizon-CzX1AZ08.js → horizon-DWG5IXc-.js} +0 -0
  371. /package/dist/web/assets/{horizon-bright-CCVNeRIP.js → horizon-bright-DFr9wTTx.js} +0 -0
  372. /package/dist/web/assets/{houston-DBlJN7xc.js → houston-NAVNhkup.js} +0 -0
  373. /package/dist/web/assets/{hy-Dyc6h6Xb.js → hy-DnyvUk3B.js} +0 -0
  374. /package/dist/web/assets/{image-gallery-DujlrjOC.js → image-gallery-DoFdNF9i.js} +0 -0
  375. /package/dist/web/assets/{imba-Bauru7s2.js → imba-CPbR5W_A.js} +0 -0
  376. /package/dist/web/assets/{ini-SjdwtaTp.js → ini-lc8_Lt0y.js} +0 -0
  377. /package/dist/web/assets/{java-D2r0ocpf.js → java-B0KCo2vK.js} +0 -0
  378. /package/dist/web/assets/{javascript-y-lof7iP.js → javascript-YqEbNRpd.js} +0 -0
  379. /package/dist/web/assets/{json-DO4LSx5A.js → json-Co0WtOlh.js} +0 -0
  380. /package/dist/web/assets/{json5-BEwWf-W3.js → json5-2Jhb0QUN.js} +0 -0
  381. /package/dist/web/assets/{jsonc-OBEFv7gP.js → jsonc-e72F11nU.js} +0 -0
  382. /package/dist/web/assets/{jsonl-DOyW4Mg3.js → jsonl-Bff8IPye.js} +0 -0
  383. /package/dist/web/assets/{jsonnet-BL5R2CpP.js → jsonnet-DRCkb-_H.js} +0 -0
  384. /package/dist/web/assets/{jssm-ksPusAyg.js → jssm-C6UiwVEm.js} +0 -0
  385. /package/dist/web/assets/{jsx-B6XEu6O-.js → jsx-u1aZlnfA.js} +0 -0
  386. /package/dist/web/assets/{kanagawa-dragon-JBVe_ji_.js → kanagawa-dragon-De-RQFV3.js} +0 -0
  387. /package/dist/web/assets/{kanagawa-lotus-ChprO68_.js → kanagawa-lotus-K8elE4QU.js} +0 -0
  388. /package/dist/web/assets/{kanagawa-wave-D_uB_R0Q.js → kanagawa-wave-BrStl_zx.js} +0 -0
  389. /package/dist/web/assets/{kdl-pf5B8N_m.js → kdl-CLCfMeZy.js} +0 -0
  390. /package/dist/web/assets/{kotlin-BdF0METC.js → kotlin-DEEC3pNh.js} +0 -0
  391. /package/dist/web/assets/{kusto-BneVP-OU.js → kusto-DTqRhCup.js} +0 -0
  392. /package/dist/web/assets/{laserwave-C4-UPIyG.js → laserwave-DsgpUSbA.js} +0 -0
  393. /package/dist/web/assets/{lean-BN6Jdymq.js → lean-Dl9MI0Y3.js} +0 -0
  394. /package/dist/web/assets/{less-Cx-fdlDC.js → less-Bg21rao6.js} +0 -0
  395. /package/dist/web/assets/{lib-CMuBa85c.js → lib-CXUwxw1M.js} +0 -0
  396. /package/dist/web/assets/{libesm-BB74PRsH.js → libesm-CVkncAQz.js} +0 -0
  397. /package/dist/web/assets/{light-plus-DFnnn_iL.js → light-plus-DGNbbK1V.js} +0 -0
  398. /package/dist/web/assets/{llvm-BGxtW0Nw.js → llvm-91_ZX3p-.js} +0 -0
  399. /package/dist/web/assets/{loader-circle-Dpqcv0eS.js → loader-circle-DouKgIXy.js} +0 -0
  400. /package/dist/web/assets/{lock-DldbT31x.js → lock-CNxUGpTk.js} +0 -0
  401. /package/dist/web/assets/{log-kQb4eJnj.js → log-DSXzbq8u.js} +0 -0
  402. /package/dist/web/assets/{logo-DqAzaq7s.js → logo-BQRNd4tc.js} +0 -0
  403. /package/dist/web/assets/{luau-Dc2cp-zS.js → luau-BE-TMpiZ.js} +0 -0
  404. /package/dist/web/assets/{make-DYg9dwaJ.js → make-ePYGKTpc.js} +0 -0
  405. /package/dist/web/assets/{markdown-O4EPpS3K.js → markdown-Cst3wgMW.js} +0 -0
  406. /package/dist/web/assets/{material-theme-ClP8yfQ2.js → material-theme-9XyvnNLb.js} +0 -0
  407. /package/dist/web/assets/{material-theme-darker-LOK55hGu.js → material-theme-darker-H1VN4MTS.js} +0 -0
  408. /package/dist/web/assets/{material-theme-lighter-j72tzZqi.js → material-theme-lighter-DAreBbuF.js} +0 -0
  409. /package/dist/web/assets/{material-theme-ocean-C19LKClA.js → material-theme-ocean-BpTR-3II.js} +0 -0
  410. /package/dist/web/assets/{material-theme-palenight-DJ5tN2ax.js → material-theme-palenight-DUNzX711.js} +0 -0
  411. /package/dist/web/assets/{matlab-DQNZ_NU8.js → matlab-BpYNufBU.js} +0 -0
  412. /package/dist/web/assets/{mdx-CJayB7XK.js → mdx-BYjfe1DK.js} +0 -0
  413. /package/dist/web/assets/{mermaid-Cb0TRX92.js → mermaid-DPCcIW9B.js} +0 -0
  414. /package/dist/web/assets/{min-dark-CWM8Bo-m.js → min-dark-DnGOAGCo.js} +0 -0
  415. /package/dist/web/assets/{min-light-BBR1sjS2.js → min-light-CwV-o31y.js} +0 -0
  416. /package/dist/web/assets/{mipsasm-Jc7u7YSS.js → mipsasm-DdeCPC97.js} +0 -0
  417. /package/dist/web/assets/{mojo-CTB0cutd.js → mojo-BmxxZ3iM.js} +0 -0
  418. /package/dist/web/assets/{monitor-4vWfqzuN.js → monitor-C835ucWH.js} +0 -0
  419. /package/dist/web/assets/{monokai-CvM_YNKx.js → monokai-Cx6cQ-MA.js} +0 -0
  420. /package/dist/web/assets/{moonbit-CbFEfYYM.js → moonbit-D2-Gj5vj.js} +0 -0
  421. /package/dist/web/assets/{move-BT38p0kT.js → move-DSWGoQa8.js} +0 -0
  422. /package/dist/web/assets/{narrat-55QZ_kOG.js → narrat-D9TnZSSu.js} +0 -0
  423. /package/dist/web/assets/{nextflow-groovy-Bg8peyqg.js → nextflow-groovy-BbL-8PwD.js} +0 -0
  424. /package/dist/web/assets/{night-owl-DzS77iPK.js → night-owl-CZitKm5d.js} +0 -0
  425. /package/dist/web/assets/{night-owl-light-DER7fGnx.js → night-owl-light-DJVX1Pno.js} +0 -0
  426. /package/dist/web/assets/{nix-BSI8YesN.js → nix-R6hr3oln.js} +0 -0
  427. /package/dist/web/assets/{nord-9bn7iNWo.js → nord-ChGHya3l.js} +0 -0
  428. /package/dist/web/assets/{nushell-qDVZn_lw.js → nushell-QQWB-GWx.js} +0 -0
  429. /package/dist/web/assets/{objective-c-Blf-ATos.js → objective-c-C0pZ6y73.js} +0 -0
  430. /package/dist/web/assets/{objective-cpp-Bz8bAq47.js → objective-cpp-BdzVM31a.js} +0 -0
  431. /package/dist/web/assets/{ocaml-B9heAsc3.js → ocaml-D4KktNlS.js} +0 -0
  432. /package/dist/web/assets/{odin-BaFYnOwO.js → odin-Y-fypmGT.js} +0 -0
  433. /package/dist/web/assets/{one-dark-pro-D6B_hSe-.js → one-dark-pro-BwecIxQL.js} +0 -0
  434. /package/dist/web/assets/{one-light-Ba-f0wyk.js → one-light-Cz7UVzLU.js} +0 -0
  435. /package/dist/web/assets/{openscad-UvYyL2MK.js → openscad-DtRe3qYD.js} +0 -0
  436. /package/dist/web/assets/{pascal-B4-qvEoG.js → pascal-Ck6I1fGG.js} +0 -0
  437. /package/dist/web/assets/{pkl-B146nqGx.js → pkl-DdFK94VW.js} +0 -0
  438. /package/dist/web/assets/{plastic-L38t-Id3.js → plastic-BQSAzZwR.js} +0 -0
  439. /package/dist/web/assets/{play-CpCAoQRZ.js → play-CJlWpVMw.js} +0 -0
  440. /package/dist/web/assets/{plsql-ItOG5Cca.js → plsql-DjXg_CP0.js} +0 -0
  441. /package/dist/web/assets/{po-imiCTaNX.js → po-Bw2SuM4r.js} +0 -0
  442. /package/dist/web/assets/{poimandres-Bu6sIba9.js → poimandres-YMK6c5VN.js} +0 -0
  443. /package/dist/web/assets/{polar-CTeGoFxV.js → polar-BMu17-O0.js} +0 -0
  444. /package/dist/web/assets/{portal-container-context-a399Hd9F.js → portal-container-context-DSGdlXrJ.js} +0 -0
  445. /package/dist/web/assets/{postcss-jvFe6GNE.js → postcss-DWib10RZ.js} +0 -0
  446. /package/dist/web/assets/{powerquery-D9Dw1hJp.js → powerquery-Dht4CK0b.js} +0 -0
  447. /package/dist/web/assets/{powershell-CZqvYQtg.js → powershell-DvrCg8ji.js} +0 -0
  448. /package/dist/web/assets/{prisma-B4D2nO_7.js → prisma-DbodYvCM.js} +0 -0
  449. /package/dist/web/assets/{prolog-CQbyUcCF.js → prolog-DZ-m6ysB.js} +0 -0
  450. /package/dist/web/assets/{proto-Cwpo0cW1.js → proto-HHpG3tGO.js} +0 -0
  451. /package/dist/web/assets/{puppet-CKxCwBod.js → puppet-BljHe_75.js} +0 -0
  452. /package/dist/web/assets/{purescript-B-MyXMMp.js → purescript-DzCiFR0B.js} +0 -0
  453. /package/dist/web/assets/{python-D6QbSR2S.js → python-B5BQ27cC.js} +0 -0
  454. /package/dist/web/assets/{qmldir-BpTSVcw_.js → qmldir-CYlcXrnn.js} +0 -0
  455. /package/dist/web/assets/{qss-DBc4gbpe.js → qss-Ce7T_TEg.js} +0 -0
  456. /package/dist/web/assets/{r-i9jMMi0s.js → r-Bku-2Jp_.js} +0 -0
  457. /package/dist/web/assets/{racket-BWNaxU7C.js → racket-DtfSL3WR.js} +0 -0
  458. /package/dist/web/assets/{raku-fqTZLn7L.js → raku-CnDLsBJr.js} +0 -0
  459. /package/dist/web/assets/{react-X4jUntEf.js → react-Dfio3PfK.js} +0 -0
  460. /package/dist/web/assets/{red-D-JEcuzA.js → red-vbqcE9b-.js} +0 -0
  461. /package/dist/web/assets/{refresh-cw-B9iv7csb.js → refresh-cw-84mz8aOE.js} +0 -0
  462. /package/dist/web/assets/{reg-BbirlU1S.js → reg-DIDLT8-A.js} +0 -0
  463. /package/dist/web/assets/{regexp-D8VTFrtg.js → regexp-CB4h-VPu.js} +0 -0
  464. /package/dist/web/assets/{rel-ByQGKiNM.js → rel-CiQEnWXa.js} +0 -0
  465. /package/dist/web/assets/{resolve-theme-Cqb9zWYc.js → resolve-theme-DPTQHAy1.js} +0 -0
  466. /package/dist/web/assets/{riscv-B5nqCfLE.js → riscv-R78iq5Rf.js} +0 -0
  467. /package/dist/web/assets/{ron-DOVh_iZR.js → ron-DhYENxc4.js} +0 -0
  468. /package/dist/web/assets/{rose-pine-jp51ZUzi.js → rose-pine-5wBeyjfJ.js} +0 -0
  469. /package/dist/web/assets/{rose-pine-dawn-B7hkmmWy.js → rose-pine-dawn-FpKtPCuH.js} +0 -0
  470. /package/dist/web/assets/{rose-pine-moon-Ff063yLj.js → rose-pine-moon-BwcIYc7C.js} +0 -0
  471. /package/dist/web/assets/{rosmsg-Cyk7Il8z.js → rosmsg-DWIGAYMl.js} +0 -0
  472. /package/dist/web/assets/{rotate-ccw-DnuPzJRE.js → rotate-ccw-CNTk2J7l.js} +0 -0
  473. /package/dist/web/assets/{rust-DRNM5epF.js → rust-DU_ofKgt.js} +0 -0
  474. /package/dist/web/assets/{sass-D_cB0-M1.js → sass-U9SUuYEy.js} +0 -0
  475. /package/dist/web/assets/{save-DxonD4y1.js → save-CtW9ojKk.js} +0 -0
  476. /package/dist/web/assets/{scala-Crl-1kKS.js → scala-op4gjA6j.js} +0 -0
  477. /package/dist/web/assets/{scheme-DCaWyEJ0.js → scheme-tEjkR8rL.js} +0 -0
  478. /package/dist/web/assets/{sdbl-BbXNqwtX.js → sdbl-C6SSRmZg.js} +0 -0
  479. /package/dist/web/assets/{send-B-t5Mz26.js → send-C_sLFt56.js} +0 -0
  480. /package/dist/web/assets/{shallow-4zAfs2KW.js → shallow-C1YoJQA-.js} +0 -0
  481. /package/dist/web/assets/{shellscript-BAgksBhW.js → shellscript-Bf2KiwQj.js} +0 -0
  482. /package/dist/web/assets/{shield-check-CdTiK1j2.js → shield-check-Du6HKYn9.js} +0 -0
  483. /package/dist/web/assets/{shield-off-DuetkXKM.js → shield-off-CWKb1MQL.js} +0 -0
  484. /package/dist/web/assets/{slack-dark-GB-ElUW_.js → slack-dark-oGgCey8r.js} +0 -0
  485. /package/dist/web/assets/{slack-ochin-tU6CFD3S.js → slack-ochin-CemlB8cE.js} +0 -0
  486. /package/dist/web/assets/{smalltalk-DSVGUbtL.js → smalltalk-CTE-6jrZ.js} +0 -0
  487. /package/dist/web/assets/{snazzy-light-DZhi3PoS.js → snazzy-light-DoEIGnmz.js} +0 -0
  488. /package/dist/web/assets/{solarized-dark-D6AMCco7.js → solarized-dark-CStiJyBg.js} +0 -0
  489. /package/dist/web/assets/{solarized-light-DepuTWSE.js → solarized-light-CfuNycS9.js} +0 -0
  490. /package/dist/web/assets/{solidity-CsxN618l.js → solidity-DL6sO5rd.js} +0 -0
  491. /package/dist/web/assets/{splunk-CF1m0Kkv.js → splunk-yK4ZUFBB.js} +0 -0
  492. /package/dist/web/assets/{sql-DvyMDgrB.js → sql-Lw03eoN8.js} +0 -0
  493. /package/dist/web/assets/{ssh-config-a8sDo-Q8.js → ssh-config-DeRp25TR.js} +0 -0
  494. /package/dist/web/assets/{stylus-DCJ-6AjT.js → stylus-D8OMsPh-.js} +0 -0
  495. /package/dist/web/assets/{swift-c-ZFxyPP.js → swift-nsl9stPV.js} +0 -0
  496. /package/dist/web/assets/{synthwave-84-9NPgBcuv.js → synthwave-84-exAgsgpf.js} +0 -0
  497. /package/dist/web/assets/{system-verilog-DPD02IOo.js → system-verilog-DBWf00T6.js} +0 -0
  498. /package/dist/web/assets/{systemd-BUr5HRtU.js → systemd-nhu27Dw5.js} +0 -0
  499. /package/dist/web/assets/{table-FLU8ei8b.js → table-GAR4upb6.js} +0 -0
  500. /package/dist/web/assets/{talonscript-BNBQV5YJ.js → talonscript-CNOEcGDc.js} +0 -0
  501. /package/dist/web/assets/{tasl-BETjpSyu.js → tasl-CjYZenSH.js} +0 -0
  502. /package/dist/web/assets/{tcl-Clp_AZiC.js → tcl-BVA09RDz.js} +0 -0
  503. /package/dist/web/assets/{terraform-B0kZ0lim.js → terraform-C5EloH1q.js} +0 -0
  504. /package/dist/web/assets/{text-wrap-B05VGRa-.js → text-wrap-D71Jqz5U.js} +0 -0
  505. /package/dist/web/assets/{tokyo-night-oOJ0a7Dk.js → tokyo-night-D764DdhO.js} +0 -0
  506. /package/dist/web/assets/{toml-DOPCSumy.js → toml-CCDYblSB.js} +0 -0
  507. /package/dist/web/assets/{trash-2-CTXlTflF.js → trash-2-C08fhIg1.js} +0 -0
  508. /package/dist/web/assets/{triangle-alert-BRq0VAfd.js → triangle-alert-85BQgs3T.js} +0 -0
  509. /package/dist/web/assets/{tsv-D6qUGf84.js → tsv-BZ4sA05e.js} +0 -0
  510. /package/dist/web/assets/{tsx-B6qYhT6O.js → tsx-DPLBzAt5.js} +0 -0
  511. /package/dist/web/assets/{turtle-C0Rjzw73.js → turtle-CwGHCq40.js} +0 -0
  512. /package/dist/web/assets/{typescript-BiydShEu.js → typescript-CUH0vBAl.js} +0 -0
  513. /package/dist/web/assets/{typespec-DjmF6Aww.js → typespec-Bq31Vq4u.js} +0 -0
  514. /package/dist/web/assets/{typst-CPuscfcL.js → typst-MD9BR9W5.js} +0 -0
  515. /package/dist/web/assets/{use-is-mobile-b94q8RvF.js → use-is-mobile-DUuki97q.js} +0 -0
  516. /package/dist/web/assets/{utils-C9OjCrou.js → utils-u2fbYPrY.js} +0 -0
  517. /package/dist/web/assets/{v-DE4vpLLQ.js → v-DZV7IxIT.js} +0 -0
  518. /package/dist/web/assets/{vala-CmUF2pS5.js → vala-urd1zi9S.js} +0 -0
  519. /package/dist/web/assets/{vb-BvcRoX4b.js → vb-DIZU3zbJ.js} +0 -0
  520. /package/dist/web/assets/{vendor-xterm-K8kpLxdb.js → vendor-xterm-DCj0zgSC.js} +0 -0
  521. /package/dist/web/assets/{verilog-BGIzyH3R.js → verilog-Ndz_ciTA.js} +0 -0
  522. /package/dist/web/assets/{vesper-CG7IrHQQ.js → vesper-C86M7LJp.js} +0 -0
  523. /package/dist/web/assets/{vhdl-DMSumomw.js → vhdl-_klwDUjd.js} +0 -0
  524. /package/dist/web/assets/{viml-Drt7AEmU.js → viml-DzRXWoQh.js} +0 -0
  525. /package/dist/web/assets/{vitesse-black-DmsjfN0b.js → vitesse-black-TWZ20_3U.js} +0 -0
  526. /package/dist/web/assets/{vitesse-dark-Dl55_WeJ.js → vitesse-dark-Dl7R6fy9.js} +0 -0
  527. /package/dist/web/assets/{vitesse-light-Crbv9aGg.js → vitesse-light-Cs0Dt2_n.js} +0 -0
  528. /package/dist/web/assets/{vyper-CB7psEJj.js → vyper-qnCVj8ia.js} +0 -0
  529. /package/dist/web/assets/{wasm-Dck8KPgP.js → wasm-Ckx9jBPe.js} +0 -0
  530. /package/dist/web/assets/{wasm-BY1tuM5m.js → wasm-Dy-SJ3W9.js} +0 -0
  531. /package/dist/web/assets/{wenyan-DhewQ68b.js → wenyan-DTEy2Lx-.js} +0 -0
  532. /package/dist/web/assets/{wgsl-DlmiP456.js → wgsl-DC7n63Sy.js} +0 -0
  533. /package/dist/web/assets/{wikitext-cQCnqNgE.js → wikitext-1zFoZiW6.js} +0 -0
  534. /package/dist/web/assets/{wit-S5MaCngB.js → wit-DtTfCRph.js} +0 -0
  535. /package/dist/web/assets/{wolfram-Bh8n_9zr.js → wolfram-B2Bgu1KL.js} +0 -0
  536. /package/dist/web/assets/{x-YDu4m2Iq.js → x-CeN0Qybc.js} +0 -0
  537. /package/dist/web/assets/{yaml-BoK_17l9.js → yaml-B-Jxk0IQ.js} +0 -0
  538. /package/dist/web/assets/{zenscript-DB3ZFwNJ.js → zenscript-BWRfvNER.js} +0 -0
  539. /package/dist/web/assets/{zig-Bv1BWmfT.js → zig-CkwuvETm.js} +0 -0
@@ -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
+ }));
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Multi-touch gesture engine for the mobile remote-desktop viewer: pinch-zoom/pan (CSS
3
+ * transform on the caller's "stage" element) plus tap/drag → pointer + wheel messages over the
4
+ * shared connection. Two modes (see design doc `research-260907-1538-remote-desktop-mobile-
5
+ * touch.md`): TOUCH maps a 1-finger drag straight to an absolute drag at the touch point;
6
+ * MOUSE keeps a client-side virtual cursor that a 1-finger drag moves *relatively*, with a tap
7
+ * clicking wherever the cursor currently sits. Two-finger gestures are shared by both modes:
8
+ * tap = right click, pinch = zoom, drag = pan (while zoomed) or scroll wheel (otherwise). The
9
+ * actual decision rules live in `remote-desktop-touch-gesture-tracker.ts`; this file only wires
10
+ * DOM touch events to them and to React state.
11
+ *
12
+ * All touch listeners are native (`addEventListener`, non-passive) rather than React props, so
13
+ * `stopPropagation()` here also stops the ancestor `BottomSheet`'s swipe-to-dismiss (a React
14
+ * synthetic handler) from ever seeing the event — same trick `mobile-explorer-sheet.tsx` uses
15
+ * for its inline-rename case.
16
+ */
17
+ import { useEffect, useRef, useState, useCallback } from "react";
18
+ import { letterboxedContentRect, type ZoomPanTransform, type Fraction, type Rect } from "./remote-desktop-coords";
19
+ import {
20
+ beginSingleTouch, advanceSingleTouch, resolveSingleTouchEnd,
21
+ beginTwoFingerTouch, advanceTwoFingerTouch, resolveTwoFingerEnd,
22
+ type SingleTrack, type TwoFingerTrack, type RemoteDesktopInputMode,
23
+ } from "./remote-desktop-touch-gesture-tracker";
24
+
25
+ export type { RemoteDesktopInputMode };
26
+
27
+ const IDENTITY_TRANSFORM: ZoomPanTransform = { scale: 1, panX: 0, panY: 0 };
28
+ /** CSS px of two-finger drag per wheel notch (`WHEEL_DELTA` = 120 units, matching a real wheel
29
+ * "click"). Tune here if a live test finds scrolling too fast/slow. */
30
+ const WHEEL_PIXELS_PER_NOTCH = 40;
31
+ const WHEEL_DELTA = 120;
32
+
33
+ export interface UseRemoteDesktopTouchOptions {
34
+ /** The never-transformed element touch listeners attach to and whose rect is the reference
35
+ * frame at `scale=1, pan=0` — must fill the same box the "stage" (canvas) sits in at rest. */
36
+ containerRef: React.RefObject<HTMLElement | null>;
37
+ /** The canvas actually being drawn into. Its CSS box is the same size as `containerRef` (it
38
+ * fills the stage, which fills the container), but its drawing-buffer size (`.width`/
39
+ * `.height`, the real capture resolution) is almost never the same *aspect ratio* — a tap
40
+ * must be measured against the letterboxed video rect this produces inside the container,
41
+ * not the container's full box, or it lands off by however big the letterbox bars are. */
42
+ canvasRef: React.RefObject<HTMLCanvasElement | null>;
43
+ mode: RemoteDesktopInputMode;
44
+ sendMessage: (msg: Record<string, unknown>) => void;
45
+ enabled: boolean;
46
+ }
47
+
48
+ export interface UseRemoteDesktopTouchResult {
49
+ /** Apply as `transform: translate(panXpx, panYpx) scale(scale)` on the stage element. */
50
+ transform: ZoomPanTransform;
51
+ /** Client-side cursor position (mouse mode only) — draw a marker at this fraction inside the
52
+ * stage. `null` in touch mode (no persistent cursor concept there). */
53
+ virtualCursor: Fraction | null;
54
+ resetZoom: () => void;
55
+ }
56
+
57
+ export function useRemoteDesktopTouch({
58
+ containerRef,
59
+ canvasRef,
60
+ mode,
61
+ sendMessage,
62
+ enabled,
63
+ }: UseRemoteDesktopTouchOptions): UseRemoteDesktopTouchResult {
64
+ const [transform, setTransform] = useState<ZoomPanTransform>(IDENTITY_TRANSFORM);
65
+ const [virtualCursor, setVirtualCursor] = useState<Fraction>({ xFrac: 0.5, yFrac: 0.5 });
66
+ const transformRef = useRef(transform);
67
+ const cursorRef = useRef(virtualCursor);
68
+ const modeRef = useRef(mode);
69
+ modeRef.current = mode;
70
+
71
+ const pendingRef = useRef<{ transform: ZoomPanTransform | null; cursor: Fraction | null; sendCursorMove: boolean; wheelPx: number }>(
72
+ { transform: null, cursor: null, sendCursorMove: false, wheelPx: 0 },
73
+ );
74
+ const rafIdRef = useRef(0);
75
+
76
+ const flush = useCallback(() => {
77
+ rafIdRef.current = 0;
78
+ const p = pendingRef.current;
79
+ if (p.transform) { transformRef.current = p.transform; setTransform(p.transform); p.transform = null; }
80
+ if (p.cursor) {
81
+ cursorRef.current = p.cursor;
82
+ setVirtualCursor(p.cursor);
83
+ if (p.sendCursorMove) sendMessage({ type: "pointer", xFrac: p.cursor.xFrac, yFrac: p.cursor.yFrac, button: null, down: null });
84
+ p.cursor = null;
85
+ p.sendCursorMove = false;
86
+ }
87
+ if (p.wheelPx !== 0) {
88
+ // Fingers moving up (negative px) reads as "scroll content up" — a positive (forward)
89
+ // wheel notch, matching a physical wheel rotated away from the user.
90
+ sendMessage({ type: "wheel", dy: -(p.wheelPx / WHEEL_PIXELS_PER_NOTCH) * WHEEL_DELTA });
91
+ p.wheelPx = 0;
92
+ }
93
+ }, [sendMessage]);
94
+
95
+ const schedule = useCallback(() => {
96
+ if (!rafIdRef.current) rafIdRef.current = requestAnimationFrame(flush);
97
+ }, [flush]);
98
+
99
+ const resetZoom = useCallback(() => {
100
+ transformRef.current = IDENTITY_TRANSFORM;
101
+ setTransform(IDENTITY_TRANSFORM);
102
+ }, []);
103
+
104
+ useEffect(() => {
105
+ const container = containerRef.current;
106
+ if (!container || !enabled) return;
107
+
108
+ let single: SingleTrack | null = null;
109
+ let two: TwoFingerTrack | null = null;
110
+ const sendAll = (msgs: Record<string, unknown>[]) => msgs.forEach(sendMessage);
111
+
112
+ // The container's rect (never transformed) is only the OUTER box the video is letterboxed
113
+ // within — `object-fit: contain` (or the matching sizing on the canvas) means the canvas
114
+ // element's CSS box fills that outer box, but the pixels it actually draws are narrower or
115
+ // shorter depending on how the capture's aspect ratio compares to the phone's. Without this,
116
+ // gestures were measured against the full outer box, so a tap landed at the finger's raw
117
+ // screen position instead of the corresponding video pixel whenever there was a letterbox.
118
+ const getVideoRect = (): Rect => {
119
+ const box = container.getBoundingClientRect();
120
+ const canvas = canvasRef.current;
121
+ if (!canvas || !canvas.width || !canvas.height) return box;
122
+ return letterboxedContentRect(box, canvas.width, canvas.height);
123
+ };
124
+
125
+ const onTouchStart = (e: TouchEvent) => {
126
+ e.preventDefault();
127
+ e.stopPropagation();
128
+ const rect = getVideoRect();
129
+ if (e.touches.length === 1) {
130
+ two = null;
131
+ const t = e.touches[0]!;
132
+ single = beginSingleTouch(t.clientX, t.clientY, rect);
133
+ } else if (e.touches.length === 2) {
134
+ single = null;
135
+ const [a, b] = [e.touches[0]!, e.touches[1]!];
136
+ two = beginTwoFingerTouch({ x: a.clientX, y: a.clientY }, { x: b.clientX, y: b.clientY }, transformRef.current.scale);
137
+ } else {
138
+ // 3+ fingers: not a gesture this viewer understands — drop tracking rather than guess.
139
+ single = null;
140
+ two = null;
141
+ }
142
+ };
143
+
144
+ const onTouchMove = (e: TouchEvent) => {
145
+ e.preventDefault();
146
+ e.stopPropagation();
147
+ if (single && e.touches.length === 1) {
148
+ const t = e.touches[0]!;
149
+ const r = advanceSingleTouch(single, t.clientX, t.clientY, modeRef.current, transformRef.current, cursorRef.current);
150
+ if (r.message) sendMessage(r.message);
151
+ if (r.cursor) {
152
+ // Mouse mode is DELTA-based (each call moves the cursor *relative* to `cursorRef`).
153
+ // rAF coalesces the state-commit + wire-send below to once per frame, but the delta
154
+ // math itself must not wait for that — updating `cursorRef.current` only inside
155
+ // `flush()` meant back-to-back touchmove events landing in the same animation frame
156
+ // each recomputed their delta against the SAME stale base position, so only the last
157
+ // one "counted" and most of the finger's movement was silently dropped (the host
158
+ // cursor barely moved). Updating the ref here, synchronously, makes every event
159
+ // compound on the last regardless of how many land before the next flush.
160
+ cursorRef.current = r.cursor;
161
+ pendingRef.current.cursor = r.cursor;
162
+ pendingRef.current.sendCursorMove = !!r.sendCursorMove;
163
+ schedule();
164
+ }
165
+ return;
166
+ }
167
+ if (two && e.touches.length === 2) {
168
+ const [a, b] = [e.touches[0]!, e.touches[1]!];
169
+ const r = advanceTwoFingerTouch(two, { x: a.clientX, y: a.clientY }, { x: b.clientX, y: b.clientY }, transformRef.current);
170
+ if (r.transform) {
171
+ // Same reasoning as the cursor above: pan is delta-based off `transformRef`, so it
172
+ // must be updated eagerly too, not just inside the throttled flush.
173
+ transformRef.current = r.transform;
174
+ pendingRef.current.transform = r.transform;
175
+ schedule();
176
+ }
177
+ if (r.wheelDeltaPx) { pendingRef.current.wheelPx += r.wheelDeltaPx; schedule(); }
178
+ }
179
+ };
180
+
181
+ const onTouchEnd = (e: TouchEvent) => {
182
+ e.preventDefault();
183
+ e.stopPropagation();
184
+ const t = e.changedTouches[0];
185
+ if (single) {
186
+ const x = t ? t.clientX : single.last.x;
187
+ const y = t ? t.clientY : single.last.y;
188
+ sendAll(resolveSingleTouchEnd(single, x, y, modeRef.current, transformRef.current, cursorRef.current));
189
+ single = null;
190
+ }
191
+ if (two) {
192
+ sendAll(resolveTwoFingerEnd(two, getVideoRect(), transformRef.current));
193
+ two = null;
194
+ }
195
+ };
196
+
197
+ const onTouchCancel = (e: TouchEvent) => {
198
+ e.stopPropagation();
199
+ // A held button left down on the host would be worse than a dropped click — release it.
200
+ // Only touch mode ever sends a "down" during a drag (mouse mode only moves the virtual
201
+ // cursor until a tap), so only touch mode has anything to release here.
202
+ if (single?.moved && modeRef.current === "touch") {
203
+ sendAll(resolveSingleTouchEnd(single, single.last.x, single.last.y, "touch", transformRef.current, cursorRef.current));
204
+ }
205
+ single = null;
206
+ two = null;
207
+ };
208
+
209
+ container.addEventListener("touchstart", onTouchStart, { passive: false });
210
+ container.addEventListener("touchmove", onTouchMove, { passive: false });
211
+ container.addEventListener("touchend", onTouchEnd, { passive: false });
212
+ container.addEventListener("touchcancel", onTouchCancel, { passive: false });
213
+
214
+ return () => {
215
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
216
+ container.removeEventListener("touchstart", onTouchStart);
217
+ container.removeEventListener("touchmove", onTouchMove);
218
+ container.removeEventListener("touchend", onTouchEnd);
219
+ container.removeEventListener("touchcancel", onTouchCancel);
220
+ };
221
+ }, [containerRef, canvasRef, enabled, sendMessage, schedule]);
222
+
223
+ return { transform, virtualCursor: mode === "mouse" ? virtualCursor : null, resetZoom };
224
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Forwards keydown/keyup from a hidden `<input>` to the shared connection, so a toolbar
3
+ * "Keyboard" button can bring up the phone's soft keyboard without a physical keyboard ever
4
+ * being attached. Mirrors `use-remote-input-capture.ts`'s key handling exactly: `code`
5
+ * (physical key, layout-independent) is sent, never `key` — the host maps `code` → VK.
6
+ */
7
+ import { useEffect, useRef, useCallback } from "react";
8
+
9
+ export interface UseRemoteDesktopVirtualKeyboardResult {
10
+ inputRef: React.RefObject<HTMLInputElement | null>;
11
+ /** Focus the hidden input, which brings up the soft keyboard on a touch device. */
12
+ show: () => void;
13
+ }
14
+
15
+ export function useRemoteDesktopVirtualKeyboard(
16
+ sendMessage: (msg: Record<string, unknown>) => void,
17
+ enabled: boolean,
18
+ ): UseRemoteDesktopVirtualKeyboardResult {
19
+ const inputRef = useRef<HTMLInputElement | null>(null);
20
+
21
+ useEffect(() => {
22
+ const input = inputRef.current;
23
+ if (!input || !enabled) return;
24
+
25
+ // A lost keyup (blur, tab hidden, WS drop) must not leave a modifier logically held on the
26
+ // host — force a release on every path that could lose the matching keyup.
27
+ const releaseAll = () => sendMessage({ type: "releaseAll" });
28
+ const onKeyDown = (e: KeyboardEvent) => { e.preventDefault(); sendMessage({ type: "key", code: e.code, down: true }); };
29
+ const onKeyUp = (e: KeyboardEvent) => { e.preventDefault(); sendMessage({ type: "key", code: e.code, down: false }); };
30
+ const onBlur = () => releaseAll();
31
+ const onVisibilityChange = () => { if (document.hidden) releaseAll(); };
32
+
33
+ input.addEventListener("keydown", onKeyDown);
34
+ input.addEventListener("keyup", onKeyUp);
35
+ input.addEventListener("blur", onBlur);
36
+ document.addEventListener("visibilitychange", onVisibilityChange);
37
+
38
+ return () => {
39
+ input.removeEventListener("keydown", onKeyDown);
40
+ input.removeEventListener("keyup", onKeyUp);
41
+ input.removeEventListener("blur", onBlur);
42
+ document.removeEventListener("visibilitychange", onVisibilityChange);
43
+ releaseAll();
44
+ };
45
+ }, [enabled, sendMessage]);
46
+
47
+ const show = useCallback(() => inputRef.current?.focus(), []);
48
+
49
+ return { inputRef, show };
50
+ }