@hienlh/ppm 0.8.87 → 0.8.89

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 (190) hide show
  1. package/CHANGELOG.md +75 -40
  2. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-3Xe18azI.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-Yy35llnn.js} +1 -1
  4. package/dist/web/assets/api-settings-Bid0NHuI.js +1 -0
  5. package/dist/web/assets/{arc-BAOivWpI.js → arc-B9n1Gvb5.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-DqAZP_F6.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-h3cDF2vI.js} +1 -1
  9. package/dist/web/assets/{browser-tab-DaHGm_0i.js → browser-tab-DSWumOSG.js} +1 -1
  10. package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
  11. package/dist/web/assets/channel-C2fMafck.js +1 -0
  12. package/dist/web/assets/chat-tab-Ccwf-c6M.js +8 -0
  13. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C3aZvW7B.js} +1 -1
  14. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
  15. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
  16. package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
  17. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
  18. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
  19. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
  20. package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-tDjHsAUs.js} +1 -1
  21. package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +2 -0
  22. package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +1 -0
  23. package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
  24. package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
  25. package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
  26. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
  27. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
  28. package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
  29. package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
  30. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
  31. package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
  32. package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-C7Gry6md.js} +1 -1
  33. package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +1 -0
  34. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
  35. package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
  36. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
  37. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
  38. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
  39. package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-av5aeHLq.js} +1 -1
  40. package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +1 -0
  41. package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +1 -0
  42. package/dist/web/assets/clone-B2hUek6n.js +1 -0
  43. package/dist/web/assets/code-editor-DLTcPb55.js +2 -0
  44. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-qudEiMCT.js} +1 -1
  45. package/dist/web/assets/{csv-preview-DLqYtXxt.js → csv-preview-DUbHtTAS.js} +1 -1
  46. package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BFcnKyBF.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
  48. package/dist/web/assets/{database-viewer-DXk79Nel.js → database-viewer-BrpPlYG7.js} +1 -1
  49. package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
  52. package/dist/web/assets/diff-viewer-Dx96kcTu.js +4 -0
  53. package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
  54. package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
  55. package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
  56. package/dist/web/assets/git-graph-CoN6voTp.js +1 -0
  57. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
  58. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
  59. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-Duh_bWLa.js} +1 -1
  60. package/dist/web/assets/index-CtbNK_ih.css +2 -0
  61. package/dist/web/assets/index-DRdx_Wqn.js +37 -0
  62. package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +1 -0
  63. package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +2 -0
  64. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B9L-Ge-H.js} +1 -1
  65. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
  66. package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
  67. package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
  68. package/dist/web/assets/keybindings-store-DHGoLYnP.js +1 -0
  69. package/dist/web/assets/{line-B78g-52T.js → line-B75-Rx70.js} +1 -1
  70. package/dist/web/assets/{linear-DP4mkX3m.js → linear-Bcjv9FQt.js} +1 -1
  71. package/dist/web/assets/{markdown-renderer-Brj8_LQM.js → markdown-renderer-BqsXIW9n.js} +5 -5
  72. package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-8u2leTXI.js} +2 -2
  73. package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
  74. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-LFEjVtwQ.js} +1 -1
  75. package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +1 -0
  76. package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +1 -0
  77. package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
  78. package/dist/web/assets/{postgres-viewer-CwkTGmqy.js → postgres-viewer-Lw8xaGfc.js} +1 -1
  79. package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-CdjGIDfw.js} +1 -1
  80. package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +1 -0
  81. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
  82. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
  83. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
  84. package/dist/web/assets/settings-tab-DDCC58we.js +1 -0
  85. package/dist/web/assets/{sqlite-viewer-CFYTwgA8.js → sqlite-viewer-DECA802J.js} +1 -1
  86. package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-C4EMl6jf.js} +1 -1
  87. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +1 -0
  88. package/dist/web/assets/{tab-store-BJw7OCmy.js → tab-store--SlERlDs.js} +1 -1
  89. package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-DneNM6WP.js} +2 -2
  90. package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-A4PN_Efm.js} +1 -1
  91. package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +1 -0
  92. package/dist/web/assets/{use-monaco-theme-CNzekTN3.js → use-monaco-theme-CrtYAJMR.js} +1 -1
  93. package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
  94. package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
  95. package/dist/web/index.html +10 -11
  96. package/dist/web/sw.js +1 -1
  97. package/docs/code-standards.md +155 -0
  98. package/docs/codebase-summary.md +261 -95
  99. package/docs/project-changelog.md +38 -3
  100. package/docs/project-roadmap.md +2 -2
  101. package/docs/streaming-input-guide.md +267 -0
  102. package/docs/system-architecture.md +151 -0
  103. package/package.json +1 -1
  104. package/snapshot-state.md +1526 -0
  105. package/src/providers/claude-agent-sdk.ts +244 -102
  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 +6 -0
  114. package/src/server/routes/chat.ts +14 -3
  115. package/src/server/routes/mcp.ts +84 -0
  116. package/src/server/routes/settings.ts +14 -0
  117. package/src/server/ws/chat.ts +127 -81
  118. package/src/services/account.service.ts +2 -2
  119. package/src/services/chat.service.ts +10 -15
  120. package/src/services/claude-usage.service.ts +2 -7
  121. package/src/services/db.service.ts +8 -0
  122. package/src/services/mcp-config.service.ts +111 -0
  123. package/src/types/api.ts +1 -1
  124. package/src/types/chat.ts +23 -2
  125. package/src/types/config.ts +33 -11
  126. package/src/types/mcp.ts +47 -0
  127. package/src/utils/ndjson-line-parser.ts +36 -0
  128. package/src/web/components/chat/chat-history-bar.tsx +48 -29
  129. package/src/web/components/chat/chat-tab.tsx +29 -24
  130. package/src/web/components/chat/message-input.tsx +64 -5
  131. package/src/web/components/chat/provider-selector.tsx +150 -0
  132. package/src/web/components/chat/session-picker.tsx +3 -1
  133. package/src/web/components/chat/usage-badge.tsx +58 -8
  134. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  135. package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
  136. package/src/web/components/settings/mcp-settings-section.tsx +143 -0
  137. package/src/web/components/settings/settings-tab.tsx +5 -2
  138. package/src/web/hooks/use-chat.ts +32 -15
  139. package/src/web/lib/api-mcp.ts +38 -0
  140. package/test-tokens.mjs +212 -0
  141. package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
  142. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
  143. package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
  144. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  145. package/dist/web/assets/channel-wrd-NHWf.js +0 -1
  146. package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
  147. package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
  148. package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
  149. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  150. package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
  151. package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
  152. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  153. package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
  154. package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
  155. package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
  156. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  157. package/dist/web/assets/index-CgQXpBb_.css +0 -2
  158. package/dist/web/assets/index-DEeeRoka.js +0 -37
  159. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  160. package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
  161. package/dist/web/assets/input-BglMT33g.js +0 -1
  162. package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
  163. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  164. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  165. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  166. package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
  167. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
  168. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  169. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-BKIT_Qeg.js} +0 -0
  170. /package/dist/web/assets/{array-B9UHiPd-.js → array-DqLCdDFv.js} +0 -0
  171. /package/dist/web/assets/{chevron-right-DeV0ehiG.js → chevron-right-CHnjJt4E.js} +0 -0
  172. /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-DbesTfa7.js} +0 -0
  173. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-CWPXKqbJ.js} +0 -0
  174. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-CrJzLgRD.js} +0 -0
  175. /package/dist/web/assets/{dist-lF8CoYII.js → dist-CALwEtco.js} +0 -0
  176. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-Cep75xXf.js} +0 -0
  177. /package/dist/web/assets/{dist-DylI9XxN.js → dist-DGDPTxs1.js} +0 -0
  178. /package/dist/web/assets/{init-DlZdxViB.js → init-C0r9Gk5G.js} +0 -0
  179. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
  180. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-DzXRfQ_m.js} +0 -0
  181. /package/dist/web/assets/{lib-BQ34Db2e.js → lib-BeaDXEkP.js} +0 -0
  182. /package/dist/web/assets/{math-069Z4SuC.js → math-y9zN1W-N.js} +0 -0
  183. /package/dist/web/assets/{path-6uRLdFF7.js → path-DIKpVbHL.js} +0 -0
  184. /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-Bf_JiD2A.js} +0 -0
  185. /package/dist/web/assets/{react-ER-4DN55.js → react-SKk5z-bm.js} +0 -0
  186. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-nHaDi0Kw.js} +0 -0
  187. /package/dist/web/assets/{src-BqX54PbV.js → src-Dw4QhedI.js} +0 -0
  188. /package/dist/web/assets/{table-C7X5UAEI.js → table-CQVQM2SB.js} +0 -0
  189. /package/dist/web/assets/{tag-CCtdV063.js → tag-Q2dZiSPX.js} +0 -0
  190. /package/dist/web/assets/{utils-BNytJOb1.js → utils-DMiycH3O.js} +0 -0
