Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ export function ConversationRowStats(props: {
const runtime = formatDurationTotal(
props.conversation.turns.map((turn) => turn.cumulativeDurationMs),
);
const turnCount = props.conversation.turns.length;
const primaryStats = [
`${props.conversation.turns.length} turns`,
`${turnCount} ${turnCount === 1 ? "turn" : "turns"}`,
tokens,
runtime ? `${runtime} runtime` : undefined,
runtime ? `${runtime} runtime` : "",
].filter(Boolean);
const secondaryStats = [
props.timeLabel,
Expand Down
159 changes: 159 additions & 0 deletions packages/junior-dashboard/src/client/components/Metric.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {
useId,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";

import { cn } from "../styles";

export type MetricTooltipLine = {
label?: string;
labelStyle?: "code";
value: string;
};

export type MetricListItem = {
content: ReactNode;
key: string;
};

type TooltipPosition = {
left: number;
top: number;
width: number;
};

function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}

function tooltipPosition(
trigger: HTMLElement,
align: "left" | "right" | undefined,
): TooltipPosition {
const margin = 16;
const viewportWidth = window.innerWidth;
const width = Math.min(320, Math.max(256, viewportWidth - margin * 2));
const rect = trigger.getBoundingClientRect();
const preferredLeft = align === "right" ? rect.right - width : rect.left;
return {
left: Math.round(
clamp(preferredLeft, margin, viewportWidth - width - margin),
),
top: Math.round(rect.bottom + 8),
width,
};
}

/** Render compact metadata text with an optional styled hover/focus tooltip. */
export function MetricValue(props: {
align?: "left" | "right";
children: ReactNode;
className?: string;
tooltip?: MetricTooltipLine[];
}) {
const tooltipId = useId();
const triggerRef = useRef<HTMLSpanElement>(null);
const [position, setPosition] = useState<TooltipPosition | null>(null);
const tooltip = props.tooltip?.filter((line) => line.value.trim());
if (!tooltip?.length) {
return <span className={props.className}>{props.children}</span>;
}

const showTooltip = () => {
if (!triggerRef.current) return;
setPosition(tooltipPosition(triggerRef.current, props.align));
};
const hideTooltip = () => setPosition(null);
const tooltipStyle: CSSProperties | undefined = position
? {
left: position.left,
top: position.top,
width: position.width,
}
: undefined;

return (
<span className={cn("relative inline-flex", props.className)}>
<span
aria-describedby={position ? tooltipId : undefined}
className="border-b border-dotted border-white/20 outline-none transition-colors hover:border-white/45 focus-visible:border-white/45"
onBlur={hideTooltip}
onFocus={showTooltip}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
ref={triggerRef}
tabIndex={0}
>
{props.children}
</span>
{position ? (
<span
className="pointer-events-none fixed z-30 border border-white/15 bg-[#050505] px-3 py-2 text-left text-[0.76rem] font-normal leading-relaxed text-[#b8b8b8] shadow-xl shadow-black/35"
id={tooltipId}
role="tooltip"
style={tooltipStyle}
>
<span className="grid max-h-72 grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-1.5 overflow-y-auto">
{tooltip.map((line, index) => (
<span
className={
line.label
? "contents"
: "col-span-2 block min-w-0 break-words text-[#d6d6d6]"
}
key={`${index}-${line.label ?? ""}-${line.value}`}
>
{line.label ? (
<span
className={cn(
"min-w-0 break-words font-medium text-[#888]",
line.labelStyle === "code" &&
"break-all font-mono text-[0.74rem] text-[#d6d6d6]",
)}
>
{line.label}
</span>
) : null}
{line.label ? (
<span className="whitespace-nowrap text-right text-[#d6d6d6]">
{line.value}
</span>
) : (
line.value
)}
</span>
))}
</span>
</span>
) : null}
</span>
);
}

/** Render inline metadata with consistent dot spacing across dashboard headers. */
export function MetricList(props: {
className?: string;
items: MetricListItem[];
}) {
return (
<div
className={cn(
"flex flex-wrap items-center gap-x-1.5 gap-y-1",
props.className,
)}
>
{props.items.map((item, index) => (
<span
className="inline-flex min-w-0 items-center gap-x-1.5"
key={item.key}
>
{index > 0 ? <span className="text-[#666]">·</span> : null}
<span className="min-w-0">{item.content}</span>
</span>
))}
</div>
);
}
124 changes: 124 additions & 0 deletions packages/junior-dashboard/src/client/components/TelemetryMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
formatCompactNumber,
formatMs,
formatTime,
formatTokenSummary,
type MessageSummary,
type TokenUsageSummary,
type ToolCallSummary,
} from "../format";
import { MetricValue, type MetricTooltipLine } from "./Metric";

function plural(label: string, count: number): string {
return `${formatCompactNumber(count)} ${label}${count === 1 ? "" : "s"}`;
}
Comment thread
cursor[bot] marked this conversation as resolved.

function isMetricTooltipLine(
line: MetricTooltipLine | undefined,
): line is MetricTooltipLine {
return Boolean(line);
}

function tokenTooltip(summary: TokenUsageSummary): MetricTooltipLine[] {
const lines: Array<MetricTooltipLine | undefined> = [
summary.inputTokens !== undefined
? { label: "input", value: formatCompactNumber(summary.inputTokens) }
: undefined,
summary.outputTokens !== undefined
? { label: "output", value: formatCompactNumber(summary.outputTokens) }
: undefined,
summary.cachedInputTokens !== undefined
? {
label: "cached",
value: formatCompactNumber(summary.cachedInputTokens),
}
: undefined,
summary.cacheCreationTokens !== undefined
? {
label: "cache write",
value: formatCompactNumber(summary.cacheCreationTokens),
}
: undefined,
summary.providerTotalTokens !== undefined
? {
label: "provider",
value: formatCompactNumber(summary.providerTotalTokens),
}
: undefined,
];
return lines.filter(isMetricTooltipLine);
}

/** Render total token usage with a hoverable breakdown. */
export function TokenMetric(props: {
align?: "left" | "right";
summary: TokenUsageSummary | undefined;
}) {
if (!props.summary) return null;
return (
<MetricValue align={props.align} tooltip={tokenTooltip(props.summary)}>
{formatTokenSummary(props.summary)}
</MetricValue>
);
}

/** Render a duration value with start/end timestamps in the tooltip. */
export function DurationMetric(props: {
align?: "left" | "right";
endedAt?: string;
label: string;
startedAt?: string;
}) {
if (!props.label || props.label === "none") return null;
const lines: Array<MetricTooltipLine | undefined> = [
props.startedAt
? { label: "started", value: formatTime(props.startedAt) }
: undefined,
props.endedAt
? { label: "ended", value: formatTime(props.endedAt) }
: undefined,
];
const tooltip = lines.filter(isMetricTooltipLine);
return (
<MetricValue align={props.align} tooltip={tooltip}>
{props.label}
</MetricValue>
);
}

/** Render a tool-call count with top tool names, counts, and matched duration. */
export function ToolCallsMetric(props: {
align?: "left" | "right";
loading?: boolean;
summary: ToolCallSummary | undefined;
}) {
if (props.loading) return <span>tool calls loading</span>;
if (!props.summary || props.summary.total <= 0) return null;
const tooltip = props.summary.items.map((item) => ({
label: item.name,
labelStyle: "code" as const,
value: [
plural("call", item.count),
item.totalDurationMs !== undefined
? formatMs(item.totalDurationMs)
: undefined,
]
.filter(Boolean)
.join(" · "),
}));
return (
<MetricValue align={props.align} tooltip={tooltip}>
{plural("tool call", props.summary.total)}
</MetricValue>
);
}

/** Render a conversational message count. */
export function MessagesMetric(props: {
loading?: boolean;
summary: MessageSummary | undefined;
}) {
if (props.loading) return <span>messages loading</span>;
if (!props.summary) return null;
return <MetricValue>{plural("message", props.summary.total)}</MetricValue>;
}
6 changes: 3 additions & 3 deletions packages/junior-dashboard/src/client/components/ToolFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ export function ToolFrame(props: {

/** Provide the shared transcript tool-frame shell for nonstandard part views. */
export function toolFrameClass(): string {
return "border border-[#beaaff]/20 bg-[#111] transition-colors hover:border-[#beaaff]/45 hover:bg-[rgba(190,170,255,0.06)]";
return "border-l border-[#beaaff]/20 pl-3 transition-colors hover:border-[#beaaff]/40";
}

function toolHeaderClass(interactive: boolean): string {
return cn(
"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 px-3 py-2 font-mono text-[0.84rem] leading-tight text-[#b8b8b8] max-md:grid-cols-1 max-md:gap-1",
"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 py-1.5 font-mono text-[0.82rem] leading-tight text-[#b8b8b8] max-md:grid-cols-1 max-md:gap-1",
interactive
? "cursor-pointer hover:bg-[rgba(190,170,255,0.07)]"
? "cursor-pointer list-none transition-colors hover:text-[#d6d6d6] [&::-webkit-details-marker]:hidden"
: "cursor-default",
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export function TranscriptToolView(props: {
? formatMs(props.resultTimestamp - props.timestamp)
: undefined;
const meta = [
props.timestamp ? formatMessageTimestamp(props.timestamp) : undefined,
typeof props.timestamp === "number"
? formatMessageTimestamp(props.timestamp)
: undefined,
duration,
props.result ? formatBytes(outputBytes) : undefined,
props.result ? undefined : "missing result",
Expand All @@ -51,7 +53,7 @@ export function TranscriptToolView(props: {
meta={meta}
raw
signature={
<strong className="min-w-0 break-words font-bold text-white">
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
{toolName}
</strong>
}
Expand All @@ -74,7 +76,7 @@ export function TranscriptToolView(props: {
meta={meta}
signature={
<>
<strong className="min-w-0 break-words font-bold text-white">
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
{toolName}
</strong>
{isPreviewableValue(input) ? (
Expand Down Expand Up @@ -113,12 +115,12 @@ function ToolBodySection(props: {
return (
<div
className={cn(
"border-t border-white/10 px-3",
props.padded === false ? "" : "py-3",
"border-t border-white/10",
props.padded === false ? "" : "py-2",
)}
>
{props.label ? (
<div className="pb-2 font-mono text-[0.78rem] uppercase leading-none text-[#888]">
<div className="pb-2 font-mono text-[0.78rem] leading-none text-[#888]">
{props.label}
</div>
) : null}
Expand Down Expand Up @@ -173,7 +175,7 @@ function ToolArgEntry(props: { index: number; name: string; value: string }) {
return (
<span>
{props.index > 0 ? <span className="text-[#888]">, </span> : null}
<span className="text-white">{props.name}</span>
<span className="text-[#d6d6d6]">{props.name}</span>
<span className="text-[#888]">: </span>
<ToolArgValue value={props.value} />
</span>
Expand Down
Loading
Loading