@hienlh/ppm 0.8.86 → 0.8.87

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 (219) hide show
  1. package/CHANGELOG.md +193 -5
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-5eBmZ_lt.js → _basePickBy-5PGDJbfF.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-DimLlN0y.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  5. package/dist/web/assets/api-settings-Bx1GaNmQ.js +1 -0
  6. package/dist/web/assets/{arc-D4SasZrA.js → arc-BAOivWpI.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-nv0WbM7d.js → architectureDiagram-2XIMDMQ5-DWBCPMLF.js} +1 -1
  9. package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
  10. package/dist/web/assets/{blockDiagram-WCTKOSBZ-C1XvYrb8.js → blockDiagram-WCTKOSBZ-TEF8Ally.js} +1 -1
  11. package/dist/web/assets/browser-tab-DaHGm_0i.js +1 -0
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-CygDrbWJ.js → c4Diagram-IC4MRINW-dV22iAsY.js} +1 -1
  13. package/dist/web/assets/channel-wrd-NHWf.js +1 -0
  14. package/dist/web/assets/chat-tab-BDYE0KHF.js +8 -0
  15. package/dist/web/assets/chevron-right-DeV0ehiG.js +1 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-C2FDgsgT.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-jF4w6cat.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-BVCECZFi.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXTbeu5d.js → chunk-7R4GIKGN-BbIFzsIv.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-BaZqOsTs.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-Bky2tcH7.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-Cp4BK9A8.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-BosFEH7G.js → chunk-GEFDOKGD-BbQkJu8C.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-H5Gbjsbr.js → chunk-JSJVCQXG-23tyvw8k.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-CWerSUwS.js → chunk-KX2RTZJC-sQ0o-39C.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-FvwP7jUy.js → chunk-KYZI473N-BcUZNnwd.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-D1PI_ORP.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-C7Vzk_AI.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-BceYBGYX.js → chunk-NQ4KR5QH-wMgTlP7f.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-WPtzgxql.js → chunk-O4XLMI2P-JC6EGoUz.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-DlHXDeLY.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-Ci_Prygb.js → chunk-PQ6SQG4A-D6BTbCQw.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-CO0zMN-z.js → chunk-PU5JKC2W-Dw8ClWch.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-IAEEzfpM.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-BLXalOgc.js → chunk-WL4C6EOR-DfofndiH.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-Dx1Ri_p2.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-m9pPGKn7.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-B_08ExbI.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-DqSOVcYe.js → chunk-YBOYWFTD-CeU4Q-xC.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +1 -0
  45. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  46. package/dist/web/assets/code-editor-DTA3c9Y8.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-DlL82QHu.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
  48. package/dist/web/assets/csv-preview-DLqYtXxt.js +10 -0
  49. package/dist/web/assets/{dagre-BmVoh2At.js → dagre-Dbb5k38K.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-sDrRW9MQ.js → dagre-KLK3FWXG-BH7aWGRP.js} +1 -1
  51. package/dist/web/assets/database-viewer-DXk79Nel.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-ChnAhgni.js → diagram-E7M64L7V-B1Qz70Do.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-DW1J1uJd.js → diagram-IFDJBPK2-k55eVqVU.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-CQ32hyG_.js → diagram-P4PSJMXO-BkfNRc9U.js} +1 -1
  55. package/dist/web/assets/diff-viewer-HhIcsOQE.js +4 -0
  56. package/dist/web/assets/dist-DylI9XxN.js +13 -0
  57. package/dist/web/assets/dist-lF8CoYII.js +41 -0
  58. package/dist/web/assets/{erDiagram-INFDFZHY-6CHo6nOw.js → erDiagram-INFDFZHY-CKzVujYI.js} +1 -1
  59. package/dist/web/assets/{flowDiagram-PKNHOUZH-DroDiNT0.js → flowDiagram-PKNHOUZH-DIqcTrDV.js} +1 -1
  60. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DP0QBh8w.js → ganttDiagram-A5KZAMGK-D4v7ZbVE.js} +1 -1
  61. package/dist/web/assets/git-graph-CQtWu8yE.js +1 -0
  62. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  63. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js → gitGraphDiagram-K3NZZRJ6-BTXo57mF.js} +1 -1
  64. package/dist/web/assets/{graphlib-CQBb2thr.js → graphlib-BcsNnGcW.js} +1 -1
  65. package/dist/web/assets/index-CgQXpBb_.css +2 -0
  66. package/dist/web/assets/index-DEeeRoka.js +37 -0
  67. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  68. package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +2 -0
  69. package/dist/web/assets/input-BglMT33g.js +1 -0
  70. package/dist/web/assets/{isEmpty-B4kqZBtn.js → isEmpty-bnrF3Qbc.js} +1 -1
  71. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-46yibrV5.js → ishikawaDiagram-PHBUUO56-BOyvKMmB.js} +1 -1
  72. package/dist/web/assets/{journeyDiagram-4ABVD52K-BcmRwjK-.js → journeyDiagram-4ABVD52K-ufoasAy6.js} +1 -1
  73. package/dist/web/assets/{kanban-definition-K7BYSVSG-B619K53y.js → kanban-definition-K7BYSVSG-Bi0UTUeN.js} +1 -1
  74. package/dist/web/assets/keybindings-store-1CJ7VX57.js +1 -0
  75. package/dist/web/assets/lib-BQ34Db2e.js +4 -0
  76. package/dist/web/assets/{line-1gcO63_w.js → line-B78g-52T.js} +1 -1
  77. package/dist/web/assets/{linear-DfRqDoVd.js → linear-DP4mkX3m.js} +1 -1
  78. package/dist/web/assets/markdown-renderer-Brj8_LQM.js +69 -0
  79. package/dist/web/assets/{mermaid-parser.core-XtjZQOeM.js → mermaid-parser.core-DMIWdgEW.js} +2 -2
  80. package/dist/web/assets/{mindmap-definition-YRQLILUH-CifOFo_q.js → mindmap-definition-YRQLILUH-BsfWvIoO.js} +1 -1
  81. package/dist/web/assets/{ordinal-BJYw-iDX.js → ordinal-_K3x1fkz.js} +1 -1
  82. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
  83. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
  84. package/dist/web/assets/{pieDiagram-SKSYHLDU-BuHUh_fO.js → pieDiagram-SKSYHLDU-WP0XXw51.js} +1 -1
  85. package/dist/web/assets/postgres-viewer-CwkTGmqy.js +1 -0
  86. package/dist/web/assets/{quadrantDiagram-337W2JSQ-Bau_hj6Z.js → quadrantDiagram-337W2JSQ-FHMogtsh.js} +1 -1
  87. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
  88. package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
  89. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-Cq2b-uwp.js → requirementDiagram-Z7DCOOCP-BatTxyWb.js} +1 -1
  90. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js → sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js} +1 -1
  91. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-qPxiTUcS.js → sequenceDiagram-2WXFIKYE-ByxQqGgs.js} +1 -1
  92. package/dist/web/assets/settings-tab-BDE1MsIh.js +1 -0
  93. package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +1 -0
  94. package/dist/web/assets/{stateDiagram-RAJIS63D-Dulj2oa8.js → stateDiagram-RAJIS63D-f8opcZNY.js} +1 -1
  95. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +1 -0
  96. package/dist/web/assets/tab-store-BJw7OCmy.js +1 -0
  97. package/dist/web/assets/{terminal-tab-wKgpSPAT.js → terminal-tab-CCDLZA5Y.js} +2 -2
  98. package/dist/web/assets/{timeline-definition-YZTLITO2-BWyDnCYq.js → timeline-definition-YZTLITO2-58BlOSf9.js} +1 -1
  99. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  100. package/dist/web/assets/use-monaco-theme-CNzekTN3.js +11 -0
  101. package/dist/web/assets/{vennDiagram-LZ73GAT5-B9Iv2bNV.js → vennDiagram-LZ73GAT5-BOSy9ma9.js} +1 -1
  102. package/dist/web/assets/{xychartDiagram-JWTSCODW-ChXcMzBQ.js → xychartDiagram-JWTSCODW-z5MVJauZ.js} +1 -1
  103. package/dist/web/index.html +12 -11
  104. package/dist/web/sw.js +1 -1
  105. package/docs/code-standards.md +232 -7
  106. package/docs/codebase-summary.md +9 -3
  107. package/docs/design-guidelines.md +21 -0
  108. package/docs/project-changelog.md +115 -1
  109. package/docs/project-roadmap.md +41 -19
  110. package/docs/system-architecture.md +212 -15
  111. package/package.json +3 -2
  112. package/src/cli/commands/autostart.ts +1 -1
  113. package/src/cli/commands/restart.ts +9 -1
  114. package/src/cli/commands/status.ts +19 -0
  115. package/src/index.ts +2 -3
  116. package/src/providers/claude-agent-sdk.ts +92 -25
  117. package/src/providers/mock-provider.ts +6 -1
  118. package/src/server/index.ts +38 -166
  119. package/src/server/routes/browser-preview.ts +159 -0
  120. package/src/server/routes/chat.ts +52 -3
  121. package/src/server/routes/project-scoped.ts +2 -0
  122. package/src/server/routes/proxy.ts +46 -53
  123. package/src/server/routes/tunnel.ts +0 -32
  124. package/src/server/routes/workspace.ts +35 -0
  125. package/src/server/ws/chat.ts +207 -146
  126. package/src/services/account-selector.service.ts +16 -8
  127. package/src/services/account.service.ts +19 -13
  128. package/src/services/claude-usage.service.ts +48 -11
  129. package/src/services/cloud-ws.service.ts +227 -0
  130. package/src/services/cloud.service.ts +10 -6
  131. package/src/services/db.service.ts +111 -6
  132. package/src/services/proxy.service.ts +4 -19
  133. package/src/services/supervisor.ts +285 -25
  134. package/src/types/api.ts +9 -1
  135. package/src/types/chat.ts +3 -1
  136. package/src/web/app.tsx +41 -35
  137. package/src/web/components/browser/browser-tab.tsx +106 -97
  138. package/src/web/components/chat/account-rotation-settings.tsx +163 -0
  139. package/src/web/components/chat/chat-history-bar.tsx +72 -6
  140. package/src/web/components/chat/chat-tab.tsx +32 -16
  141. package/src/web/components/chat/chat-welcome.tsx +148 -0
  142. package/src/web/components/chat/message-input.tsx +107 -13
  143. package/src/web/components/chat/message-list.tsx +27 -15
  144. package/src/web/components/chat/session-picker.tsx +78 -31
  145. package/src/web/components/chat/usage-badge.tsx +11 -1
  146. package/src/web/components/editor/code-editor.tsx +36 -26
  147. package/src/web/components/editor/csv-preview.tsx +228 -0
  148. package/src/web/components/editor/editor-breadcrumb.tsx +216 -0
  149. package/src/web/components/editor/editor-toolbar.tsx +74 -0
  150. package/src/web/components/layout/command-palette.tsx +3 -1
  151. package/src/web/components/layout/editor-panel.tsx +162 -18
  152. package/src/web/components/layout/panel-layout.tsx +17 -1
  153. package/src/web/components/settings/proxy-settings-section.tsx +40 -42
  154. package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
  155. package/src/web/hooks/use-chat.ts +211 -201
  156. package/src/web/hooks/use-global-keybindings.ts +25 -2
  157. package/src/web/hooks/use-server-reload.ts +9 -0
  158. package/src/web/hooks/use-url-sync.ts +173 -21
  159. package/src/web/hooks/use-voice-input.ts +111 -0
  160. package/src/web/lib/csv-parser.ts +134 -0
  161. package/src/web/stores/connection-store.ts +39 -0
  162. package/src/web/stores/keybindings-store.ts +1 -0
  163. package/src/web/stores/panel-store.ts +73 -19
  164. package/src/web/stores/panel-utils.ts +145 -3
  165. package/dist/web/assets/api-settings-CFw-lh5k.js +0 -1
  166. package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +0 -1
  167. package/dist/web/assets/browser-tab-CmsL5eny.js +0 -1
  168. package/dist/web/assets/channel-DmKoFTd_.js +0 -1
  169. package/dist/web/assets/chat-tab-CFWsf13Z.js +0 -7
  170. package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +0 -2
  171. package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +0 -1
  172. package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +0 -1
  173. package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +0 -1
  174. package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +0 -1
  175. package/dist/web/assets/clone-CijCFRT5.js +0 -1
  176. package/dist/web/assets/code-editor-H_dAh_fJ.js +0 -1
  177. package/dist/web/assets/database-viewer-DBzsgEJ8.js +0 -1
  178. package/dist/web/assets/diff-viewer-DzS-OnAR.js +0 -4
  179. package/dist/web/assets/dist-0Va_2L7G.js +0 -16
  180. package/dist/web/assets/dist-D9irYETY.js +0 -41
  181. package/dist/web/assets/git-graph-D3C7F8o3.js +0 -1
  182. package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +0 -1
  183. package/dist/web/assets/index-CIkjfera.js +0 -31
  184. package/dist/web/assets/index-WKLuYsBY.css +0 -2
  185. package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +0 -1
  186. package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +0 -2
  187. package/dist/web/assets/input-CGp1nFIg.js +0 -1
  188. package/dist/web/assets/keybindings-store-BdaoLwSo.js +0 -1
  189. package/dist/web/assets/markdown-renderer-DH49Zag7.js +0 -69
  190. package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +0 -1
  191. package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +0 -1
  192. package/dist/web/assets/postgres-viewer-B9FYk8sD.js +0 -1
  193. package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +0 -1
  194. package/dist/web/assets/settings-store-DWXGVHsE.js +0 -2
  195. package/dist/web/assets/settings-tab-D-q8pd-5.js +0 -1
  196. package/dist/web/assets/sqlite-viewer-CDqcTePw.js +0 -1
  197. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +0 -1
  198. package/dist/web/assets/tab-store-BPeiymiH.js +0 -1
  199. package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +0 -1
  200. package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +0 -11
  201. package/src/services/port-tunnel.service.ts +0 -97
  202. /package/dist/web/assets/{api-client-DOElml5u.js → api-client-BfBM3I7n.js} +0 -0
  203. /package/dist/web/assets/{array-CYkMkqnU.js → array-B9UHiPd-.js} +0 -0
  204. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DpsNbZOc.js} +0 -0
  205. /package/dist/web/assets/{cytoscape.esm-HeHO0VhB.js → cytoscape.esm-BW-DbntU.js} +0 -0
  206. /package/dist/web/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-5eAKkKJC.js} +0 -0
  207. /package/dist/web/assets/{dist-BUYzeuKe.js → dist-CSJdAyA9.js} +0 -0
  208. /package/dist/web/assets/{init-Rr1s_RiX.js → init-DlZdxViB.js} +0 -0
  209. /package/dist/web/assets/{isArrayLikeObject-BB-mzMLb.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  210. /package/dist/web/assets/{katex-CKoArbIw.js → katex-Bqvo_ZG0.js} +0 -0
  211. /package/dist/web/assets/{math-B7b0HgJF.js → math-069Z4SuC.js} +0 -0
  212. /package/dist/web/assets/{path-BAQ3hXlG.js → path-6uRLdFF7.js} +0 -0
  213. /package/dist/web/assets/{preload-helper-DeiOTZKJ.js → preload-helper-uTix4PVD.js} +0 -0
  214. /package/dist/web/assets/{react-Dev-wu-s.js → react-ER-4DN55.js} +0 -0
  215. /package/dist/web/assets/{rough.esm-Dwml_la6.js → rough.esm-JX0wREDd.js} +0 -0
  216. /package/dist/web/assets/{src-B_cC68fH.js → src-BqX54PbV.js} +0 -0
  217. /package/dist/web/assets/{table-COiJDPRA.js → table-C7X5UAEI.js} +0 -0
  218. /package/dist/web/assets/{tag-LMq02LfE.js → tag-CCtdV063.js} +0 -0
  219. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.js} +0 -0