@@ -10,11 +10,11 @@ import { useNotificationStore } from "@/stores/notification-store";
10
10
  import { openBugReportPopup } from "@/lib/report-bug";
11
11
  import { getAISettings } from "@/lib/api-settings";
12
12
  import { MessageList } from "./message-list";
13
- import { MessageInput, type ChatAttachment } from "./message-input";
13
+ import { MessageInput, type ChatAttachment, type MessagePriority } from "./message-input";
14
14
  import { SlashCommandPicker, type SlashItem } from "./slash-command-picker";
15
15
  import { FilePicker } from "./file-picker";
16
16
  import { ChatHistoryBar } from "./chat-history-bar";
17
- import { ChatWelcome } from "./chat-welcome";
17
+
18
18
  import type { DragEvent } from "react";
19
19
  import type { FileNode } from "../../../types/project";
20
20
  import type { Session, SessionInfo } from "../../../types/chat";
@@ -90,7 +90,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
90
90
  pendingApproval,
91
91
  contextWindowPct,
92
92
  sessionTitle,
93
- streamingAccountLabel,
93
+ migratedSessionId,
94
94
  sendMessage,
95
95
  respondToApproval,
96
96
  cancelStreaming,
@@ -99,6 +99,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
99
99
  isConnected,
100
100
  } = useChat(sessionId, providerId, projectName);
