@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bunfig.toml +2 -0
  3. package/dist/web/assets/chat-tab-C0AcTU9S.js +7 -0
  4. package/dist/web/assets/{code-editor-DXqocnye.js → code-editor-CYt4hqge.js} +1 -1
  5. package/dist/web/assets/{database-viewer-ChX5vA56.js → database-viewer-CDto4TrW.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-8RNfSVOl.js → diff-viewer-2zSPeCzX.js} +1 -1
  7. package/dist/web/assets/git-graph-HfH98qwn.js +1 -0
  8. package/dist/web/assets/index-CTOMzCnZ.js +28 -0
  9. package/dist/web/assets/index-D6GLlwUx.css +2 -0
  10. package/dist/web/assets/keybindings-store-DrjDQzVs.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-BOHSi1fK.js → markdown-renderer-dqkYhU3y.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-DRo3924t.js → postgres-viewer-kqZBNVYW.js} +1 -1
  13. package/dist/web/assets/settings-tab-jhRBFgf_.js +1 -0
  14. package/dist/web/assets/{sqlite-viewer-0iVQjCmF.js → sqlite-viewer-C2744fw1.js} +1 -1
  15. package/dist/web/assets/switch-PAf5UhcN.js +1 -0
  16. package/dist/web/assets/{terminal-tab-Cuznr8Lg.js → terminal-tab-BDqc6Dl5.js} +1 -1
  17. package/dist/web/index.html +3 -3
  18. package/dist/web/sw.js +1 -1
  19. package/package.json +1 -1
  20. package/src/server/routes/accounts.ts +60 -6
  21. package/src/services/account.service.ts +180 -17
  22. package/src/services/claude-usage.service.ts +9 -2
  23. package/src/services/db.service.ts +25 -3
  24. package/src/web/components/chat/chat-history-bar.tsx +2 -1
  25. package/src/web/components/chat/message-list.tsx +4 -2
  26. package/src/web/components/chat/usage-badge.tsx +118 -22
  27. package/src/web/components/settings/accounts-settings-section.tsx +268 -33
  28. package/src/web/lib/api-settings.ts +49 -0
  29. package/src/web/styles/globals.css +7 -0
  30. package/test-claude-oauth-v2.mjs +165 -0
  31. package/test-claude-oauth.mjs +175 -0
  32. package/test-verify-oat.mjs +106 -0
  33. package/dist/web/assets/ai-settings-section-BxCMGg-I.js +0 -1
  34. package/dist/web/assets/chat-tab-DtIaMWNT.js +0 -7
  35. package/dist/web/assets/git-graph-DYbWcg6M.js +0 -1
  36. package/dist/web/assets/index-BzhcIgja.js +0 -28
  37. package/dist/web/assets/index-sMxUHxFZ.css +0 -2
  38. package/dist/web/assets/keybindings-store-DBQQ_pTh.js +0 -1
  39. 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-[11px] text-text-tertiary/60 select-none">via {message.accountLabel}</p>
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 { getAllAccountUsages, type AccountUsageEntry } from "../../lib/api-settings";
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 flex-wrap">
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
- {entry.accountStatus === "disabled" && (
137
- <span className="text-[9px] text-text-subtle shrink-0">disabled</span>
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
- setLoadingAll(true);
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 Limits</span>
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 usage data"
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 accounts...</p>
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
  }