@hienlh/ppm 0.9.0-beta.2 → 0.9.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +10 -26
  2. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-COwDPZl_.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-DCb0mkTp.js} +1 -1
  4. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CuUkz5gb.js} +1 -1
  5. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-D0bJaFyD.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-BVEUkQYB.js} +1 -1
  8. package/dist/web/assets/arrow-left-C_j9Ki73.js +1 -0
  9. package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-CU2t4NHJ.js} +1 -1
  10. package/dist/web/assets/browser-tab-BhTdeeZd.js +1 -0
  11. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-DzjR91sM.js} +1 -1
  12. package/dist/web/assets/channel-CKNZAqoN.js +1 -0
  13. package/dist/web/assets/chat-tab-ZiiUVOxM.js +7 -0
  14. package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-0YMkpW2S.js} +1 -1
  15. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-Dp0pTM5r.js} +1 -1
  16. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CuYKSUgJ.js} +1 -1
  17. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-DvbvLUIN.js} +2 -2
  18. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CcEW1AMZ.js} +1 -1
  19. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cgt-qg75.js} +1 -1
  20. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-JCLgVcaC.js} +1 -1
  21. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-B82RP9ow.js} +1 -1
  22. package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +2 -0
  23. package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +1 -0
  24. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-Pb-JMOgO.js} +1 -1
  25. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-BRj-ZEvL.js} +1 -1
  26. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-CBRPKraG.js} +1 -1
  27. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-DNFj84V6.js} +1 -1
  28. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BnPzQK-O.js} +1 -1
  29. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-BRj25yO7.js} +1 -1
  30. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BdXwVXjJ.js} +1 -1
  31. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-LfXT4p8B.js} +1 -1
  32. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-EdgQyTqa.js} +1 -1
  33. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-D3thuSok.js} +1 -1
  34. package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +1 -0
  35. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-LdG7RqsM.js} +1 -1
  36. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-BHFnnXOt.js} +1 -1
  37. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DUmQrLsF.js} +1 -1
  38. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-CsGTseUr.js} +1 -1
  39. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-5W2emiq4.js} +1 -1
  40. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-COdZIaX4.js} +1 -1
  41. package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +1 -0
  42. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +1 -0
  43. package/dist/web/assets/clone-DNDy9Sms.js +1 -0
  44. package/dist/web/assets/{code-editor-CQ7gq0Vj.js → code-editor-BRMOypkX.js} +1 -1
  45. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-C1QJ6GPW.js} +1 -1
  46. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-CWo8w9wK.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-Br4t5TRV.js} +1 -1
  48. package/dist/web/assets/database-viewer-CEoDpzPz.js +1 -0
  49. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-CkDC2uAj.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-NvhckwcA.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO--nUaNiyB.js} +1 -1
  52. package/dist/web/assets/{diff-viewer-BjtTemkK.js → diff-viewer-jDU2bcGj.js} +1 -1
  53. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DK4QEZYh.js} +1 -1
  54. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-B9h_Ba-v.js} +1 -1
  55. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BVlftqyZ.js} +1 -1
  56. package/dist/web/assets/git-graph-DMQzw4Sp.js +1 -0
  57. package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +1 -0
  58. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js} +1 -1
  59. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BbbiUImY.js} +1 -1
  60. package/dist/web/assets/index-B4Iz1Wbi.css +2 -0
  61. package/dist/web/assets/index-QiSWS6f-.js +37 -0
  62. package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +1 -0
  63. package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +2 -0
  64. package/dist/web/assets/input-DGlv6gt_.js +41 -0
  65. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-DXomfd7J.js} +1 -1
  66. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-cW7SMLa_.js} +1 -1
  67. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-DFQXUZsc.js} +1 -1
  68. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-BMUhjxqj.js} +1 -1
  69. package/dist/web/assets/keybindings-store-BplH-yiN.js +1 -0
  70. package/dist/web/assets/{line-DBLLF7lH.js → line--xyfYP3x.js} +1 -1
  71. package/dist/web/assets/{linear-BLFWatDe.js → linear-BdqW7iQu.js} +1 -1
  72. package/dist/web/assets/{markdown-renderer-BtPXdzTv.js → markdown-renderer-BCjJbGP8.js} +5 -5
  73. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-BY8JfkE_.js} +2 -2
  74. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-DIv-LMXG.js} +1 -1
  75. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-CIoJK3nc.js} +1 -1
  76. package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +1 -0
  77. package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +1 -0
  78. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-seSK40d1.js} +1 -1
  79. package/dist/web/assets/postgres-viewer-s0snZ9CL.js +1 -0
  80. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-BaRFqlsA.js} +1 -1
  81. package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +1 -0
  82. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-1WWjMQB_.js} +1 -1
  83. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-DEGGYsk7.js} +1 -1
  84. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BtRvoUTC.js} +1 -1
  85. package/dist/web/assets/{settings-store-Bbhg_ptG.js → settings-store-D3dJqGhB.js} +2 -2
  86. package/dist/web/assets/settings-tab-2YkgmrY0.js +1 -0
  87. package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +1 -0
  88. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C16aO8tn.js} +1 -1
  89. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +1 -0
  90. package/dist/web/assets/switch-mjGtIVDJ.js +1 -0
  91. package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DSz5PQI0.js} +1 -1
  92. package/dist/web/assets/{terminal-tab--Gw14HP3.js → terminal-tab-MRg8y1xF.js} +2 -2
  93. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DrjxCpEM.js} +1 -1
  94. package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +1 -0
  95. package/dist/web/assets/{use-monaco-theme-DHbyUrzJ.js → use-monaco-theme-BQzvItNE.js} +1 -1
  96. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-DfYFnniI.js} +1 -1
  97. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-BRvXOVlG.js} +1 -1
  98. package/dist/web/index.html +12 -10
  99. package/dist/web/sw.js +1 -1
  100. package/docs/code-standards.md +260 -7
  101. package/docs/codebase-summary.md +255 -95
  102. package/docs/project-changelog.md +88 -1
  103. package/docs/system-architecture.md +177 -12
  104. package/package.json +1 -1
  105. package/src/providers/claude-agent-sdk.ts +9 -0
  106. package/src/providers/cli-provider-base.ts +238 -0
  107. package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
  108. package/src/providers/cursor-cli/cursor-history.ts +207 -0
  109. package/src/providers/cursor-cli/cursor-provider.ts +146 -0
  110. package/src/providers/mock-provider.ts +1 -1
  111. package/src/providers/provider.interface.ts +1 -0
  112. package/src/providers/registry.ts +43 -4
  113. package/src/server/index.ts +8 -0
  114. package/src/server/routes/browser-preview.ts +89 -0
  115. package/src/server/routes/chat.ts +14 -3
  116. package/src/server/routes/settings.ts +14 -0
  117. package/src/server/ws/chat.ts +24 -8
  118. package/src/services/chat.service.ts +10 -15
  119. package/src/types/chat.ts +21 -2
  120. package/src/types/config.ts +33 -11
  121. package/src/utils/ndjson-line-parser.ts +36 -0
  122. package/src/web/components/browser/browser-tab.tsx +269 -0
  123. package/src/web/components/chat/chat-history-bar.tsx +49 -29
  124. package/src/web/components/chat/chat-tab.tsx +14 -2
  125. package/src/web/components/chat/message-input.tsx +91 -3
  126. package/src/web/components/chat/provider-selector.tsx +150 -0
  127. package/src/web/components/chat/session-picker.tsx +3 -1
  128. package/src/web/components/layout/command-palette.tsx +4 -0
  129. package/src/web/components/layout/editor-panel.tsx +1 -0
  130. package/src/web/components/layout/mobile-nav.tsx +2 -2
  131. package/src/web/components/layout/panel-layout.tsx +17 -1
  132. package/src/web/components/layout/tab-bar.tsx +2 -0
  133. package/src/web/components/layout/tab-content.tsx +5 -0
  134. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  135. package/src/web/hooks/use-chat.ts +11 -0
  136. package/src/web/hooks/use-global-keybindings.ts +7 -0
  137. package/src/web/hooks/use-voice-input.ts +111 -0
  138. package/src/web/stores/keybindings-store.ts +1 -0
  139. package/src/web/stores/panel-store.ts +10 -10
  140. package/src/web/stores/tab-store.ts +2 -1
  141. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  142. package/dist/web/assets/channel-w7yboq56.js +0 -1
  143. package/dist/web/assets/chat-tab-DmF14O6G.js +0 -7
  144. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  145. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  146. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  147. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  148. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  149. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  150. package/dist/web/assets/database-viewer-B27aRtdQ.js +0 -1
  151. package/dist/web/assets/git-graph-BGXo0o-J.js +0 -1
  152. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  153. package/dist/web/assets/index-BAioKo_2.css +0 -2
  154. package/dist/web/assets/index-CfClIVo2.js +0 -37
  155. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  156. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  157. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  158. package/dist/web/assets/keybindings-store-nDbczFnq.js +0 -1
  159. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  160. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  161. package/dist/web/assets/postgres-viewer-BMg-qFcO.js +0 -1
  162. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  163. package/dist/web/assets/settings-tab-NPuwQHzs.js +0 -1
  164. package/dist/web/assets/sqlite-viewer-CAsUczio.js +0 -1
  165. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  166. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  167. package/snapshot-state.md +0 -1526
  168. package/test-tokens.mjs +0 -212
  169. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
  170. /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
  171. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
  172. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
  173. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
  174. /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
  175. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
  176. /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
  177. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
  178. /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
  179. /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
  180. /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
  181. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
  182. /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
  183. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
  184. /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
  185. /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
  186. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
  187. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-Bslrbb-G.js} +0 -0
