@hienlh/ppm 0.9.0-beta.3 → 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 (186) hide show
  1. package/CHANGELOG.md +9 -44
  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-D3VJc1tY.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-D5vGZJnH.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-DcGMlbRm.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-DHMITI3S.js → terminal-tab-MRg8y1xF.js} +1 -1
  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 +85 -212
  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 +91 -106
  118. package/src/services/chat.service.ts +10 -15
  119. package/src/types/api.ts +1 -1
  120. package/src/types/chat.ts +21 -4
  121. package/src/types/config.ts +33 -11
  122. package/src/utils/ndjson-line-parser.ts +36 -0
  123. package/src/web/components/browser/browser-tab.tsx +269 -0
  124. package/src/web/components/chat/chat-history-bar.tsx +49 -29
  125. package/src/web/components/chat/chat-tab.tsx +17 -5
  126. package/src/web/components/chat/message-input.tsx +94 -43
  127. package/src/web/components/chat/provider-selector.tsx +150 -0
  128. package/src/web/components/chat/session-picker.tsx +3 -1
  129. package/src/web/components/layout/command-palette.tsx +4 -0
  130. package/src/web/components/layout/editor-panel.tsx +1 -0
  131. package/src/web/components/layout/mobile-nav.tsx +2 -2
  132. package/src/web/components/layout/panel-layout.tsx +17 -1
  133. package/src/web/components/layout/tab-bar.tsx +2 -0
  134. package/src/web/components/layout/tab-content.tsx +5 -0
  135. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  136. package/src/web/hooks/use-chat.ts +20 -21
  137. package/src/web/hooks/use-global-keybindings.ts +7 -0
  138. package/src/web/hooks/use-voice-input.ts +111 -0
  139. package/src/web/stores/keybindings-store.ts +1 -0
  140. package/src/web/stores/panel-store.ts +10 -10
  141. package/src/web/stores/tab-store.ts +2 -1
  142. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  143. package/dist/web/assets/channel-w7yboq56.js +0 -1
  144. package/dist/web/assets/chat-tab-DxkvWelV.js +0 -7
  145. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  146. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  147. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  148. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  149. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  150. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  151. package/dist/web/assets/database-viewer-qlwORhh0.js +0 -1
  152. package/dist/web/assets/git-graph-B2fHtKEc.js +0 -1
  153. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  154. package/dist/web/assets/index-BAioKo_2.css +0 -2
  155. package/dist/web/assets/index-Ccq6zi2E.js +0 -37
  156. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  157. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  158. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  159. package/dist/web/assets/keybindings-store-e3pqlQbf.js +0 -1
  160. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  161. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  162. package/dist/web/assets/postgres-viewer-CZzbMFtb.js +0 -1
  163. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  164. package/dist/web/assets/settings-tab-BOmLAhkD.js +0 -1
  165. package/dist/web/assets/sqlite-viewer-CrrzHXqq.js +0 -1
  166. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  167. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  168. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
  169. /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
  170. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
  171. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
  172. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
  173. /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
  174. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
  175. /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
  176. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
  177. /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
  178. /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
  179. /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
  180. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
  181. /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
  182. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
  183. /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
  184. /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
  185. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
  186. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-Bslrbb-G.js} +0 -0
@@ -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 } 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";
@@ -20,10 +22,8 @@ export interface ChatAttachment {
20
22
  status: "uploading" | "ready" | "error";
21
23
  }
22
24
 
