@hienlh/ppm 0.18.12 → 0.18.13

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 (532) hide show
  1. package/AGENTS.md +17 -5
  2. package/CHANGELOG.md +22 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +6 -6
  5. package/assets/skills/ppm/SKILL.md +1 -1
  6. package/assets/skills/ppm/references/http-api.md +19 -1
  7. package/dist/web/assets/{adaptive-context-menu-DGCKKeTy.js → adaptive-context-menu-CbgPYQmj.js} +1 -1
  8. package/dist/web/assets/{ai-resource-editor-BOHHTTe7.js → ai-resource-editor-unM2HNmg.js} +1 -1
  9. package/dist/web/assets/{ai-settings-section-BigC_nZR.js → ai-settings-section-CdktdrwF.js} +1 -1
  10. package/dist/web/assets/{angular-html-BYPaydvh.js → angular-html-DcpbdcNG.js} +1 -1
  11. package/dist/web/assets/{angular-ts-Bb4AMKlk.js → angular-ts-Bv_WIk34.js} +1 -1
  12. package/dist/web/assets/api-client-BogqsFna.js +1 -0
  13. package/dist/web/assets/{api-settings-Bd3M4YpI.js → api-settings-BdCtex9i.js} +1 -1
  14. package/dist/web/assets/api-settings-DfXViPKz.js +1 -0
  15. package/dist/web/assets/{api-themes-Cpx419SL.js → api-themes-pRRoLvgD.js} +1 -1
  16. package/dist/web/assets/{apl-DB2VjFCS.js → apl-D5_lLszu.js} +1 -1
  17. package/dist/web/assets/architecture-PBZL5I3N-BzFm9ELu.js +1 -0
  18. package/dist/web/assets/{astro-BeoJdVpk.js → astro-CBHwddYL.js} +1 -1
  19. package/dist/web/assets/{audio-preview-BgFuTn-0.js → audio-preview-BD1iDovf.js} +1 -1
  20. package/dist/web/assets/{blade-Cf2q1sPf.js → blade-DGe1d2Pq.js} +1 -1
  21. package/dist/web/assets/{bsl-W1kOaNpu.js → bsl-2lyAzRJA.js} +1 -1
  22. package/dist/web/assets/{button-5zTrkEoN.js → button-2WCAS8dI.js} +1 -1
  23. package/dist/web/assets/{cairo-BoAZFcje.js → cairo-DjzZeJrq.js} +1 -1
  24. package/dist/web/assets/chat-tab-DzK8LGti.js +15 -0
  25. package/dist/web/assets/{cobol-B0Nm7RXj.js → cobol-cC3XIuAJ.js} +1 -1
  26. package/dist/web/assets/code-editor-CuMfPN9X.js +10 -0
  27. package/dist/web/assets/{coffee-BakMn3on.js → coffee-4eGboVoU.js} +1 -1
  28. package/dist/web/assets/{conflict-editor-BUs5k9q_.js → conflict-editor-DdJag4QK.js} +1 -1
  29. package/dist/web/assets/{context-menu-Bq92km4s.js → context-menu-BO1c9jKy.js} +1 -1
  30. package/dist/web/assets/{cpp-BRAJQHK2.js → cpp-DF9_G-OU.js} +1 -1
  31. package/dist/web/assets/{crystal-kqqO3zkK.js → crystal-OavZlzcm.js} +1 -1
  32. package/dist/web/assets/{csv-preview-ByoDgLAZ.js → csv-preview-DnXwouF3.js} +1 -1
  33. package/dist/web/assets/{data-grid-overlay-editor-Brkf9z41.js → data-grid-overlay-editor-B9Gpjm3l.js} +1 -1
  34. package/dist/web/assets/{database-viewer-Btc3h8Yu.js → database-viewer-Dk8oGyZx.js} +1 -1
  35. package/dist/web/assets/{dialog-DwtGlowE.js → dialog-B-l2ZxVm.js} +1 -1
  36. package/dist/web/assets/{diff-viewer-CZaDnR4w.js → diff-viewer-eaUBjLCH.js} +1 -1
  37. package/dist/web/assets/{docx-preview-DAD_vg59.js → docx-preview-DAdHPlhZ.js} +1 -1
  38. package/dist/web/assets/{edge-Cq1ZKZuE.js → edge-TzYh8OlN.js} +1 -1
  39. package/dist/web/assets/{edit-diff-preview-B_VE7ppQ.js → edit-diff-preview-DlwP3UIU.js} +1 -1
  40. package/dist/web/assets/{elixir-BM_JoJRk.js → elixir-Cy3RhKKH.js} +1 -1
  41. package/dist/web/assets/{elm-Dw7oO08Y.js → elm-BTBZdweK.js} +1 -1
  42. package/dist/web/assets/{erb-CfKOGAfk.js → erb-dv1mI0y4.js} +1 -1
  43. package/dist/web/assets/{erlang-DlDmGVeE.js → erlang-DjeiDrkl.js} +1 -1
  44. package/dist/web/assets/{esm-DGAP0GFh.js → esm-FRwvcesu.js} +1 -1
  45. package/dist/web/assets/explorer-body-DkCkSVrN.js +2 -0
  46. package/dist/web/assets/explorer-window-content-MsnTVngu.js +1 -0
  47. package/dist/web/assets/{extension-store-Cpfh2vMm.js → extension-store-DUaZINyC.js} +1 -1
  48. package/dist/web/assets/{extension-webview-B2OnjuCu.js → extension-webview-W0qRfaAU.js} +1 -1
  49. package/dist/web/assets/{file-store-aOERIEo-.js → file-store-yReVbR8q.js} +1 -1
  50. package/dist/web/assets/{fortran-fixed-form-XnAhQWxS.js → fortran-fixed-form-BCH9DAIJ.js} +1 -1
  51. package/dist/web/assets/{fsharp-CdNvy0PG.js → fsharp-C93NYKYD.js} +1 -1
  52. package/dist/web/assets/{gdresource-DxuG5i8i.js → gdresource-BcIEE2eX.js} +1 -1
  53. package/dist/web/assets/{git-commit-3lMonsur.js → git-commit-BVjlXEy8.js} +1 -1
  54. package/dist/web/assets/{git-log-panel-Co0PBPHC.js → git-log-panel-2_4HuP47.js} +1 -1
  55. package/dist/web/assets/{git-rebase-N8Z9kIMf.js → git-rebase-Bj_21B1V.js} +1 -1
  56. package/dist/web/assets/gitGraph-HDMCJU4V-B72vcb2r.js +1 -0
  57. package/dist/web/assets/glide-data-grid-B0IY6n_Q.js +138 -0
  58. package/dist/web/assets/{glimmer-js-Dp9OXiib.js → glimmer-js-C20yj4Zv.js} +1 -1
  59. package/dist/web/assets/{glimmer-ts-CDVKBcDj.js → glimmer-ts-ipbYC7kh.js} +1 -1
  60. package/dist/web/assets/{glsl-DPJxI4us.js → glsl-Dovazu3t.js} +1 -1
  61. package/dist/web/assets/{graphql-BrFhaQQl.js → graphql-DpMlM7hC.js} +1 -1
  62. package/dist/web/assets/{group-chat-tab-rOd8J8GC.js → group-chat-tab-DRLDHOp6.js} +1 -1
  63. package/dist/web/assets/{hack-BzXbDMlA.js → hack-gc7LCBuV.js} +1 -1
  64. package/dist/web/assets/{haml-CPpkX0s5.js → haml-vtlFm9gm.js} +1 -1
  65. package/dist/web/assets/{handlebars-BqrApy52.js → handlebars-DGKhpCji.js} +1 -1
  66. package/dist/web/assets/{html-DmjUl-OA.js → html-DvfhQbXb.js} +1 -1
  67. package/dist/web/assets/{html-derivative-Cnd6XFR5.js → html-derivative-C3oTSSCN.js} +1 -1
  68. package/dist/web/assets/{http-DMLWYun2.js → http-SG6zLTo7.js} +1 -1
  69. package/dist/web/assets/{hurl-CKkCVxbj.js → hurl-BWXsYlqW.js} +1 -1
  70. package/dist/web/assets/{hxml-CAx19TtQ.js → hxml-DUdMzaoh.js} +1 -1
  71. package/dist/web/assets/{image-preview-DHUoylQ1.js → image-preview-DxgFOV1T.js} +1 -1
  72. package/dist/web/assets/index-CBn4SOoT.js +52 -0
  73. package/dist/web/assets/index-DWpNR3nB.css +2 -0
  74. package/dist/web/assets/info-3K5VOQVL-3irPxRK_.js +1 -0
  75. package/dist/web/assets/{input-DcEk7mA3.js → input-DuAF-Stj.js} +1 -1
  76. package/dist/web/assets/{jinja-BmB9YN9l.js → jinja-955w9RKM.js} +1 -1
  77. package/dist/web/assets/{jison-BaVsKKpw.js → jison-WWfqPo50.js} +1 -1
  78. package/dist/web/assets/{julia-BgVBLveX.js → julia-BFrfnaNl.js} +1 -1
  79. package/dist/web/assets/{just-C2ENIbl9.js → just-BrZdyR_I.js} +1 -1
  80. package/dist/web/assets/{keybindings-store-BGIyqXHI.js → keybindings-store-0Wqfa7Qp.js} +1 -1
  81. package/dist/web/assets/keybindings-store-d54pv2NK.js +1 -0
  82. package/dist/web/assets/{latex-BiDIWxKL.js → latex-CwtPPASD.js} +1 -1
  83. package/dist/web/assets/{liquid-NxpapreE.js → liquid-CYZuvDDd.js} +1 -1
  84. package/dist/web/assets/{lua-DVepQvwH.js → lua-BV095ZyY.js} +1 -1
  85. package/dist/web/assets/markdown-renderer-BTS8ifdu.js +1 -0
  86. package/dist/web/assets/{markdown-renderer-DlcDhgrF.js → markdown-renderer-CnRcKK3d.js} +3 -3
  87. package/dist/web/assets/{marko-CJ2606Xl.js → marko-CNeljZ1C.js} +1 -1
  88. package/dist/web/assets/{mdc-D2bjqclE.js → mdc-JfGapShy.js} +1 -1
  89. package/dist/web/assets/{media-url-H17XJF-I.js → media-url-Bm4PsduE.js} +1 -1
  90. package/dist/web/assets/{mobile-bottom-sheet-g-r2hCFa.js → mobile-bottom-sheet-M5iZDX7y.js} +1 -1
  91. package/dist/web/assets/mobile-explorer-sheet-BqK2-WuN.js +1 -0
  92. package/dist/web/assets/{nextflow-BtJwUbfF.js → nextflow-DD5C8nqX.js} +1 -1
  93. package/dist/web/assets/{nginx-j4gyIhHE.js → nginx-Ca7B__x_.js} +1 -1
  94. package/dist/web/assets/{nim-ClH_gjP5.js → nim-B8FogsDw.js} +1 -1
  95. package/dist/web/assets/notification-store-Yvy1I8gd.js +1 -0
  96. package/dist/web/assets/{number-overlay-editor-C1EqJnWq.js → number-overlay-editor-DG9xJwEG.js} +1 -1
  97. package/dist/web/assets/{overview-panel-vTeeJYVZ.js → overview-panel-s53bnl0q.js} +1 -1
  98. package/dist/web/assets/packet-RMMSAZCW-Z5jWjFCV.js +1 -0
  99. package/dist/web/assets/{panel-store-BSnkjgHt.js → panel-store-B4rijFKN.js} +1 -1
  100. package/dist/web/assets/{pdf-preview-CqFGRY0Z.js → pdf-preview-DYt3aBzM.js} +1 -1
  101. package/dist/web/assets/{perl-C9H7X7CT.js → perl-BR78hS8R.js} +1 -1
  102. package/dist/web/assets/{php-Bx2U-cQ_.js → php-DXBOMC9z.js} +1 -1
  103. package/dist/web/assets/pie-UPGHQEXC-Bq7UMi8P.js +1 -0
  104. package/dist/web/assets/{postgres-viewer-DHwcQucn.js → postgres-viewer-BvoWSuoZ.js} +3 -3
  105. package/dist/web/assets/{process-table-BGRJNlpn.js → process-table-BYwKigiD.js} +1 -1
  106. package/dist/web/assets/{project-store--nvQiy75.js → project-store-BuY7rxn_.js} +1 -1
  107. package/dist/web/assets/{pug-Cw74sKsE.js → pug-CvTwVVKV.js} +1 -1
  108. package/dist/web/assets/{qml-BXD94Eqz.js → qml-CPSoZ1-B.js} +1 -1
  109. package/dist/web/assets/radar-KQ55EAFF-BLo4BOTD.js +1 -0
  110. package/dist/web/assets/{razor-DZ0-Ge0S.js → razor-BUA1YKtF.js} +1 -1
  111. package/dist/web/assets/{rst-Dtdh0QUj.js → rst-BJsq7ISz.js} +1 -1
  112. package/dist/web/assets/{ruby-BC51BW2Q.js → ruby-DSDMeYf5.js} +1 -1
  113. package/dist/web/assets/{run-in-terminal-Sd3KyjN4.js → run-in-terminal-C9xpskFn.js} +1 -1
  114. package/dist/web/assets/{sas-BN8i4qOU.js → sas-B1nJOjbN.js} +1 -1
  115. package/dist/web/assets/{scroll-area-0gljZ6lF.js → scroll-area-DBcG1fQo.js} +1 -1
  116. package/dist/web/assets/{scss-H-QtA7Rz.js → scss-BZFyRiQn.js} +1 -1
  117. package/dist/web/assets/{send-to-chat-u6m8l-rH.js → send-to-chat-BIg70MUZ.js} +1 -1
  118. package/dist/web/assets/{settings-store-CHY6Kzin.js → settings-store-GV5pkkke.js} +2 -2
  119. package/dist/web/assets/{settings-tab-IY6K1MiZ.js → settings-tab-BePvIcjd.js} +1 -1
  120. package/dist/web/assets/settings-tab-Br-lJSPa.js +1 -0
  121. package/dist/web/assets/{shaderlab-CalNsj_x.js → shaderlab-laINRXBo.js} +1 -1
  122. package/dist/web/assets/{shellsession-BFrcoR8x.js → shellsession-BpOOvWVR.js} +1 -1
  123. package/dist/web/assets/{soy-CNbku7vP.js → soy-BW4d91Sa.js} +1 -1
  124. package/dist/web/assets/{sparql-BN_orcVp.js → sparql-BsvvyoHn.js} +1 -1
  125. package/dist/web/assets/{sql-query-editor-C6pC3I8l.js → sql-query-editor-qQce5QJ6.js} +1 -1
  126. package/dist/web/assets/{sqlite-viewer-DTr5PzUb.js → sqlite-viewer-yhc0YNvu.js} +1 -1
  127. package/dist/web/assets/{stata-C6a5Ijzq.js → stata-K-dZInUt.js} +1 -1
  128. package/dist/web/assets/{surrealql-ClplI_HY.js → surrealql-CpNp2sgf.js} +1 -1
  129. package/dist/web/assets/{svelte-DNm-UNXB.js → svelte-BG6iBA8R.js} +1 -1
  130. package/dist/web/assets/{switch-DKvKya9y.js → switch-_NcrktLe.js} +1 -1
  131. package/dist/web/assets/system-monitor-body-D8Z5QXBB.js +2 -0
  132. package/dist/web/assets/system-monitor-tab-BclYXFTe.js +1 -0
  133. package/dist/web/assets/{system-monitor-window-content-BUqRsPK2.js → system-monitor-window-content-CaOhD0AU.js} +1 -1
  134. package/dist/web/assets/{tab-host-window-content-BnqGPu8a.js → tab-host-window-content-f_Rc5rJK.js} +1 -1
  135. package/dist/web/assets/{tab-store-CJuJRapl.js → tab-store-tFnyPmm4.js} +1 -1
  136. package/dist/web/assets/team-member-sheet-QDHoYCs0.js +2 -0
  137. package/dist/web/assets/{team-member-window-content-T4Q5JI-G.js → team-member-window-content-B1jkKkDi.js} +1 -1
  138. package/dist/web/assets/{templ-CSdqVobu.js → templ-gxNKXmt3.js} +1 -1
  139. package/dist/web/assets/{terminal-tab-Bb2rZegQ.js → terminal-tab-UvwM0sty.js} +3 -3
  140. package/dist/web/assets/{tex-DzSJ4kcZ.js → tex-C0ON27g5.js} +1 -1
  141. package/dist/web/assets/{tool-cards-lhqhrDTT.js → tool-cards-jB7GT-wj.js} +4 -4
  142. package/dist/web/assets/treemap-KZPCXAKY-dyrE6SW2.js +1 -0
  143. package/dist/web/assets/{ts-tags-C28w3bTg.js → ts-tags-BX_CTVgQ.js} +1 -1
  144. package/dist/web/assets/{twig-i5jY3QXB.js → twig-CwL7XnU0.js} +1 -1
  145. package/dist/web/assets/{use-blob-url-Dsq9OjTh.js → use-blob-url-DcRcjNTE.js} +1 -1
  146. package/dist/web/assets/use-coarse-long-press-BmI_GVW5.js +1 -0
  147. package/dist/web/assets/{use-monaco-theme-BZvlsoBv.js → use-monaco-theme--9tOOkDA.js} +1 -1
  148. package/dist/web/assets/{use-open-team-member-DkiEjFYE.js → use-open-team-member-1hRsHLKg.js} +1 -1
  149. package/dist/web/assets/{use-websocket-B8R50f7f.js → use-websocket-rxJLbFqy.js} +1 -1
  150. package/dist/web/assets/{vendor-mermaid-By4AVvJY.js → vendor-mermaid-CHTa6D69.js} +1 -1
  151. package/dist/web/assets/video-preview-DMhnf2Gi.js +1 -0
  152. package/dist/web/assets/{vue-Ceyet_lv.js → vue-Bykm6wSS.js} +1 -1
  153. package/dist/web/assets/{vue-html-GuU7g_hg.js → vue-html-CETdK_iV.js} +1 -1
  154. package/dist/web/assets/{vue-vine-CxXoaJr2.js → vue-vine-DiJb6fO5.js} +1 -1
  155. package/dist/web/assets/{window-store-CsEW6jiE.js → window-store-CQ1lghvv.js} +1 -1
  156. package/dist/web/assets/{xml-CbqOVhkP.js → xml-BNw8J5YW.js} +1 -1
  157. package/dist/web/assets/{xsl-KfAmCauy.js → xsl-CsG9L31b.js} +1 -1
  158. package/dist/web/index.html +46 -45
  159. package/dist/web/sw.js +1 -1
  160. package/docs/architecture/ai-chat-and-providers.md +309 -0
  161. package/docs/architecture/data-and-storage.md +356 -0
  162. package/docs/architecture/extensions.md +496 -0
  163. package/docs/architecture/integrations.md +214 -0
  164. package/docs/architecture/workspace-and-ui.md +391 -0
  165. package/docs/claude-agent-sdk-reference.md +1 -1
  166. package/docs/code-patterns.md +361 -0
  167. package/docs/code-standards.md +8 -358
  168. package/docs/codebase-subsystems.md +364 -0
  169. package/docs/codebase-summary.md +34 -379
  170. package/docs/deployment-guide.md +280 -490
  171. package/docs/design-guidelines.md +1 -1
  172. package/docs/lessons-learned.md +27 -10
  173. package/docs/project-changelog.md +18 -806
  174. package/docs/project-overview-pdr.md +117 -111
  175. package/docs/project-roadmap.md +89 -133
  176. package/docs/system-architecture.md +77 -1826
  177. package/package.json +2 -2
  178. package/src/cli/commands/autostart.ts +2 -2
  179. package/src/cli/commands/config-cmd.ts +12 -3
  180. package/src/cli/commands/status.ts +3 -2
  181. package/src/cli/commands/stop.ts +32 -4
  182. package/src/server/index.ts +17 -3
  183. package/src/server/routes/chat-unread.ts +36 -0
  184. package/src/server/routes/named-tunnel.ts +132 -0
  185. package/src/server/routes/tunnel.ts +13 -0
  186. package/src/server/ws/chat.ts +5 -2
  187. package/src/services/autostart-register.ts +47 -29
  188. package/src/services/cloud.service.ts +26 -1
  189. package/src/services/config-secret-keys.ts +48 -0
  190. package/src/services/config.service.ts +1 -1
  191. package/src/services/db.service.ts +62 -12
  192. package/src/services/extension-rpc-handlers.ts +9 -1
  193. package/src/services/file-watcher/watch-tree.ts +67 -33
  194. package/src/services/file-watcher.service.ts +28 -10
  195. package/src/services/fs-credential-path-guard.ts +103 -0
  196. package/src/services/fs-path-guard.service.ts +14 -66
  197. package/src/services/named-tunnel/cloudflare-dns-api.ts +40 -0
  198. package/src/services/named-tunnel/cloudflare-zone-api.ts +28 -0
  199. package/src/services/named-tunnel/cloudflared-cert.ts +95 -0
  200. package/src/services/named-tunnel/cloudflared-exec.ts +50 -0
  201. package/src/services/named-tunnel/cloudflared-login-helpers.ts +98 -0
  202. package/src/services/named-tunnel/cloudflared-login.service.ts +182 -0
  203. package/src/services/named-tunnel/hostname-rules.ts +58 -0
  204. package/src/services/named-tunnel/login-output-parser.ts +36 -0
  205. package/src/services/named-tunnel/named-tunnel-args.ts +110 -0
  206. package/src/services/named-tunnel/named-tunnel-config.ts +67 -0
  207. package/src/services/named-tunnel/named-tunnel-probe-state.ts +95 -0
  208. package/src/services/named-tunnel/named-tunnel-readiness.ts +59 -0
  209. package/src/services/named-tunnel/named-tunnel-runtime.ts +42 -0
  210. package/src/services/named-tunnel/named-tunnel-setup-confirm.ts +98 -0
  211. package/src/services/named-tunnel/named-tunnel-setup.service.ts +191 -0
  212. package/src/services/supervisor-state.ts +67 -2
  213. package/src/services/supervisor.ts +454 -157
  214. package/src/services/tunnel-registry-parse.ts +27 -0
  215. package/src/services/tunnel-registry.service.ts +2 -15
  216. package/src/services/tunnel.service.ts +12 -3
  217. package/src/types/config.ts +31 -0
  218. package/src/web/app.tsx +5 -0
  219. package/src/web/components/chat/chat-history-bar.tsx +95 -98
  220. package/src/web/components/chat/chat-toolbar-overflow-menu.tsx +57 -0
  221. package/src/web/components/chat/session-debug-dialog.tsx +35 -27
  222. package/src/web/components/layout/cloud-share-named-tunnel-row.tsx +111 -0
  223. package/src/web/components/layout/cloud-share-popover.tsx +56 -14
  224. package/src/web/components/shared/connection-lost-overlay.tsx +44 -5
  225. package/src/web/components/tunnels/named-tunnel/hostname-validation.ts +14 -0
  226. package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +96 -0
  227. package/src/web/components/tunnels/named-tunnel/named-tunnel-hostname-field.tsx +51 -0
  228. package/src/web/components/tunnels/named-tunnel/named-tunnel-login-step.tsx +105 -0
  229. package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +159 -0
  230. package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-content.tsx +197 -0
  231. package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-popup.tsx +57 -0
  232. package/src/web/components/tunnels/named-tunnel/named-tunnel-step-reducer.ts +191 -0
  233. package/src/web/components/tunnels/named-tunnel/use-named-tunnel-setup.ts +212 -0
  234. package/src/web/components/tunnels/tunnel-manager-tab.tsx +4 -0
  235. package/src/web/hooks/use-global-events.ts +3 -1
  236. package/src/web/lib/api-named-tunnel.ts +54 -0
  237. package/src/web/lib/last-known-local-url.ts +23 -0
  238. package/src/web/stores/notification-store.ts +20 -1
  239. package/dist/web/assets/api-client-DTZEgjBQ.js +0 -1
  240. package/dist/web/assets/api-settings-kd1j4iR8.js +0 -1
  241. package/dist/web/assets/architecture-PBZL5I3N-8dCMT9r7.js +0 -1
  242. package/dist/web/assets/chat-tab-E3bf_V8X.js +0 -15
  243. package/dist/web/assets/code-editor-BUlM2gXk.js +0 -10
  244. package/dist/web/assets/explorer-body-9aBmyVpZ.js +0 -2
  245. package/dist/web/assets/explorer-window-content-DRtqoSPA.js +0 -1
  246. package/dist/web/assets/gitGraph-HDMCJU4V-2e_8dfbs.js +0 -1
  247. package/dist/web/assets/glide-data-grid-C4_zIneN.js +0 -138
  248. package/dist/web/assets/index-BImUaigO.js +0 -52
  249. package/dist/web/assets/index-CSfi6DSU.css +0 -2
  250. package/dist/web/assets/info-3K5VOQVL-BZi1Gha1.js +0 -1
  251. package/dist/web/assets/keybindings-store-CzV2sPls.js +0 -1
  252. package/dist/web/assets/markdown-renderer-CcbR33VG.js +0 -1
  253. package/dist/web/assets/mobile-explorer-sheet-SIwQXlwA.js +0 -1
  254. package/dist/web/assets/notification-store-DV5QtUTg.js +0 -1
  255. package/dist/web/assets/packet-RMMSAZCW-o5htEcin.js +0 -1
  256. package/dist/web/assets/pie-UPGHQEXC-BXAIFSAD.js +0 -1
  257. package/dist/web/assets/radar-KQ55EAFF-Dbt2Aebl.js +0 -1
  258. package/dist/web/assets/settings-tab-Dic6TKkU.js +0 -1
  259. package/dist/web/assets/system-monitor-body-iwGiCns9.js +0 -2
  260. package/dist/web/assets/system-monitor-tab-DdXQhEel.js +0 -1
  261. package/dist/web/assets/team-member-sheet-LkAFSCvb.js +0 -2
  262. package/dist/web/assets/treemap-KZPCXAKY-DUbnl9dz.js +0 -1
  263. package/dist/web/assets/video-preview-m0-3p7QZ.js +0 -1
  264. /package/dist/web/assets/{abap-C6nB04tn.js → abap-DGlrX_8G.js} +0 -0
  265. /package/dist/web/assets/{actionscript-3-Vx4rw2sC.js → actionscript-3-CUqRtUJE.js} +0 -0
  266. /package/dist/web/assets/{ada-Ccd8vM20.js → ada-CY3Spa33.js} +0 -0
  267. /package/dist/web/assets/{andromeeda-DnBX5xMg.js → andromeeda-Lxs2TJn7.js} +0 -0
  268. /package/dist/web/assets/{apache-B-9_sx0g.js → apache-DfxgGdgM.js} +0 -0
  269. /package/dist/web/assets/{apex-_wPVBYoL.js → apex-QVDF1dnI.js} +0 -0
  270. /package/dist/web/assets/{api-client-aQb5hHE5.js → api-client-oY1tC3tM.js} +0 -0
  271. /package/dist/web/assets/{applescript-CcvD-qr0.js → applescript-pwJ8QXh-.js} +0 -0
  272. /package/dist/web/assets/{apply-theme-BeLRfsSu.js → apply-theme-B_bMVL6a.js} +0 -0
  273. /package/dist/web/assets/{ara-C40Ztoms.js → ara-mPjeX5YX.js} +0 -0
  274. /package/dist/web/assets/{asciidoc-CybYkJV4.js → asciidoc-Bf7LsDkI.js} +0 -0
  275. /package/dist/web/assets/{asm-9eOKwzR0.js → asm-rRKC1OUx.js} +0 -0
  276. /package/dist/web/assets/{aurora-x-B5ZH4t7t.js → aurora-x-DwQr0DkG.js} +0 -0
  277. /package/dist/web/assets/{awk-Dqilob-2.js → awk-Bpt768fN.js} +0 -0
  278. /package/dist/web/assets/{ayu-dark-CPgRhf5r.js → ayu-dark-D9lwQHMs.js} +0 -0
  279. /package/dist/web/assets/{ayu-light-Do06Q52f.js → ayu-light-B4-ptrU5.js} +0 -0
  280. /package/dist/web/assets/{ayu-mirage-_BhxcrxY.js → ayu-mirage-DAyRzH5Q.js} +0 -0
  281. /package/dist/web/assets/{ballerina-DDwK54QW.js → ballerina-Cgdpj1HC.js} +0 -0
  282. /package/dist/web/assets/{bat-BGyGlzGk.js → bat-4FrN36S2.js} +0 -0
  283. /package/dist/web/assets/{beancount-CRWBdapI.js → beancount-DkcxQER3.js} +0 -0
  284. /package/dist/web/assets/{berry-DbSFn2gO.js → berry-Dnuut-Ml.js} +0 -0
  285. /package/dist/web/assets/{bibtex-QpsV9gm9.js → bibtex-CCDwmbuA.js} +0 -0
  286. /package/dist/web/assets/{bicep-CxS8CUve.js → bicep-BC0ryoGM.js} +0 -0
  287. /package/dist/web/assets/{bird2-CuZhQzYR.js → bird2-C50V_A-0.js} +0 -0
  288. /package/dist/web/assets/{c-9bnDqE3g.js → c-Ddorhq0U.js} +0 -0
  289. /package/dist/web/assets/{c3-JMZTrOhA.js → c3-DdIf5r9E.js} +0 -0
  290. /package/dist/web/assets/{cadence-DsDNayJS.js → cadence-s9pd1Wng.js} +0 -0
  291. /package/dist/web/assets/{can-open-in-ppm-BJfpynya.js → can-open-in-ppm-C6hZV1OP.js} +0 -0
  292. /package/dist/web/assets/{catppuccin-frappe-DYIKXXBe.js → catppuccin-frappe-CzJNTyu-.js} +0 -0
  293. /package/dist/web/assets/{catppuccin-latte-IYYOnUJ8.js → catppuccin-latte-BNpt-m7s.js} +0 -0
  294. /package/dist/web/assets/{catppuccin-macchiato-BrJ_7mfS.js → catppuccin-macchiato-FYGkfsJ5.js} +0 -0
  295. /package/dist/web/assets/{catppuccin-mocha-gzSu7v7-.js → catppuccin-mocha-it2g5D1q.js} +0 -0
  296. /package/dist/web/assets/{clarity-DLzsC0Hb.js → clarity-BVFBTjMe.js} +0 -0
  297. /package/dist/web/assets/{clipboard-7fw0Shic.js → clipboard-BWznymz7.js} +0 -0
  298. /package/dist/web/assets/{clojure-DS1BvoTt.js → clojure-BSYvXMI8.js} +0 -0
  299. /package/dist/web/assets/{cmake-CDvsJwYp.js → cmake-DGEqH-Np.js} +0 -0
  300. /package/dist/web/assets/{codeowners-BjsYiflJ.js → codeowners-D8FCOxWg.js} +0 -0
  301. /package/dist/web/assets/{codeql-CG-7ARat.js → codeql-COV0eluG.js} +0 -0
  302. /package/dist/web/assets/{common-lisp-DSO1Lbh9.js → common-lisp-xHnZUlgt.js} +0 -0
  303. /package/dist/web/assets/{coq-DYdh3oP_.js → coq-Cs_2i3Xg.js} +0 -0
  304. /package/dist/web/assets/{csharp-DxEBz5s0.js → csharp-B1ONB4PT.js} +0 -0
  305. /package/dist/web/assets/{css-dal3Q-8C.js → css-BgGQbvrZ.js} +0 -0
  306. /package/dist/web/assets/{csv-CpqjMGiA.js → csv-CNHmQBzs.js} +0 -0
  307. /package/dist/web/assets/{csv-parser-3umBA4NY.js → csv-parser-DWR24J17.js} +0 -0
  308. /package/dist/web/assets/{cue-DinoJ_5k.js → cue-ZXAKzwIy.js} +0 -0
  309. /package/dist/web/assets/{cypher-DzxqmkUW.js → cypher-4PL5bmT5.js} +0 -0
  310. /package/dist/web/assets/{d-BxNwJuEC.js → d-CEsV3RT2.js} +0 -0
  311. /package/dist/web/assets/{dark-plus-Clmv3V-0.js → dark-plus-NtFnYSDz.js} +0 -0
  312. /package/dist/web/assets/{dart-BWVtXD35.js → dart-CKHn4mDf.js} +0 -0
  313. /package/dist/web/assets/{data-grid-types-B4n51Fck.js → data-grid-types-CxU8oo-j.js} +0 -0
  314. /package/dist/web/assets/{dax-DsImuWgJ.js → dax-Cmkj1I_M.js} +0 -0
  315. /package/dist/web/assets/{desktop-B0jJuG2r.js → desktop-DJLalvPQ.js} +0 -0
  316. /package/dist/web/assets/{diff-CWqAAxoE.js → diff-BnS2wpj6.js} +0 -0
  317. /package/dist/web/assets/{dist-C0eNNfbt.js → dist-CuIdRyUB.js} +0 -0
  318. /package/dist/web/assets/{dist-B-EXdSYE.js → dist-uYjs3Wv6.js} +0 -0
  319. /package/dist/web/assets/{docker-CYFCJdIL.js → docker-CovNy7ml.js} +0 -0
  320. /package/dist/web/assets/{dotenv-CKDUjURg.js → dotenv-Gh4w3I6B.js} +0 -0
  321. /package/dist/web/assets/{dracula-CSoPspHE.js → dracula-NF0tAHXl.js} +0 -0
  322. /package/dist/web/assets/{dracula-soft-BuFjw5L1.js → dracula-soft-Cj_qBurK.js} +0 -0
  323. /package/dist/web/assets/{dream-maker-B6PdDRZu.js → dream-maker-CRuZPcl7.js} +0 -0
  324. /package/dist/web/assets/{emacs-lisp-CMcXvD0G.js → emacs-lisp-BACmC9qj.js} +0 -0
  325. /package/dist/web/assets/{everforest-dark-cU8-wYzi.js → everforest-dark-Dd9av4QG.js} +0 -0
  326. /package/dist/web/assets/{everforest-light-KhorllqK.js → everforest-light-C-jsss-B.js} +0 -0
  327. /package/dist/web/assets/{external-link-B8BynhvY.js → external-link-B6Hl0U4X.js} +0 -0
  328. /package/dist/web/assets/{fennel-BCELZK87.js → fennel-BJA9XnL8.js} +0 -0
  329. /package/dist/web/assets/{file-exclamation-point-CZ4DiR75.js → file-exclamation-point-JzhTU2TX.js} +0 -0
  330. /package/dist/web/assets/{fish-BQc0y_jy.js → fish-CKlhNSc2.js} +0 -0
  331. /package/dist/web/assets/{fluent-BTINnwF6.js → fluent-wfglRk5D.js} +0 -0
  332. /package/dist/web/assets/{folder-symlink-B8XrtvQK.js → folder-symlink-Dx1hOk8X.js} +0 -0
  333. /package/dist/web/assets/{format-bytes-WVcFxJuT.js → format-bytes-utwHMTry.js} +0 -0
  334. /package/dist/web/assets/{fortran-free-form-BQim9wDz.js → fortran-free-form-BlXCdid3.js} +0 -0
  335. /package/dist/web/assets/{gdscript-03w5u8un.js → gdscript-BwAVG6Et.js} +0 -0
  336. /package/dist/web/assets/{gdshader-Dp2daNzU.js → gdshader-DPc_T7ib.js} +0 -0
  337. /package/dist/web/assets/{genie-CoNcjPiD.js → genie-B-zZSLCX.js} +0 -0
  338. /package/dist/web/assets/{gherkin-NrPOSPQL.js → gherkin-CJ0ajpGs.js} +0 -0
  339. /package/dist/web/assets/{github-dark-z4RE9ZqA.js → github-dark-Dz8ZkzSP.js} +0 -0
  340. /package/dist/web/assets/{github-dark-default-DOKw-1cI.js → github-dark-default-CNUYo_kp.js} +0 -0
  341. /package/dist/web/assets/{github-dark-dimmed-DJRNMkEA.js → github-dark-dimmed-241z_Ycx.js} +0 -0
  342. /package/dist/web/assets/{github-dark-high-contrast-Bj7jJ_Gs.js → github-dark-high-contrast-7r6yZZYH.js} +0 -0
  343. /package/dist/web/assets/{github-light-B4k_yMFu.js → github-light-C8gmZn-D.js} +0 -0
  344. /package/dist/web/assets/{github-light-default-BHc-Uzhs.js → github-light-default-GAEgq1eA.js} +0 -0
  345. /package/dist/web/assets/{github-light-high-contrast-CHisIywL.js → github-light-high-contrast-3z91352E.js} +0 -0
  346. /package/dist/web/assets/{gleam-CfbypZ4q.js → gleam-C7vxW5zd.js} +0 -0
  347. /package/dist/web/assets/{gn-YoAjyhJa.js → gn-CyJIXwHP.js} +0 -0
  348. /package/dist/web/assets/{gnuplot-xVkc1f1c.js → gnuplot-CqLbGgPd.js} +0 -0
  349. /package/dist/web/assets/{go-GPCwP3Gu.js → go-DdDcK9aX.js} +0 -0
  350. /package/dist/web/assets/{groovy-Crh4Yww9.js → groovy-UYHggEqH.js} +0 -0
  351. /package/dist/web/assets/{gruvbox-dark-hard-BluUFuDS.js → gruvbox-dark-hard-B4ierkJn.js} +0 -0
  352. /package/dist/web/assets/{gruvbox-dark-medium-Cxk1FWQp.js → gruvbox-dark-medium-DYwkDSZ6.js} +0 -0
  353. /package/dist/web/assets/{gruvbox-dark-soft-CHXMiFmj.js → gruvbox-dark-soft-CVL4P-Lz.js} +0 -0
  354. /package/dist/web/assets/{gruvbox-light-hard-B6eKPjAD.js → gruvbox-light-hard-BJ0LewUZ.js} +0 -0
  355. /package/dist/web/assets/{gruvbox-light-medium-pDwMm1ut.js → gruvbox-light-medium-CaqqjeGJ.js} +0 -0
  356. /package/dist/web/assets/{gruvbox-light-soft-Cxu6kHqK.js → gruvbox-light-soft-ByKMoVNr.js} +0 -0
  357. /package/dist/web/assets/{haskell-C6JKHjbK.js → haskell-BilYRVPp.js} +0 -0
  358. /package/dist/web/assets/{haxe-BuBNdF0a.js → haxe-Brs9Bj1s.js} +0 -0
  359. /package/dist/web/assets/{hcl-DZpDdGj2.js → hcl-ClrZpol-.js} +0 -0
  360. /package/dist/web/assets/{hjson-CBkhYRuo.js → hjson-9PvHnYMX.js} +0 -0
  361. /package/dist/web/assets/{hlsl-C5nMxnp9.js → hlsl-BbBk4vqV.js} +0 -0
  362. /package/dist/web/assets/{horizon-VAFvA6R6.js → horizon-E5Py0cny.js} +0 -0
  363. /package/dist/web/assets/{horizon-bright-BC0xn7T3.js → horizon-bright-DVCsNGEk.js} +0 -0
  364. /package/dist/web/assets/{houston-BxrUivmz.js → houston-JdFEax9Y.js} +0 -0
  365. /package/dist/web/assets/{hy-DLtOXoaH.js → hy-W_W3tLKT.js} +0 -0
  366. /package/dist/web/assets/{image-gallery-Gnf7TdCR.js → image-gallery-5IURd6db.js} +0 -0
  367. /package/dist/web/assets/{imba-BdpFXM2W.js → imba-CfAqJz7T.js} +0 -0
  368. /package/dist/web/assets/{ini-BInJO_T0.js → ini-C_3gvKWs.js} +0 -0
  369. /package/dist/web/assets/{java-Dxuddnkb.js → java-CKqtXZ-j.js} +0 -0
  370. /package/dist/web/assets/{javascript-CVamJfLY.js → javascript-C7gSEfae.js} +0 -0
  371. /package/dist/web/assets/{json-BLZ0ozYk.js → json-BoN6e11T.js} +0 -0
  372. /package/dist/web/assets/{json5-BnXzmMKs.js → json5-CgE7C8HW.js} +0 -0
  373. /package/dist/web/assets/{jsonc-CSgkSga7.js → jsonc-DjyoattY.js} +0 -0
  374. /package/dist/web/assets/{jsonl-CY5W2nF2.js → jsonl-lI82wGSw.js} +0 -0
  375. /package/dist/web/assets/{jsonnet-CEJYH3sF.js → jsonnet-CszEI8va.js} +0 -0
  376. /package/dist/web/assets/{jssm-CsJc7Uhk.js → jssm-CZXDlszb.js} +0 -0
  377. /package/dist/web/assets/{jsx-jCiRaHU7.js → jsx-BeRz0eEz.js} +0 -0
  378. /package/dist/web/assets/{kanagawa-dragon-DfoA_GxX.js → kanagawa-dragon-BOmnUiyd.js} +0 -0
  379. /package/dist/web/assets/{kanagawa-lotus-BOLYJpWd.js → kanagawa-lotus-ffU_P5w-.js} +0 -0
  380. /package/dist/web/assets/{kanagawa-wave-DfKlbT1G.js → kanagawa-wave-DKTJZrMu.js} +0 -0
  381. /package/dist/web/assets/{kdl-CmssIFTi.js → kdl-BWECJveH.js} +0 -0
  382. /package/dist/web/assets/{kotlin-BRsNgp6x.js → kotlin-DpVwBCSt.js} +0 -0
  383. /package/dist/web/assets/{kusto-T8X9jUab.js → kusto-BgVqjUZA.js} +0 -0
  384. /package/dist/web/assets/{laserwave-DkJMVJ8Z.js → laserwave-CEVCi8em.js} +0 -0
  385. /package/dist/web/assets/{lean-C65KGVPq.js → lean-CGyfRhLJ.js} +0 -0
  386. /package/dist/web/assets/{less-exIPdDlf.js → less-B0_H10Ku.js} +0 -0
  387. /package/dist/web/assets/{lib-mC1NZfuw.js → lib-hrC3bruJ.js} +0 -0
  388. /package/dist/web/assets/{libesm-DSutUtl1.js → libesm-DVpFmwJu.js} +0 -0
  389. /package/dist/web/assets/{light-plus-DiHUXAZt.js → light-plus-DqzMqPID.js} +0 -0
  390. /package/dist/web/assets/{llvm-CJBnEzFM.js → llvm-9ucnfNiF.js} +0 -0
  391. /package/dist/web/assets/{loader-circle-gSPVxfvH.js → loader-circle-Dpqcv0eS.js} +0 -0
  392. /package/dist/web/assets/{lock-Did-87UV.js → lock-DldbT31x.js} +0 -0
  393. /package/dist/web/assets/{log-DxMyUMuP.js → log-m6A2dOSj.js} +0 -0
  394. /package/dist/web/assets/{logo-CC9NKzca.js → logo-ByPcsC9P.js} +0 -0
  395. /package/dist/web/assets/{luau-Bw10I6zf.js → luau-DFCo8nBn.js} +0 -0
  396. /package/dist/web/assets/{make-C_6M90m6.js → make-C90w3MeA.js} +0 -0
  397. /package/dist/web/assets/{markdown-bR9_R-LF.js → markdown-DT0uywZG.js} +0 -0
  398. /package/dist/web/assets/{material-theme-BUx2Qujn.js → material-theme-BVUO93tJ.js} +0 -0
  399. /package/dist/web/assets/{material-theme-darker-CRJrPgKm.js → material-theme-darker-xXgGDwnF.js} +0 -0
  400. /package/dist/web/assets/{material-theme-lighter-CUAYppPa.js → material-theme-lighter-BjStPzWB.js} +0 -0
  401. /package/dist/web/assets/{material-theme-ocean-DuINF1fg.js → material-theme-ocean-wFmSiKjy.js} +0 -0
  402. /package/dist/web/assets/{material-theme-palenight-W6_icGCj.js → material-theme-palenight-e-0CtRoe.js} +0 -0
  403. /package/dist/web/assets/{matlab-rfqn_b3z.js → matlab-BNaNQAUH.js} +0 -0
  404. /package/dist/web/assets/{mdx-Bh1B_YOJ.js → mdx-DfpVWAqO.js} +0 -0
  405. /package/dist/web/assets/{mermaid-Dxf6Ya2k.js → mermaid-Bpq7wFCP.js} +0 -0
  406. /package/dist/web/assets/{min-dark-CivZ2L0h.js → min-dark-CL2Y0WH6.js} +0 -0
  407. /package/dist/web/assets/{min-light-B1Ykqtma.js → min-light-DOgr_IdP.js} +0 -0
  408. /package/dist/web/assets/{mipsasm-DynHaM0d.js → mipsasm-C9ie2zlB.js} +0 -0
  409. /package/dist/web/assets/{mojo-Clw5PbGH.js → mojo-Md2A2sJy.js} +0 -0
  410. /package/dist/web/assets/{monitor-Dv8vn_6O.js → monitor-4vWfqzuN.js} +0 -0
  411. /package/dist/web/assets/{monokai-DFIooFZT.js → monokai-nbXIEySB.js} +0 -0
  412. /package/dist/web/assets/{moonbit-DnBSZWGM.js → moonbit-CJ-T-s4A.js} +0 -0
  413. /package/dist/web/assets/{move-CCcL2PfL.js → move-DwEehLIK.js} +0 -0
  414. /package/dist/web/assets/{narrat-BDarg5Lk.js → narrat-CuF4sIKr.js} +0 -0
  415. /package/dist/web/assets/{nextflow-groovy-DrZOeCNW.js → nextflow-groovy-1dYhfPfq.js} +0 -0
  416. /package/dist/web/assets/{night-owl-BnZZCK7v.js → night-owl-gZS5mfzY.js} +0 -0
  417. /package/dist/web/assets/{night-owl-light-CEwBsVHS.js → night-owl-light-DDAster5.js} +0 -0
  418. /package/dist/web/assets/{nix-amCzOPMR.js → nix-BlbEYs2c.js} +0 -0
  419. /package/dist/web/assets/{nord-BIs2BC6q.js → nord-BRiPb8Vy.js} +0 -0
  420. /package/dist/web/assets/{nushell-Y02fo4yw.js → nushell-DgWicB1i.js} +0 -0
  421. /package/dist/web/assets/{objective-c-7xOZtWW1.js → objective-c-D8mPZ42Y.js} +0 -0
  422. /package/dist/web/assets/{objective-cpp-eVtjUvVB.js → objective-cpp-B0pNXzuj.js} +0 -0
  423. /package/dist/web/assets/{ocaml-Bi6ZwqG4.js → ocaml-lQd7aK5d.js} +0 -0
  424. /package/dist/web/assets/{odin-8xf1Y0S9.js → odin-CxtIOBjm.js} +0 -0
  425. /package/dist/web/assets/{one-dark-pro-hVv-_y_b.js → one-dark-pro-T_UFnWfe.js} +0 -0
  426. /package/dist/web/assets/{one-light-CJJ3MtHt.js → one-light-LBVqwyV-.js} +0 -0
  427. /package/dist/web/assets/{openscad-B2HcywLk.js → openscad-C5dnS6J7.js} +0 -0
  428. /package/dist/web/assets/{pascal-C7fllV_7.js → pascal-CmWCWQML.js} +0 -0
  429. /package/dist/web/assets/{pkl-qvLC1IWP.js → pkl-BWWOtDQk.js} +0 -0
  430. /package/dist/web/assets/{plastic-BT1wHZv2.js → plastic-ekhupckF.js} +0 -0
  431. /package/dist/web/assets/{play-4MZi1Axp.js → play-CpCAoQRZ.js} +0 -0
  432. /package/dist/web/assets/{plsql-1MsmdmJD.js → plsql-BPsDHXx4.js} +0 -0
  433. /package/dist/web/assets/{plus-BhMZqFsN.js → plus-BR0qewJI.js} +0 -0
  434. /package/dist/web/assets/{po-BigskytD.js → po-BX2FiuW2.js} +0 -0
  435. /package/dist/web/assets/{poimandres-DuklniF7.js → poimandres-DzXOdZcN.js} +0 -0
  436. /package/dist/web/assets/{polar-e2pjN_sp.js → polar-DDRXgJzG.js} +0 -0
  437. /package/dist/web/assets/{portal-container-context-Cm5HipFr.js → portal-container-context-a399Hd9F.js} +0 -0
  438. /package/dist/web/assets/{postcss-DtAeYjWw.js → postcss-BHW6XBhx.js} +0 -0
  439. /package/dist/web/assets/{powerquery-Cr2PtehI.js → powerquery-B89c2NBb.js} +0 -0
  440. /package/dist/web/assets/{powershell-BxFTGLJ4.js → powershell-Dvxrd3Tu.js} +0 -0
  441. /package/dist/web/assets/{prisma-COWJiwdZ.js → prisma-lMK8EFLU.js} +0 -0
  442. /package/dist/web/assets/{prolog-DFvGKRx6.js → prolog-D0YrtMG4.js} +0 -0
  443. /package/dist/web/assets/{proto-CcK80lFt.js → proto-J0mM25aw.js} +0 -0
  444. /package/dist/web/assets/{puppet-BF60bZ4F.js → puppet-CUT0xkhh.js} +0 -0
  445. /package/dist/web/assets/{purescript-zL_69MzU.js → purescript-CoJWSGRp.js} +0 -0
  446. /package/dist/web/assets/{python-CIwKhfb_.js → python-BBrmuz6P.js} +0 -0
  447. /package/dist/web/assets/{qmldir-CXNe04Lo.js → qmldir-CUtzs4Q7.js} +0 -0
  448. /package/dist/web/assets/{qss-C8lAWe6G.js → qss-Dm1RZ8aL.js} +0 -0
  449. /package/dist/web/assets/{r-CL5ZVL3_.js → r-BGP7qZTe.js} +0 -0
  450. /package/dist/web/assets/{racket-BBfh0uh0.js → racket-K_6U1lqJ.js} +0 -0
  451. /package/dist/web/assets/{raku-o6E8R5Q3.js → raku-bNSiZeW_.js} +0 -0
  452. /package/dist/web/assets/{react-CBTnT4sY.js → react-X4jUntEf.js} +0 -0
  453. /package/dist/web/assets/{red-GomRF2Xp.js → red-uVeIEF0f.js} +0 -0
  454. /package/dist/web/assets/{refresh-cw-B06yaBrW.js → refresh-cw-B9iv7csb.js} +0 -0
  455. /package/dist/web/assets/{reg-_ffDIvx9.js → reg-DSm9GWjh.js} +0 -0
  456. /package/dist/web/assets/{regexp-Bl5sF65W.js → regexp-CljRm_j_.js} +0 -0
  457. /package/dist/web/assets/{rel-DgMdqDuj.js → rel-d3JcopZg.js} +0 -0
  458. /package/dist/web/assets/{resolve-theme-CRt_7wpd.js → resolve-theme-DLKmm-lE.js} +0 -0
  459. /package/dist/web/assets/{riscv-8b3-Ye4X.js → riscv-CJmaELjT.js} +0 -0
  460. /package/dist/web/assets/{ron-DrmU1KHP.js → ron-D0g9B1rW.js} +0 -0
  461. /package/dist/web/assets/{rose-pine-C_-E2tF1.js → rose-pine-DOrD2BSH.js} +0 -0
  462. /package/dist/web/assets/{rose-pine-dawn-DEfhNlFH.js → rose-pine-dawn-CzZQ-LCF.js} +0 -0
  463. /package/dist/web/assets/{rose-pine-moon-BdvX3gdn.js → rose-pine-moon-CdmsYp7C.js} +0 -0
  464. /package/dist/web/assets/{rosmsg-B6GhGpd5.js → rosmsg-CmDth6pO.js} +0 -0
  465. /package/dist/web/assets/{rotate-ccw-COXu_9oj.js → rotate-ccw-DnuPzJRE.js} +0 -0
  466. /package/dist/web/assets/{rust-DmC97M1-.js → rust-DGdP6jiR.js} +0 -0
  467. /package/dist/web/assets/{sass-B0tJOHLM.js → sass-Dkgt2FKZ.js} +0 -0
  468. /package/dist/web/assets/{save-CtIGOjmx.js → save-DxonD4y1.js} +0 -0
  469. /package/dist/web/assets/{scala-n8zaFOXv.js → scala-BQLTON6C.js} +0 -0
  470. /package/dist/web/assets/{scheme-BKD9ztBU.js → scheme-DKlxUGCi.js} +0 -0
  471. /package/dist/web/assets/{sdbl-D8gB5-rS.js → sdbl-C3iZJQeZ.js} +0 -0
  472. /package/dist/web/assets/{send-BlyNJAzy.js → send-B-t5Mz26.js} +0 -0
  473. /package/dist/web/assets/{shallow-DYo6goJ4.js → shallow-jwPzEunk.js} +0 -0
  474. /package/dist/web/assets/{shellscript-BfXlf_Ao.js → shellscript-BRu-ZQAx.js} +0 -0
  475. /package/dist/web/assets/{shield-check-B0iGUiOK.js → shield-check-CdTiK1j2.js} +0 -0
  476. /package/dist/web/assets/{shield-off-BNLWBhcq.js → shield-off-DuetkXKM.js} +0 -0
  477. /package/dist/web/assets/{slack-dark-CylEddse.js → slack-dark-qkm_u2zz.js} +0 -0
  478. /package/dist/web/assets/{slack-ochin-w8wg8vuC.js → slack-ochin-BP8_8e4m.js} +0 -0
  479. /package/dist/web/assets/{smalltalk-hiAXA64u.js → smalltalk-Bnz4gt07.js} +0 -0
  480. /package/dist/web/assets/{snazzy-light-BwlV3Xty.js → snazzy-light-fbPZg5io.js} +0 -0
  481. /package/dist/web/assets/{solarized-dark-vmNGdykz.js → solarized-dark-CM0vTRoi.js} +0 -0
  482. /package/dist/web/assets/{solarized-light-DGv_0JXq.js → solarized-light-C4_W9mvs.js} +0 -0
  483. /package/dist/web/assets/{solidity-Y7QSH0B1.js → solidity-Bz6v1PoD.js} +0 -0
  484. /package/dist/web/assets/{splunk-BtgpLjHi.js → splunk-DyNxu-0i.js} +0 -0
  485. /package/dist/web/assets/{sql-CRHYhOia.js → sql-DXRL1gw1.js} +0 -0
  486. /package/dist/web/assets/{ssh-config-DOqSs3au.js → ssh-config-2z1QHuOG.js} +0 -0
  487. /package/dist/web/assets/{stylus-crCAEaBT.js → stylus-D4I-VaeY.js} +0 -0
  488. /package/dist/web/assets/{swift-eE3rT7TC.js → swift-C_2G-tIc.js} +0 -0
  489. /package/dist/web/assets/{synthwave-84-BWy14LBX.js → synthwave-84-osBjChRP.js} +0 -0
  490. /package/dist/web/assets/{system-verilog-DC0-DZM4.js → system-verilog-zQP2qPNU.js} +0 -0
  491. /package/dist/web/assets/{systemd-to5p6N8I.js → systemd-UGfjRSBM.js} +0 -0
  492. /package/dist/web/assets/{table-Bbc783ko.js → table-FLU8ei8b.js} +0 -0
  493. /package/dist/web/assets/{talonscript-DoTcU8wm.js → talonscript-C1XrFPwo.js} +0 -0
  494. /package/dist/web/assets/{tasl-cNZvUs95.js → tasl-B5raVn43.js} +0 -0
  495. /package/dist/web/assets/{tcl-mZE8sZH8.js → tcl-3-vbeMIc.js} +0 -0
  496. /package/dist/web/assets/{terraform-CqHPpIfu.js → terraform-a8VXZDm9.js} +0 -0
  497. /package/dist/web/assets/{text-wrap-CBT4Gxtg.js → text-wrap-B05VGRa-.js} +0 -0
  498. /package/dist/web/assets/{tokyo-night-Tp2rTIZd.js → tokyo-night-4SRhf-Hz.js} +0 -0
  499. /package/dist/web/assets/{toml-CU_vscgn.js → toml-B4N0i8wT.js} +0 -0
  500. /package/dist/web/assets/{trash-2-Bk4yYlck.js → trash-2-CTXlTflF.js} +0 -0
  501. /package/dist/web/assets/{triangle-alert-B8EYecpe.js → triangle-alert-BRq0VAfd.js} +0 -0
  502. /package/dist/web/assets/{tsv-ik7iFB9D.js → tsv-DA5k9Lvw.js} +0 -0
  503. /package/dist/web/assets/{tsx-BMvvMKiv.js → tsx-D9SZ8owC.js} +0 -0
  504. /package/dist/web/assets/{turtle-DBqKCq9o.js → turtle-BTHTvyY_.js} +0 -0
  505. /package/dist/web/assets/{typescript-DXJDmMZZ.js → typescript-DYzV9p5L.js} +0 -0
  506. /package/dist/web/assets/{typespec-CBseCMtZ.js → typespec-BC15eh7F.js} +0 -0
  507. /package/dist/web/assets/{typst-4oY25U9y.js → typst-BJUqzA6D.js} +0 -0
  508. /package/dist/web/assets/{use-is-mobile-D1weanbj.js → use-is-mobile-DFWViA4S.js} +0 -0
  509. /package/dist/web/assets/{utils-BNRH507E.js → utils-C9OjCrou.js} +0 -0
  510. /package/dist/web/assets/{v-BXaZ_1oy.js → v-50Lps6oA.js} +0 -0
  511. /package/dist/web/assets/{vala-BImWJE7z.js → vala-8wiUvvrD.js} +0 -0
  512. /package/dist/web/assets/{vb-CLfj2_sy.js → vb-BlgPLt6B.js} +0 -0
  513. /package/dist/web/assets/{vendor-xterm-_yyu5n32.js → vendor-xterm-BZOrwF_F.js} +0 -0
  514. /package/dist/web/assets/{verilog-BWa8Ed2b.js → verilog-CD9pVW5p.js} +0 -0
  515. /package/dist/web/assets/{vesper-BGj1cg6m.js → vesper-BrivW4PT.js} +0 -0
  516. /package/dist/web/assets/{vhdl-mByK7mlx.js → vhdl-Bunxfa7N.js} +0 -0
  517. /package/dist/web/assets/{viml-CugmlW5g.js → viml-DndUb2qM.js} +0 -0
  518. /package/dist/web/assets/{vitesse-black-vPdpy9TL.js → vitesse-black-BurZSkxW.js} +0 -0
  519. /package/dist/web/assets/{vitesse-dark-BuQK3dUh.js → vitesse-dark-8UShspzC.js} +0 -0
  520. /package/dist/web/assets/{vitesse-light-B3nz1XL9.js → vitesse-light-DghCAZ90.js} +0 -0
  521. /package/dist/web/assets/{vyper-DH_GTIHG.js → vyper-b-Clr-8u.js} +0 -0
  522. /package/dist/web/assets/{wasm-DfVbVJlW.js → wasm-C5v2T06Y.js} +0 -0
  523. /package/dist/web/assets/{wasm-C-LF9GjX.js → wasm-CpcKStkw.js} +0 -0
  524. /package/dist/web/assets/{wenyan-CzQkHyHx.js → wenyan-Dz2srLZR.js} +0 -0
  525. /package/dist/web/assets/{wgsl-Du_M1DW9.js → wgsl-CeD1-dRb.js} +0 -0
  526. /package/dist/web/assets/{wikitext-BWN_w5S7.js → wikitext-CyeS-9FH.js} +0 -0
  527. /package/dist/web/assets/{wit-BR13v5HD.js → wit-Kr-q-qBQ.js} +0 -0
  528. /package/dist/web/assets/{wolfram-BoXxp_Xl.js → wolfram-BKm8bmln.js} +0 -0
  529. /package/dist/web/assets/{x-nhriOX9o.js → x-YDu4m2Iq.js} +0 -0
  530. /package/dist/web/assets/{yaml-DpR_EF-d.js → yaml-2XSxomjM.js} +0 -0
  531. /package/dist/web/assets/{zenscript-C-EdUtUd.js → zenscript-CKIS34vf.js} +0 -0
  532. /package/dist/web/assets/{zig-BD4eA5nU.js → zig-DdHGNllX.js} +0 -0
