@hienlh/ppm 0.7.16 → 0.7.17
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 +15 -0
- package/bunfig.toml +2 -0
- package/dist/web/assets/chat-tab-C0AcTU9S.js +7 -0
- package/dist/web/assets/{code-editor-DXqocnye.js → code-editor-CYt4hqge.js} +1 -1
- package/dist/web/assets/{database-viewer-ChX5vA56.js → database-viewer-CDto4TrW.js} +1 -1
- package/dist/web/assets/{diff-viewer-8RNfSVOl.js → diff-viewer-2zSPeCzX.js} +1 -1
- package/dist/web/assets/git-graph-HfH98qwn.js +1 -0
- package/dist/web/assets/index-CTOMzCnZ.js +28 -0
- package/dist/web/assets/index-D6GLlwUx.css +2 -0
- package/dist/web/assets/keybindings-store-DrjDQzVs.js +1 -0
- package/dist/web/assets/{markdown-renderer-BOHSi1fK.js → markdown-renderer-dqkYhU3y.js} +1 -1
- package/dist/web/assets/{postgres-viewer-DRo3924t.js → postgres-viewer-kqZBNVYW.js} +1 -1
- package/dist/web/assets/settings-tab-jhRBFgf_.js +1 -0
- package/dist/web/assets/{sqlite-viewer-0iVQjCmF.js → sqlite-viewer-C2744fw1.js} +1 -1
- package/dist/web/assets/switch-PAf5UhcN.js +1 -0
- package/dist/web/assets/{terminal-tab-Cuznr8Lg.js → terminal-tab-BDqc6Dl5.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/routes/accounts.ts +60 -6
- package/src/services/account.service.ts +180 -17
- package/src/services/claude-usage.service.ts +9 -2
- package/src/services/db.service.ts +25 -3
- package/src/web/components/chat/chat-history-bar.tsx +2 -1
- package/src/web/components/chat/message-list.tsx +4 -2
- package/src/web/components/chat/usage-badge.tsx +118 -22
- package/src/web/components/settings/accounts-settings-section.tsx +268 -33
- package/src/web/lib/api-settings.ts +49 -0
- package/src/web/styles/globals.css +7 -0
- package/test-claude-oauth-v2.mjs +165 -0
- package/test-claude-oauth.mjs +175 -0
- package/test-verify-oat.mjs +106 -0
- package/dist/web/assets/ai-settings-section-BxCMGg-I.js +0 -1
- package/dist/web/assets/chat-tab-DtIaMWNT.js +0 -7
- package/dist/web/assets/git-graph-DYbWcg6M.js +0 -1
- package/dist/web/assets/index-BzhcIgja.js +0 -28
- package/dist/web/assets/index-sMxUHxFZ.css +0 -2
- package/dist/web/assets/keybindings-store-DBQQ_pTh.js +0 -1
- package/dist/web/assets/settings-tab-6ytjTMb9.js +0 -1
|
@@ -141,8 +141,10 @@ function MessageBubble({ message, isStreaming, projectName, onFork }: { message:
|
|
|
141
141
|
<MarkdownContent content={message.content} projectName={projectName} />
|
|
142
142
|
</div>
|
|
143
143
|
)}
|
|
144
|
-
{message.accountLabel && (
|
|
145
|
-
<p className="text-[
|
|
144
|
+
{!isStreaming && message.accountLabel && (
|
|
145
|
+
<p className="text-[10px] select-none" style={{ color: "var(--color-text-subtle)" }}>
|
|
146
|
+
via {message.accountLabel}
|
|
147
|
+
</p>
|
|
146
148
|
)}
|
|
147
149
|
</div>
|
|
148
150
|
);
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
-
import { Activity, RefreshCw } from "lucide-react";
|
|
2
|
+
import { Activity, RefreshCw, Eye, ShieldCheck, Loader2, X } from "lucide-react";
|
|
3
|
+
import { Switch } from "@/components/ui/switch";
|
|
3
4
|
import type { UsageInfo, LimitBucket } from "../../../types/chat";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getAccounts,
|
|
7
|
+
getActiveAccount,
|
|
8
|
+
getAllAccountUsages,
|
|
9
|
+
patchAccount,
|
|
10
|
+
verifyAccount,
|
|
11
|
+
type AccountInfo,
|
|
12
|
+
type AccountUsageEntry,
|
|
13
|
+
type OAuthProfileData,
|
|
14
|
+
} from "../../lib/api-settings";
|
|
5
15
|
|
|
6
16
|
interface UsageBadgeProps {
|
|
7
17
|
usage: UsageInfo;
|
|
@@ -111,19 +121,30 @@ function formatLastUpdated(ts: number | null | undefined): string | null {
|
|
|
111
121
|
if (secs < 5) return "just now";
|
|
112
122
|
if (secs < 60) return `${secs}s ago`;
|
|
113
123
|
const mins = Math.floor(secs / 60);
|
|
114
|
-
return `${mins}m ago`;
|
|
124
|
+
if (mins < 60) return `${mins}m ago`;
|
|
125
|
+
const hrs = Math.floor(mins / 60);
|
|
126
|
+
const remainMins = mins % 60;
|
|
127
|
+
if (hrs < 24) return remainMins > 0 ? `${hrs}h ${remainMins}m ago` : `${hrs}h ago`;
|
|
128
|
+
const days = Math.floor(hrs / 24);
|
|
129
|
+
return `${days}d ago`;
|
|
115
130
|
}
|
|
116
131
|
|
|
117
|
-
function AccountUsageCard({ entry, isActive }: {
|
|
132
|
+
function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onVerify, verifyingId, onViewProfile }: {
|
|
118
133
|
entry: AccountUsageEntry;
|
|
119
134
|
isActive: boolean;
|
|
135
|
+
accountInfo?: AccountInfo;
|
|
136
|
+
onToggle?: (id: string, status: string) => void;
|
|
137
|
+
onVerify?: (id: string) => void;
|
|
138
|
+
verifyingId?: string | null;
|
|
139
|
+
onViewProfile?: (profile: OAuthProfileData) => void;
|
|
120
140
|
}) {
|
|
121
141
|
const { usage } = entry;
|
|
122
142
|
const hasBuckets = usage.session || usage.weekly || usage.weeklyOpus || usage.weeklySonnet;
|
|
143
|
+
const status = accountInfo?.status ?? entry.accountStatus;
|
|
123
144
|
|
|
124
145
|
return (
|
|
125
146
|
<div className={`rounded-md border p-2 space-y-1.5 ${isActive ? "border-primary/30 bg-primary/5" : "border-border/50"}`}>
|
|
126
|
-
<div className="flex items-center gap-1.5
|
|
147
|
+
<div className="flex items-center gap-1.5">
|
|
127
148
|
<span className="text-xs font-medium truncate flex-1 min-w-0">
|
|
128
149
|
{entry.accountLabel ?? entry.accountId.slice(0, 8)}
|
|
129
150
|
</span>
|
|
@@ -133,9 +154,36 @@ function AccountUsageCard({ entry, isActive }: {
|
|
|
133
154
|
{!entry.isOAuth && (
|
|
134
155
|
<span className="text-[9px] text-text-subtle shrink-0">API key</span>
|
|
135
156
|
)}
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
{/* Account controls */}
|
|
158
|
+
<div className="flex items-center gap-0.5 shrink-0">
|
|
159
|
+
{onViewProfile && accountInfo?.profileData && (
|
|
160
|
+
<button
|
|
161
|
+
className="p-1 rounded cursor-pointer text-text-subtle hover:text-foreground hover:bg-surface-elevated transition-colors"
|
|
162
|
+
onClick={() => onViewProfile(accountInfo.profileData!)}
|
|
163
|
+
title="View profile"
|
|
164
|
+
>
|
|
165
|
+
<Eye className="size-3" />
|
|
166
|
+
</button>
|
|
167
|
+
)}
|
|
168
|
+
{onVerify && (
|
|
169
|
+
<button
|
|
170
|
+
className="p-1 rounded cursor-pointer text-text-subtle hover:text-green-600 hover:bg-surface-elevated transition-colors"
|
|
171
|
+
onClick={() => onVerify(entry.accountId)}
|
|
172
|
+
disabled={verifyingId === entry.accountId}
|
|
173
|
+
title="Verify token"
|
|
174
|
+
>
|
|
175
|
+
{verifyingId === entry.accountId ? <Loader2 className="size-3 animate-spin" /> : <ShieldCheck className="size-3" />}
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
{onToggle && (
|
|
179
|
+
<Switch
|
|
180
|
+
checked={status !== "disabled"}
|
|
181
|
+
onCheckedChange={() => onToggle(entry.accountId, status)}
|
|
182
|
+
disabled={status === "cooldown"}
|
|
183
|
+
className="scale-[0.6] cursor-pointer"
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
139
187
|
</div>
|
|
140
188
|
{hasBuckets ? (
|
|
141
189
|
<div className="space-y-1.5">
|
|
@@ -160,27 +208,51 @@ function AccountUsageCard({ entry, isActive }: {
|
|
|
160
208
|
|
|
161
209
|
export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, lastFetchedAt }: UsageDetailPanelProps) {
|
|
162
210
|
const [allUsages, setAllUsages] = useState<AccountUsageEntry[]>([]);
|
|
211
|
+
const [accounts, setAccounts] = useState<AccountInfo[]>([]);
|
|
212
|
+
const [activeAccountId, setActiveAccountId] = useState<string | null>(null);
|
|
163
213
|
const [loadingAll, setLoadingAll] = useState(false);
|
|
214
|
+
const [verifyingId, setVerifyingId] = useState<string | null>(null);
|
|
215
|
+
const [profileView, setProfileView] = useState<OAuthProfileData | null>(null);
|
|
216
|
+
|
|
217
|
+
async function loadAll() {
|
|
218
|
+
setLoadingAll(true);
|
|
219
|
+
const [usages, accs, active] = await Promise.allSettled([
|
|
220
|
+
getAllAccountUsages(), getAccounts(), getActiveAccount(),
|
|
221
|
+
]);
|
|
222
|
+
if (usages.status === "fulfilled") setAllUsages(usages.value);
|
|
223
|
+
if (accs.status === "fulfilled") setAccounts(accs.value);
|
|
224
|
+
if (active.status === "fulfilled") setActiveAccountId(active.value?.id ?? null);
|
|
225
|
+
setLoadingAll(false);
|
|
226
|
+
}
|
|
164
227
|
|
|
165
228
|
useEffect(() => {
|
|
166
229
|
if (!visible) return;
|
|
167
|
-
|
|
168
|
-
getAllAccountUsages()
|
|
169
|
-
.then(setAllUsages)
|
|
170
|
-
.catch(() => {})
|
|
171
|
-
.finally(() => setLoadingAll(false));
|
|
230
|
+
loadAll();
|
|
172
231
|
}, [visible]);
|
|
173
232
|
|
|
174
233
|
if (!visible) return null;
|
|
175
234
|
|
|
235
|
+
const accountMap = new Map(accounts.map((a) => [a.id, a]));
|
|
176
236
|
const hasCost = usage.queryCostUsd != null || usage.totalCostUsd != null;
|
|
177
237
|
const hasMultipleAccounts = allUsages.length > 0;
|
|
178
238
|
|
|
239
|
+
async function handleToggle(id: string, status: string) {
|
|
240
|
+
await patchAccount(id, { status: status === "disabled" ? "active" : "disabled" });
|
|
241
|
+
loadAll();
|
|
242
|
+
onReload?.();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function handleVerify(id: string) {
|
|
246
|
+
setVerifyingId(id);
|
|
247
|
+
try { await verifyAccount(id); loadAll(); } catch { /* silent */ }
|
|
248
|
+
setVerifyingId(null);
|
|
249
|
+
}
|
|
250
|
+
|
|
179
251
|
return (
|
|
180
252
|
<div className="border-b border-border bg-surface px-3 py-2.5 space-y-2.5 max-h-[350px] overflow-y-auto">
|
|
181
253
|
<div className="flex items-center justify-between">
|
|
182
254
|
<div className="flex items-center gap-2">
|
|
183
|
-
<span className="text-xs font-semibold text-text-primary">Usage
|
|
255
|
+
<span className="text-xs font-semibold text-text-primary">Usage & Accounts</span>
|
|
184
256
|
{lastFetchedAt && (
|
|
185
257
|
<span className="text-[10px] text-text-subtle">{formatLastUpdated(new Date(lastFetchedAt).getTime())}</span>
|
|
186
258
|
)}
|
|
@@ -188,19 +260,19 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
188
260
|
<div className="flex items-center gap-1">
|
|
189
261
|
{onReload && (
|
|
190
262
|
<button
|
|
191
|
-
onClick={onReload}
|
|
263
|
+
onClick={() => { onReload(); loadAll(); }}
|
|
192
264
|
disabled={loading}
|
|
193
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1 disabled:opacity-50"
|
|
194
|
-
title="Refresh
|
|
265
|
+
className="text-xs text-text-subtle hover:text-text-primary px-1 disabled:opacity-50 cursor-pointer"
|
|
266
|
+
title="Refresh"
|
|
195
267
|
>
|
|
196
268
|
<RefreshCw className={`size-3 ${loading ? "animate-spin" : ""}`} />
|
|
197
269
|
</button>
|
|
198
270
|
)}
|
|
199
271
|
<button
|
|
200
272
|
onClick={onClose}
|
|
201
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1"
|
|
273
|
+
className="text-xs text-text-subtle hover:text-text-primary px-1 cursor-pointer"
|
|
202
274
|
>
|
|
203
|
-
|
|
275
|
+
<X className="size-3" />
|
|
204
276
|
</button>
|
|
205
277
|
</div>
|
|
206
278
|
</div>
|
|
@@ -208,19 +280,23 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
208
280
|
{hasMultipleAccounts ? (
|
|
209
281
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-1.5">
|
|
210
282
|
{loadingAll ? (
|
|
211
|
-
<p className="text-[10px] text-text-subtle">Loading
|
|
283
|
+
<p className="text-[10px] text-text-subtle">Loading...</p>
|
|
212
284
|
) : (
|
|
213
285
|
allUsages.map((entry) => (
|
|
214
286
|
<AccountUsageCard
|
|
215
287
|
key={entry.accountId}
|
|
216
288
|
entry={entry}
|
|
217
|
-
isActive={entry.accountId === usage.activeAccountId}
|
|
289
|
+
isActive={entry.accountId === (activeAccountId ?? usage.activeAccountId)}
|
|
290
|
+
accountInfo={accountMap.get(entry.accountId)}
|
|
291
|
+
onToggle={handleToggle}
|
|
292
|
+
onVerify={handleVerify}
|
|
293
|
+
verifyingId={verifyingId}
|
|
294
|
+
onViewProfile={setProfileView}
|
|
218
295
|
/>
|
|
219
296
|
))
|
|
220
297
|
)}
|
|
221
298
|
</div>
|
|
222
299
|
) : (
|
|
223
|
-
// Fallback: single-account view (legacy or no accounts configured)
|
|
224
300
|
<>
|
|
225
301
|
{usage.session || usage.weekly || usage.weeklyOpus || usage.weeklySonnet ? (
|
|
226
302
|
<div className="space-y-2.5">
|
|
@@ -255,6 +331,26 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
255
331
|
)}
|
|
256
332
|
</div>
|
|
257
333
|
)}
|
|
334
|
+
|
|
335
|
+
{/* Inline profile popup */}
|
|
336
|
+
{profileView && (
|
|
337
|
+
<div className="border-t border-border pt-2">
|
|
338
|
+
<div className="flex items-center justify-between mb-1">
|
|
339
|
+
<span className="text-[10px] font-medium text-text-subtle">Profile</span>
|
|
340
|
+
<button className="text-text-subtle hover:text-foreground cursor-pointer" onClick={() => setProfileView(null)}>
|
|
341
|
+
<X className="size-3" />
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
<div className="grid grid-cols-[70px_1fr] gap-x-2 gap-y-0.5 text-[10px]">
|
|
345
|
+
{profileView.account?.display_name && <><span className="text-text-subtle">Name</span><span>{profileView.account.display_name}</span></>}
|
|
346
|
+
{profileView.account?.email && <><span className="text-text-subtle">Email</span><span>{profileView.account.email}</span></>}
|
|
347
|
+
{profileView.organization?.name && <><span className="text-text-subtle">Org</span><span>{profileView.organization.name}</span></>}
|
|
348
|
+
{profileView.organization?.organization_type && <><span className="text-text-subtle">Type</span><span>{profileView.organization.organization_type}</span></>}
|
|
349
|
+
{profileView.organization?.rate_limit_tier && <><span className="text-text-subtle">Tier</span><span>{profileView.organization.rate_limit_tier}</span></>}
|
|
350
|
+
{profileView.organization?.subscription_status && <><span className="text-text-subtle">Status</span><span>{profileView.organization.subscription_status}</span></>}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
258
354
|
</div>
|
|
259
355
|
);
|
|
260
356
|
}
|