@hienlh/ppm 0.9.0-beta.3 → 0.9.0-beta.5

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 (185) hide show
  1. package/CHANGELOG.md +7 -29
  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-CEYAwJUI.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-C8bt4tdL.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-D3VJc1tY.js → code-editor-BVt8uVtX.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-CNi9B6tN.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-D5vGZJnH.js → diff-viewer-CnjvJhwy.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-BNbraUOe.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-CKvJPAPG.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-Cmf2viOa.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-DcGMlbRm.js → markdown-renderer-CFW_zoSR.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-Dg23WW__.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-rB971cXs.js +1 -0
  87. package/dist/web/assets/sqlite-viewer-D3N938wy.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-DHMITI3S.js → terminal-tab-BdHlIi8e.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 +18 -3
  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-DxkvWelV.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-qlwORhh0.js +0 -1
  151. package/dist/web/assets/git-graph-B2fHtKEc.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-Ccq6zi2E.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-e3pqlQbf.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-CZzbMFtb.js +0 -1
  162. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  163. package/dist/web/assets/settings-tab-BOmLAhkD.js +0 -1
  164. package/dist/web/assets/sqlite-viewer-CrrzHXqq.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/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
  168. /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
  169. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
  170. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
  171. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
  172. /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
  173. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
  174. /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
  175. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
  176. /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
  177. /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
  178. /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
  179. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
  180. /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
  181. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
  182. /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
  183. /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
  184. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
  185. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-Bslrbb-G.js} +0 -0
@@ -14,6 +14,7 @@ import { MessageInput, type ChatAttachment, type MessagePriority } from "./messa
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, Zap, ListOrdered, Clock } from "lucide-react";
2
+ import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff, Zap, ListOrdered, Clock } 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";
@@ -46,6 +48,10 @@ interface MessageInputProps {
46
48
  permissionMode?: string;
47
49
  /** Permission mode change handler */
48
50
  onModeChange?: (mode: string) => void;
51
+ /** Current provider ID */
52
+ providerId?: string;
53
+ /** Provider change handler — undefined when session is active (locked) */
54
+ onProviderChange?: (providerId: string) => void;
49
55
  }
50
56
 
