@hienlh/ppm 0.8.85 → 0.8.86

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