@hienlh/ppm 0.7.16 → 0.7.18
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/bunfig.toml +2 -0
- package/dist/web/assets/chat-tab-CXg6B4pP.js +7 -0
- package/dist/web/assets/{code-editor-DXqocnye.js → code-editor-hi_D7gq-.js} +1 -1
- package/dist/web/assets/{database-viewer-ChX5vA56.js → database-viewer-CbB7uUNK.js} +1 -1
- package/dist/web/assets/{diff-viewer-8RNfSVOl.js → diff-viewer-ahy-4aRe.js} +1 -1
- package/dist/web/assets/git-graph-Dm8O90yN.js +1 -0
- package/dist/web/assets/index-Bw-o6Ji3.js +28 -0
- package/dist/web/assets/index-D6GLlwUx.css +2 -0
- package/dist/web/assets/keybindings-store-D-ezVO2O.js +1 -0
- package/dist/web/assets/{markdown-renderer-BOHSi1fK.js → markdown-renderer-Dok83ft3.js} +1 -1
- package/dist/web/assets/{postgres-viewer-DRo3924t.js → postgres-viewer-BlESHVhJ.js} +1 -1
- package/dist/web/assets/settings-tab-2RCHMDfQ.js +1 -0
- package/dist/web/assets/{sqlite-viewer-0iVQjCmF.js → sqlite-viewer-LZsr5d7e.js} +1 -1
- package/dist/web/assets/switch-PAf5UhcN.js +1 -0
- package/dist/web/assets/{terminal-tab-Cuznr8Lg.js → terminal-tab-CCQsVaE6.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 +128 -26
- 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,57 @@ 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[]>([]);
|
|
163
|
-
const [
|
|
211
|
+
const [accounts, setAccounts] = useState<AccountInfo[]>([]);
|
|
212
|
+
const [activeAccountId, setActiveAccountId] = useState<string | null>(null);
|
|
213
|
+
const [loadingAll, setLoadingAll] = useState(true);
|
|
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
|
|
|
233
|
+
// Re-fetch account usages after parent refreshes from Anthropic API
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!visible || !lastFetchedAt) return;
|
|
236
|
+
loadAll();
|
|
237
|
+
}, [lastFetchedAt]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
238
|
+
|
|
174
239
|
if (!visible) return null;
|
|
175
240
|
|
|
241
|
+
const accountMap = new Map(accounts.map((a) => [a.id, a]));
|
|
176
242
|
const hasCost = usage.queryCostUsd != null || usage.totalCostUsd != null;
|
|
177
243
|
const hasMultipleAccounts = allUsages.length > 0;
|
|
178
244
|
|
|
245
|
+
async function handleToggle(id: string, status: string) {
|
|
246
|
+
await patchAccount(id, { status: status === "disabled" ? "active" : "disabled" });
|
|
247
|
+
loadAll();
|
|
248
|
+
onReload?.();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function handleVerify(id: string) {
|
|
252
|
+
setVerifyingId(id);
|
|
253
|
+
try { await verifyAccount(id); loadAll(); } catch { /* silent */ }
|
|
254
|
+
setVerifyingId(null);
|
|
255
|
+
}
|
|
256
|
+
|
|
179
257
|
return (
|
|
180
258
|
<div className="border-b border-border bg-surface px-3 py-2.5 space-y-2.5 max-h-[350px] overflow-y-auto">
|
|
181
259
|
<div className="flex items-center justify-between">
|
|
182
260
|
<div className="flex items-center gap-2">
|
|
183
|
-
<span className="text-xs font-semibold text-text-primary">Usage
|
|
261
|
+
<span className="text-xs font-semibold text-text-primary">Usage & Accounts</span>
|
|
184
262
|
{lastFetchedAt && (
|
|
185
263
|
<span className="text-[10px] text-text-subtle">{formatLastUpdated(new Date(lastFetchedAt).getTime())}</span>
|
|
186
264
|
)}
|
|
@@ -188,39 +266,43 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
188
266
|
<div className="flex items-center gap-1">
|
|
189
267
|
{onReload && (
|
|
190
268
|
<button
|
|
191
|
-
onClick={onReload}
|
|
192
|
-
disabled={loading}
|
|
193
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1 disabled:opacity-50"
|
|
194
|
-
title="Refresh
|
|
269
|
+
onClick={() => { setLoadingAll(true); onReload(); }}
|
|
270
|
+
disabled={loading || loadingAll}
|
|
271
|
+
className="text-xs text-text-subtle hover:text-text-primary px-1 disabled:opacity-50 cursor-pointer"
|
|
272
|
+
title="Refresh"
|
|
195
273
|
>
|
|
196
|
-
<RefreshCw className={`size-3 ${loading ? "animate-spin" : ""}`} />
|
|
274
|
+
<RefreshCw className={`size-3 ${(loading || loadingAll) ? "animate-spin" : ""}`} />
|
|
197
275
|
</button>
|
|
198
276
|
)}
|
|
199
277
|
<button
|
|
200
278
|
onClick={onClose}
|
|
201
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1"
|
|
279
|
+
className="text-xs text-text-subtle hover:text-text-primary px-1 cursor-pointer"
|
|
202
280
|
>
|
|
203
|
-
|
|
281
|
+
<X className="size-3" />
|
|
204
282
|
</button>
|
|
205
283
|
</div>
|
|
206
284
|
</div>
|
|
207
285
|
|
|
208
|
-
{hasMultipleAccounts ? (
|
|
286
|
+
{(hasMultipleAccounts || loadingAll) ? (
|
|
209
287
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-1.5">
|
|
210
288
|
{loadingAll ? (
|
|
211
|
-
<p className="text-[10px] text-text-subtle">Loading
|
|
289
|
+
<p className="text-[10px] text-text-subtle">Loading...</p>
|
|
212
290
|
) : (
|
|
213
291
|
allUsages.map((entry) => (
|
|
214
292
|
<AccountUsageCard
|
|
215
293
|
key={entry.accountId}
|
|
216
294
|
entry={entry}
|
|
217
|
-
isActive={entry.accountId === usage.activeAccountId}
|
|
295
|
+
isActive={entry.accountId === (activeAccountId ?? usage.activeAccountId)}
|
|
296
|
+
accountInfo={accountMap.get(entry.accountId)}
|
|
297
|
+
onToggle={handleToggle}
|
|
298
|
+
onVerify={handleVerify}
|
|
299
|
+
verifyingId={verifyingId}
|
|
300
|
+
onViewProfile={setProfileView}
|
|
218
301
|
/>
|
|
219
302
|
))
|
|
220
303
|
)}
|
|
221
304
|
</div>
|
|
222
305
|
) : (
|
|
223
|
-
// Fallback: single-account view (legacy or no accounts configured)
|
|
224
306
|
<>
|
|
225
307
|
{usage.session || usage.weekly || usage.weeklyOpus || usage.weeklySonnet ? (
|
|
226
308
|
<div className="space-y-2.5">
|
|
@@ -255,6 +337,26 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
255
337
|
)}
|
|
256
338
|
</div>
|
|
257
339
|
)}
|
|
340
|
+
|
|
341
|
+
{/* Inline profile popup */}
|
|
342
|
+
{profileView && (
|
|
343
|
+
<div className="border-t border-border pt-2">
|
|
344
|
+
<div className="flex items-center justify-between mb-1">
|
|
345
|
+
<span className="text-[10px] font-medium text-text-subtle">Profile</span>
|
|
346
|
+
<button className="text-text-subtle hover:text-foreground cursor-pointer" onClick={() => setProfileView(null)}>
|
|
347
|
+
<X className="size-3" />
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
<div className="grid grid-cols-[70px_1fr] gap-x-2 gap-y-0.5 text-[10px]">
|
|
351
|
+
{profileView.account?.display_name && <><span className="text-text-subtle">Name</span><span>{profileView.account.display_name}</span></>}
|
|
352
|
+
{profileView.account?.email && <><span className="text-text-subtle">Email</span><span>{profileView.account.email}</span></>}
|
|
353
|
+
{profileView.organization?.name && <><span className="text-text-subtle">Org</span><span>{profileView.organization.name}</span></>}
|
|
354
|
+
{profileView.organization?.organization_type && <><span className="text-text-subtle">Type</span><span>{profileView.organization.organization_type}</span></>}
|
|
355
|
+
{profileView.organization?.rate_limit_tier && <><span className="text-text-subtle">Tier</span><span>{profileView.organization.rate_limit_tier}</span></>}
|
|
356
|
+
{profileView.organization?.subscription_status && <><span className="text-text-subtle">Status</span><span>{profileView.organization.subscription_status}</span></>}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
258
360
|
</div>
|
|
259
361
|
);
|
|
260
362
|
}
|