@hienlh/ppm 0.8.85 → 0.8.86
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/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +5 -187
- package/bun.lock +0 -5
- package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-5eBmZ_lt.js} +1 -1
- package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-DimLlN0y.js} +1 -1
- package/dist/web/assets/api-settings-CFw-lh5k.js +1 -0
- package/dist/web/assets/{arc-BAOivWpI.js → arc-D4SasZrA.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-nv0WbM7d.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-C1XvYrb8.js} +1 -1
- package/dist/web/assets/browser-tab-CmsL5eny.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW-CygDrbWJ.js} +1 -1
- package/dist/web/assets/channel-DmKoFTd_.js +1 -0
- package/dist/web/assets/chat-tab-CFWsf13Z.js +7 -0
- package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C2FDgsgT.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-jF4w6cat.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-BVCECZFi.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-DXTbeu5d.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BaZqOsTs.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Bky2tcH7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Cp4BK9A8.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-BosFEH7G.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-H5Gbjsbr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-CWerSUwS.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-FvwP7jUy.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-D1PI_ORP.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C7Vzk_AI.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-BceYBGYX.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-WPtzgxql.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-DlHXDeLY.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-Ci_Prygb.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-CO0zMN-z.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-IAEEzfpM.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-BLXalOgc.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dx1Ri_p2.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-m9pPGKn7.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-B_08ExbI.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-DqSOVcYe.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +1 -0
- package/dist/web/assets/clone-CijCFRT5.js +1 -0
- package/dist/web/assets/code-editor-H_dAh_fJ.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-DlL82QHu.js} +1 -1
- package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BmVoh2At.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-sDrRW9MQ.js} +1 -1
- package/dist/web/assets/database-viewer-DBzsgEJ8.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-ChnAhgni.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-DW1J1uJd.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-CQ32hyG_.js} +1 -1
- package/dist/web/assets/diff-viewer-DzS-OnAR.js +4 -0
- package/dist/web/assets/dist-0Va_2L7G.js +16 -0
- package/dist/web/assets/dist-D9irYETY.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-6CHo6nOw.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-DroDiNT0.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-DP0QBh8w.js} +1 -1
- package/dist/web/assets/git-graph-D3C7F8o3.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js} +1 -1
- package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CQBb2thr.js} +1 -1
- package/dist/web/assets/index-CIkjfera.js +31 -0
- package/dist/web/assets/index-WKLuYsBY.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +2 -0
- package/dist/web/assets/input-CGp1nFIg.js +1 -0
- package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B4kqZBtn.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-46yibrV5.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-BcmRwjK-.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-B619K53y.js} +1 -1
- package/dist/web/assets/keybindings-store-BdaoLwSo.js +1 -0
- package/dist/web/assets/{line-B78g-52T.js → line-1gcO63_w.js} +1 -1
- package/dist/web/assets/{linear-DP4mkX3m.js → linear-DfRqDoVd.js} +1 -1
- package/dist/web/assets/markdown-renderer-DH49Zag7.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-XtjZQOeM.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-CifOFo_q.js} +1 -1
- package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-BJYw-iDX.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-BuHUh_fO.js} +1 -1
- package/dist/web/assets/postgres-viewer-B9FYk8sD.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-Bau_hj6Z.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-Cq2b-uwp.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-qPxiTUcS.js} +1 -1
- package/dist/web/assets/settings-store-DWXGVHsE.js +2 -0
- package/dist/web/assets/settings-tab-D-q8pd-5.js +1 -0
- package/dist/web/assets/sqlite-viewer-CDqcTePw.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-Dulj2oa8.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +1 -0
- package/dist/web/assets/tab-store-BPeiymiH.js +1 -0
- package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-wKgpSPAT.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-BWyDnCYq.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +1 -0
- package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-B9Iv2bNV.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-ChXcMzBQ.js} +1 -1
- package/dist/web/index.html +11 -12
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +7 -232
- package/docs/codebase-summary.md +3 -9
- package/docs/design-guidelines.md +0 -21
- package/docs/project-changelog.md +1 -115
- package/docs/project-roadmap.md +19 -41
- package/docs/system-architecture.md +15 -212
- package/package.json +2 -3
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/restart.ts +1 -9
- package/src/cli/commands/status.ts +0 -19
- package/src/index.ts +3 -2
- package/src/providers/claude-agent-sdk.ts +31 -94
- package/src/providers/mock-provider.ts +1 -6
- package/src/server/index.ts +166 -38
- package/src/server/routes/chat.ts +3 -52
- package/src/server/routes/project-scoped.ts +0 -2
- package/src/server/routes/proxy.ts +53 -46
- package/src/server/routes/tunnel.ts +32 -0
- package/src/server/ws/chat.ts +146 -207
- package/src/services/account-selector.service.ts +8 -16
- package/src/services/account.service.ts +13 -19
- package/src/services/claude-usage.service.ts +11 -48
- package/src/services/cloud.service.ts +6 -10
- package/src/services/db.service.ts +6 -111
- package/src/services/port-tunnel.service.ts +97 -0
- package/src/services/proxy.service.ts +19 -4
- package/src/services/supervisor.ts +25 -285
- package/src/types/api.ts +1 -9
- package/src/types/chat.ts +1 -3
- package/src/web/app.tsx +35 -41
- package/src/web/components/browser/browser-tab.tsx +97 -106
- package/src/web/components/chat/chat-history-bar.tsx +6 -72
- package/src/web/components/chat/chat-tab.tsx +16 -32
- package/src/web/components/chat/message-input.tsx +13 -107
- package/src/web/components/chat/message-list.tsx +15 -27
- package/src/web/components/chat/session-picker.tsx +31 -78
- package/src/web/components/chat/usage-badge.tsx +1 -11
- package/src/web/components/editor/code-editor.tsx +26 -36
- package/src/web/components/layout/command-palette.tsx +1 -3
- package/src/web/components/layout/editor-panel.tsx +18 -162
- package/src/web/components/layout/panel-layout.tsx +1 -17
- package/src/web/components/settings/proxy-settings-section.tsx +42 -40
- package/src/web/hooks/use-chat.ts +201 -211
- package/src/web/hooks/use-global-keybindings.ts +2 -25
- package/src/web/hooks/use-server-reload.ts +0 -9
- package/src/web/hooks/use-url-sync.ts +21 -173
- package/src/web/stores/keybindings-store.ts +0 -1
- package/src/web/stores/panel-store.ts +19 -73
- package/src/web/stores/panel-utils.ts +3 -145
- package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-DaHGm_0i.js +0 -1
- package/dist/web/assets/channel-wrd-NHWf.js +0 -1
- package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
- package/dist/web/assets/chevron-right-DeV0ehiG.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
- package/dist/web/assets/clone-LRxlvnMj.js +0 -1
- package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
- package/dist/web/assets/csv-preview-DLqYtXxt.js +0 -10
- package/dist/web/assets/database-viewer-DXk79Nel.js +0 -1
- package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
- package/dist/web/assets/dist-DylI9XxN.js +0 -13
- package/dist/web/assets/dist-lF8CoYII.js +0 -41
- package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
- package/dist/web/assets/index-CgQXpBb_.css +0 -2
- package/dist/web/assets/index-DEeeRoka.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
- package/dist/web/assets/input-BglMT33g.js +0 -1
- package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
- package/dist/web/assets/lib-BQ34Db2e.js +0 -4
- package/dist/web/assets/markdown-renderer-Brj8_LQM.js +0 -69
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
- package/dist/web/assets/postgres-viewer-CwkTGmqy.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
- package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
- package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
- package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
- package/dist/web/assets/tab-store-BJw7OCmy.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
- package/dist/web/assets/use-monaco-theme-CNzekTN3.js +0 -11
- package/docs/streaming-input-guide.md +0 -267
- package/snapshot-state.md +0 -1526
- package/src/server/routes/browser-preview.ts +0 -159
- package/src/server/routes/workspace.ts +0 -35
- package/src/services/cloud-ws.service.ts +0 -227
- package/src/web/components/chat/account-rotation-settings.tsx +0 -163
- package/src/web/components/chat/chat-welcome.tsx +0 -148
- package/src/web/components/editor/csv-preview.tsx +0 -228
- package/src/web/components/editor/editor-breadcrumb.tsx +0 -216
- package/src/web/components/editor/editor-toolbar.tsx +0 -74
- package/src/web/components/shared/connection-lost-overlay.tsx +0 -89
- package/src/web/hooks/use-voice-input.ts +0 -111
- package/src/web/lib/csv-parser.ts +0 -134
- package/src/web/stores/connection-store.ts +0 -39
- package/test-tokens.mjs +0 -212
- /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-DOElml5u.js} +0 -0
- /package/dist/web/assets/{array-B9UHiPd-.js → array-CYkMkqnU.js} +0 -0
- /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-ChOTgl3e.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-HeHO0VhB.js} +0 -0
- /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-Beh6XjaL.js} +0 -0
- /package/dist/web/assets/{dist-CSJdAyA9.js → dist-BUYzeuKe.js} +0 -0
- /package/dist/web/assets/{init-DlZdxViB.js → init-Rr1s_RiX.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BB-mzMLb.js} +0 -0
- /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-CKoArbIw.js} +0 -0
- /package/dist/web/assets/{math-069Z4SuC.js → math-B7b0HgJF.js} +0 -0
- /package/dist/web/assets/{path-6uRLdFF7.js → path-BAQ3hXlG.js} +0 -0
- /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-DeiOTZKJ.js} +0 -0
- /package/dist/web/assets/{react-ER-4DN55.js → react-Dev-wu-s.js} +0 -0
- /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-Dwml_la6.js} +0 -0
- /package/dist/web/assets/{src-BqX54PbV.js → src-B_cC68fH.js} +0 -0
- /package/dist/web/assets/{table-C7X5UAEI.js → table-COiJDPRA.js} +0 -0
- /package/dist/web/assets/{tag-CCtdV063.js → tag-LMq02LfE.js} +0 -0
- /package/dist/web/assets/{utils-BNytJOb1.js → utils-btZ8C8-R.js} +0 -0
|
@@ -140,24 +140,10 @@ POST /api/db/connections/:id/query → Execute query (readonly ch
|
|
|
140
140
|
PATCH /api/db/connections/:id/cell → Update cell value (single)
|
|
141
141
|
GET /api/upgrade/status → Get current + available versions, install method
|
|
142
142
|
POST /api/upgrade/apply → Install new version, trigger supervisor self-replace
|
|
143
|
-
GET /api/project/:name/workspace → Get saved workspace layout + metadata
|
|
144
|
-
PUT /api/project/:name/workspace → Save workspace layout (layout JSON)
|
|
145
143
|
WS /ws/project/:name/chat/:sessionId → Chat streaming
|
|
146
144
|
WS /ws/project/:name/terminal/:id → Terminal I/O
|
|
147
145
|
```
|
|
148
146
|
|
|
149
|
-
**URL Format (Deterministic Tabs, v0.8.77+):**
|
|
150
|
-
```
|
|
151
|
-
/project/{name} → Project root (project switcher)
|
|
152
|
-
/project/{name}/editor/{filePath} → Open editor tab (e.g., src/index.ts)
|
|
153
|
-
/project/{name}/chat/{provider}/{sessionId} → Open chat tab
|
|
154
|
-
/project/{name}/terminal/{index} → Open terminal tab
|
|
155
|
-
/project/{name}/database/{connId}/{table} → Open database browser
|
|
156
|
-
/project/{name}/git-graph → Git history graph (singleton)
|
|
157
|
-
/project/{name}/settings → Settings panel (singleton)
|
|
158
|
-
```
|
|
159
|
-
Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `chat:claude/abc123`). Deep links auto-create missing tabs.
|
|
160
|
-
|
|
161
147
|
---
|
|
162
148
|
|
|
163
149
|
### Service Layer (Business Logic)
|
|
@@ -175,7 +161,7 @@ Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `
|
|
|
175
161
|
|---------|---------|-------------|
|
|
176
162
|
| **ChatService** | Session management, message streaming | createSession, streamMessage, getHistory |
|
|
177
163
|
| **ConfigService** | Config loading (YAML→SQLite migration) | load, save, getToken |
|
|
178
|
-
| **DbService** | SQLite persistence (
|
|
164
|
+
| **DbService** | SQLite persistence (9 tables, WAL, connections/accounts CRUD) | getDb, openTestDb, getConnections, insertConnection, deleteConnection, getTableCache |
|
|
179
165
|
| **TableCacheService** | Cache table metadata, search tables | syncTables, searchTables, invalidateCache |
|
|
180
166
|
| **GitService** | Git command execution | status, diff, commit, stage, branch |
|
|
181
167
|
| **FileService** | File operations with validation | read, write, tree, delete, mkdir |
|
|
@@ -219,47 +205,7 @@ interface AIProvider {
|
|
|
219
205
|
**Implementations:**
|
|
220
206
|
- **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.
|
|
221
207
|
- **mock-provider** (Testing) — Returns canned responses
|
|
222
|
-
- **
|
|
223
|
-
- **codex/gemini** (Planned) — Pluggable via `CliProvider` extension (~100-150 lines each)
|
|
224
|
-
|
|
225
|
-
#### Multi-Provider Architecture (v0.8.61+)
|
|
226
|
-
|
|
227
|
-
PPM supports multiple AI providers through a generic `AIProvider` interface and extensible base classes:
|
|
228
|
-
|
|
229
|
-
**Provider Types:**
|
|
230
|
-
1. **SDK-based** (claude-agent-sdk) — Uses Anthropic SDK for rich features (approvals, thinking blocks)
|
|
231
|
-
2. **CLI-based** (cursor-cli, codex, gemini) — Spawns external binary with NDJSON streaming
|
|
232
|
-
|
|
233
|
-
**Base Classes:**
|
|
234
|
-
- `AIProvider` interface — Defines required methods (createSession, sendMessage) + optional capabilities (abortQuery, getMessages, listSessionsByDir, ensureProjectPath)
|
|
235
|
-
- `CliProvider` abstract class — Shared spawn/parse/abort logic for all CLI-spawning providers
|
|
236
|
-
- Provider-specific subclasses implement: `buildArgs()`, `mapEvent()`, `extractSessionId()`, `isAvailable()`
|
|
237
|
-
|
|
238
|
-
**Streaming Infrastructure:**
|
|
239
|
-
- `parseNdjsonLines()` utility — Async generator that buffers partial TCP packets, yields complete JSON lines
|
|
240
|
-
- `ChatEvent` union type — Normalized event format across all providers (text, tool_use, thinking, approval_request, system, done, error)
|
|
241
|
-
- Event mappers translate provider-specific JSON → ChatEvent (e.g., Cursor's `reasoning` type → `thinking` event)
|
|
242
|
-
|
|
243
|
-
**Provider Registration & Bootstrap:**
|
|
244
|
-
- `ProviderRegistry` maintains active provider instances
|
|
245
|
-
- `bootstrapProviders()` async function checks `isAvailable()` on CLI providers before registering
|
|
246
|
-
- Graceful fallback: if Cursor binary not found, provider skips registration (no crash, logged as info)
|
|
247
|
-
- Config type `AIProviderConfig.type` union: `"agent-sdk" | "cli" | "mock"`
|
|
248
|
-
|
|
249
|
-
**CLI-Provider Features:**
|
|
250
|
-
- **Session capture** — Extract session ID from provider's init event, re-key process tracking
|
|
251
|
-
- **Workspace trust auto-retry** — Detect trust prompts in stderr, retry once with `--trust` flag
|
|
252
|
-
- **Process lifecycle** — Track active processes per session, escalate SIGTERM → SIGKILL on abort
|
|
253
|
-
- **History loading** — Override `listSessions()` to read native provider history (e.g., Cursor SQLite DAG)
|
|
254
|
-
- **Graceful degradation** — Missing binary → provider skipped, not fatal
|
|
255
|
-
|
|
256
|
-
**New Files (v0.8.61):**
|
|
257
|
-
- `src/utils/ndjson-line-parser.ts` — NDJSON streaming parser
|
|
258
|
-
- `src/providers/cli-provider-base.ts` — Abstract base class for CLI providers
|
|
259
|
-
- `src/providers/cursor-cli/cursor-provider.ts` — CursorCliProvider implementation
|
|
260
|
-
- `src/providers/cursor-cli/cursor-event-mapper.ts` — NDJSON → ChatEvent mapping
|
|
261
|
-
- `src/providers/cursor-cli/cursor-history.ts` — SQLite DAG reader for Cursor history
|
|
262
|
-
- `src/web/components/chat/provider-selector.tsx` — UI component for provider selection
|
|
208
|
+
- **Note:** CLI provider removed (v2); agent SDK is sole AI provider with Windows CLI fallback
|
|
263
209
|
|
|
264
210
|
---
|
|
265
211
|
|
|
@@ -274,11 +220,11 @@ PPM supports multiple AI providers through a generic `AIProvider` interface and
|
|
|
274
220
|
- Enforce security (no parent directory access)
|
|
275
221
|
|
|
276
222
|
**Key Patterns:**
|
|
277
|
-
- SQLite: WAL mode, foreign keys, lazy init, schema
|
|
223
|
+
- SQLite: WAL mode, foreign keys, lazy init, schema v1 with 6 tables
|
|
278
224
|
- Path validation: `projectPath/relativePath` only, reject `..`
|
|
279
225
|
- Caching: Directory trees cached with TTL
|
|
280
226
|
- Error handling: Descriptive messages (file not found, permission denied)
|
|
281
|
-
- Migration: Automatic YAML→SQLite migration on first run with new db.service
|
|
227
|
+
- Migration: Automatic YAML→SQLite migration on first run with new db.service
|
|
282
228
|
|
|
283
229
|
---
|
|
284
230
|
|
|
@@ -298,24 +244,6 @@ PPM supports multiple AI providers through a generic `AIProvider` interface and
|
|
|
298
244
|
const messages = chatStore((s) => s.messages); // Subscribe to messages only
|
|
299
245
|
```
|
|
300
246
|
|
|
301
|
-
#### Workspace Sync (v0.8.77+)
|
|
302
|
-
|
|
303
|
-
**Deterministic Tab IDs & URL Routing:**
|
|
304
|
-
- Tab IDs derived from type + metadata: `deriveTabId(type, metadata) → {type}:{identifier}`
|
|
305
|
-
- Examples: `editor:src/index.ts`, `chat:claude/abc123`, `terminal:1`, `git-graph`
|
|
306
|
-
- URLs rebuilt from active tab: `/project/{name}/{type}/{identifier}`
|
|
307
|
-
- Deep linking: URL → `parseUrlState()` → auto-create tabs if missing
|
|
308
|
-
|
|
309
|
-
**Workspace Persistence:**
|
|
310
|
-
1. **Client**: PanelStore layout (grid, panels, tabs) cached in localStorage per project
|
|
311
|
-
2. **Server**: Workspace JSON persisted in `workspace_state` SQLite table
|
|
312
|
-
3. **Sync Flow:**
|
|
313
|
-
- User loads project → fetch workspace from server (GET `/api/project/:name/workspace`)
|
|
314
|
-
- Latest-wins: server `updated_at` vs client localStorage timestamp
|
|
315
|
-
- Panel layout changes debounced (1.5s) → POST to server
|
|
316
|
-
- On reconnect: server layout restored, client edits queued
|
|
317
|
-
4. **Cross-Device:** Any device can load workspace, browser restores exact grid + active tabs
|
|
318
|
-
|
|
319
247
|
---
|
|
320
248
|
|
|
321
249
|
## Communication Protocols
|
|
@@ -525,27 +453,7 @@ Returns full updated config. Validates ranges/enums before writing.
|
|
|
525
453
|
|
|
526
454
|
---
|
|
527
455
|
|
|
528
|
-
## Chat Streaming Flow
|
|
529
|
-
|
|
530
|
-
### Architecture Overview (v0.8.55+)
|
|
531
|
-
|
|
532
|
-
PPM uses a **persistent streaming session** model instead of per-message query execution:
|
|
533
|
-
|
|
534
|
-
**Key Changes:**
|
|
535
|
-
- Provider maintains **long-lived AsyncGenerator streaming input** per chat session (not per message)
|
|
536
|
-
- Follow-up messages **push into the existing generator** instead of abort-and-replace
|
|
537
|
-
- **Single streaming loop** per session decoupled from WebSocket message handler
|
|
538
|
-
- Message priority support: `now` (interrupt current), `next` (queue first), `later` (queue at end)
|
|
539
|
-
- Supports image attachments in messages
|
|
540
|
-
|
|
541
|
-
**Design Benefits:**
|
|
542
|
-
- Continuous context preservation — multi-turn conversations flow naturally
|
|
543
|
-
- No SDK subprocess restarts between messages (faster)
|
|
544
|
-
- Clean separation: BE owns Claude connection, FE disconnect doesn't abort
|
|
545
|
-
- Message buffering on reconnect — clients that lose WS connection sync turn events
|
|
546
|
-
- Tool approvals don't restart the query — integrated into streaming loop
|
|
547
|
-
|
|
548
|
-
### Message Flow
|
|
456
|
+
## Chat Streaming Flow
|
|
549
457
|
|
|
550
458
|
```
|
|
551
459
|
User types: "Debug this function"
|
|
@@ -554,26 +462,18 @@ MessageInput.tsx calls useChat.sendMessage()
|
|
|
554
462
|
↓
|
|
555
463
|
useChat opens WebSocket: WS /ws/project/:name/chat/:sessionId
|
|
556
464
|
↓
|
|
557
|
-
Sends: { type: "message", content: "Debug..."
|
|
558
|
-
↓
|
|
559
|
-
WS handler in chat.ts receives message
|
|
465
|
+
Sends: { type: "message", content: "Debug..." }
|
|
560
466
|
↓
|
|
561
|
-
|
|
562
|
-
If streaming, new message priority determines queue behavior:
|
|
563
|
-
• priority: "now" → abort current, restart with new content
|
|
564
|
-
• priority: "next" → push into pending queue (higher priority)
|
|
565
|
-
• priority: "later" → push to end of queue (FIFO)
|
|
566
|
-
↓
|
|
567
|
-
runStreamLoop() executes in detached async context
|
|
467
|
+
Server routes to ChatService.streamMessage()
|
|
568
468
|
↓
|
|
569
469
|
ChatService calls provider.sendMessage() (async generator)
|
|
570
470
|
↓
|
|
571
|
-
Provider (Claude SDK)
|
|
572
|
-
1. { type: "text", content: "Here's what..." }
|
|
573
|
-
2. { type: "text", content: " happens..." }
|
|
574
|
-
3. { type: "tool_use", tool: "read_file", input: {...} }
|
|
471
|
+
Provider (Claude SDK) streams response:
|
|
472
|
+
1. Yields: { type: "text", content: "Here's what..." }
|
|
473
|
+
2. Yields: { type: "text", content: " happens..." }
|
|
474
|
+
3. Yields: { type: "tool_use", tool: "read_file", input: {...} }
|
|
575
475
|
↓
|
|
576
|
-
|
|
476
|
+
ChatService wraps as WebSocket messages:
|
|
577
477
|
{ type: "text", content: "Here's what..." }
|
|
578
478
|
{ type: "text", content: " happens..." }
|
|
579
479
|
{ type: "tool_use", tool: "read_file", input: {...} }
|
|
@@ -585,112 +485,15 @@ User sees tool approval prompt, clicks "Approve"
|
|
|
585
485
|
↓
|
|
586
486
|
Client sends: { type: "approval_response", requestId, approved: true }
|
|
587
487
|
↓
|
|
588
|
-
|
|
488
|
+
ChatService.onToolApproval() executes tool (file_read, git commands, etc.)
|
|
589
489
|
↓
|
|
590
|
-
|
|
490
|
+
Provider continues streaming with tool result
|
|
591
491
|
↓
|
|
592
492
|
Final response streamed, then: { type: "done", sessionId }
|
|
593
493
|
↓
|
|
594
|
-
|
|
595
|
-
↓
|
|
596
|
-
useChat saves message to store, displays in chat history
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
### Session State Management
|
|
600
|
-
|
|
601
|
-
**Session Entry** (BE-owned, persists across FE disconnections):
|
|
602
|
-
```typescript
|
|
603
|
-
interface SessionEntry {
|
|
604
|
-
providerId: string; // Which AI provider (e.g., "claude")
|
|
605
|
-
clients: Set<ChatWsSocket>; // Connected FE clients (may be empty)
|
|
606
|
-
abort?: AbortController; // Current stream abort handle
|
|
607
|
-
projectPath?: string; // Project context
|
|
608
|
-
projectName?: string;
|
|
609
|
-
pingIntervals: Map<...>; // Per-client keepalive
|
|
610
|
-
phase: SessionPhase; // "initializing" | "connecting" | "thinking" | "streaming" | "idle"
|
|
611
|
-
cleanupTimer?: ReturnType<...>; // Auto-cleanup if no FE reconnects (5min)
|
|
612
|
-
pendingApprovalEvent?: {...}; // Current tool approval waiting
|
|
613
|
-
turnEvents: unknown[]; // Buffered events (for reconnect sync)
|
|
614
|
-
streamPromise?: Promise<void>; // Track ongoing runStreamLoop
|
|
615
|
-
permissionMode?: string; // Sticky permission mode for session
|
|
616
|
-
}
|
|
494
|
+
useChat closes WebSocket, saves message to store
|
|
617
495
|
```
|
|
618
496
|
|
|
619
|
-
**Client Connection States:**
|
|
620
|
-
- **Active streaming + FE connected** → Events broadcast to all clients in real-time
|
|
621
|
-
- **Active streaming + FE disconnected** → Events buffered in turnEvents array, BE stream continues
|
|
622
|
-
- **FE reconnects** → Receive session_state + buffered turnEvents, resync with stream
|
|
623
|
-
- **Idle (no query running)** → Phase is "idle", ready for next message
|
|
624
|
-
- **Idle + no FE for 5min** → Cleanup timer removes session from memory
|
|
625
|
-
|
|
626
|
-
### Follow-up Messages
|
|
627
|
-
|
|
628
|
-
**Abort-and-Replace Pattern:**
|
|
629
|
-
```typescript
|
|
630
|
-
if (entry.phase !== "idle" && entry.abort) {
|
|
631
|
-
console.log(`[chat] aborting current query for new message`);
|
|
632
|
-
entry.abort.abort();
|
|
633
|
-
await entry.streamPromise; // Wait for cleanup
|
|
634
|
-
// Re-fetch entry — may have been mutated during cleanup
|
|
635
|
-
entry = activeSessions.get(sessionId)!;
|
|
636
|
-
}
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
**Multiple Message Queueing:**
|
|
640
|
-
- First message: immediately starts runStreamLoop
|
|
641
|
-
- Second message (while streaming): abort current, wait, start new runStreamLoop
|
|
642
|
-
- Priority modes (future): could queue messages for intelligent interleaving
|
|
643
|
-
|
|
644
|
-
### WebSocket Reconnection Sync
|
|
645
|
-
|
|
646
|
-
```
|
|
647
|
-
FE WebSocket closes (network issue, tab closes)
|
|
648
|
-
↓
|
|
649
|
-
BE keeps session alive, streaming continues
|
|
650
|
-
↓
|
|
651
|
-
FE reconnects: WS /ws/project/:name/chat/:sessionId
|
|
652
|
-
↓
|
|
653
|
-
open() handler checks activeSessions.get(sessionId)
|
|
654
|
-
↓
|
|
655
|
-
If exists (entry found):
|
|
656
|
-
1. Clear cleanup timer (FE is back)
|
|
657
|
-
2. Send session_state with current phase + pendingApproval
|
|
658
|
-
3. If phase !== "idle", send buffered turnEvents
|
|
659
|
-
4. Add WS to clients Set
|
|
660
|
-
↓
|
|
661
|
-
FE processes session_state, renders current phase
|
|
662
|
-
↓
|
|
663
|
-
FE applies buffered events to rebuild turn state
|
|
664
|
-
↓
|
|
665
|
-
FE displays: "reconnected, current phase: streaming" etc.
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
### Phase Transitions
|
|
669
|
-
|
|
670
|
-
```
|
|
671
|
-
idle → initializing → connecting → thinking/streaming ↔ thinking/streaming → idle
|
|
672
|
-
^ ↑ ↓
|
|
673
|
-
└──────────────────────────────────────────────────────────────────────────┘
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
**Phase Descriptions:**
|
|
677
|
-
- **idle** — No query running, ready to accept new message
|
|
678
|
-
- **initializing** — Preparing (permission checks, session resume)
|
|
679
|
-
- **connecting** — Waiting for first SDK event (heartbeat: "connecting" with elapsed time every 5s)
|
|
680
|
-
- **thinking** — Receiving thinking content (extended thinking)
|
|
681
|
-
- **streaming** — Receiving text/tool_use content (dynamic switch between thinking/streaming)
|
|
682
|
-
|
|
683
|
-
### Image Attachment Support
|
|
684
|
-
|
|
685
|
-
Messages can now include images:
|
|
686
|
-
```typescript
|
|
687
|
-
type ChatWsClientMessage =
|
|
688
|
-
| { type: "message"; content: string; images?: { id: string; data: string }[]; priority?: string }
|
|
689
|
-
| ...
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
Images are passed to provider's message context and included in tool input/output.
|
|
693
|
-
|
|
694
497
|
---
|
|
695
498
|
|
|
696
499
|
## Terminal Flow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hienlh/ppm",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.86",
|
|
4
4
|
"description": "Personal Project Manager — mobile-first web IDE with AI assistance",
|
|
5
5
|
"author": "hienlh",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"dev": "concurrently \"bun run dev:server\" \"bun run dev:web\"",
|
|
14
|
-
"dev:server": "bun run --hot src/
|
|
14
|
+
"dev:server": "bun run --hot src/index.ts start --profile dev -f",
|
|
15
15
|
"dev:web": "bun run vite --config vite.config.ts",
|
|
16
16
|
"build:web": "bun run vite build --config vite.config.ts",
|
|
17
17
|
"build": "bun run build:web && bun build src/index.ts --compile --outfile dist/ppm",
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"@radix-ui/react-switch": "^1.2.6",
|
|
46
46
|
"@skitee3000/bun-pty": "^0.3.3",
|
|
47
47
|
"@tanstack/react-table": "^8.21.3",
|
|
48
|
-
"@tanstack/react-virtual": "^3.13.23",
|
|
49
48
|
"@uiw/react-codemirror": "^4.25.8",
|
|
50
49
|
"@xterm/addon-fit": "^0.11.0",
|
|
51
50
|
"@xterm/addon-web-links": "^0.12.0",
|
|
@@ -10,7 +10,7 @@ export function registerAutoStartCommands(program: Command): void {
|
|
|
10
10
|
.command("enable")
|
|
11
11
|
.description("Register PPM to start automatically on boot")
|
|
12
12
|
.option("-p, --port <port>", "Override port")
|
|
13
|
-
.option("-s, --share", "
|
|
13
|
+
.option("-s, --share", "Enable Cloudflare tunnel on boot")
|
|
14
14
|
.option("-c, --config <path>", "Config file path")
|
|
15
15
|
.option("--profile <name>", "DB profile name")
|
|
16
16
|
.action(async (options) => {
|
|
@@ -9,7 +9,7 @@ const RESTARTING_FLAG = resolve(PPM_DIR, ".restarting");
|
|
|
9
9
|
const RESTART_RESULT = resolve(PPM_DIR, ".restart-result");
|
|
10
10
|
|
|
11
11
|
/** Restart only the server process, keeping the tunnel alive */
|
|
12
|
-
export async function restartServer(options: { config?: string
|
|
12
|
+
export async function restartServer(options: { config?: string }) {
|
|
13
13
|
// Ignore SIGHUP so this process survives when PPM terminal dies
|
|
14
14
|
process.on("SIGHUP", () => {});
|
|
15
15
|
|
|
@@ -34,14 +34,6 @@ export async function restartServer(options: { config?: string; force?: boolean
|
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Check if supervisor is paused — require --force to resume
|
|
38
|
-
const state = status.state as string | undefined;
|
|
39
|
-
if (state === "paused" && !options.force) {
|
|
40
|
-
console.log("\n Server is paused (crashed too many times).");
|
|
41
|
-
console.log(" Use 'ppm restart --force' to resume.\n");
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
37
|
const oldServerPid = status.pid as number | undefined;
|
|
46
38
|
console.log("\n Restarting PPM server via supervisor...");
|
|
47
39
|
console.log(" If you're using PPM terminal, wait a few seconds for auto-reconnect.\n");
|
|
@@ -15,10 +15,6 @@ interface DaemonStatus {
|
|
|
15
15
|
tunnelAlive: boolean;
|
|
16
16
|
supervisorPid: number | null;
|
|
17
17
|
supervisorAlive: boolean;
|
|
18
|
-
state: string | null;
|
|
19
|
-
pausedAt: string | null;
|
|
20
|
-
pauseReason: string | null;
|
|
21
|
-
lastCrashError: string | null;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
function isAlive(pid: number): boolean {
|
|
@@ -30,7 +26,6 @@ function getDaemonStatus(): DaemonStatus {
|
|
|
30
26
|
running: false, pid: null, port: null, host: null,
|
|
31
27
|
shareUrl: null, tunnelPid: null, tunnelAlive: false,
|
|
32
28
|
supervisorPid: null, supervisorAlive: false,
|
|
33
|
-
state: null, pausedAt: null, pauseReason: null, lastCrashError: null,
|
|
34
29
|
};
|
|
35
30
|
|
|
36
31
|
if (existsSync(STATUS_FILE)) {
|
|
@@ -51,10 +46,6 @@ function getDaemonStatus(): DaemonStatus {
|
|
|
51
46
|
tunnelAlive,
|
|
52
47
|
supervisorPid,
|
|
53
48
|
supervisorAlive,
|
|
54
|
-
state: (data.state as string) ?? null,
|
|
55
|
-
pausedAt: (data.pausedAt as string) ?? null,
|
|
56
|
-
pauseReason: (data.pauseReason as string) ?? null,
|
|
57
|
-
lastCrashError: (data.lastCrashError as string) ?? null,
|
|
58
49
|
};
|
|
59
50
|
} catch { return dead; }
|
|
60
51
|
}
|
|
@@ -170,16 +161,6 @@ export async function showStatus(options: { json?: boolean; all?: boolean }) {
|
|
|
170
161
|
if (status.supervisorPid) {
|
|
171
162
|
console.log(` Supervisor: ${status.supervisorAlive ? "running" : "stopped"} (PID: ${status.supervisorPid})`);
|
|
172
163
|
}
|
|
173
|
-
// Show state info
|
|
174
|
-
const state = status.state ?? (status.running ? "running" : "stopped");
|
|
175
|
-
if (state === "paused") {
|
|
176
|
-
console.log(` State: PAUSED — ${status.pauseReason ?? "unknown reason"}`);
|
|
177
|
-
if (status.pausedAt) console.log(` Paused: ${status.pausedAt}`);
|
|
178
|
-
if (status.lastCrashError) console.log(` Error: ${status.lastCrashError}`);
|
|
179
|
-
console.log(`\n Resume: ppm restart --force`);
|
|
180
|
-
} else if (state === "upgrading") {
|
|
181
|
-
console.log(` State: UPGRADING`);
|
|
182
|
-
}
|
|
183
164
|
console.log(` Server: ${status.running ? "running" : "stopped"} (PID: ${status.pid})`);
|
|
184
165
|
if (status.port) console.log(` Local: http://localhost:${status.port}/`);
|
|
185
166
|
if (status.tunnelPid) {
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,9 @@ program
|
|
|
16
16
|
.command("start")
|
|
17
17
|
.description("Start the PPM server (background by default)")
|
|
18
18
|
.option("-p, --port <port>", "Port to listen on")
|
|
19
|
-
.option("-
|
|
19
|
+
.option("-f, --foreground", "Run in foreground (default: background daemon)")
|
|
20
|
+
.option("-d, --daemon", "Run as background daemon (default, kept for compat)")
|
|
21
|
+
.option("-s, --share", "Share via public URL (Cloudflare tunnel)")
|
|
20
22
|
.option("-c, --config <path>", "Path to config file (YAML import into DB)")
|
|
21
23
|
.option("--profile <name>", "DB profile name (e.g. 'dev' → ppm.dev.db)")
|
|
22
24
|
.action(async (options) => {
|
|
@@ -49,7 +51,6 @@ program
|
|
|
49
51
|
.command("restart")
|
|
50
52
|
.description("Restart the server (keeps tunnel alive)")
|
|
51
53
|
.option("-c, --config <path>", "Path to config file")
|
|
52
|
-
.option("--force", "Force resume from paused state")
|
|
53
54
|
.action(async (options) => {
|
|
54
55
|
const { restartServer } = await import("./cli/commands/restart.ts");
|
|
55
56
|
await restartServer(options);
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from "./provider.interface.ts";
|
|
14
14
|
import { configService } from "../services/config.service.ts";
|
|
15
15
|
import { updateFromSdkEvent } from "../services/claude-usage.service.ts";
|
|
16
|
-
import { getSessionMapping, setSessionMapping
|
|
16
|
+
import { getSessionMapping, setSessionMapping } from "../services/db.service.ts";
|
|
17
17
|
import { accountSelector } from "../services/account-selector.service.ts";
|
|
18
18
|
import { accountService } from "../services/account.service.ts";
|
|
19
19
|
import { resolve } from "node:path";
|
|
@@ -131,16 +131,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
131
131
|
return null;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/** Extract text content from an SDK assistant message */
|
|
135
|
-
private extractAssistantText(msg: unknown): string {
|
|
136
|
-
const content = (msg as any)?.message?.content;
|
|
137
|
-
if (!Array.isArray(content)) return "";
|
|
138
|
-
return content
|
|
139
|
-
.filter((b: any) => b.type === "text" && typeof b.text === "string")
|
|
140
|
-
.map((b: any) => b.text)
|
|
141
|
-
.join("");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
/** Read current provider config from yaml (fresh each call) */
|
|
145
135
|
private getProviderConfig(): Partial<import("../types/config.ts").AIProviderConfig> {
|
|
146
136
|
const ai = configService.get("ai");
|
|
@@ -176,11 +166,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
176
166
|
(s) => s.sessionId === sessionId || s.sessionId === mappedSdkId,
|
|
177
167
|
);
|
|
178
168
|
if (found) {
|
|
179
|
-
const dbTitle = getSessionTitle(found.sessionId);
|
|
180
169
|
const meta: Session = {
|
|
181
170
|
id: sessionId,
|
|
182
171
|
providerId: this.id,
|
|
183
|
-
title:
|
|
172
|
+
title: found.customTitle ?? found.summary ?? "Resumed Chat",
|
|
173
|
+
projectPath: (found as any).cwd || undefined,
|
|
184
174
|
createdAt: new Date(found.lastModified).toISOString(),
|
|
185
175
|
};
|
|
186
176
|
this.activeSessions.set(sessionId, meta);
|
|
@@ -211,13 +201,10 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
211
201
|
async listSessionsByDir(dir?: string): Promise<SessionInfo[]> {
|
|
212
202
|
try {
|
|
213
203
|
const sdkSessions = await sdkListSessions({ dir, limit: 50 });
|
|
214
|
-
// Overlay DB titles (user-set) over SDK titles
|
|
215
|
-
const ids = sdkSessions.map((s) => s.sessionId);
|
|
216
|
-
const dbTitles = getSessionTitles(ids);
|
|
217
204
|
return sdkSessions.map((s) => ({
|
|
218
205
|
id: s.sessionId,
|
|
219
206
|
providerId: this.id,
|
|
220
|
-
title:
|
|
207
|
+
title: s.customTitle ?? s.summary ?? s.firstPrompt ?? "Chat",
|
|
221
208
|
createdAt: new Date(s.lastModified).toISOString(),
|
|
222
209
|
updatedAt: new Date(s.lastModified).toISOString(),
|
|
223
210
|
}));
|
|
@@ -495,8 +482,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
495
482
|
if ((msg as any).type === "result" && (msg as any).subtype === "error_during_execution" && ((msg as any).num_turns ?? 0) === 0 && retryCount < MAX_RETRIES) {
|
|
496
483
|
retryCount++;
|
|
497
484
|
console.warn(`[sdk] transient error on first event — retrying (attempt ${retryCount}/${MAX_RETRIES})`);
|
|
498
|
-
//
|
|
499
|
-
|
|
485
|
+
// Resume existing SDK session on retry to preserve conversation history.
|
|
486
|
+
// If first message, SDK may have partially created the session — resume with sdkId.
|
|
487
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: sdkId };
|
|
500
488
|
const rq = query({
|
|
501
489
|
prompt: message,
|
|
502
490
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -516,24 +504,22 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
516
504
|
|
|
517
505
|
// Log all system events for debugging SDK lifecycle
|
|
518
506
|
if (msg.type === "system") {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
507
|
+
console.log(`[sdk] session=${sessionId} system: subtype=${(msg as any).subtype ?? "none"} ${JSON.stringify(msg).slice(0, 500)}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Capture SDK session metadata from init message
|
|
511
|
+
if (msg.type === "system" && (msg as any).subtype === "init") {
|
|
512
|
+
const initMsg = msg as any;
|
|
513
|
+
// SDK may assign a different session_id than our UUID
|
|
514
|
+
if (initMsg.session_id && initMsg.session_id !== sessionId) {
|
|
515
|
+
// Persist mapping so resume works after server restart
|
|
516
|
+
setSessionMapping(sessionId, initMsg.session_id);
|
|
517
|
+
// Update our in-memory mapping
|
|
518
|
+
const oldMeta = this.activeSessions.get(sessionId);
|
|
519
|
+
if (oldMeta) {
|
|
520
|
+
this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
|
|
531
521
|
}
|
|
532
522
|
}
|
|
533
|
-
|
|
534
|
-
// Yield system events so streaming loop can transition phases
|
|
535
|
-
// (e.g. connecting → thinking when hooks/init arrive)
|
|
536
|
-
yield { type: "system" as any, subtype } as any;
|
|
537
523
|
continue;
|
|
538
524
|
}
|
|
539
525
|
|
|
@@ -630,26 +616,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
630
616
|
// Full assistant message
|
|
631
617
|
if (msg.type === "assistant") {
|
|
632
618
|
// SDK assistant messages can carry an error field for auth/billing/rate-limit failures
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
// SDK sometimes returns errors as text content without setting a specific error field.
|
|
636
|
-
// Detect known HTTP error patterns in text and reclassify accordingly.
|
|
637
|
-
if (!assistantError || assistantError === "unknown") {
|
|
638
|
-
const textContent = this.extractAssistantText(msg);
|
|
639
|
-
if (textContent) {
|
|
640
|
-
if (/API Error:\s*401\b.*authentication_error/i.test(textContent)) {
|
|
641
|
-
assistantError = "authentication_failed";
|
|
642
|
-
console.warn(`[sdk] session=${sessionId} detected 401 in assistant text content — treating as auth error`);
|
|
643
|
-
} else if (/API Error:\s*5\d{2}\b/i.test(textContent) || /internal server error/i.test(textContent)) {
|
|
644
|
-
assistantError = "server_error";
|
|
645
|
-
console.warn(`[sdk] session=${sessionId} detected 5xx in assistant text content — treating as server error`);
|
|
646
|
-
} else if (/API Error:\s*429\b/i.test(textContent) || /rate.?limit/i.test(textContent)) {
|
|
647
|
-
assistantError = "rate_limit";
|
|
648
|
-
console.warn(`[sdk] session=${sessionId} detected 429/rate-limit in assistant text content — treating as rate limit`);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
619
|
+
const assistantError = (msg as any).error as string | undefined;
|
|
653
620
|
if (assistantError) {
|
|
654
621
|
// Dump full SDK message for debugging
|
|
655
622
|
console.error(`[sdk] session=${sessionId} cwd=${effectiveCwd} assistant error: ${assistantError} (isFirst=${isFirstMessage} retry=${retryCount})`);
|
|
@@ -660,13 +627,13 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
660
627
|
authRetried = true;
|
|
661
628
|
try {
|
|
662
629
|
await accountService.refreshAccessToken(account.id, false);
|
|
630
|
+
console.log(`[sdk] session=${sessionId} OAuth token refreshed for ${account.id} — retrying`);
|
|
631
|
+
// Re-build env with refreshed token
|
|
663
632
|
const refreshedAccount = accountService.getWithTokens(account.id);
|
|
664
633
|
if (refreshedAccount) {
|
|
665
|
-
const label = refreshedAccount.label ?? refreshedAccount.email ?? "Unknown";
|
|
666
|
-
console.log(`[sdk] session=${sessionId} OAuth token refreshed for ${account.id} (${label}) — retrying`);
|
|
667
|
-
yield { type: "account_retry" as const, reason: "Token refreshed", accountId: refreshedAccount.id, accountLabel: label };
|
|
668
634
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
669
|
-
|
|
635
|
+
// Resume existing SDK session to preserve conversation history
|
|
636
|
+
const retryOpts = { ...queryOptions, sessionId: undefined, resume: sdkId, env: retryEnv };
|
|
670
637
|
const rq = query({
|
|
671
638
|
prompt: message,
|
|
672
639
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
@@ -691,8 +658,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
691
658
|
};
|
|
692
659
|
const hint = errorHints[assistantError] ?? `API error: ${assistantError}`;
|
|
693
660
|
yield { type: "error", message: hint };
|
|
694
|
-
// Skip emitting the raw 401 error as text content — already shown as error event
|
|
695
|
-
continue;
|
|
696
661
|
}
|
|
697
662
|
const content = (msg as any).message?.content;
|
|
698
663
|
if (Array.isArray(content)) {
|
|
@@ -748,30 +713,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
748
713
|
yield { type: "error", message: "Rate limited. This account is now on cooldown. Please retry." };
|
|
749
714
|
break;
|
|
750
715
|
} else if (errCode === 401) {
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const refreshedAccount = accountService.getWithTokens(account.id);
|
|
757
|
-
if (refreshedAccount) {
|
|
758
|
-
const label = refreshedAccount.label ?? refreshedAccount.email ?? "Unknown";
|
|
759
|
-
console.log(`[sdk] 401 in result on account ${account.id} (${label}) — token refreshed, retrying`);
|
|
760
|
-
yield { type: "account_retry" as const, reason: "Token refreshed", accountId: refreshedAccount.id, accountLabel: label };
|
|
761
|
-
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
762
|
-
const retryOpts = { ...queryOptions, sessionId: undefined, resume: undefined, env: retryEnv };
|
|
763
|
-
const rq = query({
|
|
764
|
-
prompt: message,
|
|
765
|
-
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
766
|
-
});
|
|
767
|
-
this.activeQueries.set(sessionId, rq);
|
|
768
|
-
eventSource = rq;
|
|
769
|
-
continue retryLoop;
|
|
770
|
-
}
|
|
771
|
-
} catch {
|
|
772
|
-
accountSelector.onAuthError(account.id);
|
|
773
|
-
}
|
|
774
|
-
} else {
|
|
716
|
+
// Try refresh once
|
|
717
|
+
try {
|
|
718
|
+
await accountService.refreshAccessToken(account.id, false);
|
|
719
|
+
console.log(`[sdk] 401 on account ${account.id} — token refreshed`);
|
|
720
|
+
} catch {
|
|
775
721
|
accountSelector.onAuthError(account.id);
|
|
776
722
|
}
|
|
777
723
|
} else {
|
|
@@ -813,16 +759,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
813
759
|
}
|
|
814
760
|
|
|
815
761
|
// Surface non-success subtypes as errors so FE can display them
|
|
816
|
-
// But suppress abort errors — user-initiated cancel is not a real error
|
|
817
762
|
if (subtype && subtype !== "success") {
|
|
818
|
-
const errorsArr0 = Array.isArray(result.errors) ? result.errors : [];
|
|
819
|
-
const abortDetail = errorsArr0.join(" ") + " " + (typeof result.error === "string" ? result.error : "");
|
|
820
|
-
if (subtype === "error_during_execution" && /abort|request was aborted/i.test(abortDetail)) {
|
|
821
|
-
console.log(`[sdk] session=${sessionId} suppressing abort error (user-initiated cancel)`);
|
|
822
|
-
resultSubtype = subtype;
|
|
823
|
-
resultNumTurns = result.num_turns as number | undefined;
|
|
824
|
-
break;
|
|
825
|
-
}
|
|
826
763
|
// SDK error results use `errors: string[]` array (not singular `error`)
|
|
827
764
|
const errorsArr = Array.isArray(result.errors) ? result.errors : [];
|
|
828
765
|
const sdkDetail = errorsArr.length > 0
|
|
@@ -92,13 +92,8 @@ export class MockProvider implements AIProvider {
|
|
|
92
92
|
const abortController = new AbortController();
|
|
93
93
|
this.activeAborts.set(sessionId, abortController);
|
|
94
94
|
|
|
95
|
-
// Simulate SDK system events (hooks, init) — real SDK emits these before content
|
|
96
|
-
yield { type: "system" as any, subtype: "hook_started" } as any;
|
|
97
|
-
await sleep(50);
|
|
98
|
-
yield { type: "system" as any, subtype: "init" } as any;
|
|
99
|
-
|
|
100
95
|
// Simulate thinking delay
|
|
101
|
-
await sleep(
|
|
96
|
+
await sleep(300);
|
|
102
97
|
|
|
103
98
|
// Pick a response
|
|
104
99
|
const responseText =
|