101
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
+
102
109
  // Auto-clear notification badge when this tab is active and document is visible.
103
110
  // Handles the case where notification arrived while browser tab was hidden.
104
111
  useEffect(() => {
@@ -142,11 +149,11 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
142
149
  useTabStore.getState().openTab({
143
150
  type: "chat",
144
151
  title: "AI Chat",
145
- metadata: { projectName },
152
+ metadata: { projectName, providerId },
146
153
  projectId: projectName || null,
147
154
  closable: true,
148
155
  });
149
- }, [projectName]);
156
+ }, [projectName, providerId]);
150
157
 
151
158
  const handleSelectSession = useCallback((session: SessionInfo) => {
152
159
  setSessionId(session.id);
@@ -198,7 +205,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
198
205
  );
199
206
 
200
207
  const handleSend = useCallback(
201
- async (content: string, attachments: ChatAttachment[] = []) => {
208
+ async (content: string, attachments: ChatAttachment[] = [], priority?: MessagePriority) => {
202
209
  const fullContent = buildMessageWithAttachments(content, attachments);
203
210
  if (!fullContent.trim()) return;
204
211
 
@@ -220,7 +227,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
220
227
  return;
221
228
  }
222
229
  }
223
- sendMessage(fullContent, { permissionMode });
230
+ sendMessage(fullContent, { permissionMode, priority });
224
231
  },
225
232
  [sessionId, providerId, projectName, sendMessage, buildMessageWithAttachments, permissionMode],
226
233
  );
@@ -323,22 +330,18 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
323
330
  </div>
324
331
  )}
325
332
 
326
- {/* Messages or Welcome screen */}
327
- {!sessionId ? (
328
- <ChatWelcome projectName={projectName} onSelectSession={handleSelectSession} />
329
- ) : (
330
- <MessageList
331
- messages={messages}
332
- messagesLoading={messagesLoading}
333
- pendingApproval={pendingApproval}
334
- onApprovalResponse={respondToApproval}
335
- isStreaming={isStreaming}
336
- phase={phase}
337
- connectingElapsed={connectingElapsed}
338
- projectName={projectName}
339
- onFork={!isStreaming ? handleFork : undefined}
340
- />
341
- )}
333
+ {/* Messages */}
334
+ <MessageList
335
+ messages={messages}
336
+ messagesLoading={messagesLoading}
337
+ pendingApproval={pendingApproval}
338
+ onApprovalResponse={respondToApproval}
339
+ isStreaming={isStreaming}
340
+ phase={phase}
341
+ connectingElapsed={connectingElapsed}
342
+ projectName={projectName}
343
+ onFork={!isStreaming ? handleFork : undefined}
344
+ />
342
345
 
343
346
  {/* Bottom toolbar */}
344
347
  <div className="border-t border-border bg-background shrink-0">
