@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
@@ -7,7 +7,6 @@ import type {
7
7
  ChatMessage,
8
8
  SendMessageOpts,
9
9
  } from "../providers/provider.interface.ts";
10
- import { MockProvider } from "../providers/mock-provider.ts";
11
10
 
12
11
  class ChatService {
13
12
  async createSession(
@@ -34,19 +33,18 @@ class ChatService {
34
33
  if (providerId) {
35
34
  const provider = providerRegistry.get(providerId);
36
35
  if (!provider) throw new Error(`Provider "${providerId}" not found`);
37
- // Pass dir to providers that support it (SDK provider)
38
- if (dir && "listSessionsByDir" in provider) {
39
- return (provider as any).listSessionsByDir(dir);
36
+ if (dir && provider.listSessionsByDir) {
37
+ return provider.listSessionsByDir(dir);
40
38
  }
41
39
  return provider.listSessions();
42
40
  }
43
41
  // Aggregate from all providers
44
42
  const all: SessionInfo[] = [];
45
- for (const info of providerRegistry.list()) {
43
+ for (const info of providerRegistry.listAll()) {
46
44
  const provider = providerRegistry.get(info.id);
47
45
  if (provider) {
48
- if (dir && "listSessionsByDir" in provider) {
49
- all.push(...await (provider as any).listSessionsByDir(dir));
46
+ if (dir && provider.listSessionsByDir) {
47
+ all.push(...await provider.listSessionsByDir(dir));
50
48
  } else {
51
49
  all.push(...await provider.listSessions());
52
50
  }
@@ -83,13 +81,13 @@ class ChatService {
83
81
 
84
82
  /** Look up a session across all providers (for WS handler) */
85
83
  getSession(sessionId: string): Session | null {
86
- for (const info of providerRegistry.list()) {
84
+ for (const info of providerRegistry.listAll()) {
87
85
  const provider = providerRegistry.get(info.id);
88
86
  if (!provider) continue;
89
- const sessions = (provider as any).sessions as Map<string, unknown> | undefined;
90
- if (sessions?.has(sessionId)) {
87
+ // Use internal sessions Map SDK stores {meta, sdk}, others store Session directly
88
+ const sessions = (provider as any).sessions ?? (provider as any).activeSessions;
89
+ if (sessions instanceof Map && sessions.has(sessionId)) {
91
90
  const entry = sessions.get(sessionId);
92
- // SDK provider stores {meta, sdk}, others store Session directly
93
91
  if (entry && typeof entry === "object" && "meta" in entry) {
94
92
  return (entry as { meta: Session }).meta;
95
93
  }
@@ -102,10 +100,7 @@ class ChatService {
102
100
  async getMessages(providerId: string, sessionId: string): Promise<ChatMessage[]> {
103
101
  const provider = providerRegistry.get(providerId);
104
102
  if (!provider) return [];
105
- if ("getMessages" in provider && typeof (provider as any).getMessages === "function") {
106
- return await (provider as any).getMessages(sessionId);
107
- }
108
- return [];
103
+ return await provider.getMessages?.(sessionId) ?? [];
109
104
  }
110
105
  }
111
106
 
package/src/types/chat.ts CHANGED
@@ -5,18 +5,35 @@ export interface SendMessageOpts {
5
5
  export interface AIProvider {
6
6
  id: string;
7
7
  name: string;
8
+
9
+ // Session lifecycle (required)
8
10
  createSession(config: SessionConfig): Promise<Session>;
9
11
  resumeSession(sessionId: string): Promise<Session>;
10
12
  listSessions(): Promise<SessionInfo[]>;
11
13
  deleteSession(sessionId: string): Promise<void>;
14
+
15
+ // Streaming (required)
12
16
  sendMessage(
13
17
  sessionId: string,
14
18
  message: string,
15
19
  opts?: SendMessageOpts,
16
20
  ): AsyncIterable<ChatEvent>;
17
- /** Resolve a pending tool/question approval by requestId */
21
+
22
+ // Optional capabilities — providers implement what they support
18
23
  resolveApproval?(requestId: string, approved: boolean, data?: unknown): void;
19
24
  onToolApproval?: (callback: ToolApprovalHandler) => void;
25
+ abortQuery?(sessionId: string): void;
26
+ getMessages?(sessionId: string): Promise<ChatMessage[]>;
27
+ listSessionsByDir?(dir: string): Promise<SessionInfo[]>;
28
+ ensureProjectPath?(sessionId: string, path: string): void;
29
+ setForkSource?(sessionId: string, sourceSessionId: string): void;
30
+ isAvailable?(): Promise<boolean>;
31
+ listModels?(): Promise<ModelOption[]>;
32
+ }
33
+
34
+ export interface ModelOption {
35
+ value: string;
36
+ label: string;
20
37
  }
21
38
 
22
39
  export interface Session {
@@ -90,7 +107,9 @@ export type ChatEvent =
90
107
  | { type: "approval_request"; requestId: string; tool: string; input: unknown }
91
108
  | { type: "error"; message: string }
92
109
  | { type: "done"; sessionId: string; resultSubtype?: ResultSubtype; numTurns?: number; contextWindowPct?: number }
93
- | { type: "account_info"; accountId: string; accountLabel: string };
110
+ | { type: "session_migrated"; oldSessionId: string; newSessionId: string }
111
+ | { type: "account_info"; accountId: string; accountLabel: string }
112
+ | { type: "system"; subtype: string };
94
113
 
95
114
  export type ToolApprovalHandler = (
96
115
  tool: string,
@@ -44,18 +44,24 @@ const VALID_PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermiss
44
44
  export type PermissionMode = typeof VALID_PERMISSION_MODES[number];
45
45
 
46
46
  export interface AIProviderConfig {
47
- type: "agent-sdk" | "mock";
47
+ type: "agent-sdk" | "cli" | "mock";
48
+
49
+ // Common fields (all providers)
50
+ permission_mode?: PermissionMode;
51
+ system_prompt?: string;
52
+ model?: string;
53
+
54
+ // SDK-specific (Claude)
48
55
  api_key_env?: string;
49
56
  api_key?: string;
50
57
  base_url?: string;
51
- // Agent SDK-specific settings (ignored by mock provider)
52
- model?: string;
53
58
  effort?: "low" | "medium" | "high" | "max";
54
59
  max_turns?: number;
55
60
  max_budget_usd?: number;
56
61
  thinking_budget_tokens?: number;
57
- permission_mode?: PermissionMode;
58
- system_prompt?: string;
62
+
63
+ // CLI-specific (Cursor, Codex, Gemini)
64
+ cli_command?: string;
59
65
  }
60
66
 
61
67
  export const DEFAULT_CONFIG: PpmConfig = {
@@ -80,11 +86,13 @@ export const DEFAULT_CONFIG: PpmConfig = {
80
86
  },
81
87
  };
82
88
 
83
- const VALID_TYPES = ["agent-sdk", "mock"] as const;
89
+ const VALID_TYPES = ["agent-sdk", "cli", "mock"] as const;
84
90
  const VALID_EFFORTS = ["low", "medium", "high"] as const;
85
91
  const VALID_MODELS = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"] as const;
92
+ /** Allowed CLI commands for CLI providers (prevents command injection) */
93
+ const VALID_CLI_COMMANDS = ["cursor-agent", "codex", "gemini"] as const;
86
94
  /** Only these values are allowed for default_provider in config */
87
- export const VALID_PROVIDERS = ["claude"] as const;
95
+ export const VALID_PROVIDERS = ["claude", "cursor"] as const;
88
96
  const VALID_THEMES: ThemeConfig[] = ["light", "dark", "system"];
89
97
 
90
98
  /** Validate AI provider config fields. Returns array of error messages (empty = valid). */
@@ -93,9 +101,22 @@ export function validateAIProviderConfig(config: Partial<AIProviderConfig>): str
93
101
  if (config.type != null && !VALID_TYPES.includes(config.type as any)) {
94
102
  errors.push(`type must be one of: ${VALID_TYPES.join(", ")}`);
95
103
  }
96
- if (config.model != null && !VALID_MODELS.includes(config.model as any)) {
97
- errors.push(`model must be one of: ${VALID_MODELS.join(", ")}`);
104
+
105
+ // CLI-specific validation
106
+ if (config.type === "cli") {
107
+ if (!config.cli_command) {
108
+ errors.push("cli_command is required for CLI providers");
109
+ } else if (!VALID_CLI_COMMANDS.includes(config.cli_command as any)) {
110
+ errors.push(`cli_command must be one of: ${VALID_CLI_COMMANDS.join(", ")}`);
111
+ }
112
+ // CLI providers accept any model string — skip VALID_MODELS check
113
+ } else {
114
+ // SDK/mock model validation
115
+ if (config.model != null && !VALID_MODELS.includes(config.model as any)) {
116
+ errors.push(`model must be one of: ${VALID_MODELS.join(", ")}`);
117
+ }
98
118
  }
119
+
99
120
  if (config.effort && !VALID_EFFORTS.includes(config.effort as any)) {
100
121
  errors.push(`effort must be one of: ${VALID_EFFORTS.join(", ")}`);
101
122
  }
@@ -149,8 +170,9 @@ export function sanitizeConfig(config: PpmConfig): boolean {
149
170
  dirty = true;
150
171
  }
151
172
 
152
- // Fix invalid default_provider — must be in VALID_PROVIDERS
153
- if (!VALID_PROVIDERS.includes(config.ai.default_provider as any)) {
173
+ // Fix invalid default_provider — must be in VALID_PROVIDERS or be a registered provider key
174
+ if (!VALID_PROVIDERS.includes(config.ai.default_provider as any) &&
175
+ !config.ai.providers[config.ai.default_provider]) {
154
176
  config.ai.default_provider = DEFAULT_CONFIG.ai.default_provider;
155
177
  dirty = true;
156
178
  }
@@ -0,0 +1,36 @@
1
+ import type { Readable } from "node:stream";
2
+
3
+ /**
4
+ * Async generator: reads from a stream, buffers partial lines,
5
+ * yields parsed JSON objects one per complete line.
6
+ * Handles TCP packet splitting across JSON boundaries.
7
+ */
8
+ export async function* parseNdjsonLines(stream: Readable): AsyncIterable<unknown> {
9
+ let buffer = "";
10
+
11
+ for await (const chunk of stream) {
12
+ buffer += chunk.toString();
13
+ const lines = buffer.split(/\r?\n/);
14
+ buffer = lines.pop() || ""; // keep trailing partial
15
+
16
+ for (const line of lines) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed) continue;
19
+ try {
20
+ yield JSON.parse(trimmed);
21
+ } catch {
22
+ // Skip non-JSON lines (e.g. deprecation warnings from CLI)
23
+ console.warn(`[ndjson] skipping non-JSON line: ${trimmed.slice(0, 100)}`);
24
+ }
25
+ }
26
+ }
27
+
28
+ // Flush remaining buffer
29
+ if (buffer.trim()) {
30
+ try {
31
+ yield JSON.parse(buffer.trim());
32
+ } catch {
33
+ // ignore trailing non-JSON
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,269 @@
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+ import {
3
+ ArrowLeft,
4
+ ArrowRight,
5
+ RotateCcw,
6
+ ExternalLink,
7
+ Globe,
8
+ } from "lucide-react";
9
+ import { useTabStore } from "@/stores/tab-store";
10
+
11
+ /** Parse a URL string — returns normalized URL or null if invalid */
12
+ function parseUrl(input: string): string | null {
13
+ let url = input.trim();
14
+ if (!url) return null;
15
+
16
+ // If just a port number, treat as localhost
17
+ if (/^\d+$/.test(url)) return `http://localhost:${url}`;
18
+
19
+ // If host:port without scheme, add http://
20
+ if (/^localhost(:\d+)?/.test(url)) url = `http://${url}`;
21
+ if (/^[\w.-]+:\d+/.test(url) && !url.includes("://")) url = `http://${url}`;
22
+
23
+ // If no scheme at all, add https:// for external, http:// for localhost
24
+ if (!url.includes("://")) {
25
+ url = url.includes("localhost") ? `http://${url}` : `https://${url}`;
26
+ }
27
+
28
+ try {
29
+ new URL(url);
30
+ return url;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /** Check if a URL is a localhost address */
37
+ function isLocalhost(url: string): boolean {
38
+ try {
39
+ const u = new URL(url);
40
+ return (
41
+ u.hostname === "localhost" ||
42
+ u.hostname === "127.0.0.1" ||
43
+ u.hostname === "0.0.0.0" ||
44
+ u.hostname === "::1"
45
+ );
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /** Convert URL to iframe src — proxy localhost through backend */
52
+ function toIframeSrc(url: string): string {
53
+ if (!isLocalhost(url)) return url;
54
+
55
+ try {
56
+ const u = new URL(url);
57
+ const port = u.port || "80";
58
+ const path = u.pathname + u.search + u.hash;
59
+ return `/api/preview/${port}${path}`;
60
+ } catch {
61
+ return url;
62
+ }
63
+ }
64
+
65
+ /** Extract display URL from iframe src (reverse of toIframeSrc) */
66
+ function fromIframeSrc(src: string): string {
67
+ const match = src.match(/^\/api\/preview\/(\d+)(\/.*)?$/);
68
+ if (match) {
69
+ const port = match[1];
70
+ const path = match[2] || "/";
71
+ return `http://localhost:${port}${path}`;
72
+ }
73
+ return src;
74
+ }
75
+
76
+ interface BrowserTabProps {
77
+ metadata?: Record<string, unknown>;
78
+ tabId?: string;
79
+ }
80
+
81
+ export function BrowserTab({ metadata, tabId }: BrowserTabProps) {
82
+ const initialUrl = (metadata?.url as string) || "http://localhost:3000";
83
+ const [addressBar, setAddressBar] = useState(initialUrl);
84
+ const [currentUrl, setCurrentUrl] = useState(initialUrl);
85
+ const [iframeSrc, setIframeSrc] = useState(toIframeSrc(initialUrl));
86
+ const [canGoBack, setCanGoBack] = useState(false);
87
+ const [canGoForward, setCanGoForward] = useState(false);
88
+ const [loading, setLoading] = useState(true);
89
+ const [error, setError] = useState<string | null>(null);
90
+ const iframeRef = useRef<HTMLIFrameElement>(null);
91
+ const updateTab = useTabStore((s) => s.updateTab);
92
+
93
+ // Navigation history (iframe same-origin only)
94
+ const historyRef = useRef<string[]>([initialUrl]);
95
+ const historyIdxRef = useRef(0);
96
+
97
+ const navigate = useCallback(
98
+ (url: string, addToHistory = true) => {
99
+ const parsed = parseUrl(url);
100
+ if (!parsed) {
101
+ setError("Invalid URL");
102
+ return;
103
+ }
104
+
105
+ setError(null);
106
+ setCurrentUrl(parsed);
107
+ setAddressBar(parsed);
108
+ setIframeSrc(toIframeSrc(parsed));
109
+ setLoading(true);
110
+
111
+ if (addToHistory) {
112
+ const h = historyRef.current;
113
+ const idx = historyIdxRef.current;
114
+ // Truncate forward history
115
+ historyRef.current = h.slice(0, idx + 1);
116
+ historyRef.current.push(parsed);
117
+ historyIdxRef.current = historyRef.current.length - 1;
118
+ }
119
+
120
+ setCanGoBack(historyIdxRef.current > 0);
121
+ setCanGoForward(
122
+ historyIdxRef.current < historyRef.current.length - 1,
123
+ );
124
+
125
+ // Update tab title
126
+ if (tabId) {
127
+ try {
128
+ const u = new URL(parsed);
129
+ const title = isLocalhost(parsed)
130
+ ? `localhost:${u.port || "80"}`
131
+ : u.hostname;
132
+ updateTab(tabId, { title });
133
+ } catch {}
134
+ }
135
+ },
136
+ [tabId, updateTab],
137
+ );
138
+
139
+ const goBack = useCallback(() => {
140
+ if (historyIdxRef.current > 0) {
141
+ historyIdxRef.current--;
142
+ navigate(historyRef.current[historyIdxRef.current]!, false);
143
+ }
144
+ }, [navigate]);
145
+
146
+ const goForward = useCallback(() => {
147
+ if (historyIdxRef.current < historyRef.current.length - 1) {
148
+ historyIdxRef.current++;
149
+ navigate(historyRef.current[historyIdxRef.current]!, false);
150
+ }
151
+ }, [navigate]);
152
+
153
+ const reload = useCallback(() => {
154
+ setLoading(true);
155
+ setError(null);
156
+ if (iframeRef.current) {
157
+ // Force reload by re-setting src
158
+ const src = iframeRef.current.src;
159
+ iframeRef.current.src = "";
160
+ requestAnimationFrame(() => {
161
+ if (iframeRef.current) iframeRef.current.src = src;
162
+ });
163
+ }
164
+ }, []);
165
+
166
+ const openExternal = useCallback(() => {
167
+ window.open(currentUrl, "_blank");
168
+ }, [currentUrl]);
169
+
170
+ const handleAddressKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
171
+ if (e.key === "Enter") {
172
+ e.preventDefault();
173
+ navigate(addressBar);
174
+ }
175
+ };
176
+
177
+ // Navigate when metadata.url changes (e.g. opened from command palette)
178
+ useEffect(() => {
179
+ const metaUrl = metadata?.url as string | undefined;
180
+ if (metaUrl && metaUrl !== currentUrl) {
181
+ navigate(metaUrl);
182
+ }
183
+ }, [metadata?.url]);
184
+
185
+ return (
186
+ <div className="flex flex-col h-full w-full bg-background">
187
+ {/* Toolbar */}
188
+ <div className="flex items-center gap-1 px-2 py-1.5 border-b border-border bg-surface shrink-0">
189
+ {/* Nav buttons */}
190
+ <button
191
+ onClick={goBack}
192
+ disabled={!canGoBack}
193
+ className="p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors"
194
+ title="Back"
195
+ >
196
+ <ArrowLeft className="size-4" />
197
+ </button>
198
+ <button
199
+ onClick={goForward}
200
+ disabled={!canGoForward}
201
+ className="p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors"
202
+ title="Forward"
203
+ >
204
+ <ArrowRight className="size-4" />
205
+ </button>
206
+ <button
207
+ onClick={reload}
208
+ className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
209
+ title="Reload"
210
+ >
211
+ <RotateCcw className={`size-4 ${loading ? "animate-spin" : ""}`} />
212
+ </button>
213
+
214
+ {/* Address bar */}
215
+ <div className="flex-1 flex items-center gap-2 mx-1 px-2.5 py-1.5 rounded-md bg-background border border-border focus-within:border-accent/50 transition-colors">
216
+ <Globe className="size-3.5 text-text-subtle shrink-0" />
217
+ <input
218
+ type="text"
219
+ value={addressBar}
220
+ onChange={(e) => setAddressBar(e.target.value)}
221
+ onKeyDown={handleAddressKeyDown}
222
+ placeholder="Enter URL or port (e.g. 3000, localhost:8080)"
223
+ className="flex-1 bg-transparent text-xs text-text-primary outline-none placeholder:text-text-subtle min-w-0"
224
+ />
225
+ </div>
226
+
227
+ {/* Open external */}
228
+ <button
229
+ onClick={openExternal}
230
+ className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
231
+ title="Open in browser"
232
+ >
233
+ <ExternalLink className="size-4" />
234
+ </button>
235
+ </div>
236
+
237
+ {/* Content */}
238
+ <div className="flex-1 relative min-h-0">
239
+ {error ? (
240
+ <div className="flex items-center justify-center h-full text-text-secondary text-sm">
241
+ <p>{error}</p>
242
+ </div>
243
+ ) : (
244
+ <iframe
245
+ ref={iframeRef}
246
+ src={iframeSrc}
247
+ className="w-full h-full border-0"
248
+ sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-modals"
249
+ onLoad={() => setLoading(false)}
250
+ onError={() => {
251
+ setLoading(false);
252
+ setError(`Failed to load ${currentUrl}`);
253
+ }}
254
+ />
255
+ )}
256
+
257
+ {/* Loading overlay */}
258
+ {loading && !error && (
259
+ <div className="absolute inset-0 flex items-center justify-center bg-background/50">
260
+ <div className="flex items-center gap-2 text-sm text-text-secondary">
261
+ <RotateCcw className="size-4 animate-spin" />
262
+ <span>Loading...</span>
263
+ </div>
264
+ </div>
265
+ )}
266
+ </div>
267
+ </div>
268
+ );
269
+ }
@@ -1,11 +1,12 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
- import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff } from "lucide-react";
2
+ import { History, Settings2, Loader2, RefreshCw, Search, Pencil, Check, X, BellOff } from "lucide-react";
3
3
  import { Activity } from "lucide-react";
4
4
  import { api, projectUrl } from "@/lib/api-client";
5
5
  import { useTabStore } from "@/stores/tab-store";
6
6
  import { useNotificationStore } from "@/stores/notification-store";
7
7
  import { AISettingsSection } from "@/components/settings/ai-settings-section";
8
8
  import { UsageDetailPanel } from "./usage-badge";
9
+ import { ProviderBadge } from "./provider-selector";
9
10
  import type { SessionInfo } from "../../../types/chat";
10
11
  import type { UsageInfo } from "../../../types/chat";
11
12
 
@@ -19,6 +20,7 @@ interface ChatHistoryBarProps {
19
20
  refreshUsage?: () => void;
20
21
  lastFetchedAt?: string | null;
21
22
  sessionId?: string | null;
23
+ providerId?: string;
22
24
  onSelectSession?: (session: SessionInfo) => void;
23
25
  onBugReport?: () => void;
24
26
  isConnected?: boolean;
@@ -50,7 +52,7 @@ function pctColor(pct: number): string {
50
52
 
51
53
  export function ChatHistoryBar({
52
54
  projectName, usageInfo, contextWindowPct, usageLoading, refreshUsage, lastFetchedAt,
53
- sessionId, onSelectSession, onBugReport, isConnected, onReconnect,
55
+ sessionId, providerId, onSelectSession, onBugReport, isConnected, onReconnect,
54
56
  }: ChatHistoryBarProps) {
55
57
  const [activePanel, setActivePanel] = useState<PanelType>(null);
56
58
  const [sessions, setSessions] = useState<SessionInfo[]>([]);
@@ -94,7 +96,7 @@ export function ChatHistoryBar({
94
96
  type: "chat",
95
97
  title: session.title || "Chat",
96
98
  projectId: projectName ?? null,
97
- metadata: { projectName, sessionId: session.id },
99
+ metadata: { projectName, sessionId: session.id, providerId: session.providerId },
98
100
  closable: true,
99
101
  });
100
102
  }
@@ -126,7 +128,8 @@ export function ChatHistoryBar({
126
128
  ? sessions.filter((s) => (s.title || "").toLowerCase().includes(searchQuery.toLowerCase()))
127
129
  : sessions;
128
130
 
129
- // Usage badge display
131
+ // Usage badge display — only meaningful for Claude (SDK) provider
132
+ const isClaudeProvider = !providerId || providerId === "claude";
130
133
  const fiveHourPct = usageInfo.fiveHour != null ? Math.round(usageInfo.fiveHour * 100) : null;
131
134
  const sevenDayPct = usageInfo.sevenDay != null ? Math.round(usageInfo.sevenDay * 100) : null;
132
135
  const worstPct = Math.max(fiveHourPct ?? 0, sevenDayPct ?? 0);
@@ -147,6 +150,14 @@ export function ChatHistoryBar({
147
150
  <span>History</span>
148
151
  </button>
149
152
 
153
+ {/* Active provider indicator */}
154
+ {sessionId && providerId && providerId !== "mock" && (
155
+ <span className="flex items-center gap-1 px-1.5 py-0.5 text-[11px] text-text-secondary">
156
+ <ProviderBadge providerId={providerId} />
157
+ <span className="capitalize">{providerId}</span>
158
+ </span>
159
+ )}
160
+
150
161
  {/* Config */}
151
162
  <button
152
163
  onClick={() => togglePanel("config")}
@@ -158,28 +169,37 @@ export function ChatHistoryBar({
158
169
  <Settings2 className="size-3" />
159
170
  </button>
160
171
 
161
- {/* Usage & Accounts */}
162
- <button
163
- onClick={() => togglePanel("usage")}
164
- className={`flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-medium tabular-nums transition-colors hover:bg-surface-elevated ${
165
- activePanel === "usage" ? "bg-primary/10" : ""
166
- } ${usageColor}`}
167
- title="Usage limits"
168
- >
169
- <Activity className="size-3" />
170
- {usageInfo.activeAccountLabel && (
171
- <span className="text-text-secondary font-normal truncate max-w-[60px]">[{usageInfo.activeAccountLabel}]</span>
172
- )}
173
- <span>5h:{fiveHourPct != null ? `${fiveHourPct}%` : "--%"}</span>
174
- <span className="text-text-subtle">·</span>
175
- <span>Wk:{sevenDayPct != null ? `${sevenDayPct}%` : "--%"}</span>
176
- {contextWindowPct != null && (
177
- <>
178
- <span className="text-text-subtle">·</span>
179
- <span className={pctColor(contextWindowPct)}>Ctx:{contextWindowPct}%</span>
180
- </>
181
- )}
182
- </button>
172
+ {/* Usage & Accounts — full display for Claude, minimal for other providers */}
173
+ {isClaudeProvider ? (
174
+ <button
175
+ onClick={() => togglePanel("usage")}
176
+ className={`flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-medium tabular-nums transition-colors hover:bg-surface-elevated ${
177
+ activePanel === "usage" ? "bg-primary/10" : ""
178
+ } ${usageColor}`}
179
+ title="Usage limits"
180
+ >
181
+ <Activity className="size-3" />
182
+ {usageInfo.activeAccountLabel && (
183
+ <span className="text-text-secondary font-normal truncate max-w-[60px]">[{usageInfo.activeAccountLabel}]</span>
184
+ )}
185
+ <span>5h:{fiveHourPct != null ? `${fiveHourPct}%` : "--%"}</span>
186
+ <span className="text-text-subtle">·</span>
187
+ <span>Wk:{sevenDayPct != null ? `${sevenDayPct}%` : "--%"}</span>
188
+ {contextWindowPct != null && (
189
+ <>
190
+ <span className="text-text-subtle">·</span>
191
+ <span className={pctColor(contextWindowPct)}>Ctx:{contextWindowPct}%</span>
192
+ </>
193
+ )}
194
+ </button>
195
+ ) : (
196
+ contextWindowPct != null && (
197
+ <span className={`flex items-center gap-1 px-1.5 py-0.5 text-[11px] font-medium tabular-nums ${pctColor(contextWindowPct)}`}>
198
+ <Activity className="size-3" />
199
+ <span>Ctx:{contextWindowPct}%</span>
200
+ </span>
201
+ )
202
+ )}
183
203
 
184
204
  {/* Spacer */}
185
205
  <div className="flex-1" />
@@ -247,7 +267,7 @@ export function ChatHistoryBar({
247
267
  key={session.id}
248
268
  className="flex items-center gap-2 w-full px-3 py-1.5 text-left hover:bg-surface-elevated transition-colors group"
249
269
  >
250
- <MessageSquare className="size-3 shrink-0 text-text-subtle" />
270
+ <ProviderBadge providerId={session.providerId} />
251
271
  {editingId === session.id ? (
252
272
  <form
253
273
  className="flex items-center gap-1 flex-1 min-w-0"
@@ -303,8 +323,8 @@ export function ChatHistoryBar({
303
323
  </div>
304
324
  )}
305
325
 
306
- {/* Usage panel */}
307
- {activePanel === "usage" && (
326
+ {/* Usage panel — only for Claude provider */}
327
+ {activePanel === "usage" && isClaudeProvider && (
308
328
  <UsageDetailPanel
309
329
  usage={usageInfo}
310
330
  visible={true}