@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
@@ -1,141 +1,150 @@
1
- import { useState, useRef } from "react";
2
- import { Globe, Loader2, ExternalLink, Copy, X, RefreshCw } from "lucide-react";
3
- import { Button } from "@/components/ui/button";
4
- import { Input } from "@/components/ui/input";
1
+ import { useState, useRef, useCallback } from "react";
2
+ import { ExternalLink, Globe, Loader2, RefreshCw, X } from "lucide-react";
3
+ import { useTabStore } from "@/stores/tab-store";
5
4
  import { api } from "@/lib/api-client";
6
5
 
7
6
  interface BrowserTabProps {
8
7
  metadata?: Record<string, unknown>;
8
+ tabId?: string;
9
9
  }
10
10
 
11
- export function BrowserTab({ metadata }: BrowserTabProps) {
12
- const initialPort = metadata?.port ? Number(metadata.port) : undefined;
13
-
14
- const [port, setPort] = useState(initialPort?.toString() ?? "");
11
+ export function BrowserTab({ metadata, tabId }: BrowserTabProps) {
12
+ const initialPort = (metadata?.port as number) || 0;
13
+ const [portInput, setPortInput] = useState(initialPort ? String(initialPort) : "");
15
14
  const [tunnelUrl, setTunnelUrl] = useState<string | null>(null);
16
15
  const [loading, setLoading] = useState(false);
17
16
  const [error, setError] = useState<string | null>(null);
18
- const [copied, setCopied] = useState(false);
19
17
  const iframeRef = useRef<HTMLIFrameElement>(null);
18
+ const updateTab = useTabStore((s) => s.updateTab);
20
19
 
21
- const startTunnel = async () => {
22
- const p = Number(port);
23
- if (!p || p < 1 || p > 65535) {
24
- setError("Enter a valid port (1-65535)");
25
- return;
26
- }
20
+ const startTunnel = useCallback(async (port: number) => {
27
21
  setLoading(true);
28
22
  setError(null);
23
+ setTunnelUrl(null);
24
+
29
25
  try {
30
- const data = await api.post<{ port: number; url: string }>("/api/tunnel/port/start", { port: p });
31
- setTunnelUrl(data.url);
32
- } catch (e) {
33
- setError((e as Error).message);
26
+ const res = await api.post<{ port: number; url: string }>("/api/preview/tunnel", { port });
27
+ setTunnelUrl(res.url);
28
+ if (tabId) updateTab(tabId, { title: `localhost:${port}`, metadata: { ...metadata, port } });
29
+ } catch (e: any) {
30
+ setError(e.message || `Failed to start tunnel for port ${port}`);
34
31
  } finally {
35
32
  setLoading(false);
36
33
  }
37
- };
34
+ }, [tabId, metadata, updateTab]);
38
35
 
39
- const stopTunnel = async () => {
40
- const p = Number(port);
41
- if (!p) return;
42
- try {
43
- await api.post("/api/tunnel/port/stop", { port: p });
44
- } catch {}
36
+ const stopTunnel = useCallback(async () => {
37
+ const port = parseInt(portInput, 10);
38
+ if (!port) return;
39
+ try { await api.del(`/api/preview/tunnel/${port}`); } catch {}
45
40
  setTunnelUrl(null);
46
- };
41
+ if (tabId) updateTab(tabId, { title: "Browser" });
42
+ }, [portInput, tabId, updateTab]);
47
43
 
48
- const copyUrl = () => {
49
- if (!tunnelUrl) return;
50
- navigator.clipboard.writeText(tunnelUrl);
51
- setCopied(true);
52
- setTimeout(() => setCopied(false), 2000);
44
+ const handleSubmit = (e: React.FormEvent) => {
45
+ e.preventDefault();
46
+ const port = parseInt(portInput, 10);
47
+ if (port >= 1 && port <= 65535) startTunnel(port);
48
+ else setError("Port must be 1-65535");
53
49
  };
54
50
 
55
- const reloadIframe = () => {
51
+ const reload = () => {
56
52
  if (iframeRef.current) {
57
- iframeRef.current.src = iframeRef.current.src;
53
+ const src = iframeRef.current.src;
54
+ iframeRef.current.src = "";
55
+ requestAnimationFrame(() => { if (iframeRef.current) iframeRef.current.src = src; });
58
56
  }
59
57
  };
60
58
 
61
- // Port input screen
59
+ // No tunnel yet — show port input
62
60
  if (!tunnelUrl) {
63
61
  return (
64
- <div className="flex items-center justify-center h-full p-4">
65
- <div className="w-full max-w-sm space-y-4 text-center">
66
- <Globe className="size-10 mx-auto text-muted-foreground" />
67
- <div className="space-y-1">
68
- <h3 className="text-sm font-medium">Open Tunnel</h3>
69
- <p className="text-xs text-muted-foreground">
70
- Enter a local port to create a public tunnel URL
71
- </p>
72
- </div>
73
-
74
- <form
75
- onSubmit={(e) => { e.preventDefault(); startTunnel(); }}
76
- className="flex gap-2"
77
- >
78
- <Input
62
+ <div className="flex flex-col items-center justify-center h-full gap-4 p-6">
63
+ <Globe className="size-12 text-text-subtle" />
64
+ <h2 className="text-lg font-medium text-text-primary">Open Localhost</h2>
65
+ <p className="text-sm text-text-secondary text-center max-w-sm">
66
+ Enter the port of your local dev server to preview it here.
67
+ </p>
68
+ <form onSubmit={handleSubmit} className="flex items-center gap-2 w-full max-w-xs">
69
+ <div className="flex-1 flex items-center gap-2 px-3 py-2.5 rounded-lg bg-surface border border-border focus-within:border-accent/50 transition-colors">
70
+ <span className="text-sm text-text-subtle shrink-0">localhost:</span>
71
+ <input
79
72
  type="number"
80
- placeholder="e.g. 3000"
81
- value={port}
82
- onChange={(e) => setPort(e.target.value)}
83
- className="h-9 text-sm font-mono flex-1"
73
+ value={portInput}
74
+ onChange={(e) => setPortInput(e.target.value)}
75
+ placeholder="3000"
84
76
  min={1}
85
77
  max={65535}
86
78
  autoFocus
87
- disabled={loading}
79
+ className="flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-subtle min-w-0 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
88
80
  />
89
- <Button
90
- type="submit"
91
- size="sm"
92
- className="h-9 px-4 cursor-pointer"
93
- disabled={loading || !port}
94
- >
95
- {loading ? <Loader2 className="size-4 animate-spin" /> : "Connect"}
96
- </Button>
97
- </form>
98
-
99
- {error && (
100
- <p className="text-xs text-red-500">{error}</p>
101
- )}
102
- </div>
81
+ </div>
82
+ <button
83
+ type="submit"
84
+ disabled={loading || !portInput}
85
+ className="px-4 py-2.5 rounded-lg bg-accent text-white text-sm font-medium hover:bg-accent/90 disabled:opacity-50 transition-colors shrink-0"
86
+ >
87
+ {loading ? <Loader2 className="size-4 animate-spin" /> : "Open"}
88
+ </button>
89
+ </form>
90
+ {error && <p className="text-sm text-red-400">{error}</p>}
91
+ {loading && (
92
+ <div className="flex items-center gap-2 text-sm text-text-secondary">
93
+ <Loader2 className="size-4 animate-spin" />
94
+ <span>Starting tunnel... (may take a few seconds)</span>
95
+ </div>
96
+ )}
103
97
  </div>
104
98
  );
105
99
  }
106
100
 
107
- // Tunnel active — show toolbar + iframe
101
+ // Tunnel active — show iframe
108
102
  return (
109
- <div className="flex flex-col h-full">
103
+ <div className="flex flex-col h-full w-full bg-background">
110
104
  {/* Toolbar */}
111
- <div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-muted/30 shrink-0">
112
- <code className="text-[11px] font-mono text-muted-foreground truncate flex-1">
113
- {tunnelUrl}
114
- </code>
115
- <Button variant="ghost" size="sm" className="h-6 px-1.5 cursor-pointer" onClick={copyUrl} title="Copy URL">
116
- {copied ? <span className="text-[10px]">Copied!</span> : <Copy className="size-3" />}
117
- </Button>
118
- <Button variant="ghost" size="sm" className="h-6 px-1.5 cursor-pointer" onClick={reloadIframe} title="Reload">
119
- <RefreshCw className="size-3" />
120
- </Button>
121
- <a href={tunnelUrl} target="_blank" rel="noopener noreferrer">
122
- <Button variant="ghost" size="sm" className="h-6 px-1.5 cursor-pointer" title="Open in new tab">
123
- <ExternalLink className="size-3" />
124
- </Button>
125
- </a>
126
- <Button variant="ghost" size="sm" className="h-6 px-1.5 cursor-pointer text-red-500" onClick={stopTunnel} title="Stop tunnel">
127
- <X className="size-3" />
128
- </Button>
105
+ <div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border bg-surface shrink-0">
106
+ <Globe className="size-4 text-text-subtle shrink-0" />
107
+ <span className="text-xs text-text-primary font-medium">localhost:{portInput}</span>
108
+ <span className="text-xs text-text-subtle truncate ml-1">({tunnelUrl})</span>
109
+ <div className="flex-1" />
110
+ <button
111
+ onClick={reload}
112
+ className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
113
+ title="Reload"
114
+ >
115
+ <RefreshCw className="size-3.5" />
116
+ </button>
117
+ <button
118
+ onClick={() => window.open(tunnelUrl, "_blank")}
119
+ className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
120
+ title="Open in browser"
121
+ >
122
+ <ExternalLink className="size-3.5" />
123
+ </button>
124
+ <button
125
+ onClick={stopTunnel}
126
+ className="p-1.5 rounded hover:bg-surface-elevated text-red-400 transition-colors"
127
+ title="Stop tunnel"
128
+ >
129
+ <X className="size-3.5" />
130
+ </button>
129
131
  </div>
130
132
 
131
- {/* Iframe preview */}
132
- <iframe
133
- ref={iframeRef}
134
- src={tunnelUrl}
135
- className="flex-1 w-full border-none bg-white"
136
- sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
137
- title={`Preview port ${port}`}
138
- />
133
+ {/* iframe */}
134
+ <div className="flex-1 relative min-h-0">
135
+ <iframe
136
+ ref={iframeRef}
137
+ src={tunnelUrl}
138
+ className="w-full h-full border-0"
139
+ sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-modals"
140
+ onLoad={() => setLoading(false)}
141
+ />
142
+ {loading && (
143
+ <div className="absolute inset-0 flex items-center justify-center bg-background/50">
144
+ <Loader2 className="size-5 animate-spin text-text-secondary" />
145
+ </div>
146
+ )}
147
+ </div>
139
148
  </div>
140
149
  );
141
150
  }
@@ -0,0 +1,163 @@
1
+ import { useState, useEffect, useSyncExternalStore } from "react";
2
+ import { Settings, X } from "lucide-react";
3
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
4
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
5
+ import { cn } from "@/lib/utils";
6
+ import {
7
+ getAccountSettings,
8
+ updateAccountSettings,
9
+ type AccountSettings,
10
+ } from "../../lib/api-settings";
11
+
12
+ interface AccountRotationSettingsProps {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ }
16
+
17
+ const mdQuery = typeof window !== "undefined" ? window.matchMedia("(min-width: 768px)") : null;
18
+ function subscribeMedia(cb: () => void) {
19
+ mdQuery?.addEventListener("change", cb);
20
+ return () => mdQuery?.removeEventListener("change", cb);
21
+ }
22
+ function getIsDesktop() {
23
+ return mdQuery?.matches ?? true;
24
+ }
25
+
26
+ function SettingsContent() {
27
+ const [settings, setSettings] = useState<AccountSettings | null>(null);
28
+ const [loading, setLoading] = useState(true);
29
+
30
+ useEffect(() => {
31
+ setLoading(true);
32
+ getAccountSettings()
33
+ .then(setSettings)
34
+ .finally(() => setLoading(false));
35
+ }, []);
36
+
37
+ if (loading) {
38
+ return <p className="text-xs text-text-subtle py-4 text-center">Loading...</p>;
39
+ }
40
+ if (!settings) {
41
+ return <p className="text-xs text-text-subtle py-4 text-center">Failed to load settings</p>;
42
+ }
43
+
44
+ return (
45
+ <div className="space-y-4">
46
+ {/* Strategy */}
47
+ <div className="space-y-1.5">
48
+ <label className="text-xs font-medium text-text-primary">Rotation Strategy</label>
49
+ <Select
50
+ value={settings.strategy}
51
+ onValueChange={async (v) => {
52
+ const updated = await updateAccountSettings({ strategy: v as AccountSettings["strategy"] });
53
+ setSettings(updated);
54
+ }}
55
+ >
56
+ <SelectTrigger className="w-full h-9 text-xs">
57
+ <SelectValue />
58
+ </SelectTrigger>
59
+ <SelectContent>
60
+ <SelectItem value="round-robin">Round-robin</SelectItem>
61
+ <SelectItem value="fill-first">Fill-first</SelectItem>
62
+ <SelectItem value="lowest-usage">Lowest usage</SelectItem>
63
+ </SelectContent>
64
+ </Select>
65
+ <p className="text-[10px] text-text-subtle">
66
+ {settings.strategy === "round-robin" && "Cycles through accounts evenly"}
67
+ {settings.strategy === "fill-first" && "Uses one account until its limit, then moves on"}
68
+ {settings.strategy === "lowest-usage" && "Picks the account with the lowest current usage"}
69
+ </p>
70
+ </div>
71
+
72
+ {/* Max Retry */}
73
+ <div className="space-y-1.5">
74
+ <label className="text-xs font-medium text-text-primary">Max Retry</label>
75
+ <input
76
+ type="number"
77
+ min={0}
78
+ value={settings.maxRetry}
79
+ className="w-full h-9 text-xs border rounded-md px-3 bg-background"
80
+ onChange={async (e) => {
81
+ const v = parseInt(e.target.value, 10);
82
+ if (!isNaN(v) && v >= 0) {
83
+ const updated = await updateAccountSettings({ maxRetry: v });
84
+ setSettings(updated);
85
+ }
86
+ }}
87
+ />
88
+ <p className="text-[10px] text-text-subtle">
89
+ How many accounts to try on failure. 0 = try all available accounts.
90
+ </p>
91
+ </div>
92
+
93
+ {/* Active accounts */}
94
+ <div className="flex items-center justify-between text-xs border-t border-border pt-3">
95
+ <span className="text-text-subtle">Active accounts</span>
96
+ <span className="font-medium text-text-primary">{settings.activeCount}</span>
97
+ </div>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ export function AccountRotationSettings({ open, onOpenChange }: AccountRotationSettingsProps) {
103
+ const isDesktop = useSyncExternalStore(subscribeMedia, getIsDesktop);
104
+
105
+ if (!open) return null;
106
+
107
+ // Desktop: Dialog
108
+ if (isDesktop) {
109
+ return (
110
+ <Dialog open={open} onOpenChange={onOpenChange}>
111
+ <DialogContent className="sm:max-w-sm">
112
+ <DialogHeader>
113
+ <DialogTitle className="text-sm flex items-center gap-2">
114
+ <Settings className="size-4" /> Rotation & Retry
115
+ </DialogTitle>
116
+ </DialogHeader>
117
+ <SettingsContent />
118
+ </DialogContent>
119
+ </Dialog>
120
+ );
121
+ }
122
+
123
+ // Mobile: Bottom sheet
124
+ return (
125
+ <>
126
+ <div
127
+ className="fixed inset-0 z-50 transition-opacity duration-200 opacity-100"
128
+ onClick={() => onOpenChange(false)}
129
+ style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
130
+ />
131
+ <div
132
+ className={cn(
133
+ "fixed bottom-0 left-0 right-0 z-50 bg-background rounded-t-2xl border-t border-border shadow-2xl",
134
+ "transition-transform duration-300 ease-out max-h-[85vh] overflow-y-auto",
135
+ "translate-y-0",
136
+ )}
137
+ >
138
+ {/* Drag handle */}
139
+ <div className="flex justify-center pt-3 pb-1">
140
+ <div className="w-10 h-1 rounded-full bg-border" />
141
+ </div>
142
+
143
+ {/* Header */}
144
+ <div className="flex items-center justify-between px-4 py-2 border-b border-border">
145
+ <span className="text-sm font-semibold flex items-center gap-2">
146
+ <Settings className="size-4" /> Rotation & Retry
147
+ </span>
148
+ <button
149
+ onClick={() => onOpenChange(false)}
150
+ className="flex items-center justify-center size-7 rounded-md hover:bg-surface-elevated transition-colors"
151
+ >
152
+ <X className="size-4" />
153
+ </button>
154
+ </div>
155
+
156
+ {/* Content */}
157
+ <div className="px-4 py-4 pb-8">
158
+ <SettingsContent />
159
+ </div>
160
+ </div>
161
+ </>
162
+ );
163
+ }
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
- import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff } from "lucide-react";
2
+ import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck, Pin, PinOff } from "lucide-react";
3
3
  import { Activity } from "lucide-react";
4
4
  import { api, projectUrl } from "@/lib/api-client";
5
5
  import { useTabStore } from "@/stores/tab-store";
@@ -22,6 +22,7 @@ interface ChatHistoryBarProps {
22
22
  onSelectSession?: (session: SessionInfo) => void;
23
23
  onBugReport?: () => void;
24
24
  isConnected?: boolean;
25
+ streamingAccountLabel?: string | null;
25
26
  onReconnect?: () => void;
26
27
  }
27
28
 
@@ -48,9 +49,37 @@ function pctColor(pct: number): string {
48
49
  return "text-green-500";
49
50
  }
50
51
 
52
+ function DebugCopyButton({ sessionId, projectName }: { sessionId: string; projectName: string }) {
53
+ const [copied, setCopied] = useState(false);
54
+ return (
55
+ <button
56
+ onClick={async () => {
57
+ try {
58
+ const data = await api.get<{ ppmSessionId: string; sdkSessionId: string; jsonlPath: string | null; projectPath: string }>(
59
+ `${projectUrl(projectName)}/chat/sessions/${sessionId}/debug?project=${encodeURIComponent(projectName)}`,
60
+ );
61
+ const info = [
62
+ `PPM Session: ${data.ppmSessionId}`,
63
+ `SDK Session: ${data.sdkSessionId}`,
64
+ data.jsonlPath ? `JSONL: ${data.jsonlPath}` : `JSONL: not found`,
65
+ data.projectPath ? `Project: ${data.projectPath}` : null,
66
+ ].filter(Boolean).join("\n");
67
+ await navigator.clipboard.writeText(info);
68
+ setCopied(true);
69
+ setTimeout(() => setCopied(false), 1500);
70
+ } catch { /* silent */ }
71
+ }}
72
+ className={`p-1 rounded transition-colors ${copied ? "text-green-500 bg-green-500/10" : "text-text-subtle hover:text-text-secondary hover:bg-surface-elevated"}`}
73
+ title={copied ? "Copied!" : "Copy session debug info"}
74
+ >
75
+ {copied ? <ClipboardCheck className="size-3" /> : <Bug className="size-3" />}
76
+ </button>
77
+ );
78
+ }
79
+
51
80
  export function ChatHistoryBar({
52
81
  projectName, usageInfo, contextWindowPct, usageLoading, refreshUsage, lastFetchedAt,
53
- sessionId, onSelectSession, onBugReport, isConnected, onReconnect,
82
+ sessionId, onSelectSession, onBugReport, isConnected, streamingAccountLabel, onReconnect,
54
83
  }: ChatHistoryBarProps) {
55
84
  const [activePanel, setActivePanel] = useState<PanelType>(null);
56
85
  const [sessions, setSessions] = useState<SessionInfo[]>([]);
@@ -121,6 +150,27 @@ export function ChatHistoryBar({
121
150
 
122
151
  const cancelEditing = useCallback(() => setEditingId(null), []);
123
152
 
153
+ const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
154
+ e.stopPropagation();
155
+ if (!projectName) return;
156
+ const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
157
+ try {
158
+ if (session.pinned) {
159
+ await api.del(url);
160
+ } else {
161
+ await api.put(url);
162
+ }
163
+ setSessions((prev) => {
164
+ const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
165
+ return updated.sort((a, b) => {
166
+ if (a.pinned && !b.pinned) return -1;
167
+ if (!a.pinned && b.pinned) return 1;
168
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
169
+ });
170
+ });
171
+ } catch { /* silent */ }
172
+ }, [projectName]);
173
+
124
174
  // Filter sessions by search query
125
175
  const filteredSessions = searchQuery.trim()
126
176
  ? sessions.filter((s) => (s.title || "").toLowerCase().includes(searchQuery.toLowerCase()))
@@ -167,8 +217,8 @@ export function ChatHistoryBar({
167
217
  title="Usage limits"
168
218
  >
169
219
  <Activity className="size-3" />
170
- {usageInfo.activeAccountLabel && (
171
- <span className="text-text-secondary font-normal truncate max-w-[60px]">[{usageInfo.activeAccountLabel}]</span>
220
+ {(streamingAccountLabel || usageInfo.activeAccountLabel) && (
221
+ <span className="text-text-secondary font-normal truncate max-w-[60px]">[{streamingAccountLabel || usageInfo.activeAccountLabel}]</span>
172
222
  )}
173
223
  <span>5h:{fiveHourPct != null ? `${fiveHourPct}%` : "--%"}</span>
174
224
  <span className="text-text-subtle">·</span>
@@ -195,6 +245,11 @@ export function ChatHistoryBar({
195
245
  </button>
196
246
  )}
197
247
 
248
+ {/* Debug info — copy session IDs + JSONL path */}
249
+ {sessionId && (
250
+ <DebugCopyButton sessionId={sessionId} projectName={projectName} />
251
+ )}
252
+
198
253
  {/* Connection indicator */}
199
254
  {onReconnect && (
200
255
  <button
@@ -277,9 +332,20 @@ export function ChatHistoryBar({
277
332
  >
278
333
  {session.title || "Untitled"}
279
334
  </button>
335
+ <button
336
+ onClick={(e) => togglePin(e, session)}
337
+ className={`p-0.5 rounded transition-all ${
338
+ session.pinned
339
+ ? "text-primary hover:text-primary/70"
340
+ : "text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100"
341
+ }`}
342
+ title={session.pinned ? "Unpin session" : "Pin session"}
343
+ >
344
+ {session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
345
+ </button>
280
346
  <button
281
347
  onClick={(e) => startEditing(session, e)}
282
- className="p-0.5 rounded text-text-subtle hover:text-text-secondary opacity-0 group-hover:opacity-100 transition-opacity"
348
+ className="p-0.5 rounded text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100 transition-opacity"
283
349
  title="Rename session"
284
350
  >
285
351
  <Pencil className="size-3" />
@@ -287,7 +353,7 @@ export function ChatHistoryBar({
287
353
  </>
288
354
  )}
289
355
  {editingId !== session.id && session.updatedAt && (
290
- <span className="text-[10px] text-text-subtle shrink-0">{formatDate(session.updatedAt)}</span>
356
+ <span className="text-[10px] text-text-subtle shrink-0 w-10 text-right">{formatDate(session.updatedAt)}</span>
291
357
  )}
292
358
  </div>
293
359
  ))
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useRef, useEffect } from "react";
2
- import { Upload, X } from "lucide-react";
2
+ import { Loader2, Upload, X } from "lucide-react";
3
3
  import { api, projectUrl } from "@/lib/api-client";
4
4
  import { useChat } from "@/hooks/use-chat";
5
5
  import { useUsage } from "@/hooks/use-usage";
@@ -14,6 +14,7 @@ import { MessageInput, type ChatAttachment } from "./message-input";
14
14
  import { SlashCommandPicker, type SlashItem } from "./slash-command-picker";
15
15
  import { FilePicker } from "./file-picker";
16
16
  import { ChatHistoryBar } from "./chat-history-bar";
17
+ import { ChatWelcome } from "./chat-welcome";
17
18
  import type { DragEvent } from "react";
18
19
  import type { FileNode } from "../../../types/project";
19
20
  import type { Session, SessionInfo } from "../../../types/chat";
@@ -83,12 +84,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
83
84
  messages,
84
85
  messagesLoading,
85
86
  isStreaming,
86
- streamingStatus,
87
+ phase,
88
+ isReconnecting,
87
89
  connectingElapsed,
88
- thinkingWarningThreshold,
89
90
  pendingApproval,
90
91
  contextWindowPct,
91
92
  sessionTitle,
93
+ streamingAccountLabel,
92
94
  sendMessage,
93
95
  respondToApproval,
94
96
  cancelStreaming,
@@ -311,19 +313,32 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
311
313
  </div>
312
314
  )}
313
315
 
314
- {/* Messages */}
315
- <MessageList
316
- messages={messages}
317
- messagesLoading={messagesLoading}
318
- pendingApproval={pendingApproval}
319
- onApprovalResponse={respondToApproval}
320
- isStreaming={isStreaming}
321
- streamingStatus={streamingStatus}
322
- connectingElapsed={connectingElapsed}
323
- thinkingWarningThreshold={thinkingWarningThreshold}
324
- projectName={projectName}
325
- onFork={!isStreaming ? handleFork : undefined}
326
- />
316
+ {/* Reconnect overlay */}
317
+ {isReconnecting && (
318
+ <div className="absolute inset-0 z-50 flex items-center justify-center bg-background/60 backdrop-blur-sm">
319
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
320
+ <Loader2 className="size-4 animate-spin" />
321
+ <span>Reconnecting...</span>
322
+ </div>
323
+ </div>
324
+ )}
325
+
326
+ {/* Messages or Welcome screen */}
327
+ {!sessionId ? (
328
+ <ChatWelcome projectName={projectName} onSelectSession={handleSelectSession} />
329
+ ) : (
330
+ <MessageList
331
+ messages={messages}
332
+ messagesLoading={messagesLoading}
333
+ pendingApproval={pendingApproval}
334
+ onApprovalResponse={respondToApproval}
335
+ isStreaming={isStreaming}
336
+ phase={phase}
337
+ connectingElapsed={connectingElapsed}
338
+ projectName={projectName}
339
+ onFork={!isStreaming ? handleFork : undefined}
340
+ />
341
+ )}
327
342
 
328
343
  {/* Bottom toolbar */}
329
344
  <div className="border-t border-border bg-background shrink-0">
@@ -339,6 +354,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
339
354
  onSelectSession={handleSelectSession}
340
355
  onBugReport={sessionId ? () => openBugReportPopup(version, { sessionId, projectName }) : undefined}
341
356
  isConnected={isConnected}
357
+ streamingAccountLabel={streamingAccountLabel}
342
358
  onReconnect={() => {
343
359
  if (!isConnected) reconnect();
344
360
  refetchMessages();