23
- export type MessagePriority = 'now' | 'next' | 'later';
24
-
25
25
  interface MessageInputProps {
26
- onSend: (content: string, attachments: ChatAttachment[], priority?: MessagePriority) => void;
26
+ onSend: (content: string, attachments: ChatAttachment[]) => void;
27
27
  isStreaming?: boolean;
28
28
  onCancel?: () => void;
29
29
  disabled?: boolean;
@@ -46,6 +46,10 @@ interface MessageInputProps {
46
46
  permissionMode?: string;
47
47
  /** Permission mode change handler */
48
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;
49
53
  }
50
54
 
51
55
  export const MessageInput = memo(function MessageInput({
@@ -65,18 +69,54 @@ export const MessageInput = memo(function MessageInput({
65
69
  autoFocus,
66
70
  permissionMode,
67
71
  onModeChange,
72
+ providerId,
73
+ onProviderChange,
68
74
  }: MessageInputProps) {
69
75
  const [value, setValue] = useState(initialValue ?? "");
70
76
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
71
77
  const [modeSelectorOpen, setModeSelectorOpen] = useState(false);
72
78
  const [pendingSend, setPendingSend] = useState(false);
73
- const [priority, setPriority] = useState<MessagePriority>('next');
74
79
  const textareaRef = useRef<HTMLTextAreaElement>(null);
75
80
  const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
76
81
  const fileInputRef = useRef<HTMLInputElement>(null);
77
82
  const slashItemsRef = useRef<SlashItem[]>([]);
78
83
  const fileItemsRef = useRef<FileNode[]>([]);
79
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
+
80
120
  // Apply initialValue when it changes (e.g. "Ask AI" from command palette)
81
121
  useEffect(() => {
82
122
  if (initialValue) {
@@ -289,7 +329,7 @@ export const MessageInput = memo(function MessageInput({
289
329
 
290
330
  onSlashStateChange?.(false, "");
291
331
  onFileStateChange?.(false, "");
292
- onSend(trimmed, readyAttachments, isStreaming ? priority : undefined);
332
+ onSend(trimmed, readyAttachments);
293
333
  setValue("");
294
334
  // Revoke preview URLs
295
335
  for (const att of attachments) {
@@ -297,10 +337,9 @@ export const MessageInput = memo(function MessageInput({
297
337
  }
298
338
  setAttachments([]);
299
339
  setPendingSend(false);
300
- setPriority('next');
301
340
  if (textareaRef.current) textareaRef.current.style.height = "auto";
302
341
  if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
303
- }, [value, attachments, onSend, onSlashStateChange, onFileStateChange, isStreaming, priority]);
342
+ }, [value, attachments, onSend, onSlashStateChange, onFileStateChange]);
304
343
 
305
344
  const handleSend = useCallback(() => {
306
345
  if (disabled) return;
@@ -456,7 +495,7 @@ export const MessageInput = memo(function MessageInput({
456
495
  >
457
496
  {/* Attachment chips (inside container, aligned with input) */}
458
497
  <AttachmentChips attachments={attachments} onRemove={removeAttachment} />
459
- {/* Mobile: mode chip row */}
498
+ {/* Mobile: mode chip + provider selector row */}
460
499
  <div className="flex items-center gap-1 px-2 pt-2 md:hidden relative">
461
500
  <ModeChip
462
501
  mode={permissionMode ?? "bypassPermissions"}
@@ -468,9 +507,15 @@ export const MessageInput = memo(function MessageInput({
468
507
  open={modeSelectorOpen}
469
508
  onOpenChange={setModeSelectorOpen}
470
509
  />
471
- {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
510
+ {onProviderChange && projectName && (
511
+ <ProviderSelector
512
+ value={providerId ?? "claude"}
513
+ onChange={onProviderChange}
514
+ projectName={projectName}
515
+ />
516
+ )}
472
517
  </div>
473
- {/* Mobile: single row — attach + textarea + send */}
518
+ {/* Mobile: single row — attach + mic + textarea + send */}
474
519
  <div className="flex items-end gap-1 md:hidden px-2 py-2">
475
520
  <button
476
521
  type="button"
@@ -481,6 +526,21 @@ export const MessageInput = memo(function MessageInput({
481
526
  >
482
527
  <Paperclip className="size-4" />
483
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
+ )}
484
544
  <textarea
485
545
  ref={mobileTextareaRef}
486
546
  value={value}
@@ -540,6 +600,21 @@ export const MessageInput = memo(function MessageInput({
540
600
  >
541
601
  <Paperclip className="size-4" />
542
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
+ )}
543
618
  {/* Mode indicator chip */}
544
619
  <div className="relative">
545
620
  <ModeChip
@@ -553,7 +628,14 @@ export const MessageInput = memo(function MessageInput({
553
628
  onOpenChange={setModeSelectorOpen}
554
629
  />
555
630
  </div>
556
- {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
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
+ )}
557
639
  </div>
558
640
  <div className="flex items-center gap-1">
559
641
  {showCancel ? (
@@ -600,34 +682,3 @@ function ModeChip({ mode, onClick }: { mode: string; onClick: () => void }) {
600
682
  </button>
601
683
  );
602
684
  }
603
-
604
- const PRIORITY_OPTIONS: { value: MessagePriority; label: string; Icon: typeof Zap }[] = [
605
- { value: 'now', label: 'Interrupt', Icon: Zap },
606
- { value: 'next', label: 'Queue', Icon: ListOrdered },
607
- { value: 'later', label: 'Later', Icon: Clock },
608
- ];
609
-
610
- /** Compact priority toggle — visible only during streaming */
611
- function PriorityToggle({ value, onChange }: { value: MessagePriority; onChange: (v: MessagePriority) => void }) {
612
- const cycle = useCallback(() => {
613
- const order: MessagePriority[] = ['next', 'later', 'now'];
614
- const idx = order.indexOf(value);
615
- onChange(order[(idx + 1) % order.length]!);
616
- }, [value, onChange]);
617
-
618
- const current = PRIORITY_OPTIONS.find((o) => o.value === value) ?? PRIORITY_OPTIONS[1]!;
619
- const Icon = current.Icon;
620
-
621
- return (
622
- <button
623
- type="button"
624
- onClick={(e) => { e.stopPropagation(); cycle(); }}
625
- 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"
626
- aria-label={`Message priority: ${current.label}`}
627
- title={`Priority: ${current.label} (click to cycle)`}
628
- >
629
- <Icon className="size-3" />
630
- <span>{current.label}</span>
631
- </button>
632
- );
633
- }
@@ -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() {