@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
@@ -10,11 +10,13 @@ import type { ChatWsClientMessage, SessionPhase } from "../../types/api.ts";
10
10
  // — a chat socket is not guaranteed to exist now that chat tabs mount lazily.
11
11
  import { broadcastGlobalEvent } from "./global.ts";
12
12
  import { bashOutputSpy } from "../../services/bash-output-spy.ts";
13
+ import { nestedSubagentSpy } from "../../services/nested-subagent-spy.ts";
14
+ import { resolveSessionDir } from "../../services/subagent-transcript-merger.ts";
13
15
  import { backgroundShellRegistry } from "../../services/background-shell-registry.ts";
14
16
  import { basename } from "node:path";
15
17
  import { configService } from "../../services/config.service.ts";
16
18
  import { formatTurnUsageLog } from "../../shared/turn-usage.ts";
17
- import { isTerminalAgentStatus } from "../../shared/background-agent-status.ts";
19
+ import { isAsyncAgentLaunchAck, isTerminalAgentStatus } from "../../shared/background-agent-status.ts";
18
20
  import { cacheReleaseDelayMs, selectWarmIdleEvictions } from "../../services/subprocess-retention.ts";
19
21
 
20
22
  /** Resolve the SESSION's provider config — not the global default provider's.
@@ -47,10 +49,33 @@ function resolveSessionThinkingEnabled(sessionId: string): boolean {
47
49
  }
48
50
 
49
51
  const PING_INTERVAL_MS = 15_000; // 15s keepalive
50
- const CLEANUP_TIMEOUT_MS = 5 * 60_000; // 5min after Claude done + no FE
51
- /** How many clientless sessions may hold a live SDK subprocess at once. */
52
- const MAX_WARM_IDLE_SESSIONS = 5;
52
+ /**
53
+ * When an abandoned session's entry is dropped.
54
+ *
55
+ * Deliberately *not* derived from the cache window. This timer also drops `activeSessions`
56
+ * — which carries up to MAX_TURN_EVENTS buffered events per session, tool results included —
57
+ * plus team watchers and the background-shell registry. Stretching it to the cache window
58
+ * held all of that for an hour to protect a subprocess it knows nothing about.
59
+ *
60
+ * It still must not preempt `scheduleSubprocessRelease`, so when that release is pending the
61
+ * timer reschedules itself instead of running. The two stay independent, and the long wait
62
+ * happens only for sessions that actually have a warm subprocess to protect.
63
+ */
64
+ const CLEANUP_TIMEOUT_MS = 5 * 60_000;
65
+ /**
66
+ * How many clientless sessions may hold a live SDK subprocess at once.
67
+ *
68
+ * This, not the retention window, is what bounds the memory: a longer window keeps these
69
+ * slots filled for longer but never adds a slot. At ~350MB per subprocess, 10 costs ~3.5GB.
70
+ */
71
+ const MAX_WARM_IDLE_SESSIONS = 10;
53
72
  const MAX_TURN_EVENTS = 10_000; // memory safety cap