@@ -351,10 +354,10 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
351
354
  refreshUsage={refreshUsage}
352
355
  lastFetchedAt={lastFetchedAt}
353
356
  sessionId={sessionId}
357
+ providerId={providerId}
354
358
  onSelectSession={handleSelectSession}
355
359
  onBugReport={sessionId ? () => openBugReportPopup(version, { sessionId, projectName }) : undefined}
356
360
  isConnected={isConnected}
357
- streamingAccountLabel={streamingAccountLabel}
358
361
  onReconnect={() => {
359
362
  if (!isConnected) reconnect();
360
363
  refetchMessages();
@@ -393,6 +396,8 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
393
396
  externalFiles={externalFiles}
394
397
  permissionMode={permissionMode}
395
398
  onModeChange={setPermissionMode}
399
+ providerId={providerId}
400
+ onProviderChange={!sessionId ? setProviderId : undefined}
396
401
  />
397
402
  </div>
398
403
 
@@ -1,11 +1,12 @@
1
1
  import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
2
- import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff } from "lucide-react";
2
+ import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff, Zap, ListOrdered, Clock } from "lucide-react";
3
3
  import { useVoiceInput } from "@/hooks/use-voice-input";
4
4
  import { api, projectUrl, getAuthToken } from "@/lib/api-client";
5
5
  import { randomId } from "@/lib/utils";
6
6
  import { isSupportedFile, isImageFile } from "@/lib/file-support";
7
7
  import { AttachmentChips } from "./attachment-chips";
8
8
  import { ModeSelector, getModeLabel, getModeIcon } from "./mode-selector";
9
+ import { ProviderSelector } from "./provider-selector";
9
10
  import type { SlashItem } from "./slash-command-picker";
10
11
  import type { FileNode } from "../../../types/project";
11
12
  import { flattenFileTree } from "./file-picker";
@@ -21,8 +22,10 @@ export interface ChatAttachment {
21
22
  status: "uploading" | "ready" | "error";
22
23
  }
23
24
 