51
57
  export const MessageInput = memo(function MessageInput({
@@ -65,6 +71,8 @@ export const MessageInput = memo(function MessageInput({
65
71
  autoFocus,
66
72
  permissionMode,
67
73
  onModeChange,
74
+ providerId,
75
+ onProviderChange,
68
76
  }: MessageInputProps) {
69
77
  const [value, setValue] = useState(initialValue ?? "");
70
78
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
@@ -77,6 +85,41 @@ export const MessageInput = memo(function MessageInput({
77
85
  const slashItemsRef = useRef<SlashItem[]>([]);
78
86
  const fileItemsRef = useRef<FileNode[]>([]);
79
87
 
88
+ // Voice input (Web Speech API)
89
+ const voice = useVoiceInput();
90
+ // Store pre-voice text so voice appends to existing input
91
+ const preVoiceTextRef = useRef("");
92
+ const voiceResultCb = useCallback((text: string) => {
93
+ const prefix = preVoiceTextRef.current;
94
+ const newValue = prefix ? prefix + " " + text : text;
95
+ setValue(newValue);
96
+ // Auto-resize textarea
97
+ requestAnimationFrame(() => {
98
+ const ta = window.matchMedia("(min-width: 768px)").matches
99
+ ? textareaRef.current
100
+ : mobileTextareaRef.current;
101
+ if (ta) {
102
+ ta.style.height = "auto";
103
+ ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
104
+ }
105
+ });
106
+ }, []);
107
+ const handleVoiceToggle = useCallback(() => {
108
+ if (voice.isListening) {
109
+ voice.stop();
110
+ } else {
111
+ preVoiceTextRef.current = value.trim();
112
+ voice.start(voiceResultCb);
113
+ }
114
+ }, [voice.isListening, voice.start, voice.stop, value, voiceResultCb]);
115
+
116
+ // Listen for global keyboard shortcut (Cmd+Shift+V) to toggle voice
117
+ useEffect(() => {
118
+ const handler = () => { if (voice.supported) handleVoiceToggle(); };
119
+ window.addEventListener("toggle-voice-input", handler);
120
+ return () => window.removeEventListener("toggle-voice-input", handler);
121
+ }, [voice.supported, handleVoiceToggle]);
122
+
80
123
  // Apply initialValue when it changes (e.g. "Ask AI" from command palette)
81
124
  useEffect(() => {
82
125
  if (initialValue) {
@@ -456,7 +499,7 @@ export const MessageInput = memo(function MessageInput({
456
499
  >
457
500
  {/* Attachment chips (inside container, aligned with input) */}
458
501
  <AttachmentChips attachments={attachments} onRemove={removeAttachment} />
459
- {/* Mobile: mode chip row */}
502
+ {/* Mobile: mode chip + provider selector row */}
460
503
  <div className="flex items-center gap-1 px-2 pt-2 md:hidden relative">
461
504
  <ModeChip
462
505
  mode={permissionMode ?? "bypassPermissions"}
@@ -468,9 +511,16 @@ export const MessageInput = memo(function MessageInput({
468
511
  open={modeSelectorOpen}
469
512
  onOpenChange={setModeSelectorOpen}
470
513
  />
514
+ {onProviderChange && projectName && (
515
+ <ProviderSelector
516
+ value={providerId ?? "claude"}
517
+ onChange={onProviderChange}
518
+ projectName={projectName}
519
+ />
520
+ )}
471
521
  {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
472
522
  </div>
473
- {/* Mobile: single row — attach + textarea + send */}
523
+ {/* Mobile: single row — attach + mic + textarea + send */}
474
524
  <div className="flex items-end gap-1 md:hidden px-2 py-2">
475
525
  <button
476
526
  type="button"
@@ -481,6 +531,21 @@ export const MessageInput = memo(function MessageInput({
481
531
  >
482
532
  <Paperclip className="size-4" />
483
533
  </button>
534
+ {voice.supported && (
535
+ <button
536
+ type="button"
537
+ onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
538
+ disabled={disabled}
539
+ className={`flex items-center justify-center size-7 shrink-0 rounded-full transition-colors disabled:opacity-50 ${
540
+ voice.isListening
541
+ ? "bg-red-600 text-white animate-pulse"
542
+ : "text-text-subtle hover:text-text-primary"
543
+ }`}
544
+ aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
545
+ >
546
+ {voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
547
+ </button>
548
+ )}
484
549
  <textarea
485
550
  ref={mobileTextareaRef}
486
551
  value={value}
@@ -540,6 +605,21 @@ export const MessageInput = memo(function MessageInput({
540
605
  >
541
606
  <Paperclip className="size-4" />
542
607
  </button>
608
+ {voice.supported && (
609
+ <button
610
+ type="button"
611
+ onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
612
+ disabled={disabled}
613
+ className={`flex items-center justify-center size-8 rounded-full transition-colors disabled:opacity-50 ${
614
+ voice.isListening
615
+ ? "bg-red-600 text-white animate-pulse"
616
+ : "text-text-subtle hover:text-text-primary hover:bg-surface-elevated"
617
+ }`}
618
+ aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
619
+ >
620
+ {voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
621
+ </button>
622
+ )}
543
623
  {/* Mode indicator chip */}
544
624
  <div className="relative">
545
625
  <ModeChip
@@ -553,6 +633,14 @@ export const MessageInput = memo(function MessageInput({
553
633
  onOpenChange={setModeSelectorOpen}
554
634
  />
555
635
  </div>
636
+ {/* Provider selector — only when no active session */}
637
+ {onProviderChange && projectName && (
638
+ <ProviderSelector
639
+ value={providerId ?? "claude"}
640
+ onChange={onProviderChange}
641
+ projectName={projectName}
642
+ />
643
+ )}
556
644
  {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
557
645
  </div>
558
646
  <div className="flex items-center gap-1">
@@ -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() {