@hienlh/ppm 0.9.0-beta.2 → 0.9.0-beta.4
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 +10 -26
- package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-COwDPZl_.js} +1 -1
- package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-DCb0mkTp.js} +1 -1
- package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CuUkz5gb.js} +1 -1
- package/dist/web/assets/{arc-C2Qaz-ch.js → arc-D0bJaFyD.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-BVEUkQYB.js} +1 -1
- package/dist/web/assets/arrow-left-C_j9Ki73.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-CU2t4NHJ.js} +1 -1
- package/dist/web/assets/browser-tab-BhTdeeZd.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-DzjR91sM.js} +1 -1
- package/dist/web/assets/channel-CKNZAqoN.js +1 -0
- package/dist/web/assets/chat-tab-ZiiUVOxM.js +7 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-0YMkpW2S.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-Dp0pTM5r.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CuYKSUgJ.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-DvbvLUIN.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CcEW1AMZ.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cgt-qg75.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-JCLgVcaC.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-B82RP9ow.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-Pb-JMOgO.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-BRj-ZEvL.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-CBRPKraG.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-DNFj84V6.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BnPzQK-O.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-BRj25yO7.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BdXwVXjJ.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-LfXT4p8B.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-EdgQyTqa.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-D3thuSok.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-LdG7RqsM.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-BHFnnXOt.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DUmQrLsF.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-CsGTseUr.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-5W2emiq4.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-COdZIaX4.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +1 -0
- package/dist/web/assets/clone-DNDy9Sms.js +1 -0
- package/dist/web/assets/{code-editor-CQ7gq0Vj.js → code-editor-BRMOypkX.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-C1QJ6GPW.js} +1 -1
- package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-CWo8w9wK.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-Br4t5TRV.js} +1 -1
- package/dist/web/assets/database-viewer-CEoDpzPz.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-CkDC2uAj.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-NvhckwcA.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO--nUaNiyB.js} +1 -1
- package/dist/web/assets/{diff-viewer-BjtTemkK.js → diff-viewer-jDU2bcGj.js} +1 -1
- package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DK4QEZYh.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-B9h_Ba-v.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BVlftqyZ.js} +1 -1
- package/dist/web/assets/git-graph-DMQzw4Sp.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js} +1 -1
- package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BbbiUImY.js} +1 -1
- package/dist/web/assets/index-B4Iz1Wbi.css +2 -0
- package/dist/web/assets/index-QiSWS6f-.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +2 -0
- package/dist/web/assets/input-DGlv6gt_.js +41 -0
- package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-DXomfd7J.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-cW7SMLa_.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-DFQXUZsc.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-BMUhjxqj.js} +1 -1
- package/dist/web/assets/keybindings-store-BplH-yiN.js +1 -0
- package/dist/web/assets/{line-DBLLF7lH.js → line--xyfYP3x.js} +1 -1
- package/dist/web/assets/{linear-BLFWatDe.js → linear-BdqW7iQu.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BtPXdzTv.js → markdown-renderer-BCjJbGP8.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-BY8JfkE_.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-DIv-LMXG.js} +1 -1
- package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-CIoJK3nc.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-seSK40d1.js} +1 -1
- package/dist/web/assets/postgres-viewer-s0snZ9CL.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-BaRFqlsA.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-1WWjMQB_.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-DEGGYsk7.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BtRvoUTC.js} +1 -1
- package/dist/web/assets/{settings-store-Bbhg_ptG.js → settings-store-D3dJqGhB.js} +2 -2
- package/dist/web/assets/settings-tab-2YkgmrY0.js +1 -0
- package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C16aO8tn.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +1 -0
- package/dist/web/assets/switch-mjGtIVDJ.js +1 -0
- package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DSz5PQI0.js} +1 -1
- package/dist/web/assets/{terminal-tab--Gw14HP3.js → terminal-tab-MRg8y1xF.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DrjxCpEM.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +1 -0
- package/dist/web/assets/{use-monaco-theme-DHbyUrzJ.js → use-monaco-theme-BQzvItNE.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-DfYFnniI.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-BRvXOVlG.js} +1 -1
- package/dist/web/index.html +12 -10
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +260 -7
- package/docs/codebase-summary.md +255 -95
- package/docs/project-changelog.md +88 -1
- package/docs/system-architecture.md +177 -12
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +9 -0
- package/src/providers/cli-provider-base.ts +238 -0
- package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
- package/src/providers/cursor-cli/cursor-history.ts +207 -0
- package/src/providers/cursor-cli/cursor-provider.ts +146 -0
- package/src/providers/mock-provider.ts +1 -1
- package/src/providers/provider.interface.ts +1 -0
- package/src/providers/registry.ts +43 -4
- package/src/server/index.ts +8 -0
- package/src/server/routes/browser-preview.ts +89 -0
- package/src/server/routes/chat.ts +14 -3
- package/src/server/routes/settings.ts +14 -0
- package/src/server/ws/chat.ts +24 -8
- package/src/services/chat.service.ts +10 -15
- package/src/types/chat.ts +21 -2
- package/src/types/config.ts +33 -11
- package/src/utils/ndjson-line-parser.ts +36 -0
- package/src/web/components/browser/browser-tab.tsx +269 -0
- package/src/web/components/chat/chat-history-bar.tsx +49 -29
- package/src/web/components/chat/chat-tab.tsx +14 -2
- package/src/web/components/chat/message-input.tsx +91 -3
- package/src/web/components/chat/provider-selector.tsx +150 -0
- package/src/web/components/chat/session-picker.tsx +3 -1
- package/src/web/components/layout/command-palette.tsx +4 -0
- package/src/web/components/layout/editor-panel.tsx +1 -0
- package/src/web/components/layout/mobile-nav.tsx +2 -2
- package/src/web/components/layout/panel-layout.tsx +17 -1
- package/src/web/components/layout/tab-bar.tsx +2 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/settings/ai-settings-section.tsx +196 -137
- package/src/web/hooks/use-chat.ts +11 -0
- package/src/web/hooks/use-global-keybindings.ts +7 -0
- package/src/web/hooks/use-voice-input.ts +111 -0
- package/src/web/stores/keybindings-store.ts +1 -0
- package/src/web/stores/panel-store.ts +10 -10
- package/src/web/stores/tab-store.ts +2 -1
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
- package/dist/web/assets/channel-w7yboq56.js +0 -1
- package/dist/web/assets/chat-tab-DmF14O6G.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
- package/dist/web/assets/clone-BSi6cgDh.js +0 -1
- package/dist/web/assets/database-viewer-B27aRtdQ.js +0 -1
- package/dist/web/assets/git-graph-BGXo0o-J.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
- package/dist/web/assets/index-BAioKo_2.css +0 -2
- package/dist/web/assets/index-CfClIVo2.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
- package/dist/web/assets/input-Brjz2Vv-.js +0 -41
- package/dist/web/assets/keybindings-store-nDbczFnq.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
- package/dist/web/assets/postgres-viewer-BMg-qFcO.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
- package/dist/web/assets/settings-tab-NPuwQHzs.js +0 -1
- package/dist/web/assets/sqlite-viewer-CAsUczio.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
- package/snapshot-state.md +0 -1526
- package/test-tokens.mjs +0 -212
- /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
- /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
- /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
- /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
- /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
- /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
- /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
- /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
- /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
- /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
- /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
- /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
- /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
- /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
- /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
- /package/dist/web/assets/{utils-btZ8C8-R.js → utils-Bslrbb-G.js} +0 -0
|
@@ -205,7 +205,47 @@ interface AIProvider {
|
|
|
205
205
|
**Implementations:**
|
|
206
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.
|
|
207
207
|
- **mock-provider** (Testing) — Returns canned responses
|
|
208
|
-
- **
|
|
208
|
+
- **cursor-cli** (CLI-based) — Spawns `cursor-agent` CLI binary with NDJSON streaming. Extends `CliProvider` base class.
|
|
209
|
+
- **codex/gemini** (Planned) — Pluggable via `CliProvider` extension (~100-150 lines each)
|
|
210
|
+
|
|
211
|
+
#### Multi-Provider Architecture (v0.8.61+)
|
|
212
|
+
|
|
213
|
+
PPM supports multiple AI providers through a generic `AIProvider` interface and extensible base classes:
|
|
214
|
+
|
|
215
|
+
**Provider Types:**
|
|
216
|
+
1. **SDK-based** (claude-agent-sdk) — Uses Anthropic SDK for rich features (approvals, thinking blocks)
|
|
217
|
+
2. **CLI-based** (cursor-cli, codex, gemini) — Spawns external binary with NDJSON streaming
|
|
218
|
+
|
|
219
|
+
**Base Classes:**
|
|
220
|
+
- `AIProvider` interface — Defines required methods (createSession, sendMessage) + optional capabilities (abortQuery, getMessages, listSessionsByDir, ensureProjectPath)
|
|
221
|
+
- `CliProvider` abstract class — Shared spawn/parse/abort logic for all CLI-spawning providers
|
|
222
|
+
- Provider-specific subclasses implement: `buildArgs()`, `mapEvent()`, `extractSessionId()`, `isAvailable()`
|
|
223
|
+
|
|
224
|
+
**Streaming Infrastructure:**
|
|
225
|
+
- `parseNdjsonLines()` utility — Async generator that buffers partial TCP packets, yields complete JSON lines
|
|
226
|
+
- `ChatEvent` union type — Normalized event format across all providers (text, tool_use, thinking, approval_request, system, done, error)
|
|
227
|
+
- Event mappers translate provider-specific JSON → ChatEvent (e.g., Cursor's `reasoning` type → `thinking` event)
|
|
228
|
+
|
|
229
|
+
**Provider Registration & Bootstrap:**
|
|
230
|
+
- `ProviderRegistry` maintains active provider instances
|
|
231
|
+
- `bootstrapProviders()` async function checks `isAvailable()` on CLI providers before registering
|
|
232
|
+
- Graceful fallback: if Cursor binary not found, provider skips registration (no crash, logged as info)
|
|
233
|
+
- Config type `AIProviderConfig.type` union: `"agent-sdk" | "cli" | "mock"`
|
|
234
|
+
|
|
235
|
+
**CLI-Provider Features:**
|
|
236
|
+
- **Session capture** — Extract session ID from provider's init event, re-key process tracking
|
|
237
|
+
- **Workspace trust auto-retry** — Detect trust prompts in stderr, retry once with `--trust` flag
|
|
238
|
+
- **Process lifecycle** — Track active processes per session, escalate SIGTERM → SIGKILL on abort
|
|
239
|
+
- **History loading** — Override `listSessions()` to read native provider history (e.g., Cursor SQLite DAG)
|
|
240
|
+
- **Graceful degradation** — Missing binary → provider skipped, not fatal
|
|
241
|
+
|
|
242
|
+
**New Files (v0.8.61):**
|
|
243
|
+
- `src/utils/ndjson-line-parser.ts` — NDJSON streaming parser
|
|
244
|
+
- `src/providers/cli-provider-base.ts` — Abstract base class for CLI providers
|
|
245
|
+
- `src/providers/cursor-cli/cursor-provider.ts` — CursorCliProvider implementation
|
|
246
|
+
- `src/providers/cursor-cli/cursor-event-mapper.ts` — NDJSON → ChatEvent mapping
|
|
247
|
+
- `src/providers/cursor-cli/cursor-history.ts` — SQLite DAG reader for Cursor history
|
|
248
|
+
- `src/web/components/chat/provider-selector.tsx` — UI component for provider selection
|
|
209
249
|
|
|
210
250
|
---
|
|
211
251
|
|
|
@@ -453,7 +493,27 @@ Returns full updated config. Validates ranges/enums before writing.
|
|
|
453
493
|
|
|
454
494
|
---
|
|
455
495
|
|
|
456
|
-
## Chat Streaming Flow
|
|
496
|
+
## Chat Streaming Flow (Persistent AsyncGenerator Sessions)
|
|
497
|
+
|
|
498
|
+
### Architecture Overview (v0.8.55+)
|
|
499
|
+
|
|
500
|
+
PPM uses a **persistent streaming session** model instead of per-message query execution:
|
|
501
|
+
|
|
502
|
+
**Key Changes:**
|
|
503
|
+
- Provider maintains **long-lived AsyncGenerator streaming input** per chat session (not per message)
|
|
504
|
+
- Follow-up messages **push into the existing generator** instead of abort-and-replace
|
|
505
|
+
- **Single streaming loop** per session decoupled from WebSocket message handler
|
|
506
|
+
- Message priority support: `now` (interrupt current), `next` (queue first), `later` (queue at end)
|
|
507
|
+
- Supports image attachments in messages
|
|
508
|
+
|
|
509
|
+
**Design Benefits:**
|
|
510
|
+
- Continuous context preservation — multi-turn conversations flow naturally
|
|
511
|
+
- No SDK subprocess restarts between messages (faster)
|
|
512
|
+
- Clean separation: BE owns Claude connection, FE disconnect doesn't abort
|
|
513
|
+
- Message buffering on reconnect — clients that lose WS connection sync turn events
|
|
514
|
+
- Tool approvals don't restart the query — integrated into streaming loop
|
|
515
|
+
|
|
516
|
+
### Message Flow
|
|
457
517
|
|
|
458
518
|
```
|
|
459
519
|
User types: "Debug this function"
|
|
@@ -462,18 +522,26 @@ MessageInput.tsx calls useChat.sendMessage()
|
|
|
462
522
|
↓
|
|
463
523
|
useChat opens WebSocket: WS /ws/project/:name/chat/:sessionId
|
|
464
524
|
↓
|
|
465
|
-
Sends: { type: "message", content: "Debug..." }
|
|
525
|
+
Sends: { type: "message", content: "Debug...", priority?: "now"|"next"|"later" }
|
|
526
|
+
↓
|
|
527
|
+
WS handler in chat.ts receives message
|
|
466
528
|
↓
|
|
467
|
-
|
|
529
|
+
If already streaming with different content → abort previous + wait cleanup
|
|
530
|
+
If streaming, new message priority determines queue behavior:
|
|
531
|
+
• priority: "now" → abort current, restart with new content
|
|
532
|
+
• priority: "next" → push into pending queue (higher priority)
|
|
533
|
+
• priority: "later" → push to end of queue (FIFO)
|
|
534
|
+
↓
|
|
535
|
+
runStreamLoop() executes in detached async context
|
|
468
536
|
↓
|
|
469
537
|
ChatService calls provider.sendMessage() (async generator)
|
|
470
538
|
↓
|
|
471
|
-
Provider (Claude SDK)
|
|
472
|
-
1.
|
|
473
|
-
2.
|
|
474
|
-
3.
|
|
539
|
+
Provider (Claude SDK) yields events:
|
|
540
|
+
1. { type: "text", content: "Here's what..." }
|
|
541
|
+
2. { type: "text", content: " happens..." }
|
|
542
|
+
3. { type: "tool_use", tool: "read_file", input: {...} }
|
|
475
543
|
↓
|
|
476
|
-
|
|
544
|
+
Stream loop buffers + broadcasts to all connected clients:
|
|
477
545
|
{ type: "text", content: "Here's what..." }
|
|
478
546
|
{ type: "text", content: " happens..." }
|
|
479
547
|
{ type: "tool_use", tool: "read_file", input: {...} }
|
|
@@ -485,15 +553,112 @@ User sees tool approval prompt, clicks "Approve"
|
|
|
485
553
|
↓
|
|
486
554
|
Client sends: { type: "approval_response", requestId, approved: true }
|
|
487
555
|
↓
|
|
488
|
-
|
|
556
|
+
Provider continues streaming with tool result (no restart)
|
|
489
557
|
↓
|
|
490
|
-
|
|
558
|
+
If multiple messages queued, next message processes after done event
|
|
491
559
|
↓
|
|
492
560
|
Final response streamed, then: { type: "done", sessionId }
|
|
493
561
|
↓
|
|
494
|
-
|
|
562
|
+
Phase transitions to idle, clients can send new message
|
|
563
|
+
↓
|
|
564
|
+
useChat saves message to store, displays in chat history
|
|
495
565
|
```
|
|
496
566
|
|
|
567
|
+
### Session State Management
|
|
568
|
+
|
|
569
|
+
**Session Entry** (BE-owned, persists across FE disconnections):
|
|
570
|
+
```typescript
|
|
571
|
+
interface SessionEntry {
|
|
572
|
+
providerId: string; // Which AI provider (e.g., "claude")
|
|
573
|
+
clients: Set<ChatWsSocket>; // Connected FE clients (may be empty)
|
|
574
|
+
abort?: AbortController; // Current stream abort handle
|
|
575
|
+
projectPath?: string; // Project context
|
|
576
|
+
projectName?: string;
|
|
577
|
+
pingIntervals: Map<...>; // Per-client keepalive
|
|
578
|
+
phase: SessionPhase; // "initializing" | "connecting" | "thinking" | "streaming" | "idle"
|
|
579
|
+
cleanupTimer?: ReturnType<...>; // Auto-cleanup if no FE reconnects (5min)
|
|
580
|
+
pendingApprovalEvent?: {...}; // Current tool approval waiting
|
|
581
|
+
turnEvents: unknown[]; // Buffered events (for reconnect sync)
|
|
582
|
+
streamPromise?: Promise<void>; // Track ongoing runStreamLoop
|
|
583
|
+
permissionMode?: string; // Sticky permission mode for session
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Client Connection States:**
|
|
588
|
+
- **Active streaming + FE connected** → Events broadcast to all clients in real-time
|
|
589
|
+
- **Active streaming + FE disconnected** → Events buffered in turnEvents array, BE stream continues
|
|
590
|
+
- **FE reconnects** → Receive session_state + buffered turnEvents, resync with stream
|
|
591
|
+
- **Idle (no query running)** → Phase is "idle", ready for next message
|
|
592
|
+
- **Idle + no FE for 5min** → Cleanup timer removes session from memory
|
|
593
|
+
|
|
594
|
+
### Follow-up Messages
|
|
595
|
+
|
|
596
|
+
**Abort-and-Replace Pattern:**
|
|
597
|
+
```typescript
|
|
598
|
+
if (entry.phase !== "idle" && entry.abort) {
|
|
599
|
+
console.log(`[chat] aborting current query for new message`);
|
|
600
|
+
entry.abort.abort();
|
|
601
|
+
await entry.streamPromise; // Wait for cleanup
|
|
602
|
+
// Re-fetch entry — may have been mutated during cleanup
|
|
603
|
+
entry = activeSessions.get(sessionId)!;
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Multiple Message Queueing:**
|
|
608
|
+
- First message: immediately starts runStreamLoop
|
|
609
|
+
- Second message (while streaming): abort current, wait, start new runStreamLoop
|
|
610
|
+
- Priority modes (future): could queue messages for intelligent interleaving
|
|
611
|
+
|
|
612
|
+
### WebSocket Reconnection Sync
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
FE WebSocket closes (network issue, tab closes)
|
|
616
|
+
↓
|
|
617
|
+
BE keeps session alive, streaming continues
|
|
618
|
+
↓
|
|
619
|
+
FE reconnects: WS /ws/project/:name/chat/:sessionId
|
|
620
|
+
↓
|
|
621
|
+
open() handler checks activeSessions.get(sessionId)
|
|
622
|
+
↓
|
|
623
|
+
If exists (entry found):
|
|
624
|
+
1. Clear cleanup timer (FE is back)
|
|
625
|
+
2. Send session_state with current phase + pendingApproval
|
|
626
|
+
3. If phase !== "idle", send buffered turnEvents
|
|
627
|
+
4. Add WS to clients Set
|
|
628
|
+
↓
|
|
629
|
+
FE processes session_state, renders current phase
|
|
630
|
+
↓
|
|
631
|
+
FE applies buffered events to rebuild turn state
|
|
632
|
+
↓
|
|
633
|
+
FE displays: "reconnected, current phase: streaming" etc.
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Phase Transitions
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
idle → initializing → connecting → thinking/streaming ↔ thinking/streaming → idle
|
|
640
|
+
^ ↑ ↓
|
|
641
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**Phase Descriptions:**
|
|
645
|
+
- **idle** — No query running, ready to accept new message
|
|
646
|
+
- **initializing** — Preparing (permission checks, session resume)
|
|
647
|
+
- **connecting** — Waiting for first SDK event (heartbeat: "connecting" with elapsed time every 5s)
|
|
648
|
+
- **thinking** — Receiving thinking content (extended thinking)
|
|
649
|
+
- **streaming** — Receiving text/tool_use content (dynamic switch between thinking/streaming)
|
|
650
|
+
|
|
651
|
+
### Image Attachment Support
|
|
652
|
+
|
|
653
|
+
Messages can now include images:
|
|
654
|
+
```typescript
|
|
655
|
+
type ChatWsClientMessage =
|
|
656
|
+
| { type: "message"; content: string; images?: { id: string; data: string }[]; priority?: string }
|
|
657
|
+
| ...
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Images are passed to provider's message context and included in tool input/output.
|
|
661
|
+
|
|
497
662
|
---
|
|
498
663
|
|
|
499
664
|
## Terminal Flow
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
SessionInfo,
|
|
11
11
|
ChatEvent,
|
|
12
12
|
ChatMessage,
|
|
13
|
+
ModelOption,
|
|
13
14
|
} from "./provider.interface.ts";
|
|
14
15
|
import { configService } from "../services/config.service.ts";
|
|
15
16
|
import { updateFromSdkEvent } from "../services/claude-usage.service.ts";
|
|
@@ -239,6 +240,14 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
239
240
|
this.forkSources.set(sessionId, sourceSessionId);
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
async listModels(): Promise<ModelOption[]> {
|
|
244
|
+
return [
|
|
245
|
+
{ value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
246
|
+
{ value: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
247
|
+
{ value: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
|
|
242
251
|
/**
|
|
243
252
|
* Resolve a pending approval from FE (tool approval or AskUserQuestion answer).
|
|
244
253
|
* Called by WS handler when client sends approval_response.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIProvider,
|
|
3
|
+
Session,
|
|
4
|
+
SessionConfig,
|
|
5
|
+
SessionInfo,
|
|
6
|
+
ChatEvent,
|
|
7
|
+
SendMessageOpts,
|
|
8
|
+
} from "./provider.interface.ts";
|
|
9
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
10
|
+
import { parseNdjsonLines } from "../utils/ndjson-line-parser.ts";
|
|
11
|
+
import { configService } from "../services/config.service.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Abstract base class for CLI-spawning AI providers.
|
|
15
|
+
* Handles process lifecycle, NDJSON streaming, abort, and cleanup.
|
|
16
|
+
* Subclasses only implement event mapping + arg building.
|
|
17
|
+
*/
|
|
18
|
+
export abstract class CliProvider implements AIProvider {
|
|
19
|
+
abstract readonly id: string;
|
|
20
|
+
abstract readonly name: string;
|
|
21
|
+
abstract readonly cliCommand: string;
|
|
22
|
+
|
|
23
|
+
/** Build CLI args for a given message/session */
|
|
24
|
+
abstract buildArgs(params: {
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
message: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
permissionMode?: string;
|
|
29
|
+
isResume: boolean;
|
|
30
|
+
}): string[];
|
|
31
|
+
|
|
32
|
+
/** Map a raw JSON object from CLI stdout → ChatEvent[] */
|
|
33
|
+
abstract mapEvent(raw: unknown, sessionId: string): ChatEvent[];
|
|
34
|
+
|
|
35
|
+
/** Extract session ID from CLI init event (provider-specific) */
|
|
36
|
+
abstract extractSessionId(raw: unknown): string | null;
|
|
37
|
+
|
|
38
|
+
/** Check if CLI binary exists on this machine */
|
|
39
|
+
abstract isAvailable(): Promise<boolean>;
|
|
40
|
+
|
|
41
|
+
// --- Shared state ---
|
|
42
|
+
protected sessions = new Map<string, Session>();
|
|
43
|
+
protected activeProcesses = new Map<string, ChildProcess>();
|
|
44
|
+
private messageCount = new Map<string, number>();
|
|
45
|
+
|
|
46
|
+
// --- Session lifecycle ---
|
|
47
|
+
|
|
48
|
+
async createSession(config: SessionConfig): Promise<Session> {
|
|
49
|
+
const id = crypto.randomUUID();
|
|
50
|
+
const session: Session = {
|
|
51
|
+
id,
|
|
52
|
+
providerId: this.id,
|
|
53
|
+
title: config.title ?? "New Chat",
|
|
54
|
+
projectName: config.projectName,
|
|
55
|
+
projectPath: config.projectPath,
|
|
56
|
+
createdAt: new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
this.sessions.set(id, session);
|
|
59
|
+
this.messageCount.set(id, 0);
|
|
60
|
+
return session;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async resumeSession(sessionId: string): Promise<Session> {
|
|
64
|
+
const existing = this.sessions.get(sessionId);
|
|
65
|
+
if (existing) return existing;
|
|
66
|
+
const session: Session = {
|
|
67
|
+
id: sessionId,
|
|
68
|
+
providerId: this.id,
|
|
69
|
+
title: "Resumed Chat",
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
this.sessions.set(sessionId, session);
|
|
73
|
+
this.messageCount.set(sessionId, 1);
|
|
74
|
+
return session;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async listSessions(): Promise<SessionInfo[]> {
|
|
78
|
+
return Array.from(this.sessions.values()).map((s) => ({
|
|
79
|
+
id: s.id,
|
|
80
|
+
providerId: s.providerId,
|
|
81
|
+
title: s.title,
|
|
82
|
+
projectName: s.projectName,
|
|
83
|
+
createdAt: s.createdAt,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async deleteSession(sessionId: string): Promise<void> {
|
|
88
|
+
this.sessions.delete(sessionId);
|
|
89
|
+
this.messageCount.delete(sessionId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Streaming ---
|
|
93
|
+
|
|
94
|
+
async *sendMessage(
|
|
95
|
+
sessionId: string,
|
|
96
|
+
message: string,
|
|
97
|
+
opts?: SendMessageOpts,
|
|
98
|
+
): AsyncIterable<ChatEvent> {
|
|
99
|
+
if (!this.sessions.has(sessionId)) {
|
|
100
|
+
await this.resumeSession(sessionId);
|
|
101
|
+
}
|
|
102
|
+
const meta = this.sessions.get(sessionId)!;
|
|
103
|
+
|
|
104
|
+
if (meta.title === "New Chat") {
|
|
105
|
+
meta.title = message.slice(0, 50) + (message.length > 50 ? "..." : "");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const count = this.messageCount.get(sessionId) ?? 0;
|
|
109
|
+
const isResume = count > 0;
|
|
110
|
+
this.messageCount.set(sessionId, count + 1);
|
|
111
|
+
|
|
112
|
+
const config = this.getProviderConfig();
|
|
113
|
+
const args = this.buildArgs({
|
|
114
|
+
sessionId: isResume ? sessionId : undefined,
|
|
115
|
+
message,
|
|
116
|
+
model: config?.model,
|
|
117
|
+
permissionMode: opts?.permissionMode || config?.permission_mode,
|
|
118
|
+
isResume,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const cwd = meta.projectPath || process.cwd();
|
|
122
|
+
let capturedSessionId = isResume ? sessionId : null;
|
|
123
|
+
|
|
124
|
+
const proc = this.spawnProcess(args, cwd);
|
|
125
|
+
const processKey = sessionId;
|
|
126
|
+
this.activeProcesses.set(processKey, proc);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
for await (const raw of parseNdjsonLines(proc.stdout!)) {
|
|
130
|
+
if (!capturedSessionId) {
|
|
131
|
+
const extracted = this.extractSessionId(raw);
|
|
132
|
+
if (extracted) {
|
|
133
|
+
capturedSessionId = extracted;
|
|
134
|
+
if (capturedSessionId !== processKey) {
|
|
135
|
+
this.activeProcesses.delete(processKey);
|
|
136
|
+
this.activeProcesses.set(capturedSessionId, proc);
|
|
137
|
+
// Migrate session metadata to the real CLI-assigned ID
|
|
138
|
+
// so listSessions/getMessages use the correct key
|
|
139
|
+
const meta = this.sessions.get(processKey);
|
|
140
|
+
if (meta) {
|
|
141
|
+
this.sessions.delete(processKey);
|
|
142
|
+
meta.id = capturedSessionId;
|
|
143
|
+
this.sessions.set(capturedSessionId, meta);
|
|
144
|
+
}
|
|
145
|
+
const cnt = this.messageCount.get(processKey) ?? 0;
|
|
146
|
+
this.messageCount.delete(processKey);
|
|
147
|
+
this.messageCount.set(capturedSessionId, cnt);
|
|
148
|
+
// Notify frontend about the session ID change
|
|
149
|
+
yield { type: "session_migrated", oldSessionId: processKey, newSessionId: capturedSessionId } as ChatEvent;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const events = this.mapEvent(raw, capturedSessionId || sessionId);
|
|
155
|
+
for (const event of events) {
|
|
156
|
+
yield event;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const exitCode = await waitForExit(proc);
|
|
161
|
+
yield {
|
|
162
|
+
type: "done",
|
|
163
|
+
sessionId: capturedSessionId || sessionId,
|
|
164
|
+
resultSubtype: exitCode === 0 ? "success" : "error_during_execution",
|
|
165
|
+
};
|
|
166
|
+
} catch (err) {
|
|
167
|
+
yield {
|
|
168
|
+
type: "error",
|
|
169
|
+
message: err instanceof Error ? err.message : String(err),
|
|
170
|
+
};
|
|
171
|
+
yield {
|
|
172
|
+
type: "done",
|
|
173
|
+
sessionId: capturedSessionId || sessionId,
|
|
174
|
+
resultSubtype: "error_during_execution",
|
|
175
|
+
};
|
|
176
|
+
} finally {
|
|
177
|
+
this.activeProcesses.delete(capturedSessionId || processKey);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Abort ---
|
|
182
|
+
|
|
183
|
+
abortQuery(sessionId: string): void {
|
|
184
|
+
const proc = this.activeProcesses.get(sessionId);
|
|
185
|
+
if (!proc) return;
|
|
186
|
+
console.log(`[${this.id}] Aborting session: ${sessionId}`);
|
|
187
|
+
proc.kill("SIGTERM");
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
try { proc.kill("SIGKILL"); } catch { /* already dead */ }
|
|
190
|
+
}, 2000);
|
|
191
|
+
this.activeProcesses.delete(sessionId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Helpers ---
|
|
195
|
+
|
|
196
|
+
protected spawnProcess(args: string[], cwd: string): ChildProcess {
|
|
197
|
+
console.log(`[${this.id}] spawn: ${this.cliCommand} ${args.join(" ")} (cwd=${cwd})`);
|
|
198
|
+
const proc = spawn(this.cliCommand, args, {
|
|
199
|
+
cwd,
|
|
200
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
201
|
+
env: { ...process.env },
|
|
202
|
+
});
|
|
203
|
+
proc.stdin?.end();
|
|
204
|
+
|
|
205
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
206
|
+
const text = data.toString().trim();
|
|
207
|
+
if (text) console.error(`[${this.id}] stderr: ${text}`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return proc;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Read provider config from PPM settings */
|
|
214
|
+
protected getProviderConfig() {
|
|
215
|
+
try {
|
|
216
|
+
const ai = configService.get("ai");
|
|
217
|
+
return ai.providers[this.id] ?? null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Kill all active processes (cleanup on server start) */
|
|
224
|
+
cleanupAll(): void {
|
|
225
|
+
for (const [sessionId, proc] of this.activeProcesses) {
|
|
226
|
+
console.log(`[${this.id}] cleanup: killing orphaned process for session ${sessionId}`);
|
|
227
|
+
try { proc.kill("SIGTERM"); } catch { /* ignore */ }
|
|
228
|
+
}
|
|
229
|
+
this.activeProcesses.clear();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function waitForExit(proc: ChildProcess): Promise<number> {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
236
|
+
proc.on("error", (err) => reject(err));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ChatEvent } from "../provider.interface.ts";
|
|
2
|
+
|
|
3
|
+
interface CursorRawEvent {
|
|
4
|
+
type: string;
|
|
5
|
+
subtype?: string;
|
|
6
|
+
session_id?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
message?: {
|
|
9
|
+
content?: Array<{
|
|
10
|
+
type?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
toolName?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
args?: unknown;
|
|
15
|
+
input?: unknown;
|
|
16
|
+
toolCallId?: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
result?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Map a single Cursor NDJSON line → ChatEvent[].
|
|
25
|
+
* Returns empty array for events we don't care about.
|
|
26
|
+
*/
|
|
27
|
+
export function mapCursorEvent(raw: unknown, sessionId: string): ChatEvent[] {
|
|
28
|
+
const event = raw as CursorRawEvent;
|
|
29
|
+
if (!event?.type) return [];
|
|
30
|
+
|
|
31
|
+
switch (event.type) {
|
|
32
|
+
case "system":
|
|
33
|
+
if (event.subtype === "init") {
|
|
34
|
+
return [{ type: "system", subtype: "init" }];
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
|
|
38
|
+
case "user":
|
|
39
|
+
return [];
|
|
40
|
+
|
|
41
|
+
case "assistant": {
|
|
42
|
+
const events: ChatEvent[] = [];
|
|
43
|
+
const content = event.message?.content;
|
|
44
|
+
if (!Array.isArray(content)) return [];
|
|
45
|
+
|
|
46
|
+
for (const part of content) {
|
|
47
|
+
if (!part) continue;
|
|
48
|
+
|
|
49
|
+
if (part.type === "text" && part.text) {
|
|
50
|
+
events.push({ type: "text", content: part.text });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (part.type === "reasoning" && part.text) {
|
|
54
|
+
events.push({ type: "thinking", content: part.text });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (part.type === "tool-call" || part.type === "tool_use") {
|
|
58
|
+
const toolName = normalizeToolName(part.toolName || part.name || "Unknown");
|
|
59
|
+
const toolId = part.toolCallId || part.id || crypto.randomUUID();
|
|
60
|
+
events.push({
|
|
61
|
+
type: "tool_use",
|
|
62
|
+
tool: toolName,
|
|
63
|
+
input: part.args || part.input || {},
|
|
64
|
+
toolUseId: toolId,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return events;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "result":
|
|
72
|
+
return [];
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Normalize Cursor tool names to PPM standard */
|
|
80
|
+
function normalizeToolName(name: string): string {
|
|
81
|
+
switch (name) {
|
|
82
|
+
case "ApplyPatch": return "Edit";
|
|
83
|
+
default: return name;
|
|
84
|
+
}
|
|
85
|
+
}
|