@@ -1,5 +1,16 @@
1
1
  # PPM System Architecture
2
2
 
3
+ This file holds the cross-cutting picture: the layer map, the protocols every feature speaks, auth,
4
+ deployment and the error/security posture. Subsystem detail lives beside it:
5
+
6
+ | Document | Covers |
7
+ |---|---|
8
+ | [AI Chat & Providers](architecture/ai-chat-and-providers.md) | Provider adapters, AI configuration, the persistent chat streaming session |
9
+ | [Extension System](architecture/extensions.md) | Manifest, lifecycle, RPC, worker isolation, contribution registry, dev workflow |
10
+ | [Data & Storage](architecture/data-and-storage.md) | SQLite schema and access, database viewer, MCP server management, group-chat model |
11
+ | [Workspace & UI](architecture/workspace-and-ui.md) | Workspace switching, editor, terminal, git, file service, OS File Explorer, tab-host windows and Document PiP |
12
+ | [Integrations](architecture/integrations.md) | PPMBot Telegram coordinator, Jira watcher auto-debug |
13
+
3
14
  ## High-Level Architecture
4
15
 
5
16
  ```
@@ -15,7 +26,7 @@
15
26
  ├──────────────────────────────┼────────────────────────────────────────┤
16
27
  │ PPM Server (Bun) │
17
28
  │ ┌────────────────────────────────────────────────────────────────┐ │
18
- │ │ Hono HTTP Framework (Port 8080) │ │
29
+ │ │ Hono HTTP Framework (default port 3210) │ │
19
30
  │ ├────────────────────────────────────────────────────────────────┤ │
20
31
  │ │ Routes (src/server/routes/) │ │
21
32
  │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │
@@ -196,8 +207,8 @@ Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `
196
207
  | **ChatService** | Session management, message streaming | createSession, streamMessage, getHistory |