73
+ /**
74
+ * Share of the turn buffer nested-agent children may take. They are restored
75
+ * from disk on reload anyway; this only keeps a chatty grandchild from evicting
76
+ * the turn's own later events out of a reconnecting client's replay.
77
+ */
78
+ const MAX_NESTED_TURN_EVENTS = 2_000;
54
79
  const BUFFERABLE_TYPES = new Set([
55
80
  "text", "thinking", "tool_use", "tool_result",
56
81
  "approval_request", "error", "done", "account_info", "account_retry",
@@ -91,8 +116,10 @@ interface SessionEntry {
91
116
  lastImplicitTeamProbe?: number;
92
117
  /** Compact indicator state — sticky until turn ends or boundary received, synced on reconnect */
93
118
  compactStatus?: "compacting" | null;
94
- /** toolUseIds of Bash calls launched with run_in_background — spy is kept alive for these */
119
+ /** toolUseIds of Bash/Agent calls launched with run_in_background — their spy outlives the tool_result */
95
120
  backgroundToolUseIds?: Set<string>;
121
+ /** Nested-agent children buffered into turnEvents this turn (see MAX_NESTED_TURN_EVENTS) */
122
+ nestedBuffered?: number;
96
123
  /** When the last client left, for evicting the least recently used warm subprocess */
97
124
  idleSince?: number;
98
125
  /** When the last turn completed — the moment this session's prompt cache was last written */
@@ -116,7 +143,19 @@ function listWarmIdleSessions(): { sessionId: string; entry: SessionEntry; idleS
116
143
  /** Release a session's subprocess, if it still has one and nobody is using it. */
117
144
  function releaseSubprocess(sessionId: string, reason: string, note: string): void {
118
145
  const entry = activeSessions.get(sessionId);
119
- if (!entry || entry.clients.size > 0 || entry.isStreamingActive) return;
146
+ if (!entry) return;
147
+ // Disarm before deciding anything: past this point the pending release is either being
148
+ // performed or is moot, and a timer left armed is not merely stale. startCleanupTimer
149
+ // reschedules itself while `cacheReleaseTimer` is set, so an entry evicted by
150
+ // enforceWarmIdleCap — which calls straight in here — would outlive its own subprocess by
151
+ // up to the full TTL, holding its turnEvents buffer, ping interval, team watchers and
152
+ // shell registry. That is the accumulation the 5-minute cleanup exists to stop, on the one
153
+ // path that only runs when memory is already tight. Clearing here covers every caller.
154
+ if (entry.cacheReleaseTimer) {
155
+ clearTimeout(entry.cacheReleaseTimer);
156
+ entry.cacheReleaseTimer = undefined;
157
+ }
158
+ if (entry.clients.size > 0 || entry.isStreamingActive) return;
120
159
  const provider = providerRegistry.get(entry.providerId);
121
160
  if (!provider?.hasStreamingSession?.(sessionId)) return;
122
161
  provider.abortQuery?.(sessionId, reason);
@@ -124,6 +163,30 @@ function releaseSubprocess(sessionId: string, reason: string, note: string): voi
124
163
  logSessionEvent(sessionId, "INFO", note);
125
164
  }
126
165
 
166
+ /**
167
+ * Drop a session's live subprocess so its next turn is rebuilt from the transcript on disk.
168
+ *
169
+ * Routes that rewrite the JSONL need this. The subprocess holds the conversation in memory,
170
+ * so a rewrite it never learns about simply does not apply: the user strips an oversized
171
+ * image, sends again, and the same image is re-sent from memory and fails identically.
172
+ * `listRunningSessions()` does not cover it — that skips `phase === "idle"`, and a warm idle
173
+ * subprocess is exactly this case. Unlike `releaseSubprocess` it does not require the session
174
+ * to be clientless, because the tab being open is the normal way to reach the strip button.
175
+ */
176
+ export function dropSubprocessForTranscriptRewrite(sessionId: string): void {
177
+ const entry = activeSessions.get(sessionId);
178
+ if (!entry) return;
179
+ const provider = providerRegistry.get(entry.providerId);
180
+ if (!provider?.hasStreamingSession?.(sessionId)) return;
181
+ provider.abortQuery?.(sessionId, "transcript_rewritten");
182
+ if (entry.cacheReleaseTimer) {
183
+ clearTimeout(entry.cacheReleaseTimer);
184
+ entry.cacheReleaseTimer = undefined;
185
+ }
186
+ console.log(`[chat] session=${sessionId} released subprocess (transcript_rewritten)`);
187
+ logSessionEvent(sessionId, "INFO", "Subprocess released: the transcript was rewritten, so the next turn is rebuilt from disk");
188
+ }
189
+
127
190
  /** Tear down the longest-idle subprocesses once too many sessions are holding one. */
128
191
  function enforceWarmIdleCap(): void {
129
192
  const warmIdle = listWarmIdleSessions();
@@ -153,7 +216,10 @@ function scheduleSubprocessRelease(sessionId: string): void {
153
216
  if (!provider?.hasStreamingSession?.(sessionId)) return;
154
217
 
155
218
  const note = "Subprocess released: its prompt cache has expired, so keeping it warm saves nothing";
156
- const delay = cacheReleaseDelayMs(entry.lastTurnEndedAt, Date.now());
219
+ // The window is the provider's to state: an API-key install's cache dies at five minutes,
220
+ // so holding the subprocess for an hour there guards nothing and costs ~350MB.
221
+ const ttlMs = provider.promptCacheTtlMs?.(sessionId);
222
+ const delay = cacheReleaseDelayMs(entry.lastTurnEndedAt, Date.now(), ttlMs);
157
223
  if (delay === 0) {
158
224
  releaseSubprocess(sessionId, "cache_expired", note);
159
225
  return;
@@ -271,6 +337,24 @@ function bufferAndBroadcast(sessionId: string, event: unknown): void {
271
337
  broadcast(sessionId, event);
272
338
  }
273
339
 
340
+ /**
341
+ * Emit a nested-agent child read off disk. Buffered for reconnect replay only
342
+ * while its turn is still in flight and under the nested budget; a background
343
+ * agent that outlives the turn streams its children unbuffered, since the next
344
+ * turn's replay is not the place for them and reload restores them from disk.
345
+ */
346
+ function emitNestedChild(sessionId: string, child: unknown): void {
347
+ const entry = activeSessions.get(sessionId);
348
+ if (!entry) return;
349
+ const inFlight = entry.isStreamingActive && entry.phase !== "idle";
350
+ if (inFlight && (entry.nestedBuffered ?? 0) < MAX_NESTED_TURN_EVENTS) {
351
+ entry.nestedBuffered = (entry.nestedBuffered ?? 0) + 1;
352
+ bufferAndBroadcast(sessionId, child);
353
+ } else {
354
+ broadcast(sessionId, child);
355
+ }
356
+ }
357
+
274
358
  /** How often a session may stat ~/.claude/teams looking for an implicit team. */
275
359
  const IMPLICIT_TEAM_PROBE_INTERVAL_MS = 3_000;
276
360
 
@@ -367,6 +451,13 @@ function startCleanupTimer(sessionId: string): void {
367
451
  entry.cleanupTimer = setTimeout(() => {
368
452
  // Double-check: don't kill if streaming started while timer was pending
369
453
  if (entry.isStreamingActive) return;
454
+ // A pending release means the subprocess is still worth holding, and dropping the entry
455
+ // takes it with us. Come back after that timer rather than stretching this one.
456
+ if (entry.cacheReleaseTimer) {
457
+ entry.cleanupTimer = undefined;
458
+ startCleanupTimer(sessionId);
459
+ return;
460
+ }
370
461
  console.log(`[chat] session=${sessionId} cleanup: idle with no FE for ${CLEANUP_TIMEOUT_MS / 1000}s`);
371
462
  logSessionEvent(sessionId, "INFO", "Session cleaned up (idle, no FE reconnected)");
372
463
  // Backstop for the subprocess: scheduleSubprocessRelease normally gets there first,
@@ -405,6 +496,7 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
405
496
  entry.isStreamingActive = true;
406
497
  entry.pendingApprovalEvent = undefined;
407
498
  entry.turnEvents = [];
499
+ entry.nestedBuffered = 0;
408
500
  setPhase(sessionId, "connecting");
409
501
 
410
502
  let heartbeat: ReturnType<typeof setInterval> | undefined;
@@ -484,6 +576,9 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
484
576
  // the card can settle. Harmless for background bash tasks: no card matches them.
485
577
  if (sub === "task_notification" && taskToolUseId && isTerminalAgentStatus(taskStatus)) {
486
578
  broadcast(sessionId, { type: "subagent_status", toolUseId: taskToolUseId, status: taskStatus });
579
+ // The agent is done, so its nested workers are too — release the tail.
580
+ nestedSubagentSpy.stopSpy(taskToolUseId);
581
+ entry.backgroundToolUseIds?.delete(taskToolUseId);
487
582
  }
488
583
  if (taskId) {
489
584
  // Ensure the shell is registered even if the spy missed the file (fallback).
@@ -549,6 +644,18 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
549
644
  entry.pendingTeamCreate = ev.toolUseId;
550
645
  console.log(`[chat] session=${sessionId} TeamCreate tool_use detected, toolUseId=${ev.toolUseId}`);
551
646
  }
647
+ // A session-level Agent card: the SDK streams its agent's own steps, but
648
+ // nothing from agents that agent spawns in turn. Tail those nested
649
+ // transcripts from disk so the card keeps moving instead of freezing on
650
+ // the step that forked them. Claude-SDK-only — the layout is the CLI's.
651
+ if (providerId === "claude" && (ev.tool === "Agent" || ev.tool === "Task") && ev.toolUseId && !ev.parentToolUseId) {
652
+ const sessionDir = resolveSessionDir(sessionId, entry.projectPath);
653
+ if (sessionDir) {
654
+ nestedSubagentSpy.startSpy(sessionId, ev.toolUseId, sessionDir, (events) => {
655
+ for (const child of events) emitNestedChild(sessionId, child);
656
+ });
657
+ }
658
+ }
552
659
  // Start output spy for real-time streaming (Bash on Linux/macOS, PowerShell on Windows).
553
660
  // Claude-SDK-only: it tails the SDK's per-tool output file. Other providers
554
661
  // (codex/cursor) run commands in their own subprocess with no such file.
@@ -600,10 +707,18 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
600
707
  } else if (evType === "tool_result") {
601
708
  logSessionEvent(sessionId, "TOOL_RESULT", `error=${ev.isError ?? false} ${(ev.output ?? "").slice(0, 300)}`);
602
709
  console.log(`[chat] session=${sessionId} tool_result: toolUseId=${ev.toolUseId} pendingTeamCreate=${entry.pendingTeamCreate} output=${(ev.output ?? "").slice(0, 200)}`);
710
+ // A backgrounded Agent's tool_result is only a launch ack while the agent
711
+ // runs on — keep its nested spy until the terminal task_notification.
712
+ // Keyed off the ack text: `input.run_in_background` is optional and absent
713
+ // from most recorded calls (see background-agent-status.ts).
714
+ if (ev.toolUseId && !ev.parentToolUseId && isAsyncAgentLaunchAck(ev.output)) {
715
+ (entry.backgroundToolUseIds ??= new Set()).add(ev.toolUseId);
716
+ }
603
717
  // Stop bash output spy for this tool — EXCEPT background commands, whose
604
718
  // process keeps running after tool_result; keep tailing their .output.
605
719
  if (ev.toolUseId && !entry.backgroundToolUseIds?.has(ev.toolUseId)) {
606
720
  bashOutputSpy.stopSpy(ev.toolUseId);
721
+ nestedSubagentSpy.stopSpy(ev.toolUseId);
607
722
  }
608
723
  // Detect team creation from TeamCreate tool_result (legacy explicit teams)
609
724
  if (entry.pendingTeamCreate && entry.pendingTeamCreate === ev.toolUseId) {
@@ -689,6 +804,7 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
689
804
  console.log(`[chat] session_migrated: ${sessionId} → ${newId}`);
690
805
  // Stop spies tagged with old session ID before re-keying
691
806
  bashOutputSpy.stopAllForSession(sessionId);
807
+ nestedSubagentSpy.stopAllForSession(sessionId);
692
808
  backgroundShellRegistry.clearSession(sessionId);
693
809
  const oldEntry = activeSessions.get(sessionId);
694
810
  if (oldEntry) {
@@ -741,6 +857,9 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
741
857
  bufferAndBroadcast(sessionId, { type: "error", message: errMsg });
742
858
  } finally {
743
859
  if (heartbeat) clearInterval(heartbeat);
860
+ // Drain nested-agent tails while their turn buffer still exists, so the last
861
+ // records land in this turn's replay instead of the head of the next one.
862
+ nestedSubagentSpy.stopAllForSession(sessionId);
744
863
  entry.isStreamingActive = false;
745
864
  entry.turnEvents = [];
746
865
  // Force-clear compact status on stream teardown (error, close, etc.)
@@ -0,0 +1,86 @@
1
+ /**
2
+ * WS handler for `/ws/remote-desktop` — binary out = framed video access units (1-byte
3
+ * key/delta header + Annex-B bytes), text in = JSON control messages.
4
+ *
5
+ * The connection is *not* trusted just because the upgrade succeeded: the coarse PPM auth
6
+ * token already travels as `?token=` on every WS URL (`isWsUpgradeAuthorized`,
7
+ * `src/server/index.ts`) and proves nothing beyond "holds the one reusable app token", so a
8
+ * single-use short-TTL nonce (`POST /api/remote-desktop/session`) must be presented as the
9
+ * client's *first* message — `{type:"auth", nonce}` — before capture or input starts. Kept out
10
+ * of the query string (unlike the coarse token) so it never lands in proxy/tunnel access logs.
11
+ *
12
+ * Both the feature flag and `auth.enabled` are re-checked here even though
13
+ * `src/server/index.ts` already gated the upgrade on them — this handler must never depend on
14
+ * a single call site staying correct as fetch-time routing evolves, and must NOT reuse
15
+ * `isWsUpgradeAuthorized()`, which returns `true` unconditionally when PPM auth is disabled.
16
+ */
17
+ import { configService } from "../../services/config.service.ts";
18
+ import { isRemoteDesktopEnabled } from "../../services/remote-desktop/remote-desktop-flag.ts";
19
+ import { consumeRemoteDesktopNonce } from "../../services/remote-desktop/remote-desktop-nonce.ts";
20
+ import {
21
+ createRemoteDesktopSession,
22
+ registerRemoteDesktopExitSweep,
23
+ type RemoteDesktopSession,
24
+ type RemoteDesktopSocket,
25
+ } from "../../services/remote-desktop/remote-desktop-session.ts";
26
+
27
+ registerRemoteDesktopExitSweep();
28
+
29
+ interface RemoteDesktopWs {
30
+ data: { type: "remote-desktop"; session?: RemoteDesktopSession; authenticated?: boolean };
31
+ send: (d: string | Uint8Array) => number;
32
+ getBufferedAmount?: () => number;
33
+ close: (code?: number, reason?: string) => void;
34
+ }
35
+
36
+ function guardOrClose(ws: RemoteDesktopWs): boolean {
37
+ if (!isRemoteDesktopEnabled()) { ws.close(1008, "remote desktop is disabled"); return false; }
38
+ if (!configService.get("auth").enabled) {
39
+ ws.close(1008, "remote desktop requires PPM authentication to be enabled");
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+
45
+ async function authenticateFirstMessage(ws: RemoteDesktopWs, text: string): Promise<void> {
46
+ let parsed: Record<string, unknown>;
47
+ try { parsed = JSON.parse(text); } catch { ws.close(1008, "expected auth message"); return; }
48
+ const nonce = parsed.type === "auth" ? parsed.nonce : undefined;
49
+ if (typeof nonce !== "string" || !consumeRemoteDesktopNonce(nonce)) {
50
+ ws.close(1008, "invalid or expired session nonce");
51
+ return;
52
+ }
53
+ ws.data.authenticated = true;
54
+ try {
55
+ const socket: RemoteDesktopSocket = {
56
+ send: (d) => ws.send(d),
57
+ getBufferedAmount: ws.getBufferedAmount ? () => ws.getBufferedAmount!() : undefined,
58
+ close: (code, reason) => ws.close(code, reason),
59
+ };
60
+ ws.data.session = await createRemoteDesktopSession(socket);
61
+ } catch (e) {
62
+ console.error(`[remote-desktop] failed to start capture: ${(e as Error).message}`);
63
+ ws.send(JSON.stringify({ type: "error", message: (e as Error).message }));
64
+ ws.close(1011, "capture failed to start");
65
+ }
66
+ }
67
+
68
+ export const remoteDesktopWebSocket = {
69
+ open(ws: RemoteDesktopWs) {
70
+ guardOrClose(ws); // nothing else to do until the client's first (auth) message arrives
71
+ },
72
+
73
+ async message(ws: RemoteDesktopWs, msg: string | ArrayBuffer | Uint8Array) {
74
+ if (!guardOrClose(ws)) return;
75
+ const text = typeof msg === "string" ? msg : new TextDecoder().decode(msg as ArrayBuffer);
76
+ if (!ws.data.authenticated) {
77
+ await authenticateFirstMessage(ws, text);
78
+ return;
79
+ }
80
+ await ws.data.session?.handleClientMessage(text);
81
+ },
82
+
83
+ close(ws: RemoteDesktopWs) {
84
+ ws.data.session?.close();
85
+ },
86
+ };
@@ -131,21 +131,36 @@ class AccountService {
131
131
  * Returns the refreshed account with fresh tokens, or null if refresh failed.
132
132
  */
133
133
  async ensureFreshToken(id: string): Promise<AccountWithTokens | null> {
134
+ return (await this.ensureFreshTokenChecked(id)).account;
135
+ }
136
+
137
+ /**
138
+ * `ensureFreshToken`, keeping the reason it failed.
139
+ *
140
+ * `rejected` means the OAuth server turned the refresh token itself down (`invalid_grant`
141
+ * / `invalid_request`): nothing local recovers from that, only a fresh sign-in. Anything
142
+ * else — a network drop, a 429, a decrypt failure — is transient and worth retrying,
143
+ * which is a different sentence to put in front of a user. `ensureFreshToken` collapses
144
+ * the two because its callers are background loops with nobody to tell; the one caller
145
+ * that has a user in front of it (`PATCH /api/accounts/:id`) uses this.
146
+ */
147
+ async ensureFreshTokenChecked(id: string): Promise<{ account: AccountWithTokens | null; rejected: boolean }> {
134
148
  const acc = this.getWithTokens(id);
135
- if (!acc) return null;
149
+ if (!acc) return { account: null, rejected: false };
136
150
  // Only OAuth tokens need refresh
137
- if (!acc.accessToken.startsWith("sk-ant-oat")) return acc;
138
- if (!acc.expiresAt) return acc;
151
+ if (!acc.accessToken.startsWith("sk-ant-oat")) return { account: acc, rejected: false };
152
+ if (!acc.expiresAt) return { account: acc, rejected: false };
139
153
  const nowS = Math.floor(Date.now() / 1000);
140
154
  const REFRESH_BUFFER_S = 3600; // 1 hour — refresh proactively before expiry
141
- if (acc.expiresAt - nowS > REFRESH_BUFFER_S) return acc; // still fresh
155
+ if (acc.expiresAt - nowS > REFRESH_BUFFER_S) return { account: acc, rejected: false }; // still fresh
142
156
  try {
143
157
  console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s, buffer=${REFRESH_BUFFER_S}s)`);
144
158
  await this.refreshAccessToken(id, false, false, REFRESH_BUFFER_S);
145
- return this.getWithTokens(id);
159
+ return { account: this.getWithTokens(id), rejected: false };
146
160
  } catch (e) {
147
- console.error(`[accounts] Pre-flight refresh failed for ${id}: ${(e as Error).message ?? e}`);
148
- return null;
161
+ const msg = (e as Error).message ?? String(e);
162
+ console.error(`[accounts] Pre-flight refresh failed for ${id}: ${msg}`);
163
+ return { account: null, rejected: /invalid_grant|invalid_request/.test(msg) };
149
164
  }
150
165
  }
151
166
 
@@ -183,6 +198,8 @@ class AccountService {
183
198
  }
184
199
  if (params.label) updateAccount(dup.id, { label: params.label });
185
200
  if (params.email) updateAccount(dup.id, { email: params.email });
201
+ // After the email lands, or the first re-add of a row that had none logs a UUID.
202
+ this.noteParkSurvivedReAdd(dup.id);
186
203
  return this.toAccount(getAccountById(dup.id)!);
187
204
  }
188
205
 
@@ -284,9 +301,18 @@ class AccountService {
284
301
  // Check for duplicate — update tokens on existing account
285
302
  const dup = this.findDuplicate(email, info.profileData);
286
303
  if (dup) {
287
- updateAccount(dup.id, { access_token: encrypt(params.apiKey), status: "active", cooldown_until: null });
304
+ // Mirrors updateTokens(): a fresh token ends a cooldown, but a park is a decision
305
+ // about the rotation and pasting the token in again does not reverse it. This was the
306
+ // one door left that force-enabled, which is how the two paths disagreed.
307
+ const parked = getAccountById(dup.id)?.status === "disabled";
308
+ updateAccount(dup.id, {
309
+ access_token: encrypt(params.apiKey),
310
+ cooldown_until: null,
311
+ ...(parked ? {} : { status: "active" as const }),
312
+ });
288
313
  if (info.profileData) updateAccount(dup.id, { profile_json: JSON.stringify(info.profileData) });
289
314
  if (email) updateAccount(dup.id, { email });
315
+ this.noteParkSurvivedReAdd(dup.id);
290
316
  return this.toAccount(getAccountById(dup.id)!);
291
317
  }
292
318
 
@@ -326,15 +352,39 @@ class AccountService {
326
352
  }
327
353
 
328
354
  updateTokens(id: string, accessToken: string, refreshToken: string, expiresAt: number): void {
355
+ // A disabled account keeps that status. Usage polling refreshes parked accounts on
356
+ // purpose (a GET costs no quota), and export does the same before writing a backup;
357
+ // activating here put them back in the chat rotation behind the user's back, so the
358
+ // toggle looked like it had been forgotten — most visibly after a restart, which
359
+ // polls immediately. Cooldown still clears for everything else: a fresh token is
360
+ // precisely what ends a cooldown.
361
+ const parked = getAccountById(id)?.status === "disabled";
329
362
  updateAccount(id, {
330
363
  access_token: encrypt(accessToken),
331
364
  refresh_token: encrypt(refreshToken),
332
365
  expires_at: expiresAt,
333
- status: "active",
366
+ // The cooldown clears either way — a fresh token is what ends one, and leaving the
367
+ // timestamp on a parked account would resurrect it the moment the park is lifted.
334
368
  cooldown_until: null,
369
+ ...(parked ? {} : { status: "active" as const }),
335
370
  });
336
371
  }
337
372
 
373
+ /**
374
+ * Both re-add doors land here once the new tokens are stored. Neither lifts a park —
375
+ * disabling is a decision about the rotation, and carrying credentials in does not reverse
376
+ * it — but landing tokens and then showing nothing is exactly what made the OAuth path look
377
+ * like a no-op. Saying so is the whole point; keeping it in one place is what stops the two
378
+ * doors from drifting apart again.
379
+ */
380
+ private noteParkSurvivedReAdd(id: string): void {
381
+ const row = getAccountById(id);
382
+ if (row?.status !== "disabled") return;
383
+ console.log(
384
+ `[accounts] Tokens updated for ${row.email ?? id} — account stays disabled; enable it in Settings`,
385
+ );
386
+ }
387
+
338
388
  setCooldown(id: string, untilMs: number): void {
339
389
  updateAccount(id, {
340
390
  status: "cooldown",
@@ -352,7 +402,11 @@ class AccountService {
352
402
  const acc = this.list().find((a) => a.id === id);
353
403
  const nowS = Math.floor(Date.now() / 1000);
354
404
  if (acc?.expiresAt && acc.expiresAt < nowS) {
355
- throw new Error("Cannot re-enable expired temporary account. Please login again or import a fresh backup.");
405
+ // Signing in again refreshes the tokens but no longer re-enables anything, so the
406
+ // text says what actually happens: get a live token in, then flip it back on here.
407
+ throw new Error(
408
+ "Cannot re-enable expired temporary account. Sign in again or import a fresh backup to refresh its tokens, then enable it here.",
409
+ );
356
410
  }
357
411
  }
358
412
  updateAccount(id, { status: "active", cooldown_until: null });
@@ -552,9 +606,15 @@ class AccountService {
552
606
 
553
607
  /**
554
608
  * POST the refresh grant, retrying transient failures with backoff.
555
- * A network blip (e.g. waking from sleep) must not cost a whole refresh cycle
556
- * Anthropic's refresh token dies shortly after the access token, so the retry
557
- * window before it lapses for good is narrow.
609
+ * A network blip (e.g. waking from sleep) must not cost a whole refresh cycle: this
610
+ * runs at or past access-token expiry, so a dropped attempt leaves the account unable
611
+ * to serve a turn until the next sweep.
612
+ *
613
+ * It is *not* that the refresh token is about to lapse with it. Measured against a
614
+ * live install's history: 324 refreshes, zero `invalid_grant`, and one that succeeded
615
+ * after a 58-hour gap — 50 hours past the 8-hour access-token expiry. Refresh tokens
616
+ * outlive their access tokens by days, which is what makes freezing a parked account
617
+ * (see `startAutoRefresh`) safe rather than a slow way to kill it.
558
618
  */
559
619
  private async postRefreshGrant(refreshToken: string, label: string): Promise<Response> {
560
620
  const backoffMs = [2_000, 6_000];
@@ -664,7 +724,8 @@ class AccountService {
664
724
  * Export accounts backup.
665
725
  * @param includeRefreshToken - if true, includes refresh tokens (full transfer).
666
726
  * Source keeps its refresh token; it will be auto-cleared if it becomes invalid.
667
- * Default false = temporary export (access-only, ~1h).
727
+ * Default false = temporary export (access-only, ~1h — see `refreshBeforeExport`,
728
+ * which freshens the access token first so the window starts at export time).
668
729
  */
669
730
  exportEncrypted(password: string, accountIds?: string[], includeRefreshToken = false): string {
670
731
  const rows = accountIds?.length
@@ -709,14 +770,25 @@ class AccountService {
709
770
  let accessToken = row.access_token;
710
771
  if (!looksEncrypted(accessToken)) accessToken = encrypt(accessToken);
711
772
  const refreshToken = looksEncrypted(row.refresh_token) ? row.refresh_token : encrypt(row.refresh_token);
773
+ // An account parked on THIS machine stays parked: the import carries tokens, not
774
+ // the local decision about who is in the rotation. A new account still takes the
775
+ // backup's status below, since there is no local decision to respect. An account
776
+ // the failed-refresh path disabled is recoverable in one click — the import gives
777
+ // it back a live refresh token, which is what setEnabled() checks for.
778
+ const parked = existing.status === "disabled";
712
779
  updateAccount(existing.id, {
713
780
  access_token: accessToken,
714
781
  refresh_token: refreshToken,
715
782
  expires_at: row.expires_at,
716
- status: "active",
783
+ ...(parked ? {} : { status: "active" as const }),
717
784
  });
718
785
  imported++;
719
- fullTransferIds.push(existing.id);
786
+ // The usage poller and export both refresh a parked account, because a token
787
+ // nothing ever refreshes eventually dies. This one is different: it is not
788
+ // keeping the token alive, it is claiming ownership of it — and claiming a token
789
+ // for an account this machine has taken out of the rotation buys nothing here
790
+ // while invalidating the machine still using it. Enabling the account claims it.
791
+ if (!parked) fullTransferIds.push(existing.id);
720
792
  console.log(`[accounts] Updated ${row.email ?? existing.id} tokens from import`);
721
793
  }
722
794
  continue; // skip if import doesn't have refresh token
@@ -726,6 +798,11 @@ class AccountService {
726
798
  let accessToken = row.access_token;
727
799
  if (!looksEncrypted(accessToken)) accessToken = encrypt(accessToken);
728
800
  const refreshToken = hasRefresh ? (looksEncrypted(row.refresh_token) ? row.refresh_token : encrypt(row.refresh_token)) : encrypt("");
801
+ // The column has no CHECK constraint and the blob is user-supplied, so anything
802
+ // outside the three real statuses lands as active. A stray value reads as *on* in
803
+ // the UI (`status !== "disabled"`) while never being selectable for a turn.
804
+ const importedStatus =
805
+ row.status === "disabled" || row.status === "cooldown" ? row.status : "active";
729
806
  insertAccount({
730
807
  id: row.id,
731
808
  label: row.label,
@@ -733,7 +810,7 @@ class AccountService {
733
810
  access_token: accessToken,
734
811
  refresh_token: refreshToken,
735
812
  expires_at: row.expires_at,
736
- status: row.status ?? "active",
813
+ status: importedStatus,
737
814
  cooldown_until: row.cooldown_until,
738
815
  priority: row.priority ?? 0,
739
816
  total_requests: row.total_requests ?? 0,
@@ -741,7 +818,7 @@ class AccountService {
741
818
  profile_json: row.profile_json ?? null,
742
819
  });
743
820
  imported++;
744
- if (hasRefresh) fullTransferIds.push(row.id);
821
+ if (hasRefresh && importedStatus !== "disabled") fullTransferIds.push(row.id);
745
822
  }
746
823
 
747
824
  // Immediately refresh full-transfer accounts to claim ownership
@@ -781,11 +858,17 @@ class AccountService {
781
858
  const accounts = this.list();
782
859
  const nowS = Math.floor(Date.now() / 1000);
783
860
  for (const acc of accounts) {
784
- // Freeze disabled accounts: skip background refresh entirely.
785
- // Refreshing rotates the OAuth refresh token; when the same account is
786
- // shared across machines/processes the rotation races and one side gets
787
- // invalid_grant, killing an account the user only meant to park. Leaving
788
- // a disabled account untouched keeps its token intact until re-enabled.
861
+ // Skip disabled accounts: this timer exists to keep the chat rotation supplied and
862
+ // a parked account is not in it. Refreshing rotates the OAuth refresh token; when
863
+ // the same account is shared across machines the rotation races and one side gets
864
+ // invalid_grant, killing an account the user only meant to park.
865
+ //
866
+ // A preference, not a guarantee, and worth knowing before relying on it: the usage
867
+ // poller runs on the same 5-minute cadence with the same 1-hour buffer and does not
868
+ // skip parked accounts — deliberately, because a token nothing ever refreshes does
869
+ // eventually die. So while polling is on, a parked account's token still rotates
870
+ // here-or-there; this skip only declines to be the one that does it. Enabling the
871
+ // account proves the token either way (`PATCH /api/accounts/:id`).
789
872
  if (acc.status === "disabled") continue;
790
873
  if (!acc.expiresAt) continue;
791
874
  if (acc.expiresAt - nowS > REFRESH_BUFFER_S) continue;
@@ -0,0 +1,66 @@
1
+ import { resolve, basename } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { getPpmDir, isIsolatedPpmHome } from "../ppm-dir.ts";
4
+
5
+ /**
6
+ * Where database snapshots live, and how their filenames encode when and why
7
+ * they were taken.
8
+ *
9
+ * The directory deliberately sits OUTSIDE `~/.ppm` in production. A snapshot
10
+ * kept next to the database it protects dies with it: the whole point is to
11
+ * survive the case where something wipes or overwrites the PPM directory.
12
+ * Under an isolated `PPM_HOME` (tests) it goes back inside that temp dir, so a
13
+ * test run can never write snapshots into the user's real home.
14
+ *
15
+ * `homedir()` here is a deliberate exception to the getPpmDir()-only rule (see
16
+ * CLAUDE.md "PPM Directory"): the sibling location is the entire reason this
17
+ * directory survives a PPM-directory wipe, so it cannot be derived from
18
+ * `getPpmDir()` in production.
19
+ */
20
+ export function getBackupsDir(): string {
21
+ return isIsolatedPpmHome()
22
+ ? resolve(getPpmDir(), "backups")
23
+ : resolve(homedir(), ".ppm-backups");
24
+ }
25
+
26
+ /** Why a snapshot was taken. Encoded in the filename so `ppm restore` can explain each candidate. */
27
+ export type BackupReason = "hourly" | "premigrate" | "start" | "manual";
28
+
29
+ export const BACKUP_REASONS: readonly BackupReason[] = ["hourly", "premigrate", "start", "manual"];
30
+
31
+ /** `2026-09-06T15:46:00.000Z` → `20260906T154600Z` (filename-safe, still sorts chronologically). */
32
+ export function formatBackupStamp(date: Date): string {
33
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
34
+ }
35
+
36
+ /**
37
+ * Snapshot filename. `dbFile` is the SOURCE database's basename (`ppm.db`,
38
+ * `ppm.dev.db`), kept in the name so a dev-profile run never prunes or
39
+ * restores production snapshots.
40
+ */
41
+ export function backupFileName(dbFile: string, date: Date, reason: BackupReason): string {
42
+ const stem = basename(dbFile).replace(/\.db$/, "");
43
+ return `${stem}-${formatBackupStamp(date)}-${reason}.db`;
44
+ }
45
+
46
+ export interface ParsedBackupName {
47
+ dbStem: string;
48
+ date: Date;
49
+ reason: BackupReason;
50
+ }
51
+
52
+ /**
53
+ * Inverse of `backupFileName`. Returns null for anything this module did not
54
+ * write — including `.partial` files from an interrupted snapshot, which must
55
+ * never be offered as a restore candidate.
56
+ */
57
+ export function parseBackupName(name: string): ParsedBackupName | null {
58
+ const m = /^(.+)-(\d{8}T\d{6}Z)-([a-z]+)\.db$/.exec(name);
59
+ if (!m) return null;
60
+ const [, dbStem, stamp, reason] = m;
61
+ if (!BACKUP_REASONS.includes(reason as BackupReason)) return null;
62
+ const iso = `${stamp!.slice(0, 4)}-${stamp!.slice(4, 6)}-${stamp!.slice(6, 8)}T${stamp!.slice(9, 11)}:${stamp!.slice(11, 13)}:${stamp!.slice(13, 15)}Z`;
63
+ const date = new Date(iso);
64
+ if (Number.isNaN(date.getTime())) return null;
65
+ return { dbStem: dbStem!, date, reason: reason as BackupReason };
66
+ }