@hienlh/ppm 0.8.88 → 0.8.90
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 +17 -0
- package/dist/web/assets/api-settings-Bid0NHuI.js +1 -0
- package/dist/web/assets/{browser-tab-DJLH0eDY.js → browser-tab-SHBc1OCK.js} +1 -1
- package/dist/web/assets/chat-tab-dssvQaJN.js +8 -0
- package/dist/web/assets/{code-editor-CaGdx-lS.js → code-editor-BomcTYQ4.js} +1 -1
- package/dist/web/assets/{database-viewer-i4Ddk6mO.js → database-viewer-B47ck-1v.js} +1 -1
- package/dist/web/assets/{diff-viewer-DQDS7yjv.js → diff-viewer-Dw2v2RU2.js} +1 -1
- package/dist/web/assets/{git-graph-DUs-TN1u.js → git-graph-Co7fcau-.js} +1 -1
- package/dist/web/assets/{index-Dm6RN1A1.js → index-CQu4iIvy.js} +6 -6
- package/dist/web/assets/index-CqhIj4Ko.css +2 -0
- package/dist/web/assets/keybindings-store-OkhvRBpn.js +1 -0
- package/dist/web/assets/{markdown-renderer-L1NgC2Rw.js → markdown-renderer-C0n-Ucfa.js} +1 -1
- package/dist/web/assets/{postgres-viewer-_uDispGW.js → postgres-viewer-D4vyH--N.js} +1 -1
- package/dist/web/assets/{settings-tab-Bp4041i6.js → settings-tab-CisRYqMl.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-GW-QCjHn.js → sqlite-viewer-D3t4nApY.js} +1 -1
- package/dist/web/assets/{terminal-tab-E4cWujj4.js → terminal-tab-DFz3Bd_N.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-zABXAAla.js → use-monaco-theme-Dopv6S2i.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/docs/streaming-input-guide.md +267 -0
- package/package.json +1 -1
- package/snapshot-state.md +1526 -0
- package/src/services/account.service.ts +2 -2
- package/src/services/claude-usage.service.ts +2 -7
- package/src/services/mcp-config.service.ts +15 -6
- package/src/services/supervisor.ts +19 -2
- package/src/web/app.tsx +3 -2
- package/src/web/components/chat/usage-badge.tsx +58 -8
- package/src/web/components/layout/upgrade-banner.tsx +15 -5
- package/test-session-ops.mjs +444 -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-Dh4oFOpX.js +0 -1
- package/dist/web/assets/chat-tab-C8HFXqGS.js +0 -8
- package/dist/web/assets/index-DhtLEnPD.css +0 -2
- package/dist/web/assets/keybindings-store-qVLDZz97.js +0 -1
|
@@ -139,7 +139,7 @@ class AccountService {
|
|
|
139
139
|
await this.refreshAccessToken(id, false);
|
|
140
140
|
return this.getWithTokens(id);
|
|
141
141
|
} catch (e) {
|
|
142
|
-
console.error(`[accounts] Pre-flight refresh failed for ${id}
|
|
142
|
+
console.error(`[accounts] Pre-flight refresh failed for ${id}: ${(e as Error).message ?? e}`);
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -709,7 +709,7 @@ class AccountService {
|
|
|
709
709
|
try {
|
|
710
710
|
await this.refreshAccessToken(acc.id, false);
|
|
711
711
|
} catch (e) {
|
|
712
|
-
console.error(`[accounts] Auto-refresh failed for ${acc.id}
|
|
712
|
+
console.error(`[accounts] Auto-refresh failed for ${acc.id}: ${(e as Error).message ?? e}`);
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
715
|
};
|
|
@@ -273,14 +273,9 @@ export function getUsageForAccount(accountId: string): ClaudeUsage {
|
|
|
273
273
|
return row ? snapshotToUsage(row) : {};
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
/** Get usage for all accounts
|
|
276
|
+
/** Get usage for all accounts */
|
|
277
277
|
export function getAllAccountUsages(): AccountUsageEntry[] {
|
|
278
|
-
const
|
|
279
|
-
const accounts = accountService.list().filter(acc => {
|
|
280
|
-
// Exclude expired accounts without refresh token (temporary/invalid)
|
|
281
|
-
if (!accountService.hasRefreshToken(acc.id) && acc.expiresAt && acc.expiresAt < nowS) return false;
|
|
282
|
-
return true;
|
|
283
|
-
});
|
|
278
|
+
const accounts = accountService.list();
|
|
284
279
|
const snapshots = getAllLatestSnapshots();
|
|
285
280
|
const snapshotMap = new Map(snapshots.map(s => [s.account_id, s]));
|
|
286
281
|
return accounts.map(acc => {
|
|
@@ -27,13 +27,22 @@ export class McpConfigService {
|
|
|
27
27
|
|
|
28
28
|
/** List all MCP servers as Record (SDK-compatible format) */
|
|
29
29
|
list(): Record<string, McpServerConfig> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
30
|
+
try {
|
|
31
|
+
const rows = this.db.query("SELECT name, config FROM mcp_servers ORDER BY name").all() as { name: string; config: string }[];
|
|
32
|
+
const result: Record<string, McpServerConfig> = {};
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const parsed = safeParse(row.config, row.name);
|
|
35
|
+
if (parsed) result[row.name] = parsed;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
const msg = (e as Error).message ?? String(e);
|
|
40
|
+
if (msg.includes("no such table")) {
|
|
41
|
+
console.warn("[mcp] mcp_servers table not found — returning empty list");
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
throw e;
|
|
35
45
|
}
|
|
36
|
-
return result;
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
/** List as array with metadata (for UI) */
|
|
@@ -378,17 +378,33 @@ function adoptTunnel(): boolean {
|
|
|
378
378
|
const status = readStatus();
|
|
379
379
|
const pid = status.tunnelPid as number;
|
|
380
380
|
const url = status.shareUrl as string;
|
|
381
|
-
if (!pid || !url)
|
|
381
|
+
if (!pid || !url) {
|
|
382
|
+
log("DEBUG", `adoptTunnel: missing tunnelPid(${pid}) or shareUrl(${url}) in status`);
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
382
385
|
process.kill(pid, 0); // throws if process is dead
|
|
383
386
|
adoptedTunnelPid = pid;
|
|
384
387
|
tunnelUrl = url;
|
|
385
388
|
log("INFO", `Adopted existing tunnel (PID: ${pid}, URL: ${url})`);
|
|
386
389
|
return true;
|
|
387
|
-
} catch {
|
|
390
|
+
} catch (e) {
|
|
391
|
+
log("WARN", `adoptTunnel: tunnel PID ${(readStatus().tunnelPid)} unreachable: ${e}`);
|
|
388
392
|
return false;
|
|
389
393
|
}
|
|
390
394
|
}
|
|
391
395
|
|
|
396
|
+
/** Kill stale tunnel PID from status.json (cleanup after failed adoption) */
|
|
397
|
+
function killStaleTunnel() {
|
|
398
|
+
try {
|
|
399
|
+
const status = readStatus();
|
|
400
|
+
const pid = status.tunnelPid as number;
|
|
401
|
+
if (!pid) return;
|
|
402
|
+
try { process.kill(pid, "SIGTERM"); } catch {}
|
|
403
|
+
log("INFO", `Killed stale tunnel (PID: ${pid})`);
|
|
404
|
+
} catch {}
|
|
405
|
+
updateStatus({ tunnelPid: null, shareUrl: null });
|
|
406
|
+
}
|
|
407
|
+
|
|
392
408
|
/** Spawn new supervisor from updated code, wait for it to be healthy, then exit */
|
|
393
409
|
async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
394
410
|
log("INFO", "Starting self-replace for upgrade");
|
|
@@ -706,6 +722,7 @@ export async function runSupervisor(opts: {
|
|
|
706
722
|
startTunnelProbe(opts.port);
|
|
707
723
|
// Try adopting tunnel kept alive from previous upgrade; spawn new if dead
|
|
708
724
|
if (!adoptTunnel()) {
|
|
725
|
+
killStaleTunnel(); // kill orphaned tunnel before spawning new one
|
|
709
726
|
promises.push(spawnTunnel(opts.port));
|
|
710
727
|
}
|
|
711
728
|
}
|
package/src/web/app.tsx
CHANGED
|
@@ -37,6 +37,7 @@ type AuthState = "checking" | "authenticated" | "unauthenticated";
|
|
|
37
37
|
|
|
38
38
|
export function App() {
|
|
39
39
|
const [authState, setAuthState] = useState<AuthState>("checking");
|
|
40
|
+
const [upgradeBannerVisible, setUpgradeBannerVisible] = useState(false);
|
|
40
41
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
41
42
|
const [drawerTab, setDrawerTab] = useState<"explorer" | "git" | "settings" | undefined>();
|
|
42
43
|
const [projectSheetOpen, setProjectSheetOpen] = useState(false);
|
|
@@ -229,11 +230,11 @@ export function App() {
|
|
|
229
230
|
<TooltipProvider>
|
|
230
231
|
<div className="h-dvh flex flex-col bg-background text-foreground overflow-hidden relative">
|
|
231
232
|
{/* Upgrade banner — shown when new version available */}
|
|
232
|
-
<UpgradeBanner />
|
|
233
|
+
<UpgradeBanner onVisibilityChange={setUpgradeBannerVisible} />
|
|
233
234
|
|
|
234
235
|
{/* Mobile device name badge — floating top-left */}
|
|
235
236
|
{deviceName && (
|
|
236
|
-
<div className="md:hidden fixed
|
|
237
|
+
<div className={cn("md:hidden fixed left-0 z-50 px-2 py-0.5 bg-primary/80 text-primary-foreground text-[10px] font-medium rounded-br transition-[top]", upgradeBannerVisible ? "top-7" : "top-0")}>
|
|
237
238
|
{deviceName}
|
|
238
239
|
</div>
|
|
239
240
|
)}
|
|
@@ -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} />
|
|
@@ -12,7 +12,11 @@ interface UpgradeStatus {
|
|
|
12
12
|
installMethod: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
interface UpgradeBannerProps {
|
|
16
|
+
onVisibilityChange?: (visible: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
16
20
|
const [availableVersion, setAvailableVersion] = useState<string | null>(null);
|
|
17
21
|
const [upgrading, setUpgrading] = useState(false);
|
|
18
22
|
const [dismissed, setDismissed] = useState(false);
|
|
@@ -61,10 +65,16 @@ export function UpgradeBanner() {
|
|
|
61
65
|
setDismissed(true);
|
|
62
66
|
}, [availableVersion]);
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
const visible = !!availableVersion && !dismissed;
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
onVisibilityChange?.(visible);
|
|
72
|
+
}, [visible, onVisibilityChange]);
|
|
73
|
+
|
|
74
|
+
if (!visible) return null;
|
|
65
75
|
|
|
66
76
|
return (
|
|
67
|
-
<div className="w-full bg-blue-600 dark:bg-blue-700 text-white px-3 py-
|
|
77
|
+
<div className="w-full bg-blue-600 dark:bg-blue-700 text-white px-3 py-1 flex items-center justify-between gap-2 z-50 text-sm shrink-0">
|
|
68
78
|
{upgrading ? (
|
|
69
79
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
70
80
|
<Loader2 className="size-4 animate-spin shrink-0" />
|
|
@@ -83,13 +93,13 @@ export function UpgradeBanner() {
|
|
|
83
93
|
<div className="flex items-center gap-1 shrink-0">
|
|
84
94
|
<button
|
|
85
95
|
onClick={handleUpgrade}
|
|
86
|
-
className="bg-white text-blue-600 font-medium rounded-full px-3 min-h-[
|
|
96
|
+
className="bg-white text-blue-600 font-medium rounded-full px-3 py-0.5 text-xs min-h-[28px] min-w-[28px] flex items-center justify-center hover:bg-blue-50 active:bg-blue-100 transition-colors"
|
|
87
97
|
>
|
|
88
98
|
Upgrade
|
|
89
99
|
</button>
|
|
90
100
|
<button
|
|
91
101
|
onClick={handleDismiss}
|
|
92
|
-
className="min-h-[
|
|
102
|
+
className="min-h-[28px] min-w-[28px] flex items-center justify-center rounded-full hover:bg-blue-500 active:bg-blue-800 transition-colors"
|
|
93
103
|
aria-label="Dismiss upgrade notification"
|
|
94
104
|
>
|
|
95
105
|
<X className="size-4" />
|