@@ -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
+
17
18
  import type { DragEvent } from "react";
18
19
  import type { FileNode } from "../../../types/project";
19
20
  import type { Session, SessionInfo } from "../../../types/chat";
@@ -89,6 +90,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
89
90
  pendingApproval,
90
91
  contextWindowPct,
91
92
  sessionTitle,
93
+ migratedSessionId,
92
94
  sendMessage,
93
95
  respondToApproval,
94
96
  cancelStreaming,
@@ -97,6 +99,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
97
99
  isConnected,
98
100
  } = useChat(sessionId, providerId, projectName);
99
101
 
102
+ // When CLI provider assigns a different session ID, update our state
103
+ useEffect(() => {
104
+ if (migratedSessionId && migratedSessionId !== sessionId) {
105
+ setSessionId(migratedSessionId);
106
+ }
107
+ }, [migratedSessionId]); // eslint-disable-line react-hooks/exhaustive-deps
108
+
100
109
  // Auto-clear notification badge when this tab is active and document is visible.
101
110
  // Handles the case where notification arrived while browser tab was hidden.
102
111
  useEffect(() => {
@@ -140,11 +149,11 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
140
149
  useTabStore.getState().openTab({
141
150
  type: "chat",
142
151
  title: "AI Chat",
143
- metadata: { projectName },
152
+ metadata: { projectName, providerId },
144
153
  projectId: projectName || null,
145
154
  closable: true,
146
155
  });
147
- }, [projectName]);
156
+ }, [projectName, providerId]);
148
157
 
