refactor(ui): give the collapsed log drawer its own sidebar toggle

Open, the trace sidebar is byte-identical to before: the toggle sits over its
header exactly where it did, and the header keeps the padding that makes room
for it. Collapsed, that button has nowhere to live, so the drawer header shows
one instead, on the model row or the request id row when the log names no
model.

Both come from SidebarToggle, so they cannot drift in design. The chevrons now
point the way the sidebar will move: right while it is open, left while it is
collapsed.
This commit is contained in:
Yuneng Jiang 2026-08-29 11:53:14 -07:00
parent 20e6d6457a
commit 4a3dcd5e8e
No known key found for this signature in database
2 changed files with 88 additions and 82 deletions

View file

@ -312,90 +312,94 @@ export function LogDetailsDrawer({
<SheetTitle className="sr-only">
{logEntry?.request_id ? `Request ${logEntry.request_id} details` : "Request details"}
</SheetTitle>
<div style={{ height: "100%" }} className="flex">
<div style={{ height: "100%" }} className="flex relative">
{!isSidebarCollapsed && (
<div className="border-r border-border bg-muted flex flex-col shrink-0" style={{ width: SIDEBAR_WIDTH_PX }}>
<div className="flex items-start gap-1 py-2 pl-1 pr-3 border-b border-border bg-card">
<SidebarToggle
isCollapsed={isSidebarCollapsed}
onToggle={() => setIsSidebarCollapsed((collapsed) => !collapsed)}
/>
<div className="min-w-0 flex-1">
<div className="text-[10px] uppercase tracking-wide text-muted-foreground">
{isSessionMode ? "Session" : "Trace"}
<SidebarToggle
isCollapsed={false}
onToggle={() => setIsSidebarCollapsed(true)}
className="absolute top-2 left-2 z-raised"
/>
)}
{!isSidebarCollapsed && (
<div className="border-r border-border bg-muted flex flex-col" style={{ width: SIDEBAR_WIDTH_PX }}>
<div className="pl-12 pr-3 py-2 border-b border-border bg-card">
<div className="flex items-start justify-between gap-2">
<div>
<div className="text-[10px] uppercase tracking-wide text-muted-foreground">
{isSessionMode ? "Session" : "Trace"}
</div>
<div className="font-mono text-[12px] text-foreground leading-tight flex items-center gap-1">
<span className="truncate">{leftPanelDisplayId}</span>
<button
type="button"
onClick={handleCopyLeftPanelId}
className="text-muted-foreground hover:text-foreground"
aria-label="Copy trace id"
>
{copiedLeftPanelId ? <Check className="size-3" /> : <Copy className="size-3" />}
</button>
</div>
</div>
<div className="font-mono text-[12px] text-foreground leading-tight flex items-center gap-1">
<span className="truncate">{leftPanelDisplayId}</span>
<button
type="button"
onClick={handleCopyLeftPanelId}
className="text-muted-foreground hover:text-foreground"
aria-label="Copy trace id"
>
{copiedLeftPanelId ? <Check className="size-3" /> : <Copy className="size-3" />}
</button>
</div>
<div className="mt-1 text-[11px] text-muted-foreground font-mono">
{logsForList.length} req
{[
isSessionMode
? llmCount
: logsForList.filter(
(row) =>
!MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type),
).length,
isSessionMode
? agentCount
: logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length,
isSessionMode
? mcpCount
: logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
].map((count, i) => {
const label = [" LLM", " Agent", " MCP"][i];
return count > 0 ? (
<span key={label}>
<span className="mx-1.5">·</span>
{count}
{label}
</span>
) : null;
})}
<span className="mx-1.5">·</span>
{isSessionMode ? getSpendString(totalSessionCost) : getSpendString(currentLog.spend || 0)}
{isSessionMode && (
<>
</div>
<div className="mt-1 text-[11px] text-muted-foreground font-mono">
{logsForList.length} req
{[
isSessionMode
? llmCount
: logsForList.filter(
(row) => !MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type),
).length,
isSessionMode
? agentCount
: logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length,
isSessionMode
? mcpCount
: logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
].map((count, i) => {
const label = [" LLM", " Agent", " MCP"][i];
return count > 0 ? (
<span key={label}>
<span className="mx-1.5">·</span>
{sessionDurationSeconds}s
</>
)}
</div>
{count}
{label}
</span>
) : null;
})}
<span className="mx-1.5">·</span>
{isSessionMode ? getSpendString(totalSessionCost) : getSpendString(currentLog.spend || 0)}
{isSessionMode && (
<div className="text-[11px] text-muted-foreground font-mono whitespace-nowrap">
{cacheHitCount}/{logsForList.length} cached
</div>
)}
{isSessionMode && sessionTruncated && (
<div className="mt-1 text-[11px] text-warning font-mono">
Showing most recent {logsForList.length} of {sessionTotalCount}
</div>
)}
{isSessionMode && (
<Tabs
className="mt-1.5"
value={sessionSortMode}
onValueChange={(value) => setSessionSortMode(value as SessionLogSortMode)}
>
<TabsList className="w-full">
<TabsTrigger value="duration" className="text-[11px]">
Duration
</TabsTrigger>
<TabsTrigger value="start_time" className="text-[11px]">
Start time
</TabsTrigger>
</TabsList>
</Tabs>
<>
<span className="mx-1.5">·</span>
{sessionDurationSeconds}s
</>
)}
</div>
{isSessionMode && (
<div className="text-[11px] text-muted-foreground font-mono whitespace-nowrap">
{cacheHitCount}/{logsForList.length} cached
</div>
)}
{isSessionMode && sessionTruncated && (
<div className="mt-1 text-[11px] text-warning font-mono">
Showing most recent {logsForList.length} of {sessionTotalCount}
</div>
)}
{isSessionMode && (
<Tabs
className="mt-1.5"
value={sessionSortMode}
onValueChange={(value) => setSessionSortMode(value as SessionLogSortMode)}
>
<TabsList className="w-full">
<TabsTrigger value="duration" className="text-[11px]">
Duration
</TabsTrigger>
<TabsTrigger value="start_time" className="text-[11px]">
Start time
</TabsTrigger>
</TabsList>
</Tabs>
)}
</div>
<div className="flex-1 overflow-y-auto">

View file

@ -1,21 +1,23 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cva.config";
export interface SidebarToggleProps {
isCollapsed: boolean;
onToggle: () => void;
className?: string;
}
export function SidebarToggle({ isCollapsed, onToggle }: SidebarToggleProps) {
export function SidebarToggle({ isCollapsed, onToggle, className }: SidebarToggleProps) {
return (
<Button
variant="ghost"
size="icon-xs"
size="icon-sm"
onClick={onToggle}
className="shrink-0"
className={cn("shrink-0 bg-card! border! border-border! rounded-md!", className)}
aria-label={isCollapsed ? "Expand trace sidebar" : "Collapse trace sidebar"}
>
{isCollapsed ? <ChevronRight className="size-4" /> : <ChevronLeft className="size-4" />}
{isCollapsed ? <ChevronLeft className="size-4" /> : <ChevronRight className="size-4" />}
</Button>
);
}