@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.
- package/CHANGELOG.md +75 -40
- package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-3Xe18azI.js} +1 -1
- package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-Yy35llnn.js} +1 -1
- package/dist/web/assets/api-settings-Bid0NHuI.js +1 -0
- package/dist/web/assets/{arc-BAOivWpI.js → arc-B9n1Gvb5.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-DqAZP_F6.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-h3cDF2vI.js} +1 -1
- package/dist/web/assets/{browser-tab-DaHGm_0i.js → browser-tab-DSWumOSG.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
- package/dist/web/assets/channel-C2fMafck.js +1 -0
- package/dist/web/assets/chat-tab-Ccwf-c6M.js +8 -0
- package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C3aZvW7B.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-tDjHsAUs.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-C7Gry6md.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-av5aeHLq.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +1 -0
- package/dist/web/assets/clone-B2hUek6n.js +1 -0
- package/dist/web/assets/code-editor-DLTcPb55.js +2 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-qudEiMCT.js} +1 -1
- package/dist/web/assets/{csv-preview-DLqYtXxt.js → csv-preview-DUbHtTAS.js} +1 -1
- package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BFcnKyBF.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
- package/dist/web/assets/{database-viewer-DXk79Nel.js → database-viewer-BrpPlYG7.js} +1 -1
- package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
- package/dist/web/assets/diff-viewer-Dx96kcTu.js +4 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
- package/dist/web/assets/git-graph-CoN6voTp.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
- package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-Duh_bWLa.js} +1 -1
- package/dist/web/assets/index-CtbNK_ih.css +2 -0
- package/dist/web/assets/index-DRdx_Wqn.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +2 -0
- package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B9L-Ge-H.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
- package/dist/web/assets/keybindings-store-DHGoLYnP.js +1 -0
- package/dist/web/assets/{line-B78g-52T.js → line-B75-Rx70.js} +1 -1
- package/dist/web/assets/{linear-DP4mkX3m.js → linear-Bcjv9FQt.js} +1 -1
- package/dist/web/assets/{markdown-renderer-Brj8_LQM.js → markdown-renderer-BqsXIW9n.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-8u2leTXI.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
- package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-LFEjVtwQ.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CwkTGmqy.js → postgres-viewer-Lw8xaGfc.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-CdjGIDfw.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
- package/dist/web/assets/settings-tab-DDCC58we.js +1 -0
- package/dist/web/assets/{sqlite-viewer-CFYTwgA8.js → sqlite-viewer-DECA802J.js} +1 -1
- package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-C4EMl6jf.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +1 -0
- package/dist/web/assets/{tab-store-BJw7OCmy.js → tab-store--SlERlDs.js} +1 -1
- package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-DneNM6WP.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-A4PN_Efm.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +1 -0
- package/dist/web/assets/{use-monaco-theme-CNzekTN3.js → use-monaco-theme-CrtYAJMR.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
- package/dist/web/index.html +10 -11
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +155 -0
- package/docs/codebase-summary.md +261 -95
- package/docs/project-changelog.md +38 -3
- package/docs/project-roadmap.md +2 -2
- package/docs/streaming-input-guide.md +267 -0
- package/docs/system-architecture.md +151 -0
- package/package.json +1 -1
- package/snapshot-state.md +1526 -0
- package/src/providers/claude-agent-sdk.ts +244 -102
- package/src/providers/cli-provider-base.ts +238 -0
- package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
- package/src/providers/cursor-cli/cursor-history.ts +207 -0
- package/src/providers/cursor-cli/cursor-provider.ts +146 -0
- package/src/providers/mock-provider.ts +1 -1
- package/src/providers/provider.interface.ts +1 -0
- package/src/providers/registry.ts +43 -4
- package/src/server/index.ts +6 -0
- package/src/server/routes/chat.ts +14 -3
- package/src/server/routes/mcp.ts +84 -0
- package/src/server/routes/settings.ts +14 -0
- package/src/server/ws/chat.ts +127 -81
- package/src/services/account.service.ts +2 -2
- package/src/services/chat.service.ts +10 -15
- package/src/services/claude-usage.service.ts +2 -7
- package/src/services/db.service.ts +8 -0
- package/src/services/mcp-config.service.ts +111 -0
- package/src/types/api.ts +1 -1
- package/src/types/chat.ts +23 -2
- package/src/types/config.ts +33 -11
- package/src/types/mcp.ts +47 -0
- package/src/utils/ndjson-line-parser.ts +36 -0
- package/src/web/components/chat/chat-history-bar.tsx +48 -29
- package/src/web/components/chat/chat-tab.tsx +29 -24
- package/src/web/components/chat/message-input.tsx +64 -5
- package/src/web/components/chat/provider-selector.tsx +150 -0
- package/src/web/components/chat/session-picker.tsx +3 -1
- package/src/web/components/chat/usage-badge.tsx +58 -8
- package/src/web/components/settings/ai-settings-section.tsx +196 -137
- package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
- package/src/web/components/settings/mcp-settings-section.tsx +143 -0
- package/src/web/components/settings/settings-tab.tsx +5 -2
- package/src/web/hooks/use-chat.ts +32 -15
- package/src/web/lib/api-mcp.ts +38 -0
- package/test-tokens.mjs +212 -0
- package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
- package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
- package/dist/web/assets/channel-wrd-NHWf.js +0 -1
- package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
- package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
- package/dist/web/assets/clone-LRxlvnMj.js +0 -1
- package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
- package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
- package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
- package/dist/web/assets/index-CgQXpBb_.css +0 -2
- package/dist/web/assets/index-DEeeRoka.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
- package/dist/web/assets/input-BglMT33g.js +0 -1
- package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
- package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
- /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-BKIT_Qeg.js} +0 -0
- /package/dist/web/assets/{array-B9UHiPd-.js → array-DqLCdDFv.js} +0 -0
- /package/dist/web/assets/{chevron-right-DeV0ehiG.js → chevron-right-CHnjJt4E.js} +0 -0
- /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-DbesTfa7.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-CWPXKqbJ.js} +0 -0
- /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-CrJzLgRD.js} +0 -0
- /package/dist/web/assets/{dist-lF8CoYII.js → dist-CALwEtco.js} +0 -0
- /package/dist/web/assets/{dist-CSJdAyA9.js → dist-Cep75xXf.js} +0 -0
- /package/dist/web/assets/{dist-DylI9XxN.js → dist-DGDPTxs1.js} +0 -0
- /package/dist/web/assets/{init-DlZdxViB.js → init-C0r9Gk5G.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
- /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-DzXRfQ_m.js} +0 -0
- /package/dist/web/assets/{lib-BQ34Db2e.js → lib-BeaDXEkP.js} +0 -0
- /package/dist/web/assets/{math-069Z4SuC.js → math-y9zN1W-N.js} +0 -0
- /package/dist/web/assets/{path-6uRLdFF7.js → path-DIKpVbHL.js} +0 -0
- /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-Bf_JiD2A.js} +0 -0
- /package/dist/web/assets/{react-ER-4DN55.js → react-SKk5z-bm.js} +0 -0
- /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-nHaDi0Kw.js} +0 -0
- /package/dist/web/assets/{src-BqX54PbV.js → src-Dw4QhedI.js} +0 -0
- /package/dist/web/assets/{table-C7X5UAEI.js → table-CQVQM2SB.js} +0 -0
- /package/dist/web/assets/{tag-CCtdV063.js → tag-Q2dZiSPX.js} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
{
|
|
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} />
|