@hienlh/ppm 0.20.1 → 0.20.3
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 +21 -0
- package/CLAUDE.md +1 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +5 -1
- package/dist/web/assets/accounts-settings-section-DzDmIsPe.js +1 -0
- package/dist/web/assets/{ai-resource-editor-BLctP1Pr.js → ai-resource-editor-D7C9wmwA.js} +1 -1
- package/dist/web/assets/{ai-settings-section-0hSDp7xx.js → ai-settings-section-BW5a07-Z.js} +1 -1
- package/dist/web/assets/{ai-settings-section-CTEU2uGa.js → ai-settings-section-DZuwllLy.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-BkcoSV0E.js → appearance-settings-section-RfH2N1rj.js} +1 -1
- package/dist/web/assets/chat-tab-Bz0pk0_E.js +15 -0
- package/dist/web/assets/{code-editor-C4kKhamq.js → code-editor-C4o1SyuO.js} +2 -2
- package/dist/web/assets/{conflict-editor-BEULKwHk.js → conflict-editor-Dk65mBIe.js} +1 -1
- package/dist/web/assets/{csv-preview-k3eM8nFf.js → csv-preview-DENi3CFz.js} +1 -1
- package/dist/web/assets/{database-viewer-Cslir1dr.js → database-viewer-BiBC7ulB.js} +1 -1
- package/dist/web/assets/{diff-viewer-DzAIvBbn.js → diff-viewer-CSvZJHak.js} +1 -1
- package/dist/web/assets/{explorer-body-D6H-4cz9.js → explorer-body-QXS0h4AF.js} +1 -1
- package/dist/web/assets/{explorer-window-content-C5W_JzCz.js → explorer-window-content-CQgqoRsj.js} +1 -1
- package/dist/web/assets/{extension-manager-section-DwicI3CA.js → extension-manager-section-CyC7wX35.js} +1 -1
- package/dist/web/assets/{extension-webview-Z3HhC8Pd.js → extension-webview-Da8SIlRa.js} +1 -1
- package/dist/web/assets/{files-settings-section-ByV7bRbB.js → files-settings-section-D8cebM51.js} +1 -1
- package/dist/web/assets/{general-settings-section-nLksKHur.js → general-settings-section-1HVTvlIw.js} +1 -1
- package/dist/web/assets/{git-log-panel-iUXltiTB.js → git-log-panel-C7a4rp8f.js} +1 -1
- package/dist/web/assets/{glide-data-grid-C1QB4DHt.js → glide-data-grid-BT9p_4AN.js} +1 -1
- package/dist/web/assets/{group-chat-tab-ezXKVVpg.js → group-chat-tab-BUHGnUyW.js} +1 -1
- package/dist/web/assets/index-CuYtJ5xF.css +2 -0
- package/dist/web/assets/{index-CJWbFurD.js → index-kGmcC9cb.js} +5 -5
- package/dist/web/assets/{jira-watcher-section-DwARwskB.js → jira-watcher-section-BxzwW4uQ.js} +1 -1
- package/dist/web/assets/{keybindings-store-S7YLGd8B.js → keybindings-store-DjJum4OS.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-BoUDvdDy.js → keyboard-shortcuts-section-DDKG3BU1.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DKdfiSIX.js → markdown-renderer-dHeak2mE.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DYFmMC_8.js → markdown-renderer-iBw1fCI_.js} +1 -1
- package/dist/web/assets/{mobile-explorer-sheet-CMdqLCV-.js → mobile-explorer-sheet-CzL8SLtz.js} +1 -1
- package/dist/web/assets/{notification-store-DpSYFj36.js → notification-store-DVi6Ds71.js} +1 -1
- package/dist/web/assets/{notifications-settings-section-CIf3-3xd.js → notifications-settings-section-shQ-ugKh.js} +1 -1
- package/dist/web/assets/{pdf-preview-C-7IdkIK.js → pdf-preview-DB26KSzm.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BYXFYlLg.js → postgres-viewer-CJe2meyF.js} +1 -1
- package/dist/web/assets/{ppmbot-settings-section-B9dXvQ6p.js → ppmbot-settings-section-Ba4c9AI9.js} +1 -1
- package/dist/web/assets/{process-table-B6NeTWt8.js → process-table-PXwLsMuG.js} +1 -1
- package/dist/web/assets/{proxy-settings-section-CUqq7pdU.js → proxy-settings-section-BHBhK6HC.js} +1 -1
- package/dist/web/assets/{query-audit-section-C1TYrPIp.js → query-audit-section-BD5ZVqDz.js} +1 -1
- package/dist/web/assets/{remote-desktop-mobile-sheet-DuKwKe5F.js → remote-desktop-mobile-sheet-Di4QG8VX.js} +2 -2
- package/dist/web/assets/{remote-desktop-mobile-view-DxMTtsa6.js → remote-desktop-mobile-view-DzoZvAqc.js} +1 -1
- package/dist/web/assets/{remote-desktop-readiness-gate-BwjfLWqg.js → remote-desktop-readiness-gate-CHO-KJ9Z.js} +1 -1
- package/dist/web/assets/{remote-desktop-window-content-BIl4N5Hn.js → remote-desktop-window-content-X324gYbW.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-C0qOb7GI.js → schedules-settings-section-a4-h6THc.js} +1 -1
- package/dist/web/assets/{settings-body-qbrJXX_A.js → settings-body-Fq7BVDw2.js} +2 -2
- package/dist/web/assets/{settings-tab-BKRgYVxf.js → settings-tab-CVLQy-ie.js} +1 -1
- package/dist/web/assets/{settings-window-content-DWsxJDu-.js → settings-window-content-Bi7M0iaZ.js} +1 -1
- package/dist/web/assets/{sql-query-editor-CsZOpCW3.js → sql-query-editor-D1r265fl.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DUrnCaP7.js → sqlite-viewer-DHkIotRl.js} +1 -1
- package/dist/web/assets/{system-monitor-body-C5cMpg7I.js → system-monitor-body-y6VUkYTL.js} +2 -2
- package/dist/web/assets/{system-monitor-tab-C66ywbVZ.js → system-monitor-tab-DRAOejhk.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-CENaXYJ-.js → system-monitor-window-content-B5i57xHD.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-HViABGoS.js → tab-host-window-content-SkyPFFlc.js} +1 -1
- package/dist/web/assets/{team-member-sheet-CK6eyBRL.js → team-member-sheet-BB63u-Lf.js} +2 -2
- package/dist/web/assets/{team-member-window-content-D6wxg37P.js → team-member-window-content-D6tVTXuV.js} +1 -1
- package/dist/web/assets/{terminal-tab-BmyTawmN.js → terminal-tab-BYaHAkI5.js} +1 -1
- package/dist/web/assets/{tool-cards-y-XJcJHo.js → tool-cards-DRJu20sL.js} +2 -2
- package/dist/web/assets/use-accounts-data-Ba1_uvEE.js +1 -0
- package/dist/web/assets/{use-monaco-theme-BkM5rMgL.js → use-monaco-theme-Uakgj3hJ.js} +1 -1
- package/dist/web/assets/{use-remote-desktop-display-choice-BT2r1td1.js → use-remote-desktop-display-choice-Cd3Xalca.js} +1 -1
- package/dist/web/assets/{use-websocket-BvVTwMjI.js → use-websocket-BC_EGf5q.js} +1 -1
- package/dist/web/assets/{video-preview-BEaofjN7.js → video-preview-BUb43zWb.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/deployment-guide.md +10 -0
- package/docs/lessons-learned.md +14 -0
- package/package.json +1 -1
- package/src/providers/codex-app-server/codex-event-mapper.ts +11 -0
- package/src/providers/codex-app-server/codex-history.ts +118 -45
- package/src/providers/codex-app-server/codex-provider.ts +277 -15
- package/src/providers/codex-app-server/codex-rollout-header.ts +99 -0
- package/src/providers/codex-app-server/codex-rollout-items.ts +6 -0
- package/src/providers/codex-app-server/codex-rollout-transfer.ts +58 -0
- package/src/providers/codex-app-server/codex-subagent-thread.ts +103 -0
- package/src/providers/codex-app-server/codex-usage-limit.ts +69 -0
- package/src/providers/codex-app-server/codex-usage-parser.ts +31 -14
- package/src/server/routes/chat.ts +23 -2
- package/src/server/routes/codex-accounts.ts +32 -3
- package/src/server/ws/chat.ts +15 -11
- package/src/services/codex-account-cooldown.ts +56 -0
- package/src/services/codex-account-login.ts +109 -34
- package/src/services/codex-account.service.ts +56 -10
- package/src/services/codex-login-callback.ts +57 -0
- package/src/web/components/chat/chat-tab.tsx +8 -2
- package/src/web/components/chat/codex-usage-panel.tsx +129 -165
- package/src/web/components/chat/usage-badge.tsx +38 -129
- package/src/web/components/chat/usage-panel-shell.tsx +153 -0
- package/src/web/components/settings/accounts/account-card.tsx +20 -3
- package/src/web/components/settings/accounts/codex-accounts-section.tsx +9 -2
- package/src/web/components/settings/accounts/codex-add-account-dialog.tsx +47 -27
- package/src/web/components/settings/accounts/codex-usage-rows.tsx +17 -6
- package/src/web/components/settings/accounts/use-codex-accounts.ts +90 -44
- package/src/web/components/ui/tooltip.tsx +14 -1
- package/src/web/hooks/use-usage.ts +13 -1
- package/dist/web/assets/accounts-settings-section-_sG1AqtH.js +0 -1
- package/dist/web/assets/chat-tab-CJi4uYb9.js +0 -15
- package/dist/web/assets/index-CoaVwAF6.css +0 -2
- package/dist/web/assets/use-accounts-data-BbOb2QNP.js +0 -1
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat header's Codex usage panel.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately the same panel as the Claude one, not a lookalike: the frame comes from
|
|
5
|
+
* `UsagePanelShell` and every card is the `AccountCard` that Settings renders. It used to
|
|
6
|
+
* draw both itself, which is how the two ended up with cards of different shapes stacked in
|
|
7
|
+
* different directions — the drift the shared Settings layout exists to stop.
|
|
8
|
+
*
|
|
9
|
+
* What stays Codex-specific is only what genuinely differs: its accounts come from
|
|
10
|
+
* `/api/codex-accounts` rather than the Claude pool, an account is a CODEX_HOME rather than
|
|
11
|
+
* an OAuth grant so there is no token to expire, and a plan may have no 5-hour window at
|
|
12
|
+
* all — so the quota rows follow what the account actually reports.
|
|
13
|
+
*/
|
|
14
|
+
|
|
1
15
|
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import {
|
|
16
|
+
import { Loader2 } from "lucide-react";
|
|
3
17
|
import { api } from "@/lib/api-client";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
18
|
+
import { AccountCard } from "@/components/settings/accounts/account-card";
|
|
19
|
+
import { AccountCardShell } from "@/components/settings/accounts/accounts-pane-header";
|
|
20
|
+
import { AccountBucketRow } from "@/components/settings/accounts/account-bucket-row";
|
|
7
21
|
import { formatResetTime } from "@/components/settings/accounts/account-usage-format";
|
|
22
|
+
import type { AccountUsageEntry } from "@/lib/api-settings";
|
|
8
23
|
import type { UsageInfo } from "../../../types/chat";
|
|
24
|
+
import { UsagePanelShell } from "./usage-panel-shell";
|
|
9
25
|
|
|
10
26
|
interface CodexAccount { id: string; label: string; type: string; planType?: string | null; status?: "active" | "disabled" }
|
|
11
27
|
type Usage = Pick<UsageInfo, "fiveHour" | "sevenDay" | "session" | "weekly">;
|
|
@@ -15,20 +31,23 @@ function atCap(util: number | null | undefined): boolean {
|
|
|
15
31
|
return Math.round((util ?? 0) * 100) >= 100;
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
/**
|
|
35
|
+
* A Codex account in the shape every account card speaks.
|
|
36
|
+
*
|
|
37
|
+
* `isOAuth` carries the account kind rather than an OAuth fact: a `chatgpt` account is a
|
|
38
|
+
* sign-in and an `apiKey` one is a static key, which is exactly the distinction the card's
|
|
39
|
+
* "API key" badge already draws.
|
|
40
|
+
*/
|
|
41
|
+
function toEntry(account: CodexAccount, usage: Usage): AccountUsageEntry {
|
|
42
|
+
return {
|
|
43
|
+
accountId: account.id,
|
|
44
|
+
accountLabel: account.label,
|
|
45
|
+
accountStatus: account.status ?? "active",
|
|
46
|
+
isOAuth: account.type !== "apiKey",
|
|
47
|
+
usage: { session: usage.session, weekly: usage.weekly },
|
|
48
|
+
};
|
|
25
49
|
}
|
|
26
50
|
|
|
27
|
-
/** Read-only usage panel opened from the chat toolbar badge (Claude parity).
|
|
28
|
-
* Shows every managed Codex account's 5h/weekly utilization. When no managed
|
|
29
|
-
* accounts exist, chats run on the ambient ~/.codex login — its usage comes
|
|
30
|
-
* from the session `usage` prop. Login/management lives in Settings → AI
|
|
31
|
-
* Provider → Codex. */
|
|
32
51
|
export function CodexUsagePanel({ onClose, usage, onReload, onSelectAccount, selectedAccountId }: {
|
|
33
52
|
onClose: () => void;
|
|
34
53
|
usage: UsageInfo;
|
|
@@ -44,43 +63,7 @@ export function CodexUsagePanel({ onClose, usage, onReload, onSelectAccount, sel
|
|
|
44
63
|
const [loading, setLoading] = useState(false);
|
|
45
64
|
const [selectingId, setSelectingId] = useState<string | null>(null);
|
|
46
65
|
const [togglingId, setTogglingId] = useState<string | null>(null);
|
|
47
|
-
const [
|
|
48
|
-
|
|
49
|
-
// This panel draws its own cards rather than reusing Settings' AccountCard, so the
|
|
50
|
-
// selection control is rebuilt here. Kept visually the same as the Claude panel's on
|
|
51
|
-
// purpose — the two sub-tabs drifting apart is the thing the shared Settings layout was
|
|
52
|
-
// introduced to stop, and a differently-shaped button here would start it again.
|
|
53
|
-
const select = useCallback(async (id: string, label: string, refused: string | null) => {
|
|
54
|
-
if (!onSelectAccount) return;
|
|
55
|
-
// Refuse here rather than leaving the button inert: the user pressed something and is
|
|
56
|
-
// owed the reason. The server checks again, which catches an account that became
|
|
57
|
-
// unusable between this render and the click.
|
|
58
|
-
if (refused) { setToggleError(`Cannot switch to this account — ${refused}`); return; }
|
|
59
|
-
setSelectingId(id);
|
|
60
|
-
setToggleError(null);
|
|
61
|
-
try { setToggleError((await onSelectAccount(id, label)) ?? null); } finally { setSelectingId(null); }
|
|
62
|
-
}, [onSelectAccount]);
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Switch a Codex account on or off without leaving the chat.
|
|
66
|
-
*
|
|
67
|
-
* No pending-token dance like the Claude side: the Codex route answers immediately because
|
|
68
|
-
* there is no refresh token to prove. The pending id is still tracked so the switch cannot
|
|
69
|
-
* be double-fired on a slow connection.
|
|
70
|
-
*/
|
|
71
|
-
const toggle = useCallback(async (id: string, status: string) => {
|
|
72
|
-
setTogglingId(id);
|
|
73
|
-
setToggleError(null);
|
|
74
|
-
try {
|
|
75
|
-
await api.patch(`/api/codex-accounts/${id}`, { status: status === "disabled" ? "active" : "disabled" });
|
|
76
|
-
const d = await api.get<{ accounts: CodexAccount[] }>("/api/codex-accounts");
|
|
77
|
-
setAccounts(d.accounts);
|
|
78
|
-
} catch (e) {
|
|
79
|
-
setToggleError((e as Error).message || "Could not change the account");
|
|
80
|
-
} finally {
|
|
81
|
-
setTogglingId(null);
|
|
82
|
-
}
|
|
83
|
-
}, []);
|
|
66
|
+
const [panelError, setPanelError] = useState<string | null>(null);
|
|
84
67
|
|
|
85
68
|
const load = useCallback(async () => {
|
|
86
69
|
setLoading(true);
|
|
@@ -104,126 +87,107 @@ export function CodexUsagePanel({ onClose, usage, onReload, onSelectAccount, sel
|
|
|
104
87
|
} catch { /* retain the last successful reading */ } finally { setLoading(false); }
|
|
105
88
|
}, [onReload]);
|
|
106
89
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<RefreshCw className={`size-3 ${loading ? "animate-spin" : ""}`} />
|
|
121
|
-
</button>
|
|
122
|
-
</TooltipTrigger>
|
|
123
|
-
<TooltipContent side="top">Refresh</TooltipContent>
|
|
124
|
-
</Tooltip>
|
|
125
|
-
<Tooltip>
|
|
126
|
-
<TooltipTrigger asChild>
|
|
127
|
-
<button
|
|
128
|
-
onClick={onClose}
|
|
129
|
-
className="text-text-subtle hover:text-text-primary px-1 cursor-pointer"
|
|
130
|
-
aria-label="Close usage panel"
|
|
131
|
-
>
|
|
132
|
-
<X className="size-3" />
|
|
133
|
-
</button>
|
|
134
|
-
</TooltipTrigger>
|
|
135
|
-
<TooltipContent side="top">Close</TooltipContent>
|
|
136
|
-
</Tooltip>
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
90
|
+
/**
|
|
91
|
+
* Why an account cannot take this chat, in the words the card shows.
|
|
92
|
+
*
|
|
93
|
+
* Reached, not approaching: an account at 96% still answers, one at 100% does not. Both
|
|
94
|
+
* windows are checked because a plan may have only one of them, and whichever it has is
|
|
95
|
+
* the one that refuses the turn.
|
|
96
|
+
*/
|
|
97
|
+
const unselectableReason = useCallback((account: CodexAccount, u: Usage): string | null => {
|
|
98
|
+
if (account.status === "disabled") return "Disabled";
|
|
99
|
+
if (atCap(u.fiveHour)) return `5-hour limit reached${u.session?.resetsAt ? ` — resets ${formatResetTime(u.session)}` : ""}`;
|
|
100
|
+
if (atCap(u.sevenDay)) return `Weekly limit reached${u.weekly?.resetsAt ? ` — resets ${formatResetTime(u.weekly)}` : ""}`;
|
|
101
|
+
return null;
|
|
102
|
+
}, []);
|
|
139
103
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
104
|
+
const handleSelect = useCallback(async (accountId: string) => {
|
|
105
|
+
if (!onSelectAccount) return;
|
|
106
|
+
const account = accounts.find((a) => a.id === accountId);
|
|
107
|
+
if (!account) return;
|
|
108
|
+
// Refuse here rather than leaving the button inert: the user pressed something and is
|
|
109
|
+
// owed the reason. The server checks again, which catches an account that became
|
|
110
|
+
// unusable between this render and the click.
|
|
111
|
+
const refused = unselectableReason(account, usages[accountId] ?? {});
|
|
112
|
+
if (refused) { setPanelError(`Cannot switch to this account — ${refused.toLowerCase()}.`); return; }
|
|
113
|
+
setSelectingId(accountId);
|
|
114
|
+
setPanelError(null);
|
|
115
|
+
try { setPanelError((await onSelectAccount(accountId, account.label)) ?? null); }
|
|
116
|
+
finally { setSelectingId(null); }
|
|
117
|
+
}, [onSelectAccount, accounts, usages, unselectableReason]);
|
|
148
118
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Switch a Codex account on or off without leaving the chat.
|
|
121
|
+
*
|
|
122
|
+
* No pending-token dance like the Claude side: the Codex route answers immediately because
|
|
123
|
+
* there is no refresh token to prove. The pending id is still tracked so the switch cannot
|
|
124
|
+
* be double-fired on a slow connection.
|
|
125
|
+
*/
|
|
126
|
+
const handleToggle = useCallback(async (id: string, status: string) => {
|
|
127
|
+
setTogglingId(id);
|
|
128
|
+
setPanelError(null);
|
|
129
|
+
try {
|
|
130
|
+
await api.patch(`/api/codex-accounts/${id}`, { status: status === "disabled" ? "active" : "disabled" });
|
|
131
|
+
const d = await api.get<{ accounts: CodexAccount[] }>("/api/codex-accounts");
|
|
132
|
+
setAccounts(d.accounts);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
setPanelError((e as Error).message || "Could not change the account");
|
|
135
|
+
} finally {
|
|
136
|
+
setTogglingId(null);
|
|
137
|
+
}
|
|
138
|
+
}, []);
|
|
152
139
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
140
|
+
/* No managed accounts → chats run on the ambient ~/.codex login, whose usage arrives on
|
|
141
|
+
the session prop rather than from the accounts endpoint. */
|
|
142
|
+
const ambient = loading && accounts.length === 0 ? (
|
|
143
|
+
<div className="text-xs text-text-subtle flex items-center gap-2"><Loader2 className="size-3 animate-spin" /> Loading…</div>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<AccountCardShell active={false}>
|
|
147
|
+
<div className="flex items-center gap-2">
|
|
148
|
+
<span className="text-sm font-medium text-text-primary truncate flex-1 min-w-0">{usage.activeAccountLabel || "Default login"}</span>
|
|
149
|
+
<span className="text-[10px] uppercase tracking-wide text-text-subtle border border-border rounded px-1 shrink-0">~/.codex</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="space-y-2">
|
|
152
|
+
<AccountBucketRow label="5-Hour Session" bucket={usage.session} />
|
|
153
|
+
<AccountBucketRow label="Weekly" bucket={usage.weekly} />
|
|
154
|
+
{!usage.session && !usage.weekly && <p className="text-xs text-text-subtle">No usage data yet</p>}
|
|
155
|
+
</div>
|
|
156
|
+
</AccountCardShell>
|
|
157
|
+
<p className="text-[11px] text-text-subtle">Using your default <code>~/.codex</code> login. Add managed accounts in Settings → AI Provider → Codex.</p>
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
167
160
|
|
|
168
|
-
|
|
161
|
+
return (
|
|
162
|
+
<UsagePanelShell
|
|
163
|
+
title="Codex Usage"
|
|
164
|
+
onClose={onClose}
|
|
165
|
+
onReload={onReload ? () => void reload() : undefined}
|
|
166
|
+
reloading={loading}
|
|
167
|
+
error={panelError}
|
|
168
|
+
onDismissError={() => setPanelError(null)}
|
|
169
|
+
cardCount={accounts.length}
|
|
170
|
+
fallback={ambient}
|
|
171
|
+
>
|
|
172
|
+
{(layout) => accounts.map((a) => {
|
|
169
173
|
const u = usages[a.id] ?? {};
|
|
170
|
-
const isServing = a.id === (selectedAccountId ?? usage.activeAccountId);
|
|
171
|
-
// Reached, not approaching: an account at 96% still answers, one at 100% does not.
|
|
172
|
-
const refused = a.status === "disabled"
|
|
173
|
-
? "it is switched off."
|
|
174
|
-
: atCap(u.fiveHour)
|
|
175
|
-
? "it has reached its 5-hour limit."
|
|
176
|
-
: atCap(u.sevenDay)
|
|
177
|
-
? "it has reached its weekly limit."
|
|
178
|
-
: null;
|
|
179
174
|
return (
|
|
180
|
-
<
|
|
175
|
+
<AccountCard
|
|
181
176
|
key={a.id}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
onClick={() => select(a.id, a.label, refused)}
|
|
195
|
-
disabled={selectingId === a.id}
|
|
196
|
-
className={[
|
|
197
|
-
"shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors cursor-pointer disabled:cursor-wait",
|
|
198
|
-
refused
|
|
199
|
-
? "text-text-subtle hover:text-error hover:bg-error/10"
|
|
200
|
-
: "text-text-secondary hover:text-foreground hover:bg-surface-elevated",
|
|
201
|
-
].join(" ")}
|
|
202
|
-
>
|
|
203
|
-
{selectingId === a.id ? "Switching…" : "Use"}
|
|
204
|
-
</button>
|
|
205
|
-
</TooltipTrigger>
|
|
206
|
-
<TooltipContent side="top">
|
|
207
|
-
{refused ? `Cannot use this account — ${refused}` : "Use this account for this chat"}
|
|
208
|
-
</TooltipContent>
|
|
209
|
-
</Tooltip>
|
|
210
|
-
)}
|
|
211
|
-
{a.status === "disabled" && <span className="text-[10px] text-text-subtle shrink-0">Off</span>}
|
|
212
|
-
<span className="text-[10px] uppercase tracking-wide text-text-subtle border border-border rounded px-1">{a.type}</span>
|
|
213
|
-
{a.planType && <span className="text-[10px] text-text-subtle">{a.planType}</span>}
|
|
214
|
-
<Switch
|
|
215
|
-
checked={a.status !== "disabled"}
|
|
216
|
-
onCheckedChange={() => void toggle(a.id, a.status ?? "active")}
|
|
217
|
-
disabled={togglingId === a.id}
|
|
218
|
-
aria-label={a.status === "disabled" ? "Enable account" : "Disable account"}
|
|
219
|
-
className="cursor-pointer shrink-0"
|
|
220
|
-
/>
|
|
221
|
-
</div>
|
|
222
|
-
<UsageBar label="5-Hour" frac={u.fiveHour} bucket={u.session} />
|
|
223
|
-
<UsageBar label="Weekly" frac={u.sevenDay} bucket={u.weekly} />
|
|
224
|
-
</div>
|
|
177
|
+
entry={toEntry(a, u)}
|
|
178
|
+
// What is serving THIS chat, not what the rotation would pick next —
|
|
179
|
+
// those differ the moment a session is bound to an account.
|
|
180
|
+
isActive={a.id === (selectedAccountId ?? usage.activeAccountId)}
|
|
181
|
+
layout={layout}
|
|
182
|
+
planLabel={a.planType}
|
|
183
|
+
onSelect={onSelectAccount ? handleSelect : undefined}
|
|
184
|
+
unselectableReason={unselectableReason(a, u)}
|
|
185
|
+
selecting={selectingId === a.id}
|
|
186
|
+
onToggle={handleToggle}
|
|
187
|
+
toggling={togglingId === a.id}
|
|
188
|
+
/>
|
|
225
189
|
);
|
|
226
190
|
})}
|
|
227
|
-
</
|
|
191
|
+
</UsagePanelShell>
|
|
228
192
|
);
|
|
229
193
|
}
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { useState } from "react";
|
|
20
|
-
import { Activity,
|
|
20
|
+
import { Activity, RefreshCw } from "lucide-react";
|
|
21
21
|
import type { UsageInfo } from "../../../types/chat";
|
|
22
22
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
23
|
-
import { openSettings } from "@/components/settings/open-settings";
|
|
24
23
|
import { AccountCard } from "@/components/settings/accounts/account-card";
|
|
25
24
|
import { AccountBucketRow } from "@/components/settings/accounts/account-bucket-row";
|
|
26
25
|
import { useAccountsData } from "@/components/settings/accounts/use-accounts-data";
|
|
27
26
|
import { formatLastUpdated, formatResetTime, pctColor } from "@/components/settings/accounts/account-usage-format";
|
|
27
|
+
import { UsagePanelShell } from "./usage-panel-shell";
|
|
28
28
|
|
|
29
29
|
/** Matches the whole-percent figure the usage bars show, so a refusal agrees with the card. */
|
|
30
30
|
function atCap(util: number | null | undefined): boolean {
|
|
@@ -91,7 +91,6 @@ export function UsageDetailPanel({
|
|
|
91
91
|
// One strip for anything that went wrong in this panel — a refused switch or a refused
|
|
92
92
|
// toggle. Two separate messages in two places would be harder to notice, not clearer.
|
|
93
93
|
const [panelError, setPanelError] = useState<string | null>(null);
|
|
94
|
-
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
95
94
|
|
|
96
95
|
if (!visible) return null;
|
|
97
96
|
|
|
@@ -158,131 +157,21 @@ export function UsageDetailPanel({
|
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
const hasCost = usage.queryCostUsd != null || usage.totalCostUsd != null;
|
|
161
|
-
const hasPerAccountUsage = usages.length > 0;
|
|
162
|
-
|
|
163
|
-
// Roughly square, so the cards fill the viewport instead of leaving a long empty column.
|
|
164
|
-
const fsCount = usages.length || 1;
|
|
165
|
-
const fsCols = Math.ceil(Math.sqrt(fsCount));
|
|
166
|
-
const fsRows = Math.ceil(fsCount / fsCols);
|
|
167
160
|
|
|
168
161
|
return (
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
: "space-y-2.5 max-h-[350px] overflow-y-auto"
|
|
174
|
-
}`}
|
|
175
|
-
>
|
|
176
|
-
<div className="flex items-center justify-between shrink-0">
|
|
177
|
-
<div className="flex items-center gap-2">
|
|
178
|
-
<span className="text-xs font-semibold text-text-primary">Usage</span>
|
|
179
|
-
{lastFetchedAt && (
|
|
180
|
-
<span className="text-[10px] text-text-subtle">{formatLastUpdated(new Date(lastFetchedAt).getTime())}</span>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
<div className="flex items-center gap-1">
|
|
184
|
-
<Tooltip>
|
|
185
|
-
<TooltipTrigger asChild>
|
|
186
|
-
<button
|
|
187
|
-
onClick={() => openSettings("accounts")}
|
|
188
|
-
className="flex items-center gap-1 text-[10px] text-text-subtle hover:text-text-primary px-1 cursor-pointer"
|
|
189
|
-
>
|
|
190
|
-
Manage accounts <ExternalLink className="size-3" />
|
|
191
|
-
</button>
|
|
192
|
-
</TooltipTrigger>
|
|
193
|
-
<TooltipContent side="top">Add, remove or rotate accounts</TooltipContent>
|
|
194
|
-
</Tooltip>
|
|
195
|
-
{hasPerAccountUsage && (
|
|
196
|
-
<Tooltip>
|
|
197
|
-
<TooltipTrigger asChild>
|
|
198
|
-
<button
|
|
199
|
-
onClick={() => setIsFullscreen((v) => !v)}
|
|
200
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1 cursor-pointer"
|
|
201
|
-
aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen view"}
|
|
202
|
-
>
|
|
203
|
-
{isFullscreen ? <Minimize2 className="size-3" /> : <Maximize2 className="size-3" />}
|
|
204
|
-
</button>
|
|
205
|
-
</TooltipTrigger>
|
|
206
|
-
<TooltipContent side="top">{isFullscreen ? "Exit fullscreen" : "Fullscreen view"}</TooltipContent>
|
|
207
|
-
</Tooltip>
|
|
208
|
-
)}
|
|
209
|
-
{onReload && (
|
|
210
|
-
<Tooltip>
|
|
211
|
-
<TooltipTrigger asChild>
|
|
212
|
-
<button
|
|
213
|
-
onClick={() => { onReload(); void reload(); }}
|
|
214
|
-
disabled={loading || refreshing}
|
|
215
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1 disabled:opacity-50 cursor-pointer"
|
|
216
|
-
aria-label="Refresh usage"
|
|
217
|
-
>
|
|
218
|
-
<RefreshCw className={`size-3 ${(loading || refreshing) ? "animate-spin" : ""}`} />
|
|
219
|
-
</button>
|
|
220
|
-
</TooltipTrigger>
|
|
221
|
-
<TooltipContent side="top">Refresh</TooltipContent>
|
|
222
|
-
</Tooltip>
|
|
223
|
-
)}
|
|
224
|
-
<button
|
|
225
|
-
onClick={() => { setIsFullscreen(false); onClose(); }}
|
|
226
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1 cursor-pointer"
|
|
227
|
-
aria-label="Close usage panel"
|
|
228
|
-
>
|
|
229
|
-
<X className="size-3" />
|
|
230
|
-
</button>
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
|
|
234
|
-
{/* The server distinguishes "this login was rejected, sign in again" from "could not
|
|
235
|
-
reach Anthropic, try shortly", and the difference decides what the user should do.
|
|
236
|
-
Showing its words verbatim is the only way that survives to them. */}
|
|
237
|
-
{panelError && (
|
|
238
|
-
<div className="shrink-0 flex items-start gap-2 rounded border border-error/40 bg-error/10 px-2 py-1.5 text-[11px] text-error">
|
|
239
|
-
<span className="flex-1">{panelError}</span>
|
|
240
|
-
<button
|
|
241
|
-
onClick={() => setPanelError(null)}
|
|
242
|
-
className="shrink-0 text-error/70 hover:text-error cursor-pointer"
|
|
243
|
-
aria-label="Dismiss"
|
|
244
|
-
>
|
|
245
|
-
<X className="size-3" />
|
|
246
|
-
</button>
|
|
247
|
-
</div>
|
|
162
|
+
<UsagePanelShell
|
|
163
|
+
title="Usage"
|
|
164
|
+
meta={lastFetchedAt && (
|
|
165
|
+
<span className="text-[10px] text-text-subtle">{formatLastUpdated(new Date(lastFetchedAt).getTime())}</span>
|
|
248
166
|
)}
|
|
249
|
-
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
gridTemplateColumns: `repeat(${fsCols}, minmax(0, 1fr))`,
|
|
258
|
-
gridTemplateRows: `repeat(${fsRows}, minmax(0, 1fr))`,
|
|
259
|
-
} : undefined}
|
|
260
|
-
>
|
|
261
|
-
{initialLoading ? (
|
|
262
|
-
<p className="text-[10px] text-text-subtle">Loading...</p>
|
|
263
|
-
) : (
|
|
264
|
-
usages.map((entry) => {
|
|
265
|
-
const info = accountMap.get(entry.accountId);
|
|
266
|
-
return (
|
|
267
|
-
<AccountCard
|
|
268
|
-
key={entry.accountId}
|
|
269
|
-
entry={entry}
|
|
270
|
-
// What is serving THIS chat, not what the rotation would pick next —
|
|
271
|
-
// those differ the moment a session is bound to an account.
|
|
272
|
-
isActive={entry.accountId === (selectedAccountId ?? usage.activeAccountId ?? activeAccountId)}
|
|
273
|
-
accountInfo={info}
|
|
274
|
-
flash={flashIds.has(entry.accountId)}
|
|
275
|
-
layout={isFullscreen ? "grid" : "strip"}
|
|
276
|
-
onSelect={onSelectAccount ? handleSelect : undefined}
|
|
277
|
-
unselectableReason={unselectableReason(info, entry)}
|
|
278
|
-
selecting={selectingId === entry.accountId}
|
|
279
|
-
onToggle={handleToggle}
|
|
280
|
-
toggling={togglingId === entry.accountId}
|
|
281
|
-
/>
|
|
282
|
-
);
|
|
283
|
-
})
|
|
284
|
-
)}
|
|
285
|
-
</div>
|
|
167
|
+
onClose={onClose}
|
|
168
|
+
onReload={onReload ? () => { onReload(); void reload(); } : undefined}
|
|
169
|
+
reloading={loading || refreshing}
|
|
170
|
+
error={panelError}
|
|
171
|
+
onDismissError={() => setPanelError(null)}
|
|
172
|
+
cardCount={usages.length}
|
|
173
|
+
fallback={initialLoading ? (
|
|
174
|
+
<p className="text-[10px] text-text-subtle">Loading...</p>
|
|
286
175
|
) : usage.session || usage.weekly || usage.weeklyOpus || usage.weeklySonnet ? (
|
|
287
176
|
<div className="space-y-2.5">
|
|
288
177
|
<AccountBucketRow label="5-Hour Session" bucket={usage.session} />
|
|
@@ -293,8 +182,7 @@ export function UsageDetailPanel({
|
|
|
293
182
|
) : (
|
|
294
183
|
<p className="text-xs text-text-subtle">No usage data available</p>
|
|
295
184
|
)}
|
|
296
|
-
|
|
297
|
-
{hasCost && (
|
|
185
|
+
footer={hasCost ? (
|
|
298
186
|
<div className="border-t border-border pt-2 space-y-1">
|
|
299
187
|
{usage.queryCostUsd != null && (
|
|
300
188
|
<div className="flex items-center justify-between text-xs">
|
|
@@ -313,7 +201,28 @@ export function UsageDetailPanel({
|
|
|
313
201
|
</div>
|
|
314
202
|
)}
|
|
315
203
|
</div>
|
|
316
|
-
)}
|
|
317
|
-
|
|
204
|
+
) : undefined}
|
|
205
|
+
>
|
|
206
|
+
{(layout) => usages.map((entry) => {
|
|
207
|
+
const info = accountMap.get(entry.accountId);
|
|
208
|
+
return (
|
|
209
|
+
<AccountCard
|
|
210
|
+
key={entry.accountId}
|
|
211
|
+
entry={entry}
|
|
212
|
+
// What is serving THIS chat, not what the rotation would pick next —
|
|
213
|
+
// those differ the moment a session is bound to an account.
|
|
214
|
+
isActive={entry.accountId === (selectedAccountId ?? usage.activeAccountId ?? activeAccountId)}
|
|
215
|
+
accountInfo={info}
|
|
216
|
+
flash={flashIds.has(entry.accountId)}
|
|
217
|
+
layout={layout}
|
|
218
|
+
onSelect={onSelectAccount ? handleSelect : undefined}
|
|
219
|
+
unselectableReason={unselectableReason(info, entry)}
|
|
220
|
+
selecting={selectingId === entry.accountId}
|
|
221
|
+
onToggle={handleToggle}
|
|
222
|
+
toggling={togglingId === entry.accountId}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
})}
|
|
226
|
+
</UsagePanelShell>
|
|
318
227
|
);
|
|
319
228
|
}
|