@hienlh/ppm 0.9.0-beta.3 → 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 +9 -44
- 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-D3VJc1tY.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-D5vGZJnH.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-DcGMlbRm.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-DHMITI3S.js → terminal-tab-MRg8y1xF.js} +1 -1
- 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 +85 -212
- 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 +91 -106
- package/src/services/chat.service.ts +10 -15
- package/src/types/api.ts +1 -1
- package/src/types/chat.ts +21 -4
- 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 +17 -5
- package/src/web/components/chat/message-input.tsx +94 -43
- 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 +20 -21
- 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-DxkvWelV.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-qlwORhh0.js +0 -1
- package/dist/web/assets/git-graph-B2fHtKEc.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-Ccq6zi2E.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-e3pqlQbf.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-CZzbMFtb.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
- package/dist/web/assets/settings-tab-BOmLAhkD.js +0 -1
- package/dist/web/assets/sqlite-viewer-CrrzHXqq.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/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";
|
|
@@ -24,84 +25,6 @@ function getSdkSessionId(ppmId: string): string {
|
|
|
24
25
|
return getSessionMapping(ppmId) ?? ppmId;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// ── Streaming Input: message channel for persistent query ──
|
|
28
|
-
|
|
29
|
-
interface MessageController {
|
|
30
|
-
push(msg: any): void;
|
|
31
|
-
done(): void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function createMessageChannel(): {
|
|
35
|
-
generator: AsyncGenerator<any, void, undefined>;
|
|
36
|
-
controller: MessageController;
|
|
37
|
-
} {
|
|
38
|
-
const queue: any[] = [];
|
|
39
|
-
let resolve: ((msg: any) => void) | null = null;
|
|
40
|
-
let isDone = false;
|
|
41
|
-
|
|
42
|
-
async function* gen(): AsyncGenerator<any, void, undefined> {
|
|
43
|
-
while (!isDone) {
|
|
44
|
-
if (queue.length > 0) {
|
|
45
|
-
yield queue.shift()!;
|
|
46
|
-
} else {
|
|
47
|
-
const msg = await new Promise<any>((r) => { resolve = r; });
|
|
48
|
-
if (!isDone) yield msg;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
generator: gen(),
|
|
55
|
-
controller: {
|
|
56
|
-
push(msg: any) {
|
|
57
|
-
if (isDone) return;
|
|
58
|
-
if (resolve) {
|
|
59
|
-
const r = resolve;
|
|
60
|
-
resolve = null;
|
|
61
|
-
r(msg);
|
|
62
|
-
} else {
|
|
63
|
-
queue.push(msg);
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
done() {
|
|
67
|
-
isDone = true;
|
|
68
|
-
if (resolve) {
|
|
69
|
-
const r = resolve;
|
|
70
|
-
resolve = null;
|
|
71
|
-
r(null); // Unblock pending promise; isDone prevents yield
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Build a MessageParam with optional image content blocks */
|
|
79
|
-
function buildMessageParam(
|
|
80
|
-
text: string,
|
|
81
|
-
images?: Array<{ data: string; mediaType: string }>,
|
|
82
|
-
): { role: 'user'; content: string | any[] } {
|
|
83
|
-
if (!images || images.length === 0) {
|
|
84
|
-
return { role: 'user' as const, content: text };
|
|
85
|
-
}
|
|
86
|
-
const blocks: any[] = [];
|
|
87
|
-
for (const img of images) {
|
|
88
|
-
blocks.push({
|
|
89
|
-
type: 'image',
|
|
90
|
-
source: { type: 'base64', media_type: img.mediaType, data: img.data },
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
if (text.trim()) {
|
|
94
|
-
blocks.push({ type: 'text', text });
|
|
95
|
-
}
|
|
96
|
-
return { role: 'user' as const, content: blocks };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
interface StreamingSession {
|
|
100
|
-
meta: Session;
|
|
101
|
-
query: any;
|
|
102
|
-
controller: MessageController;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
28
|
/**
|
|
106
29
|
* Pending approval: canUseTool callback creates a promise,
|
|
107
30
|
* yields an approval_request event, then awaits resolution from FE.
|
|
@@ -127,8 +50,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
127
50
|
private activeQueries = new Map<string, { close: () => void }>();
|
|
128
51
|
/** Fork source: ppmSessionId → sourceSessionId (used on first message to fork) */
|
|
129
52
|
private forkSources = new Map<string, string>();
|
|
130
|
-
/** Streaming sessions: persistent query + message channel per session */
|
|
131
|
-
private streamingSessions = new Map<string, StreamingSession>();
|
|
132
53
|
|
|
133
54
|
/** Auth-related env keys for diagnostic logging */
|
|
134
55
|
private readonly AUTH_ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN"];
|
|
@@ -299,7 +220,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
299
220
|
}
|
|
300
221
|
|
|
301
222
|
async deleteSession(sessionId: string): Promise<void> {
|
|
302
|
-
this.closeStreamingSession(sessionId);
|
|
303
223
|
this.activeSessions.delete(sessionId);
|
|
304
224
|
this.messageCount.delete(sessionId);
|
|
305
225
|
}
|
|
@@ -320,6 +240,14 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
320
240
|
this.forkSources.set(sessionId, sourceSessionId);
|
|
321
241
|
}
|
|
322
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
|
+
|
|
323
251
|
/**
|
|
324
252
|
* Resolve a pending approval from FE (tool approval or AskUserQuestion answer).
|
|
325
253
|
* Called by WS handler when client sends approval_response.
|
|
@@ -332,63 +260,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
332
260
|
}
|
|
333
261
|
}
|
|
334
262
|
|
|
335
|
-
/**
|
|
336
|
-
* Push a follow-up message into an existing streaming session's generator.
|
|
337
|
-
* Called by WS handler for follow-up messages (Phase 2).
|
|
338
|
-
*/
|
|
339
|
-
pushMessage(sessionId: string, content: string, opts?: { priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> }): void {
|
|
340
|
-
const ss = this.streamingSessions.get(sessionId);
|
|
341
|
-
if (!ss) {
|
|
342
|
-
console.warn(`[sdk] pushMessage: no streaming session for ${sessionId}`);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const msgContent = buildMessageParam(content, opts?.images);
|
|
346
|
-
ss.controller.push({
|
|
347
|
-
type: 'user',
|
|
348
|
-
message: msgContent,
|
|
349
|
-
parent_tool_use_id: null,
|
|
350
|
-
session_id: sessionId,
|
|
351
|
-
priority: opts?.priority ?? 'next',
|
|
352
|
-
});
|
|
353
|
-
console.log(`[sdk] pushMessage: session=${sessionId} priority=${opts?.priority ?? 'next'}`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** Close a streaming session — generator + query cleanup */
|
|
357
|
-
closeStreamingSession(sessionId: string): void {
|
|
358
|
-
const ss = this.streamingSessions.get(sessionId);
|
|
359
|
-
if (ss) {
|
|
360
|
-
ss.controller.done();
|
|
361
|
-
ss.query.close();
|
|
362
|
-
this.streamingSessions.delete(sessionId);
|
|
363
|
-
console.log(`[sdk] closeStreamingSession: session=${sessionId}`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/** Check if a streaming session is active for a given session ID */
|
|
368
|
-
hasStreamingSession(sessionId: string): boolean {
|
|
369
|
-
return this.streamingSessions.has(sessionId);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
263
|
async *sendMessage(
|
|
373
264
|
sessionId: string,
|
|
374
265
|
message: string,
|
|
375
|
-
opts?: import("./provider.interface.ts").SendMessageOpts & { forkSession?: boolean
|
|
266
|
+
opts?: import("./provider.interface.ts").SendMessageOpts & { forkSession?: boolean },
|
|
376
267
|
): AsyncIterable<ChatEvent> {
|
|
377
|
-
// Follow-up: push into existing streaming session, yield nothing
|
|
378
|
-
const existingStream = this.streamingSessions.get(sessionId);
|
|
379
|
-
if (existingStream) {
|
|
380
|
-
const msgContent = buildMessageParam(message, opts?.images);
|
|
381
|
-
existingStream.controller.push({
|
|
382
|
-
type: 'user',
|
|
383
|
-
message: msgContent,
|
|
384
|
-
parent_tool_use_id: null,
|
|
385
|
-
session_id: sessionId,
|
|
386
|
-
priority: opts?.priority ?? 'next',
|
|
387
|
-
});
|
|
388
|
-
console.log(`[sdk] sendMessage follow-up: session=${sessionId} pushed to generator`);
|
|
389
|
-
return; // Events flow through first-message's consumer loop
|
|
390
|
-
}
|
|
391
|
-
|
|
392
268
|
if (!this.activeSessions.has(sessionId)) {
|
|
393
269
|
await this.resumeSession(sessionId);
|
|
394
270
|
}
|
|
@@ -511,7 +387,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
511
387
|
let resultSubtype: string | undefined;
|
|
512
388
|
let resultNumTurns: number | undefined;
|
|
513
389
|
let resultContextWindowPct: number | undefined;
|
|
514
|
-
let yieldedDone = false;
|
|
515
390
|
try {
|
|
516
391
|
// Resolve SDK's actual session ID for resume (may differ from PPM's UUID)
|
|
517
392
|
// For fork: use the source session's SDK id
|
|
@@ -583,25 +458,14 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
583
458
|
includePartialMessages: true,
|
|
584
459
|
};
|
|
585
460
|
|
|
586
|
-
// Streaming input: create message channel and persistent query
|
|
587
|
-
const { generator: streamGen, controller: streamCtrl } = createMessageChannel();
|
|
588
|
-
const firstMsg = {
|
|
589
|
-
type: 'user' as const,
|
|
590
|
-
message: buildMessageParam(message),
|
|
591
|
-
parent_tool_use_id: null,
|
|
592
|
-
session_id: sessionId,
|
|
593
|
-
};
|
|
594
|
-
streamCtrl.push(firstMsg);
|
|
595
|
-
|
|
596
461
|
const q = query({
|
|
597
|
-
prompt:
|
|
462
|
+
prompt: message,
|
|
598
463
|
options: {
|
|
599
464
|
...queryOptions,
|
|
600
465
|
...(permissionHooks && { hooks: permissionHooks }),
|
|
601
466
|
canUseTool,
|
|
602
467
|
} as any,
|
|
603
468
|
});
|
|
604
|
-
this.streamingSessions.set(sessionId, { meta, query: q, controller: streamCtrl });
|
|
605
469
|
this.activeQueries.set(sessionId, q);
|
|
606
470
|
let eventSource: AsyncIterable<any> = q;
|
|
607
471
|
|
|
@@ -616,29 +480,22 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
616
480
|
let retryCount = 0;
|
|
617
481
|
let authRetried = false;
|
|
618
482
|
|
|
619
|
-
let hadAnyEvents = false;
|
|
620
483
|
retryLoop: while (true) {
|
|
621
484
|
let sdkEventCount = 0;
|
|
622
485
|
for await (const msg of eventSource) {
|
|
623
486
|
sdkEventCount++;
|
|
624
|
-
hadAnyEvents = true;
|
|
625
487
|
if (sdkEventCount === 1) {
|
|
626
488
|
console.log(`[sdk] first event received: type=${(msg as any).type} subtype=${(msg as any).subtype ?? "none"}`);
|
|
627
489
|
// Detect immediate failure: first event is a result with error + 0 turns
|
|
628
490
|
if ((msg as any).type === "result" && (msg as any).subtype === "error_during_execution" && ((msg as any).num_turns ?? 0) === 0 && retryCount < MAX_RETRIES) {
|
|
629
491
|
retryCount++;
|
|
630
492
|
console.warn(`[sdk] transient error on first event — retrying (attempt ${retryCount}/${MAX_RETRIES})`);
|
|
631
|
-
//
|
|
632
|
-
streamCtrl.done();
|
|
633
|
-
q.close();
|
|
634
|
-
const { generator: retryGen, controller: retryCtrl } = createMessageChannel();
|
|
635
|
-
retryCtrl.push(firstMsg);
|
|
493
|
+
// Re-create query for retry — don't reuse sessionId in case SDK partially created it
|
|
636
494
|
const retryOpts = { ...queryOptions, sessionId: undefined, resume: undefined };
|
|
637
495
|
const rq = query({
|
|
638
|
-
prompt:
|
|
496
|
+
prompt: message,
|
|
639
497
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
640
498
|
});
|
|
641
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller: retryCtrl });
|
|
642
499
|
this.activeQueries.set(sessionId, rq);
|
|
643
500
|
eventSource = rq;
|
|
644
501
|
continue retryLoop;
|
|
@@ -784,17 +641,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
784
641
|
const refreshedAccount = accountService.getWithTokens(account.id);
|
|
785
642
|
if (refreshedAccount) {
|
|
786
643
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
787
|
-
// Close failed query and old channel, create new channel + query with refreshed token
|
|
788
|
-
streamCtrl.done();
|
|
789
|
-
q.close();
|
|
790
|
-
const { generator: authRetryGen, controller: authRetryCtrl } = createMessageChannel();
|
|
791
|
-
authRetryCtrl.push(firstMsg);
|
|
792
644
|
const retryOpts = { ...queryOptions, sessionId: undefined, resume: undefined, env: retryEnv };
|
|
793
645
|
const rq = query({
|
|
794
|
-
prompt:
|
|
646
|
+
prompt: message,
|
|
795
647
|
options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
|
|
796
648
|
});
|
|
797
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl });
|
|
798
649
|
this.activeQueries.set(sessionId, rq);
|
|
799
650
|
eventSource = rq;
|
|
800
651
|
continue retryLoop;
|
|
@@ -866,9 +717,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
866
717
|
const errCode = this.detectResultErrorCode(msg);
|
|
867
718
|
if (errCode === 429) {
|
|
868
719
|
accountSelector.onRateLimit(account.id);
|
|
869
|
-
// Post-stream 429 — surface error
|
|
720
|
+
// Post-stream 429 already has content — surface error to user
|
|
870
721
|
yield { type: "error", message: "Rate limited. This account is now on cooldown. Please retry." };
|
|
871
|
-
|
|
722
|
+
break;
|
|
872
723
|
} else if (errCode === 401) {
|
|
873
724
|
// Try refresh once
|
|
874
725
|
try {
|
|
@@ -976,26 +827,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
976
827
|
}
|
|
977
828
|
}
|
|
978
829
|
}
|
|
979
|
-
|
|
980
|
-
// Streaming input: yield done for this turn, then continue for next turn
|
|
981
|
-
yieldedDone = true;
|
|
982
|
-
yield {
|
|
983
|
-
type: "done",
|
|
984
|
-
sessionId,
|
|
985
|
-
resultSubtype: resultSubtype as any,
|
|
986
|
-
numTurns: resultNumTurns,
|
|
987
|
-
contextWindowPct: resultContextWindowPct,
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
// Reset per-turn state for next turn
|
|
991
|
-
lastPartialText = "";
|
|
992
|
-
pendingToolCount = 0;
|
|
993
|
-
assistantContent = "";
|
|
994
|
-
resultSubtype = undefined;
|
|
995
|
-
resultNumTurns = undefined;
|
|
996
|
-
resultContextWindowPct = undefined;
|
|
997
|
-
sdkEventCount = 0;
|
|
998
|
-
continue; // Wait for next turn from generator
|
|
830
|
+
break;
|
|
999
831
|
}
|
|
1000
832
|
}
|
|
1001
833
|
|
|
@@ -1004,7 +836,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1004
836
|
yield approvalEvents.shift()!;
|
|
1005
837
|
}
|
|
1006
838
|
|
|
1007
|
-
if (
|
|
839
|
+
if (sdkEventCount === 0) {
|
|
1008
840
|
yield { type: "error", message: "Claude did not respond. Check that 'claude' CLI works in your terminal." };
|
|
1009
841
|
}
|
|
1010
842
|
break; // Exit retryLoop — normal completion
|
|
@@ -1014,47 +846,88 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1014
846
|
console.error(`[sdk] session=${sessionId} cwd=${meta.projectPath} error: ${msg}`);
|
|
1015
847
|
if (msg.includes("abort") || msg.includes("closed")) {
|
|
1016
848
|
// User-initiated abort or WS closed — nothing to report
|
|
1017
|
-
} else if (msg.includes("exited with code")) {
|
|
1018
|
-
//
|
|
1019
|
-
console.warn(`[sdk] session
|
|
1020
|
-
|
|
849
|
+
} else if (!isFirstMessage && msg.includes("exited with code")) {
|
|
850
|
+
// SDK subprocess crashed during session resume — retry as fresh session
|
|
851
|
+
console.warn(`[sdk] session resume failed, retrying as fresh session`);
|
|
852
|
+
try {
|
|
853
|
+
const providerConfig = this.getProviderConfig();
|
|
854
|
+
const effectiveCwd = meta.projectPath || homedir();
|
|
855
|
+
const retryAccount = accountSelector.isEnabled() ? accountSelector.next() : null;
|
|
856
|
+
const queryEnv = this.buildQueryEnv(meta.projectPath, retryAccount);
|
|
857
|
+
const retryOptions = {
|
|
858
|
+
...(process.platform === "win32" && { executable: "node" }),
|
|
859
|
+
cwd: effectiveCwd,
|
|
860
|
+
systemPrompt: systemPromptOpt,
|
|
861
|
+
settingSources: ["user", "project"],
|
|
862
|
+
env: queryEnv,
|
|
863
|
+
settings: { permissions: { allow: [], deny: [] } },
|
|
864
|
+
allowedTools,
|
|
865
|
+
permissionMode,
|
|
866
|
+
allowDangerouslySkipPermissions: isBypass,
|
|
867
|
+
...(providerConfig.model && { model: providerConfig.model }),
|
|
868
|
+
maxTurns: providerConfig.max_turns ?? 100,
|
|
869
|
+
includePartialMessages: true,
|
|
870
|
+
};
|
|
871
|
+
const retryQuery = query({
|
|
872
|
+
prompt: message,
|
|
873
|
+
options: {
|
|
874
|
+
...retryOptions,
|
|
875
|
+
...(permissionHooks && { hooks: permissionHooks }),
|
|
876
|
+
canUseTool,
|
|
877
|
+
} as any,
|
|
878
|
+
});
|
|
879
|
+
this.activeQueries.set(sessionId, retryQuery);
|
|
880
|
+
for await (const retryMsg of retryQuery) {
|
|
881
|
+
if (retryMsg.type === "system") continue;
|
|
882
|
+
if (retryMsg.type === "result") {
|
|
883
|
+
const r = retryMsg as any;
|
|
884
|
+
if (r.subtype && r.subtype !== "success") {
|
|
885
|
+
const retryErrors = Array.isArray(r.errors) ? r.errors.join("\n") : "";
|
|
886
|
+
yield { type: "error", message: retryErrors || `Agent stopped: ${r.subtype}` };
|
|
887
|
+
}
|
|
888
|
+
resultSubtype = r.subtype;
|
|
889
|
+
resultNumTurns = r.num_turns;
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
if ((retryMsg as any).type === "assistant") {
|
|
893
|
+
const content = (retryMsg as any).message?.content;
|
|
894
|
+
if (Array.isArray(content)) {
|
|
895
|
+
for (const block of content) {
|
|
896
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
897
|
+
yield { type: "text", content: block.text };
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
} catch (retryErr) {
|
|
904
|
+
const retryMsg = (retryErr as Error).message ?? String(retryErr);
|
|
905
|
+
console.error(`[sdk] retry also failed: ${retryMsg}`);
|
|
906
|
+
yield { type: "error", message: `SDK error: ${msg}` };
|
|
907
|
+
}
|
|
1021
908
|
} else {
|
|
1022
909
|
yield { type: "error", message: `SDK error: ${msg}` };
|
|
1023
910
|
}
|
|
1024
911
|
} finally {
|
|
1025
912
|
this.activeQueries.delete(sessionId);
|
|
1026
|
-
this.streamingSessions.delete(sessionId);
|
|
1027
|
-
console.log(`[sdk] session=${sessionId} streaming session ended`);
|
|
1028
913
|
}
|
|
1029
914
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
numTurns: resultNumTurns,
|
|
1038
|
-
contextWindowPct: resultContextWindowPct,
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
915
|
+
yield {
|
|
916
|
+
type: "done",
|
|
917
|
+
sessionId,
|
|
918
|
+
resultSubtype: resultSubtype as any,
|
|
919
|
+
numTurns: resultNumTurns,
|
|
920
|
+
contextWindowPct: resultContextWindowPct,
|
|
921
|
+
};
|
|
1041
922
|
}
|
|
1042
923
|
|
|
1043
924
|
|
|
1044
|
-
/**
|
|
925
|
+
/** Abort an active query for a session */
|
|
1045
926
|
abortQuery(sessionId: string): void {
|
|
1046
|
-
const ss = this.streamingSessions.get(sessionId);
|
|
1047
|
-
if (ss && typeof ss.query.interrupt === "function") {
|
|
1048
|
-
ss.query.interrupt().catch(() => {});
|
|
1049
|
-
console.log(`[sdk] abortQuery: interrupted session=${sessionId}`);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
// Fallback: close query entirely and clean up streaming session
|
|
1053
927
|
const q = this.activeQueries.get(sessionId);
|
|
1054
928
|
if (q) {
|
|
1055
929
|
q.close();
|
|
1056
930
|
this.activeQueries.delete(sessionId);
|
|
1057
|
-
this.streamingSessions.delete(sessionId);
|
|
1058
931
|
}
|
|
1059
932
|
}
|
|
1060
933
|
|