149
158
  const handleSelectSession = useCallback((session: SessionInfo) => {
150
159
  setSessionId(session.id);
@@ -345,6 +354,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
345
354
  refreshUsage={refreshUsage}
346
355
  lastFetchedAt={lastFetchedAt}
347
356
  sessionId={sessionId}
357
+ providerId={providerId}
348
358
  onSelectSession={handleSelectSession}
349
359
  onBugReport={sessionId ? () => openBugReportPopup(version, { sessionId, projectName }) : undefined}
350
360
  isConnected={isConnected}
@@ -386,6 +396,8 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
386
396
  externalFiles={externalFiles}
387
397
  permissionMode={permissionMode}
388
398
  onModeChange={setPermissionMode}
399
+ providerId={providerId}
400
+ onProviderChange={!sessionId ? setProviderId : undefined}
389
401
  />
390
402
  </div>
391
403
 
@@ -1,10 +1,12 @@
1
1
  import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
2
- import { ArrowUp, Square, Paperclip, Loader2 } from "lucide-react";
2
+ import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff } from "lucide-react";
3
+ import { useVoiceInput } from "@/hooks/use-voice-input";
3
4
  import { api, projectUrl, getAuthToken } from "@/lib/api-client";
4
5
  import { randomId } from "@/lib/utils";
5
6
  import { isSupportedFile, isImageFile } from "@/lib/file-support";
6
7
  import { AttachmentChips } from "./attachment-chips";
7
8
  import { ModeSelector, getModeLabel, getModeIcon } from "./mode-selector";
9
+ import { ProviderSelector } from "./provider-selector";
8
10
  import type { SlashItem } from "./slash-command-picker";
9
11
  import type { FileNode } from "../../../types/project";
10
12
  import { flattenFileTree } from "./file-picker";
@@ -44,6 +46,10 @@ interface MessageInputProps {
44
46
  permissionMode?: string;
45
47
  /** Permission mode change handler */
46
48
  onModeChange?: (mode: string) => void;
49
+ /** Current provider ID */
50
+ providerId?: string;
51
+ /** Provider change handler — undefined when session is active (locked) */
52
+ onProviderChange?: (providerId: string) => void;
47
53
  }
48
54
 
49
55
  export const MessageInput = memo(function MessageInput({
@@ -63,6 +69,8 @@ export const MessageInput = memo(function MessageInput({
63
69
  autoFocus,
64
70
  permissionMode,
65
71
  onModeChange,
72
+ providerId,
73
+ onProviderChange,
66
74
  }: MessageInputProps) {
67
75
  const [value, setValue] = useState(initialValue ?? "");
68
76
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
@@ -74,6 +82,41 @@ export const MessageInput = memo(function MessageInput({
74
82
  const slashItemsRef = useRef<SlashItem[]>([]);
75
83
  const fileItemsRef = useRef<FileNode[]>([]);
76
84
 
85
+ // Voice input (Web Speech API)
86
+ const voice = useVoiceInput();
87
+ // Store pre-voice text so voice appends to existing input
88
+ const preVoiceTextRef = useRef("");
89
+ const voiceResultCb = useCallback((text: string) => {
90
+ const prefix = preVoiceTextRef.current;
91
+ const newValue = prefix ? prefix + " " + text : text;
92
+ setValue(newValue);
93
+ // Auto-resize textarea
94
+ requestAnimationFrame(() => {
95
+ const ta = window.matchMedia("(min-width: 768px)").matches
96
+ ? textareaRef.current
97
+ : mobileTextareaRef.current;
98
+ if (ta) {
99
+ ta.style.height = "auto";
100
+ ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
101
+ }
102
+ });
103
+ }, []);
104
+ const handleVoiceToggle = useCallback(() => {
105
+ if (voice.isListening) {
106
+ voice.stop();
107
+ } else {
108
+ preVoiceTextRef.current = value.trim();
109
+ voice.start(voiceResultCb);
110
+ }
111
+ }, [voice.isListening, voice.start, voice.stop, value, voiceResultCb]);
112
+
113
+ // Listen for global keyboard shortcut (Cmd+Shift+V) to toggle voice
114
+ useEffect(() => {
115
+ const handler = () => { if (voice.supported) handleVoiceToggle(); };
116
+ window.addEventListener("toggle-voice-input", handler);
117
+ return () => window.removeEventListener("toggle-voice-input", handler);
118
+ }, [voice.supported, handleVoiceToggle]);
119
+
77
120
  // Apply initialValue when it changes (e.g. "Ask AI" from command palette)
78
121
  useEffect(() => {
79
122
  if (initialValue) {
@@ -452,7 +495,7 @@ export const MessageInput = memo(function MessageInput({
452
495
  >
453
496
  {/* Attachment chips (inside container, aligned with input) */}
454
497
  <AttachmentChips attachments={attachments} onRemove={removeAttachment} />
455
- {/* Mobile: mode chip row */}
498
+ {/* Mobile: mode chip + provider selector row */}
456
499
  <div className="flex items-center gap-1 px-2 pt-2 md:hidden relative">
457
500
  <ModeChip
458
501
  mode={permissionMode ?? "bypassPermissions"}
@@ -464,8 +507,15 @@ export const MessageInput = memo(function MessageInput({
464
507
  open={modeSelectorOpen}
465
508
  onOpenChange={setModeSelectorOpen}
466
509
  />
510
+ {onProviderChange && projectName && (
511
+ <ProviderSelector
512
+ value={providerId ?? "claude"}
513
+ onChange={onProviderChange}
514
+ projectName={projectName}
515
+ />
516
+ )}
467
517
  </div>
468
- {/* Mobile: single row — attach + textarea + send */}
518
+ {/* Mobile: single row — attach + mic + textarea + send */}
469
519
  <div className="flex items-end gap-1 md:hidden px-2 py-2">
470
520
  <button
471
521
  type="button"
@@ -476,6 +526,21 @@ export const MessageInput = memo(function MessageInput({
476
526
  >
477
527
  <Paperclip className="size-4" />
478
528
  </button>
529
+ {voice.supported && (
530
+ <button
531
+ type="button"
532
+ onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
533
+ disabled={disabled}
534
+ className={`flex items-center justify-center size-7 shrink-0 rounded-full transition-colors disabled:opacity-50 ${
535
+ voice.isListening
536
+ ? "bg-red-600 text-white animate-pulse"
537
+ : "text-text-subtle hover:text-text-primary"
538
+ }`}
539
+ aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
540
+ >
541
+ {voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
542
+ </button>
543
+ )}
479
544
  <textarea
480
545
  ref={mobileTextareaRef}
481
546
  value={value}
@@ -535,6 +600,21 @@ export const MessageInput = memo(function MessageInput({
535
600
  >
536
601
  <Paperclip className="size-4" />
537
602
  </button>
603
+ {voice.supported && (
604
+ <button
605
+ type="button"
606
+ onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
607
+ disabled={disabled}
608
+ className={`flex items-center justify-center size-8 rounded-full transition-colors disabled:opacity-50 ${
609
+ voice.isListening
610
+ ? "bg-red-600 text-white animate-pulse"
611
+ : "text-text-subtle hover:text-text-primary hover:bg-surface-elevated"
612
+ }`}
613
+ aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
614
+ >
615
+ {voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
616
+ </button>
617
+ )}
538
618
  {/* Mode indicator chip */}
539
619
  <div className="relative">
540
620
  <ModeChip
@@ -548,6 +628,14 @@ export const MessageInput = memo(function MessageInput({
548
628
  onOpenChange={setModeSelectorOpen}
549
629
  />
550
630
  </div>
631
+ {/* Provider selector — only when no active session */}
632
+ {onProviderChange && projectName && (
633
+ <ProviderSelector
634
+ value={providerId ?? "claude"}
635
+ onChange={onProviderChange}
636
+ projectName={projectName}
637
+ />
638
+ )}
551
639
  </div>
552
640
  <div className="flex items-center gap-1">
553
641
  {showCancel ? (
@@ -0,0 +1,150 @@
1
+ import { useState, useEffect, useRef, useCallback, type KeyboardEvent } from "react";
2
+ import { Check } from "lucide-react";
3
+ import { api, projectUrl } from "@/lib/api-client";
4
+
5
+ interface ProviderInfo {
6
+ id: string;
7
+ name: string;
8
+ }
9
+
10
+ interface ProviderSelectorProps {
11
+ value: string;
12
+ onChange: (providerId: string) => void;
13
+ projectName: string;
14
+ }
15
+
16
+ const PROVIDER_ICONS: Record<string, string> = {
17
+ claude: "C",
18
+ cursor: "▶",
19
+ codex: "◆",
20
+ gemini: "G",
21
+ };
22
+
23
+ /**
24
+ * Provider selector chip + popup — matches ModeSelector style.
25
+ * Hidden when only 1 provider available.
26
+ */
27
+ export function ProviderSelector({ value, onChange, projectName }: ProviderSelectorProps) {
28
+ const [providers, setProviders] = useState<ProviderInfo[]>([]);
29
+ const [open, setOpen] = useState(false);
30
+ const panelRef = useRef<HTMLDivElement>(null);
31
+ const focusedRef = useRef(0);
32
+
33
+ useEffect(() => {
34
+ if (!projectName) return;
35
+ api.get<ProviderInfo[]>(`${projectUrl(projectName)}/chat/providers`)
36
+ .then(setProviders)
37
+ .catch(() => {});
38
+ }, [projectName]);
39
+
40
+ // Close on click outside
41
+ useEffect(() => {
42
+ if (!open) return;
43
+ const handler = (e: MouseEvent) => {
44
+ if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
45
+ setOpen(false);
46
+ }
47
+ };
48
+ document.addEventListener("mousedown", handler);
49
+ return () => document.removeEventListener("mousedown", handler);
50
+ }, [open]);
51
+
52
+ // Focus current on open
53
+ useEffect(() => {
54
+ if (open) {
55
+ focusedRef.current = Math.max(0, providers.findIndex((p) => p.id === value));
56
+ }
57
+ }, [open, value, providers]);
58
+
59
+ const handleKeyDown = useCallback((e: KeyboardEvent) => {
60
+ if (e.key === "Escape") { setOpen(false); return; }
61
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
62
+ e.preventDefault();
63
+ const dir = e.key === "ArrowDown" ? 1 : -1;
64
+ focusedRef.current = (focusedRef.current + dir + providers.length) % providers.length;
65
+ const el = panelRef.current?.querySelector(`[data-idx="${focusedRef.current}"]`) as HTMLElement;
66
+ el?.focus();
67
+ }
68
+ if (e.key === "Enter") {
69
+ e.preventDefault();
70
+ const p = providers[focusedRef.current];
71
+ if (p) { onChange(p.id); setOpen(false); }
72
+ }
73
+ }, [onChange, providers]);
74
+
75
+ // Hide when only 1 provider
76
+ if (providers.length <= 1) return null;
77
+
78
+ const current = providers.find((p) => p.id === value);
79
+ const icon = PROVIDER_ICONS[value] || "?";
80
+
81
+ return (
82
+ <div className="relative">
83
+ {/* Chip — same style as ModeChip */}
84
+ <button
85
+ type="button"
86
+ onClick={(e) => { e.stopPropagation(); setOpen((v) => !v); }}
87
+ className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-[11px] text-text-subtle hover:text-text-primary hover:bg-surface-elevated transition-colors border border-transparent hover:border-border"
88
+ aria-label={`AI Provider: ${current?.name ?? value}`}
89
+ >
90
+ <span className="inline-flex h-3.5 w-3.5 items-center justify-center rounded text-[9px] font-bold bg-surface-elevated shrink-0">
91
+ {icon}
92
+ </span>
93
+ <span className="max-w-[80px] truncate capitalize">{current?.name ?? value}</span>
94
+ </button>
95
+
96
+ {/* Popup panel — same style as ModeSelector */}
97
+ {open && (
98
+ <div
99
+ ref={panelRef}
100
+ role="listbox"
101
+ aria-label="AI Providers"
102
+ onKeyDown={handleKeyDown}
103
+ onMouseDown={(e) => e.stopPropagation()}
104
+ onClick={(e) => e.stopPropagation()}
105
+ className="absolute bottom-full left-0 mb-1 z-50 w-56 rounded-lg border border-border bg-surface shadow-lg"
106
+ >
107
+ <div className="px-3 py-2 border-b border-border">
108
+ <span className="text-xs font-medium text-text-secondary">Provider</span>
109
+ </div>
110
+ <div className="py-1">
111
+ {providers.map((p, idx) => {
112
+ const pIcon = PROVIDER_ICONS[p.id] || "?";
113
+ const isActive = p.id === value;
114
+ return (
115
+ <button
116
+ key={p.id}
117
+ data-idx={idx}
118
+ role="option"
119
+ aria-selected={isActive}
120
+ tabIndex={0}
121
+ onClick={() => { onChange(p.id); setOpen(false); }}
122
+ className={`w-full flex items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-surface-elevated focus:bg-surface-elevated focus:outline-none ${isActive ? "bg-surface-elevated" : ""}`}
123
+ >
124
+ <span className="inline-flex h-5 w-5 items-center justify-center rounded text-[11px] font-bold bg-surface-elevated text-text-subtle shrink-0">
125
+ {pIcon}
126
+ </span>
127
+ <span className="flex-1 text-sm font-medium text-text-primary capitalize">{p.name}</span>
128
+ {isActive && <Check className="size-4 shrink-0 text-primary" />}
129
+ </button>
130
+ );
131
+ })}
132
+ </div>
133
+ </div>
134
+ )}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ /** Small provider badge for session lists */
140
+ export function ProviderBadge({ providerId }: { providerId: string }) {
141
+ const icon = PROVIDER_ICONS[providerId] || "?";
142
+ return (
143
+ <span
144
+ className="inline-flex h-4 w-4 items-center justify-center rounded text-[10px] font-bold bg-surface-elevated text-text-subtle shrink-0"
145
+ title={providerId}
146
+ >
147
+ {icon}
148
+ </span>
149
+ );
150
+ }
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
2
  import { api, projectUrl } from "@/lib/api-client";
3
3
  import { Plus, Trash2, MessageSquare, ChevronDown } from "lucide-react";
4
+ import { ProviderBadge } from "./provider-selector";
4
5
  import type { SessionInfo } from "../../../types/chat";
5
6
 
6
7
  interface SessionPickerProps {
@@ -114,7 +115,8 @@ export function SessionPicker({
114
115
  }`}
115
116
  >
116
117
  <div className="flex flex-col min-w-0 flex-1">
117
- <span className="truncate text-xs font-medium">
118
+ <span className="flex items-center gap-1.5 truncate text-xs font-medium">
119
+ <ProviderBadge providerId={session.providerId} />
118
120
  {session.title}
119
121
  </span>
120
122
  <span className="text-xs text-text-subtle">
@@ -10,6 +10,8 @@ import {
10
10
  FileCode,
11
11
  FolderOpen,
12
12
  Loader2,
13
+ Globe,
14
+ Mic,
13
15
  } from "lucide-react";
14
16
  import { useTabStore, type TabType } from "@/stores/tab-store";
15
17
  import { useProjectStore } from "@/stores/project-store";
@@ -156,7 +158,9 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
156
158
  { id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
157
159
  { id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
158
160
  { 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" },
159
162
  { 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")) },
160
164
  { id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action", shortcut: formatShortcut(getBinding("open-git-status")) },
161
165
  {
162
166
  id: "settings", label: "Settings", icon: Settings,
@@ -23,6 +23,7 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
23
23
  "git-graph": lazy(() => import("@/components/git/git-graph").then((m) => ({ default: m.GitGraph }))),
24
24
  "git-diff": lazy(() => import("@/components/editor/diff-viewer").then((m) => ({ default: m.DiffViewer }))),
25
25
  settings: lazy(() => import("@/components/settings/settings-tab").then((m) => ({ default: m.SettingsTab }))),
26
+ browser: lazy(() => import("@/components/browser/browser-tab").then((m) => ({ default: m.BrowserTab }))),
26
27
  };
27
28
 
28
29
  interface EditorPanelProps {
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
2
2
  import {
3
3
  Terminal, MessageSquare, GitBranch, Database,
4
4
  FileDiff, FileCode, Settings, Menu, X, ArrowLeft, ArrowRight, SplitSquareVertical, MoveVertical, Layers, Plus,
5
- ChevronLeft, ChevronRight,
5
+ ChevronLeft, ChevronRight, Globe,
6
6
  } from "lucide-react";
7
7
  import { usePanelStore } from "@/stores/panel-store";
8
8
  import { useProjectStore, resolveOrder } from "@/stores/project-store";
@@ -25,7 +25,7 @@ const NEW_TAB_LABELS: Partial<Record<TabType, string>> = Object.fromEntries(NEW_
25
25
 
26
26
  const TAB_ICONS: Record<TabType, React.ElementType> = {
27
27
  terminal: Terminal, chat: MessageSquare, editor: FileCode, database: Database, sqlite: Database, postgres: Database,
28
- "git-graph": GitBranch, "git-diff": FileDiff, settings: Settings,
28
+ "git-graph": GitBranch, "git-diff": FileDiff, settings: Settings, browser: Globe,
29
29
  };
30
30
 
31
31
  interface MobileNavProps { onMenuPress: () => void; onProjectsPress: () => void; }
@@ -1,6 +1,8 @@
1
+ import { useEffect } from "react";
1
2
  import { Panel, Group, Separator } from "react-resizable-panels";
2
3
  import { GripVertical, GripHorizontal } from "lucide-react";
3
4
  import { usePanelStore } from "@/stores/panel-store";
5
+ import { createPanel } from "@/stores/panel-utils";
4
6
  import { EditorPanel } from "./editor-panel";
5
7
 
6
8
  interface PanelLayoutProps {
@@ -13,7 +15,21 @@ export function PanelLayout({ projectName }: PanelLayoutProps) {
13
15
  );
14
16
  const panelCount = grid.flat().length;
15
17
 
16
- if (panelCount <= 1 && grid[0]?.[0]) {
18
+ // Recover from empty grid (corrupt persisted state or edge-case bug)
19
+ useEffect(() => {
20
+ if (panelCount === 0) {
21
+ const p = createPanel();
22
+ usePanelStore.setState((s) => ({
23
+ panels: { ...s.panels, [p.id]: p },
24
+ grid: [[p.id]],
25
+ focusedPanelId: p.id,
26
+ }));
27
+ }
28
+ }, [panelCount]);
29
+
30
+ if (panelCount === 0) return null;
31
+
32
+ if (panelCount === 1 && grid[0]?.[0]) {
17
33
  return <EditorPanel panelId={grid[0][0]} projectName={projectName} />;
18
34
  }
19
35
 
@@ -10,6 +10,7 @@ import {
10
10
  Database,
11
11
  ChevronLeft,
12
12
  ChevronRight,
13
+ Globe,
13
14
  } from "lucide-react";
14
15
  import { useTabStore, type TabType } from "@/stores/tab-store";
15
16
  import { usePanelStore } from "@/stores/panel-store";
@@ -33,6 +34,7 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
33
34
  "git-graph": GitBranch,
34
35
  "git-diff": FileDiff,
35
36
  settings: Settings,
37
+ browser: Globe,
36
38
  };
37
39
 
38
40
  interface TabBarProps {
@@ -48,6 +48,11 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
48
48
  default: m.SettingsTab,
49
49
  })),
50
50
  ),
51
+ browser: lazy(() =>
52
+ import("@/components/browser/browser-tab").then((m) => ({
53
+ default: m.BrowserTab,
54
+ })),
55
+ ),
51
56
  };
52
57
 
53
58
  function LoadingFallback() {