@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.
- package/AGENTS.md +17 -5
- package/CHANGELOG.md +22 -0
- package/CLAUDE.md +3 -0
- package/README.md +6 -6
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +19 -1
- package/dist/web/assets/{adaptive-context-menu-DGCKKeTy.js → adaptive-context-menu-CbgPYQmj.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-BOHHTTe7.js → ai-resource-editor-unM2HNmg.js} +1 -1
- package/dist/web/assets/{ai-settings-section-BigC_nZR.js → ai-settings-section-CdktdrwF.js} +1 -1
- package/dist/web/assets/{angular-html-BYPaydvh.js → angular-html-DcpbdcNG.js} +1 -1
- package/dist/web/assets/{angular-ts-Bb4AMKlk.js → angular-ts-Bv_WIk34.js} +1 -1
- package/dist/web/assets/api-client-BogqsFna.js +1 -0
- package/dist/web/assets/{api-settings-Bd3M4YpI.js → api-settings-BdCtex9i.js} +1 -1
- package/dist/web/assets/api-settings-DfXViPKz.js +1 -0
- package/dist/web/assets/{api-themes-Cpx419SL.js → api-themes-pRRoLvgD.js} +1 -1
- package/dist/web/assets/{apl-DB2VjFCS.js → apl-D5_lLszu.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-BzFm9ELu.js +1 -0
- package/dist/web/assets/{astro-BeoJdVpk.js → astro-CBHwddYL.js} +1 -1
- package/dist/web/assets/{audio-preview-BgFuTn-0.js → audio-preview-BD1iDovf.js} +1 -1
- package/dist/web/assets/{blade-Cf2q1sPf.js → blade-DGe1d2Pq.js} +1 -1
- package/dist/web/assets/{bsl-W1kOaNpu.js → bsl-2lyAzRJA.js} +1 -1
- package/dist/web/assets/{button-5zTrkEoN.js → button-2WCAS8dI.js} +1 -1
- package/dist/web/assets/{cairo-BoAZFcje.js → cairo-DjzZeJrq.js} +1 -1
- package/dist/web/assets/chat-tab-DzK8LGti.js +15 -0
- package/dist/web/assets/{cobol-B0Nm7RXj.js → cobol-cC3XIuAJ.js} +1 -1
- package/dist/web/assets/code-editor-CuMfPN9X.js +10 -0
- package/dist/web/assets/{coffee-BakMn3on.js → coffee-4eGboVoU.js} +1 -1
- package/dist/web/assets/{conflict-editor-BUs5k9q_.js → conflict-editor-DdJag4QK.js} +1 -1
- package/dist/web/assets/{context-menu-Bq92km4s.js → context-menu-BO1c9jKy.js} +1 -1
- package/dist/web/assets/{cpp-BRAJQHK2.js → cpp-DF9_G-OU.js} +1 -1
- package/dist/web/assets/{crystal-kqqO3zkK.js → crystal-OavZlzcm.js} +1 -1
- package/dist/web/assets/{csv-preview-ByoDgLAZ.js → csv-preview-DnXwouF3.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-Brkf9z41.js → data-grid-overlay-editor-B9Gpjm3l.js} +1 -1
- package/dist/web/assets/{database-viewer-Btc3h8Yu.js → database-viewer-Dk8oGyZx.js} +1 -1
- package/dist/web/assets/{dialog-DwtGlowE.js → dialog-B-l2ZxVm.js} +1 -1
- package/dist/web/assets/{diff-viewer-CZaDnR4w.js → diff-viewer-eaUBjLCH.js} +1 -1
- package/dist/web/assets/{docx-preview-DAD_vg59.js → docx-preview-DAdHPlhZ.js} +1 -1
- package/dist/web/assets/{edge-Cq1ZKZuE.js → edge-TzYh8OlN.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-B_VE7ppQ.js → edit-diff-preview-DlwP3UIU.js} +1 -1
- package/dist/web/assets/{elixir-BM_JoJRk.js → elixir-Cy3RhKKH.js} +1 -1
- package/dist/web/assets/{elm-Dw7oO08Y.js → elm-BTBZdweK.js} +1 -1
- package/dist/web/assets/{erb-CfKOGAfk.js → erb-dv1mI0y4.js} +1 -1
- package/dist/web/assets/{erlang-DlDmGVeE.js → erlang-DjeiDrkl.js} +1 -1
- package/dist/web/assets/{esm-DGAP0GFh.js → esm-FRwvcesu.js} +1 -1
- package/dist/web/assets/explorer-body-DkCkSVrN.js +2 -0
- package/dist/web/assets/explorer-window-content-MsnTVngu.js +1 -0
- package/dist/web/assets/{extension-store-Cpfh2vMm.js → extension-store-DUaZINyC.js} +1 -1
- package/dist/web/assets/{extension-webview-B2OnjuCu.js → extension-webview-W0qRfaAU.js} +1 -1
- package/dist/web/assets/{file-store-aOERIEo-.js → file-store-yReVbR8q.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-XnAhQWxS.js → fortran-fixed-form-BCH9DAIJ.js} +1 -1
- package/dist/web/assets/{fsharp-CdNvy0PG.js → fsharp-C93NYKYD.js} +1 -1
- package/dist/web/assets/{gdresource-DxuG5i8i.js → gdresource-BcIEE2eX.js} +1 -1
- package/dist/web/assets/{git-commit-3lMonsur.js → git-commit-BVjlXEy8.js} +1 -1
- package/dist/web/assets/{git-log-panel-Co0PBPHC.js → git-log-panel-2_4HuP47.js} +1 -1
- package/dist/web/assets/{git-rebase-N8Z9kIMf.js → git-rebase-Bj_21B1V.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-B72vcb2r.js +1 -0
- package/dist/web/assets/glide-data-grid-B0IY6n_Q.js +138 -0
- package/dist/web/assets/{glimmer-js-Dp9OXiib.js → glimmer-js-C20yj4Zv.js} +1 -1
- package/dist/web/assets/{glimmer-ts-CDVKBcDj.js → glimmer-ts-ipbYC7kh.js} +1 -1
- package/dist/web/assets/{glsl-DPJxI4us.js → glsl-Dovazu3t.js} +1 -1
- package/dist/web/assets/{graphql-BrFhaQQl.js → graphql-DpMlM7hC.js} +1 -1
- package/dist/web/assets/{group-chat-tab-rOd8J8GC.js → group-chat-tab-DRLDHOp6.js} +1 -1
- package/dist/web/assets/{hack-BzXbDMlA.js → hack-gc7LCBuV.js} +1 -1
- package/dist/web/assets/{haml-CPpkX0s5.js → haml-vtlFm9gm.js} +1 -1
- package/dist/web/assets/{handlebars-BqrApy52.js → handlebars-DGKhpCji.js} +1 -1
- package/dist/web/assets/{html-DmjUl-OA.js → html-DvfhQbXb.js} +1 -1
- package/dist/web/assets/{html-derivative-Cnd6XFR5.js → html-derivative-C3oTSSCN.js} +1 -1
- package/dist/web/assets/{http-DMLWYun2.js → http-SG6zLTo7.js} +1 -1
- package/dist/web/assets/{hurl-CKkCVxbj.js → hurl-BWXsYlqW.js} +1 -1
- package/dist/web/assets/{hxml-CAx19TtQ.js → hxml-DUdMzaoh.js} +1 -1
- package/dist/web/assets/{image-preview-DHUoylQ1.js → image-preview-DxgFOV1T.js} +1 -1
- package/dist/web/assets/index-CBn4SOoT.js +52 -0
- package/dist/web/assets/index-DWpNR3nB.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-3irPxRK_.js +1 -0
- package/dist/web/assets/{input-DcEk7mA3.js → input-DuAF-Stj.js} +1 -1
- package/dist/web/assets/{jinja-BmB9YN9l.js → jinja-955w9RKM.js} +1 -1
- package/dist/web/assets/{jison-BaVsKKpw.js → jison-WWfqPo50.js} +1 -1
- package/dist/web/assets/{julia-BgVBLveX.js → julia-BFrfnaNl.js} +1 -1
- package/dist/web/assets/{just-C2ENIbl9.js → just-BrZdyR_I.js} +1 -1
- package/dist/web/assets/{keybindings-store-BGIyqXHI.js → keybindings-store-0Wqfa7Qp.js} +1 -1
- package/dist/web/assets/keybindings-store-d54pv2NK.js +1 -0
- package/dist/web/assets/{latex-BiDIWxKL.js → latex-CwtPPASD.js} +1 -1
- package/dist/web/assets/{liquid-NxpapreE.js → liquid-CYZuvDDd.js} +1 -1
- package/dist/web/assets/{lua-DVepQvwH.js → lua-BV095ZyY.js} +1 -1
- package/dist/web/assets/markdown-renderer-BTS8ifdu.js +1 -0
- package/dist/web/assets/{markdown-renderer-DlcDhgrF.js → markdown-renderer-CnRcKK3d.js} +3 -3
- package/dist/web/assets/{marko-CJ2606Xl.js → marko-CNeljZ1C.js} +1 -1
- package/dist/web/assets/{mdc-D2bjqclE.js → mdc-JfGapShy.js} +1 -1
- package/dist/web/assets/{media-url-H17XJF-I.js → media-url-Bm4PsduE.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-g-r2hCFa.js → mobile-bottom-sheet-M5iZDX7y.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BqK2-WuN.js +1 -0
- package/dist/web/assets/{nextflow-BtJwUbfF.js → nextflow-DD5C8nqX.js} +1 -1
- package/dist/web/assets/{nginx-j4gyIhHE.js → nginx-Ca7B__x_.js} +1 -1
- package/dist/web/assets/{nim-ClH_gjP5.js → nim-B8FogsDw.js} +1 -1
- package/dist/web/assets/notification-store-Yvy1I8gd.js +1 -0
- package/dist/web/assets/{number-overlay-editor-C1EqJnWq.js → number-overlay-editor-DG9xJwEG.js} +1 -1
- package/dist/web/assets/{overview-panel-vTeeJYVZ.js → overview-panel-s53bnl0q.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-Z5jWjFCV.js +1 -0
- package/dist/web/assets/{panel-store-BSnkjgHt.js → panel-store-B4rijFKN.js} +1 -1
- package/dist/web/assets/{pdf-preview-CqFGRY0Z.js → pdf-preview-DYt3aBzM.js} +1 -1
- package/dist/web/assets/{perl-C9H7X7CT.js → perl-BR78hS8R.js} +1 -1
- package/dist/web/assets/{php-Bx2U-cQ_.js → php-DXBOMC9z.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-Bq7UMi8P.js +1 -0
- package/dist/web/assets/{postgres-viewer-DHwcQucn.js → postgres-viewer-BvoWSuoZ.js} +3 -3
- package/dist/web/assets/{process-table-BGRJNlpn.js → process-table-BYwKigiD.js} +1 -1
- package/dist/web/assets/{project-store--nvQiy75.js → project-store-BuY7rxn_.js} +1 -1
- package/dist/web/assets/{pug-Cw74sKsE.js → pug-CvTwVVKV.js} +1 -1
- package/dist/web/assets/{qml-BXD94Eqz.js → qml-CPSoZ1-B.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-BLo4BOTD.js +1 -0
- package/dist/web/assets/{razor-DZ0-Ge0S.js → razor-BUA1YKtF.js} +1 -1
- package/dist/web/assets/{rst-Dtdh0QUj.js → rst-BJsq7ISz.js} +1 -1
- package/dist/web/assets/{ruby-BC51BW2Q.js → ruby-DSDMeYf5.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Sd3KyjN4.js → run-in-terminal-C9xpskFn.js} +1 -1
- package/dist/web/assets/{sas-BN8i4qOU.js → sas-B1nJOjbN.js} +1 -1
- package/dist/web/assets/{scroll-area-0gljZ6lF.js → scroll-area-DBcG1fQo.js} +1 -1
- package/dist/web/assets/{scss-H-QtA7Rz.js → scss-BZFyRiQn.js} +1 -1
- package/dist/web/assets/{send-to-chat-u6m8l-rH.js → send-to-chat-BIg70MUZ.js} +1 -1
- package/dist/web/assets/{settings-store-CHY6Kzin.js → settings-store-GV5pkkke.js} +2 -2
- package/dist/web/assets/{settings-tab-IY6K1MiZ.js → settings-tab-BePvIcjd.js} +1 -1
- package/dist/web/assets/settings-tab-Br-lJSPa.js +1 -0
- package/dist/web/assets/{shaderlab-CalNsj_x.js → shaderlab-laINRXBo.js} +1 -1
- package/dist/web/assets/{shellsession-BFrcoR8x.js → shellsession-BpOOvWVR.js} +1 -1
- package/dist/web/assets/{soy-CNbku7vP.js → soy-BW4d91Sa.js} +1 -1
- package/dist/web/assets/{sparql-BN_orcVp.js → sparql-BsvvyoHn.js} +1 -1
- package/dist/web/assets/{sql-query-editor-C6pC3I8l.js → sql-query-editor-qQce5QJ6.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DTr5PzUb.js → sqlite-viewer-yhc0YNvu.js} +1 -1
- package/dist/web/assets/{stata-C6a5Ijzq.js → stata-K-dZInUt.js} +1 -1
- package/dist/web/assets/{surrealql-ClplI_HY.js → surrealql-CpNp2sgf.js} +1 -1
- package/dist/web/assets/{svelte-DNm-UNXB.js → svelte-BG6iBA8R.js} +1 -1
- package/dist/web/assets/{switch-DKvKya9y.js → switch-_NcrktLe.js} +1 -1
- package/dist/web/assets/system-monitor-body-D8Z5QXBB.js +2 -0
- package/dist/web/assets/system-monitor-tab-BclYXFTe.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-BUqRsPK2.js → system-monitor-window-content-CaOhD0AU.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-BnqGPu8a.js → tab-host-window-content-f_Rc5rJK.js} +1 -1
- package/dist/web/assets/{tab-store-CJuJRapl.js → tab-store-tFnyPmm4.js} +1 -1
- package/dist/web/assets/team-member-sheet-QDHoYCs0.js +2 -0
- package/dist/web/assets/{team-member-window-content-T4Q5JI-G.js → team-member-window-content-B1jkKkDi.js} +1 -1
- package/dist/web/assets/{templ-CSdqVobu.js → templ-gxNKXmt3.js} +1 -1
- package/dist/web/assets/{terminal-tab-Bb2rZegQ.js → terminal-tab-UvwM0sty.js} +3 -3
- package/dist/web/assets/{tex-DzSJ4kcZ.js → tex-C0ON27g5.js} +1 -1
- package/dist/web/assets/{tool-cards-lhqhrDTT.js → tool-cards-jB7GT-wj.js} +4 -4
- package/dist/web/assets/treemap-KZPCXAKY-dyrE6SW2.js +1 -0
- package/dist/web/assets/{ts-tags-C28w3bTg.js → ts-tags-BX_CTVgQ.js} +1 -1
- package/dist/web/assets/{twig-i5jY3QXB.js → twig-CwL7XnU0.js} +1 -1
- package/dist/web/assets/{use-blob-url-Dsq9OjTh.js → use-blob-url-DcRcjNTE.js} +1 -1
- package/dist/web/assets/use-coarse-long-press-BmI_GVW5.js +1 -0
- package/dist/web/assets/{use-monaco-theme-BZvlsoBv.js → use-monaco-theme--9tOOkDA.js} +1 -1
- package/dist/web/assets/{use-open-team-member-DkiEjFYE.js → use-open-team-member-1hRsHLKg.js} +1 -1
- package/dist/web/assets/{use-websocket-B8R50f7f.js → use-websocket-rxJLbFqy.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-By4AVvJY.js → vendor-mermaid-CHTa6D69.js} +1 -1
- package/dist/web/assets/video-preview-DMhnf2Gi.js +1 -0
- package/dist/web/assets/{vue-Ceyet_lv.js → vue-Bykm6wSS.js} +1 -1
- package/dist/web/assets/{vue-html-GuU7g_hg.js → vue-html-CETdK_iV.js} +1 -1
- package/dist/web/assets/{vue-vine-CxXoaJr2.js → vue-vine-DiJb6fO5.js} +1 -1
- package/dist/web/assets/{window-store-CsEW6jiE.js → window-store-CQ1lghvv.js} +1 -1
- package/dist/web/assets/{xml-CbqOVhkP.js → xml-BNw8J5YW.js} +1 -1
- package/dist/web/assets/{xsl-KfAmCauy.js → xsl-CsG9L31b.js} +1 -1
- package/dist/web/index.html +46 -45
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +309 -0
- package/docs/architecture/data-and-storage.md +356 -0
- package/docs/architecture/extensions.md +496 -0
- package/docs/architecture/integrations.md +214 -0
- package/docs/architecture/workspace-and-ui.md +391 -0
- package/docs/claude-agent-sdk-reference.md +1 -1
- package/docs/code-patterns.md +361 -0
- package/docs/code-standards.md +8 -358
- package/docs/codebase-subsystems.md +364 -0
- package/docs/codebase-summary.md +34 -379
- package/docs/deployment-guide.md +280 -490
- package/docs/design-guidelines.md +1 -1
- package/docs/lessons-learned.md +27 -10
- package/docs/project-changelog.md +18 -806
- package/docs/project-overview-pdr.md +117 -111
- package/docs/project-roadmap.md +89 -133
- package/docs/system-architecture.md +77 -1826
- package/package.json +2 -2
- package/src/cli/commands/autostart.ts +2 -2
- package/src/cli/commands/config-cmd.ts +12 -3
- package/src/cli/commands/status.ts +3 -2
- package/src/cli/commands/stop.ts +32 -4
- package/src/server/index.ts +17 -3
- package/src/server/routes/chat-unread.ts +36 -0
- package/src/server/routes/named-tunnel.ts +132 -0
- package/src/server/routes/tunnel.ts +13 -0
- package/src/server/ws/chat.ts +5 -2
- package/src/services/autostart-register.ts +47 -29
- package/src/services/cloud.service.ts +26 -1
- package/src/services/config-secret-keys.ts +48 -0
- package/src/services/config.service.ts +1 -1
- package/src/services/db.service.ts +62 -12
- package/src/services/extension-rpc-handlers.ts +9 -1
- package/src/services/file-watcher/watch-tree.ts +67 -33
- package/src/services/file-watcher.service.ts +28 -10
- package/src/services/fs-credential-path-guard.ts +103 -0
- package/src/services/fs-path-guard.service.ts +14 -66
- package/src/services/named-tunnel/cloudflare-dns-api.ts +40 -0
- package/src/services/named-tunnel/cloudflare-zone-api.ts +28 -0
- package/src/services/named-tunnel/cloudflared-cert.ts +95 -0
- package/src/services/named-tunnel/cloudflared-exec.ts +50 -0
- package/src/services/named-tunnel/cloudflared-login-helpers.ts +98 -0
- package/src/services/named-tunnel/cloudflared-login.service.ts +182 -0
- package/src/services/named-tunnel/hostname-rules.ts +58 -0
- package/src/services/named-tunnel/login-output-parser.ts +36 -0
- package/src/services/named-tunnel/named-tunnel-args.ts +110 -0
- package/src/services/named-tunnel/named-tunnel-config.ts +67 -0
- package/src/services/named-tunnel/named-tunnel-probe-state.ts +95 -0
- package/src/services/named-tunnel/named-tunnel-readiness.ts +59 -0
- package/src/services/named-tunnel/named-tunnel-runtime.ts +42 -0
- package/src/services/named-tunnel/named-tunnel-setup-confirm.ts +98 -0
- package/src/services/named-tunnel/named-tunnel-setup.service.ts +191 -0
- package/src/services/supervisor-state.ts +67 -2
- package/src/services/supervisor.ts +454 -157
- package/src/services/tunnel-registry-parse.ts +27 -0
- package/src/services/tunnel-registry.service.ts +2 -15
- package/src/services/tunnel.service.ts +12 -3
- package/src/types/config.ts +31 -0
- package/src/web/app.tsx +5 -0
- package/src/web/components/chat/chat-history-bar.tsx +95 -98
- package/src/web/components/chat/chat-toolbar-overflow-menu.tsx +57 -0
- package/src/web/components/chat/session-debug-dialog.tsx +35 -27
- package/src/web/components/layout/cloud-share-named-tunnel-row.tsx +111 -0
- package/src/web/components/layout/cloud-share-popover.tsx +56 -14
- package/src/web/components/shared/connection-lost-overlay.tsx +44 -5
- package/src/web/components/tunnels/named-tunnel/hostname-validation.ts +14 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +96 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-hostname-field.tsx +51 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-login-step.tsx +105 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +159 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-content.tsx +197 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-setup-popup.tsx +57 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-step-reducer.ts +191 -0
- package/src/web/components/tunnels/named-tunnel/use-named-tunnel-setup.ts +212 -0
- package/src/web/components/tunnels/tunnel-manager-tab.tsx +4 -0
- package/src/web/hooks/use-global-events.ts +3 -1
- package/src/web/lib/api-named-tunnel.ts +54 -0
- package/src/web/lib/last-known-local-url.ts +23 -0
- package/src/web/stores/notification-store.ts +20 -1
- package/dist/web/assets/api-client-DTZEgjBQ.js +0 -1
- package/dist/web/assets/api-settings-kd1j4iR8.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-8dCMT9r7.js +0 -1
- package/dist/web/assets/chat-tab-E3bf_V8X.js +0 -15
- package/dist/web/assets/code-editor-BUlM2gXk.js +0 -10
- package/dist/web/assets/explorer-body-9aBmyVpZ.js +0 -2
- package/dist/web/assets/explorer-window-content-DRtqoSPA.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-2e_8dfbs.js +0 -1
- package/dist/web/assets/glide-data-grid-C4_zIneN.js +0 -138
- package/dist/web/assets/index-BImUaigO.js +0 -52
- package/dist/web/assets/index-CSfi6DSU.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BZi1Gha1.js +0 -1
- package/dist/web/assets/keybindings-store-CzV2sPls.js +0 -1
- package/dist/web/assets/markdown-renderer-CcbR33VG.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-SIwQXlwA.js +0 -1
- package/dist/web/assets/notification-store-DV5QtUTg.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-o5htEcin.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BXAIFSAD.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Dbt2Aebl.js +0 -1
- package/dist/web/assets/settings-tab-Dic6TKkU.js +0 -1
- package/dist/web/assets/system-monitor-body-iwGiCns9.js +0 -2
- package/dist/web/assets/system-monitor-tab-DdXQhEel.js +0 -1
- package/dist/web/assets/team-member-sheet-LkAFSCvb.js +0 -2
- package/dist/web/assets/treemap-KZPCXAKY-DUbnl9dz.js +0 -1
- package/dist/web/assets/video-preview-m0-3p7QZ.js +0 -1
- /package/dist/web/assets/{abap-C6nB04tn.js → abap-DGlrX_8G.js} +0 -0
- /package/dist/web/assets/{actionscript-3-Vx4rw2sC.js → actionscript-3-CUqRtUJE.js} +0 -0
- /package/dist/web/assets/{ada-Ccd8vM20.js → ada-CY3Spa33.js} +0 -0
- /package/dist/web/assets/{andromeeda-DnBX5xMg.js → andromeeda-Lxs2TJn7.js} +0 -0
- /package/dist/web/assets/{apache-B-9_sx0g.js → apache-DfxgGdgM.js} +0 -0
- /package/dist/web/assets/{apex-_wPVBYoL.js → apex-QVDF1dnI.js} +0 -0
- /package/dist/web/assets/{api-client-aQb5hHE5.js → api-client-oY1tC3tM.js} +0 -0
- /package/dist/web/assets/{applescript-CcvD-qr0.js → applescript-pwJ8QXh-.js} +0 -0
- /package/dist/web/assets/{apply-theme-BeLRfsSu.js → apply-theme-B_bMVL6a.js} +0 -0
- /package/dist/web/assets/{ara-C40Ztoms.js → ara-mPjeX5YX.js} +0 -0
- /package/dist/web/assets/{asciidoc-CybYkJV4.js → asciidoc-Bf7LsDkI.js} +0 -0
- /package/dist/web/assets/{asm-9eOKwzR0.js → asm-rRKC1OUx.js} +0 -0
- /package/dist/web/assets/{aurora-x-B5ZH4t7t.js → aurora-x-DwQr0DkG.js} +0 -0
- /package/dist/web/assets/{awk-Dqilob-2.js → awk-Bpt768fN.js} +0 -0
- /package/dist/web/assets/{ayu-dark-CPgRhf5r.js → ayu-dark-D9lwQHMs.js} +0 -0
- /package/dist/web/assets/{ayu-light-Do06Q52f.js → ayu-light-B4-ptrU5.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-_BhxcrxY.js → ayu-mirage-DAyRzH5Q.js} +0 -0
- /package/dist/web/assets/{ballerina-DDwK54QW.js → ballerina-Cgdpj1HC.js} +0 -0
- /package/dist/web/assets/{bat-BGyGlzGk.js → bat-4FrN36S2.js} +0 -0
- /package/dist/web/assets/{beancount-CRWBdapI.js → beancount-DkcxQER3.js} +0 -0
- /package/dist/web/assets/{berry-DbSFn2gO.js → berry-Dnuut-Ml.js} +0 -0
- /package/dist/web/assets/{bibtex-QpsV9gm9.js → bibtex-CCDwmbuA.js} +0 -0
- /package/dist/web/assets/{bicep-CxS8CUve.js → bicep-BC0ryoGM.js} +0 -0
- /package/dist/web/assets/{bird2-CuZhQzYR.js → bird2-C50V_A-0.js} +0 -0
- /package/dist/web/assets/{c-9bnDqE3g.js → c-Ddorhq0U.js} +0 -0
- /package/dist/web/assets/{c3-JMZTrOhA.js → c3-DdIf5r9E.js} +0 -0
- /package/dist/web/assets/{cadence-DsDNayJS.js → cadence-s9pd1Wng.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-BJfpynya.js → can-open-in-ppm-C6hZV1OP.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-DYIKXXBe.js → catppuccin-frappe-CzJNTyu-.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-IYYOnUJ8.js → catppuccin-latte-BNpt-m7s.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-BrJ_7mfS.js → catppuccin-macchiato-FYGkfsJ5.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-gzSu7v7-.js → catppuccin-mocha-it2g5D1q.js} +0 -0
- /package/dist/web/assets/{clarity-DLzsC0Hb.js → clarity-BVFBTjMe.js} +0 -0
- /package/dist/web/assets/{clipboard-7fw0Shic.js → clipboard-BWznymz7.js} +0 -0
- /package/dist/web/assets/{clojure-DS1BvoTt.js → clojure-BSYvXMI8.js} +0 -0
- /package/dist/web/assets/{cmake-CDvsJwYp.js → cmake-DGEqH-Np.js} +0 -0
- /package/dist/web/assets/{codeowners-BjsYiflJ.js → codeowners-D8FCOxWg.js} +0 -0
- /package/dist/web/assets/{codeql-CG-7ARat.js → codeql-COV0eluG.js} +0 -0
- /package/dist/web/assets/{common-lisp-DSO1Lbh9.js → common-lisp-xHnZUlgt.js} +0 -0
- /package/dist/web/assets/{coq-DYdh3oP_.js → coq-Cs_2i3Xg.js} +0 -0
- /package/dist/web/assets/{csharp-DxEBz5s0.js → csharp-B1ONB4PT.js} +0 -0
- /package/dist/web/assets/{css-dal3Q-8C.js → css-BgGQbvrZ.js} +0 -0
- /package/dist/web/assets/{csv-CpqjMGiA.js → csv-CNHmQBzs.js} +0 -0
- /package/dist/web/assets/{csv-parser-3umBA4NY.js → csv-parser-DWR24J17.js} +0 -0
- /package/dist/web/assets/{cue-DinoJ_5k.js → cue-ZXAKzwIy.js} +0 -0
- /package/dist/web/assets/{cypher-DzxqmkUW.js → cypher-4PL5bmT5.js} +0 -0
- /package/dist/web/assets/{d-BxNwJuEC.js → d-CEsV3RT2.js} +0 -0
- /package/dist/web/assets/{dark-plus-Clmv3V-0.js → dark-plus-NtFnYSDz.js} +0 -0
- /package/dist/web/assets/{dart-BWVtXD35.js → dart-CKHn4mDf.js} +0 -0
- /package/dist/web/assets/{data-grid-types-B4n51Fck.js → data-grid-types-CxU8oo-j.js} +0 -0
- /package/dist/web/assets/{dax-DsImuWgJ.js → dax-Cmkj1I_M.js} +0 -0
- /package/dist/web/assets/{desktop-B0jJuG2r.js → desktop-DJLalvPQ.js} +0 -0
- /package/dist/web/assets/{diff-CWqAAxoE.js → diff-BnS2wpj6.js} +0 -0
- /package/dist/web/assets/{dist-C0eNNfbt.js → dist-CuIdRyUB.js} +0 -0
- /package/dist/web/assets/{dist-B-EXdSYE.js → dist-uYjs3Wv6.js} +0 -0
- /package/dist/web/assets/{docker-CYFCJdIL.js → docker-CovNy7ml.js} +0 -0
- /package/dist/web/assets/{dotenv-CKDUjURg.js → dotenv-Gh4w3I6B.js} +0 -0
- /package/dist/web/assets/{dracula-CSoPspHE.js → dracula-NF0tAHXl.js} +0 -0
- /package/dist/web/assets/{dracula-soft-BuFjw5L1.js → dracula-soft-Cj_qBurK.js} +0 -0
- /package/dist/web/assets/{dream-maker-B6PdDRZu.js → dream-maker-CRuZPcl7.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CMcXvD0G.js → emacs-lisp-BACmC9qj.js} +0 -0
- /package/dist/web/assets/{everforest-dark-cU8-wYzi.js → everforest-dark-Dd9av4QG.js} +0 -0
- /package/dist/web/assets/{everforest-light-KhorllqK.js → everforest-light-C-jsss-B.js} +0 -0
- /package/dist/web/assets/{external-link-B8BynhvY.js → external-link-B6Hl0U4X.js} +0 -0
- /package/dist/web/assets/{fennel-BCELZK87.js → fennel-BJA9XnL8.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-CZ4DiR75.js → file-exclamation-point-JzhTU2TX.js} +0 -0
- /package/dist/web/assets/{fish-BQc0y_jy.js → fish-CKlhNSc2.js} +0 -0
- /package/dist/web/assets/{fluent-BTINnwF6.js → fluent-wfglRk5D.js} +0 -0
- /package/dist/web/assets/{folder-symlink-B8XrtvQK.js → folder-symlink-Dx1hOk8X.js} +0 -0
- /package/dist/web/assets/{format-bytes-WVcFxJuT.js → format-bytes-utwHMTry.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BQim9wDz.js → fortran-free-form-BlXCdid3.js} +0 -0
- /package/dist/web/assets/{gdscript-03w5u8un.js → gdscript-BwAVG6Et.js} +0 -0
- /package/dist/web/assets/{gdshader-Dp2daNzU.js → gdshader-DPc_T7ib.js} +0 -0
- /package/dist/web/assets/{genie-CoNcjPiD.js → genie-B-zZSLCX.js} +0 -0
- /package/dist/web/assets/{gherkin-NrPOSPQL.js → gherkin-CJ0ajpGs.js} +0 -0
- /package/dist/web/assets/{github-dark-z4RE9ZqA.js → github-dark-Dz8ZkzSP.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DOKw-1cI.js → github-dark-default-CNUYo_kp.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-DJRNMkEA.js → github-dark-dimmed-241z_Ycx.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-Bj7jJ_Gs.js → github-dark-high-contrast-7r6yZZYH.js} +0 -0
- /package/dist/web/assets/{github-light-B4k_yMFu.js → github-light-C8gmZn-D.js} +0 -0
- /package/dist/web/assets/{github-light-default-BHc-Uzhs.js → github-light-default-GAEgq1eA.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-CHisIywL.js → github-light-high-contrast-3z91352E.js} +0 -0
- /package/dist/web/assets/{gleam-CfbypZ4q.js → gleam-C7vxW5zd.js} +0 -0
- /package/dist/web/assets/{gn-YoAjyhJa.js → gn-CyJIXwHP.js} +0 -0
- /package/dist/web/assets/{gnuplot-xVkc1f1c.js → gnuplot-CqLbGgPd.js} +0 -0
- /package/dist/web/assets/{go-GPCwP3Gu.js → go-DdDcK9aX.js} +0 -0
- /package/dist/web/assets/{groovy-Crh4Yww9.js → groovy-UYHggEqH.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-BluUFuDS.js → gruvbox-dark-hard-B4ierkJn.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-Cxk1FWQp.js → gruvbox-dark-medium-DYwkDSZ6.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-CHXMiFmj.js → gruvbox-dark-soft-CVL4P-Lz.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-B6eKPjAD.js → gruvbox-light-hard-BJ0LewUZ.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-pDwMm1ut.js → gruvbox-light-medium-CaqqjeGJ.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-Cxu6kHqK.js → gruvbox-light-soft-ByKMoVNr.js} +0 -0
- /package/dist/web/assets/{haskell-C6JKHjbK.js → haskell-BilYRVPp.js} +0 -0
- /package/dist/web/assets/{haxe-BuBNdF0a.js → haxe-Brs9Bj1s.js} +0 -0
- /package/dist/web/assets/{hcl-DZpDdGj2.js → hcl-ClrZpol-.js} +0 -0
- /package/dist/web/assets/{hjson-CBkhYRuo.js → hjson-9PvHnYMX.js} +0 -0
- /package/dist/web/assets/{hlsl-C5nMxnp9.js → hlsl-BbBk4vqV.js} +0 -0
- /package/dist/web/assets/{horizon-VAFvA6R6.js → horizon-E5Py0cny.js} +0 -0
- /package/dist/web/assets/{horizon-bright-BC0xn7T3.js → horizon-bright-DVCsNGEk.js} +0 -0
- /package/dist/web/assets/{houston-BxrUivmz.js → houston-JdFEax9Y.js} +0 -0
- /package/dist/web/assets/{hy-DLtOXoaH.js → hy-W_W3tLKT.js} +0 -0
- /package/dist/web/assets/{image-gallery-Gnf7TdCR.js → image-gallery-5IURd6db.js} +0 -0
- /package/dist/web/assets/{imba-BdpFXM2W.js → imba-CfAqJz7T.js} +0 -0
- /package/dist/web/assets/{ini-BInJO_T0.js → ini-C_3gvKWs.js} +0 -0
- /package/dist/web/assets/{java-Dxuddnkb.js → java-CKqtXZ-j.js} +0 -0
- /package/dist/web/assets/{javascript-CVamJfLY.js → javascript-C7gSEfae.js} +0 -0
- /package/dist/web/assets/{json-BLZ0ozYk.js → json-BoN6e11T.js} +0 -0
- /package/dist/web/assets/{json5-BnXzmMKs.js → json5-CgE7C8HW.js} +0 -0
- /package/dist/web/assets/{jsonc-CSgkSga7.js → jsonc-DjyoattY.js} +0 -0
- /package/dist/web/assets/{jsonl-CY5W2nF2.js → jsonl-lI82wGSw.js} +0 -0
- /package/dist/web/assets/{jsonnet-CEJYH3sF.js → jsonnet-CszEI8va.js} +0 -0
- /package/dist/web/assets/{jssm-CsJc7Uhk.js → jssm-CZXDlszb.js} +0 -0
- /package/dist/web/assets/{jsx-jCiRaHU7.js → jsx-BeRz0eEz.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-DfoA_GxX.js → kanagawa-dragon-BOmnUiyd.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-BOLYJpWd.js → kanagawa-lotus-ffU_P5w-.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-DfKlbT1G.js → kanagawa-wave-DKTJZrMu.js} +0 -0
- /package/dist/web/assets/{kdl-CmssIFTi.js → kdl-BWECJveH.js} +0 -0
- /package/dist/web/assets/{kotlin-BRsNgp6x.js → kotlin-DpVwBCSt.js} +0 -0
- /package/dist/web/assets/{kusto-T8X9jUab.js → kusto-BgVqjUZA.js} +0 -0
- /package/dist/web/assets/{laserwave-DkJMVJ8Z.js → laserwave-CEVCi8em.js} +0 -0
- /package/dist/web/assets/{lean-C65KGVPq.js → lean-CGyfRhLJ.js} +0 -0
- /package/dist/web/assets/{less-exIPdDlf.js → less-B0_H10Ku.js} +0 -0
- /package/dist/web/assets/{lib-mC1NZfuw.js → lib-hrC3bruJ.js} +0 -0
- /package/dist/web/assets/{libesm-DSutUtl1.js → libesm-DVpFmwJu.js} +0 -0
- /package/dist/web/assets/{light-plus-DiHUXAZt.js → light-plus-DqzMqPID.js} +0 -0
- /package/dist/web/assets/{llvm-CJBnEzFM.js → llvm-9ucnfNiF.js} +0 -0
- /package/dist/web/assets/{loader-circle-gSPVxfvH.js → loader-circle-Dpqcv0eS.js} +0 -0
- /package/dist/web/assets/{lock-Did-87UV.js → lock-DldbT31x.js} +0 -0
- /package/dist/web/assets/{log-DxMyUMuP.js → log-m6A2dOSj.js} +0 -0
- /package/dist/web/assets/{logo-CC9NKzca.js → logo-ByPcsC9P.js} +0 -0
- /package/dist/web/assets/{luau-Bw10I6zf.js → luau-DFCo8nBn.js} +0 -0
- /package/dist/web/assets/{make-C_6M90m6.js → make-C90w3MeA.js} +0 -0
- /package/dist/web/assets/{markdown-bR9_R-LF.js → markdown-DT0uywZG.js} +0 -0
- /package/dist/web/assets/{material-theme-BUx2Qujn.js → material-theme-BVUO93tJ.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-CRJrPgKm.js → material-theme-darker-xXgGDwnF.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-CUAYppPa.js → material-theme-lighter-BjStPzWB.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-DuINF1fg.js → material-theme-ocean-wFmSiKjy.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-W6_icGCj.js → material-theme-palenight-e-0CtRoe.js} +0 -0
- /package/dist/web/assets/{matlab-rfqn_b3z.js → matlab-BNaNQAUH.js} +0 -0
- /package/dist/web/assets/{mdx-Bh1B_YOJ.js → mdx-DfpVWAqO.js} +0 -0
- /package/dist/web/assets/{mermaid-Dxf6Ya2k.js → mermaid-Bpq7wFCP.js} +0 -0
- /package/dist/web/assets/{min-dark-CivZ2L0h.js → min-dark-CL2Y0WH6.js} +0 -0
- /package/dist/web/assets/{min-light-B1Ykqtma.js → min-light-DOgr_IdP.js} +0 -0
- /package/dist/web/assets/{mipsasm-DynHaM0d.js → mipsasm-C9ie2zlB.js} +0 -0
- /package/dist/web/assets/{mojo-Clw5PbGH.js → mojo-Md2A2sJy.js} +0 -0
- /package/dist/web/assets/{monitor-Dv8vn_6O.js → monitor-4vWfqzuN.js} +0 -0
- /package/dist/web/assets/{monokai-DFIooFZT.js → monokai-nbXIEySB.js} +0 -0
- /package/dist/web/assets/{moonbit-DnBSZWGM.js → moonbit-CJ-T-s4A.js} +0 -0
- /package/dist/web/assets/{move-CCcL2PfL.js → move-DwEehLIK.js} +0 -0
- /package/dist/web/assets/{narrat-BDarg5Lk.js → narrat-CuF4sIKr.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-DrZOeCNW.js → nextflow-groovy-1dYhfPfq.js} +0 -0
- /package/dist/web/assets/{night-owl-BnZZCK7v.js → night-owl-gZS5mfzY.js} +0 -0
- /package/dist/web/assets/{night-owl-light-CEwBsVHS.js → night-owl-light-DDAster5.js} +0 -0
- /package/dist/web/assets/{nix-amCzOPMR.js → nix-BlbEYs2c.js} +0 -0
- /package/dist/web/assets/{nord-BIs2BC6q.js → nord-BRiPb8Vy.js} +0 -0
- /package/dist/web/assets/{nushell-Y02fo4yw.js → nushell-DgWicB1i.js} +0 -0
- /package/dist/web/assets/{objective-c-7xOZtWW1.js → objective-c-D8mPZ42Y.js} +0 -0
- /package/dist/web/assets/{objective-cpp-eVtjUvVB.js → objective-cpp-B0pNXzuj.js} +0 -0
- /package/dist/web/assets/{ocaml-Bi6ZwqG4.js → ocaml-lQd7aK5d.js} +0 -0
- /package/dist/web/assets/{odin-8xf1Y0S9.js → odin-CxtIOBjm.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-hVv-_y_b.js → one-dark-pro-T_UFnWfe.js} +0 -0
- /package/dist/web/assets/{one-light-CJJ3MtHt.js → one-light-LBVqwyV-.js} +0 -0
- /package/dist/web/assets/{openscad-B2HcywLk.js → openscad-C5dnS6J7.js} +0 -0
- /package/dist/web/assets/{pascal-C7fllV_7.js → pascal-CmWCWQML.js} +0 -0
- /package/dist/web/assets/{pkl-qvLC1IWP.js → pkl-BWWOtDQk.js} +0 -0
- /package/dist/web/assets/{plastic-BT1wHZv2.js → plastic-ekhupckF.js} +0 -0
- /package/dist/web/assets/{play-4MZi1Axp.js → play-CpCAoQRZ.js} +0 -0
- /package/dist/web/assets/{plsql-1MsmdmJD.js → plsql-BPsDHXx4.js} +0 -0
- /package/dist/web/assets/{plus-BhMZqFsN.js → plus-BR0qewJI.js} +0 -0
- /package/dist/web/assets/{po-BigskytD.js → po-BX2FiuW2.js} +0 -0
- /package/dist/web/assets/{poimandres-DuklniF7.js → poimandres-DzXOdZcN.js} +0 -0
- /package/dist/web/assets/{polar-e2pjN_sp.js → polar-DDRXgJzG.js} +0 -0
- /package/dist/web/assets/{portal-container-context-Cm5HipFr.js → portal-container-context-a399Hd9F.js} +0 -0
- /package/dist/web/assets/{postcss-DtAeYjWw.js → postcss-BHW6XBhx.js} +0 -0
- /package/dist/web/assets/{powerquery-Cr2PtehI.js → powerquery-B89c2NBb.js} +0 -0
- /package/dist/web/assets/{powershell-BxFTGLJ4.js → powershell-Dvxrd3Tu.js} +0 -0
- /package/dist/web/assets/{prisma-COWJiwdZ.js → prisma-lMK8EFLU.js} +0 -0
- /package/dist/web/assets/{prolog-DFvGKRx6.js → prolog-D0YrtMG4.js} +0 -0
- /package/dist/web/assets/{proto-CcK80lFt.js → proto-J0mM25aw.js} +0 -0
- /package/dist/web/assets/{puppet-BF60bZ4F.js → puppet-CUT0xkhh.js} +0 -0
- /package/dist/web/assets/{purescript-zL_69MzU.js → purescript-CoJWSGRp.js} +0 -0
- /package/dist/web/assets/{python-CIwKhfb_.js → python-BBrmuz6P.js} +0 -0
- /package/dist/web/assets/{qmldir-CXNe04Lo.js → qmldir-CUtzs4Q7.js} +0 -0
- /package/dist/web/assets/{qss-C8lAWe6G.js → qss-Dm1RZ8aL.js} +0 -0
- /package/dist/web/assets/{r-CL5ZVL3_.js → r-BGP7qZTe.js} +0 -0
- /package/dist/web/assets/{racket-BBfh0uh0.js → racket-K_6U1lqJ.js} +0 -0
- /package/dist/web/assets/{raku-o6E8R5Q3.js → raku-bNSiZeW_.js} +0 -0
- /package/dist/web/assets/{react-CBTnT4sY.js → react-X4jUntEf.js} +0 -0
- /package/dist/web/assets/{red-GomRF2Xp.js → red-uVeIEF0f.js} +0 -0
- /package/dist/web/assets/{refresh-cw-B06yaBrW.js → refresh-cw-B9iv7csb.js} +0 -0
- /package/dist/web/assets/{reg-_ffDIvx9.js → reg-DSm9GWjh.js} +0 -0
- /package/dist/web/assets/{regexp-Bl5sF65W.js → regexp-CljRm_j_.js} +0 -0
- /package/dist/web/assets/{rel-DgMdqDuj.js → rel-d3JcopZg.js} +0 -0
- /package/dist/web/assets/{resolve-theme-CRt_7wpd.js → resolve-theme-DLKmm-lE.js} +0 -0
- /package/dist/web/assets/{riscv-8b3-Ye4X.js → riscv-CJmaELjT.js} +0 -0
- /package/dist/web/assets/{ron-DrmU1KHP.js → ron-D0g9B1rW.js} +0 -0
- /package/dist/web/assets/{rose-pine-C_-E2tF1.js → rose-pine-DOrD2BSH.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-DEfhNlFH.js → rose-pine-dawn-CzZQ-LCF.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-BdvX3gdn.js → rose-pine-moon-CdmsYp7C.js} +0 -0
- /package/dist/web/assets/{rosmsg-B6GhGpd5.js → rosmsg-CmDth6pO.js} +0 -0
- /package/dist/web/assets/{rotate-ccw-COXu_9oj.js → rotate-ccw-DnuPzJRE.js} +0 -0
- /package/dist/web/assets/{rust-DmC97M1-.js → rust-DGdP6jiR.js} +0 -0
- /package/dist/web/assets/{sass-B0tJOHLM.js → sass-Dkgt2FKZ.js} +0 -0
- /package/dist/web/assets/{save-CtIGOjmx.js → save-DxonD4y1.js} +0 -0
- /package/dist/web/assets/{scala-n8zaFOXv.js → scala-BQLTON6C.js} +0 -0
- /package/dist/web/assets/{scheme-BKD9ztBU.js → scheme-DKlxUGCi.js} +0 -0
- /package/dist/web/assets/{sdbl-D8gB5-rS.js → sdbl-C3iZJQeZ.js} +0 -0
- /package/dist/web/assets/{send-BlyNJAzy.js → send-B-t5Mz26.js} +0 -0
- /package/dist/web/assets/{shallow-DYo6goJ4.js → shallow-jwPzEunk.js} +0 -0
- /package/dist/web/assets/{shellscript-BfXlf_Ao.js → shellscript-BRu-ZQAx.js} +0 -0
- /package/dist/web/assets/{shield-check-B0iGUiOK.js → shield-check-CdTiK1j2.js} +0 -0
- /package/dist/web/assets/{shield-off-BNLWBhcq.js → shield-off-DuetkXKM.js} +0 -0
- /package/dist/web/assets/{slack-dark-CylEddse.js → slack-dark-qkm_u2zz.js} +0 -0
- /package/dist/web/assets/{slack-ochin-w8wg8vuC.js → slack-ochin-BP8_8e4m.js} +0 -0
- /package/dist/web/assets/{smalltalk-hiAXA64u.js → smalltalk-Bnz4gt07.js} +0 -0
- /package/dist/web/assets/{snazzy-light-BwlV3Xty.js → snazzy-light-fbPZg5io.js} +0 -0
- /package/dist/web/assets/{solarized-dark-vmNGdykz.js → solarized-dark-CM0vTRoi.js} +0 -0
- /package/dist/web/assets/{solarized-light-DGv_0JXq.js → solarized-light-C4_W9mvs.js} +0 -0
- /package/dist/web/assets/{solidity-Y7QSH0B1.js → solidity-Bz6v1PoD.js} +0 -0
- /package/dist/web/assets/{splunk-BtgpLjHi.js → splunk-DyNxu-0i.js} +0 -0
- /package/dist/web/assets/{sql-CRHYhOia.js → sql-DXRL1gw1.js} +0 -0
- /package/dist/web/assets/{ssh-config-DOqSs3au.js → ssh-config-2z1QHuOG.js} +0 -0
- /package/dist/web/assets/{stylus-crCAEaBT.js → stylus-D4I-VaeY.js} +0 -0
- /package/dist/web/assets/{swift-eE3rT7TC.js → swift-C_2G-tIc.js} +0 -0
- /package/dist/web/assets/{synthwave-84-BWy14LBX.js → synthwave-84-osBjChRP.js} +0 -0
- /package/dist/web/assets/{system-verilog-DC0-DZM4.js → system-verilog-zQP2qPNU.js} +0 -0
- /package/dist/web/assets/{systemd-to5p6N8I.js → systemd-UGfjRSBM.js} +0 -0
- /package/dist/web/assets/{table-Bbc783ko.js → table-FLU8ei8b.js} +0 -0
- /package/dist/web/assets/{talonscript-DoTcU8wm.js → talonscript-C1XrFPwo.js} +0 -0
- /package/dist/web/assets/{tasl-cNZvUs95.js → tasl-B5raVn43.js} +0 -0
- /package/dist/web/assets/{tcl-mZE8sZH8.js → tcl-3-vbeMIc.js} +0 -0
- /package/dist/web/assets/{terraform-CqHPpIfu.js → terraform-a8VXZDm9.js} +0 -0
- /package/dist/web/assets/{text-wrap-CBT4Gxtg.js → text-wrap-B05VGRa-.js} +0 -0
- /package/dist/web/assets/{tokyo-night-Tp2rTIZd.js → tokyo-night-4SRhf-Hz.js} +0 -0
- /package/dist/web/assets/{toml-CU_vscgn.js → toml-B4N0i8wT.js} +0 -0
- /package/dist/web/assets/{trash-2-Bk4yYlck.js → trash-2-CTXlTflF.js} +0 -0
- /package/dist/web/assets/{triangle-alert-B8EYecpe.js → triangle-alert-BRq0VAfd.js} +0 -0
- /package/dist/web/assets/{tsv-ik7iFB9D.js → tsv-DA5k9Lvw.js} +0 -0
- /package/dist/web/assets/{tsx-BMvvMKiv.js → tsx-D9SZ8owC.js} +0 -0
- /package/dist/web/assets/{turtle-DBqKCq9o.js → turtle-BTHTvyY_.js} +0 -0
- /package/dist/web/assets/{typescript-DXJDmMZZ.js → typescript-DYzV9p5L.js} +0 -0
- /package/dist/web/assets/{typespec-CBseCMtZ.js → typespec-BC15eh7F.js} +0 -0
- /package/dist/web/assets/{typst-4oY25U9y.js → typst-BJUqzA6D.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-D1weanbj.js → use-is-mobile-DFWViA4S.js} +0 -0
- /package/dist/web/assets/{utils-BNRH507E.js → utils-C9OjCrou.js} +0 -0
- /package/dist/web/assets/{v-BXaZ_1oy.js → v-50Lps6oA.js} +0 -0
- /package/dist/web/assets/{vala-BImWJE7z.js → vala-8wiUvvrD.js} +0 -0
- /package/dist/web/assets/{vb-CLfj2_sy.js → vb-BlgPLt6B.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-_yyu5n32.js → vendor-xterm-BZOrwF_F.js} +0 -0
- /package/dist/web/assets/{verilog-BWa8Ed2b.js → verilog-CD9pVW5p.js} +0 -0
- /package/dist/web/assets/{vesper-BGj1cg6m.js → vesper-BrivW4PT.js} +0 -0
- /package/dist/web/assets/{vhdl-mByK7mlx.js → vhdl-Bunxfa7N.js} +0 -0
- /package/dist/web/assets/{viml-CugmlW5g.js → viml-DndUb2qM.js} +0 -0
- /package/dist/web/assets/{vitesse-black-vPdpy9TL.js → vitesse-black-BurZSkxW.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BuQK3dUh.js → vitesse-dark-8UShspzC.js} +0 -0
- /package/dist/web/assets/{vitesse-light-B3nz1XL9.js → vitesse-light-DghCAZ90.js} +0 -0
- /package/dist/web/assets/{vyper-DH_GTIHG.js → vyper-b-Clr-8u.js} +0 -0
- /package/dist/web/assets/{wasm-DfVbVJlW.js → wasm-C5v2T06Y.js} +0 -0
- /package/dist/web/assets/{wasm-C-LF9GjX.js → wasm-CpcKStkw.js} +0 -0
- /package/dist/web/assets/{wenyan-CzQkHyHx.js → wenyan-Dz2srLZR.js} +0 -0
- /package/dist/web/assets/{wgsl-Du_M1DW9.js → wgsl-CeD1-dRb.js} +0 -0
- /package/dist/web/assets/{wikitext-BWN_w5S7.js → wikitext-CyeS-9FH.js} +0 -0
- /package/dist/web/assets/{wit-BR13v5HD.js → wit-Kr-q-qBQ.js} +0 -0
- /package/dist/web/assets/{wolfram-BoXxp_Xl.js → wolfram-BKm8bmln.js} +0 -0
- /package/dist/web/assets/{x-nhriOX9o.js → x-YDu4m2Iq.js} +0 -0
- /package/dist/web/assets/{yaml-DpR_EF-d.js → yaml-2XSxomjM.js} +0 -0
- /package/dist/web/assets/{zenscript-C-EdUtUd.js → zenscript-CKIS34vf.js} +0 -0
- /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
|
-
│ │
|
|
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
|
|
200
|
-
| **DbService** | SQLite persistence (
|
|
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:
|
|
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:
|
|
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
|
|
868
|
-
-
|
|
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/
|
|
396
|
+
macOS / Linux / Windows Host
|
|
2022
397
|
├── ppm (compiled binary)
|
|
2023
398
|
│ └── Embeds: server code, frontend assets
|
|
2024
|
-
|
|
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
|
-
|
|
2038
|
-
|
|
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 (
|
|
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
|
-
###
|
|
2145
|
-
|
|
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
|
-
|