@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
@@ -1,6 +1,5 @@
1
1
  import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
2
- import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff } from "lucide-react";
3
- import { useVoiceInput } from "@/hooks/use-voice-input";
2
+ import { ArrowUp, Square, Paperclip } from "lucide-react";
4
3
  import { api, projectUrl, getAuthToken } from "@/lib/api-client";
5
4
  import { randomId } from "@/lib/utils";
6
5
  import { isSupportedFile, isImageFile } from "@/lib/file-support";
@@ -68,48 +67,12 @@ export const MessageInput = memo(function MessageInput({
68
67
  const [value, setValue] = useState(initialValue ?? "");
69
68
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
70
69
  const [modeSelectorOpen, setModeSelectorOpen] = useState(false);
71
- const [pendingSend, setPendingSend] = useState(false);
72
70
  const textareaRef = useRef<HTMLTextAreaElement>(null);
73
71
  const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
74
72
  const fileInputRef = useRef<HTMLInputElement>(null);
75
73
  const slashItemsRef = useRef<SlashItem[]>([]);
76
74
  const fileItemsRef = useRef<FileNode[]>([]);
77
75
 
78
- // Voice input (Web Speech API)
79
- const voice = useVoiceInput();
80
- // Store pre-voice text so voice appends to existing input
81
- const preVoiceTextRef = useRef("");
82
- const voiceResultCb = useCallback((text: string) => {
83
- const prefix = preVoiceTextRef.current;
84
- const newValue = prefix ? prefix + " " + text : text;
85
- setValue(newValue);
86
- // Auto-resize textarea
87
- requestAnimationFrame(() => {
88
- const ta = window.matchMedia("(min-width: 768px)").matches
89
- ? textareaRef.current
90
- : mobileTextareaRef.current;
91
- if (ta) {
92
- ta.style.height = "auto";
93
- ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
94
- }
95
- });
96
- }, []);
97
- const handleVoiceToggle = useCallback(() => {
98
- if (voice.isListening) {
99
- voice.stop();
100
- } else {
101
- preVoiceTextRef.current = value.trim();
102
- voice.start(voiceResultCb);
103
- }
104
- }, [voice.isListening, voice.start, voice.stop, value, voiceResultCb]);
105
-
106
- // Listen for global keyboard shortcut (Cmd+Shift+V) to toggle voice
107
- useEffect(() => {
108
- const handler = () => { if (voice.supported) handleVoiceToggle(); };
109
- window.addEventListener("toggle-voice-input", handler);
110
- return () => window.removeEventListener("toggle-voice-input", handler);
111
- }, [voice.supported, handleVoiceToggle]);
112
-
113
76
  // Apply initialValue when it changes (e.g. "Ask AI" from command palette)
114
77
  useEffect(() => {
115
78
  if (initialValue) {
@@ -311,18 +274,14 @@ export const MessageInput = memo(function MessageInput({
311
274
  });
312
275
  }, []);
313
276
 
314
- /** Execute the actual send (called directly or after uploads complete) */
315
- const executeSend = useCallback(() => {
277
+ const handleSend = useCallback(() => {
316
278
  const trimmed = value.trim();
317
279
  const readyAttachments = attachments.filter((a) => a.status === "ready");
318
- if (!trimmed && readyAttachments.length === 0) {
319
- setPendingSend(false);
320
- return;
321
- }
280
+ if (!trimmed && readyAttachments.length === 0) return;
281
+ if (disabled) return;
322
282
 
323
283
  onSlashStateChange?.(false, "");
324
284
  onFileStateChange?.(false, "");
325
- if (voice.isListening) voice.stop();
326
285
  onSend(trimmed, readyAttachments);
327
286
  setValue("");
328
287
  // Revoke preview URLs
@@ -330,32 +289,9 @@ export const MessageInput = memo(function MessageInput({
330
289
  if (att.previewUrl) URL.revokeObjectURL(att.previewUrl);
331
290
  }
332
291
  setAttachments([]);
333
- setPendingSend(false);
334
292
  if (textareaRef.current) textareaRef.current.style.height = "auto";
335
293
  if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
336
- }, [value, attachments, onSend, onSlashStateChange, onFileStateChange]);
337
-
338
- const handleSend = useCallback(() => {
339
- if (disabled) return;
340
-
341
- // If files are still uploading, queue the send for when they finish
342
- if (attachments.some((a) => a.status === "uploading")) {
343
- const trimmed = value.trim();
344
- if (trimmed || attachments.some((a) => a.status !== "error")) {
345
- setPendingSend(true);
346
- }
347
- return;
348
- }
349
-
350
- executeSend();
351
- }, [value, attachments, disabled, executeSend]);
352
-
353
- // Auto-send when queued and all uploads complete
354
- useEffect(() => {
355
- if (!pendingSend) return;
356
- if (attachments.some((a) => a.status === "uploading")) return;
357
- executeSend();
358
- }, [pendingSend, attachments, executeSend]);
294
+ }, [value, attachments, disabled, onSend, onSlashStateChange, onFileStateChange]);
359
295
 
360
296
  const handleKeyDown = useCallback(
361
297
  (e: KeyboardEvent<HTMLTextAreaElement>) => {
@@ -468,7 +404,7 @@ export const MessageInput = memo(function MessageInput({
468
404
  [processFiles],
469
405
  );
470
406
 
471
- const hasContent = value.trim().length > 0 || attachments.some((a) => a.status !== "error");
407
+ const hasContent = value.trim().length > 0 || attachments.some((a) => a.status === "ready");
472
408
  const showCancel = isStreaming && !hasContent;
473
409
 
474
410
  return (
@@ -502,7 +438,7 @@ export const MessageInput = memo(function MessageInput({
502
438
  onOpenChange={setModeSelectorOpen}
503
439
  />
504
440
  </div>
505
- {/* Mobile: single row — attach + mic + textarea + send */}
441
+ {/* Mobile: single row — attach + textarea + send */}
506
442
  <div className="flex items-end gap-1 md:hidden px-2 py-2">
507
443
  <button
508
444
  type="button"
@@ -513,21 +449,6 @@ export const MessageInput = memo(function MessageInput({
513
449
  >
514
450
  <Paperclip className="size-4" />
515
451
  </button>
516
- {voice.supported && (
517
- <button
518
- type="button"
519
- onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
520
- disabled={disabled}
521
- className={`flex items-center justify-center size-7 shrink-0 rounded-full transition-colors disabled:opacity-50 ${
522
- voice.isListening
523
- ? "bg-red-600 text-white animate-pulse"
524
- : "text-text-subtle hover:text-text-primary"
525
- }`}
526
- aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
527
- >
528
- {voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
529
- </button>
530
- )}
531
452
  <textarea
532
453
  ref={mobileTextareaRef}
533
454
  value={value}
@@ -551,12 +472,12 @@ export const MessageInput = memo(function MessageInput({
551
472
  </button>
552
473
  ) : (
553
474
  <button
554
- onClick={(e) => { e.stopPropagation(); pendingSend ? setPendingSend(false) : handleSend(); }}
475
+ onClick={(e) => { e.stopPropagation(); handleSend(); }}
555
476
  disabled={disabled || !hasContent}
556
477
  className="flex items-center justify-center size-7 shrink-0 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 transition-colors"
557
- aria-label={pendingSend ? "Cancel queued send" : "Send"}
478
+ aria-label="Send"
558
479
  >
559
- {pendingSend ? <Loader2 className="size-3.5 animate-spin" /> : <ArrowUp className="size-3.5" />}
480
+ <ArrowUp className="size-3.5" />
560
481
  </button>
561
482
  )}
562
483
  </div>
@@ -587,21 +508,6 @@ export const MessageInput = memo(function MessageInput({
587
508
  >
588
509
  <Paperclip className="size-4" />
589
510
  </button>
590
- {voice.supported && (
591
- <button
592
- type="button"
593
- onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
594
- disabled={disabled}
595
- className={`flex items-center justify-center size-8 rounded-full transition-colors disabled:opacity-50 ${
596
- voice.isListening
597
- ? "bg-red-600 text-white animate-pulse"
598
- : "text-text-subtle hover:text-text-primary hover:bg-surface-elevated"
599
- }`}
600
- aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
601
- >
602
- {voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
603
- </button>
604
- )}
605
511
  {/* Mode indicator chip */}
606
512
  <div className="relative">
607
513
  <ModeChip
@@ -627,12 +533,12 @@ export const MessageInput = memo(function MessageInput({
627
533
  </button>
628
534
  ) : (
629
535
  <button
630
- onClick={(e) => { e.stopPropagation(); pendingSend ? setPendingSend(false) : handleSend(); }}
536
+ onClick={(e) => { e.stopPropagation(); handleSend(); }}
631
537
  disabled={disabled || !hasContent}
632
538
  className="flex items-center justify-center size-8 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
633
- aria-label={pendingSend ? "Cancel queued send" : "Send message"}
539
+ aria-label="Send message"
634
540
  >
635
- {pendingSend ? <Loader2 className="size-4 animate-spin" /> : <ArrowUp className="size-4" />}
541
+ <ArrowUp className="size-4" />
636
542
  </button>
637
543
  )}
638
544
  </div>
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
2
2
  import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
3
3
  import { getAuthToken } from "@/lib/api-client";
4
4
  import type { ChatMessage, ChatEvent } from "../../../types/chat";
5
- import type { SessionPhase } from "../../../types/api";
5
+ import type { StreamingStatus } from "@/hooks/use-chat";
6
6
  import { ToolCard } from "./tool-cards";
7
7
  import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
8
8
  import { cn, basename } from "@/lib/utils";
@@ -39,8 +39,9 @@ interface MessageListProps {
39
39
  pendingApproval: { requestId: string; tool: string; input: unknown } | null;
40
40
  onApprovalResponse: (requestId: string, approved: boolean, data?: unknown) => void;
41
41
  isStreaming: boolean;
42
- phase?: SessionPhase;
42
+ streamingStatus?: StreamingStatus;
43
43
  connectingElapsed?: number;
44
+ thinkingWarningThreshold?: number;
44
45
  projectName?: string;
45
46
  /** Called when user clicks Fork/Rewind — opens new forked chat tab */
46
47
  onFork?: (userMessage: string) => void;
@@ -52,8 +53,9 @@ export function MessageList({
52
53
  pendingApproval,
53
54
  onApprovalResponse,
54
55
  isStreaming,
55
- phase,
56
+ streamingStatus,
56
57
  connectingElapsed,
58
+ thinkingWarningThreshold,
57
59
  projectName,
58
60
  onFork,
59
61
  }: MessageListProps) {
@@ -106,7 +108,7 @@ export function MessageList({
106
108
  : <ApprovalCard approval={pendingApproval} onRespond={onApprovalResponse} />
107
109
  )}
108
110
 
109
- {isStreaming && <ThinkingIndicator lastMessage={messages[messages.length - 1]} phase={phase} elapsed={connectingElapsed} />}
111
+ {isStreaming && <ThinkingIndicator lastMessage={messages[messages.length - 1]} streamingStatus={streamingStatus} elapsed={connectingElapsed} warningThreshold={thinkingWarningThreshold} />}
110
112
  </StickToBottom.Content>
111
113
  <ScrollToBottomButton />
112
114
  </StickToBottom>
@@ -610,13 +612,6 @@ function InterleavedEvents({ events, isStreaming, projectName }: { events: ChatE
610
612
  groups.push({ kind: "thinking", content: thinkingBuffer });
611
613
  thinkingBuffer = "";
612
614
  }
613
- if (event.type === "account_retry") {
614
- if (textBuffer) { groups.push({ kind: "text", content: textBuffer }); textBuffer = ""; }
615
- const label = (event as any).accountLabel ?? "another account";
616
- const reason = (event as any).reason ?? "Auth failed";
617
- groups.push({ kind: "text", content: `\n\n> ↻ ${reason} — retrying with **${label}**...\n\n` });
618
- continue;
619
- }
620
615
  if (event.type === "text") {
621
616
  textBuffer += event.content;
622
617
  } else if (event.type === "tool_use") {
@@ -726,11 +721,9 @@ function ThinkingBlock({ content, isStreaming }: { content: string; isStreaming:
726
721
  {!isStreaming && <span className="text-text-subtle/50 ml-auto">{content.length > 100 ? `${Math.round(content.length / 4)} tokens` : ""}</span>}
727
722
  </button>
728
723
  {expanded && (
729
- <StickToBottom className="max-h-60 overflow-y-auto" resize="smooth" initial="instant">
730
- <StickToBottom.Content className="px-2 pb-2 text-text-subtle/80 whitespace-pre-wrap text-[11px] leading-relaxed">
731
- {content}
732
- </StickToBottom.Content>
733
- </StickToBottom>
724
+ <div className="px-2 pb-2 text-text-subtle/80 whitespace-pre-wrap max-h-60 overflow-y-auto text-[11px] leading-relaxed">
725
+ {content}
726
+ </div>
734
727
  )}
735
728
  </div>
736
729
  );
@@ -758,13 +751,14 @@ function StreamingText({ content, animate: isStreaming, projectName }: { content
758
751
  * - After tool: "Processing..."
759
752
  * - Text streaming: hidden
760
753
  */
761
- function ThinkingIndicator({ lastMessage, phase, elapsed }: { lastMessage?: ChatMessage; phase?: SessionPhase; elapsed?: number }) {
762
- // Show indicator when:
754
+ function ThinkingIndicator({ lastMessage, streamingStatus, elapsed, warningThreshold = 15 }: { lastMessage?: ChatMessage; streamingStatus?: StreamingStatus; elapsed?: number; warningThreshold?: number }) {
755
+ // Show "Thinking" when:
763
756
  // 1. No assistant message yet (waiting for first response)
764
- // 2. Last event is tool_result (Claude thinking after tool execution)
757
+ // 2. Last event is tool_use/tool_result (Claude thinking after tool execution)
765
758
  // Hide when text is actively streaming (text itself is the indicator)
766
759
 
767
760
  const isWaiting = !lastMessage || lastMessage.role !== "assistant";
761
+ // Show Thinking only after tool_result (tool finished), not tool_use (tool still running)
768
762
  const isAfterTool = (() => {
769
763
  if (!lastMessage?.events?.length) return false;
770
764
  const last = lastMessage.events[lastMessage.events.length - 1]!;
@@ -773,19 +767,13 @@ function ThinkingIndicator({ lastMessage, phase, elapsed }: { lastMessage?: Chat
773
767
 
774
768
  if (!isWaiting && !isAfterTool) return null;
775
769
 
776
- const label = phase === "initializing" ? "Initializing"
777
- : phase === "connecting" ? "Connecting"
778
- : phase === "thinking" ? "Thinking"
779
- : "Processing";
780
-
781
- const isLong = phase === "connecting" && (elapsed ?? 0) >= 30;
782
-
770
+ const isLong = isWaiting && (elapsed ?? 0) >= warningThreshold;
783
771
  return (
784
772
  <div className="flex flex-col gap-1 text-sm">
785
773
  <div className="flex items-center gap-2 text-text-subtle">
786
774
  <Loader2 className="size-3 animate-spin" />
787
775
  <span>
788
- {label}
776
+ Thinking
789
777
  {isWaiting && (elapsed ?? 0) > 0 && <span className="text-text-subtle/60">... ({elapsed}s)</span>}
790
778
  </span>
791
779
  </div>
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
2
  import { api, projectUrl } from "@/lib/api-client";
3
- import { Plus, Trash2, MessageSquare, ChevronDown, Pin, PinOff } from "lucide-react";
3
+ import { Plus, Trash2, MessageSquare, ChevronDown } from "lucide-react";
4
4
  import type { SessionInfo } from "../../../types/chat";
5
5
 
6
6
  interface SessionPickerProps {
@@ -57,75 +57,6 @@ export function SessionPicker({
57
57
  }
58
58
  };
59
59
 
60
- const handleTogglePin = async (e: React.MouseEvent, session: SessionInfo) => {
61
- e.stopPropagation();
62
- if (!projectName) return;
63
- const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
64
- try {
65
- if (session.pinned) {
66
- await api.del(url);
67
- } else {
68
- await api.put(url);
69
- }
70
- setSessions((prev) => {
71
- const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
72
- return updated.sort((a, b) => {
73
- if (a.pinned && !b.pinned) return -1;
74
- if (!a.pinned && b.pinned) return 1;
75
- return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
76
- });
77
- });
78
- } catch {
79
- // Silently fail
80
- }
81
- };
82
-
83
- function renderSessionRow(session: SessionInfo) {
84
- return (
85
- <div
86
- key={session.id}
87
- onClick={() => {
88
- onSelectSession(session);
89
- setOpen(false);
90
- }}
91
- className={`group flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-surface-elevated transition-colors ${
92
- session.id === currentSessionId
93
- ? "bg-surface-elevated text-text-primary"
94
- : "text-text-secondary"
95
- }`}
96
- >
97
- <div className="flex flex-col min-w-0 flex-1">
98
- <span className="truncate text-xs font-medium">
99
- {session.title}
100
- </span>
101
- <span className="text-xs text-text-subtle">
102
- {new Date(session.createdAt).toLocaleDateString()}
103
- </span>
104
- </div>
105
- <div className="flex items-center gap-0.5 shrink-0">
106
- <button
107
- onClick={(e) => handleTogglePin(e, session)}
108
- className={`p-1 rounded transition-colors ${
109
- session.pinned
110
- ? "text-primary hover:text-primary/70"
111
- : "text-text-subtle md:opacity-0 md:group-hover:opacity-100 hover:text-text-primary"
112
- }`}
113
- aria-label={session.pinned ? "Unpin session" : "Pin session"}
114
- >
115
- {session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
116
- </button>
117
- <button
118
- onClick={(e) => handleDelete(e, session)}
119
- className="p-1 rounded hover:bg-red-500/20 text-text-subtle hover:text-red-400 transition-colors md:opacity-0 md:group-hover:opacity-100"
120
- aria-label="Delete session"
121
- >
122
- <Trash2 className="size-3" />
123
- </button>
124
- </div>
125
- </div>
126
- );
127
- }
128
-
129
60
  return (
130
61
  <div className="relative">
131
62
  <button
@@ -169,14 +100,36 @@ export function SessionPicker({
169
100
  No sessions yet
170
101
  </p>
171
102
  )}
172
- {sessions.filter((s) => s.pinned).length > 0 && (
173
- <p className="px-3 py-1 text-[10px] text-text-subtle uppercase tracking-wider bg-surface">Pinned</p>
174
- )}
175
- {sessions.filter((s) => s.pinned).map((session) => renderSessionRow(session))}
176
- {sessions.filter((s) => s.pinned).length > 0 && sessions.filter((s) => !s.pinned).length > 0 && (
177
- <div className="border-t border-border" />
178
- )}
179
- {sessions.filter((s) => !s.pinned).map((session) => renderSessionRow(session))}
103
+ {sessions.map((session) => (
104
+ <div
105
+ key={session.id}
106
+ onClick={() => {
107
+ onSelectSession(session);
108
+ setOpen(false);
109
+ }}
110
+ className={`flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-surface-elevated transition-colors ${
111
+ session.id === currentSessionId
112
+ ? "bg-surface-elevated text-text-primary"
113
+ : "text-text-secondary"
114
+ }`}
115
+ >
116
+ <div className="flex flex-col min-w-0 flex-1">
117
+ <span className="truncate text-xs font-medium">
118
+ {session.title}
119
+ </span>
120
+ <span className="text-xs text-text-subtle">
121
+ {new Date(session.createdAt).toLocaleDateString()}
122
+ </span>
123
+ </div>
124
+ <button
125
+ onClick={(e) => handleDelete(e, session)}
126
+ className="p-1 rounded hover:bg-red-500/20 text-text-subtle hover:text-red-400 transition-colors shrink-0"
127
+ aria-label="Delete session"
128
+ >
129
+ <Trash2 className="size-3" />
130
+ </button>
131
+ </div>
132
+ ))}
180
133
  </div>
181
134
  </div>
182
135
  </>
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
- import { Activity, RefreshCw, Eye, Download, Upload, Plus, X, Settings } from "lucide-react";
2
+ import { Activity, RefreshCw, Eye, Download, Upload, Plus, X } from "lucide-react";
3
3
  import { Switch } from "@/components/ui/switch";
4
4
  import type { UsageInfo, LimitBucket } from "../../../types/chat";
5
5
  import {
@@ -12,7 +12,6 @@ import {
12
12
  type OAuthProfileData,
13
13
  } from "../../lib/api-settings";
14
14
  import { AddAccountDialog, ExportAccountsDialog, ImportAccountsDialog } from "./account-dialogs";
15
- import { AccountRotationSettings } from "./account-rotation-settings";
16
15
 
17
16
  interface UsageBadgeProps {
18
17
  usage: UsageInfo;
@@ -246,7 +245,6 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
246
245
  const [showAddDialog, setShowAddDialog] = useState(false);
247
246
  const [showExportDialog, setShowExportDialog] = useState(false);
248
247
  const [showImportDialog, setShowImportDialog] = useState(false);
249
- const [showRotationSettings, setShowRotationSettings] = useState(false);
250
248
  const [exportPreselect, setExportPreselect] = useState<string | null>(null);
251
249
  const [message, setMessage] = useState<string | null>(null);
252
250
  const msgTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
@@ -340,13 +338,6 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
340
338
  )}
341
339
  </div>
342
340
  <div className="flex items-center gap-1">
343
- <button
344
- onClick={() => setShowRotationSettings(true)}
345
- className="text-xs text-text-subtle hover:text-text-primary px-1 cursor-pointer"
346
- title="Rotation & retry settings"
347
- >
348
- <Settings className="size-3" />
349
- </button>
350
341
  {onReload && (
351
342
  <button
352
343
  onClick={() => { onReload(); loadAll(); }}
@@ -464,7 +455,6 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
464
455
  <AddAccountDialog open={showAddDialog} onOpenChange={setShowAddDialog} onSuccess={handleSuccess} />
465
456
  <ExportAccountsDialog open={showExportDialog} onOpenChange={(v) => { setShowExportDialog(v); if (!v) setExportPreselect(null); }} accounts={accounts} preselectId={exportPreselect} onMessage={showMessage} />
466
457
  <ImportAccountsDialog open={showImportDialog} onOpenChange={setShowImportDialog} onSuccess={handleSuccess} />
467
- <AccountRotationSettings open={showRotationSettings} onOpenChange={setShowRotationSettings} />
468
458
  </div>
469
459
  );
470
460
  }
@@ -7,12 +7,7 @@ import { useTabStore } from "@/stores/tab-store";
7
7
  import { useSettingsStore } from "@/stores/settings-store";
8
8
  import { basename } from "@/lib/utils";
9
9
  import { useMonacoTheme } from "@/lib/use-monaco-theme";
10
- import { Loader2, FileWarning, ExternalLink } from "lucide-react";
11
- import { EditorBreadcrumb } from "./editor-breadcrumb";
12
- import { EditorToolbar } from "./editor-toolbar";
13
- import { lazy, Suspense } from "react";
14
-
15
- const CsvPreview = lazy(() => import("./csv-preview").then((m) => ({ default: m.CsvPreview })));
10
+ import { Loader2, FileWarning, ExternalLink, Code, Eye, WrapText } from "lucide-react";
16
11
 
17
12
  /** Image extensions renderable inline */
18
13
  const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico"]);
@@ -63,9 +58,7 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
63
58
  const isPdf = ext === "pdf";
64
59
  const isSqlite = SQLITE_EXTS.has(ext);
65
60
  const isMarkdown = ext === "md" || ext === "mdx";
66
- const isCsv = ext === "csv";
67
61
  const [mdMode, setMdMode] = useState<"edit" | "preview">("preview");
68
- const [csvMode, setCsvMode] = useState<"table" | "raw">("table");
69
62
 
70
63
  // Redirect .db files to sqlite viewer by changing tab type
71
64
  useEffect(() => {
@@ -203,36 +196,33 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
203
196
  );
204
197
  }
205
198
 
199
+ const mdModeButtons = isMarkdown ? (
200
+ <>
201
+ <button type="button" onClick={() => setMdMode("edit")}
202
+ className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "edit" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
203
+ >
204
+ <Code className="size-3" /> Edit
205
+ </button>
206
+ <button type="button" onClick={() => setMdMode("preview")}
207
+ className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "preview" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
208
+ >
209
+ <Eye className="size-3" /> Preview
210
+ </button>
211
+ </>
212
+ ) : null;
213
+
214
+ const wrapBtn = (
215
+ <button type="button" onClick={toggleWordWrap} title="Toggle word wrap (Alt+Z)"
216
+ className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${wordWrap ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
217
+ >
218
+ <WrapText className="size-3" />
219
+ <span className="hidden sm:inline">Wrap</span>
220
+ </button>
221
+ );
222
+
206
223
  return (
207
224
  <div className="flex flex-col h-full w-full overflow-hidden">
208
- {/* Breadcrumb + Toolbar bar desktop only */}
209
- {filePath && projectName && tabId && (
210
- <div className="hidden md:flex items-center h-7 border-b border-border bg-background shrink-0">
211
- <EditorBreadcrumb
212
- filePath={filePath}
213
- projectName={projectName}
214
- tabId={tabId}
215
- className="flex items-center flex-1 min-w-0 overflow-x-auto scrollbar-none px-2 gap-0.5"
216
- />
217
- <EditorToolbar
218
- ext={ext}
219
- mdMode={mdMode}
220
- onMdModeChange={setMdMode}
221
- csvMode={csvMode}
222
- onCsvModeChange={setCsvMode}
223
- wordWrap={wordWrap}
224
- onToggleWordWrap={toggleWordWrap}
225
- className="shrink-0 flex items-center gap-1 px-2"
226
- />
227
- </div>
228
- )}
229
-
230
- {/* Content area */}
231
- {isCsv && csvMode === "table" ? (
232
- <Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>}>
233
- <CsvPreview content={content ?? ""} onContentChange={handleChange} wordWrap={wordWrap} />
234
- </Suspense>
235
- ) : isMarkdown && mdMode === "preview" ? (
225
+ {isMarkdown && mdMode === "preview" ? (
236
226
  <MarkdownPreview content={content ?? ""} />
237
227
  ) : (
238
228
  <div className="flex-1 overflow-hidden">
@@ -11,7 +11,6 @@ import {
11
11
  FolderOpen,
12
12
  Loader2,
13
13
  Globe,
14
- Mic,
15
14
  } from "lucide-react";
16
15
  import { useTabStore, type TabType } from "@/stores/tab-store";
17
16
  import { useProjectStore } from "@/stores/project-store";
@@ -158,9 +157,8 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
158
157
  { id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
159
158
  { id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
160
159
  { id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action", shortcut: formatShortcut(getBinding("open-git-graph")) },
161
- { id: "browser", label: "Open Browser", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "web preview localhost iframe url", group: "action" },
162
160
  { id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
163
- { id: "voice-input", label: "Voice Input", icon: Mic, action: () => { window.dispatchEvent(new CustomEvent("toggle-voice-input")); onClose(); }, keywords: "speech microphone dictate voice", group: "action", shortcut: formatShortcut(getBinding("voice-input")) },
161
+ { id: "browser", label: "Browser (Port Tunnel)", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "preview tunnel port iframe web", group: "action" },
164
162
  { id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action", shortcut: formatShortcut(getBinding("open-git-status")) },
165
163
  {
166
164
  id: "settings", label: "Settings", icon: Settings,