shraga 0.1.59 → 0.1.60
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/dist/client/assets/index-LCOnn8yS.css +10 -0
- package/dist/client/assets/index-NHccVXR_.js +1946 -0
- package/dist/client/index.html +2 -2
- package/package.json +2 -1
- package/src/client/components/MachineStats.tsx +73 -13
- package/src/client/components/ui/hover-card.tsx +33 -0
- package/dist/client/assets/index-DNHg3blr.js +0 -1948
- package/dist/client/assets/index-DXJfr5b2.css +0 -10
package/dist/client/index.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
15
15
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-NHccVXR_.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-LCOnn8yS.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.60",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@livx.cc/mcp-firebase": "^0.1.14",
|
|
60
60
|
"@radix-ui/react-accordion": "^1.2.2",
|
|
61
61
|
"@radix-ui/react-dialog": "^1.1.4",
|
|
62
|
+
"@radix-ui/react-hover-card": "^1.1.23",
|
|
62
63
|
"@radix-ui/react-scroll-area": "^1.2.2",
|
|
63
64
|
"@radix-ui/react-slot": "^1.1.1",
|
|
64
65
|
"@tailwindcss/typography": "^0.5.19",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import type { AgentSocket, ServerEvent } from '@/lib/ws';
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
|
+
import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card';
|
|
4
5
|
|
|
5
6
|
type Sample = Extract<ServerEvent, { type: 'stats' }>['sample'];
|
|
6
7
|
|
|
@@ -98,15 +99,66 @@ export function UsageMetric({ usage }: { usage: Usage | null }) {
|
|
|
98
99
|
const top = usage && binding(usage.limits);
|
|
99
100
|
if (!usage || !top) return null;
|
|
100
101
|
|
|
101
|
-
// Every window, each labelled from its OWN resets_at — the `weekly_*` kinds do NOT reset weekly.
|
|
102
|
-
const detail = usage.limits
|
|
103
|
-
.map(l => `${l.scopeLabel ?? l.kind} ${l.percent}%${untilLabel(l.resetsAt) ? ` (resets in ${untilLabel(l.resetsAt)})` : ''}`)
|
|
104
|
-
.join('\n');
|
|
105
|
-
const plan = usage.subscriptionType ? `claude ${usage.subscriptionType} — ` : 'claude ';
|
|
106
|
-
|
|
107
102
|
// No `series`: usage has no server-side history to seed from, so it renders a GAUGE (full from the
|
|
108
103
|
// first paint) instead of a sparkline that would plot tab-uptime and be empty for the first minutes.
|
|
109
|
-
|
|
104
|
+
// No `title` either — the breakdown lives in the hover card below; a native tooltip on the same
|
|
105
|
+
// element would race it, appear a second late, and repeat the card word for word.
|
|
106
|
+
return (
|
|
107
|
+
<HoverCard openDelay={120} closeDelay={80}>
|
|
108
|
+
<HoverCardTrigger asChild>
|
|
109
|
+
<span className="cursor-default">
|
|
110
|
+
<Metric label="usage" value={top.percent} severity={top.severity} />
|
|
111
|
+
</span>
|
|
112
|
+
</HoverCardTrigger>
|
|
113
|
+
<HoverCardContent>
|
|
114
|
+
<UsageCard usage={usage} />
|
|
115
|
+
</HoverCardContent>
|
|
116
|
+
</HoverCard>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The hover breakdown: one row per reported window. Exported bare so the rows can be asserted
|
|
121
|
+
* without driving a real hover (Radix only mounts the content once open). */
|
|
122
|
+
export function UsageCard({ usage }: { usage: Usage }) {
|
|
123
|
+
const top = binding(usage.limits);
|
|
124
|
+
return (
|
|
125
|
+
<div className="text-[11px] leading-tight">
|
|
126
|
+
<div className="flex items-baseline justify-between gap-2 pb-2 text-muted-foreground">
|
|
127
|
+
<span className="uppercase tracking-wide">claude usage</span>
|
|
128
|
+
{usage.subscriptionType && <span className="tabular-nums">{usage.subscriptionType} plan</span>}
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex flex-col gap-3">
|
|
131
|
+
{usage.limits.map((l, i) => {
|
|
132
|
+
const until = untilLabel(l.resetsAt);
|
|
133
|
+
const isTop = l === top;
|
|
134
|
+
return (
|
|
135
|
+
<div key={`${l.kind}-${i}`} data-headline={isTop || undefined} className={cn('flex flex-col gap-1', !isTop && 'opacity-60')}>
|
|
136
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
137
|
+
<span className={cn('truncate', isTop && 'font-medium')}>
|
|
138
|
+
{isTop && <span className="mr-1 text-muted-foreground" aria-hidden>▸</span>}
|
|
139
|
+
{windowLabel(l)}
|
|
140
|
+
</span>
|
|
141
|
+
<span className={cn('tabular-nums shrink-0', level(l.percent, l.severity))}>{l.percent}%</span>
|
|
142
|
+
</div>
|
|
143
|
+
<Gauge value={l.percent} className={cn('w-full', level(l.percent, l.severity))} />
|
|
144
|
+
<span className="text-[10px] text-muted-foreground">{until ? `resets in ${until}` : 'no reset reported'}</span>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
<div className="mt-3 border-t pt-2 text-[10px] text-muted-foreground">▸ is the window the strip is showing</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Human name for a window, derived ONLY from what the payload actually states. `weekly_*` is never
|
|
155
|
+
* printed as "weekly"/"7 days": that window rolls on a ~72h cadence, so the kind name is a lie and
|
|
156
|
+
* only resets_at (rendered separately) tells the truth about its length. */
|
|
157
|
+
export function windowLabel(l: UsageLimit): string {
|
|
158
|
+
if (l.scopeLabel) return `${l.scopeLabel} window`;
|
|
159
|
+
if (l.kind === 'session') return 'current session';
|
|
160
|
+
if (l.kind === 'weekly_all') return 'all models';
|
|
161
|
+
return l.kind.replace(/_/g, ' ');
|
|
110
162
|
}
|
|
111
163
|
|
|
112
164
|
/** The window that gates you first is simply the FULLEST one, whichever kind it is.
|
|
@@ -118,15 +170,20 @@ export function binding(limits: UsageLimit[]): UsageLimit | null {
|
|
|
118
170
|
return limits.reduce<UsageLimit | null>((best, l) => (!best || l.percent > best.percent ? l : best), null);
|
|
119
171
|
}
|
|
120
172
|
|
|
121
|
-
/** Human "time until reset", derived from resets_at only — never from the limit's kind name.
|
|
173
|
+
/** Human "time until reset", derived from resets_at only — never from the limit's kind name.
|
|
174
|
+
* Rounded to whole minutes FIRST so a value handed in as exactly 4h does not render "3h 59m"
|
|
175
|
+
* because a few milliseconds elapsed between building it and reading the clock. */
|
|
122
176
|
export function untilLabel(iso: string | null): string | null {
|
|
123
177
|
if (!iso) return null;
|
|
124
178
|
const ms = new Date(iso).getTime() - Date.now();
|
|
125
179
|
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
if (
|
|
129
|
-
|
|
180
|
+
const min = Math.round(ms / 60_000);
|
|
181
|
+
if (min < 1) return '1m';
|
|
182
|
+
if (min < 60) return `${min}m`;
|
|
183
|
+
const h = Math.floor(min / 60), m = min % 60;
|
|
184
|
+
if (h < 48) return m ? `${h}h ${m}m` : `${h}h`;
|
|
185
|
+
const d = Math.floor(h / 24), rh = h % 24;
|
|
186
|
+
return rh ? `${d}d ${rh}h` : `${d}d`;
|
|
130
187
|
}
|
|
131
188
|
|
|
132
189
|
/** Only severities we have actually SEEN mean something here. The vendor's vocabulary is not
|
|
@@ -143,8 +200,11 @@ function level(v: number, severity?: string) {
|
|
|
143
200
|
|
|
144
201
|
function Metric({ label, value, series, title, severity }: { label: string; value: number; series?: number[]; title?: string; severity?: string }) {
|
|
145
202
|
const tone = level(value, severity);
|
|
203
|
+
// Plain-text fallback only where nothing richer exists (cpu/mem). The usage metric passes no title:
|
|
204
|
+
// it owns a hover card, and a native tooltip on the same element would double up on it.
|
|
205
|
+
const tip = title ?? (series ? `${label} ${value}% — last ${series.length} samples` : undefined);
|
|
146
206
|
return (
|
|
147
|
-
<span className="flex items-center gap-1" title={
|
|
207
|
+
<span className="flex items-center gap-1" title={tip}>
|
|
148
208
|
<span className="uppercase tracking-wide">{label}</span>
|
|
149
209
|
{series ? <Sparkline series={series} className={tone} /> : <Gauge value={value} className={tone} />}
|
|
150
210
|
<span className={cn('tabular-nums', tone)}>{value}%</span>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
const HoverCard = HoverCardPrimitive.Root;
|
|
6
|
+
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
|
7
|
+
|
|
8
|
+
/** Portalled + collision-aware by default: the only consumer today sits in the bottom-left corner of
|
|
9
|
+
* a fixed 256px sidebar, so the card MUST be free to flip upward and slide rightward into the main
|
|
10
|
+
* pane instead of being clipped by the sidebar box or the viewport edge. */
|
|
11
|
+
const HoverCardContent = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
|
14
|
+
>(({ className, align = 'start', side = 'top', sideOffset = 8, collisionPadding = 8, ...props }, ref) => (
|
|
15
|
+
<HoverCardPrimitive.Portal>
|
|
16
|
+
<HoverCardPrimitive.Content
|
|
17
|
+
ref={ref}
|
|
18
|
+
align={align}
|
|
19
|
+
side={side}
|
|
20
|
+
sideOffset={sideOffset}
|
|
21
|
+
collisionPadding={collisionPadding}
|
|
22
|
+
className={cn(
|
|
23
|
+
'z-50 w-64 rounded-md border bg-card p-3 text-card-foreground shadow-md outline-none',
|
|
24
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
</HoverCardPrimitive.Portal>
|
|
30
|
+
));
|
|
31
|
+
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
|
32
|
+
|
|
33
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent };
|