197
208
  | **SessionBranchService** | Edit-message global branch tree (`session_branches` table) — links forked sessions, resolves version groups, collapses history to per-tree heads. `resolveVersionMap` batches every ordinal's group for a session in 2 queries (the per-ordinal `resolveVersionGroup` walks the ancestor chain with one query per hop) | recordBranch, resolveVersionGroup, resolveVersionMap, collapseTreesToHeads, hasChildren, getTreeByRoot |
198
209
  | **TaskStatusAggregator** | Rebuild Claude Task* state from session JSONL (TaskCreate/TaskUpdate/TaskStop tracking) | aggregateTasks |
199
- | **ConfigService** | Config loading (YAML→SQLite migration) | load, save, getToken |
200
- | **DbService** | SQLite persistence (10 tables, WAL, connections/accounts/workspace CRUD) | getDb, openTestDb, getWorkspace, setWorkspace, getConnections, insertConnection, deleteConnection, getTableCache |
210
+ | **ConfigService** | Config in SQLite, dotted keys with a typed cache | load, get, set, getToken |
211
+ | **DbService** | SQLite persistence (WAL, schema v41, connections/accounts/workspace CRUD) | getDb, openTestDb, getWorkspace, setWorkspace, getConnections, insertConnection, deleteConnection, getTableCache |
201
212
  | **TableCacheService** | Cache table metadata, search tables | syncTables, searchTables, invalidateCache |
