<script lang="ts">
import { onDestroy } from "svelte";
import type { Readable } from "svelte/store";
import { Database, Gauge } from "lucide-svelte";
import Battery from "./Battery.svelte";
import { readMetrics, type Metrics } from "../lib/metrics";
import type { TimelineItem } from "../lib/streaming-v2";
export let items: Readable<TimelineItem[]> | TimelineItem[] | undefined = undefined;
export let metrics: Metrics | undefined = undefined;
let timelineItems: TimelineItem[] = [];
let unsubscribe: (() => void) | undefined = undefined;
$: {
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
}
if (items && typeof (items as any).subscribe === "function") {
unsubscribe = (items as Readable<TimelineItem[]>).subscribe((val) => {
timelineItems = val ?? [];
});
} else if (Array.isArray(items)) {
timelineItems = items;
} else {
timelineItems = [];
}
}
onDestroy(() => {
if (unsubscribe) unsubscribe();
});
$: m = metrics ?? readMetrics(timelineItems);
function fmtTokens(n: number): string {
if (n >= 1_000_000) {
const m = n / 1_000_000;
return Number.isInteger(m) ? `${m}m` : `${m.toFixed(1)}m`;
}
if (n >= 1_000) {
const k = n / 1_000;
return Number.isInteger(k) ? `${k}k` : `${Math.round(k)}k`;
}
return String(n);
}
$: cachePct = m.cached
? Math.round(m.cached.ratio <= 1 ? m.cached.ratio * 100 : m.cached.ratio)
: null;
$: cacheTitle = m.cached
? `cached ${fmtTokens(m.cached.cached)}/${fmtTokens(m.cached.prompt)} tokens — ${cachePctText}`
: "cache: not reported";
$: cachePctText = m.cached && m.cached.ratio <= 1 ? `${Math.round(m.cached.ratio * 100)}%` : "unknown";
$: tps = m.tokPerSec !== null ? Math.round(m.tokPerSec) : null;
</script>
<div class="metrics" role="region" aria-label="session metrics">
<div class="cluster-context">
<Battery consumed={m.context?.consumed} budget={m.context?.budget} />
<span
class="figure metric figure-context"
title={m.context ? `context ${fmtTokens(m.context.consumed)} of ${fmtTokens(m.context.budget)} tokens` : "context: not reported"}
>
{#if m.context}
<span class="num">{fmtTokens(m.context.consumed)}/{fmtTokens(m.context.budget)}</span>
{:else}
<span class="mute">unknown</span>
{/if}
</span>
</div>
<div class="cluster-telemetry">
<span class="figure metric figure-cache" title={cacheTitle}>
<Database size={13} stroke-width={1.5} />
{#if !m.reported}
<span class="mute">unknown</span>
{:else if m.cached && cachePct !== null}
<span class="num">{cachePct}%</span>
{:else}
<span class="mute">unknown</span>
{/if}
</span>
<span
class="figure metric figure-speed"
title={tps !== null ? `${tps} tokens per second` : "generation speed: not reported"}
>
<Gauge size={13} stroke-width={1.5} />
{#if !m.reported}
<span class="mute">unknown</span>
{:else if tps !== null}
<span><span class="num">{tps}</span> t/s</span>
{:else}
<span class="mute">unknown</span>
{/if}
</span>
{#if m.route}
<span class="figure metric figure-route mute">{m.route}</span>
{/if}
</div>
</div>
<style>
.metrics {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
font-family: var(--mono);
font-size: var(--fs-1);
color: var(--fg);
line-height: 1;
white-space: nowrap;
}
.cluster-context,
.cluster-telemetry {
display: inline-flex;
align-items: center;
gap: var(--s-4);
flex-shrink: 0;
}
.figure {
display: inline-flex;
align-items: center;
gap: var(--s-2);
flex-shrink: 0;
}
/* Drop figures gracefully when narrow (CSS only) */
/* shed the least important figure first, measured against the bar that holds them */
@container (max-width: 830px) {
.figure-route { display: none; }
}
@container (max-width: 700px) {
.figure-speed { display: none; }
}
@container (max-width: 600px) {
.figure-cache { display: none; }
}
/* last resort: the battery still says how full the context is, the numbers move to its title */
@container (max-width: 520px) {
.figure-context { display: none; }
}
</style>