25
+ export type MessagePriority = 'now' | 'next' | 'later';
26
+
24
27
  interface MessageInputProps {
25
- onSend: (content: string, attachments: ChatAttachment[]) => void;
28
+ onSend: (content: string, attachments: ChatAttachment[], priority?: MessagePriority) => void;
26
29
  isStreaming?: boolean;
27
30
  onCancel?: () => void;
28
31
  disabled?: boolean;
@@ -45,6 +48,10 @@ interface MessageInputProps {
45
48
  permissionMode?: string;
46
49
  /** Permission mode change handler */
47
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;
48
55
  }
49
56
 
50
57
  export const MessageInput = memo(function MessageInput({
@@ -64,11 +71,14 @@ export const MessageInput = memo(function MessageInput({
64
71
  autoFocus,
65
72
  permissionMode,
66
73
  onModeChange,
74
+ providerId,
75
+ onProviderChange,
67
76
  }: MessageInputProps) {
68
77
  const [value, setValue] = useState(initialValue ?? "");
69
78
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
70
79
  const [modeSelectorOpen, setModeSelectorOpen] = useState(false);
71
80
  const [pendingSend, setPendingSend] = useState(false);
81
+ const [priority, setPriority] = useState<MessagePriority>('next');
72
82
  const textareaRef = useRef<HTMLTextAreaElement>(null);
73
83
  const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
74
84
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -323,7 +333,7 @@ export const MessageInput = memo(function MessageInput({
323
333
  onSlashStateChange?.(false, "");
324
334
  onFileStateChange?.(false, "");
325
335
  if (voice.isListening) voice.stop();
326
- onSend(trimmed, readyAttachments);
336
+ onSend(trimmed, readyAttachments, isStreaming ? priority : undefined);
327
337
  setValue("");
328
338
  // Revoke preview URLs
329
339
  for (const att of attachments) {
@@ -331,9 +341,10 @@ export const MessageInput = memo(function MessageInput({
331
341
  }
332
342
  setAttachments([]);
333
343
  setPendingSend(false);
344
+ setPriority('next');
334
345
  if (textareaRef.current) textareaRef.current.style.height = "auto";
335
346
  if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
336
- }, [value, attachments, onSend, onSlashStateChange, onFileStateChange]);
347
+ }, [value, attachments, onSend, onSlashStateChange, onFileStateChange, isStreaming, priority]);
337
348
 
338
349
  const handleSend = useCallback(() => {
339
350
  if (disabled) return;
@@ -489,7 +500,7 @@ export const MessageInput = memo(function MessageInput({
489
500
  >
490
501
  {/* Attachment chips (inside container, aligned with input) */}
491
502
  <AttachmentChips attachments={attachments} onRemove={removeAttachment} />
492
- {/* Mobile: mode chip row */}
503
+ {/* Mobile: mode chip + provider selector row */}
493
504
  <div className="flex items-center gap-1 px-2 pt-2 md:hidden relative">
494
505
  <ModeChip
495
506
  mode={permissionMode ?? "bypassPermissions"}
@@ -501,6 +512,14 @@ export const MessageInput = memo(function MessageInput({
501
512
  open={modeSelectorOpen}
502
513
  onOpenChange={setModeSelectorOpen}
503
514
  />
515
+ {onProviderChange && projectName && (
516
+ <ProviderSelector
517
+ value={providerId ?? "claude"}
518
+ onChange={onProviderChange}
519
+ projectName={projectName}
520
+ />
521
+ )}
522
+ {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
504
523
  </div>
505
524
  {/* Mobile: single row — attach + mic + textarea + send */}
506
525
  <div className="flex items-end gap-1 md:hidden px-2 py-2">
@@ -615,6 +634,15 @@ export const MessageInput = memo(function MessageInput({
615
634
  onOpenChange={setModeSelectorOpen}
616
635
  />
617
636
  </div>
637
+ {/* Provider selector — only when no active session */}
638
+ {onProviderChange && projectName && (
639
+ <ProviderSelector
640
+ value={providerId ?? "claude"}
641
+ onChange={onProviderChange}
642
+ projectName={projectName}
643
+ />
644
+ )}
645
+ {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
618
646
  </div>
619
647
  <div className="flex items-center gap-1">
620
648
  {showCancel ? (
@@ -661,3 +689,34 @@ function ModeChip({ mode, onClick }: { mode: string; onClick: () => void }) {
661
689
  </button>
662
690
  );
663
691
  }
692
+
693
+ const PRIORITY_OPTIONS: { value: MessagePriority; label: string; Icon: typeof Zap }[] = [
694
+ { value: 'now', label: 'Interrupt', Icon: Zap },
695
+ { value: 'next', label: 'Queue', Icon: ListOrdered },
696
+ { value: 'later', label: 'Later', Icon: Clock },
697
+ ];
698
+
699
+ /** Compact priority toggle — visible only during streaming */
700
+ function PriorityToggle({ value, onChange }: { value: MessagePriority; onChange: (v: MessagePriority) => void }) {
701
+ const cycle = useCallback(() => {
702
+ const order: MessagePriority[] = ['next', 'later', 'now'];
703
+ const idx = order.indexOf(value);
704
+ onChange(order[(idx + 1) % order.length]!);
705
+ }, [value, onChange]);
706
+
707
+ const current = PRIORITY_OPTIONS.find((o) => o.value === value) ?? PRIORITY_OPTIONS[1]!;
708
+ const Icon = current.Icon;
709
+
710
+ return (
711
+ <button
712
+ type="button"
713
+ onClick={(e) => { e.stopPropagation(); cycle(); }}
714
+ 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"
715
+ aria-label={`Message priority: ${current.label}`}
716
+ title={`Priority: ${current.label} (click to cycle)`}
717
+ >
718
+ <Icon className="size-3" />
719
+ <span>{current.label}</span>
720
+ </button>
721
+ );
722
+ }
@@ -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, Pin, PinOff } from "lucide-react";
4
+ import { ProviderBadge } from "./provider-selector";
4
5
  import type { SessionInfo } from "../../../types/chat";
5
6
 
6
7
  interface SessionPickerProps {
@@ -95,7 +96,8 @@ export function SessionPicker({
95
96
  }`}
96
97
  >
97
98
  <div className="flex flex-col min-w-0 flex-1">
98
- <span className="truncate text-xs font-medium">
99
+ <span className="flex items-center gap-1.5 truncate text-xs font-medium">
100
+ <ProviderBadge providerId={session.providerId} />
99
101
  {session.title}
100
102
  </span>
101
103
  <span className="text-xs text-text-subtle">
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
- import { Activity, RefreshCw, Eye, Download, Upload, Plus, X, Settings } from "lucide-react";
2
+ import { Activity, RefreshCw, Eye, Download, Upload, Plus, X, Settings, Trash2 } from "lucide-react";
3
3
  import { Switch } from "@/components/ui/switch";
4
4
  import type { UsageInfo, LimitBucket } from "../../../types/chat";
5
5
  import {
@@ -7,6 +7,7 @@ import {
7
7
  getActiveAccount,
8
8
  getAllAccountUsages,
9
9
  patchAccount,
10
+ deleteAccount,
10
11
  type AccountInfo,
11
12
  type AccountUsageEntry,
12
13
  type OAuthProfileData,
@@ -152,11 +153,12 @@ function formatLastUpdated(ts: number | null | undefined): string | null {
152
153
  return `${days}d ago`;
153
154
  }
154
155
 
155
- function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, onViewProfile, flash }: {
156
+ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onDelete, onExport, onViewProfile, flash }: {
156
157
  entry: AccountUsageEntry;
157
158
  isActive: boolean;
158
159
  accountInfo?: AccountInfo;
159
160
  onToggle?: (id: string, status: string) => void;
161
+ onDelete?: (id: string, display: string) => void;
160
162
  onExport?: (id: string) => void;
161
163
  onViewProfile?: (profile: OAuthProfileData) => void;
162
164
  flash?: boolean;
@@ -164,19 +166,24 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
164
166
  const { usage } = entry;
165
167
  const hasBuckets = usage.session || usage.weekly || usage.weeklyOpus || usage.weeklySonnet;
166
168
  const status = accountInfo?.status ?? entry.accountStatus;
169
+ // Expired: has expiresAt in the past AND no refresh token to auto-renew
170
+ const isExpired = !!(accountInfo && !accountInfo.hasRefreshToken && accountInfo.expiresAt && accountInfo.expiresAt < Math.floor(Date.now() / 1000));
167
171
 
168
172
  return (
169
- <div className={`rounded-md border p-2 space-y-1.5 transition-colors duration-500 min-w-[200px] shrink-0 snap-start ${flash ? "bg-primary/10 border-primary/40" : ""} ${isActive ? "border-primary/30 bg-primary/5" : "border-border/50"}`}>
173
+ <div className={`rounded-md border p-2 space-y-1.5 transition-colors duration-500 min-w-[200px] shrink-0 snap-start ${isExpired ? "opacity-50" : ""} ${flash ? "bg-primary/10 border-primary/40" : ""} ${isActive ? "border-primary/30 bg-primary/5" : "border-border/50"}`}>
170
174
  <div className="flex items-center gap-1.5">
171
175
  <span className="text-xs font-medium truncate flex-1 min-w-0">
172
176
  {entry.accountLabel ?? entry.accountId.slice(0, 8)}
173
177
  </span>
174
- {!entry.isOAuth && (
178
+ {isExpired && (
179
+ <span className="text-[9px] text-red-500 shrink-0 font-medium">Expired</span>
180
+ )}
181
+ {!entry.isOAuth && !isExpired && (
175
182
  <span className="text-[9px] text-text-subtle shrink-0">API key</span>
176
183
  )}
177
184
  {/* Account controls */}
178
185
  <div className="flex items-center gap-0.5 shrink-0">
179
- {onViewProfile && accountInfo?.profileData && (
186
+ {!isExpired && onViewProfile && accountInfo?.profileData && (
180
187
  <button
181
188
  className="p-1 rounded cursor-pointer text-text-subtle hover:text-foreground hover:bg-surface-elevated transition-colors"
182
189
  onClick={() => onViewProfile(accountInfo.profileData!)}
@@ -185,7 +192,7 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
185
192
  <Eye className="size-3" />
186
193
  </button>
187
194
  )}
188
- {onExport && entry.isOAuth && (
195
+ {!isExpired && onExport && entry.isOAuth && (
189
196
  <button
190
197
  className="p-1 rounded cursor-pointer text-text-subtle hover:text-blue-500 hover:bg-surface-elevated transition-colors"
191
198
  onClick={() => onExport(entry.accountId)}
@@ -194,7 +201,7 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
194
201
  <Download className="size-3" />
195
202
  </button>
196
203
  )}
197
- {onToggle && (
204
+ {!isExpired && onToggle && (
198
205
  <Switch
199
206
  checked={status !== "disabled"}
200
207
  onCheckedChange={() => onToggle(entry.accountId, status)}
@@ -202,6 +209,15 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
202
209
  className="scale-[0.6] cursor-pointer"
203
210
  />
204
211
  )}
212
+ {onDelete && (
213
+ <button
214
+ className="p-1 rounded cursor-pointer text-text-subtle hover:text-red-500 hover:bg-surface-elevated transition-colors"
215
+ onClick={() => onDelete(entry.accountId, entry.accountLabel ?? entry.accountId.slice(0, 8))}
216
+ title="Remove account"
217
+ >
218
+ <Trash2 className="size-3" />
219
+ </button>
220
+ )}
205
221
  </div>
206
222
  </div>
207
223
  {hasBuckets ? (
@@ -247,6 +263,7 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
247
263
  const [showExportDialog, setShowExportDialog] = useState(false);
248
264
  const [showImportDialog, setShowImportDialog] = useState(false);
249
265
  const [showRotationSettings, setShowRotationSettings] = useState(false);
266
+ const [deleteTarget, setDeleteTarget] = useState<{ id: string; display: string } | null>(null);
250
267
  const [exportPreselect, setExportPreselect] = useState<string | null>(null);
251
268
  const [message, setMessage] = useState<string | null>(null);
252
269
  const msgTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
@@ -325,13 +342,26 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
325
342
  onReload?.();
326
343
  }
327
344
 
345
+ async function confirmDeleteAccount() {
346
+ if (!deleteTarget) return;
347
+ try {
348
+ await deleteAccount(deleteTarget.id);
349
+ showMessage(`Account "${deleteTarget.display}" removed.`);
350
+ loadAll();
351
+ onReload?.();
352
+ } catch (e) {
353
+ showMessage(`Failed to remove: ${(e as Error).message}`);
354
+ }
355
+ setDeleteTarget(null);
356
+ }
357
+
328
358
  function openExportAll() {
329
359
  setExportPreselect(null);
330
360
  setShowExportDialog(true);
331
361
  }
332
362
 
333
363
  return (
334
- <div className="border-b border-border bg-surface px-3 py-2.5 space-y-2.5 max-h-[350px] overflow-y-auto">
364
+ <div className="relative border-b border-border bg-surface px-3 py-2.5 space-y-2.5 max-h-[350px] overflow-y-auto">
335
365
  <div className="flex items-center justify-between">
336
366
  <div className="flex items-center gap-2">
337
367
  <span className="text-xs font-semibold text-text-primary">Usage & Accounts</span>
@@ -384,6 +414,7 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
384
414
  isActive={entry.accountId === (activeAccountId ?? usage.activeAccountId)}
385
415
  accountInfo={accountMap.get(entry.accountId)}
386
416
  onToggle={handleToggle}
417
+ onDelete={(id, display) => setDeleteTarget({ id, display })}
387
418
  onExport={(id) => { setExportPreselect(id); setShowExportDialog(true); }}
388
419
  onViewProfile={setProfileView}
389
420
  flash={flashIds.has(entry.accountId)}
@@ -460,6 +491,25 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
460
491
  </button>
461
492
  </div>
462
493
 
494
+ {/* Delete confirmation overlay */}
495
+ {deleteTarget && (
496
+ <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm rounded-md">
497
+ <div className="bg-surface border border-border rounded-lg shadow-lg p-4 mx-4 max-w-[280px] w-full space-y-3">
498
+ <p className="text-xs text-text-primary text-center">
499
+ Remove <strong className="text-foreground">{deleteTarget.display}</strong>?
500
+ </p>
501
+ <div className="flex gap-2">
502
+ <button onClick={() => setDeleteTarget(null)} className="flex-1 px-3 py-1.5 rounded-md text-xs border border-border text-text-secondary hover:bg-surface-hover cursor-pointer transition-colors">
503
+ Cancel
504
+ </button>
505
+ <button onClick={confirmDeleteAccount} className="flex-1 px-3 py-1.5 rounded-md text-xs bg-red-500 text-white hover:bg-red-600 cursor-pointer transition-colors">
506
+ Remove
507
+ </button>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ )}
512
+
463
513
  {/* Account dialogs */}
464
514
  <AddAccountDialog open={showAddDialog} onOpenChange={setShowAddDialog} onSuccess={handleSuccess} />
465
515
  <ExportAccountsDialog open={showExportDialog} onOpenChange={(v) => { setShowExportDialog(v); if (!v) setExportPreselect(null); }} accounts={accounts} preselectId={exportPreselect} onMessage={showMessage} />