202
213
  | **GitService** | Git command execution | status, diff, commit, stage, branch |
203
214
  | **FileService** | File operations with validation | read, write, tree, delete, mkdir |
@@ -240,434 +251,6 @@ Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `
240
251
  | **AgentRunner** | Provider-backed member turn (summary from full text, not last chunk), bounded-concurrency parallel dispatch | runAgentTurn, dispatchParallel, makeEngineRunAgent |
241
252
  | **TranscriptArchive** | Option A+ archive-and-delete of member JSONLs to `~/.ppm/teams/<group>/transcripts/` (copy-verified before delete) | archiveAndDelete, readArchivedTranscript |
242
253
 
243
- ### Group-Chat Data Model (schema v35)
244
-
245
- Three tables back the native group-chat engine; the message bus is a single
246
- table keyed by `kind` + JSON `data` (spike-validated) with a monotonic `seq`
247
- PK for stable ordering, and is the durable source of truth across Stop/Resume:
248
-
249
- - `chat_groups(id, project_name, project_path, name, leader_session_id, status[active|paused|idle], max_turns=40, max_cost_usd=5.0, created_at)`
250
- - `chat_group_members(id, group_id→chat_groups, role[leader|member], persona, agent_type, model, session_id, name, color, status, joined_at)`
251
- - `chat_group_messages(seq PK, id, group_id→chat_groups, from_member, to_member, kind[task|chat|status|completion|final], summary, full_session_ref, data JSON, turn_index, created_at)`
252
-
253
- Flow: user message → engine runs sequential @mention-driven turns over the bus →
254
- converges to one `final` → member transcripts archived (Option A+). Stop aborts
255
- mid-turn (cooperative) and pauses; Resume re-spawns fresh sessions and re-enters
256
- the loop seeded from the bus (windowed + rolling summary).
257
- | **TagService** | Session tagging CRUD, bulk operations, tag-session enrichment | seedDefaultTags, getTagsByProject, createTag, updateTag, deleteTag, setSessionTag, bulkSetSessionTag, getSessionTags, getTagSessionCounts |
258
- | **DraftService** | Chat draft auto-save per session, 50KB cap | get, upsert, delete, deleteOrphaned |
259
- | **FileFilterService** | Glob pattern matching + precedence-enforced filtering (hardcoded ⊂ global ⊂ project) | mergeFilters, isPathIgnored, matchesPattern |
260
- | **SystemMetricsService** (`src/services/system-metrics/`) | Whole-machine Task Manager backend: CPU per core + RAM via `node:os`, disk/net/GPU + all processes via per-OS collectors (Linux `/proc`, macOS `ps`, Windows one long-lived PowerShell REPL child, `Win32_Process` + `PerfRawData` per 2 s tick), delta-based CPU%, grouping by app root, aggregate-only 30-min history. Two SSE tiers on `/api/system/resources/stream`: `light` (status bar, no children spawned) and `full` (`?processes=1`, demand-gated collectors, 60 s teardown). Subscriber lease (sid + 10 s ping, 30 s expiry) because Cloudflare tunnel never propagates client disconnects. Guarded `POST /resources/kill`: protected set (PPM server/supervisor/edge/cloudflared, OS-critical names), ancestor/tree-intersection rule, `startedAt` identity re-query → 409, JSON + `X-PPM-Request` header. | subscribe, unsubscribe, ping, getLatest, kill, reapExpired |
261
-
262
- **Key Files:** `src/services/*.service.ts`, `src/services/tag.service.ts`, `src/services/ppmbot/*.ts`, `src/services/bash-output-spy.ts`, `src/services/system-metrics/system-metrics.service.ts`, `src/services/system-metrics/kill-guard.ts`, `src/services/system-metrics/powershell-session.ts`, `src/services/redact-secrets.ts`, `src/services/file-filter.service.ts`, `src/cli/commands/bot-cmd.ts`
263
-
264
- ---
265
-
266
- ### File Service & Filtering (Lazy-Load Tree, Palette Index)
267
-
268
- **Component:** FileFilterService + API endpoints `/files/list`, `/files/index`, settings endpoints
269
-
270
- **Overview:** Provides efficient file discovery with VS Code-style glob filtering and gitignore support. Three-layer filter precedence enforces consistent exclude patterns across tree navigation and search indexing.
271
-
272
- **Filter Precedence (evaluated low-to-high):**
273
- 1. **Hardcoded defaults** — `node_modules/**`, `.git`, `.env*` (always excluded, cannot override)
274
- 2. **Global config** — `files.exclude`, `files.searchExclude`, `files.useIgnoreFiles` (applies to all projects)
275
- 3. **Per-project override** — Project-scoped settings (DB: `projects.settings` JSON, schema v21) override global
276
-
277
- **API Endpoints:**
278
- ```
279
- GET /api/project/:name/files/list?path=<rel>
280
- → 1-level directory children with gitignore decoration (isIgnored field)
281
- → { items: [{ name, type, isDir, isIgnored }], ... }
282
-
283
- GET /api/project/:name/files/index
284
- → Flat full-project file list (cached in memory, watcher-invalidated)
285
- → { files: [{ path, isIgnored }], ... }
286
-
287
- GET /api/settings/files
288
- → Global file filter config (all projects)
289
- → { filesExclude: [], searchExclude: [], useIgnoreFiles: bool }
290
-
291
- PATCH /api/settings/files
292
- → Update global config (partial: only specified fields)
293
- → Validates arrays ≤200 items, filters non-string patterns
294
-
295
- GET /api/project/:name/settings
296
- → Per-project settings (includes file filter overrides)
297
- → { filesExclude?: [], searchExclude?: [], useIgnoreFiles?: bool, ... }
298
-
299
- PATCH /api/project/:name/settings
300
- → Per-project override (stored in projects.settings JSON, schema v21)
301
- → Same validation as global, caches invalidation on write
302
- ```
303
-
304
- **Filtering Model:**
305
-
306
- | Config | Applies To | Validation | Notes |
307
- |--------|-----------|------------|-------|
308
- | `filesExclude` | Tree navigation | Glob patterns (max 200) | Hides from tree explorer |
309
- | `searchExclude` | Index + palette search | Glob patterns (max 200) | Hides from search results |
310
- | `useIgnoreFiles` | Both (when true) | Boolean | Include `.gitignore` + `.git/info/exclude` in filtering |
311
-
312
- **Frontend Integration:**
313
- - `useFileStore()` hook manages lazy-loading: `loadRoot()`, `loadChildren()`, `loadIndex()`
314
- - AbortController pool cancels pending requests on project switch
315
- - File tree auto-expands root (1 level), children load on-demand with spinner
316
- - Command palette + chat file-picker switched from tree-flattening to `fileIndex` from store
317
- - "Indexing project…" hint shown when `loadIndex()` is pending
318
-
319
- **Server-Side Implementation:**
320
- - `FileFilterService.mergeFilters()` — Combine hardcoded + global + project overrides with precedence
321
- - `FileFilterService.isPathIgnored()` — Check if path matches any exclude pattern (gitignore if enabled)
322
- - `FileService.list()` — 1-level enumeration with `isIgnored` field computed per item
323
- - In-memory `indexCache` (Map: projectName → FileIndex) invalidated by `fs.watch` (file changes) + manual `invalidateIndexCache()` calls
324
- - WS `file:changed` events routed to `invalidateFolder()` or `invalidateIndex()` depending on scope
325
-
326
- **Database Schema (v21+):**
327
- ```typescript
328
- // projects table gains:
329
- settings: TEXT // JSON: { filesExclude?, searchExclude?, useIgnoreFiles? }
330
-
331
- // Example:
332
- projects.settings = JSON.stringify({
333
- filesExclude: ["**/.venv", "**/*.pyc"],
334
- searchExclude: ["**/node_modules"],
335
- useIgnoreFiles: false
336
- })
337
- ```
338
-
339
- **Deprecated:** `/api/project/:name/files/tree` (marked @deprecated, still functional for backward compat)
340
-
341
- ---
342
-
343
- ### Provider Layer (AI Adapters)
344
- **Component:** Provider interface + implementations
345
-
346
- **Responsibilities:**
347
- - Abstract AI model differences behind common interface
348
- - Stream responses as async generators
349
- - Handle tool use and approval flows
350
- - Track token usage
351
-
352
- **Interface (src/providers/provider.interface.ts):**
353
- ```typescript
354
- interface AIProvider {
355
- createSession(): Promise<Session>;
356
- sendMessage(sessionId: string, message: string, context?: FileContext[]): AsyncIterable<ChatEvent>;
357
- onToolApproval(sessionId: string, requestId: string, approved: boolean, data?: unknown): Promise<void>;
358
- }
359
- ```
360
-
361
- **Implementations:**
362
- - **claude-agent-sdk** (Primary) — @anthropic-ai/claude-agent-sdk, streaming, tool use. Reads model/effort/maxTurns/budget/thinking from config. Settings refreshed per query. Windows CLI fallback for Bun subprocess pipe issues. .env poisoning mitigation. **Multi-account support:** Injects account API token from AccountService instead of relying on ANTHROPIC_API_KEY env var when accounts configured.
363
- - **mock-provider** (Testing) — Returns canned responses
364
- - **cursor-cli** (CLI-based) — Spawns `cursor-agent` CLI binary with NDJSON streaming. Extends `CliProvider` base class.
365
- - **codex/gemini** (Planned) — Pluggable via `CliProvider` extension (~100-150 lines each)
366
-
367
- #### Multi-Provider Architecture (v0.8.61+)
368
-
369
- PPM supports multiple AI providers through a generic `AIProvider` interface and extensible base classes:
370
-
371
- **Provider Types:**
372
- 1. **SDK-based** (claude-agent-sdk) — Uses Anthropic SDK for rich features (approvals, thinking blocks)
373
- 2. **CLI-based** (cursor-cli, codex, gemini) — Spawns external binary with NDJSON streaming
374
-
375
- **Base Classes:**
376
- - `AIProvider` interface — Defines required methods (createSession, sendMessage) + optional capabilities (abortQuery, getMessages, listSessionsByDir, ensureProjectPath)
377
- - `CliProvider` abstract class — Shared spawn/parse/abort logic for all CLI-spawning providers
378
- - Provider-specific subclasses implement: `buildArgs()`, `mapEvent()`, `extractSessionId()`, `isAvailable()`
379
-
380
- **Streaming Infrastructure:**
381
- - `parseNdjsonLines()` utility — Async generator that buffers partial TCP packets, yields complete JSON lines
382
- - `ChatEvent` union type — Normalized event format across all providers (text, tool_use, thinking, approval_request, system, done, error)
383
- - Event mappers translate provider-specific JSON → ChatEvent (e.g., Cursor's `reasoning` type → `thinking` event)
384
-
385
- **Provider Registration & Bootstrap:**
386
- - `ProviderRegistry` maintains active provider instances
387
- - `bootstrapProviders()` async function checks `isAvailable()` on CLI providers before registering
388
- - Graceful fallback: if Cursor binary not found, provider skips registration (no crash, logged as info)
389
- - Config type `AIProviderConfig.type` union: `"agent-sdk" | "cli" | "mock"`
390
-
391
- **CLI-Provider Features:**
392
- - **Session capture** — Extract session ID from provider's init event, re-key process tracking
393
- - **Workspace trust auto-retry** — Detect trust prompts in stderr, retry once with `--trust` flag
394
- - **Process lifecycle** — Track active processes per session, escalate SIGTERM → SIGKILL on abort
395
- - **History loading** — Override `listSessions()` to read native provider history (e.g., Cursor SQLite DAG)
396
- - **Graceful degradation** — Missing binary → provider skipped, not fatal
397
-
398
- **New Files (v0.8.61):**
399
- - `src/utils/ndjson-line-parser.ts` — NDJSON streaming parser
400
- - `src/providers/cli-provider-base.ts` — Abstract base class for CLI providers
401
- - `src/providers/cursor-cli/cursor-provider.ts` — CursorCliProvider implementation
402
- - `src/providers/cursor-cli/cursor-event-mapper.ts` — NDJSON → ChatEvent mapping
403
- - `src/providers/cursor-cli/cursor-history.ts` — SQLite DAG reader for Cursor history
404
- - `src/web/components/chat/provider-selector.tsx` — UI component for provider selection
405
-
406
- ---
407
-
408
- ### PPMBot Coordinator Service Layer (Telegram-based Team Leader)
409
- **Component:** PPMBot coordinator orchestrator + delegation executor
410
-
411
- **Responsibilities:**
412
- - Manage single persistent coordinator session per Telegram chat in `~/.ppm/bot/` workspace
413
- - Route incoming Telegram messages to coordinator (ask/answer) or delegation tracking
414
- - Decide when to answer directly vs. delegate to subagents (based on project context)
415
- - Execute delegated tasks in isolated project sessions
416
- - Track task status and report results back to Telegram
417
- - Format responses as Telegram HTML with progressive message editing
418
-
419
- **Architecture:**
420
- ```
421
- Telegram → PPMBotTelegramService (polling) → PPMBotService (orchestrator)
422
-
423
- PPMBotSessionManager (coordinator session per chat)
424
- coordinatorSession.id → chatService.sendMessage()
425
- Task Poller (5s interval)
426
-
427
- executeDelegation(taskId, telegram, providerId)
428
- ├─ getBotTask(taskId) → prompt
429
- ├─ chatService.createSession(providerId, projectPath)
430
- ├─ run async generator (abort, 900s timeout)
431
- └─ updateBotTaskStatus(taskId, "completed", {result})
432
-
433
- telegram.sendMessage(chatId, result summary)
434
- ```
435
-
436
- **Services (src/services/ppmbot/):**
437
- - **PPMBotService** — Lifecycle (start/stop), message queue, Telegram polling loop, task poller loop
438
- - **PPMBotSessionManager** — Coordinator session cache per chatID, project resolver (case-insensitive, prefix match)
439
- - **PPMBotTelegramService** — Telegram Bot API (getUpdates polling, sendMessage, editMessage, setTyping)
440
- - **PPMBotMemoryService** — SQLite project memories, contextual recall
441
- - **executeDelegation()** — Task execution in isolated session, result capture, timeout/abort handling
442
- - **PPMBotFormatterService** — Markdown → Telegram HTML, 4096-char chunking
443
- - **PPMBotStreamerService** — ChatEvent → progressive Telegram message edits (1s throttle)
444
-
445
- **Coordinator Identity (Persistent Cross-Provider):**
446
- - Location: `~/.ppm/bot/coordinator.md` (loaded on startup, cached in `coordinatorIdentity`)
447
- - Role definition: Team leader, project coordinator, decision-maker
448
- - Decision framework: Answer directly (no project context) vs. Delegate (file access needed)
449
- - Coordination tools: Bash-safe CLI commands (`ppm bot delegate`, `ppm bot task-status`, etc.)
450
- - Cross-provider: Identity text injected as XML context block, works with Claude SDK + CLI providers
451
-
452
- **Delegation Flow:**
453
- 1. User asks task in Telegram
454
- 2. Coordinator decides: delegate? → yes
455
- 3. Coordinator calls bash: `ppm bot delegate --chat <chatId> --project <name> --prompt "<enriched>"`
456
- 4. CLI creates `bot_tasks` row, returns taskId
457
- 5. Service tells user: "Working on it, I'll notify you when done"
458
- 6. Background poller (5s) detects pending task
459
- 7. Executes: `chatService.createSession()` in target project
460
- 8. Streams response, captures summary + full output
461
- 9. Updates task status → "completed"
462
- 10. Sends Telegram notification with result
463
-
464
- **Task Execution (Isolation & Safety):**
465
- - Each task = fresh isolated session (no shared context)
466
- - Timeout: 900s default (configurable per task)
467
- - Abort: AbortController on timeout, can be canceled mid-execution
468
- - Result capture: Both summary (for notification) and full text (for detailed review)
469
- - Error handling: Task status → "failed", error message stored, user notified
470
-
471
- **Database Schema (v14):**
472
- - `bot_tasks` — id (UUID), chatId, projectName, projectPath, prompt, status, resultSummary, resultFull, sessionId, error, reported, timeoutMs, createdAt, startedAt, completedAt
473
- - Indexes: `idx_bot_tasks_status` (fast poller lookup), `idx_bot_tasks_chat` (history queries)
474
-
475
- **Key Design Decisions:**
476
- 1. **Single coordinator session** — Per chat, persistent, one identity (vs. per-task sessions in ClawBot)
477
- 2. **Delegation via CLI** — Coordinator calls bash commands (safer than direct DB writes, auditable)
478
- 3. **Isolated task execution** — Each delegated task spawns fresh session (no context bleed)
479
- 4. **Background polling** — Task execution decoupled from message handler (non-blocking)
480
- 5. **Result summary + full** — Notification shows short summary; user can fetch full output via CLI
481
- 6. **Cross-provider identity** — Single `coordinator.md` works with any AI provider
482
- 7. **Bash-safe tools only** — Coordinator restricted to Bash, Read, Write, Edit, Glob, Grep (safe delegation)
483
-
484
- **CLI Expansion (ppm bot commands):**
485
- ```
486
- ppm bot delegate --chat <id> --project <name> --prompt "<text>" # Create task
487
- ppm bot task-status <id> # Check status
488
- ppm bot task-result <id> # Get full output
489
- ppm bot tasks [--chat <id>] # List recent
490
- ppm bot project list # Available projects
491
- ppm bot project current # Active project
492
- ppm bot project switch <name> # Switch project
493
- ppm bot session new <title> # Create session
494
- ppm bot session list # List sessions
495
- ppm bot session resume <id> # Resume session
496
- ppm bot session stop <id> # Stop session
497
- ppm bot status # Bot health
498
- ppm bot version # PPM version
499
- ppm bot restart # Restart service
500
- ppm bot help # Help
501
- ```
502
-
503
- **Settings UI (ppmbot-settings-section.tsx):**
504
- - Enable/disable PPMBot
505
- - Paired Telegram chats (approval management)
506
- - Default project selection
507
- - System prompt customization
508
- - Task auto-refresh (poll interval, max history)
509
- - Delegated tasks panel (status, result preview, delete)
510
-
511
- ---
512
-
513
- ### ClawBot Service Layer (Telegram Bot Integration) — LEGACY (v0.9.10)
514
- **Component:** Telegram bot service + subsidiary services
515
-
516
- **Responsibilities:**
517
- - Receive Telegram messages via long-polling (no webhooks needed)
518
- - Route Telegram user (chatID) to PPM session with pairing-based security
519
- - Persist session state + conversation memory in SQLite (FTS5)
520
- - Stream AI responses back to Telegram with progressive message editing
521
- - Format responses as Telegram HTML with proper chunking (4096 char limit)
522
-
523
- **Architecture:**
524
- ```
525
- Telegram → ClawBotTelegramService (polling) → ClawBotService (orchestrator)
526
-
527
- ClawBotSessionService (chatID→sessionID)
528
- ClawBotMemoryService (FTS5 recall)
529
- ChatService + ProviderRegistry
530
- ClawBotStreamerService (ChatEvent→edits)
531
- ClawBotFormatterService (Markdown→HTML)
532
- ```
533
-
534
- **Services (src/services/clawbot/):**
535
- - **ClawBotService** — Lifecycle management (start/stop), message queue, routing logic
536
- - **ClawBotTelegramService** — Telegram Bot API wrapper (getUpdates long-polling, sendMessage, editMessage, setTyping, command handlers)
537
- - **ClawBotSessionService** — chatID ↔ PPM sessionID bidirectional mapping, session state tracking
538
- - **ClawBotMemoryService** — FTS5 persistent memory (save, recall with relevance, decay factor, supersede logic, cross-project search by name mention)
539
- - **ClawBotFormatterService** — Markdown → Telegram HTML conversion, message chunking (respects 4096 char limit), code block formatting
540
- - **ClawBotStreamerService** — ChatEvent async generator → progressive Telegram message edits (1s throttle for rate limiting)
541
-
542
- **Security Model:**
543
- - **Pairing System** — Replace allowlists with code-based pairing: User requests pairing → receives code → owner approves in web UI → chatID registered in `clawbot_paired_chats`
544
- - **Per-User Sessions** — Each Telegram chatID maps to isolated PPM session (no cross-user interference)
545
- - **bypassPermissions** — ClawBot bot runs headless, auto-approves tools (no manual approval flow)
546
-
547
- **Database Schema (v13):**
548
- - `clawbot_sessions` — chatID (PK), sessionID (FK chat_sessions), pairedAt, lastUsed
549
- - `clawbot_memories` — id (PK), sessionID (FK), content (text), role (user|assistant), created, decay_factor (FTS5 full-text index)
550
- - `clawbot_paired_chats` — chatID (PK), pairingCode (unique, 6 chars), approvedAt, approvedBy (user ID)
551
-
552
- **Key Design Decisions:**
553
- 1. **Long-polling** — No webhooks = no public URL required, simpler for self-hosted
554
- 2. **Message queue** — Concurrent Telegram messages queued FIFO, prevents race conditions
555
- 3. **Progressive edits** — Edit same message for long responses, reduce Telegram API calls, better UX
556
- 4. **Memory system** — Hybrid extraction (AI primary + regex fallback), supports cross-project search by project name mention
557
- 5. **Config reuse** — Shares existing Telegram bot_token with notifications, separate ClawBotConfig section
558
- 6. **Session tagging** — [Claw] prefix visible in web UI without schema changes, robot icon for identification
559
-
560
- **Settings Endpoints:**
561
- - `GET /api/settings/clawbot` — Fetch config (enabled, bot token, default project, system prompt, debounce, display toggles)
562
- - `PUT /api/settings/clawbot` — Update config
563
- - `GET /api/clawbot/paired-chats` — List paired Telegram chatIDs
564
- - `POST /api/clawbot/pairing` — Request pairing code (returns code)
565
- - `POST /api/clawbot/pairing/:code/approve` — Approve pairing code (owner only)
566
- - `DELETE /api/clawbot/paired-chats/:chatId` — Revoke pairing
567
-
568
- ---
569
-
570
- ### Jira Watcher Auto-Debug Service
571
- **Component:** Jira Cloud REST API poller + direct Claude debug session orchestrator
572
-
573
- **Responsibilities:**
574
- - Poll Jira Cloud per-project on configurable interval (30s–60m)
575
- - Match issues via JQL filters (status, project key, priority, etc.)
576
- - Auto-queue or manually trigger direct Claude debug sessions (no bot_task middleman)
577
- - Manage concurrency: max 2 concurrent, max 1 per project
578
- - Track results (pending/queued/running/done/failed) with unread status
579
- - Notify via WS toast + Telegram when analysis completes
580
- - Rate-limit aware (tracks Jira API quota, auto-backoff 429 responses)
581
-
582
- **Architecture:**
583
- ```
584
- Jira Cloud API ← JiraWatcherService (poller, 30s–60m intervals per watcher)
585
- ├─ searchIssues(jql) → issue list
586
- ├─ insertResult() → SQLite jira_watch_results
587
- └─ jiraDebugService.enqueue() → concurrency queue
588
-
589
- JiraDebugSessionService (concurrency queue processor)
590
- ├─ enqueue(resultId, promptOverride?) → validate + queue
591
- ├─ processQueue() — respects MAX_CONCURRENT=2, MAX_PER_PROJECT=1
592
- ├─ runDebugSession()
593
- │ ├─ chatService.createSession(projectPath) — new isolated session
594
- │ ├─ chatService.sendMessage(prompt) — send with bypassPermissions
595
- │ ├─ capture lastAssistantText (max 500 chars)
596
- │ └─ updateResultStatus() + notificationService.broadcastWs("jira:debug_complete")
597
- └─ cancelDebug(resultId) — abort running session
598
- ```
599
-
600
- **Services (src/services/):**
601
- - **JiraConfigService** — Config CRUD, AES-256 token encryption/decryption, per-project setup
602
- - **JiraWatcherDbService** — Watchers + results table queries, enabled/disabled toggle, last polled tracking
603
- - **JiraApiClient** — Jira Cloud REST v3 (search, getIssue, transitions, test connection), rate limit state, backoff logic
604
- - **JiraWatcherService** — Main poller, timer management (startAll, startWatcher, stopWatcher, pollWatcher), prompt templating, session enqueueing
605
- - **JiraDebugSessionService** — Concurrency queue, session lifecycle, timeout management, abort handling
606
-
607
- **Database Schema (v19):**
608
- - `jira_config` — id, project_id (FK), base_url, email, api_token_encrypted, created_at
609
- - `jira_watchers` — id, jira_config_id (FK), name, jql, prompt_template, enabled, mode ("debug"|"notify"), interval_ms, last_polled_at, created_at
610
- - `jira_watch_results` — id, watcher_id, issue_key, issue_summary, issue_updated, session_id (FK chat_sessions.id), status ("pending"|"queued"|"running"|"done"|"failed"), ai_summary, source ("watcher"|"manual"), triggered_by ("auto"|"manual"), read_at (nullable), deleted, created_at
611
-
612
- **API Routes (src/server/routes/jira*.ts):**
613
- ```
614
- POST /api/jira/config — Create/update config (baseUrl, email, token)
615
- GET /api/jira/config — Get config for active project
616
- DELETE /api/jira/config — Delete config
617
- POST /api/jira/config/test — Test Jira connection
618
- GET /api/jira/watchers — List watchers for config
619
- POST /api/jira/watchers — Create watcher (name, jql, mode, interval)
620
- PATCH /api/jira/watchers/:id — Update watcher
621
- DELETE /api/jira/watchers/:id — Delete watcher (soft delete results)
622
- POST /api/jira/watchers/:id/enable — Enable/disable watcher
623
- POST /api/jira/watchers/:id/poll — Trigger poll now
624
- GET /api/jira/results — List results (paginated, filterable)
625
- POST /api/jira/results/:id/debug — Manually trigger debug for result (with optional prompt override)
626
- POST /api/jira/results/:id/read — Mark result as read
627
- DELETE /api/jira/results/:id — Delete result (soft delete)
628
- GET /api/jira/search — Search Jira (for filter builder UI)
629
- GET /api/jira/ticket/:key — Get full ticket details
630
- GET /api/jira/metadata — Fetch projects, issue types, priorities, statuses
631
- ```
632
-
633
- **CLI Commands (src/cli/commands/jira*.ts):**
634
- ```
635
- ppm jira config set <project> --url <url> --email <email> --token <token>
636
- ppm jira config show <project>
637
- ppm jira config remove <project>
638
- ppm jira config test <project>
639
- ppm jira watch add <project> <name> --jql <jql> [--mode debug|notify] [--interval 300000]
640
- ppm jira watch list <project>
641
- ppm jira watch enable/disable <project> <watcherId>
642
- ppm jira watch remove <project> <watcherId>
643
- ppm jira watch test <project> <watcherId>
644
- ppm jira watch pull <project> <watcherId>
645
- ppm jira results list <project> [--limit 50]
646
- ppm jira results delete <project> <resultId>
647
- ppm jira track <issue-key> — Manually track ticket (insert result, queue debug)
648
- ```
649
-
650
- **Frontend (src/web/components/jira/):**
651
- - **jira-settings-tab.tsx** — Config form, test button, token input
652
- - **jira-filter-builder.tsx** — JQL builder UI (projects, issue types, priorities, statuses, custom JQL)
653
- - **jira-watcher-list.tsx** — List watchers, enable/disable, edit, delete, poll now, interval controls
654
- - **jira-results-panel.tsx** — Results table (issue key, status, summary, AI summary), unread badge, delete, manual debug button
655
- - **jira-debug-prompt-dialog.tsx** — Modal for prompt override when manually triggering debug
656
- - **jira-ticket-detail.tsx** — Modal with full ticket, AI analysis, debug status
657
- - **jira-store.ts** — Zustand (configs, watchers, results, filters, settings, unread count)
658
-
659
- **Key Design Decisions:**
660
- 1. **Direct Claude sessions** — Replaced bot_task flow with direct `chatService.sendMessage()` (simpler, faster, no task overhead)
661
- 2. **Concurrency queue** — Max 2 concurrent globally, max 1 per project (prevents resource starvation, respects project context)
662
- 3. **Manual debug trigger** — Users can override watcher prompt and manually queue debug for any pending result
663
- 4. **Unread tracking** — `read_at` column marks when user views result, UI shows unread badge count
664
- 5. **Prompt templating** — Support {issue_key}, {summary}, {description}, {status}, {priority} placeholders in watcher templates
665
- 6. **Timeout protection** — 10-minute timeout with AbortController graceful cleanup and error capture
666
- 7. **WS notifications** — `jira:debug_complete` event streamed to UI for instant toast feedback
667
- 8. **Soft deletes** — Results marked deleted=1 (preserve history, don't lose tracking)
668
-
669
- ---
670
-
671
254
  ### Data Access Layer (SQLite + Filesystem + Git)
672
255
  **Components:** SQLite via bun:sqlite, direct filesystem access, simple-git wrapper
673
256
 
@@ -683,7 +266,7 @@ ppm jira track <issue-key> — Manually track ticket (insert res
683
266
  - Path validation: `projectPath/relativePath` only, reject `..`
684
267
  - Caching: Directory trees cached with TTL
685
268
  - Error handling: Descriptive messages (file not found, permission denied)
686
- - Migration: Automatic YAML→SQLite migration on first run with new db.service; schema auto-upgrade on version bump
269
+ - Migration: schema auto-upgrades on version bump (`CURRENT_SCHEMA_VERSION` in `db.service.ts`). The historical YAML config path is gone — `js-yaml` survives only to parse skill frontmatter in `slash-discovery/skill-loader.ts`
687
270
 
688
271
  ---
689
272
 
@@ -776,75 +359,10 @@ HTTP/1.1 200 OK
776
359
 
777
360
  ---
778
361
 
779
- ## Project Workspace Management
780
-
781
- ### Keep-Alive Pattern (v2.0+)
782
- When switching projects, workspaces are preserved instead of destroyed:
783
- 1. **Workspace Mount State**: Each project's UI (tabs, terminal xterm DOM, file selections) remains mounted in the DOM
784
- 2. **Visibility Toggle**: CSS `display: none/block` hides/shows workspaces instead of React unmounting
785
- 3. **Terminal DOM Persistence**: xterm.js terminal instances retain their DOM structure across switches (prevents re-render flicker)
786
- 4. **Cache Efficiency**: Zustand stores persist open tabs, selections, and scroll positions per project
787
-
788
- **Benefits:**
789
- - Instant project switching (no DOM reconstruction)
790
- - Terminal history preserved across switches
791
- - Smooth UX without flashing/re-rendering
792
- - Reduced network requests (cached UI state)
793
-
794
- ### Project Color, Avatar & Ordering (v2.0+)
795
- **Storage**:
796
- - Colors stored as optional `color` field in `Project` interface (hex string or undefined)
797
- - Custom avatar images stored content-addressed at `~/.ppm/avatars/<sha256>.webp` via AvatarStorageService
798
-
799
- **Endpoints:**
800
- - `PATCH /api/projects/:name/color` — Update project color
801
- - `POST /api/projects/:name/image` — Upload avatar (multipart, 2MB cap, client resizes to 128×128 webp 0.85)
802
- - `GET /api/projects/:name/image` — Stream avatar (immutable cache headers, path-traversal safe)
803
- - `DELETE /api/projects/:name/image` — Remove avatar (reverts to color+initials)
804
- - `PATCH /api/projects/reorder` — Reorder projects array in config
805
-
806
- **UI Components:**
807
- - `ProjectBar` (52px sidebar) — Shows project avatars (custom image or color+initials), context menu for reorder/rename/delete/color-picker/change-image
808
- - `ProjectBottomSheet` (mobile) — Bottom sheet switcher with long-press menu ("Change Image"/"Remove Image")
809
- - `ProjectAvatar` component — Renders `<img>` with token auth + cache-bust query param, fallback to color+initials on missing/error
810
- - `PROJECT_PALETTE` — 12-color palette for default colors when not customized
811
-
812
- **Avatar Upload & Caching:**
813
- - Client: canvas center-crop to 128×128, export as webp 0.85 quality (via `resize-image.ts`)
814
- - Validation: MIME type (image/png, image/jpeg, image/webp, image/avif), file size ≤10MB
815
- - Server: SHA256 hash for deduplication (same image across projects = same file)
816
- - Cleanup: Deleted on project remove, preserved across rename (updated in project metadata)
817
- - Auth: Token in URL query param allows `<img>` to load when auth enabled
818
- - Cache: Immutable headers (max-age=31536000) + `?v=hash` cache-bust for updates
819
-
820
- ---
821
-
822
- ## Code Editor Migration (v2.0+)
823
-
824
- **Migration**: CodeMirror 6 → Monaco Editor (@monaco-editor/react)
825
-
826
- **Reasons:**
827
- - Better syntax highlighting for complex languages
828
- - Superior IntelliSense and code completion
829
- - Performance improvements on large files
830
- - More polished diff viewer experience
831
-
832
- **Components Updated:**
833
- - `src/web/components/editor/code-editor.tsx` — Monaco Editor with language detection
834
- - `src/web/components/editor/diff-viewer.tsx` — Monaco diff viewer for git diffs
835
-
836
- **Features:**
837
- - Alt+Z toggle for word wrap
838
- - Automatic language detection from file extension
839
- - Theme sync with app dark/light mode
840
- - Responsive layout with proper scrolling
841
-
842
- ---
843
-
844
362
  ## Authentication Flow
845
363
 
846
364
  ```