@@ -5,7 +5,7 @@ import { useNotificationStore } from "@/stores/notification-store";
5
5
  import { usePanelStore } from "@/stores/panel-store";
6
6
  import { playNotificationSound } from "@/lib/notification-sounds";
7
7
  import type { ChatMessage, ChatEvent } from "../../types/chat";
8
- import type { ChatWsServerMessage } from "../../types/api";
8
+ import type { ChatWsServerMessage, SessionPhase } from "../../types/api";
9
9
 
10
10
  interface ApprovalRequest {
11
11
  requestId: string;
@@ -13,21 +13,17 @@ interface ApprovalRequest {
13
13
  input: unknown;
14
14
  }
15
15
 
16
- /** Streaming phase: connecting → streaming → idle */
17
- export type StreamingStatus = "idle" | "connecting" | "streaming";
18
-
19
16
  interface UseChatReturn {
20
17
  messages: ChatMessage[];
21
18
  messagesLoading: boolean;
22
19
  isStreaming: boolean;
23
- streamingStatus: StreamingStatus;
20
+ phase: SessionPhase;
21
+ isReconnecting: boolean;
24
22
  connectingElapsed: number;
25
- thinkingWarningThreshold: number;
26
23
  pendingApproval: ApprovalRequest | null;
27
- /** Context window usage % from last completed query (0–100) */
28
24
  contextWindowPct: number | null;
29
- /** Updated session title from SDK summary (set after stream completes) */
30
25
  sessionTitle: string | null;
26
+ streamingAccountLabel: string | null;
31
27
  sendMessage: (content: string, opts?: { permissionMode?: string }) => void;
32
28
  respondToApproval: (requestId: string, approved: boolean, data?: unknown) => void;
33
29
  cancelStreaming: () => void;
@@ -49,201 +45,148 @@ function isSessionTabActive(sid: string): boolean {
49
45
  export function useChat(sessionId: string | null, providerId = "claude", projectName = ""): UseChatReturn {
50
46
  const [messages, setMessages] = useState<ChatMessage[]>([]);
51
47
  const [messagesLoading, setMessagesLoading] = useState(false);
52
- const [isStreaming, setIsStreaming] = useState(false);
53
- const [streamingStatus, setStreamingStatus] = useState<StreamingStatus>("idle");
54
- /** Elapsed seconds while connecting (sent by BE heartbeat every 5s) */
48
+ const [phase, setPhase] = useState<SessionPhase>("idle");
49
+ const [isReconnecting, setIsReconnecting] = useState(false);
55
50
  const [connectingElapsed, setConnectingElapsed] = useState(0);
56
- /** Warning threshold in seconds — higher for deeper thinking modes */
57
- const [thinkingWarningThreshold, setThinkingWarningThreshold] = useState(15);
58
51
  const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
59
52
  const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
60
53
  const [sessionTitle, setSessionTitle] = useState<string | null>(null);
54
+ const [streamingAccountLabel, setStreamingAccountLabel] = useState<string | null>(null);
61
55
  const [isConnected, setIsConnected] = useState(false);
62
56
  const streamingContentRef = useRef("");
63
57
  const streamingEventsRef = useRef<ChatEvent[]>([]);
64
58
  const streamingAccountRef = useRef<{ accountId: string; accountLabel: string } | null>(null);
65
- const isStreamingRef = useRef(false);
59
+ const phaseRef = useRef<SessionPhase>("idle");
66
60
  const pendingMessageRef = useRef<string | null>(null);
67
61
  const sendRef = useRef<(data: string) => void>(() => {});
68
62
  const refetchRef = useRef<(() => void) | null>(null);
69
- // Refs for notification dispatch inside handleMessage (which has [] deps)
70
63
  const sessionIdRef = useRef(sessionId);
71
64
  sessionIdRef.current = sessionId;
72
65
  const projectNameRef = useRef(projectName);
73
66
  projectNameRef.current = projectName;
74
67
 
75
- const handleMessage = useCallback((event: MessageEvent) => {
76
- let data: ChatWsServerMessage;
77
- try {
78
- data = JSON.parse(event.data as string) as ChatWsServerMessage;
79
- } catch {
80
- return;
81
- }
82
-
83
- // Ignore keepalive pings
84
- if ((data as any).type === "ping") return;
85
-
86
- // Handle title updates from SDK summary
87
- if ((data as any).type === "title_updated") {
88
- setSessionTitle((data as any).title ?? null);
89
- return;
90
- }
68
+ // Derived state
69
+ const isStreaming = phase !== "idle";
70
+
71
+ /**
72
+ * Route a child event to its parent Agent/Task tool_use's children array.
73
+ * Creates a new parent object to ensure React detects the change on re-render.
74
+ * Returns true if routed (caller should skip flat append), false if no parent found.
75
+ */
76
+ const routeToParent = useCallback((childEvent: ChatEvent, parentToolUseId: string): boolean => {
77
+ const idx = streamingEventsRef.current.findIndex(
78
+ (e) => e.type === "tool_use"
79
+ && (e.tool === "Agent" || e.tool === "Task")
80
+ && (e as any).toolUseId === parentToolUseId,
81
+ );
82
+ if (idx === -1) return false;
83
+ const parent = streamingEventsRef.current[idx]!;
84
+ if (parent.type !== "tool_use") return false;
85
+ const newChildren = [...(parent.children ?? []), childEvent];
86
+ streamingEventsRef.current[idx] = { ...parent, children: newChildren };
87
+ return true;
88
+ }, []);
91
89
 
92
- // Handle streaming status updates (connecting streaming → idle)
93
- if ((data as any).type === "streaming_status") {
94
- const s = (data as any).status ?? "idle";
95
- setStreamingStatus(s);
96
- setConnectingElapsed(s === "connecting" ? ((data as any).elapsed ?? 0) : 0);
97
- // Compute warning threshold based on effort/thinking budget
98
- if (s === "connecting") {
99
- const effort = (data as any).effort as string | undefined;
100
- const budget = (data as any).thinkingBudget as number | undefined;
101
- // Higher thinking = longer acceptable wait time
102
- let threshold = 15; // default
103
- if (budget && budget > 0) {
104
- // Rough: 1k tokens ≈ 2s thinking time
105
- threshold = Math.max(15, Math.round(budget / 500));
106
- } else if (effort === "high") {
107
- threshold = 30;
108
- } else if (effort === "low") {
109
- threshold = 10;
110
- }
111
- setThinkingWarningThreshold(threshold);
90
+ /** Trigger re-render with latest events snapshot */
91
+ const syncMessages = useCallback(() => {
92
+ const content = streamingContentRef.current;
93
+ const events = [...streamingEventsRef.current];
94
+ const account = streamingAccountRef.current;
95
+ setMessages((prev) => {
96
+ const last = prev[prev.length - 1];
97
+ if (last?.role === "assistant" && !last.id.startsWith("final-")) {
98
+ return [...prev.slice(0, -1), { ...last, content, events, ...account }];
112
99
  }
113
- return;
114
- }
100
+ return [...prev, {
101
+ id: `streaming-${Date.now()}`,
102
+ role: "assistant" as const,
103
+ content,
104
+ events,
105
+ timestamp: new Date().toISOString(),
106
+ ...account,
107
+ }];
108
+ });
109
+ }, []);
115
110
 
116
- // Handle connected event (new session)
117
- if ((data as any).type === "connected") {
118
- setIsConnected(true);
119
- if ((data as any).sessionTitle) setSessionTitle((data as any).sessionTitle);
120
- return;
121
- }
111
+ /** Process a single stream event reused by live events and turn_events replay */
112
+ const processStreamEvent = useCallback((data: unknown) => {
113
+ const ev = data as any;
114
+ const evType = ev?.type;
115
+ if (!evType) return;
122
116
 
123
- // Handle status event (FE reconnected to existing session)
124
- if ((data as any).type === "status") {
125
- setIsConnected(true);
126
- const status = data as any;
127
- if (status.sessionTitle) setSessionTitle(status.sessionTitle);
128
- if (status.isStreaming) {
129
- isStreamingRef.current = true;
130
- setIsStreaming(true);
131
- }
132
- if (status.pendingApproval) {
133
- setPendingApproval({
134
- requestId: status.pendingApproval.requestId,
135
- tool: status.pendingApproval.tool,
136
- input: status.pendingApproval.input,
137
- });
117
+ switch (evType) {
118
+ case "account_info": {
119
+ streamingAccountRef.current = { accountId: ev.accountId, accountLabel: ev.accountLabel };
120
+ setStreamingAccountLabel(ev.accountLabel ?? null);
121
+ break;
138
122
  }
139
- // Refetch history to catch up on events missed during disconnect
140
- refetchRef.current?.();
141
- return;
142
- }
143
-
144
- /**
145
- * Route a child event to its parent Agent/Task tool_use's children array.
146
- * Creates a new parent object to ensure React detects the change on re-render.
147
- * Returns true if routed (caller should skip flat append), false if no parent found.
148
- */
149
- const routeToParent = (childEvent: ChatEvent, parentToolUseId: string): boolean => {
150
- const idx = streamingEventsRef.current.findIndex(
151
- (e) => e.type === "tool_use"
152
- && (e.tool === "Agent" || e.tool === "Task")
153
- && (e as any).toolUseId === parentToolUseId,
154
- );
155
- if (idx === -1) return false;
156
- const parent = streamingEventsRef.current[idx]!;
157
- if (parent.type !== "tool_use") return false;
158
- // Create new object so React detects the change via shallow comparison
159
- const newChildren = [...(parent.children ?? []), childEvent];
160
- streamingEventsRef.current[idx] = { ...parent, children: newChildren };
161
- return true;
162
- };
163
123
 
164
- /** Trigger re-render with latest events snapshot */
165
- const syncMessages = () => {
166
- const content = streamingContentRef.current;
167
- const events = [...streamingEventsRef.current];
168
- const account = streamingAccountRef.current;
169
- setMessages((prev) => {
170
- const last = prev[prev.length - 1];
171
- if (last?.role === "assistant" && !last.id.startsWith("final-")) {
172
- return [...prev.slice(0, -1), { ...last, content, events, ...account }];
124
+ case "account_retry": {
125
+ // Update streaming account to the new one being tried
126
+ if (ev.accountId && ev.accountLabel) {
127
+ streamingAccountRef.current = { accountId: ev.accountId, accountLabel: ev.accountLabel };
128
+ setStreamingAccountLabel(ev.accountLabel);
173
129
  }
174
- return [...prev, {
175
- id: `streaming-${Date.now()}`,
176
- role: "assistant" as const,
177
- content,
178
- events,
179
- timestamp: new Date().toISOString(),
180
- ...account,
181
- }];
182
- });
183
- };
184
-
185
- switch (data.type) {
186
- case "account_info": {
187
- streamingAccountRef.current = { accountId: (data as any).accountId, accountLabel: (data as any).accountLabel };
130
+ // Surface retry as a system-level event in the stream
131
+ streamingEventsRef.current.push(ev as ChatEvent);
132
+ syncMessages();
188
133
  break;
189
134
  }
190
135
 
191
136
  case "text": {
192
- const pid = (data as any).parentToolUseId as string | undefined;
193
- if (pid && routeToParent(data, pid)) {
194
- // Child text routed to parent — just re-render
137
+ const pid = ev.parentToolUseId as string | undefined;
138
+ if (pid && routeToParent(ev as ChatEvent, pid)) {
195
139
  syncMessages();
196
140
  break;
197
141
  }
198
- streamingContentRef.current += data.content;
199
- streamingEventsRef.current.push(data);
142
+ streamingContentRef.current += ev.content;
143
+ streamingEventsRef.current.push(ev as ChatEvent);
200
144
  syncMessages();
201
145
  break;
202
146
  }
203
147
 
204
148
  case "thinking": {
205
- const pid = (data as any).parentToolUseId as string | undefined;
206
- if (pid && routeToParent(data, pid)) {
149
+ const pid = ev.parentToolUseId as string | undefined;
150
+ if (pid && routeToParent(ev as ChatEvent, pid)) {
207
151
  syncMessages();
208
152
  break;
209
153
  }
210
- streamingEventsRef.current.push(data);
154
+ streamingEventsRef.current.push(ev as ChatEvent);
211
155
  syncMessages();
212
156
  break;
213
157
  }
214
158
 
215
159
  case "tool_use": {
216
- const pid = (data as any).parentToolUseId as string | undefined;
217
- if (pid && routeToParent(data, pid)) {
160
+ const pid = ev.parentToolUseId as string | undefined;
161
+ if (pid && routeToParent(ev as ChatEvent, pid)) {
218
162
  syncMessages();
219
163
  break;
220
164
  }
221
- streamingEventsRef.current.push(data);
165
+ streamingEventsRef.current.push(ev as ChatEvent);
222
166
  syncMessages();
223
167
  break;
224
168
  }
225
169
 
226
170
  case "tool_result": {
227
- const pid = (data as any).parentToolUseId as string | undefined;
228
- if (pid && routeToParent(data, pid)) {
171
+ const pid = ev.parentToolUseId as string | undefined;
172
+ if (pid && routeToParent(ev as ChatEvent, pid)) {
229
173
  syncMessages();
230
174
  break;
231
175
  }
232
- streamingEventsRef.current.push(data);
176
+ streamingEventsRef.current.push(ev as ChatEvent);
233
177
  syncMessages();
234
178
  break;
235
179
  }
236
180
 
237
181
  case "approval_request": {
238
- streamingEventsRef.current.push(data);
182
+ streamingEventsRef.current.push(ev as ChatEvent);
239
183
  setPendingApproval({
240
- requestId: data.requestId,
241
- tool: data.tool,
242
- input: data.input,
184
+ requestId: ev.requestId,
185
+ tool: ev.tool,
186
+ input: ev.input,
243
187
  });
244
- // Local notification badge — only if this tab is NOT active
245
188
  if (sessionIdRef.current && !isSessionTabActive(sessionIdRef.current)) {
246
- const nType = data.tool === "AskUserQuestion" ? "question" : "approval_request";
189
+ const nType = ev.tool === "AskUserQuestion" ? "question" : "approval_request";
247
190
  useNotificationStore.getState().addNotification(sessionIdRef.current, nType, projectNameRef.current);
248
191
  playNotificationSound(nType);
249
192
  }
@@ -251,73 +194,148 @@ export function useChat(sessionId: string | null, providerId = "claude", project
251
194
  }
252
195
 
253
196
  case "error": {
254
- streamingEventsRef.current.push(data);
197
+ streamingEventsRef.current.push(ev as ChatEvent);
255
198
  const errEvents = [...streamingEventsRef.current];
256
199
  setMessages((prev) => {
257
200
  const last = prev[prev.length - 1];
258
201
  if (last?.role === "assistant") {
259
- return [
260
- ...prev.slice(0, -1),
261
- { ...last, events: errEvents },
262
- ];
202
+ return [...prev.slice(0, -1), { ...last, events: errEvents }];
263
203
  }
264
- return [
265
- ...prev,
266
- {
267
- id: `error-${Date.now()}`,
268
- role: "system" as const,
269
- content: data.message,
270
- events: [data],
271
- timestamp: new Date().toISOString(),
272
- },
273
- ];
204
+ return [...prev, {
205
+ id: `error-${Date.now()}`,
206
+ role: "system" as const,
207
+ content: ev.message,
208
+ events: [ev as ChatEvent],
209
+ timestamp: new Date().toISOString(),
210
+ }];
274
211
  });
275
- isStreamingRef.current = false;
276
- setIsStreaming(false);
277
- setStreamingStatus("idle");
212
+ // Phase reset comes from BE via phase_changed
278
213
  break;
279
214
  }
280
215
 
281
216
  case "done": {
282
217
  // Idempotent: may receive duplicate done (provider + stream loop finally)
283
- if (!isStreamingRef.current) break;
284
- // Capture context window usage from SDK result
285
- if (data.contextWindowPct != null) {
286
- setContextWindowPct(data.contextWindowPct);
218
+ if (phaseRef.current === "idle") break;
219
+ if (ev.contextWindowPct != null) {
220
+ setContextWindowPct(ev.contextWindowPct);
287
221
  }
288
- // Local notification badge — only if this tab is NOT active
289
222
  if (sessionIdRef.current && !isSessionTabActive(sessionIdRef.current)) {
290
223
  useNotificationStore.getState().addNotification(sessionIdRef.current, "done", projectNameRef.current);
291
224
  playNotificationSound("done");
292
225
  }
293
- // Finalize the streaming message — capture refs before clearing
226
+ // Finalize the streaming message
294
227
  const finalContent = streamingContentRef.current;
295
228
  const finalEvents = [...streamingEventsRef.current];
296
229
  setMessages((prev) => {
297
230
  const last = prev[prev.length - 1];
298
231
  if (last?.role === "assistant") {
299
- return [
300
- ...prev.slice(0, -1),
301
- {
302
- ...last,
303
- id: `final-${Date.now()}`,
304
- content: finalContent || last.content,
305
- events: finalEvents.length > 0 ? finalEvents : last.events,
306
- },
307
- ];
232
+ return [...prev.slice(0, -1), {
233
+ ...last,
234
+ id: `final-${Date.now()}`,
235
+ content: finalContent || last.content,
236
+ events: finalEvents.length > 0 ? finalEvents : last.events,
237
+ }];
308
238
  }
309
239
  return prev;
310
240
  });
311
241
  streamingContentRef.current = "";
312
242
  streamingEventsRef.current = [];
313
243
  streamingAccountRef.current = null;
314
- isStreamingRef.current = false;
315
- setIsStreaming(false);
316
- setStreamingStatus("idle");
244
+ setStreamingAccountLabel(null);
245
+ // Phase transition to idle comes from BE via phase_changed
317
246
  break;
318
247
  }
319
248
  }
320
- }, []);
249
+ }, [routeToParent, syncMessages]);
250
+
251
+ const handleMessage = useCallback((event: MessageEvent) => {
252
+ let data: ChatWsServerMessage;
253
+ try {
254
+ data = JSON.parse(event.data as string) as ChatWsServerMessage;
255
+ } catch {
256
+ return;
257
+ }
258
+
259
+ // Ignore keepalive pings
260
+ if ((data as any).type === "ping") return;
261
+
262
+ // Handle title updates from SDK summary
263
+ if ((data as any).type === "title_updated") {
264
+ setSessionTitle((data as any).title ?? null);
265
+ return;
266
+ }
267
+
268
+ // Handle phase transitions from BE
269
+ if ((data as any).type === "phase_changed") {
270
+ const p = (data as any).phase as SessionPhase;
271
+ setPhase(p);
272
+ phaseRef.current = p;
273
+ setConnectingElapsed(p === "connecting" ? ((data as any).elapsed ?? 0) : 0);
274
+ return;
275
+ }
276
+
277
+ // Handle session state (replaces connected + status)
278
+ if ((data as any).type === "session_state") {
279
+ setIsConnected(true);
280
+ const state = data as any;
281
+ const p = state.phase as SessionPhase;
282
+ setPhase(p);
283
+ phaseRef.current = p;
284
+ if (state.sessionTitle) setSessionTitle(state.sessionTitle);
285
+ if (state.pendingApproval) {
286
+ setPendingApproval({
287
+ requestId: state.pendingApproval.requestId,
288
+ tool: state.pendingApproval.tool,
289
+ input: state.pendingApproval.input,
290
+ });
291
+ }
292
+ // If idle, refetch history (completed turns) and hide overlay
293
+ if (p === "idle") {
294
+ refetchRef.current?.();
295
+ setIsReconnecting(false);
296
+ }
297
+ // If streaming, turn_events message will follow
298
+ return;
299
+ }
300
+
301
+ // Handle turn_events (reconnect sync with rAF chunking)
302
+ if ((data as any).type === "turn_events") {
303
+ const events = (data as any).events as unknown[];
304
+ if (!events?.length) { setIsReconnecting(false); return; }
305
+
306
+ // Truncate messages after last user message
307
+ setMessages(prev => {
308
+ const lastUserIdx = prev.findLastIndex(m => m.role === "user");
309
+ return lastUserIdx >= 0 ? prev.slice(0, lastUserIdx + 1) : prev;
310
+ });
311
+
312
+ // Reset streaming refs
313
+ streamingContentRef.current = "";
314
+ streamingEventsRef.current = [];
315
+ streamingAccountRef.current = null;
316
+
317
+ // Process events in chunks via requestAnimationFrame to avoid blocking main thread
318
+ const CHUNK_SIZE = 100;
319
+ let offset = 0;
320
+ const processChunk = () => {
321
+ const end = Math.min(offset + CHUNK_SIZE, events.length);
322
+ for (let i = offset; i < end; i++) {
323
+ processStreamEvent(events[i]);
324
+ }
325
+ offset = end;
326
+ if (offset < events.length) {
327
+ requestAnimationFrame(processChunk);
328
+ } else {
329
+ setIsReconnecting(false);
330
+ }
331
+ };
332
+ requestAnimationFrame(processChunk);
333
+ return;
334
+ }
335
+
336
+ // Route content events through processStreamEvent
337
+ processStreamEvent(data);
338
+ }, [processStreamEvent]);
321
339
 
322
340
  const wsUrl = sessionId && projectName
323
341
  ? `/ws/project/${encodeURIComponent(projectName)}/chat/${sessionId}`
@@ -336,21 +354,21 @@ export function useChat(sessionId: string | null, providerId = "claude", project
336
354
  useEffect(() => {
337
355
  let cancelled = false;
338
356
 
339
- setIsStreaming(false);
357
+ setPhase("idle");
358
+ phaseRef.current = "idle";
340
359
  setPendingApproval(null);
341
360
  streamingContentRef.current = "";
342
361
  streamingEventsRef.current = [];
343
362
  setIsConnected(false);
344
363
 
345
364
  if (sessionId && projectName) {
346
- // Load message history
347
365
  setMessagesLoading(true);
348
366
  fetch(`${projectUrl(projectName)}/chat/sessions/${sessionId}/messages?providerId=${providerId}`, {
349
367
  headers: { Authorization: `Bearer ${getAuthToken()}` },
350
368
  })
351
369
  .then((r) => r.json())
352
370
  .then((json: any) => {
353
- if (cancelled || isStreamingRef.current) return;
371
+ if (cancelled || phaseRef.current !== "idle") return;
354
372
  if (json.ok && Array.isArray(json.data) && json.data.length > 0) {
355
373
  setMessages(json.data);
356
374
  } else {
@@ -358,7 +376,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
358
376
  }
359
377
  })
360
378
  .catch(() => {
361
- if (!cancelled && !isStreamingRef.current) setMessages([]);
379
+ if (!cancelled && phaseRef.current === "idle") setMessages([]);
362
380
  })
363
381
  .finally(() => {
364
382
  if (!cancelled) setMessagesLoading(false);
@@ -377,8 +395,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
377
395
  if (!content.trim()) return;
378
396
 
379
397
  // If streaming, cancel current stream first then send immediately
380
- if (isStreamingRef.current) {
381
- // Finalize current streaming message
398
+ if (phaseRef.current !== "idle") {
382
399
  const finalContent = streamingContentRef.current;
383
400
  const finalEvents = [...streamingEventsRef.current];
384
401
  setMessages((prev) => {
@@ -391,7 +408,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
391
408
  }
392
409
  return prev;
393
410
  });
394
- // Tell backend to abort current query
395
411
  send(JSON.stringify({ type: "cancel" }));
396
412
  }
397
413
 
@@ -410,9 +426,8 @@ export function useChat(sessionId: string | null, providerId = "claude", project
410
426
  streamingContentRef.current = "";
411
427
  streamingEventsRef.current = [];
412
428
  pendingMessageRef.current = null;
413
- isStreamingRef.current = true;
414
- setIsStreaming(true);
415
- setStreamingStatus("connecting");
429
+ setPhase("initializing");
430
+ phaseRef.current = "initializing";
416
431
  setPendingApproval(null);
417
432
 
418
433
  send(JSON.stringify({ type: "message", content, permissionMode: opts?.permissionMode }));
@@ -441,13 +456,11 @@ export function useChat(sessionId: string | null, providerId = "claude", project
441
456
  (e as any).tool === "AskUserQuestion",
442
457
  );
443
458
  if (askEvt) {
444
- // Mutate input to include answers — this updates the rendered ToolCard
445
459
  const inp = (askEvt as any).input;
446
460
  if (inp && typeof inp === "object") {
447
461
  (inp as Record<string, unknown>).answers = data;
448
462
  }
449
463
  }
450
- // Force re-render messages
451
464
  setMessages((prev) => [...prev]);
452
465
  }
453
466
 
@@ -457,10 +470,8 @@ export function useChat(sessionId: string | null, providerId = "claude", project
457
470
  );
458
471
 
459
472
  const cancelStreaming = useCallback(() => {
460
- if (!isStreamingRef.current) return;
461
- // Tell backend to abort
473
+ if (phaseRef.current === "idle") return;
462
474
  send(JSON.stringify({ type: "cancel" }));
463
- // Finalize current message on FE
464
475
  const finalContent = streamingContentRef.current;
465
476
  const finalEvents = [...streamingEventsRef.current];
466
477
  setMessages((prev) => {
@@ -481,16 +492,15 @@ export function useChat(sessionId: string | null, providerId = "claude", project
481
492
  streamingContentRef.current = "";
482
493
  streamingEventsRef.current = [];
483
494
  pendingMessageRef.current = null;
484
- isStreamingRef.current = false;
485
- setIsStreaming(false);
495
+ setPhase("idle");
496
+ phaseRef.current = "idle";
486
497
  setPendingApproval(null);
487
498
  }, [send]);
488
499
 
489
500
  const reconnect = useCallback(() => {
490
501
  setIsConnected(false);
502
+ setIsReconnecting(true);
491
503
  wsReconnect();
492
- // Refetch history on manual reconnect to catch up on missed events
493
- refetchRef.current?.();
494
504
  }, [wsReconnect]);
495
505
 
496
506
  const refetchMessages = useCallback(() => {
@@ -503,7 +513,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
503
513
  .then((json: any) => {
504
514
  if (json.ok && Array.isArray(json.data) && json.data.length > 0) {
505
515
  setMessages(json.data);
506
- // Reset streaming content refs so live tokens append cleanly after history
507
516
  streamingContentRef.current = "";
508
517
  streamingEventsRef.current = [];
509
518
  }
@@ -512,19 +521,20 @@ export function useChat(sessionId: string | null, providerId = "claude", project
512
521
  .finally(() => setMessagesLoading(false));
513
522
  }, [sessionId, providerId, projectName]);
514
523
 
515
- // Keep refetchRef in sync so handleMessage (status event) can trigger refetch
524
+ // Keep refetchRef in sync
516
525
  refetchRef.current = refetchMessages;
517
526
 
518
527
  return {
519
528
  messages,
520
529
  messagesLoading,
521
530
  isStreaming,
522
- streamingStatus,
531
+ phase,
532
+ isReconnecting,
523
533
  connectingElapsed,
524
- thinkingWarningThreshold,
525
534
  pendingApproval,
526
535
  contextWindowPct,
527
536
  sessionTitle,
537
+ streamingAccountLabel,
528
538
  sendMessage,
529
539
  respondToApproval,
530
540
  cancelStreaming,