@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.
Files changed (186) hide show
  1. package/CHANGELOG.md +9 -44
  2. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-COwDPZl_.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-DCb0mkTp.js} +1 -1
  4. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CuUkz5gb.js} +1 -1
  5. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-D0bJaFyD.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-BVEUkQYB.js} +1 -1
  8. package/dist/web/assets/arrow-left-C_j9Ki73.js +1 -0
  9. package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-CU2t4NHJ.js} +1 -1
  10. package/dist/web/assets/browser-tab-BhTdeeZd.js +1 -0
  11. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-DzjR91sM.js} +1 -1
  12. package/dist/web/assets/channel-CKNZAqoN.js +1 -0
  13. package/dist/web/assets/chat-tab-ZiiUVOxM.js +7 -0
  14. package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-0YMkpW2S.js} +1 -1
  15. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-Dp0pTM5r.js} +1 -1
  16. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CuYKSUgJ.js} +1 -1
  17. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-DvbvLUIN.js} +2 -2
  18. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CcEW1AMZ.js} +1 -1
  19. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cgt-qg75.js} +1 -1
  20. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-JCLgVcaC.js} +1 -1
  21. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-B82RP9ow.js} +1 -1
  22. package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +2 -0
  23. package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +1 -0
  24. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-Pb-JMOgO.js} +1 -1
  25. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-BRj-ZEvL.js} +1 -1
  26. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-CBRPKraG.js} +1 -1
  27. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-DNFj84V6.js} +1 -1
  28. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BnPzQK-O.js} +1 -1
  29. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-BRj25yO7.js} +1 -1
  30. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BdXwVXjJ.js} +1 -1
  31. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-LfXT4p8B.js} +1 -1
  32. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-EdgQyTqa.js} +1 -1
  33. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-D3thuSok.js} +1 -1
  34. package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +1 -0
  35. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-LdG7RqsM.js} +1 -1
  36. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-BHFnnXOt.js} +1 -1
  37. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DUmQrLsF.js} +1 -1
  38. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-CsGTseUr.js} +1 -1
  39. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-5W2emiq4.js} +1 -1
  40. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-COdZIaX4.js} +1 -1
  41. package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +1 -0
  42. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +1 -0
  43. package/dist/web/assets/clone-DNDy9Sms.js +1 -0
  44. package/dist/web/assets/{code-editor-D3VJc1tY.js → code-editor-BRMOypkX.js} +1 -1
  45. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-C1QJ6GPW.js} +1 -1
  46. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-CWo8w9wK.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-Br4t5TRV.js} +1 -1
  48. package/dist/web/assets/database-viewer-CEoDpzPz.js +1 -0
  49. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-CkDC2uAj.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-NvhckwcA.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO--nUaNiyB.js} +1 -1
  52. package/dist/web/assets/{diff-viewer-D5vGZJnH.js → diff-viewer-jDU2bcGj.js} +1 -1
  53. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DK4QEZYh.js} +1 -1
  54. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-B9h_Ba-v.js} +1 -1
  55. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BVlftqyZ.js} +1 -1
  56. package/dist/web/assets/git-graph-DMQzw4Sp.js +1 -0
  57. package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +1 -0
  58. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js} +1 -1
  59. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BbbiUImY.js} +1 -1
  60. package/dist/web/assets/index-B4Iz1Wbi.css +2 -0
  61. package/dist/web/assets/index-QiSWS6f-.js +37 -0
  62. package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +1 -0
  63. package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +2 -0
  64. package/dist/web/assets/input-DGlv6gt_.js +41 -0
  65. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-DXomfd7J.js} +1 -1
  66. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-cW7SMLa_.js} +1 -1
  67. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-DFQXUZsc.js} +1 -1
  68. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-BMUhjxqj.js} +1 -1
  69. package/dist/web/assets/keybindings-store-BplH-yiN.js +1 -0
  70. package/dist/web/assets/{line-DBLLF7lH.js → line--xyfYP3x.js} +1 -1
  71. package/dist/web/assets/{linear-BLFWatDe.js → linear-BdqW7iQu.js} +1 -1
  72. package/dist/web/assets/{markdown-renderer-DcGMlbRm.js → markdown-renderer-BCjJbGP8.js} +5 -5
  73. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-BY8JfkE_.js} +2 -2
  74. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-DIv-LMXG.js} +1 -1
  75. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-CIoJK3nc.js} +1 -1
  76. package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +1 -0
  77. package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +1 -0
  78. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-seSK40d1.js} +1 -1
  79. package/dist/web/assets/postgres-viewer-s0snZ9CL.js +1 -0
  80. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-BaRFqlsA.js} +1 -1
  81. package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +1 -0
  82. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-1WWjMQB_.js} +1 -1
  83. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-DEGGYsk7.js} +1 -1
  84. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BtRvoUTC.js} +1 -1
  85. package/dist/web/assets/{settings-store-Bbhg_ptG.js → settings-store-D3dJqGhB.js} +2 -2
  86. package/dist/web/assets/settings-tab-2YkgmrY0.js +1 -0
  87. package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +1 -0
  88. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C16aO8tn.js} +1 -1
  89. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +1 -0
  90. package/dist/web/assets/switch-mjGtIVDJ.js +1 -0
  91. package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DSz5PQI0.js} +1 -1
  92. package/dist/web/assets/{terminal-tab-DHMITI3S.js → terminal-tab-MRg8y1xF.js} +1 -1
  93. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DrjxCpEM.js} +1 -1
  94. package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +1 -0
  95. package/dist/web/assets/{use-monaco-theme-DHbyUrzJ.js → use-monaco-theme-BQzvItNE.js} +1 -1
  96. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-DfYFnniI.js} +1 -1
  97. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-BRvXOVlG.js} +1 -1
  98. package/dist/web/index.html +12 -10
  99. package/dist/web/sw.js +1 -1
  100. package/docs/code-standards.md +260 -7
  101. package/docs/codebase-summary.md +255 -95
  102. package/docs/project-changelog.md +88 -1
  103. package/docs/system-architecture.md +177 -12
  104. package/package.json +1 -1
  105. package/src/providers/claude-agent-sdk.ts +85 -212
  106. package/src/providers/cli-provider-base.ts +238 -0
  107. package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
  108. package/src/providers/cursor-cli/cursor-history.ts +207 -0
  109. package/src/providers/cursor-cli/cursor-provider.ts +146 -0
  110. package/src/providers/mock-provider.ts +1 -1
  111. package/src/providers/provider.interface.ts +1 -0
  112. package/src/providers/registry.ts +43 -4
  113. package/src/server/index.ts +8 -0
  114. package/src/server/routes/browser-preview.ts +89 -0
  115. package/src/server/routes/chat.ts +14 -3
  116. package/src/server/routes/settings.ts +14 -0
  117. package/src/server/ws/chat.ts +91 -106
  118. package/src/services/chat.service.ts +10 -15
  119. package/src/types/api.ts +1 -1
  120. package/src/types/chat.ts +21 -4
  121. package/src/types/config.ts +33 -11
  122. package/src/utils/ndjson-line-parser.ts +36 -0
  123. package/src/web/components/browser/browser-tab.tsx +269 -0
  124. package/src/web/components/chat/chat-history-bar.tsx +49 -29
  125. package/src/web/components/chat/chat-tab.tsx +17 -5
  126. package/src/web/components/chat/message-input.tsx +94 -43
  127. package/src/web/components/chat/provider-selector.tsx +150 -0
  128. package/src/web/components/chat/session-picker.tsx +3 -1
  129. package/src/web/components/layout/command-palette.tsx +4 -0
  130. package/src/web/components/layout/editor-panel.tsx +1 -0
  131. package/src/web/components/layout/mobile-nav.tsx +2 -2
  132. package/src/web/components/layout/panel-layout.tsx +17 -1
  133. package/src/web/components/layout/tab-bar.tsx +2 -0
  134. package/src/web/components/layout/tab-content.tsx +5 -0
  135. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  136. package/src/web/hooks/use-chat.ts +20 -21
  137. package/src/web/hooks/use-global-keybindings.ts +7 -0
  138. package/src/web/hooks/use-voice-input.ts +111 -0
  139. package/src/web/stores/keybindings-store.ts +1 -0
  140. package/src/web/stores/panel-store.ts +10 -10
  141. package/src/web/stores/tab-store.ts +2 -1
  142. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  143. package/dist/web/assets/channel-w7yboq56.js +0 -1
  144. package/dist/web/assets/chat-tab-DxkvWelV.js +0 -7
  145. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  146. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  147. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  148. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  149. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  150. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  151. package/dist/web/assets/database-viewer-qlwORhh0.js +0 -1
  152. package/dist/web/assets/git-graph-B2fHtKEc.js +0 -1
  153. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  154. package/dist/web/assets/index-BAioKo_2.css +0 -2
  155. package/dist/web/assets/index-Ccq6zi2E.js +0 -37
  156. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  157. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  158. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  159. package/dist/web/assets/keybindings-store-e3pqlQbf.js +0 -1
  160. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  161. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  162. package/dist/web/assets/postgres-viewer-CZzbMFtb.js +0 -1
  163. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  164. package/dist/web/assets/settings-tab-BOmLAhkD.js +0 -1
  165. package/dist/web/assets/sqlite-viewer-CrrzHXqq.js +0 -1
  166. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  167. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  168. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
  169. /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
  170. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
  171. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
  172. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
  173. /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
  174. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
  175. /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
  176. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
  177. /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
  178. /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
  179. /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
  180. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
  181. /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
  182. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
  183. /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
  184. /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
  185. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
  186. /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