847
- User opens http://localhost:8080
365
+ User opens http://localhost:3210
848
366
 
849
367
  App checks localStorage for auth token
850
368
 
@@ -864,1165 +382,21 @@ For each API request:
864
382
  ```
865
383
 
866
384
  **Token Management:**
867
- - Generated on `ppm init` → stored in `ppm.yaml`
868
- - Sent from CLI via `-c <config>` flag
385
+ - Generated on `ppm init` → stored in `~/.ppm/ppm.db` (`auth.token`)
386
+ - Required on the WebSocket handshake too, as `?token=` (browsers cannot set handshake headers)
869
387
  - Stored in browser localStorage for session persistence
870
388
  - No expiry (single-user, local environment)
871
389
 
872
390
  ---
873
391
 
874
- ## AI Provider Configuration
875
-
876
- PPM exposes AI settings as global configuration (not per-session) via REST API and Settings UI. Configuration is stored in `ppm.yaml` and read fresh per query.
877
-
878
- ### Configuration Shape
879
- ```yaml
880
- ai:
881
- default_provider: claude
882
- providers:
883
- claude:
884
- type: agent-sdk
885
- api_key_env: ANTHROPIC_API_KEY
886
- model: claude-opus-4-8
887
- effort: high
888
- max_turns: 100
889
- max_budget_usd: 2.00
890
- thinking_budget_tokens: 10000
891
- ```
892
-
893
- **Fields:**
894
- - `default_provider`: Active provider name (e.g., `claude`)
895
- - `type`: Provider type (`agent-sdk` or `mock`)
896
- - `api_key_env`: Environment variable containing API key
897
- - `model`: Model ID (e.g., `claude-fable-5`, `claude-opus-4-8`, `claude-sonnet-4-6`). Default: `claude-opus-4-8`
898
- - `effort`: Processing level (`low`, `medium`, `high`, `max`)
899
- - `max_turns`: Maximum interaction turns (1-500, default 100)
900
- - `max_budget_usd`: Spending limit in USD (optional)
901
- - `thinking_budget_tokens`: Extended thinking, tri-state (optional). Omitted = adaptive (model picks depth, guided by `effort`); `0` = disabled; a positive number = fixed token budget. Per-session Thinking toggle overrides this.
902
-
903
- ### API Endpoints
904
-
905
- **GET /api/settings/ai** — Fetch current AI config
906
- ```json
907
- {
908
- "ok": true,
909
- "data": {
910
- "default_provider": "claude",
911
- "providers": { "claude": {...} }
912
- }
913
- }
914
- ```
915
-
916
- **PUT /api/settings/ai** — Update AI config (shallow merge per provider)
917
- ```json
918
- {
919
- "providers": {
920
- "claude": {
921
- "model": "claude-opus-4-6",
922
- "max_turns": 50
923
- }
924
- }
925
- }
926
- ```
927
- Returns full updated config. Validates ranges/enums before writing.
928
-
929
- ### How Provider Uses Settings
930
-
931
- 1. **SDK Provider (`sendMessage`)**
932
- - Calls `getProviderConfig()` to read fresh config from `configService`
933
- - Maps snake_case config to camelCase SDK options
934
- - Passes `model`, `effort`, `maxTurns`, `maxBudgetUsd`, `thinkingBudgetTokens` to `query()`
935
- - Falls back to defaults if fields not set
936
-
937
- 2. **Mock Provider**
938
- - Ignores AI settings (always returns canned responses for testing)
939
-
940
- 3. **Changes Take Effect**
941
- - Immediately on next query (config read fresh each time)
942
- - No active queries affected (config mid-flight not re-evaluated)
943
-
944
- ---
945
-
946
- ## Chat Streaming Flow (Persistent AsyncGenerator Sessions)
947
-
948
- ### Architecture Overview (v0.8.55+)
949
-
950
- PPM uses a **persistent streaming session** model instead of per-message query execution:
951
-
952
- **Key Changes:**
953
- - Provider maintains **long-lived AsyncGenerator streaming input** per chat session (not per message)
954
- - Follow-up messages **push into the existing generator** instead of abort-and-replace
955
- - **Single streaming loop** per session decoupled from WebSocket message handler
956
- - Message priority support: `now` (interrupt current), `next` (queue first), `later` (queue at end)
957
- - Supports image attachments in messages
958
-
959
- **Design Benefits:**
960
- - Continuous context preservation — multi-turn conversations flow naturally
961
- - No SDK subprocess restarts between messages (faster)
962
- - Clean separation: BE owns Claude connection, FE disconnect doesn't abort
963
- - Message buffering on reconnect — clients that lose WS connection sync turn events
964
- - Tool approvals don't restart the query — integrated into streaming loop
965
-
966
- ### Message Flow
967
-
968
- ```
969
- User types: "Debug this function"
970
-
971
- MessageInput.tsx calls useChat.sendMessage()
972
-
973
- useChat opens WebSocket: WS /ws/project/:name/chat/:sessionId
974
-
975
- Sends: { type: "message", content: "Debug...", priority?: "now"|"next"|"later" }
976
-
977
- WS handler in chat.ts receives message
978
-
979
- If already streaming with different content → abort previous + wait cleanup
980
- If streaming, new message priority determines queue behavior:
981
- • priority: "now" → abort current, restart with new content
982
- • priority: "next" → push into pending queue (higher priority)
983
- • priority: "later" → push to end of queue (FIFO)
984
-
985
- runStreamLoop() executes in detached async context
986
-
987
- ChatService calls provider.sendMessage() (async generator)
988
-
989
- Provider (Claude SDK) yields events:
990
- 1. { type: "text", content: "Here's what..." }
991
- 2. { type: "text", content: " happens..." }
992
- 3. { type: "tool_use", tool: "read_file", input: {...} }
993
-
994
- Stream loop buffers + broadcasts to all connected clients:
995
- { type: "text", content: "Here's what..." }
996
- { type: "text", content: " happens..." }
997
- { type: "tool_use", tool: "read_file", input: {...} }
998
- { type: "approval_request", requestId, tool, input }
999
-
1000
- Client receives, displays message incrementally
1001
-
1002
- User sees tool approval prompt, clicks "Approve"
1003
-
1004
- Client sends: { type: "approval_response", requestId, approved: true }
1005
-
1006
- Provider continues streaming with tool result (no restart)
1007
-
1008
- If multiple messages queued, next message processes after done event
1009
-
1010
- Final response streamed, then: { type: "done", sessionId }
1011
-
1012
- Phase transitions to idle, clients can send new message
1013
-
1014
- useChat saves message to store, displays in chat history
1015
- ```
1016
-
1017
- ### Session State Management
1018
-
1019
- **Session Entry** (BE-owned, persists across FE disconnections):
1020
- ```typescript
1021
- interface SessionEntry {
1022
- providerId: string; // Which AI provider (e.g., "claude")
1023
- clients: Set<ChatWsSocket>; // Connected FE clients (may be empty)
1024
- abort?: AbortController; // Current stream abort handle
1025
- projectPath?: string; // Project context
1026
- projectName?: string;
1027
- pingIntervals: Map<...>; // Per-client keepalive
1028
- phase: SessionPhase; // "initializing" | "connecting" | "thinking" | "streaming" | "idle"
1029
- cleanupTimer?: ReturnType<...>; // Auto-cleanup if no FE reconnects (5min)
1030
- pendingApprovalEvent?: {...}; // Current tool approval waiting
1031
- turnEvents: unknown[]; // Buffered events (for reconnect sync)
1032
- streamPromise?: Promise<void>; // Track ongoing runStreamLoop
1033
- permissionMode?: string; // Sticky permission mode for session
1034
- }
1035
- ```
1036
-
1037
- **Client Connection States:**
1038
- - **Active streaming + FE connected** → Events broadcast to all clients in real-time
1039
- - **Active streaming + FE disconnected** → Events buffered in turnEvents array, BE stream continues
1040
- - **FE reconnects** → Receive session_state + buffered turnEvents, resync with stream
1041
- - **Idle (no query running)** → Phase is "idle", ready for next message
1042
- - **Idle + no FE for 5min** → Cleanup timer removes session from memory
1043
-
1044
- ### Follow-up Messages
1045
-
1046
- **Abort-and-Replace Pattern:**
1047
- ```typescript
1048
- if (entry.phase !== "idle" && entry.abort) {
1049
- console.log(`[chat] aborting current query for new message`);
1050
- entry.abort.abort();
1051
- await entry.streamPromise; // Wait for cleanup
1052
- // Re-fetch entry — may have been mutated during cleanup
1053
- entry = activeSessions.get(sessionId)!;
1054
- }
1055
- ```
1056
-
1057
- **Multiple Message Queueing:**
1058
- - First message: immediately starts runStreamLoop
1059
- - Second message (while streaming): abort current, wait, start new runStreamLoop
1060
- - Priority modes (future): could queue messages for intelligent interleaving
1061
-
1062
- ### WebSocket Reconnection Sync
1063
-
1064
- ```
1065
- FE WebSocket closes (network issue, tab closes)
1066
-
1067
- BE keeps session alive, streaming continues
1068
-
1069
- FE reconnects: WS /ws/project/:name/chat/:sessionId
1070
-
1071
- open() handler checks activeSessions.get(sessionId)
1072
-
1073
- If exists (entry found):
1074
- 1. Clear cleanup timer (FE is back)
1075
- 2. Send session_state with current phase + pendingApproval
1076
- 3. If phase !== "idle", send buffered turnEvents
1077
- 4. Add WS to clients Set
1078
-
1079
- FE processes session_state, renders current phase
1080
-
1081
- FE applies buffered events to rebuild turn state
1082
-
1083
- FE displays: "reconnected, current phase: streaming" etc.
1084
- ```
1085
-
1086
- ### Phase Transitions
1087
-
1088
- ```
1089
- idle → initializing → connecting → thinking/streaming ↔ thinking/streaming → idle
1090
- ^ ↑ ↓
1091
- └──────────────────────────────────────────────────────────────────────────┘
1092
- ```
1093
-
1094
- **Phase Descriptions:**
1095
- - **idle** — No query running, ready to accept new message
1096
- - **initializing** — Preparing (permission checks, session resume)
1097
- - **connecting** — Waiting for first SDK event (heartbeat: "connecting" with elapsed time every 5s)
1098
- - **thinking** — Receiving thinking content (extended thinking)
1099
- - **streaming** — Receiving text/tool_use content (dynamic switch between thinking/streaming)
1100
-
1101
- ### Image Attachment Support
1102
-
1103
- Messages can now include images:
1104
- ```typescript
1105
- type ChatWsClientMessage =
1106
- | { type: "message"; content: string; images?: { id: string; data: string }[]; priority?: string }
1107
- | ...
1108
- ```
1109
-
1110
- Images are passed to provider's message context and included in tool input/output.
1111
-
1112
- ---
1113
-
1114
- ## Terminal Flow
1115
-
1116
- ```
1117
- User clicks Terminal tab
1118
-
1119
- TerminalTab.tsx mounts
1120
-
1121
- useTerminal hook opens WebSocket: WS /ws/project/:name/terminal/:id
1122
-
1123
- TerminalService.spawn() creates PTY (Bun.spawn)
1124
-
1125
- xterm.js renders terminal emulator
1126
-
1127
- User types: "npm test"
1128
-
1129
- xterm.js captures key event
1130
-
1131
- Sends via WebSocket: { type: "input", data: "npm test\n" }
1132
-
1133
- TerminalService.write(pty, "npm test\n")
1134
-
1135
- npm process spawned inside PTY
1136
-
1137
- Output captured: "PASS: all tests\n"
1138
-
1139
- TerminalService sends: { type: "output", data: "PASS: all tests\n" }
1140
-
1141
- xterm.js renders output
1142
-
1143
- User resizes window → xterm.js resizes terminal
1144
-
1145
- Sends: { type: "resize", cols: 120, rows: 40 }
1146
-
1147
- TerminalService calls pty.resize()
1148
-
1149
- Shell (bash/zsh) receives SIGWINCH signal
1150
-
1151
- Terminal state updated
1152
- ```
1153
-
1154
- ---
1155
-
1156
- ## Git Integration Flow
1157
-
1158
- ```
1159
- User right-clicks file in FileTree
1160
-
1161
- Context menu shows "Stage" option
1162
-
1163
- User clicks "Stage"
1164
-
1165
- FileActions.tsx calls POST /api/project/:name/git/stage
1166
-
1167
- Sends: { path: "src/index.ts" }
1168
-
1169
- GitService.stage(projectPath, "src/index.ts")
1170
-
1171
- Executes: git add src/index.ts (via simple-git)
1172
-
1173
- Returns: { ok: true }
1174
-
1175
- GitStatusPanel.tsx refreshes: GET /api/project/:name/git/status
1176
-
1177
- GitService.status() returns:
1178
- {
1179
- current: "main",
1180
- staged: ["src/index.ts"],
1181
- unstaged: ["README.md"],
1182
- untracked: ["temp.log"]
1183
- }
1184
-
1185
- UI updates: "src/index.ts" moves from "Unstaged" to "Staged"
1186
- ```
1187
-
1188
- ---
1189
-
1190
- ## Database Management (v2.0+)
1191
-
1192
- ### Architecture Overview
1193
-
1194
- PPM now supports managing external databases (SQLite & PostgreSQL) through a unified adapter pattern:
1195
-
1196
- ```
1197
- ┌─────────────────────────────────────────────────────────────────┐
1198
- │ Web UI (React) │
1199
- │ ┌──────────────────────────────────────────────────────────┐ │
1200
- │ │ Database Sidebar │ │
1201
- │ │ • Connection List (with color badges) │ │
1202
- │ │ • Create/Edit Connection Form │ │
1203
- │ │ • Color Picker (WCAG contrast-aware) │ │
1204
- │ │ • Query Execution UI │ │
1205
- │ └──────────────────────────────────────────────────────────┘ │
1206
- └─────────────────┬───────────────────────────────────────────────┘
1207
- │ HTTP REST / WebSocket
1208
- ┌─────────────────┴───────────────────────────────────────────────┐
1209
- │ PPM Server (Hono) │
1210
- │ ┌──────────────────────────────────────────────────────────┐ │
1211
- │ │ /api/db Routes │ │
1212
- │ │ • GET /connections → List all connections │ │
1213
- │ │ • POST /connections → Create connection │ │
1214
- │ │ • GET /connections/:id → Get connection (sanitized) │ │
1215
- │ │ • PUT /connections/:id → Update (readonly toggle) │ │
1216
- │ │ • DELETE /connections/:id → Remove connection │ │
1217
- │ │ • GET /connections/:id/tables → List + sync tables │ │
1218
- │ │ • GET /connections/:id/tables/:tbl → Schema + data │ │
1219
- │ │ • POST /connections/:id/query → Execute query │ │
1220
- │ │ • PATCH /connections/:id/cell → Update cell │ │
1221
- │ └──────────────────────────────────────────────────────────┘ │
1222
- │ ┌──────────────────────────────────────────────────────────┐ │
1223
- │ │ Service Layer │ │
1224
- │ │ • DbService (connection CRUD, caching) │ │
1225
- │ │ • TableCacheService (metadata cache, search) │ │
1226
- │ │ • DatabaseAdapterRegistry (extensible) │ │
1227
- │ └──────────────────────────────────────────────────────────┘ │
1228
- │ ┌──────────────────────────────────────────────────────────┐ │
1229
- │ │ Adapters (Pluggable Pattern) │ │
1230
- │ │ • SQLiteAdapter → Uses `bun:sqlite` for local files │ │
1231
- │ │ • PostgresAdapter → Uses postgres driver for servers │ │
1232
- │ │ • isReadOnlyQuery() → Safety check (CTE-safe regex) │ │
1233
- │ │ • readonly=1 by default (safe-by-default) │ │
1234
- │ └──────────────────────────────────────────────────────────┘ │
1235
- └──────────────────────────────────────────────────────────────────┘
1236
- ↓↑
1237
- ┌────────────────────────────────────────────┐
1238
- │ External Databases │
1239
- │ • SQLite files (path: /path/to/db.db) │
1240
- │ • PostgreSQL servers (connStr: postgres://)│
1241
- └────────────────────────────────────────────┘
1242
- ```
1243
-
1244
- ### DatabaseAdapter Pattern (Extensible)
1245
-
1246
- **Interface** (`src/types/database.ts`):
1247
- ```typescript
1248
- interface DatabaseAdapter {
1249
- testConnection(config: DbConnectionConfig): Promise<{ ok: boolean; error?: string }>;
1250
- getTables(config: DbConnectionConfig): Promise<DbTableInfo[]>;
1251
- getTableSchema(config: DbConnectionConfig, table: string, schema?: string): Promise<DbColumnInfo[]>;
1252
- getTableData(config: DbConnectionConfig, table: string, opts: {...}): Promise<DbPagedData>;
1253
- executeQuery(config: DbConnectionConfig, sql: string): Promise<DbQueryResult>;
1254
- updateCell(config: DbConnectionConfig, table: string, opts: {...}): Promise<void>;
1255
- }
1256
- ```
1257
-
1258
- **Implementations:**
1259
- 1. **SQLiteAdapter** — Local file-based SQLite via `bun:sqlite`
1260
- - testConnection: Opens file, runs pragma check
1261
- - Supports: SELECT, INSERT, UPDATE, DELETE (if writable), CREATE TABLE
1262
-
1263
- 2. **PostgresAdapter** — Remote PostgreSQL servers via postgres driver
1264
- - testConnection: Attempts connection with credentials
1265
- - Supports: Full SQL except DDL on readonly connections
1266
-
1267
- **Registry Pattern** (`src/services/database/adapter-registry.ts`):
1268
- ```typescript
1269
- registerAdapter("sqlite", new SQLiteAdapter());
1270
- registerAdapter("postgres", new PostgresAdapter());
1271
- // Can be extended: registerAdapter("mysql", new MysqlAdapter());
1272
- ```
1273
-
1274
- ### Security Design
1275
-
1276
- **Readonly by Default:**
1277
- - All connections created with `readonly = true` in database
1278
- - Default: read-only query execution (safe-by-default)
1279
- - Web UI toggle: Switch to writable (admin decision only)
1280
- - CLI: Cannot disable readonly via command-line (browser only)
1281
-
1282
- **Readonly Query Detection:**
1283
- ```typescript
1284
- // isReadOnlyQuery() in src/services/database/readonly-check.ts
1285
- // Checks for: SELECT, PRAGMA, EXPLAIN, WITH (CTE)
1286
- // Rejects: INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, etc.
1287
- // CTE-safe: Handles "WITH AS SELECT" (wraps CTE result check)
1288
- ```
1289
-
1290
- **Credential Handling:**
1291
- - Connection credentials stored in SQLite `connections` table as `connection_config` JSON
1292
- - **NEVER** returned in API responses (stripped by `sanitizeConn()` in routes)
1293
- - Only used internally by adapters when executing queries
1294
- - Frontend never sees passwords/connection strings
1295
-
1296
- **API Security:**
1297
- - All `/api/db` requests require valid auth token (middleware checked)
1298
- - Connection IDs are numeric (no enumeration risk)
1299
- - Connection color is user-specific (cosmetic only, not sensitive)
1300
-
1301
- ### Data Flow: Query Execution
1302
-
1303
- ```
1304
- User opens Database tab
1305
-
1306
- DatabaseSidebar fetches: GET /api/db/connections
1307
-
1308
- ConnectionList displays (sanitized, no credentials)
1309
-
1310
- User clicks connection → GET /api/db/connections/:id/tables
1311
-
1312
- DbService.getConnections() reads from SQLite
1313
-
1314
- TableCacheService.syncTables() calls adapter.getTables()
1315
-
1316
- SQLiteAdapter/PostgresAdapter queries database
1317
-
1318
- Results cached in table_metadata table
1319
-
1320
- UI displays table list + schema
1321
-
1322
- User selects table → GET /api/db/connections/:id/tables/:table
1323
-
1324
- Adapter.getTableData() executes paginated query
1325
-
1326
- Results returned: { columns, rows, total, page, limit }
1327
-
1328
- UI renders table grid with pagination
1329
-
1330
- User executes custom query → POST /api/db/connections/:id/query
1331
-
1332
- isReadOnlyQuery() checks SQL (rejects writes if readonly=true)
1333
-
1334
- Adapter.executeQuery() runs SQL
1335
-
1336
- Results returned: { columns, rows, rowsAffected, changeType }
1337
-
1338
- UI displays results (read-only highlight if mutation was blocked)
1339
- ```
1340
-
1341
- ### Connection Storage
1342
-
1343
- **SQLite Schema** (in `~/.ppm/ppm.db`):
1344
- ```sql
1345
- CREATE TABLE accounts (
1346
- id TEXT PRIMARY KEY,
1347
- account_name TEXT NOT NULL,
1348
- encrypted_api_key TEXT NOT NULL,
1349
- priority INTEGER DEFAULT 0,
1350
- is_active INTEGER DEFAULT 0, -- 1 = active, 0 = inactive
1351
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1352
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
1353
- );
1354
-
1355
- CREATE TABLE connections (
1356
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1357
- type TEXT NOT NULL, -- 'sqlite' | 'postgres'
1358
- name TEXT NOT NULL,
1359
- connection_config TEXT NOT NULL, -- JSON: { path, connectionString, ... }
1360
- readonly INTEGER DEFAULT 1, -- 1 = readonly, 0 = writable (UI-only toggle)
1361
- group_name TEXT,
1362
- color TEXT, -- Optional hex color (#3b82f6)
1363
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1364
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
1365
- );
1366
-
1367
- CREATE TABLE table_metadata (
1368
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1369
- connection_id INTEGER NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
1370
- table_name TEXT NOT NULL,
1371
- schema_name TEXT DEFAULT 'public',
1372
- row_count INTEGER,
1373
- last_synced TEXT,
1374
- UNIQUE(connection_id, table_name, schema_name)
1375
- );
1376
- ```
1377
-
1378
- ### CLI Support (ppm db)
1379
-
1380
- **Commands** (`src/cli/commands/db-cmd.ts`):
1381
- ```bash
1382
- ppm db connections # List all connections
1383
- ppm db connect # Add new connection (interactive)
1384
- ppm db remove <name> # Delete connection
1385
- ppm db query <name> <sql> # Execute query (respects readonly)
1386
- ppm db run <name> <file> # Execute SQL file (multi-statement, transactions)
1387
- ppm db tables <name> # List tables
1388
- ppm db schema <name> <table> # Show table schema
1389
- ppm db data <name> <table> # Show table data (paginated)
1390
- ```
1391
-
1392
- **CLI Safety:**
1393
- - Always respects readonly flag (cannot override via CLI)
1394
- - Uses same adapter/validation as web UI
1395
- - Table formatting for terminal output
1396
-
1397
- ---
1398
-
1399
- ## MCP Server Management
1400
-
1401
- ### Overview
1402
- MCP (Model Context Protocol) servers extend Claude with custom tools and resources. PPM manages MCP server configurations via Settings UI, storing them in SQLite and passing them to the Claude Agent SDK.
1403
-
1404
- **Features:**
1405
- - **Add/Edit/Delete** MCP servers via Settings UI
1406
- - **Auto-import** from `~/.claude.json` on first access (convenience, no forced import)
1407
- - **Three transport types:** stdio, HTTP, SSE
1408
- - **Validation** on name and config before storage
1409
- - **SDK integration:** Servers passed to `query()` as `mcpServers` object, tools auto-allowed via `mcp__*` wildcard
1410
-
1411
- ### Storage Schema
1412
-
1413
- ```sql
1414
- CREATE TABLE mcp_servers (
1415
- name TEXT PRIMARY KEY,
1416
- transport TEXT NOT NULL DEFAULT 'stdio', -- 'stdio' | 'http' | 'sse'
1417
- config TEXT NOT NULL, -- JSON: McpServerConfig
1418
- created_at TEXT DEFAULT (datetime('now')),
1419
- updated_at TEXT DEFAULT (datetime('now'))
1420
- );
1421
- ```
1422
-
1423
- **Config Format (JSON):**
1424
- ```json
1425
- {
1426
- "type": "stdio",
1427
- "command": "path/to/server",
1428
- "args": ["--flag"],
1429
- "env": { "VAR": "value" }
1430
- }
1431
- ```
1432
-
1433
- Or HTTP/SSE:
1434
- ```json
1435
- {
1436
- "type": "http",
1437
- "url": "http://localhost:3000",
1438
- "headers": { "Authorization": "Bearer token" }
1439
- }
1440
- ```
1441
-
1442
- ### REST API
1443
-
1444
- **Endpoints** (`src/server/routes/mcp.ts`):
1445
-
1446
- | Method | Endpoint | Description |
1447
- |--------|----------|-------------|
1448
- | **GET** | `/api/settings/mcp` | List all servers; auto-import on first access |
1449
- | **GET** | `/api/settings/mcp/:name` | Get single server config |
1450
- | **POST** | `/api/settings/mcp` | Add new server (validates name + config) |
1451
- | **PUT** | `/api/settings/mcp/:name` | Update existing server |
1452
- | **DELETE** | `/api/settings/mcp/:name` | Remove server |
1453
- | **GET** | `/api/settings/mcp/import/preview` | Preview servers in `~/.claude.json` |
1454
- | **POST** | `/api/settings/mcp/import` | Bulk import from `~/.claude.json` |
1455
-
1456
- **Add Server Example:**
1457
- ```bash
1458
- POST /api/settings/mcp
1459
- Content-Type: application/json
1460
-
1461
- {
1462
- "name": "file-server",
1463
- "config": {
1464
- "type": "stdio",
1465
- "command": "/usr/local/bin/file-server",
1466
- "args": ["--port", "8000"]
1467
- }
1468
- }
1469
- ```
1470
-
1471
- ### Service Layer
1472
-
1473
- **McpConfigService** (`src/services/mcp-config.service.ts`):
1474
- - `list()` — Record<name, McpServerConfig> (SDK-compatible format)
1475
- - `listWithMeta()` — Array with metadata (for UI)
1476
- - `get(name)` — Single server config
1477
- - `set(name, config)` — Add or update (upsert)
1478
- - `remove(name)` — Delete server
1479
- - `exists(name)` — Check if name exists
1480
- - `bulkImport(servers)` — Transactional import from `~/.claude.json`, skips existing/invalid
1481
-
1482
- **Validation:**
1483
- - `validateMcpName(name)` — alphanumeric + hyphens/underscores, max 50 chars
1484
- - `validateMcpConfig(config)` — type-specific checks (command for stdio, url for http/sse)
1485
-
1486
- ### Frontend Integration
1487
-
1488
- **UI Components:**
1489
- - `MCP Settings Section` (`src/web/components/settings/mcp-settings-section.tsx`) — Tab in Settings UI
1490
- - `MCP Server Dialog` (`src/web/components/settings/mcp-server-dialog.tsx`) — Add/Edit modal
1491
- - `API client` (`src/web/lib/api-mcp.ts`) — Fetch/mutate operations
1492
-
1493
- **Workflow:**
1494
- 1. User opens Settings → MCP tab
1495
- 2. **GET** `/api/settings/mcp` (auto-imports on first access)
1496
- 3. Display list with transport badge + actions (edit, delete)
1497
- 4. Click "Add" → Dialog with name + transport selector + config fields
1498
- 5. **POST** to `/api/settings/mcp` or **PUT** to update
1499
- 6. On success, list refreshes
1500
-
1501
- ### SDK Integration
1502
-
1503
- **Claude Agent SDK Provider** (`src/providers/claude-agent-sdk.ts`):
1504
- ```typescript
1505
- // Line ~574
1506
- const mcpServers = mcpConfigService.list();
1507
- const hasMcp = Object.keys(mcpServers).length > 0;
1508
-
1509
- // Line ~589: Pass to query() if servers exist
1510
- const mcpTools = ["mcp__*"];
1511
- const queryConfig = {
1512
- // ... other options
1513
- ...(hasMcp && { mcpServers }),
1514
- allowedTools: [...otherTools, ...mcpTools],
1515
- };
1516
-
1517
- const query = new Query(messages, queryConfig);
1518
- ```
1519
-
1520
- ---
1521
-
1522
- ## Extension System (v0.9.0+)
1523
-
1524
- ### Overview
1525
-
1526
- PPM Extension System enables VSCode-compatible, npm-installable extensions that run in isolated Bun Worker threads. Crash-safe, permission-based, with RPC messaging between main process and worker, and WebSocket bridge for real-time UI updates.
1527
-
1528
- **Architecture (3-tier):**
1529
- ```
1530
- Extension Code (Bun Worker) ← @ppm/vscode-compat API
1531
- │ RPC (postMessage)
1532
-
1533
- Main Process (Hono/Bun) ← extension-rpc-handlers.ts
1534
- │ WebSocket (/ws/extensions)
1535
-
1536
- Browser (React) ← Zustand store + React components
1537
- ```
1538
-
1539
- **Key components:**
1540
- - **Package Format:** npm packages (`@ppm/ext-database`, `@ppm/ext-git-graph`, `@ppm/ext-docker`, etc.)
1541
- - **Installation:** `~/.ppm/extensions/node_modules/{id}/`
1542
- - **Lifecycle:** Install → Enable → Activate → Deactivate → Remove
1543
- - **Worker Isolation:** Each activated extension runs in a Bun Worker (crash-safe, 10s activation timeout)
1544
- - **Communication:** RPC (Worker↔Main) + WebSocket (Main↔Browser)
1545
- - **API Shim:** `@ppm/vscode-compat` — VSCode-compatible API (commands, window, workspace)
1546
- - **Subprocess Access:** RPC `process:spawn` handler for extensions needing CLI commands (git, docker, npm, python, etc.)
1547
- - **State Storage:** globalState + workspaceState in SQLite via Memento
1548
- - **UI Bridge:** StatusBar, TreeView, WebviewPanel, QuickPick, InputBox, Notifications
1549
- - **Contributions:** Commands, views, configuration contributed via manifest
1550
-
1551
- **Official Extensions:**
1552
- - `@ppm/ext-database` — Database browser with SQLite/PostgreSQL support (tree view + query panel)
1553
- - `@ppm/ext-git-graph` — Git commit graph visualization (faithful vscode-git-graph SVG algorithm with Bézier curves, uses process:spawn for git CLI across registered projects)
1554
-
1555
- ### Manifest Format
1556
-
1557
- Extension metadata defined in `package.json` under `ppm` key:
1558
-
1559
- ```json
1560
- {
1561
- "name": "@ppm/ext-database",
1562
- "version": "1.0.0",
1563
- "main": "dist/extension.js",
1564
- "ppm": {
1565
- "displayName": "Database Browser",
1566
- "description": "Browse and query databases",
1567
- "icon": "database.svg",
1568
- "engines": { "ppm": ">=0.9.0" },
1569
- "activationEvents": ["onView:databases"],
1570
- "contributes": {
1571
- "commands": [
1572
- {
1573
- "command": "ppm.database.openConnection",
1574
- "title": "Open Database Connection",
1575
- "category": "Database"
1576
- }
1577
- ],
1578
- "views": {
1579
- "explorer": [
1580
- {
1581
- "id": "databases",
1582
- "name": "Databases",
1583
- "type": "tree"
1584
- }
1585
- ]
1586
- },
1587
- "configuration": {
1588
- "properties": {
1589
- "ppm.database.maxRows": {
1590
- "type": "number",
1591
- "default": 1000,
1592
- "description": "Max rows to fetch per query"
1593
- }
1594
- }
1595
- }
1596
- }
1597
- }
1598
- }
1599
- ```
1600
-
1601
- **Fields:**
1602
- - `engines.ppm` — PPM version requirement
1603
- - `activationEvents` — When extension activates (e.g., `onView:databases`, `onCommand:ext.activate`)
1604
- - `contributes` — UI elements + commands contributed by extension
1605
-
1606
- ### Installation & Lifecycle
1607
-
1608
- **Installation** (`ppm ext install @ppm/ext-database`):
1609
- 1. Fetch package from npm
1610
- 2. Extract to `~/.ppm/extensions/node_modules/{id}/`
1611
- 3. Parse manifest from `package.json`
1612
- 4. Store in SQLite `extensions` table (enabled=1)
1613
- 5. Discover contributions
1614
-
1615
- **Activation** (`ppm ext enable @ppm/ext-database` or automatic):
1616
- 1. Load manifest + entry point from disk
1617
- 2. Spawn Bun Worker (process isolation)
1618
- 3. Create scoped `@ppm/vscode-compat` API instance (RPC-backed)
1619
- 4. Call `activate(context, vscodeApi)` with 10s timeout
1620
- 5. Register contributions in `contributionRegistry`
1621
- 6. Broadcast `contributions:update` via WS to all connected browsers
1622
- 7. Mark as activated
1623
-
1624
- **Deactivation:**
1625
- 1. Unregister contributions
1626
- 2. Terminate worker
1627
- 3. Clear persisted state if needed
1628
-
1629
- **Removal** (`ppm ext remove @ppm/ext-database`):
1630
- 1. Deactivate if active
1631
- 2. Delete from `~/.ppm/extensions/`
1632
- 3. Remove from SQLite
1633
- 4. Unregister contributions
1634
-
1635
- ### RPC Protocol (Extension ↔ Main Process)
1636
-
1637
- **Message Types:**
1638
-
1639
- 1. **Request** (extension → main)
1640
- ```json
1641
- {
1642
- "type": "request",
1643
- "id": 1,
1644
- "method": "storage:get",
1645
- "params": ["extId", "global", "key"]
1646
- }
1647
- ```
1648
-
1649
- 2. **Response** (main → extension)
1650
- ```json
1651
- {
1652
- "type": "response",
1653
- "id": 1,
1654
- "result": "value"
1655
- }
1656
- ```
1657
-
1658
- 3. **Event** (both directions)
1659
- ```json
1660
- {
1661
- "type": "event",
1662
- "event": "file:changed",
1663
- "data": { "path": "/path/to/file" }
1664
- }
1665
- ```
1666
-
1667
- **Built-in Methods (vscode-compat API):**
1668
- - `commands:execute(command, ...args)` — Execute command
1669
- - `commands:list(filterInternal)` — List available commands
1670
- - `window:showMessage(level, message, items[])` — Show dialog with buttons
1671
- - `window:showQuickPick(items[], options)` — Quick pick menu
1672
- - `window:showInputBox(options)` — Text input dialog
1673
- - `window:webview:create(panelId, extensionId, viewType, title)` — Create webview panel
1674
- - `window:webview:html(panelId, html)` — Set webview content
1675
- - `window:webview:postMessage(panelId, message)` — Send message to webview
1676
- - `window:tree:update(viewId, items[])` — Update tree view items
1677
- - `window:tree:refresh(viewId)` — Refresh tree view
1678
- - `window:statusbar:update(item)` — Update/create status bar item
1679
- - `window:statusbar:remove(itemId)` — Remove status bar item
1680
- - `workspace:config:get(key)` — Read config value
1681
- - `workspace:config:update(key, value, target)` — Write config value
1682
- - `workspace:fs:readFile(filePath)` — Read file (base64 encoded)
1683
- - `workspace:fs:writeFile(filePath, base64Content)` — Write file
1684
- - `workspace:fs:stat(filePath)` — Get file metadata
1685
- - `workspace:fs:readDirectory(dirPath)` — List directory contents
1686
-
1687
- **Subprocess Execution (extensions needing CLI access):**
1688
- - `process:spawn(command, args[], options)` — Execute external command
1689
- - **Allowed commands:** git, node, bun, npm, yarn, pnpm, docker, psql, sqlite3, python3, python
1690
- - **Options:** `{ cwd?: string, timeout?: number }` (default: 30s timeout, CWD must be within registered project paths, ~/.ppm/extensions/, or current process directory)
1691
- - **Returns:** `{ code: number, stdout: string, stderr: string, error?: string }`
1692
- - **Example:** See ext-git-graph for real-world usage (runs `git log --all` across any registered project via path-based CWD)
1693
-
1694
- - Extension can define custom RPC methods via `rpc.onRequest(method, handler)`
1695
-
1696
- ### State Storage
1697
-
1698
- **Database Schema:**
1699
-
1700
- ```sql
1701
- CREATE TABLE extension_storage (
1702
- ext_id TEXT NOT NULL,
1703
- scope TEXT NOT NULL, -- 'global' | 'workspace'
1704
- key TEXT NOT NULL,
1705
- value TEXT, -- JSON-serialized
1706
- PRIMARY KEY (ext_id, scope, key)
1707
- );
1708
- ```
1709
-
1710
- **Scopes:**
1711
- - **globalState** — Persists across all projects (e.g., user settings, cache)
1712
- - **workspaceState** — Project-specific state (e.g., open panel state)
1713
-
1714
- **API** (inside extension):
1715
- ```typescript
1716
- // In activate(context: ExtensionContext)
1717
- const globalVal = context.globalState.get("lastConnection", "default");
1718
- await context.globalState.update("lastConnection", "my-db");
1719
-
1720
- const wsVal = context.workspaceState.get("selectedTable");
1721
- await context.workspaceState.update("selectedTable", "users");
1722
- ```
1723
-
1724
- ### WebSocket Bridge (Extension ↔ Browser)
1725
-
1726
- Extensions interact with the browser UI via a dedicated WebSocket at `/ws/extensions`. The main process translates between Worker RPC and WS messages.
1727
-
1728
- **Server → Client (ExtServerMsg):** `tree:update`, `tree:refresh`, `statusbar:update/remove`, `notification`, `quickpick:show`, `inputbox:show`, `webview:create/html/dispose/postMessage`, `contributions:update`
1729
-
1730
- **Client → Server (ExtClientMsg):** `ready`, `command:execute`, `tree:expand/click`, `webview:message`, `quickpick:resolve`, `inputbox:resolve`, `notification:action`
1731
-
1732
- **Message routing:**
1733
- - Extension calls `vscode.window.showInformationMessage()` → RPC → `extension-rpc-handlers.ts` → `broadcastExtMsg()` → WS → `use-extension-ws` hook → toast notification
1734
- - Browser user clicks tree item → WS `tree:click` → `extensions.ts` → Worker RPC `ext:command:execute` → CommandService → extension handler
1735
- - Webview iframe postMessage → parent → CustomEvent → WS `webview:message` → Worker RPC `ext:webview:message` → EventEmitter → extension's `onDidReceiveMessage` handler
1736
-
1737
- **Request/response pattern:** QuickPick, InputBox, and notification actions use `requestFromBrowser(msg, trackingId, 30s timeout)` — sends WS message and awaits browser response via pending Promise map.
1738
-
1739
- ### UI Components
1740
-
1741
- Extension UI state lives in Zustand (`extension-store.ts`) and renders via React:
1742
- - **StatusBar** — Fixed bottom bar with left/right aligned items
1743
- - **TreeView** — Recursive tree with expand/collapse, renders in sidebar for `ext:*` tabs
1744
- - **WebviewPanel** — Sandboxed iframe (`allow-scripts` only), `acquireVsCodeApi()` shim auto-injected
1745
- - **QuickPick** — Filterable picker with keyboard nav, bottom-sheet on mobile
1746
- - **InputBox** — Text input dialog with password mode support
1747
- - **Command Palette** — Extension commands merged with built-in commands
1748
-
1749
- ### Contribution Registry
1750
-
1751
- **Purpose:** Central registry of all extension contributions (commands, views, etc.)
1752
-
1753
- **Storage:** In-memory map during runtime
1754
-
1755
- **Endpoints:**
1756
- - `GET /api/extensions/contributions` — List all active contributions
1757
-
1758
- **Contribution Types:**
1759
- 1. **Commands** — Callable actions (e.g., `ppm.database.openConnection`)
1760
- - Registered: `registry.registerCommand(extId, command)`
1761
- - Invoked: `POST /api/extensions/{extId}/commands/{command}`
1762
-
1763
- 2. **Views** — Sidebar panels or tree views
1764
- - Registered: `registry.registerView(extId, view)`
1765
- - Rendered in UI based on `type` (tree, webview)
1766
-
1767
- 3. **Configuration** — Settings schema
1768
- - Registered: `registry.registerConfig(extId, schema)`
1769
- - Merged with global settings
1770
-
1771
- ### CLI Commands
1772
-
1773
- ```bash
1774
- ppm ext list # List installed extensions
1775
- ppm ext install @ppm/ext-database # Install from npm
1776
- ppm ext remove @ppm/ext-database # Uninstall
1777
- ppm ext enable @ppm/ext-database # Enable extension
1778
- ppm ext disable @ppm/ext-database # Disable extension
1779
- ppm ext dev /path/to/ext-src # Symlink local extension for dev
1780
- ppm ext config <ext-id> <key> <value> # Set config value
1781
- ```
1782
-
1783
- **Dev Mode** (`ppm ext dev /path/to/src`):
1784
- - Symlinks local extension to `~/.ppm/extensions/node_modules/`
1785
- - Auto-reloads on file change
1786
- - Extension runs from source (TypeScript not compiled)
1787
-
1788
- ### REST API
1789
-
1790
- **Endpoints** (`src/server/routes/extensions.ts`):
1791
-
1792
- | Method | Endpoint | Description |
1793
- |--------|----------|-------------|
1794
- | **GET** | `/api/extensions` | List installed extensions |
1795
- | **POST** | `/api/extensions` | Install extension (body: {name, version?}) |
1796
- | **GET** | `/api/extensions/:id` | Get extension info (manifest, status) |
1797
- | **DELETE** | `/api/extensions/:id` | Remove extension |
1798
- | **PATCH** | `/api/extensions/:id` | Update extension (body: {enabled}) |
1799
- | **GET** | `/api/extensions/contributions` | List all contributions (commands, views, config) |
1800
- | **POST** | `/api/extensions/:id/commands/:cmd` | Invoke extension command |
1801
-
1802
- **Example: Install Extension**
1803
- ```bash
1804
- POST /api/extensions
1805
- Content-Type: application/json
1806
-
1807
- { "name": "@ppm/ext-database", "version": "1.0.0" }
1808
-
1809
- # Response
1810
- {
1811
- "ok": true,
1812
- "data": {
1813
- "id": "@ppm/ext-database",
1814
- "version": "1.0.0",
1815
- "displayName": "Database Browser",
1816
- "enabled": true,
1817
- "activated": false
1818
- }
1819
- }
1820
- ```
1821
-
1822
- ### Service Layer
1823
-
1824
- **ExtensionService** (`src/services/extension.service.ts`):
1825
- - `discover()` — Scan `~/.ppm/extensions/` for installed packages
1826
- - `install(name)` — Fetch from npm, install locally
1827
- - `remove(id)` — Uninstall extension
1828
- - `activate(id)` — Load + run extension in worker
1829
- - `deactivate(id)` — Terminate worker, cleanup
1830
- - `parseManifest(pkg)` — Extract manifest from package.json
1831
- - `setExtensionState(extId, scope, key, value)` — Persist state
1832
-
1833
- **ExtensionInstaller** (`src/services/extension-installer.ts`):
1834
- - `installExtension(name, dir)` — npm install + verify
1835
- - `removeExtension(id, dir)` — rm -rf extension directory
1836
- - `devLinkExtension(localPath)` — Symlink for local dev
1837
-
1838
- **ExtensionManifest** (`src/services/extension-manifest.ts`):
1839
- - `parseManifest(pkg)` — Validate + parse ppm section
1840
- - `discoverManifests(dir)` — Scan all installed extensions
1841
-
1842
- **RpcChannel** (`src/services/extension-rpc.ts`):
1843
- - Bidirectional RPC messaging
1844
- - Request/response matching by ID
1845
- - Event broadcasting
1846
- - Timeout handling
1847
-
1848
- ### Worker Integration
1849
-
1850
- **ExtensionHostWorker** (`src/services/extension-host-worker.ts`):
1851
- - Worker-side code that loads + activates extension
1852
- - Loads extension code into worker context
1853
- - Exposes ExtensionContext API (globalState, workspaceState, subscriptions)
1854
- - Handles incoming RPC messages
1855
- - Communicates back to main process
1856
-
1857
- **Design:**
1858
- ```
1859
- Main Process Worker
1860
- ↓ ↓
1861
- ExtensionService ExtensionHostWorker
1862
- ↓ ↓
1863
- RpcChannel ←────────────→ RpcChannel
1864
- ↓ ↓
1865
- Sends: { Extension Code
1866
- type: "request", (User's ext.ts)
1867
- method: "..." ↓
1868
- } activate(context)
1869
- ↓ ↓
1870
- Handlers respond context.storage.get()
1871
- ↑ ↑
1872
- └─────────────────┘
1873
- ```
1874
-
1875
- ### Dev Workflow
1876
-
1877
- **Creating an Extension:**
1878
-
1879
- 1. Create npm package:
1880
- ```bash
1881
- npm init -y @ppm/ext-my-feature
1882
- npm install @ppm/extension-api
1883
- ```
1884
-
1885
- 2. Write `src/extension.ts`:
1886
- ```typescript
1887
- import type { ExtensionContext } from "@ppm/extension-api";
1888
-
1889
- export async function activate(context: ExtensionContext) {
1890
- console.log(`Extension ${context.extensionId} activated!`);
1891
-
1892
- const val = context.globalState.get("count", 0);
1893
- await context.globalState.update("count", val + 1);
1894
- }
1895
-
1896
- export function deactivate() {
1897
- console.log("Extension deactivated");
1898
- }
1899
- ```
1900
-
1901
- 3. Add to `package.json`:
1902
- ```json
1903
- {
1904
- "ppm": {
1905
- "displayName": "My Feature",
1906
- "main": "dist/extension.js",
1907
- "contributes": {
1908
- "commands": [...]
1909
- }
1910
- }
1911
- }
1912
- ```
1913
-
1914
- 4. Install locally for dev:
1915
- ```bash
1916
- ppm ext dev /path/to/ext-my-feature
1917
- ```
1918
-
1919
- 5. Extension auto-activates based on `activationEvents`, state persists
1920
-
1921
- ### Error Handling & Debugging
1922
-
1923
- **Activation Error Tracking:**
1924
- - `ExtensionService.activationErrors` Map tracks `extId → error message` for all failed activations
1925
- - Errors set during `activate()` if worker response indicates failure (`!result.ok`)
1926
- - Errors cleared on successful activation or worker termination
1927
- - Errors included in `contributions:update` message sent via WS to browser on client connect
1928
-
1929
- **User Feedback (UI):**
1930
- - **Command Errors:** When extension command fails, toast shows "Extension command failed: {error}" with error details
1931
- - **Timeout Handling:** If webview panel doesn't load within 10s, fallback UI displays activation error (if available) + "Retry" button
1932
- - **Retry Button:** User can click to re-trigger the command without page reload (re-dispatches `ext:command:execute`)
1933
-
1934
- **Breadcrumb Logging (Console):**
1935
- - **`[ExtService]`** — Main process lifecycle: activation start/success, worker lifecycle, contributions broadcast
1936
- - **`[ExtHost]`** — Worker-side execution: command routing, handler invocation, error context
1937
- - **`[ExtWS]`** — WebSocket bridge: client connect, message handling, error responses
1938
- - **Extension-specific tags** — e.g., `[ext-git-graph]` for extension-specific log context
1939
-
1940
- **Example Log Flow (normal):**
1941
- ```
1942
- [ExtService] startup: activating ext-git-graph...
1943
- [ExtWS] Client connected (1 total)
1944
- [ExtHost] activating ext-git-graph from dist/extension.js
1945
- [ExtHost] activated ext-git-graph (1 total)
1946
- [ExtService] activated ext-git-graph successfully
1947
- [ExtWS] command:execute "git-graph.view"
1948
- [ExtHost] command:execute "git-graph.view" (1 extensions active)
1949
- [ExtHost] routing "git-graph.view" → ext-git-graph
1950
- ```
1951
-
1952
- **Example Log Flow (error):**
1953
- ```
1954
- [ExtService] startup: activating ext-git-graph...
1955
- [ExtHost] activating ext-git-graph from dist/extension.ts
1956
- [ExtHost] ERROR: Cannot find module 'missing-dep'
1957
- [ExtService] Failed to activate ext-git-graph on startup: Cannot find module 'missing-dep'
1958
- → activationErrors["ext-git-graph"] = "Cannot find module 'missing-dep'"
1959
- → browser receives { type: "contributions:update", activationErrors: {"ext-git-graph": "..."} }
1960
- → user sees toast: "Extension "ext-git-graph" failed to activate: Cannot find module..."
1961
- ```
1962
-
1963
- ### Crash Safety
1964
-
1965
- **Worker Isolation:**
1966
- - Each extension in isolated Bun Worker thread
1967
- - Worker crash doesn't crash main process
1968
- - Error events logged, extension marked as failed
1969
- - Main process continues operating
1970
-
1971
- **Cleanup:**
1972
- - Worker terminates → cleanup timer expires after 5min
1973
- - Persisted state preserved in SQLite (not lost on crash)
1974
- - Next activation reloads from disk, state auto-restored
1975
-
1976
- ### Future Enhancements (Phase 2+)
1977
-
1978
- - **UI Webview Support** — Extensions define HTML/React UI panels
1979
- - **Extension Settings UI** — Auto-generate UI from `contributes.configuration`
1980
- - **Hot Reload** — Auto-reload extension on file change during dev
1981
- - **Marketplace** — Browse, rate, publish extensions (v1.0+)
1982
- - **Permissions** — User prompt for sensitive operations
1983
- - **Inter-Extension API** — Extensions can call each other via RPC
1984
-
1985
- ---
1986
-
1987
- **Tool Allow List:**
1988
- - All MCP tools automatically allowed via wildcard `mcp__*`
1989
- - MCP server connection failures don't block chat (logged as warning)
1990
-
1991
- ### Import Flow
1992
-
1993
- **Auto-import on first access:**
1994
- 1. GET `/api/settings/mcp` called
1995
- 2. If table is empty, read `~/.claude.json`
1996
- 3. If `mcpServers` key exists, bulk import (validate + skip duplicates)
1997
- 4. Return populated list
1998
-
1999
- **Manual import:**
2000
- 1. GET `/api/settings/mcp/import/preview` — show what's available
2001
- 2. POST `/api/settings/mcp/import` — import validated servers
2002
- 3. Returns `{ imported: N, skipped: M }`
2003
-
2004
- ### Error Handling
2005
-
2006
- | Scenario | Response |
2007
- |----------|----------|
2008
- | Invalid name (non-alphanumeric) | 400 Bad Request |
2009
- | Invalid config (missing required fields) | 400 Bad Request |
2010
- | Duplicate name | 409 Conflict |
2011
- | Server not found (GET/:name, PUT/:name, DELETE/:name) | 404 Not Found |
2012
- | `~/.claude.json` not found (import) | 404 Not Found |
2013
- | Corrupt config JSON (recovery) | Log warning, skip entry, continue |
2014
-
2015
- ---
2016
-
2017
392
  ## Deployment Architecture
2018
393
 
2019
394
  ### Single-Machine Deployment (Current)
2020
395
  ```
2021
- Linux/macOS Host
396
+ macOS / Linux / Windows Host
2022
397
  ├── ppm (compiled binary)
2023
398
  │ └── Embeds: server code, frontend assets
2024
- ├── ppm.yaml (config, auto-generated)
2025
- └── ~/.ppm/ (optional: session cache, logs)
399
+ └── ~/.ppm/ (ppm.db config, sessions, logs, cloudflared, status.json)
2026
400
  ```
2027
401
 
2028
402
  ### Daemon Mode (Default)
@@ -2034,10 +408,8 @@ $ ppm start
2034
408
  → Fallback compat: ppm.pid read/written for backward compatibility
2035
409
  → Supervisor checks npm registry every 15min for updates, writes availableVersion to status.json
2036
410
 
2037
- $ ppm start --foreground
2038
- Runs in foreground (debugging, CI/CD)
2039
- → WebSocket and all features fully functional
2040
- → Tunnel always active (public URL works in foreground)
411
+ (There is no foreground mode — the supervised daemon is the only one.
412
+ For logs, use `ppm logs -f`; for machine-readable state, `ppm status --json`.)
2041
413
 
2042
414
  $ ppm start
2043
415
  → Daemon mode + Cloudflare Quick Tunnel (always enabled)
@@ -2089,9 +461,9 @@ Supervisor Process (parent)
2089
461
  │ ├── Auto-restart on crash (exponential backoff, max 10 restarts)
2090
462
  │ └── If in "stopped" state, serves minimal 503 page instead of restarting
2091
463
 
2092
- ├── Tunnel Child (Cloudflare Quick Tunnel, always enabled)
464
+ ├── Tunnel Child (quick by default; named when configured — see "Tunnel Modes" below)
2093
465
  │ ├── Origin is the EDGE port, so a server port move cannot rotate the URL
2094
- │ ├── URL probe every 2min
466
+ │ ├── Quick: URL probe every 2min. Named: health probe every 30s, restart-once-then-warn
2095
467
  │ ├── Auto-reconnect on failure
2096
468
  │ └── URL persisted to status.json
2097
469
 
@@ -2124,9 +496,16 @@ Supervisor Process (parent)
2124
496
  | `ppm down` | Killed | Killed | Killed | Full cleanup, exit |
2125
497
 
2126
498
  **State Persistence:**
2127
- - Status file: `~/.ppm/status.json` — PID, port, host, shareUrl, supervisorPid, availableVersion, state
499
+ - Status file: `~/.ppm/status.json` — PID, port, host, shareUrl, supervisorPid, availableVersion, state,
500
+ `tunnelMode` ("quick"|"named", what's actually running), `tunnelWarning` (set when named degrades to
501
+ quick or the named hostname stops resolving; persists until the condition clears), `capabilities`
502
+ (`["retunnel"]` once the running supervisor understands that command — its absence is how the UI
503
+ detects a pre-upgrade supervisor and falls back to "run `ppm restart`")
2128
504
  - Lock file: `~/.ppm/.start-lock` — Prevent concurrent starts
2129
- - Command file: `~/.ppm/.supervisor-cmd` — IPC for soft_stop, resume, self_replace
505
+ - Command file: `~/.ppm/.supervisor-cmd` — IPC for soft_stop, resume, self_replace, restart, upgrade,
506
+ and `retunnel` (reload the tunnel config without a full restart, used after named-tunnel setup);
507
+ `retunnel` is deliberately the lowest-priority action — any lifecycle command overwrites a pending
508
+ one rather than getting silently dropped
2130
509
 
2131
510
  **Stopped Page Implementation:**
2132
511
  - Minimal HTTP server on same port as main server
@@ -2141,8 +520,50 @@ Supervisor Process (parent)
2141
520
 
2142
521
  ---
2143
522
 
2144
- ### Future: Multi-Machine (Not in v2)
2145
- Would require:
523
+ ### Tunnel Modes: Quick vs Named
524
+
525
+ Two ways to get a public URL, chosen by the `tunnel` config row in SQLite:
526
+
527
+ - **Quick** (default) — a Cloudflare Quick Tunnel with a random `*.trycloudflare.com` hostname that
528
+ rotates on every restart. No setup, no Cloudflare account.
529
+ - **Named** — a stable `https://<prefix>.<zone>` hostname on a Cloudflare-managed domain the user
530
+ owns, set up once through a first-run popup (Cloudflare login → pick a hostname) and reused across
531
+ restarts, hibernate, and crashes.
532
+
533
+ **Process ownership** — the split matters for reasoning about failures:
534
+ - The **supervisor** (`src/services/supervisor.ts`) owns the one long-running `cloudflared tunnel run`
535
+ (or `tunnel --url` for quick) child, in both modes — spawn, health-probe, kill, and respawn all
536
+ happen there, the same seam that already manages the server child.
537
+ - The **server** (`src/server/routes/named-tunnel.ts` → `src/services/named-tunnel/`) only
538
+ *orchestrates setup*: one-shot `cloudflared tunnel create/route/token` calls to provision the
539
+ tunnel and DNS record, then asks the supervisor to pick up the new config via `retunnel`
540
+ (`requestTunnelReload()` writes `.supervisor-cmd`). The server process never runs the long-lived
541
+ connector itself.
542
+ - A named tunnel that fails to spawn falls back to quick immediately (never leaves the process
543
+ without a public URL); the fallback and the reason are surfaced as `tunnelWarning` in `status.json`.
544
+ A named tunnel whose hostname stops resolving (e.g. the CNAME was deleted) gets exactly one
545
+ restart-and-hope; if the next health probe is still unhealthy, the connector is left running and a
546
+ `tunnelWarning` is raised instead of restarting forever (`src/services/named-tunnel/named-tunnel-probe-state.ts`).
547
+
548
+ **Where the secrets live** — three different pieces of Cloudflare-issued material, three different
549
+ homes, none of them ever in a process's argv:
550
+ - `cert.pem`'s `apiToken` (the Cloudflare API credential `cloudflared tunnel login` writes) is parsed
551
+ in-memory to call the Cloudflare API during setup and is **never persisted anywhere PPM controls** —
552
+ it stays only in `~/.cloudflared/cert.pem`, which is on `fs-credential-path-guard.ts`'s refuse-list
553
+ (see "PPM Directory" in the root `CLAUDE.md`) so no generic file route can read, copy, or move it out.
554
+ - The tunnel's run token (from `cloudflared tunnel token`) is stored in SQLite as
555
+ `tunnel.namedTunnelToken` and masked wherever config is echoed back (`ppm config get`, the extension
556
+ RPC `workspace:config:get`, `ppm status`) — see `src/services/config-secret-keys.ts`.
557
+ - The same run token is also written to `~/.ppm/named-tunnel.token` (mode 0600) and handed to
558
+ `cloudflared` via `--token-file`, never `--token <value>`, so it never appears in a process listing
559
+ or a crash dump that captures cmdlines.
560
+
561
+ Zone/account IDs read from the cert are pinned into the config row at setup time; a later login to a
562
+ different Cloudflare account is detected (`certState: "mismatch"`) and routed to a re-login rather
563
+ than silently reused.
564
+
565
+ ### Future: Multi-Machine (not planned)
566
+ PPM is single-machine by design; multi-machine would require:
2146
567
  - Central state server (Redis/Postgres)
2147
568
  - Session sharing across servers
2148
569
  - Shared filesystem or file sync protocol
@@ -2150,42 +571,6 @@ Would require:
2150
571
 
2151
572
  ---
2152
573
 
2153
- ## Frontend Performance Optimization (v0.9.86+)
2154
-
2155
- ### Memory & Re-Render Reduction
2156
-
2157
- **1. useShallow Pattern (Zustand)**
2158
- - All destructured store selectors wrapped in `useShallow()` (36 sites)
2159
- - Prevents unnecessary re-renders when object properties mutate
2160
- - Example: `const { messages, addMessage } = chatStore(useShallow(...))`
2161
-
2162
- **2. Component Memoization (React.memo)**
2163
- - 10 heavy components wrapped (CodeEditor, MessageBubble, ProjectBar, ProjectAvatar, TerminalTab, PanelLayout, Sidebar, StatusBar, StatusBarEntry, TabBar, TreeNode)
2164
- - Memoization skips re-renders if props unchanged
2165
- - Paired with `useCallback` to maintain stable references
2166
-
2167
- **3. Lazy Loading**
2168
- - MarkdownRenderer lazy-loaded from 3 sites (reduces initial bundle)
2169
- - CodeMirror on-demand in postgres-viewer
2170
- - Mermaid diagram support loaded dynamically only when diagram syntax detected
2171
-
2172
- **4. Code Splitting (vite.config.ts)**
2173
- - 5 vendor chunks: `vendor-monaco`, `vendor-mermaid`, `vendor-xterm`, `vendor-markdown`, `vendor-ui`
2174
- - Heavy libraries (>500KB) in separate chunks for better browser caching
2175
- - Each chunk independently cacheable and updated
2176
-
2177
- **5. Chat Pagination & Message Caps**
2178
- - Chat history loads 50 messages per page with load-more button (prevents DOM bloat)
2179
- - Team activity capped at 500 messages (prevents unbounded growth)
2180
-
2181
- ### Benefits
2182
- - Faster page load (lazy chunks load on-demand)
2183
- - Reduced re-render cycles (useShallow + memo)
2184
- - Lower memory footprint (capped message buffers)
2185
- - Better caching (vendor chunk stability across versions)
2186
-
2187
- ---
2188
-
2189
574
  ## Error Handling Strategy
2190
575
 
2191
576
  | Layer | Error Type | Handling |
@@ -2214,137 +599,3 @@ Would require:
2214
599
 
2215
600
  ---
2216
601
 
2217
- ## OS File Explorer Window
2218
-
2219
- A floating, OS-skinned window (Windows 11 / macOS Finder chrome, Linux → macOS skin) that browses
2220
- the **whole host filesystem** — not just registered project directories — through the widened
2221
- `/api/fs` family. This is a deliberate scope change from every other file-facing route in PPM
2222
- (project-scoped, path-validated against one repo root): the explorer's authorization boundary is
2223
- "the whole disk, behind PPM's existing session auth", not "one project".
2224
-
2225
- ### FS scope = auth boundary
2226
-
2227
- Every `/api/fs` route — including `docx-html`, `read`, `raw`, both SQLite doors — passes through
2228
- one shared guard chain (`src/services/fs-path-guard.service.ts`) before touching disk:
2229
-
2230
- | Protection | Mechanism |
2231
- |---|---|
2232
- | PPM-dir shield | `assertNotPpmDir` refuses `getPpmDir()` (config, auth token, credentials) as a source **or** destination of any read or mutation, checked at both the given path and its resolved realpath (defeats a symlink pointed at the PPM dir) |
2233
- | Protected roots | `/`, drive roots (`C:\`), `$HOME` and the PPM dir itself refuse delete/rename/move as a source |
2234
- | Download tokens | `/api/fs/download/token` issues a single-use, path-bound token; `/api/fs/raw` spends it on first use, rejects replay and any path mismatch |
2235
- | Symlink safety | every op `lstat`s the entry itself (never follows to the target) so a link *to* a protected path can itself still be deleted, but nothing can read/write *through* one into the PPM dir |
2236
- | SQL injection surface | the external-DB doors (`/api/fs/sqlite/*`) block `ATTACH`/`DETACH` by keyword scan (after stripping comments/string literals) before executing any query — the same class of guard the project-scoped `/sqlite` route also needed |
2237
- | No event-loop blocking | every op is `fs.promises`-based with bounded concurrency and a per-entry timeout — a dead network mount or sleeping USB drive cannot stall unrelated requests, which matters once scope is the whole disk instead of one project |
2238
-
2239
- ### API surface
2240
-
2241
- | Method | Path | Purpose |
2242
- |---|---|---|
2243
- | GET | `/api/system/host` | `HostInfo`: platform, path separator, homedir, drives, known folders, OS-pinned folders (Quick Access / Finder Favorites / GTK+KDE bookmarks), warnings |
2244
- | GET | `/api/fs/browse` | Directory listing (existing route, whitelist widened to `/`) |
2245
- | GET | `/api/fs/stat` | Single-entry metadata |
2246
- | POST | `/api/fs/copy` \| `/move` \| `/rename` \| `/touch` \| `/mkdir` | Mutations, collision (`EEXIST`)/self-nesting (`EINVAL`) reported for the client to resolve |
2247
- | DELETE | `/api/fs/delete` \| `/rmdir` | `{permanent?}` — OS trash (Recycle Bin / Trash / gio) by default, permanent on request |
2248
- | POST/GET | `/api/fs/download/token` / `/api/fs/raw` | Single-use, path-bound download |
2249
- | GET/POST | `/api/fs/sqlite/{tables,schema,data,query}` | External `.db` viewer — same shape as the project-scoped `/sqlite` route, `path` absolute, PPM dir refused |
2250
-
2251
- `host-info.service.ts` orchestrates three OS-specific provider sets (`src/services/host-info/`)
2252
- behind a 60s cache with in-flight de-duplication (concurrent `?refresh=true` calls share one
2253
- rebuild rather than spawning N PowerShell/plutil/findmnt processes).
2254
-
2255
- ### Floating window layer
2256
-
2257
- `src/web/components/floating-window/` — a content-agnostic window manager (zustand store):
2258
- drag/8-handle-resize gestures write geometry straight to the DOM element (no React re-render per
2259
- pointermove), committing to the store only on gesture end; rect + open windows persist to
2260
- `localStorage["ppm-windows"]`, restored once per app load and re-clamped to the current viewport.
2261
-
2262
- Windows render in a portal at **z-30..38** (`30 + rank`, capped at an 8-window dense-rank limit) —
2263
- below the app's existing `z-40` click-away backdrops and `z-50` Radix layers, so command palette,
2264
- dropdowns and dialogs always stay reachable above any number of open explorer windows. Below the
2265
- `md` breakpoint the layer never mounts at all; `src/web/components/os-explorer/mobile/` renders the
2266
- same `ExplorerBody` component inside a full-screen bottom sheet instead (`variant="sheet"`).
2267
- Drag and resize share a `gestureAbandoned()` guard (`use-window-gesture-context.ts`) for the
2268
- pointer-up that still arrives after a mid-gesture window close (e.g. dragging by a titlebar button
2269
- that closes the frame).
2270
-
2271
- **One chrome, every kind.** `WindowSkinChrome` (`window-skin-chrome.tsx`) is the titlebar every
2272
- window kind — explorer, team-member session, system monitor, detached tab — renders: it resolves
2273
- the active OS skin via `useExplorerSkin()` (Settings override, else host platform; Linux → macOS)
2274
- and delegates to that skin's `WindowsWindowChrome` / `MacosWindowChrome`
2275
- (`src/web/components/os-explorer/skins/`), scoped entirely through `[data-skin="windows"|"macos"]`
2276
- CSS variables layered over PPM's existing semantic theme tokens — no second color table. The
2277
- Windows skin's folder glyph draws only for `kind === "explorer"`; every other kind gets the bare
2278
- titlebar. The macOS skin boxes the title between the traffic lights and the PiP button as a flex
2279
- child (so a long title truncates instead of overlapping either), and puts the PiP button at the
2280
- titlebar's right end; the Windows skin puts it left of minimize.
2281
-
2282
- **PiP is a capability of the frame, not of one kind.** `useWindowBodyElement`
2283
- (`use-window-body-element.ts`) creates the single DOM element `FloatingWindow` portals a window's
2284
- content into and publishes it as that window's PiP slot (`window-pip-registry.ts`, keyed by window
2285
- id). `PipCaptionButton` (`pip/pip-caption-button.tsx`), rendered by both skins, moves that slot into
2286
- a `documentPictureInPicture` window and back; it is absent (not disabled) where the API is
2287
- unsupported. `WindowPipPlaceholder` (`window-pip-placeholder.tsx`) takes the body's place in the
2288
- frame while it plays in PiP, with a ≥44px "Bring back" control. The mechanics below (attach/detach,
2289
- style mirroring, key forwarding, resize signalling) apply to whichever window kind currently owns
2290
- the slot — a tab-host window is only the one kind whose body is itself a portal target for another
2291
- component (`TabPool`).
2292
-
2293
- #### Tab-host windows (detaching a tab into its own window)
2294
-
2295
- - **Off-grid panel.** Detaching creates `` `__win__:${windowId}` `` (`windowPanelId()`,
2296
- `stores/panel-utils.ts`) — same treatment as `__dock__`: lives in `panels`, never in `grid`, so no
2297
- grid math (rows/columns/split) sees it. `stores/window-panel-actions.ts` is the only writer of
2298
- these panels and enforces the paired invariant: `focusedPanelId` never points at one (it would
2299
- send the next `openTab()` with no explicit panel into a window). `popOutTab`/`redockFromWindow`
2300
- create and destroy the panel and its window together; every close path (titlebar ×, keyboard,
2301
- reconcile) routes through `redockFromWindow`, which re-docks to the origin panel if it is still in
2302
- the grid, else the focused grid panel, else the first grid panel. All tab types pop out except
2303
- `system-monitor`, which already has its own window kind.
2304
- - **No-remount move.** `TabPool` (`components/layout/tab-pool.tsx`) mounts every tab once into a
2305
- wrapper `div[data-tab-pool-id]` created imperatively in `ReparentingTab`
2306
- (`components/layout/reparenting-tab.tsx`) and rendered into it via `createPortal` — React attaches
2307
- its listeners to the wrapper itself, so they keep firing after the node moves, including into
2308
- another document. A `useLayoutEffect` with no deps calls `appendChild` to move the wrapper into
2309
- whichever element last registered for the tab's panel id (`slotRegistry`,
2310
- `components/layout/tab-pool-registry.ts`); `TabHostWindowContent`'s slot `div` is always mounted
2311
- (never swapped for a placeholder), because it may currently be living inside the PiP document.
2312
- - **Persistence.** Window panels persist to their own global `localStorage["ppm-window-panels"]`
2313
- key (`stores/window-panel-persistence.ts`), separate from the per-project `ppm-panels-*` blob and
2314
- not synced to the server — the same limitation window geometry (`ppm-windows`) already has.
2315
- `WINDOW_KINDS` (`window-store-types.ts`) is the single list both the window store and
2316
- `window-persistence.ts` filter against; `team-member` is excluded from restore because its body
2317
- streams a live subagent session that cannot survive a reload.
2318
- - **Reconcile.** The two halves persist separately, so a reload can restore one without the other.
2319
- `reconcileTabHostWindows` (`stores/window-panel-reconcile.ts`), run once per project via
2320
- `useWindowPanelReconcile()` after the window layer restores, and unconditionally below `md` (the
2321
- window layer never mounts there): a panel whose window is gone comes back to the grid; a window
2322
- with no panel behind it closes.
2323
- - **PiP host.** `attachPipHost`/`isDocumentPipSupported` (`floating-window/pip/pip-host.ts`,
2324
- `pip-support.ts`) move a window's *slot* element — never a tab's own wrapper — into a
2325
- `documentPictureInPicture` window, one at a time per page. `pagehide` triggers a synchronous
2326
- restore (no `await` between it and the DOM move) so a closing PiP document never strips listeners
2327
- off a still-live terminal. `pip-style-copy.ts` mirrors stylesheets + `adoptedStyleSheets` once and
2328
- `<html>` class/inline theme CSS vars + `<body>` class **and inline style** on every theme change
2329
- (the page background is an inline `background: var(--bg)` on `<body>` in `index.html`, not a
2330
- class), plus a MutationObserver for Vite HMR-injected `<style>` tags. `pip-key-forward.ts`
2331
- re-dispatches keydown/keyup from the PiP window onto the main window for app-level shortcuts,
2332
- skipping targets that own their own input (`input`, `.monaco-editor`, `.xterm-helper-textarea`,
2333
- etc.). `pip-resize-signal.ts` dispatches a non-bubbling `ppm:host-resize` CustomEvent on each
2334
- `[data-tab-pool-id]` wrapper inside the slot (on attach, every PiP `resize`, and on detach); the
2335
- terminal and editors subscribe via `onHostResize` and re-fit, because main-window
2336
- `ResizeObserver`s are late or silent for a PiP-driven size.
2337
- - **Radix portals in PiP.** `PortalContainerProvider` (`components/ui/portal-container-context.tsx`)
2338
- is mounted inside `ReparentingTab` around each tab's content, fed the PiP document's `body` while
2339
- that tab's window is in PiP (`usePipPortalContainer`, `window-pip-registry.ts`) — so a tab's own
2340
- dropdowns/tooltips/dialogs render inside the PiP document instead of opening unreachably in the
2341
- main window. `undefined` (document default) while docked.
2342
- - **Known limitations** (see the comment block atop `pip/pip-caption-button.tsx`): sonner toasts
2343
- always render in the main window (one app-root toaster); `useIsMobile()`/Tailwind `md:` read the
2344
- main window's viewport, not the PiP window's; `onSelect`/`selectionchange` degrades for PiP
2345
- content; the terminal's reconnect check reads the main document's visibility; Monaco keybindings
2346
- (Ctrl+Z, Ctrl+F, …) don't fire while the editor sits in PiP (typing still reaches the buffer); a
2347
- tab-host window's titlebar keeps the tab title captured at pop-out time.
2348
- - **Mobile.** Pop-out and PiP are hidden entirely below `md` (`useIsMobile()`) — never a scaled-down
2349
- window.
2350
-