diff --git a/console/src/components/turn-detail/call-card.tsx b/console/src/components/turn-detail/call-card.tsx index 69027d0..4e7d00b 100644 --- a/console/src/components/turn-detail/call-card.tsx +++ b/console/src/components/turn-detail/call-card.tsx @@ -1,5 +1,5 @@ import { useState } from "react" -import { ChevronRight, ChevronDown, Loader2 } from "lucide-react" +import { ChevronRight, ChevronDown, Layers, Loader2 } from "lucide-react" import { cn } from "@/lib/utils" import { formatMs, formatNumber } from "@/lib/format" import { Markdown } from "@/components/ui/markdown" @@ -29,6 +29,11 @@ interface Props { active?: boolean defaultExpanded?: boolean onOpenDetail?: (id: string) => void + /** When this call is the canonical leg of a folded proxy duplicate + * pair (e.g. one captured copy of a LiteLLM→upstream hop is hidden + * under it), >0 number of hops folded into this row. Renders a + * small "+N hop" chip in the header so the fold is discoverable. */ + hopCount?: number } export function CallCard({ @@ -39,6 +44,7 @@ export function CallCard({ active, defaultExpanded, onOpenDetail, + hopCount = 0, }: Props) { const [expanded, setExpanded] = useState(Boolean(defaultExpanded)) const speed = classify(call) @@ -87,6 +93,15 @@ export function CallCard({ finalCallId={turn.final_call_id} /> {call.model} + {hopCount > 0 && ( + + + +{hopCount} + + )} void + /** When the parent panel folds call-level proxy duplicates, this map + * tells GanttNav which canonical call ids carry hidden hops so it + * can stack a small "+N" indicator on those bars. Empty map (default) + * keeps the timeline a flat per-call view. */ + hopsByCanonical?: Map } const SLOW_THRESHOLD_MS = 10_000 @@ -25,7 +32,53 @@ function classifySpeed(call: AgentTurnCallItem): "normal" | "slow" | "warn" | "e } -export function GanttNav({ turn, calls, activeSequence, onSelect }: Props) { +/** + * Compact badge surfaced under the Timeline header when the turn is one + * leg of a proxy group. Tells the user "this turn is folded together + * with N other captured legs — see the Proxy view tab for the merged + * view". Color follows the role-tone palette used by `ProxyBadge` in + * the agent-turns list so the two callsites are visually consistent. + */ +function MultiLegBadge({ + proxy, + groupSize, +}: { + proxy: ReturnType + groupSize: number +}) { + if (!proxy) return null + const role = proxy.role + const isPrimary = role === "proxy_in" || role === "mirror_primary" + const Icon = isPrimary ? ArrowLeftRight : Copy + const label = + role === "proxy_in" + ? "via proxy" + : role === "proxy_out" + ? "proxy hop" + : role === "mirror_primary" + ? "mirrored" + : "mirror copy" + const peers = proxy.peer_turn_ids ?? (proxy.peer_turn_id ? [proxy.peer_turn_id] : []) + const title = peers.length > 0 + ? `${label} — group of ${groupSize} captured legs:\n${peers.join("\n")}` + : label + return ( +
+ + {groupSize}-leg {label} +
+ ) +} + +export function GanttNav({ turn, calls, activeSequence, onSelect, hopsByCanonical }: Props) { const { minStart, total } = useMemo(() => { if (calls.length === 0) return { minStart: turn.start_time, total: turn.duration_ms || 1 } const min = Math.min(...calls.map((c) => c.request_time)) @@ -38,11 +91,15 @@ export function GanttNav({ turn, calls, activeSequence, onSelect }: Props) { [calls, turn.final_call_id], ) + const proxy = readProxyMeta(turn.metadata) + const groupSize = proxyGroupSize(proxy) + return (