@hienlh/ppm 0.17.16 → 0.17.17
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/CHANGELOG.md +14 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +5 -1
- package/dist/web/assets/{ai-resource-editor-CsossqDO.js → ai-resource-editor-37UxM2lK.js} +1 -1
- package/dist/web/assets/{angular-html-CkxY2iBQ.js → angular-html-2g8YGiJY.js} +1 -1
- package/dist/web/assets/{angular-ts-BePeGkkC.js → angular-ts-BwSmoLfh.js} +1 -1
- package/dist/web/assets/{api-settings-DAk7D-NP.js → api-settings-BJTjIG4U.js} +1 -1
- package/dist/web/assets/{api-themes-84MT-XqR.js → api-themes-DuLpbQFV.js} +1 -1
- package/dist/web/assets/{apl-DBS7PWvB.js → apl-B44SMKHp.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-gLlhsjM9.js +1 -0
- package/dist/web/assets/{astro-Clbk1k4h.js → astro-E5620wek.js} +1 -1
- package/dist/web/assets/{audio-preview-DIkokvPM.js → audio-preview-BG8uEB67.js} +1 -1
- package/dist/web/assets/{blade-BeBsBuGS.js → blade-D-VQWAHB.js} +1 -1
- package/dist/web/assets/{bsl-DYigT3JZ.js → bsl-Bq5qFzpU.js} +1 -1
- package/dist/web/assets/{cairo-BjpO78wI.js → cairo-Cn9-ZKRa.js} +1 -1
- package/dist/web/assets/chat-tab-6G5LulPM.js +13 -0
- package/dist/web/assets/{cobol-CpuUx69s.js → cobol-IM3E3iCH.js} +1 -1
- package/dist/web/assets/code-editor-BlBFI3jn.js +10 -0
- package/dist/web/assets/{coffee-FoFd2Cxo.js → coffee-24wG7ekn.js} +1 -1
- package/dist/web/assets/{conflict-editor-Dqm1bkyL.js → conflict-editor-CYZjqn46.js} +3 -3
- package/dist/web/assets/{cpp-CO_nPfdr.js → cpp-DH9Cqt4g.js} +1 -1
- package/dist/web/assets/{crystal-D-UygAAF.js → crystal-4nZYHhdi.js} +1 -1
- package/dist/web/assets/{csv-preview-CvP4xzug.js → csv-preview-YxR9adj_.js} +1 -1
- package/dist/web/assets/data-grid-overlay-editor-DxdOKRAY.js +1 -0
- package/dist/web/assets/data-grid-types-CzEQU9PJ.js +1 -0
- package/dist/web/assets/database-viewer-CrdTyPIN.js +1 -0
- package/dist/web/assets/dialog-BpkAWOy8.js +1 -0
- package/dist/web/assets/{diff-viewer-BfWixe_z.js → diff-viewer-DiYj_kRC.js} +1 -1
- package/dist/web/assets/dist-Bn3E6I4R.js +11 -0
- package/dist/web/assets/{docx-preview-CguUoTKe.js → docx-preview-Dsrm2Hgu.js} +1 -1
- package/dist/web/assets/{edge-BSzxzX11.js → edge-DTI9d2Qi.js} +1 -1
- package/dist/web/assets/{elixir-DH1-Mrep.js → elixir-Ys0wioho.js} +1 -1
- package/dist/web/assets/{elm-B0ln5J70.js → elm-Cihc9TAz.js} +1 -1
- package/dist/web/assets/{erb-GIz6NnlZ.js → erb-BwYBWK2-.js} +1 -1
- package/dist/web/assets/{erlang-B8MgYhib.js → erlang-CzlD_Joo.js} +1 -1
- package/dist/web/assets/esm-JLkfN0k-.js +2 -0
- package/dist/web/assets/{extension-webview-DTpFt1eL.js → extension-webview-B8ph2gI9.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-D_-TrK6U.js → fortran-fixed-form-CT90EtSp.js} +1 -1
- package/dist/web/assets/{fsharp-C9cLCk2b.js → fsharp-LasxezlU.js} +1 -1
- package/dist/web/assets/{gdresource-CsZ3lXwY.js → gdresource-XqgPdfiE.js} +1 -1
- package/dist/web/assets/{git-commit-xmq0KqrA.js → git-commit-CZBgPolw.js} +1 -1
- package/dist/web/assets/{git-log-panel-DUX7N5tN.js → git-log-panel-qIsOpM57.js} +1 -1
- package/dist/web/assets/{git-rebase-D3O-jsSX.js → git-rebase-C2VzKDB1.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-BKGq--Jw.js +1 -0
- package/dist/web/assets/glide-data-grid-BrQskOnP.js +138 -0
- package/dist/web/assets/{glimmer-js-g74gESrC.js → glimmer-js-DfjWcOLD.js} +1 -1
- package/dist/web/assets/{glimmer-ts-DF4aDWHT.js → glimmer-ts-DBldQATc.js} +1 -1
- package/dist/web/assets/{glsl-D5LmfW7l.js → glsl-CzNv1NPP.js} +1 -1
- package/dist/web/assets/{graphql-_KI4RuXL.js → graphql-DDyLtQb4.js} +1 -1
- package/dist/web/assets/group-chat-tab-DYgRs0zS.js +1 -0
- package/dist/web/assets/{hack-B1NaTC39.js → hack-CtoYNYkC.js} +1 -1
- package/dist/web/assets/{haml-DDFXadZ4.js → haml-CnBVap7G.js} +1 -1
- package/dist/web/assets/{handlebars-Bp-Jq3K9.js → handlebars-Dg51esUH.js} +1 -1
- package/dist/web/assets/{html-DyB9_5Tj.js → html-Ec2_evSy.js} +1 -1
- package/dist/web/assets/{html-derivative-CC60wi-3.js → html-derivative-Cy-F7ZqL.js} +1 -1
- package/dist/web/assets/{http-C-9qngH4.js → http-C236v8Tg.js} +1 -1
- package/dist/web/assets/{hurl-BtxgpNDs.js → hurl-0rMD2gPU.js} +1 -1
- package/dist/web/assets/{hxml-DOoOS9op.js → hxml-e5uS5jTD.js} +1 -1
- package/dist/web/assets/{image-preview-CZg6FQ3w.js → image-preview-B9EWGEFv.js} +1 -1
- package/dist/web/assets/index-BYFXiFsa.css +2 -0
- package/dist/web/assets/index-BiHrB3KU.js +50 -0
- package/dist/web/assets/info-3K5VOQVL-BOhGriBF.js +1 -0
- package/dist/web/assets/{jinja-cN-150ao.js → jinja-DybFmhfq.js} +1 -1
- package/dist/web/assets/{jison-CqjsqPq5.js → jison-hkHbixdZ.js} +1 -1
- package/dist/web/assets/{julia-C-I5m8W4.js → julia-QnxY6sRE.js} +1 -1
- package/dist/web/assets/{just-CgsSW9zs.js → just-1Rs-nm8g.js} +1 -1
- package/dist/web/assets/keybindings-store-CWi8IGEx.js +1 -0
- package/dist/web/assets/{latex-BFzR9N2d.js → latex-jEBQqA7u.js} +1 -1
- package/dist/web/assets/{liquid-BWfr-_iC.js → liquid-DfJaZpxG.js} +1 -1
- package/dist/web/assets/{lua-DsXVun6g.js → lua-ti0Jr6-C.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CiQkvWP_.js → markdown-renderer-B4rtk2Hq.js} +3 -3
- package/dist/web/assets/markdown-renderer-XIhxiUQq.js +1 -0
- package/dist/web/assets/{marko-DWBbo09n.js → marko-DNj4AhPg.js} +1 -1
- package/dist/web/assets/{mdc-CUMe9LOG.js → mdc-DPFwQFe4.js} +1 -1
- package/dist/web/assets/mobile-bottom-sheet--5_YEewV.js +1 -0
- package/dist/web/assets/{nextflow-CoYbADOm.js → nextflow-BUrFV5aB.js} +1 -1
- package/dist/web/assets/{nginx-z-JRZREw.js → nginx-BrW9NdOb.js} +1 -1
- package/dist/web/assets/{nim-Ci4LHyBm.js → nim-DS_6PF2p.js} +1 -1
- package/dist/web/assets/notification-store-DEIj7OrE.js +1 -0
- package/dist/web/assets/{number-overlay-editor-lnnuUpPI.js → number-overlay-editor-CReXWiQ1.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-CdLFnL8a.js +1 -0
- package/dist/web/assets/{panel-store-D6RV_ztx.js → panel-store-DnbT1c8L.js} +1 -1
- package/dist/web/assets/{pdf-preview-B6ZqANpw.js → pdf-preview-Zl3KICd5.js} +1 -1
- package/dist/web/assets/{perl-pbDimKOO.js → perl-ytm9vKO_.js} +1 -1
- package/dist/web/assets/{php-B003E5v7.js → php-DWTD1Usw.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-COC-h0zz.js +1 -0
- package/dist/web/assets/{trash-2-CbbD5QF1.js → plus-DcuJuDC3.js} +1 -1
- package/dist/web/assets/postgres-viewer-BTupZTmP.js +3 -0
- package/dist/web/assets/{project-store-Cekcjcom.js → project-store-Sbnqc41N.js} +1 -1
- package/dist/web/assets/{pug-AoueAWJo.js → pug-Bt7KL3_r.js} +1 -1
- package/dist/web/assets/{qml-CepxioQA.js → qml-Dexdon5J.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-CiCfdJNL.js +1 -0
- package/dist/web/assets/{razor-lIvU-Gu-.js → razor-DnYCSl0G.js} +1 -1
- package/dist/web/assets/{rst-Dn-dwElE.js → rst-BEFYzwF8.js} +1 -1
- package/dist/web/assets/{ruby-DmsD1Iz5.js → ruby-BPaXFEjN.js} +1 -1
- package/dist/web/assets/{sas-6IM2P4YP.js → sas-CywiNBp8.js} +1 -1
- package/dist/web/assets/{scss-RgYEZQqh.js → scss-CGk0L6hN.js} +1 -1
- package/dist/web/assets/{settings-store-BkSf4SPQ.js → settings-store-DkRZhj-g.js} +2 -2
- package/dist/web/assets/settings-tab-BF069B1K.js +1 -0
- package/dist/web/assets/{shaderlab-B0MNEwt4.js → shaderlab-Byjk0kH7.js} +1 -1
- package/dist/web/assets/{shellsession-F8S4LrQI.js → shellsession-DbA70QB6.js} +1 -1
- package/dist/web/assets/{soy-DDlMbX7u.js → soy-Bm97kWiA.js} +1 -1
- package/dist/web/assets/sparkles-B3DmG8PJ.js +1 -0
- package/dist/web/assets/{sparql-kthmGAAe.js → sparql-CawLnniT.js} +1 -1
- package/dist/web/assets/{sql-query-editor-BYNM0RRl.js → sql-query-editor-RwHY1p-a.js} +1 -1
- package/dist/web/assets/sqlite-viewer-3UxftoM7.js +1 -0
- package/dist/web/assets/{stata-Tfbew8La.js → stata-RifEDtNO.js} +1 -1
- package/dist/web/assets/{surrealql-C0LIGqRx.js → surrealql-DGRQa5Lm.js} +1 -1
- package/dist/web/assets/{svelte-CfTy8CGF.js → svelte-w9Hb12aM.js} +1 -1
- package/dist/web/assets/system-monitor-tab-Ck1cIP7g.js +1 -0
- package/dist/web/assets/{tab-store-CcLQfu8H.js → tab-store-vn0viMOP.js} +1 -1
- package/dist/web/assets/{templ-V5H1Sbfy.js → templ-BpUwjJXV.js} +1 -1
- package/dist/web/assets/{terminal-tab-CK3lX-0Y.js → terminal-tab-B9opPxft.js} +2 -2
- package/dist/web/assets/{tex-C3H3uMZf.js → tex-DJFSsuTD.js} +1 -1
- package/dist/web/assets/tool-cards-C7iLwehG.js +7 -0
- package/dist/web/assets/treemap-KZPCXAKY-CZwuSCtA.js +1 -0
- package/dist/web/assets/{ts-tags-7AXgG5YS.js → ts-tags-vp9Qvx2O.js} +1 -1
- package/dist/web/assets/{twig-BSyS38T3.js → twig-CoazxjRB.js} +1 -1
- package/dist/web/assets/{use-blob-url-CpyRr7Cx.js → use-blob-url-DdXgVqfz.js} +1 -1
- package/dist/web/assets/use-is-mobile-CE6AamG6.js +1 -0
- package/dist/web/assets/{use-monaco-theme-Jj2OxkAE.js → use-monaco-theme-jtYkroys.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-DGOXKrJC.js → vendor-mermaid-DPpYkrMf.js} +193 -193
- package/dist/web/assets/{video-preview-BOHxkwKG.js → video-preview-Ced_VIvq.js} +1 -1
- package/dist/web/assets/{vue-C4d_br9u.js → vue-CnshsiXd.js} +1 -1
- package/dist/web/assets/{vue-html-CGdoz4vW.js → vue-html-CZshynFp.js} +1 -1
- package/dist/web/assets/{vue-vine-CqNolQlN.js → vue-vine-C5gtp_T1.js} +1 -1
- package/dist/web/assets/{xml-UmCmu-Od.js → xml-BEGJasgj.js} +1 -1
- package/dist/web/assets/{xsl-DJG6USGh.js → xsl-DXpRLjYk.js} +1 -1
- package/dist/web/index.html +22 -20
- package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
- package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
- package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
- package/dist/web/sw.js +1 -1
- package/docs/journal/binary-self-upgrade-260724.md +66 -0
- package/nul/docs/claude-agent-sdk-reference.md +780 -0
- package/nul/docs/code-standards.md +1080 -0
- package/nul/docs/codebase-summary.md +916 -0
- package/nul/docs/deployment-guide.md +702 -0
- package/nul/docs/design-guidelines.md +767 -0
- package/nul/docs/extension-development-guide.md +629 -0
- package/nul/docs/journals/260414-1400-ext-git-graph-port-complete.md +147 -0
- package/nul/docs/journals/260414-1452-git-graph-faithful-port.md +144 -0
- package/nul/docs/journals/260414-1810-git-graph-ui-improvements-complete.md +261 -0
- package/nul/docs/journals/260414-2001-bundled-extensions.md +219 -0
- package/nul/docs/journals/260415-frontend-memory-optimization.md +73 -0
- package/nul/docs/lessons-learned.md +70 -0
- package/nul/docs/project-changelog.md +574 -0
- package/nul/docs/project-overview-pdr.md +205 -0
- package/nul/docs/project-roadmap.md +222 -0
- package/nul/docs/system-architecture.md +1961 -0
- package/package.json +1 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
- package/spike-group-channel.mjs +156 -0
- package/spike-group-chat.mjs +181 -0
- package/src/server/routes/group-chat.ts +118 -9
- package/src/services/group-chat/agent-runner.ts +15 -2
- package/src/services/group-chat/context-window.ts +15 -0
- package/src/services/group-chat/group-chat.service.ts +178 -79
- package/src/services/group-chat/group-chat.store.ts +30 -0
- package/src/services/group-chat/responder-router.ts +110 -0
- package/src/services/group-chat/transcript-archive.ts +18 -0
- package/src/services/group-chat/turn-engine.ts +183 -105
- package/src/services/jsonl-transcript-parser.ts +36 -0
- package/src/types/group-chat-ws.ts +2 -2
- package/src/types/group-chat.ts +33 -4
- package/src/web/components/group-chat/group-chat-tab.tsx +109 -14
- package/src/web/components/group-chat/group-create-dialog.tsx +10 -3
- package/src/web/components/group-chat/group-full-transcript-view.tsx +9 -8
- package/src/web/components/group-chat/group-member-edit-dialog.tsx +108 -0
- package/src/web/components/group-chat/group-member-roster.tsx +51 -23
- package/src/web/components/group-chat/group-message-item.tsx +45 -26
- package/src/web/components/group-chat/group-transcript-messages.tsx +126 -0
- package/src/web/components/layout/mobile-drawer-tab-bar.tsx +10 -0
- package/src/web/components/layout/nav-section-rail.tsx +8 -2
- package/src/web/components/layout/upgrade-button.tsx +33 -11
- package/src/web/hooks/use-group-chat.ts +8 -1
- package/src/web/lib/api-group-chat.ts +47 -4
- package/src/web/lib/sidebar-tabs/tab-registry.ts +3 -1
- package/dist/web/assets/architecture-PBZL5I3N-NAS99xy0.js +0 -1
- package/dist/web/assets/chat-tab-Bh6l253v.js +0 -18
- package/dist/web/assets/click-outside-container-DXZVmhaX.js +0 -1
- package/dist/web/assets/code-editor-B7F-d4DK.js +0 -10
- package/dist/web/assets/data-grid-overlay-editor-CUlFsD-D.js +0 -1
- package/dist/web/assets/database-viewer-5oNTPgjF.js +0 -1
- package/dist/web/assets/dist-BsziT-tR.js +0 -11
- package/dist/web/assets/esm-CjVeOTNQ.js +0 -2
- package/dist/web/assets/gitGraph-HDMCJU4V-B4I3L4Sf.js +0 -1
- package/dist/web/assets/glide-data-grid-7vB1SNne.js +0 -138
- package/dist/web/assets/group-chat-tab-CNECY-zg.js +0 -1
- package/dist/web/assets/index-D2G6kmTs.js +0 -50
- package/dist/web/assets/index-DCmrZnq0.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-CBbbOe3C.js +0 -1
- package/dist/web/assets/input-24Nf_edC.js +0 -1
- package/dist/web/assets/keybindings-store-D8Cppv4w.js +0 -1
- package/dist/web/assets/notification-store-DOGxkWih.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-fr8-QSTF.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-Biu7_Pbb.js +0 -1
- package/dist/web/assets/postgres-viewer-B23xTCSs.js +0 -3
- package/dist/web/assets/radar-KQ55EAFF-CMtNRpK6.js +0 -1
- package/dist/web/assets/settings-tab-D5hAA1bY.js +0 -1
- package/dist/web/assets/sparkles-2ii4lR7b.js +0 -1
- package/dist/web/assets/sqlite-viewer-DW9f2Hqj.js +0 -1
- package/dist/web/assets/system-monitor-tab-B5f4pgdX.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-ZYVFjoaN.js +0 -1
- package/dist/web/assets/use-websocket-DrDBgIWU.js +0 -1
- /package/dist/web/assets/{abap-BZ3J5hF3.js → abap-wT6fgqwv.js} +0 -0
- /package/dist/web/assets/{actionscript-3-CkyGfT-8.js → actionscript-3-CqcHpPYj.js} +0 -0
- /package/dist/web/assets/{ada-CxNLBoe7.js → ada-DWE3Hn4E.js} +0 -0
- /package/dist/web/assets/{andromeeda-BPN5rsDz.js → andromeeda-DpQ-y-Jt.js} +0 -0
- /package/dist/web/assets/{apache-C1F5wHL6.js → apache-LRTUP4r3.js} +0 -0
- /package/dist/web/assets/{apex-BaNixdO-.js → apex-DwVeH5_h.js} +0 -0
- /package/dist/web/assets/{api-client-Dvzcc_EO.js → api-client-BK4NPNoY.js} +0 -0
- /package/dist/web/assets/{applescript-Cl_pdcch.js → applescript-DyQiO9JU.js} +0 -0
- /package/dist/web/assets/{apply-theme-ORrEdHKT.js → apply-theme-DxKsrDLA.js} +0 -0
- /package/dist/web/assets/{ara-B6UAE7M2.js → ara-Cf-OWd2S.js} +0 -0
- /package/dist/web/assets/{asciidoc-D9c9Twki.js → asciidoc-gocbKcVK.js} +0 -0
- /package/dist/web/assets/{asm-CNmtv7Y9.js → asm-C3meb5HY.js} +0 -0
- /package/dist/web/assets/{aurora-x-C9z-NyzR.js → aurora-x-BYcXaCIg.js} +0 -0
- /package/dist/web/assets/{awk-BfLHEnKa.js → awk-BcT6nIdN.js} +0 -0
- /package/dist/web/assets/{ayu-dark-Bn7trkt_.js → ayu-dark-CLZPoccg.js} +0 -0
- /package/dist/web/assets/{ayu-light-Dpya9hPf.js → ayu-light-CTyRQdS2.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-Cc0jfABD.js → ayu-mirage-DjqfqZdo.js} +0 -0
- /package/dist/web/assets/{ballerina-BtiCmN7T.js → ballerina-BCARR-zK.js} +0 -0
- /package/dist/web/assets/{bat-tmFM76zR.js → bat-_yaRjWOb.js} +0 -0
- /package/dist/web/assets/{beancount-BjPqRrcH.js → beancount-BPESBMS9.js} +0 -0
- /package/dist/web/assets/{berry-D0SQBxN2.js → berry-BSLE-Sf8.js} +0 -0
- /package/dist/web/assets/{bibtex-CkjRWQKr.js → bibtex-CvUWWCVx.js} +0 -0
- /package/dist/web/assets/{bicep-Bhq28Cxc.js → bicep--vcqqALZ.js} +0 -0
- /package/dist/web/assets/{bird2-DFGInYb3.js → bird2-Clclqgh6.js} +0 -0
- /package/dist/web/assets/{c-CP8cB4k_.js → c-CSC2LwTp.js} +0 -0
- /package/dist/web/assets/{c3-oRytyeWo.js → c3-Cl1_jUnm.js} +0 -0
- /package/dist/web/assets/{cadence-Inf1rGb7.js → cadence-BwAD7vSC.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-DAzribQo.js → catppuccin-frappe-hr0HKoFX.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-B6Y_mCCQ.js → catppuccin-latte-DXEIYlFO.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-COLbYqVL.js → catppuccin-macchiato-B2zST7sA.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-DKWaML1B.js → catppuccin-mocha-CvrdnYv9.js} +0 -0
- /package/dist/web/assets/{chevron-right-BBc6ATtm.js → chevron-right-DeUue2aK.js} +0 -0
- /package/dist/web/assets/{circle-question-mark-CveWaCux.js → circle-question-mark-PaJgx-3d.js} +0 -0
- /package/dist/web/assets/{clarity-_ZLLrrsX.js → clarity-BeQd5TQD.js} +0 -0
- /package/dist/web/assets/{clojure-DWGVCg_P.js → clojure-Csdo0vb_.js} +0 -0
- /package/dist/web/assets/{cmake-Cwh3mug7.js → cmake-Cf0q7ddg.js} +0 -0
- /package/dist/web/assets/{code-7ZAlTvsm.js → code-TekEnDFM.js} +0 -0
- /package/dist/web/assets/{codeowners-BdV6CFYi.js → codeowners-CUEzlC7R.js} +0 -0
- /package/dist/web/assets/{codeql-NvupDIDf.js → codeql-DxHyogQt.js} +0 -0
- /package/dist/web/assets/{common-lisp-B-vtBh5k.js → common-lisp-WLb-tAda.js} +0 -0
- /package/dist/web/assets/{coq-M8-FTfiZ.js → coq-DFtMD6Wn.js} +0 -0
- /package/dist/web/assets/{csharp-B7Ugdtd-.js → csharp-DEnvXVjs.js} +0 -0
- /package/dist/web/assets/{css-86k3Z-YA.js → css-DMPpCfkZ.js} +0 -0
- /package/dist/web/assets/{csv-BX5XYhfH.js → csv-DTSgNSaP.js} +0 -0
- /package/dist/web/assets/{csv-parser-d56Fzpwc.js → csv-parser-B_xNn07y.js} +0 -0
- /package/dist/web/assets/{cue-B7099gQa.js → cue-BEmVIZmd.js} +0 -0
- /package/dist/web/assets/{cypher-D99mWVIu.js → cypher-CncQVhvU.js} +0 -0
- /package/dist/web/assets/{d-CFE8oQwh.js → d-BzUSBVex.js} +0 -0
- /package/dist/web/assets/{dark-plus-BEeeDwdg.js → dark-plus-mysFNRq2.js} +0 -0
- /package/dist/web/assets/{dart-HVSWGnI5.js → dart-BBu3nkcE.js} +0 -0
- /package/dist/web/assets/{database-BLht7BVM.js → database-COYAMZaw.js} +0 -0
- /package/dist/web/assets/{dax-DjDgSJkh.js → dax-DcLVz9_9.js} +0 -0
- /package/dist/web/assets/{desktop-B5A3i7J1.js → desktop-DS2PdvoU.js} +0 -0
- /package/dist/web/assets/{diff-CpTrlaE9.js → diff-aUYk5Qys.js} +0 -0
- /package/dist/web/assets/{dist-BSCgJs-V.js → dist-Dxl3dqhC.js} +0 -0
- /package/dist/web/assets/{docker-xBhmJeHj.js → docker-12Lj1QUD.js} +0 -0
- /package/dist/web/assets/{dotenv-DKKhLEv5.js → dotenv-D-P4bzvl.js} +0 -0
- /package/dist/web/assets/{dracula-CVwQuzra.js → dracula-C-eRNn4o.js} +0 -0
- /package/dist/web/assets/{dracula-soft-CG6mB-lf.js → dracula-soft-CBB1BI7O.js} +0 -0
- /package/dist/web/assets/{dream-maker-SC-IdYY3.js → dream-maker-C_j5A0r7.js} +0 -0
- /package/dist/web/assets/{edit-diff-preview-C50i1H63.js → edit-diff-preview-CRMu-6r1.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CIBFq6W8.js → emacs-lisp-W54LT7rh.js} +0 -0
- /package/dist/web/assets/{everforest-dark-odRT_lec.js → everforest-dark-B_ISUDeh.js} +0 -0
- /package/dist/web/assets/{everforest-light-or1Voqjl.js → everforest-light-CNUYa354.js} +0 -0
- /package/dist/web/assets/{fennel-tQd1z3iu.js → fennel-DK82-oso.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-C-lL0g_E.js → file-exclamation-point-DBEC-Kst.js} +0 -0
- /package/dist/web/assets/{fish-DSuZQG0k.js → fish-CbJVJzQj.js} +0 -0
- /package/dist/web/assets/{fluent-D7xI8a8e.js → fluent-Cug5cOL1.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BlAtcPd3.js → fortran-free-form-amHz32Zw.js} +0 -0
- /package/dist/web/assets/{gdscript-VsEQXBPm.js → gdscript-t_G6VYpv.js} +0 -0
- /package/dist/web/assets/{gdshader-iqHMOdwW.js → gdshader-DqZaHh4E.js} +0 -0
- /package/dist/web/assets/{genie-C4XU4g1a.js → genie-CIJtFlZK.js} +0 -0
- /package/dist/web/assets/{gherkin-C4nfQzAB.js → gherkin-CalidqLY.js} +0 -0
- /package/dist/web/assets/{github-dark-B_pbG3CC.js → github-dark-BG2xX0wa.js} +0 -0
- /package/dist/web/assets/{github-dark-default-kFbx7C95.js → github-dark-default-tDp_EOBs.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-CpVfQEqs.js → github-dark-dimmed-DlSfPwW2.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-DYKqQwZh.js → github-dark-high-contrast-qZM7j5KB.js} +0 -0
- /package/dist/web/assets/{github-light-gEYBmKaj.js → github-light-CnvWTiyb.js} +0 -0
- /package/dist/web/assets/{github-light-default-DVHfMPQ2.js → github-light-default-Dy6OAt2Q.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-ClVJDu-q.js → github-light-high-contrast-1BHQCOPl.js} +0 -0
- /package/dist/web/assets/{gleam-BLaPu7ft.js → gleam-D-2cpXVX.js} +0 -0
- /package/dist/web/assets/{gn-BKgerUyI.js → gn-C1cC4xLc.js} +0 -0
- /package/dist/web/assets/{gnuplot-cqJuEFb-.js → gnuplot-2Gp459mV.js} +0 -0
- /package/dist/web/assets/{go-B2EHdyD8.js → go-BA1q0q8h.js} +0 -0
- /package/dist/web/assets/{groovy-DdImoj8s.js → groovy-BlIa-VfI.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-DlwstAds.js → gruvbox-dark-hard-BMIwVwaJ.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-CiYEd1vd.js → gruvbox-dark-medium-DxfuBW3n.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-C-wEa4h4.js → gruvbox-dark-soft-_NSslDDi.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-BqYmfna-.js → gruvbox-light-hard-CP5MIsL9.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-CxmfalT2.js → gruvbox-light-medium-BTMursH0.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-DGWXXMCB.js → gruvbox-light-soft-Bv6zFoVX.js} +0 -0
- /package/dist/web/assets/{haskell-H_C_IuVb.js → haskell-Da8UeSv-.js} +0 -0
- /package/dist/web/assets/{haxe-CGNSMpeZ.js → haxe-mcKlcxBa.js} +0 -0
- /package/dist/web/assets/{hcl-B7bSTSQf.js → hcl-BdZcx5JX.js} +0 -0
- /package/dist/web/assets/{hjson-DlJ-glQZ.js → hjson-pFXYbN4t.js} +0 -0
- /package/dist/web/assets/{hlsl-CehW5SJN.js → hlsl-Z0II0KR8.js} +0 -0
- /package/dist/web/assets/{horizon-Beabcz2d.js → horizon-CufN8DyP.js} +0 -0
- /package/dist/web/assets/{horizon-bright-BC87RXPa.js → horizon-bright-5qoQVyIY.js} +0 -0
- /package/dist/web/assets/{houston-Da7RFwUy.js → houston-DOFkQwep.js} +0 -0
- /package/dist/web/assets/{hy-m1H70w19.js → hy-BVbDMDcG.js} +0 -0
- /package/dist/web/assets/{imba-MExFsdAA.js → imba-DRM6LLS5.js} +0 -0
- /package/dist/web/assets/{ini-CJy_zmwy.js → ini-2KCnNvmz.js} +0 -0
- /package/dist/web/assets/{java-DqDFvJBH.js → java-BmvH-Ou1.js} +0 -0
- /package/dist/web/assets/{javascript-C3Wyv22X.js → javascript-B7VhU1Yc.js} +0 -0
- /package/dist/web/assets/{json-DnGBw_DZ.js → json-X1pNk0n7.js} +0 -0
- /package/dist/web/assets/{json5-CRN-zoN4.js → json5-CJeEJPTg.js} +0 -0
- /package/dist/web/assets/{jsonc-CKRUZNRo.js → jsonc-C1haTBsf.js} +0 -0
- /package/dist/web/assets/{jsonl-CGfD6FrH.js → jsonl-wQ8-AJgs.js} +0 -0
- /package/dist/web/assets/{jsonnet-nbFuDj0o.js → jsonnet-BrEjY3Xh.js} +0 -0
- /package/dist/web/assets/{jssm-D0ShzIqD.js → jssm-C6LgYbOs.js} +0 -0
- /package/dist/web/assets/{jsx-D27hbRbs.js → jsx-B2cfD_Fb.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-Ov3f8K0T.js → kanagawa-dragon-6UCcJpLw.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-CLyOqh-g.js → kanagawa-lotus-DOmfpsRg.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-Kj3dzabH.js → kanagawa-wave-CpxmPE7N.js} +0 -0
- /package/dist/web/assets/{kdl-DFF58Gu4.js → kdl-BrLEivmb.js} +0 -0
- /package/dist/web/assets/{kotlin-BM4PKaB4.js → kotlin-BO2FUnDB.js} +0 -0
- /package/dist/web/assets/{kusto-DrvHLCv5.js → kusto-Cev1yiq9.js} +0 -0
- /package/dist/web/assets/{laserwave-Db6i6ZkO.js → laserwave-BlEkf-eH.js} +0 -0
- /package/dist/web/assets/{lean-D0hEjzwk.js → lean-BLS3Xk2w.js} +0 -0
- /package/dist/web/assets/{less-BEj4npGG.js → less-BXmcsqcq.js} +0 -0
- /package/dist/web/assets/{lib-CBdlQ75W.js → lib-CxSBdrd7.js} +0 -0
- /package/dist/web/assets/{light-plus-DLIIbDea.js → light-plus-DS6zriYl.js} +0 -0
- /package/dist/web/assets/{llvm-DMt6umsO.js → llvm-C_wLnJgX.js} +0 -0
- /package/dist/web/assets/{log-BopvdtPW.js → log-Cg8pvmWi.js} +0 -0
- /package/dist/web/assets/{logo-CYSUFPFR.js → logo-C9-kEvPW.js} +0 -0
- /package/dist/web/assets/{luau-XC611HNO.js → luau-NwaGugfZ.js} +0 -0
- /package/dist/web/assets/{make-BMYZIfBj.js → make-DBpJE-Z8.js} +0 -0
- /package/dist/web/assets/{markdown-BLza5nvF.js → markdown-B7ZTAu7B.js} +0 -0
- /package/dist/web/assets/{material-theme-Uw27UfJW.js → material-theme-BsXH-Mss.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-dfqt8XBY.js → material-theme-darker-CW9OlIeY.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-CPtBTblV.js → material-theme-lighter-D2PAh0Kw.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-BfZgVqC-.js → material-theme-ocean-Vz6DBVi3.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-DAZoVYg8.js → material-theme-palenight-D-GleP3G.js} +0 -0
- /package/dist/web/assets/{matlab-cSzmquiW.js → matlab-C7MYlMSn.js} +0 -0
- /package/dist/web/assets/{mdx-Dq0kW5VM.js → mdx-FkOOX3Dq.js} +0 -0
- /package/dist/web/assets/{mermaid-DoIKF0yS.js → mermaid-C_9qw5it.js} +0 -0
- /package/dist/web/assets/{min-dark-DyraimDU.js → min-dark-BowNvxk2.js} +0 -0
- /package/dist/web/assets/{min-light-Co9cbcUv.js → min-light-x8oWUmbL.js} +0 -0
- /package/dist/web/assets/{mipsasm-ph6cKgHn.js → mipsasm-Dmi1AZOT.js} +0 -0
- /package/dist/web/assets/{mojo-BBT0XwmY.js → mojo-C6z7mry2.js} +0 -0
- /package/dist/web/assets/{monokai-IP0T5Rtg.js → monokai-BQ4e_Hn0.js} +0 -0
- /package/dist/web/assets/{moonbit-B8cogNXl.js → moonbit-CEeNIoB4.js} +0 -0
- /package/dist/web/assets/{move-DkY26NtM.js → move-bALfKUa7.js} +0 -0
- /package/dist/web/assets/{narrat-rBs3byG0.js → narrat-DX4JlAWT.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-DkO9wMLP.js → nextflow-groovy-CKQWtRkO.js} +0 -0
- /package/dist/web/assets/{night-owl-jVR4lFji.js → night-owl-D4PQd726.js} +0 -0
- /package/dist/web/assets/{night-owl-light-fBYJ7s3U.js → night-owl-light-BQo_YHll.js} +0 -0
- /package/dist/web/assets/{nix-WX9oavu6.js → nix-B1jThkGj.js} +0 -0
- /package/dist/web/assets/{nord-Cf7G21Pu.js → nord-35quHUKo.js} +0 -0
- /package/dist/web/assets/{nushell-DQdzcJ1H.js → nushell-L6DbJTuL.js} +0 -0
- /package/dist/web/assets/{objective-c-JSO2uvZ-.js → objective-c-CTZ4bR2U.js} +0 -0
- /package/dist/web/assets/{objective-cpp-CZMGJ9Fb.js → objective-cpp-Dm1YDIdA.js} +0 -0
- /package/dist/web/assets/{ocaml-C4S4NSDo.js → ocaml-B3n__LGy.js} +0 -0
- /package/dist/web/assets/{odin-Y2jXCgVq.js → odin-DmqZ8n3i.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-dVIxuoJY.js → one-dark-pro-16_UaUig.js} +0 -0
- /package/dist/web/assets/{one-light-BQ1IKfD1.js → one-light-B-GGjWn5.js} +0 -0
- /package/dist/web/assets/{openscad-Bddw4I3z.js → openscad-DTFB9Sj6.js} +0 -0
- /package/dist/web/assets/{pascal-BKVXrzC1.js → pascal-RcT1dQYN.js} +0 -0
- /package/dist/web/assets/{pkl-BTd-YRsO.js → pkl-Dr4yLQ_o.js} +0 -0
- /package/dist/web/assets/{plastic-eiGvBIxV.js → plastic-hp2Nry0R.js} +0 -0
- /package/dist/web/assets/{plsql-C290tieP.js → plsql-pFcO4WZ0.js} +0 -0
- /package/dist/web/assets/{po-DyDwh0tx.js → po-BkZ0_HvY.js} +0 -0
- /package/dist/web/assets/{poimandres-B87zc_g-.js → poimandres-BcTrxj7f.js} +0 -0
- /package/dist/web/assets/{polar-Cdub1YMT.js → polar-DHK3q8FF.js} +0 -0
- /package/dist/web/assets/{postcss-BcI_3K0v.js → postcss-Bv7WzijA.js} +0 -0
- /package/dist/web/assets/{powerquery-OdMrkJSe.js → powerquery-DpaU_hW9.js} +0 -0
- /package/dist/web/assets/{powershell-C8auT6xg.js → powershell-_Rqc0MGP.js} +0 -0
- /package/dist/web/assets/{prisma-DIbVv8PA.js → prisma-wyegE8us.js} +0 -0
- /package/dist/web/assets/{prolog-DbTOH3Ld.js → prolog-DspHCURv.js} +0 -0
- /package/dist/web/assets/{proto-C4Cq2dCD.js → proto-BOBiNGf5.js} +0 -0
- /package/dist/web/assets/{puppet-DJp3uqum.js → puppet-DfLvV8h2.js} +0 -0
- /package/dist/web/assets/{purescript-CLsV4rPU.js → purescript-BY5ZSOZy.js} +0 -0
- /package/dist/web/assets/{python-2gBtEza2.js → python-B758mMeg.js} +0 -0
- /package/dist/web/assets/{qmldir-DfhULcua.js → qmldir-C75bpHvy.js} +0 -0
- /package/dist/web/assets/{qss-DK--VTTV.js → qss-DcLTlbLd.js} +0 -0
- /package/dist/web/assets/{r-BArBO2Sa.js → r-D0G_Y1P5.js} +0 -0
- /package/dist/web/assets/{racket-5mijHsVy.js → racket-DTye_5qn.js} +0 -0
- /package/dist/web/assets/{raku-ir0suElk.js → raku-uJXBXkV0.js} +0 -0
- /package/dist/web/assets/{react-J9g6e3Rq.js → react-CorNPdie.js} +0 -0
- /package/dist/web/assets/{red-DSecpIOT.js → red-rs3HE3re.js} +0 -0
- /package/dist/web/assets/{reg-Dp30FusS.js → reg-DkTIaX7a.js} +0 -0
- /package/dist/web/assets/{regexp-DXJnEM_6.js → regexp-4TLSs4_m.js} +0 -0
- /package/dist/web/assets/{rel-Dr483ozj.js → rel-B0jumCjt.js} +0 -0
- /package/dist/web/assets/{resolve-theme-DJORYzKu.js → resolve-theme-CCoakF2E.js} +0 -0
- /package/dist/web/assets/{riscv-ZfBy5IA1.js → riscv-DtaWMlo0.js} +0 -0
- /package/dist/web/assets/{ron-ZJ_CljGP.js → ron-BCPxwJD6.js} +0 -0
- /package/dist/web/assets/{rose-pine-CcW_eJ6G.js → rose-pine-CNryoqGi.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-v2l_UF-j.js → rose-pine-dawn-C-gJOQVP.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-CNXkwf3D.js → rose-pine-moon-BdXSXpZh.js} +0 -0
- /package/dist/web/assets/{rosmsg-BMYMVW84.js → rosmsg-CPG8X-D5.js} +0 -0
- /package/dist/web/assets/{rust-BR-zJh5q.js → rust-CxfW3ePx.js} +0 -0
- /package/dist/web/assets/{sass-BukPOBFz.js → sass-D-7bxo_L.js} +0 -0
- /package/dist/web/assets/{save-DO5xjW95.js → save-BM4p9_9n.js} +0 -0
- /package/dist/web/assets/{scala-DUFY7D6D.js → scala-DyUsfHNc.js} +0 -0
- /package/dist/web/assets/{scheme-Dl-dNNxQ.js → scheme-ROgrwiO-.js} +0 -0
- /package/dist/web/assets/{sdbl-DdmlQhia.js → sdbl-B1EAQWpp.js} +0 -0
- /package/dist/web/assets/{shellscript-UyhrvGcd.js → shellscript-HhmB_Ufy.js} +0 -0
- /package/dist/web/assets/{shield-check-m2QzZTuZ.js → shield-check-C3qYSU8a.js} +0 -0
- /package/dist/web/assets/{shield-off-DNn7VzJe.js → shield-off-D5tU7gQT.js} +0 -0
- /package/dist/web/assets/{slack-dark-Cv6Szr-q.js → slack-dark-DnmSY72G.js} +0 -0
- /package/dist/web/assets/{slack-ochin-V6Ii3lAm.js → slack-ochin-q97gHjcd.js} +0 -0
- /package/dist/web/assets/{smalltalk-BUCv5G91.js → smalltalk-B-1waXvA.js} +0 -0
- /package/dist/web/assets/{snazzy-light-B67a78qi.js → snazzy-light-C8yWDN2g.js} +0 -0
- /package/dist/web/assets/{solarized-dark-ccWnfYyA.js → solarized-dark-ClVDEMJZ.js} +0 -0
- /package/dist/web/assets/{solarized-light-C3WyRO5I.js → solarized-light-Bts6CUIP.js} +0 -0
- /package/dist/web/assets/{solidity-BFbRPNi_.js → solidity-DYxwp0om.js} +0 -0
- /package/dist/web/assets/{splunk-zdpwCJLE.js → splunk-jK6-Sr06.js} +0 -0
- /package/dist/web/assets/{sql-B2IPUsfz.js → sql-DPpd8aUS.js} +0 -0
- /package/dist/web/assets/{ssh-config-Co4cXifq.js → ssh-config-Bimb9LVr.js} +0 -0
- /package/dist/web/assets/{stylus-CmI75XY8.js → stylus-B2DgJRJw.js} +0 -0
- /package/dist/web/assets/{swift-Dfks9YQI.js → swift-ChEH2nFL.js} +0 -0
- /package/dist/web/assets/{synthwave-84-B3LERzNS.js → synthwave-84-DwSLbgAm.js} +0 -0
- /package/dist/web/assets/{system-verilog-BFvldiwl.js → system-verilog-DoVBMhJ8.js} +0 -0
- /package/dist/web/assets/{systemd-Cu4Vvgbu.js → systemd--Ux4VAYD.js} +0 -0
- /package/dist/web/assets/{table-CAVmgQMT.js → table-Di8pv2Y_.js} +0 -0
- /package/dist/web/assets/{talonscript-Bnq32pUB.js → talonscript-DQNx9nak.js} +0 -0
- /package/dist/web/assets/{tasl-Nwe2K3J1.js → tasl-Bwy_Y9uD.js} +0 -0
- /package/dist/web/assets/{tcl-zOnpywrl.js → tcl-Drea-2jA.js} +0 -0
- /package/dist/web/assets/{terraform-Bc9Vj-n3.js → terraform-4rS5HoFk.js} +0 -0
- /package/dist/web/assets/{tokyo-night-9_KyM8M2.js → tokyo-night-C9MqaxIn.js} +0 -0
- /package/dist/web/assets/{toml-ByH-fxOY.js → toml-tSgOSv42.js} +0 -0
- /package/dist/web/assets/{tsv-DrCnDClP.js → tsv-BmL4O1Zx.js} +0 -0
- /package/dist/web/assets/{tsx-B8LafSYr.js → tsx-BwzeA880.js} +0 -0
- /package/dist/web/assets/{turtle-DuyP_QCJ.js → turtle-2DYvssFL.js} +0 -0
- /package/dist/web/assets/{typescript-BTBJb5RD.js → typescript-ZC-vonSh.js} +0 -0
- /package/dist/web/assets/{typespec-DdOFo7M5.js → typespec-DkCEO45F.js} +0 -0
- /package/dist/web/assets/{typst-LYvnBWQu.js → typst-DAZdb2n6.js} +0 -0
- /package/dist/web/assets/{utils-CTg5uAYR.js → utils-CSCvNZxE.js} +0 -0
- /package/dist/web/assets/{v-BuBYA1_x.js → v-B_wsb4C1.js} +0 -0
- /package/dist/web/assets/{vala-vRIBYYe3.js → vala-BDBNS784.js} +0 -0
- /package/dist/web/assets/{vb-DwGi-IYM.js → vb-CuOoJSSa.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-i6SHFTfU.js → vendor-xterm-CImj6i6Z.js} +0 -0
- /package/dist/web/assets/{verilog-6B1X4G2-.js → verilog-BCcOvOcc.js} +0 -0
- /package/dist/web/assets/{vesper-Dw1y_Ae7.js → vesper-vN8CREK5.js} +0 -0
- /package/dist/web/assets/{vhdl-DK8JwBzJ.js → vhdl-CI8PoqeM.js} +0 -0
- /package/dist/web/assets/{viml-BTKIEIqm.js → viml-B66dNP1B.js} +0 -0
- /package/dist/web/assets/{vitesse-black-BqqolhEg.js → vitesse-black-0bGIaCfD.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-B9JL5WQA.js → vitesse-dark-Dk-RSXq-.js} +0 -0
- /package/dist/web/assets/{vitesse-light-DYJKj2se.js → vitesse-light-BUsA5RhG.js} +0 -0
- /package/dist/web/assets/{vyper-D-jDI3WH.js → vyper-dNodzEa8.js} +0 -0
- /package/dist/web/assets/{wasm-1-GlNdpk.js → wasm-BDxaNPCj.js} +0 -0
- /package/dist/web/assets/{wasm-BBW4WmT2.js → wasm-d6SE2_qm.js} +0 -0
- /package/dist/web/assets/{wenyan-B6cM7sDO.js → wenyan-D0h8Uz55.js} +0 -0
- /package/dist/web/assets/{wgsl-FaHGQgko.js → wgsl-vKs_vsvQ.js} +0 -0
- /package/dist/web/assets/{wikitext-DtIyPUrk.js → wikitext-BIWsPJit.js} +0 -0
- /package/dist/web/assets/{wit-BMVRif5Y.js → wit-6vmKi2H3.js} +0 -0
- /package/dist/web/assets/{wolfram-DQsCGohn.js → wolfram-zoNg6F1a.js} +0 -0
- /package/dist/web/assets/{x-C2VSAp7O.js → x-BsFLg-T8.js} +0 -0
- /package/dist/web/assets/{yaml-C-Va56Gt.js → yaml-W95qP6-B.js} +0 -0
- /package/dist/web/assets/{zenscript-DdobKGI-.js → zenscript-BxircxDx.js} +0 -0
- /package/dist/web/assets/{zig-vqDFwj6q.js → zig-DD79WQtz.js} +0 -0
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
# PPM Code Standards & Conventions
|
|
2
|
+
|
|
3
|
+
## File Naming
|
|
4
|
+
|
|
5
|
+
| File Type | Convention | Example | Purpose |
|
|
6
|
+
|-----------|-----------|---------|---------|
|
|
7
|
+
| CLI commands | kebab-case | `start-cmd.ts`, `init.ts` | Descriptive command names |
|
|
8
|
+
| Services | kebab-case | `chat.service.ts`, `file.service.ts` | `{feature}.service.ts` pattern |
|
|
9
|
+
| Providers | kebab-case | `claude-agent-sdk.ts`, `mock-provider.ts` | `{name}-provider.ts` or `{name}.ts` |
|
|
10
|
+
| Routes | kebab-case | `chat.ts`, `project-scoped.ts` | Describe HTTP route group |
|
|
11
|
+
| WebSocket | kebab-case | `chat.ts`, `terminal.ts` | Match feature area |
|
|
12
|
+
| Components | PascalCase | `ChatTab.tsx`, `FileTree.tsx` | React convention |
|
|
13
|
+
| Hooks | camelCase with `use` prefix | `useChat.ts`, `useTerminal.ts` | React hook convention |
|
|
14
|
+
| Stores | kebab-case | `chat-store.ts`, `project-store.ts` | Zustand store files |
|
|
15
|
+
| Utilities | kebab-case | `utils.ts`, `file-support.ts` | Grouped by function |
|
|
16
|
+
| Types | kebab-case | `api.ts`, `chat.ts` | Group related types together |
|
|
17
|
+
| Tests | kebab-case with `.test.ts` | `chat.service.test.ts` | Match source file name |
|
|
18
|
+
|
|
19
|
+
## TypeScript Conventions
|
|
20
|
+
|
|
21
|
+
### Strict Mode
|
|
22
|
+
All files use TypeScript strict mode (`tsconfig.json` `"strict": true`).
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Required:
|
|
26
|
+
- Explicit return types on functions
|
|
27
|
+
- No `any` types (use `unknown` if necessary)
|
|
28
|
+
- No implicit `any` parameters
|
|
29
|
+
- Exhaustive type checking (switch, conditionals)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Path Aliases
|
|
33
|
+
Use `@/*` alias for web layer imports (configured in `tsconfig.json`):
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Good
|
|
37
|
+
import { useChat } from "@/hooks/use-chat";
|
|
38
|
+
import { chatStore } from "@/stores/chat-store";
|
|
39
|
+
|
|
40
|
+
// Avoid
|
|
41
|
+
import { useChat } from "../../hooks/use-chat";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Type Definitions
|
|
45
|
+
Place types near usage. Group related types in single files:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Good: src/types/chat.ts
|
|
49
|
+
export interface Session { id: string; title: string; }
|
|
50
|
+
export interface Message { id: string; content: string; role: "user" | "assistant"; }
|
|
51
|
+
export type ChatEvent = { type: "text"; content: string } | { type: "done" };
|
|
52
|
+
|
|
53
|
+
// Avoid: spread across separate files
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Enums & Unions
|
|
57
|
+
Prefer discriminated unions over enums for better tree-shaking:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Good: Discriminated union
|
|
61
|
+
type ChatEvent =
|
|
62
|
+
| { type: "text"; content: string }
|
|
63
|
+
| { type: "tool_use"; tool: string; input: unknown }
|
|
64
|
+
| { type: "done" };
|
|
65
|
+
|
|
66
|
+
// Avoid: Enum
|
|
67
|
+
enum MessageType {
|
|
68
|
+
TEXT,
|
|
69
|
+
TOOL_USE,
|
|
70
|
+
DONE,
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Async/Await
|
|
75
|
+
Always use `async`/`await` over Promise chains. Use async generators for streaming:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Good: Async generator for streaming
|
|
79
|
+
async *streamMessages(input: string) {
|
|
80
|
+
for await (const event of provider.sendMessage(input)) {
|
|
81
|
+
yield event;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Avoid: Promise chains
|
|
86
|
+
provider.sendMessage(input).then(...)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Long-Lived Streaming Sessions
|
|
90
|
+
|
|
91
|
+
For chat-like features, maintain persistent streaming sessions instead of per-query execution:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Good: Session-scoped streaming (v0.8.55+)
|
|
95
|
+
// Provider maintains AsyncGenerator per session
|
|
96
|
+
const streaming = provider.sendMessage(sessionId, content);
|
|
97
|
+
for await (const event of streaming) {
|
|
98
|
+
// Handle event (can receive multiple messages from same generator)
|
|
99
|
+
yield event;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Avoid: Per-message query execution
|
|
103
|
+
for (const message of messages) {
|
|
104
|
+
const result = await query(message); // Restarts SDK each time
|
|
105
|
+
yield result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Key Pattern: Decoupled streaming loop
|
|
109
|
+
// Store streaming generator in session entry
|
|
110
|
+
// Follow-up messages push into existing generator
|
|
111
|
+
// FE disconnect ≠ abort (BE owns the connection)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Error Handling
|
|
115
|
+
Use try-catch for async operations. Throw structured errors:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Good
|
|
119
|
+
try {
|
|
120
|
+
const file = await FileService.read(path);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
123
|
+
console.error(`Failed to read ${path}: ${message}`);
|
|
124
|
+
throw new Error(`FileService.read failed: ${message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Avoid: Silent failures
|
|
128
|
+
const file = await FileService.read(path).catch(() => null);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Component Patterns
|
|
132
|
+
|
|
133
|
+
### React Components
|
|
134
|
+
Use functional components with hooks. Keep components focused:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Good: Single responsibility
|
|
138
|
+
export function ChatTab() {
|
|
139
|
+
const { messages, sendMessage } = useChat();
|
|
140
|
+
return <div>/* Chat UI */</div>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Avoid: God component
|
|
144
|
+
export function ChatTab() {
|
|
145
|
+
// File management, git status, terminal, chat
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Zustand Stores (useShallow)
|
|
150
|
+
Define stores as singleton exports. Use `useShallow` when destructuring state to prevent unnecessary re-renders:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Good: src/web/stores/chat-store.ts
|
|
154
|
+
export const chatStore = create<ChatState>((set) => ({
|
|
155
|
+
messages: [],
|
|
156
|
+
addMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })),
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// Usage with useShallow (avoids full re-render on object mutations)
|
|
160
|
+
const { messages, addMessage } = chatStore(useShallow((state) => ({
|
|
161
|
+
messages: state.messages,
|
|
162
|
+
addMessage: state.addMessage,
|
|
163
|
+
})));
|
|
164
|
+
|
|
165
|
+
// Single selector still works without useShallow
|
|
166
|
+
const messages = chatStore((state) => state.messages);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Component Memoization (React.memo)
|
|
170
|
+
Memoize expensive components to prevent re-renders from parent updates:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Good: Memoized heavy component
|
|
174
|
+
export const CodeEditor = memo(function CodeEditor({ filePath }: Props) {
|
|
175
|
+
return <MonacoEditor path={filePath} />;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Memoized components: CodeEditor, MessageBubble, ProjectBar, ProjectAvatar, TerminalTab, PanelLayout, Sidebar, StatusBar, StatusBarEntry, TabBar, TreeNode
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Custom Hooks
|
|
182
|
+
Extract logic into hooks for reusability. Return stable references:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// Good: useChat hook
|
|
186
|
+
export function useChat() {
|
|
187
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
188
|
+
|
|
189
|
+
const sendMessage = useCallback(async (text: string) => {
|
|
190
|
+
// Send logic
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
return { messages, sendMessage };
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Lazy-Loaded Tab Content
|
|
198
|
+
Use React.lazy() for code splitting:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// Good
|
|
202
|
+
const ChatTab = lazy(() => import("./chat-tab").then(m => ({ default: m.ChatTab })));
|
|
203
|
+
|
|
204
|
+
// In component
|
|
205
|
+
<Suspense fallback={<Spinner />}>
|
|
206
|
+
<ChatTab />
|
|
207
|
+
</Suspense>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Service Patterns
|
|
211
|
+
|
|
212
|
+
### Singleton Services
|
|
213
|
+
Services are singletons exported as functions or instances:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Good: services/chat.service.ts
|
|
217
|
+
export async function createSession(projectPath: string): Promise<Session> {
|
|
218
|
+
// Shared logic across all callers
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Good: services/config.service.ts
|
|
222
|
+
export const ConfigService = {
|
|
223
|
+
load: () => YAML.parse(configFile),
|
|
224
|
+
save: (config) => YAML.stringify(config),
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Dependency Injection
|
|
229
|
+
Services should receive dependencies as parameters or imports:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Good: Pass dependencies explicitly
|
|
233
|
+
export async function streamChat(
|
|
234
|
+
session: Session,
|
|
235
|
+
message: string,
|
|
236
|
+
provider: AIProvider, // Dependency
|
|
237
|
+
) {
|
|
238
|
+
// Use provider
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Avoid: Implicit globals
|
|
242
|
+
import { globalProvider } from "./global"; // Hidden dependency
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Error Propagation
|
|
246
|
+
Services throw descriptive errors; routes catch and format:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// Good: Service throws
|
|
250
|
+
export function validatePath(path: string) {
|
|
251
|
+
if (path.includes("..")) {
|
|
252
|
+
throw new Error(`Path traversal detected: ${path}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Good: Route catches and formats
|
|
257
|
+
try {
|
|
258
|
+
const content = await FileService.read(path);
|
|
259
|
+
res.json(ok(content));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const msg = error instanceof Error ? error.message : "Unknown";
|
|
262
|
+
res.json(err(msg));
|
|
263
|
+
res.status(400);
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Provider Interface Pattern (Multi-Provider)
|
|
268
|
+
|
|
269
|
+
PPM supports multiple AI providers through an extensible interface pattern:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Core interface defines required methods + optional capability methods
|
|
273
|
+
interface AIProvider {
|
|
274
|
+
id: string;
|
|
275
|
+
name: string;
|
|
276
|
+
|
|
277
|
+
// Required: All providers must implement these
|
|
278
|
+
createSession(config: SessionConfig): Promise<Session>;
|
|
279
|
+
resumeSession(sessionId: string): Promise<Session>;
|
|
280
|
+
listSessions(): Promise<SessionInfo[]>;
|
|
281
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
282
|
+
sendMessage(sessionId: string, message: string, opts?: SendMessageOpts): AsyncIterable<ChatEvent>;
|
|
283
|
+
|
|
284
|
+
// Optional: Providers implement what they support
|
|
285
|
+
abortQuery?(sessionId: string): void;
|
|
286
|
+
getMessages?(sessionId: string): Promise<ChatMessage[]>;
|
|
287
|
+
listSessionsByDir?(dir: string): Promise<SessionInfo[]>;
|
|
288
|
+
ensureProjectPath?(sessionId: string, path: string): void;
|
|
289
|
+
isAvailable?(): Promise<boolean>;
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**SDK-based Providers (Claude):**
|
|
294
|
+
- Use Anthropic SDK for advanced features
|
|
295
|
+
- Implement all optional methods
|
|
296
|
+
- Streaming via SDK async generators
|
|
297
|
+
|
|
298
|
+
**CLI-based Providers (Cursor, Codex, Gemini):**
|
|
299
|
+
- Extend `CliProvider` abstract class
|
|
300
|
+
- Implement: `buildArgs()`, `mapEvent()`, `extractSessionId()`, `isAvailable()`
|
|
301
|
+
- Shared: spawn, parse NDJSON, abort, cleanup
|
|
302
|
+
- Override `spawnProcess()` for provider-specific quirks (e.g., workspace trust)
|
|
303
|
+
- Override `listSessions()` to read native provider history (e.g., SQLite)
|
|
304
|
+
|
|
305
|
+
**Usage Pattern (Type-Safe Optional Calls):**
|
|
306
|
+
```typescript
|
|
307
|
+
// Good: Optional chaining for capabilities
|
|
308
|
+
await provider.abortQuery?.(sessionId); // Silently skips if not implemented
|
|
309
|
+
const messages = await provider.getMessages?.(sessionId) ?? [];
|
|
310
|
+
|
|
311
|
+
// Avoid: Duck-typing checks (old pattern)
|
|
312
|
+
if ("abortQuery" in provider) {
|
|
313
|
+
(provider as any).abortQuery(sessionId); // Not type-safe
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Adding New Providers:**
|
|
318
|
+
1. Create provider class implementing/extending AIProvider or CliProvider
|
|
319
|
+
2. Implement `isAvailable()` for optional registration
|
|
320
|
+
3. Register in `ProviderRegistry` via `bootstrapProviders()` if binary available
|
|
321
|
+
4. Add event mapper if CLI-based (NDJSON → ChatEvent)
|
|
322
|
+
5. ~100-150 lines per provider (extends CliProvider)
|
|
323
|
+
|
|
324
|
+
## API Conventions
|
|
325
|
+
|
|
326
|
+
### Response Envelope
|
|
327
|
+
All REST responses use the `ApiResponse<T>` envelope:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// Good
|
|
331
|
+
{ ok: true, data: { /* payload */ } }
|
|
332
|
+
{ ok: false, error: "descriptive error message" }
|
|
333
|
+
|
|
334
|
+
// Avoid: Inconsistent shapes
|
|
335
|
+
{ success: true, result: { } }
|
|
336
|
+
{ error: "error message" } // No shape contract
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Project-Scoped Routes
|
|
340
|
+
All project-specific endpoints use the pattern `/api/project/:name/*`:
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
GET /api/projects # List all projects
|
|
344
|
+
POST /api/projects # Create project
|
|
345
|
+
DELETE /api/projects/:name # Delete project
|
|
346
|
+
GET /api/project/:name/chat/... # Chat (project-scoped)
|
|
347
|
+
GET /api/project/:name/git/... # Git (project-scoped)
|
|
348
|
+
GET /api/project/:name/files/... # Files (project-scoped)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### WebSocket Message Formats
|
|
352
|
+
Structure WebSocket messages as typed JSON objects:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// Client -> Server (chat, v0.8.55+)
|
|
356
|
+
{ type: "message"; content: string; priority?: "now"|"next"|"later"; images?: {id: string; data: string}[] }
|
|
357
|
+
{ type: "cancel" }
|
|
358
|
+
{ type: "approval_response"; requestId: string; approved: boolean; reason?: string; data?: unknown }
|
|
359
|
+
{ type: "ready" } // FE handshake after WS open
|
|
360
|
+
|
|
361
|
+
// Server -> Client (chat)
|
|
362
|
+
{ type: "text"; content: string; parentToolUseId?: string }
|
|
363
|
+
{ type: "thinking"; content: string; parentToolUseId?: string }
|
|
364
|
+
{ type: "tool_use"; tool: string; input: unknown; toolUseId?: string; parentToolUseId?: string }
|
|
365
|
+
{ type: "tool_result"; output: string; isError?: boolean; toolUseId?: string; parentToolUseId?: string }
|
|
366
|
+
{ type: "approval_request"; requestId: string; tool: string; input: unknown }
|
|
367
|
+
{ type: "done"; sessionId: string; contextWindowPct?: number; resultSubtype?: string }
|
|
368
|
+
{ type: "error"; message: string }
|
|
369
|
+
{ type: "account_info"; accountId: string; accountLabel: string }
|
|
370
|
+
{ type: "phase_changed"; phase: SessionPhase; elapsed?: number }
|
|
371
|
+
{ type: "session_state"; sessionId: string; phase: SessionPhase; pendingApproval: {...}|null; sessionTitle: string|null }
|
|
372
|
+
{ type: "turn_events"; events: unknown[] } // Buffered events on reconnect
|
|
373
|
+
{ type: "title_updated"; title: string }
|
|
374
|
+
{ type: "ping" } // Server keepalive
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**New Fields (v0.8.55+):**
|
|
378
|
+
- `priority` — Message priority for queue ordering (future: "now" interrupts, "next" queues first, "later" queues at end)
|
|
379
|
+
- `images` — Image attachments sent with message
|
|
380
|
+
- `phase_changed` — Phase transitions (initializing → connecting → thinking/streaming → idle)
|
|
381
|
+
- `session_state` — Session state snapshot on WS open/ready (includes current phase and pending approval)
|
|
382
|
+
- `turn_events` — Buffered events sent on reconnection for sync
|
|
383
|
+
- `parentToolUseId` — Hierarchical tool call support (nested tool results)
|
|
384
|
+
|
|
385
|
+
### Status Codes
|
|
386
|
+
Use standard HTTP status codes:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// Success
|
|
390
|
+
200 OK - GET successful, POST/PUT/DELETE with response body
|
|
391
|
+
201 Created - POST created resource
|
|
392
|
+
204 No Content - DELETE successful
|
|
393
|
+
|
|
394
|
+
// Client error
|
|
395
|
+
400 Bad Request - Invalid input, validation failure
|
|
396
|
+
401 Unauthorized - Missing/invalid auth token
|
|
397
|
+
403 Forbidden - Authenticated but not authorized (rare in PPM)
|
|
398
|
+
404 Not Found - Project/file/session not found
|
|
399
|
+
|
|
400
|
+
// Server error
|
|
401
|
+
500 Internal Error - Unexpected exception
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Import/Export Conventions
|
|
405
|
+
|
|
406
|
+
### Named Exports (Preferred)
|
|
407
|
+
Use named exports for better tree-shaking and clarity:
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// Good: services/file.service.ts
|
|
411
|
+
export async function read(path: string): Promise<string> { }
|
|
412
|
+
export async function write(path: string, content: string): Promise<void> { }
|
|
413
|
+
|
|
414
|
+
// Usage
|
|
415
|
+
import { read, write } from "./services/file.service";
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Default Exports (React Components Only)
|
|
419
|
+
Use default exports for React components (enables lazy loading):
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// Good: components/chat/chat-tab.tsx
|
|
423
|
+
export default function ChatTab() { }
|
|
424
|
+
|
|
425
|
+
// Usage
|
|
426
|
+
const ChatTab = lazy(() => import("./components/chat/chat-tab"));
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Wildcard Imports (Avoid)
|
|
430
|
+
Avoid wildcard imports except for types:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
// Good: Explicit imports
|
|
434
|
+
import { send, receive } from "./ws-client";
|
|
435
|
+
|
|
436
|
+
// Good: Type wildcard (rare)
|
|
437
|
+
import type * as Types from "./types";
|
|
438
|
+
|
|
439
|
+
// Avoid: Implicit exports
|
|
440
|
+
import * as WsClient from "./ws-client";
|
|
441
|
+
WsClient.send(); // Unclear what's exported
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Error Handling Patterns
|
|
445
|
+
|
|
446
|
+
### Service Layer
|
|
447
|
+
Throw descriptive errors with context:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// Good
|
|
451
|
+
throw new Error(`GitService.commit failed: ${error.message}`);
|
|
452
|
+
throw new Error(`FileService: path traversal detected: ${path}`);
|
|
453
|
+
|
|
454
|
+
// Avoid: Generic errors
|
|
455
|
+
throw new Error("Failed");
|
|
456
|
+
throw error; // Re-throw loses context
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Route Layer
|
|
460
|
+
Catch, format, and return `ApiResponse` with error:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// Good
|
|
464
|
+
try {
|
|
465
|
+
const result = await service.doSomething();
|
|
466
|
+
return res.json(ok(result));
|
|
467
|
+
} catch (error) {
|
|
468
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
469
|
+
res.status(400);
|
|
470
|
+
return res.json(err(message));
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Component Layer
|
|
475
|
+
Handle errors from API calls with user-friendly messages:
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// Good
|
|
479
|
+
try {
|
|
480
|
+
const response = await api.post("/...");
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
setError(response.error);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
setState(response.data);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
setError("Network error. Please try again.");
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Testing Conventions
|
|
492
|
+
|
|
493
|
+
### Test File Location
|
|
494
|
+
Tests colocate with source files or in `tests/` directory:
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
src/services/chat.service.ts
|
|
498
|
+
tests/unit/services/chat.service.test.ts ← Match path, add .test suffix
|
|
499
|
+
|
|
500
|
+
src/web/hooks/use-chat.ts
|
|
501
|
+
tests/unit/hooks/use-chat.test.ts
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Test Structure
|
|
505
|
+
Use AAA pattern (Arrange, Act, Assert):
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
describe("ChatService", () => {
|
|
509
|
+
it("should create session with unique ID", () => {
|
|
510
|
+
// Arrange
|
|
511
|
+
const projectPath = "/tmp/project";
|
|
512
|
+
|
|
513
|
+
// Act
|
|
514
|
+
const session = ChatService.createSession(projectPath);
|
|
515
|
+
const session2 = ChatService.createSession(projectPath);
|
|
516
|
+
|
|
517
|
+
// Assert
|
|
518
|
+
expect(session.id).not.toBe(session2.id);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Mocking
|
|
524
|
+
Mock external dependencies (providers, file system):
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// Good: Mock provider
|
|
528
|
+
const mockProvider = {
|
|
529
|
+
createSession: () => ({ id: "test-id" }),
|
|
530
|
+
sendMessage: async function*() { yield { type: "text", content: "response" }; },
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// Avoid: Mock implementation details
|
|
534
|
+
jest.spyOn(fs, "readFile").mockResolvedValue("content");
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Security Conventions
|
|
538
|
+
|
|
539
|
+
### Path Traversal Protection
|
|
540
|
+
Always validate file paths before operations:
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
// Good: Validate before file access
|
|
544
|
+
export function validatePath(path: string) {
|
|
545
|
+
const normalized = Path.normalize(path);
|
|
546
|
+
if (normalized.startsWith("..")) {
|
|
547
|
+
throw new Error("Path traversal detected");
|
|
548
|
+
}
|
|
549
|
+
if (!normalized.startsWith(projectPath)) {
|
|
550
|
+
throw new Error("Access denied: outside project directory");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Token-Based Auth
|
|
556
|
+
Every API route requires token validation via middleware:
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
// Good: Middleware validates token
|
|
560
|
+
app.use("/api", authMiddleware);
|
|
561
|
+
|
|
562
|
+
// In middleware
|
|
563
|
+
const token = req.header("Authorization");
|
|
564
|
+
if (!token || token !== config.auth.token) {
|
|
565
|
+
res.status(401);
|
|
566
|
+
res.json(err("Unauthorized"));
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Input Validation
|
|
571
|
+
Validate all user input (file paths, command arguments, message content):
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// Good: Validate before processing
|
|
575
|
+
if (!path || typeof path !== "string") {
|
|
576
|
+
throw new Error("Invalid path");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (message.length > 10000) {
|
|
580
|
+
throw new Error("Message too long");
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Documentation Conventions
|
|
585
|
+
|
|
586
|
+
### Inline Comments
|
|
587
|
+
Use comments for **why**, not **what**. Let code speak for itself:
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
// Good: Explain intent
|
|
591
|
+
// Expand node lazily to avoid blocking on large directories
|
|
592
|
+
async function loadChildren(node: TreeNode) {
|
|
593
|
+
// ...
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Avoid: Obvious comments
|
|
597
|
+
// Set messages to empty array
|
|
598
|
+
const [messages, setMessages] = useState([]);
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### JSDoc for Public APIs
|
|
602
|
+
Document exported functions with JSDoc:
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
/**
|
|
606
|
+
* Stream chat messages from AI provider.
|
|
607
|
+
*
|
|
608
|
+
* @param sessionId - Chat session ID
|
|
609
|
+
* @param message - User message text
|
|
610
|
+
* @param provider - AI provider (defaults to registry.default)
|
|
611
|
+
* @yields ChatEvent objects (text, tool_use, approval_request, done)
|
|
612
|
+
* @throws Error if session not found or provider fails
|
|
613
|
+
*/
|
|
614
|
+
export async *streamMessages(
|
|
615
|
+
sessionId: string,
|
|
616
|
+
message: string,
|
|
617
|
+
provider?: AIProvider,
|
|
618
|
+
) {
|
|
619
|
+
// ...
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Type Comments
|
|
624
|
+
Use type comments for complex types:
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// File status with git tracking info
|
|
628
|
+
type FileStatus =
|
|
629
|
+
| { status: "modified" }
|
|
630
|
+
| { status: "untracked" }
|
|
631
|
+
| { status: "staged"; originalPath?: string };
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## Performance Conventions
|
|
635
|
+
|
|
636
|
+
### Code Splitting
|
|
637
|
+
Use `React.lazy()` + `Suspense` for routes, tab components, and heavy renderers:
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
// Good: Lazy-load terminal component
|
|
641
|
+
const TerminalTab = lazy(() => import("./terminal-tab"));
|
|
642
|
+
|
|
643
|
+
// Good: Lazy-load markdown renderer on 3+ sites
|
|
644
|
+
const MarkdownRenderer = lazy(() => import("./markdown-renderer"));
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Vendor Chunk Splitting (vite.config.ts)
|
|
648
|
+
Manually split large vendor libraries into separate chunks for better caching:
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
// Chunks: vendor-monaco, vendor-mermaid, vendor-xterm, vendor-markdown, vendor-ui
|
|
652
|
+
manualChunks(id: string) {
|
|
653
|
+
if (id.includes("node_modules/monaco-editor")) return "vendor-monaco";
|
|
654
|
+
if (id.includes("node_modules/mermaid")) return "vendor-mermaid";
|
|
655
|
+
if (id.includes("node_modules/@xterm")) return "vendor-xterm";
|
|
656
|
+
// ... etc
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Dynamic Imports (On-Demand Loading)
|
|
661
|
+
Load heavy libraries only when needed:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// Good: Mermaid diagram support on-demand in markdown
|
|
665
|
+
let mermaidPromise: Promise<typeof import("mermaid")> | null = null;
|
|
666
|
+
async function loadMermaid() {
|
|
667
|
+
if (!mermaidPromise) {
|
|
668
|
+
mermaidPromise = import("mermaid").then((mod) => mod.default);
|
|
669
|
+
}
|
|
670
|
+
return mermaidPromise;
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Chat Pagination & Message Caps
|
|
675
|
+
- **Chat history:** Load 50 messages per page with load-more button
|
|
676
|
+
- **Team activity:** Cap `teamActivityRef` at 500 messages to prevent unbounded growth
|
|
677
|
+
|
|
678
|
+
### Memoization
|
|
679
|
+
Memoize expensive computations and callbacks:
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
// Good: Memoize filter result
|
|
683
|
+
const filteredFiles = useMemo(
|
|
684
|
+
() => files.filter(f => f.name.includes(query)),
|
|
685
|
+
[files, query]
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
// Good: Stable callback reference
|
|
689
|
+
const handleClick = useCallback(() => {
|
|
690
|
+
// ...
|
|
691
|
+
}, [dependencies]);
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Bundle Analysis
|
|
695
|
+
Monitor bundle size growth:
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
# Check bundle stats
|
|
699
|
+
bun run build && ls -lh dist/web/assets/
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
## Git Conventions
|
|
703
|
+
|
|
704
|
+
### Commit Messages
|
|
705
|
+
Use conventional commit format:
|
|
706
|
+
|
|
707
|
+
```
|
|
708
|
+
feat: add file attachment support to chat
|
|
709
|
+
fix: resolve WebSocket reconnection issue
|
|
710
|
+
refactor: simplify GitService.status method
|
|
711
|
+
docs: update deployment guide
|
|
712
|
+
test: add chat-service unit tests
|
|
713
|
+
chore: upgrade TypeScript to 5.9.3
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Branch Names
|
|
717
|
+
Use descriptive kebab-case names:
|
|
718
|
+
|
|
719
|
+
```
|
|
720
|
+
feature/chat-file-attachments
|
|
721
|
+
fix/websocket-reconnect
|
|
722
|
+
refactor/service-layer-cleanup
|
|
723
|
+
docs/deployment-guide
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
## CLI Design Patterns
|
|
727
|
+
|
|
728
|
+
### Command Option Handling
|
|
729
|
+
When adding new options to CLI commands (e.g., `ppm start`):
|
|
730
|
+
|
|
731
|
+
**Option Naming:**
|
|
732
|
+
- Use long form: `--foreground`, `--share` (not short-only)
|
|
733
|
+
- Add short form if common: `-f`, `-s` (optional)
|
|
734
|
+
- Keep defaults sensible (e.g., daemon mode is default)
|
|
735
|
+
|
|
736
|
+
**Implementation Pattern:**
|
|
737
|
+
```typescript
|
|
738
|
+
program
|
|
739
|
+
.command("start")
|
|
740
|
+
.option("-p, --port <port>", "Port to listen on")
|
|
741
|
+
.option("-f, --foreground", "Run in foreground")
|
|
742
|
+
.option("-s, --share", "Enable public URL via tunnel")
|
|
743
|
+
.action(async (options) => {
|
|
744
|
+
// options.port, options.foreground, options.share as booleans
|
|
745
|
+
});
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
**Server Handling:**
|
|
749
|
+
```typescript
|
|
750
|
+
export async function startServer(options: {
|
|
751
|
+
port?: string;
|
|
752
|
+
foreground?: boolean;
|
|
753
|
+
share?: boolean;
|
|
754
|
+
config?: string;
|
|
755
|
+
}) {
|
|
756
|
+
const isDaemon = !options.foreground; // Explicit: daemon is default
|
|
757
|
+
|
|
758
|
+
if (isDaemon) {
|
|
759
|
+
// Spawn child process
|
|
760
|
+
const child = Bun.spawn(/* ... */);
|
|
761
|
+
// Poll for status.json, show URLs
|
|
762
|
+
} else {
|
|
763
|
+
// Foreground: serve with logs
|
|
764
|
+
const server = Bun.serve(/* ... */);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (options.share) {
|
|
768
|
+
// Start tunnel (works in both daemon + foreground)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Status File Format
|
|
774
|
+
Daemon process communicates back via JSON file at `~/.ppm/status.json`:
|
|
775
|
+
|
|
776
|
+
```json
|
|
777
|
+
{
|
|
778
|
+
"pid": 12345,
|
|
779
|
+
"port": 8080,
|
|
780
|
+
"host": "0.0.0.0",
|
|
781
|
+
"shareUrl": "https://abc-123.trycloudflare.com"
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
**Backward Compatibility:** Fallback to `~/.ppm/ppm.pid` for legacy support.
|
|
786
|
+
|
|
787
|
+
### Feature Service Loading (Lazy)
|
|
788
|
+
Services that require external dependencies (e.g., cloudflared) should be lazy-imported:
|
|
789
|
+
|
|
790
|
+
```typescript
|
|
791
|
+
if (options.share) {
|
|
792
|
+
// Only download cloudflared if --share was used
|
|
793
|
+
const { ensureCloudflared } = await import("../services/cloudflared.service.ts");
|
|
794
|
+
await ensureCloudflared();
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
This keeps startup fast when features aren't used.
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
## Tab ID & URL Conventions (v0.8.77+)
|
|
803
|
+
|
|
804
|
+
### Deterministic Tab IDs
|
|
805
|
+
|
|
806
|
+
Tab IDs are derived from type + metadata, not random:
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
// Format: "{type}:{identifier}" for most tabs, "{type}" for singletons
|
|
810
|
+
editor:src/index.ts // Editor tab with file path
|
|
811
|
+
chat:claude/abc123 // Chat with provider/session ID
|
|
812
|
+
terminal:1 // Terminal tab index
|
|
813
|
+
database:conn-1/users // Database: connection ID / table
|
|
814
|
+
git-graph // Singleton: no identifier
|
|
815
|
+
settings // Singleton: no identifier
|
|
816
|
+
|
|
817
|
+
// Derivation in panel-utils.ts
|
|
818
|
+
export function deriveTabId(type: TabType, metadata?: Record<string, unknown>): string {
|
|
819
|
+
switch (type) {
|
|
820
|
+
case "editor":
|
|
821
|
+
return `editor:${metadata?.filePath ?? "untitled"}`;
|
|
822
|
+
case "chat": {
|
|
823
|
+
const provider = metadata?.providerId ?? "default";
|
|
824
|
+
return `chat:${provider}/${metadata?.sessionId ?? randomId()}`;
|
|
825
|
+
}
|
|
826
|
+
case "terminal":
|
|
827
|
+
return `terminal:${metadata?.terminalIndex ?? 1}`;
|
|
828
|
+
case "git-graph":
|
|
829
|
+
return "git-graph"; // Singleton
|
|
830
|
+
// ... other cases
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### URL Format
|
|
836
|
+
|
|
837
|
+
URLs are built from project name + deterministic tab ID:
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
// Format: /project/{projectName}/{type}/{identifier}
|
|
841
|
+
/project/ppm // Project root (no active tab)
|
|
842
|
+
/project/ppm/editor/src/index.ts // Open editor
|
|
843
|
+
/project/ppm/chat/claude/abc123 // Open chat
|
|
844
|
+
/project/ppm/terminal/1 // Open terminal
|
|
845
|
+
/project/ppm/database/conn-1/users // Open database
|
|
846
|
+
/project/ppm/git-graph // Git history (singleton)
|
|
847
|
+
|
|
848
|
+
// URL building in use-url-sync.ts
|
|
849
|
+
export function buildUrl(projectName: string, tabId: string | null): string {
|
|
850
|
+
const colonIdx = tabId?.indexOf(":");
|
|
851
|
+
if (colonIdx === -1) {
|
|
852
|
+
// Singleton
|
|
853
|
+
url += `/${tabId}`;
|
|
854
|
+
} else {
|
|
855
|
+
const [type, identifier] = tabId.split(":", 1);
|
|
856
|
+
url += `/${type}/${identifier}`;
|
|
857
|
+
}
|
|
858
|
+
return url;
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Deep Linking
|
|
863
|
+
|
|
864
|
+
When user navigates to a URL, parse it and auto-create tabs:
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
// In use-url-sync hook
|
|
868
|
+
export function parseUrlState(): UrlState {
|
|
869
|
+
const match = path.match(/^\/project\/([^/]+)(?:\/([^/]+)(\/.*)?)?/);
|
|
870
|
+
const [, projectName, tabType, tabIdentifierPath] = match;
|
|
871
|
+
|
|
872
|
+
// Build tab metadata from URL
|
|
873
|
+
const metadata = buildMetadataFromUrl(tabType, tabIdentifier, projectName);
|
|
874
|
+
|
|
875
|
+
// Auto-open tab if it doesn't exist
|
|
876
|
+
if (metadata) {
|
|
877
|
+
panelStore.openTab({
|
|
878
|
+
type: tabType,
|
|
879
|
+
title: getTabTitle(tabType, metadata),
|
|
880
|
+
metadata,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Server-Side Workspace Persistence
|
|
887
|
+
|
|
888
|
+
Tab layouts are persisted in the `workspace_state` SQLite table:
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
// workspace.ts routes
|
|
892
|
+
GET /api/project/:name/workspace → { layout: PanelLayout, updatedAt: string }
|
|
893
|
+
PUT /api/project/:name/workspace → { updatedAt: string }
|
|
894
|
+
|
|
895
|
+
// PanelLayout structure
|
|
896
|
+
interface PanelLayout {
|
|
897
|
+
panels: Record<string, Panel>; // panelId → { tabs, activeTabId }
|
|
898
|
+
grid: string[][]; // Row/column grid of panel IDs
|
|
899
|
+
focusedPanelId: string;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Sync is debounced (1.5s) client-side after layout changes
|
|
903
|
+
// Latest-wins: server timestamp compared with client localStorage
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### Migration from Random IDs
|
|
907
|
+
|
|
908
|
+
Old tab IDs (tab-xxxx) are automatically migrated to deterministic format:
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
// In panel-utils.ts
|
|
912
|
+
export function migrateTabIdToDeterministic(tab: Tab): Tab {
|
|
913
|
+
if (tab.id.startsWith("tab-")) {
|
|
914
|
+
// Convert to deterministic ID
|
|
915
|
+
return {
|
|
916
|
+
...tab,
|
|
917
|
+
id: deriveTabId(tab.type, tab.metadata),
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
return tab;
|
|
921
|
+
}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
This happens on first load per project; old URLs redirect to project root.
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## Provider Pattern (Multi-Provider Architecture)
|
|
929
|
+
|
|
930
|
+
### AIProvider Interface
|
|
931
|
+
All AI providers implement the `AIProvider` interface from `src/types/chat.ts`:
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
interface AIProvider {
|
|
935
|
+
// Required methods
|
|
936
|
+
createSession(): Promise<Session>;
|
|
937
|
+
sendMessage(sessionId: string, message: string, context?: FileContext[]): AsyncIterable<ChatEvent>;
|
|
938
|
+
|
|
939
|
+
// Optional methods (v0.8.60+)
|
|
940
|
+
listModels?(): Promise<ModelOption[]>;
|
|
941
|
+
isAvailable?(): Promise<boolean>;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
interface ModelOption {
|
|
945
|
+
value: string; // Model ID (e.g., "claude-sonnet-4-6")
|
|
946
|
+
label: string; // Display name (e.g., "Claude Sonnet 4.6")
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Adding a New Provider
|
|
951
|
+
|
|
952
|
+
**Step 1: Create provider file** (`src/providers/{name}-provider.ts`)
|
|
953
|
+
```typescript
|
|
954
|
+
export class MyProvider implements AIProvider {
|
|
955
|
+
async createSession(): Promise<Session> {
|
|
956
|
+
// Return { id: string; ... }
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async *sendMessage(sessionId, message, context?) {
|
|
960
|
+
// Stream ChatEvent objects
|
|
961
|
+
yield { type: "text", content: "response" };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Optional: Discover available models
|
|
965
|
+
async listModels(): Promise<ModelOption[]> {
|
|
966
|
+
return [
|
|
967
|
+
{ value: "model-1", label: "Model 1" },
|
|
968
|
+
{ value: "model-2", label: "Model 2" }
|
|
969
|
+
];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Optional: Check provider availability
|
|
973
|
+
async isAvailable(): Promise<boolean> {
|
|
974
|
+
// Check binary exists, API key set, etc.
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
**Step 2: Register in ProviderRegistry** (`src/providers/registry.ts`)
|
|
981
|
+
```typescript
|
|
982
|
+
constructor() {
|
|
983
|
+
this.providers.set("my-provider", {
|
|
984
|
+
id: "my-provider",
|
|
985
|
+
name: "My Provider",
|
|
986
|
+
instance: new MyProvider()
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
**Step 3: Add to config schema** (`schemas/ppm-config.schema.json`)
|
|
992
|
+
```json
|
|
993
|
+
{
|
|
994
|
+
"ai.providers.my_provider": {
|
|
995
|
+
"type": "object",
|
|
996
|
+
"properties": {
|
|
997
|
+
"type": { "const": "my-type" },
|
|
998
|
+
"model": { "type": "string" }
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### CLI Provider Pattern
|
|
1005
|
+
|
|
1006
|
+
For subprocess-based providers (e.g., Cursor), extend `CliProvider`:
|
|
1007
|
+
|
|
1008
|
+
```typescript
|
|
1009
|
+
import { CliProvider } from "./cli-provider-base";
|
|
1010
|
+
|
|
1011
|
+
export class CursorProvider extends CliProvider {
|
|
1012
|
+
async listModels(): Promise<ModelOption[]> {
|
|
1013
|
+
// Cache result with TTL
|
|
1014
|
+
if (this.modelsCache && Date.now() < this.modelsCache.expiry) {
|
|
1015
|
+
return this.modelsCache.models;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
try {
|
|
1019
|
+
// Execute subprocess with timeout
|
|
1020
|
+
const result = await this.executeWithTimeout(
|
|
1021
|
+
"cursor-agent --list-models",
|
|
1022
|
+
10000 // 10 second timeout
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
const models = JSON.parse(result);
|
|
1026
|
+
|
|
1027
|
+
// Cache for 5 minutes
|
|
1028
|
+
this.modelsCache = {
|
|
1029
|
+
models,
|
|
1030
|
+
expiry: Date.now() + 5 * 60 * 1000
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
return models;
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
// Graceful fallback on timeout/error
|
|
1036
|
+
return [];
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
### Testing Providers
|
|
1043
|
+
|
|
1044
|
+
Mock for deterministic testing:
|
|
1045
|
+
|
|
1046
|
+
```typescript
|
|
1047
|
+
// tests/unit/providers/my-provider.test.ts
|
|
1048
|
+
describe("MyProvider", () => {
|
|
1049
|
+
it("should stream messages", async () => {
|
|
1050
|
+
const provider = new MyProvider();
|
|
1051
|
+
const session = await provider.createSession();
|
|
1052
|
+
|
|
1053
|
+
const events: ChatEvent[] = [];
|
|
1054
|
+
for await (const event of provider.sendMessage(session.id, "test")) {
|
|
1055
|
+
events.push(event);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
expect(events.length).toBeGreaterThan(0);
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
### Provider Registry Usage
|
|
1064
|
+
|
|
1065
|
+
```typescript
|
|
1066
|
+
// Get specific provider
|
|
1067
|
+
const provider = providerRegistry.get("claude");
|
|
1068
|
+
|
|
1069
|
+
// Get user-facing providers (for dropdowns)
|
|
1070
|
+
const providers = providerRegistry.list();
|
|
1071
|
+
|
|
1072
|
+
// Get all providers including mock (internal)
|
|
1073
|
+
const allProviders = providerRegistry.listAll();
|
|
1074
|
+
|
|
1075
|
+
// Check if provider is available
|
|
1076
|
+
const available = await provider.isAvailable?.();
|
|
1077
|
+
|
|
1078
|
+
// List models
|
|
1079
|
+
const models = await provider.listModels?.() ?? [];
|
|
1080
|
+
```
|