- - **Note:** CLI provider removed (v2); agent SDK is sole AI provider with Windows CLI fallback
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
- Server routes to ChatService.streamMessage()
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) 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: {...} }
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
- ChatService wraps as WebSocket messages:
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
- ChatService.onToolApproval() executes tool (file_read, git commands, etc.)
556
+ Provider continues streaming with tool result (no restart)
489
557
 
490
- Provider continues streaming with tool result
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
- useChat closes WebSocket, saves message to store
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.0-beta.3",
3
+ "version": "0.9.0-beta.4",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -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; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> },
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: streamGen,
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
- // Close failed query and old channel, create new channel + query for retry
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: retryGen,
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: authRetryGen,
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, continue waiting for next turn
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
- continue;
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 (!hadAnyEvents) {
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
- // Subprocess crashed session will auto-recover on next message
1019
- console.warn(`[sdk] session=${sessionId} subprocess crashed: ${msg}`);
1020
- yield { type: "error", message: `SDK subprocess crashed. Send another message to auto-recover.` };
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
- // Final done event when query ends (crash, close, generator done)
1031
- // Skip if we already yielded done from the result handler (avoid duplicate)
1032
- if (!yieldedDone) {
1033
- yield {
1034
- type: "done",
1035
- sessionId,
1036
- resultSubtype: resultSubtype as any,
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
- /** Interrupt the current turn — session stays alive for the next message */
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