@tonyclaw/agent-inspector 2.1.15 → 2.1.17

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.
Files changed (73) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
  7. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
  9. package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
  10. package/.output/public/assets/index-Blqvndcn.js +1 -0
  11. package/.output/public/assets/index-Cs79WRQj.css +1 -0
  12. package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
  14. package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +255 -201
  16. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
  19. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
  21. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
  23. package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
  26. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
  27. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
  28. package/.output/server/index.mjs +63 -63
  29. package/README.md +57 -2
  30. package/package.json +1 -1
  31. package/src/cli/startupOutput.ts +16 -0
  32. package/src/cli.ts +61 -15
  33. package/src/components/ProxyViewer.tsx +535 -163
  34. package/src/components/ProxyViewerContainer.tsx +77 -67
  35. package/src/components/alerts/AlertsDialog.tsx +2 -1
  36. package/src/components/clients/ClientLogo.tsx +1 -1
  37. package/src/components/groups/GroupsDialog.tsx +19 -1
  38. package/src/components/providers/ProviderCard.tsx +133 -32
  39. package/src/components/providers/ProviderForm.tsx +61 -29
  40. package/src/components/providers/ProviderLogo.tsx +6 -1
  41. package/src/components/providers/ProvidersPanel.tsx +7 -7
  42. package/src/components/providers/SettingsDialog.tsx +13 -6
  43. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  44. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  45. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  46. package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
  47. package/src/components/proxy-viewer/LogEntry.tsx +130 -43
  48. package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
  49. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  50. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  51. package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
  52. package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
  53. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  54. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  55. package/src/components/proxy-viewer/viewerState.ts +0 -4
  56. package/src/components/ui/icon-trigger.ts +2 -0
  57. package/src/components/ui/transient-toast.tsx +104 -0
  58. package/src/lib/providerTestContract.ts +56 -10
  59. package/src/lib/providerTestPrompt.ts +23 -1
  60. package/src/lib/sessionInfoContract.ts +4 -0
  61. package/src/lib/upstreamUrl.ts +105 -1
  62. package/src/mcp/server.ts +71 -27
  63. package/src/mcp/toolHandlers.ts +24 -5
  64. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  65. package/src/proxy/sessionInfo.ts +31 -2
  66. package/src/proxy/sessionSupervisor.ts +20 -1
  67. package/src/proxy/store.ts +1 -1
  68. package/src/routes/api/providers.$providerId.test.log.ts +93 -14
  69. package/styles/globals.css +180 -180
  70. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  71. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  72. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  73. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -39,7 +39,7 @@ export function ProviderLogoStack({
39
39
  <TooltipTrigger asChild>
40
40
  <span
41
41
  className={cn(
42
- "inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-white/10 bg-black/20 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]",
42
+ "border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md",
43
43
  index > 0 && "-ml-2",
44
44
  )}
45
45
  >
@@ -50,7 +50,7 @@ export function ProviderLogoStack({
50
50
  </Tooltip>
51
51
  ))}
52
52
  {overflowCount > 0 && (
53
- <span className="-ml-2 inline-flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md border border-white/10 bg-black/40 px-1 font-mono text-[10px] text-muted-foreground">
53
+ <span className="border border-border bg-muted/40 -ml-2 inline-flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md px-1 font-mono text-[10px] text-muted-foreground">
54
54
  +{overflowCount}
55
55
  </span>
56
56
  )}
@@ -101,7 +101,7 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
101
101
 
102
102
  return (
103
103
  <Collapsible open={open} onOpenChange={setOpen}>
104
- <div className="rounded-md border border-white/10 bg-black/20 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
104
+ <div className="border border-border bg-card shadow-sm rounded-[8px]">
105
105
  <div className="flex min-w-0 items-center gap-2 px-2.5 py-2">
106
106
  <button
107
107
  type="button"
@@ -140,7 +140,7 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
140
140
  </button>
141
141
  <button
142
142
  type="button"
143
- className="inline-flex size-7 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
143
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
144
144
  onClick={nameCopy.copy}
145
145
  aria-label={nameCopy.copied ? "Copied tool name" : "Copy tool name"}
146
146
  >
@@ -153,13 +153,13 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
153
153
  </div>
154
154
  <CollapsibleContent>
155
155
  {open && (
156
- <div className="space-y-2 border-t border-white/10 bg-white/[0.02] px-2.5 py-2">
156
+ <div className="space-y-2 border-t border-white/10 bg-white/[0.025] px-2.5 py-2">
157
157
  {tool.description !== null && (
158
158
  <p className="text-xs leading-relaxed text-muted-foreground">{tool.description}</p>
159
159
  )}
160
160
  <RequiredParameters names={tool.requiredParameters} />
161
161
  {tool.schema !== null && (
162
- <ScrollArea className="max-h-64 rounded-md border border-white/10 bg-[#06080b] p-2">
162
+ <ScrollArea className="border border-border bg-card shadow-sm max-h-64 rounded-[8px] p-2">
163
163
  <Suspense
164
164
  fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
165
165
  >
@@ -203,7 +203,7 @@ export const RequestTools = memo(function RequestTools({
203
203
  const hiddenPreviewCount = summary.tools.length - previewTools.length;
204
204
 
205
205
  return (
206
- <section className="mb-3 overflow-hidden rounded-md border border-white/10 bg-[#080b0f] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
206
+ <section className="border border-border bg-card shadow-sm mb-3 overflow-hidden rounded-[8px]">
207
207
  <div className="flex min-w-0 flex-wrap items-center gap-2 px-3 py-2">
208
208
  <div className="flex min-w-0 flex-1 items-center gap-2">
209
209
  <Wrench className="size-4 shrink-0 text-sky-300/80" />
@@ -242,7 +242,7 @@ export const RequestTools = memo(function RequestTools({
242
242
  type="button"
243
243
  variant="ghost"
244
244
  size="sm"
245
- className="h-7 border border-white/10 bg-black/20 px-2 text-muted-foreground hover:border-cyan-300/30 hover:bg-cyan-400/10 hover:text-cyan-100"
245
+ className="h-7 px-2 text-muted-foreground hover:text-cyan-100"
246
246
  onClick={() => setExpanded((value) => !value)}
247
247
  aria-expanded={expanded}
248
248
  >
@@ -251,7 +251,7 @@ export const RequestTools = memo(function RequestTools({
251
251
  </Button>
252
252
  </div>
253
253
 
254
- <div className="flex min-w-0 flex-wrap gap-1.5 border-t border-white/10 bg-black/10 px-3 py-2">
254
+ <div className="flex min-w-0 flex-wrap gap-1.5 border-t border-white/10 bg-white/[0.025] px-3 py-2">
255
255
  {previewTools.map((tool) => (
256
256
  <Badge
257
257
  key={tool.name}
@@ -272,8 +272,8 @@ export const RequestTools = memo(function RequestTools({
272
272
  </div>
273
273
 
274
274
  {expanded && (
275
- <div className="space-y-2 border-t border-white/10 bg-black/15 px-3 py-3">
276
- <label className="flex h-8 items-center gap-2 rounded-md border border-white/10 bg-[#06080b] px-2 text-xs">
275
+ <div className="space-y-2 border-t border-white/10 bg-black/10 px-3 py-3">
276
+ <label className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex h-8 items-center gap-2 rounded-md px-2 text-xs">
277
277
  <Search className="size-3.5 shrink-0 text-muted-foreground" />
278
278
  <input
279
279
  value={query}
@@ -1,7 +1,6 @@
1
- import { type JSX, useMemo } from "react";
2
- import { cn } from "../../lib/utils";
1
+ import type { JSX, KeyboardEvent, MouseEvent } from "react";
3
2
  import { isTurnBoundary, type StopReason } from "../../lib/stopReason";
4
- import { getCrabVariant, getInteriorCrabVariant } from "../ui/crab-variants";
3
+ import { cn } from "../../lib/utils";
5
4
 
6
5
  export type ThreadConnectorProps = {
7
6
  stopReason: StopReason;
@@ -11,13 +10,13 @@ export type ThreadConnectorProps = {
11
10
  isTurnStart: boolean;
12
11
  /** True when this entry is the turn's only request. */
13
12
  isOnlyEntry?: boolean;
14
- /** Seed for crab variant selection (0-11). */
15
- crabIndex?: number;
16
- /** When true the crab is clickable (collapse / expand a turn). */
13
+ /** When true the timeline marker is clickable (collapse / expand a turn). */
17
14
  collapsible?: boolean;
18
15
  onToggle?: () => void;
19
16
  };
20
17
 
18
+ type MarkerTone = "start" | "end" | "fused" | "running" | "idle";
19
+
21
20
  function boundaryTitle(stopReason: StopReason): string {
22
21
  switch (stopReason) {
23
22
  case "end_turn":
@@ -32,104 +31,124 @@ function boundaryTitle(stopReason: StopReason): string {
32
31
  }
33
32
  }
34
33
 
35
- /**
36
- * Vertical timeline connector. Top spacer uses a calc() that
37
- * mixes rem (py-1 + half-line) and px (border) so the crab
38
- * centre-line matches the LogEntry display number regardless of the
39
- * root font-size. The bottom spacer is flex-1 so the outgoing
40
- * line adapts to whatever height the LogEntry occupies.
41
- */
34
+ function markerTone({
35
+ isFusedBoundary,
36
+ isBoundary,
37
+ isTurnStart,
38
+ isRunning,
39
+ }: {
40
+ isFusedBoundary: boolean;
41
+ isBoundary: boolean;
42
+ isTurnStart: boolean;
43
+ isRunning: boolean;
44
+ }): MarkerTone {
45
+ if (isFusedBoundary) return "fused";
46
+ if (isBoundary) return "end";
47
+ if (isTurnStart) return "start";
48
+ if (isRunning) return "running";
49
+ return "idle";
50
+ }
51
+
52
+ function markerClass(tone: MarkerTone, interactive: boolean): string {
53
+ const base =
54
+ "inline-flex size-3.5 items-center justify-center rounded-full border transition-colors";
55
+ const cursor = interactive ? "cursor-pointer hover:border-cyan-200/70 hover:bg-cyan-300/20" : "";
56
+
57
+ switch (tone) {
58
+ case "start":
59
+ return cn(base, cursor, "border-cyan-300/50 bg-cyan-300/12 text-cyan-200");
60
+ case "end":
61
+ return cn(base, cursor, "border-amber-300/55 bg-amber-300/12 text-amber-200");
62
+ case "fused":
63
+ return cn(
64
+ base,
65
+ cursor,
66
+ "border-slate-200/50 bg-slate-200/10 text-slate-100 ring-2 ring-cyan-300/15",
67
+ );
68
+ case "running":
69
+ return cn(
70
+ base,
71
+ cursor,
72
+ "border-amber-300/45 bg-amber-300/10 text-amber-200 ring-2 ring-amber-300/10",
73
+ );
74
+ case "idle":
75
+ return cn(base, cursor, "border-muted-foreground/25 bg-muted/30 text-muted-foreground/60");
76
+ }
77
+ }
78
+
79
+ function markerInnerClass(tone: MarkerTone): string {
80
+ switch (tone) {
81
+ case "running":
82
+ return "size-1.5 rounded-full bg-current animate-pulse";
83
+ case "idle":
84
+ return "size-1 rounded-full bg-current opacity-55";
85
+ case "start":
86
+ case "end":
87
+ case "fused":
88
+ return "size-1.5 rounded-full bg-current";
89
+ }
90
+ }
91
+
92
+ function markerTitle(tone: MarkerTone, stopReason: StopReason): string {
93
+ switch (tone) {
94
+ case "start":
95
+ return "Start of turn";
96
+ case "end":
97
+ return boundaryTitle(stopReason);
98
+ case "fused":
99
+ return "Single-request turn";
100
+ case "running":
101
+ return "Processing";
102
+ case "idle":
103
+ return "Request in turn";
104
+ }
105
+ }
106
+
42
107
  export function ThreadConnector({
43
108
  stopReason,
44
109
  isPending,
45
110
  isFirst,
46
111
  isTurnStart,
47
112
  isOnlyEntry = false,
48
- crabIndex = 0,
49
113
  collapsible = false,
50
114
  onToggle,
51
115
  }: ThreadConnectorProps): JSX.Element {
52
116
  const isBoundary = isTurnBoundary(stopReason);
53
117
  const isFusedBoundary = isOnlyEntry && isTurnStart && isBoundary;
54
118
  const isRunning = isPending && !isBoundary;
55
- const Crab = useMemo(() => getCrabVariant(crabIndex), [crabIndex]);
56
- const FusedCrab = useMemo(() => getInteriorCrabVariant(crabIndex), [crabIndex]);
119
+ const tone = markerTone({ isFusedBoundary, isBoundary, isTurnStart, isRunning });
120
+ const interactive = collapsible && onToggle !== undefined;
121
+
122
+ const handleMarkerClick = (event: MouseEvent<HTMLSpanElement>): void => {
123
+ if (!interactive || onToggle === undefined) return;
124
+ event.stopPropagation();
125
+ onToggle();
126
+ };
57
127
 
58
- const interactiveProps =
59
- collapsible && onToggle
60
- ? ({
61
- role: "button" as const,
62
- tabIndex: 0,
63
- className: "cursor-pointer",
64
- onClick: (e: React.MouseEvent) => {
65
- e.stopPropagation();
66
- onToggle();
67
- },
68
- onKeyDown: (e: React.KeyboardEvent) => {
69
- if (e.key === "Enter" || e.key === " ") {
70
- e.preventDefault();
71
- onToggle();
72
- }
73
- },
74
- } as const)
75
- : ({} as const);
128
+ const handleMarkerKeyDown = (event: KeyboardEvent<HTMLSpanElement>): void => {
129
+ if (!interactive || onToggle === undefined) return;
130
+ if (event.key !== "Enter" && event.key !== " ") return;
131
+ event.preventDefault();
132
+ onToggle();
133
+ };
76
134
 
77
135
  return (
78
136
  <div className="flex flex-col items-center w-6 shrink-0 pt-0.5 pb-0.5">
79
- {/* Top spacer — crab centre must land on the LogEntry index midline.
80
- * Index midline from container top = border(1px) + py-1(0.25rem) + ½line(0.5rem)
81
- * = 1px + 0.75rem. Crab centre = pt(2px) + spacer + 7px(half crab).
82
- * ∴ spacer = 0.75rem - 8px. (at 16px root: 12-8=4px; 2+4+7=13px). */}
83
137
  <div className="flex justify-center h-[calc(0.75rem-8px)]">
84
138
  {!isFirst && <div className="w-0.5 bg-muted-foreground/30 h-full" />}
85
139
  </div>
86
140
 
87
- {/* Crab — pinned to the fixed offset above so it never drifts
88
- * when the LogEntry expands / collapses. Clickable when the
89
- * turn is collapsible (replaces the old chevron toggle). */}
90
- {isFusedBoundary ? (
91
- <span title="Start and end of turn">
92
- <FusedCrab
93
- className={cn(
94
- "size-3.5 text-[#80FF00]",
95
- "animate-crab-settle",
96
- "drop-shadow-[0_0_4px_rgba(128,255,0,0.5)]",
97
- )}
98
- />
99
- </span>
100
- ) : isBoundary ? (
101
- <span title={boundaryTitle(stopReason)} {...interactiveProps}>
102
- <Crab
103
- className={cn(
104
- "size-3.5 text-amber-400",
105
- "animate-crab-settle",
106
- "drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
107
- )}
108
- />
109
- </span>
110
- ) : isTurnStart ? (
111
- <span title="Start of turn" {...interactiveProps}>
112
- <Crab
113
- className={cn(
114
- "size-3.5 text-emerald-400",
115
- "animate-crab-appear",
116
- "drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
117
- )}
118
- />
119
- </span>
120
- ) : isRunning ? (
121
- <span title="Processing…">
122
- <Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
123
- </span>
124
- ) : (
125
- <span>
126
- <Crab className="size-3.5 text-muted-foreground/40" />
127
- </span>
128
- )}
141
+ <span
142
+ role={interactive ? "button" : undefined}
143
+ tabIndex={interactive ? 0 : undefined}
144
+ title={markerTitle(tone, stopReason)}
145
+ className={markerClass(tone, interactive)}
146
+ onClick={handleMarkerClick}
147
+ onKeyDown={handleMarkerKeyDown}
148
+ >
149
+ <span className={markerInnerClass(tone)} />
150
+ </span>
129
151
 
130
- {/* Bottom spacer — flex-1 fills whatever height the LogEntry
131
- * consumes (header + expanded content). Turn boundaries have
132
- * no outgoing line. */}
133
152
  <div className="flex-1 flex justify-center min-h-0">
134
153
  {!isBoundary && (
135
154
  <div
@@ -13,9 +13,7 @@ import { isTurnBoundary } from "../../lib/stopReason";
13
13
  import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
14
14
  import { cn, formatTokens } from "../../lib/utils";
15
15
  import type { CapturedLog } from "../../contracts";
16
- import { getCrabVariant } from "../ui/crab-variants";
17
16
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
18
- import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
19
17
  import type { CacheTrendEntry } from "./cacheTrend";
20
18
  import { LogEntry } from "./LogEntry";
21
19
  import {
@@ -48,6 +46,8 @@ export type TurnGroupProps = {
48
46
  turnIndex?: number;
49
47
  timeDisplayFormat: TimeDisplayFormat;
50
48
  pendingFocusRequest?: PendingTurnFocusRequest;
49
+ /** Show client PID/project metadata on each log row. */
50
+ showLogClientMetadata?: boolean;
51
51
  };
52
52
 
53
53
  export const TurnGroup = memo(function TurnGroup({
@@ -61,6 +61,7 @@ export const TurnGroup = memo(function TurnGroup({
61
61
  turnIndex = 0,
62
62
  timeDisplayFormat,
63
63
  pendingFocusRequest,
64
+ showLogClientMetadata = true,
64
65
  }: TurnGroupProps): JSX.Element {
65
66
  const lastIdx = entries.length - 1;
66
67
  const lastStop = entries[lastIdx]?.stopReason ?? null;
@@ -70,7 +71,7 @@ export const TurnGroup = memo(function TurnGroup({
70
71
  const collapsible = isTurnCollapsible(entries.length, isComplete, isPending);
71
72
  const [collapsed, setCollapsed] = useState(false);
72
73
 
73
- // Auto-collapse when the turn finishes (transitions from incomplete complete)
74
+ // Auto-collapse when the turn finishes (transitions from incomplete to complete)
74
75
  const prevCompleteRef = useRef(false);
75
76
  useEffect(() => {
76
77
  if (!collapsible) {
@@ -142,11 +143,7 @@ export const TurnGroup = memo(function TurnGroup({
142
143
  };
143
144
  }, [entries, lastIdx]);
144
145
 
145
- // Crab variant creators for the dual-crab collapsed layout
146
- const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
147
- const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
148
-
149
- const bgClass = turnIndex % 2 === 0 ? "bg-[#070a0f]" : "bg-[#090d12]";
146
+ const bgClass = turnIndex % 2 === 0 ? "bg-white/[0.012]" : "bg-cyan-300/[0.012]";
150
147
  const aggregateIsSlow =
151
148
  aggregate.maxElapsed !== null &&
152
149
  slowResponseThresholdSeconds > 0 &&
@@ -160,7 +157,7 @@ export const TurnGroup = memo(function TurnGroup({
160
157
  return events;
161
158
  }, [entries]);
162
159
 
163
- // ResizeObserver re-render connectors when any LogEntry height changes
160
+ // ResizeObserver re-renders connectors when any LogEntry height changes
164
161
  const [layoutVersion, setLayoutVersion] = useState(0);
165
162
  const containerRef = useRef<HTMLDivElement>(null);
166
163
  useEffect(() => {
@@ -197,7 +194,7 @@ export const TurnGroup = memo(function TurnGroup({
197
194
  aria-label={collapsed ? undefined : turnLabel}
198
195
  data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
199
196
  className={cn(
200
- "border rounded-lg transition-[border-color,box-shadow]",
197
+ "rounded-[8px] border transition-[border-color,box-shadow]",
201
198
  isPending
202
199
  ? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
203
200
  : "border-transparent",
@@ -206,7 +203,6 @@ export const TurnGroup = memo(function TurnGroup({
206
203
  )}
207
204
  >
208
205
  {collapsed ? (
209
- /* ---- Collapsed: dual-crab (+ summary card for multi-log turns) ---- */
210
206
  <div
211
207
  data-nav-id={`turn-collapsed-${String(firstLogId)}`}
212
208
  data-nav-action="expand"
@@ -228,77 +224,38 @@ export const TurnGroup = memo(function TurnGroup({
228
224
  </span>
229
225
  </div>
230
226
 
231
- {/* Dual-crab connector for collapsed multi-request turns. */}
232
- <div className="w-6 shrink-0 flex flex-col items-center pt-0.5 pb-0.5">
227
+ <div className="w-6 shrink-0 flex flex-col items-center pt-1.5 pb-1.5">
233
228
  <div className="flex justify-center h-[calc(0.75rem-8px)]" />
234
- <span
235
- role="button"
236
- tabIndex={0}
237
- title="Start of turn — click to expand"
238
- className="cursor-pointer"
239
- onClick={(e) => {
240
- e.stopPropagation();
241
- toggleCollapse();
242
- }}
243
- onKeyDown={(e) => {
244
- if (e.key === "Enter" || e.key === " ") {
245
- e.preventDefault();
246
- toggleCollapse();
247
- }
248
- }}
249
- >
250
- <StartCrab
251
- className={cn(
252
- "size-3.5 text-emerald-400",
253
- "animate-crab-appear drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
254
- )}
255
- />
256
- </span>
257
-
229
+ <span className="size-2 rounded-full border border-cyan-300/45 bg-cyan-300/20" />
258
230
  <div className="flex-1 flex justify-center min-h-0">
259
- <div className="w-0.5 bg-muted-foreground/30 h-full" />
231
+ <div className="w-px bg-muted-foreground/25 h-full" />
260
232
  </div>
261
-
262
233
  <span
263
- role="button"
264
- tabIndex={0}
265
- title="End of Turn — click to expand"
266
- className="cursor-pointer"
267
- onClick={(e) => {
268
- e.stopPropagation();
269
- toggleCollapse();
270
- }}
271
- onKeyDown={(e) => {
272
- if (e.key === "Enter" || e.key === " ") {
273
- e.preventDefault();
274
- toggleCollapse();
275
- }
276
- }}
277
- >
278
- <EndCrab
279
- className={cn(
280
- "size-3.5 text-amber-400",
281
- "animate-crab-settle drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
282
- )}
283
- />
284
- </span>
234
+ className={cn(
235
+ "size-2 rounded-full border",
236
+ isComplete
237
+ ? "border-amber-300/45 bg-amber-300/20"
238
+ : "border-muted-foreground/30 bg-muted/30",
239
+ )}
240
+ />
285
241
  </div>
286
242
 
287
- {/* Summary content hidden for single-log turns. */}
243
+ {/* Summary content - hidden for single-log turns. */}
288
244
  {entries.length > 1 && (
289
245
  <div
290
246
  className={cn(
291
- "flex-1 min-w-0 mb-0.5 rounded-lg border border-cyan-300/10 py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
247
+ "flex-1 min-w-0 mb-0.5 rounded-[8px] py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
248
+ "border border-border bg-card shadow-sm transition-colors hover:border-cyan-200/20",
292
249
  bgClass,
293
250
  )}
294
251
  >
295
- <span className={ROW_CHEVRON_SLOT_CLASS}>
296
- <ChevronRight className="size-4 shrink-0" />
252
+ <span className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/70">
253
+ {turnLabel}
297
254
  </span>
298
255
 
299
256
  {/* ID range */}
300
257
  <span
301
- className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0"
258
+ className="text-blue-300/85 font-mono font-semibold tabular-nums shrink-0"
302
259
  title={`Log IDs ${String(entries[0]?.log.id ?? "?")} ~ ${String(
303
260
  entries[lastIdx]?.log.id ?? "?",
304
261
  )}`}
@@ -312,7 +269,7 @@ export const TurnGroup = memo(function TurnGroup({
312
269
  {entries.length} request{entries.length > 1 ? "s" : ""}
313
270
  </span>
314
271
 
315
- {/* Elapsed slowest single request in the turn (not the sum) */}
272
+ {/* Elapsed - slowest single request in the turn (not the sum) */}
316
273
  {aggregate.maxElapsed !== null && (
317
274
  <TooltipProvider>
318
275
  <Tooltip>
@@ -359,6 +316,13 @@ export const TurnGroup = memo(function TurnGroup({
359
316
 
360
317
  {/* Spacer */}
361
318
  <span className="flex-1 min-w-0" />
319
+
320
+ <span
321
+ className="border border-input bg-background inline-flex size-7 shrink-0 items-center justify-center rounded-md text-slate-200"
322
+ aria-hidden="true"
323
+ >
324
+ <ChevronRight className="size-4 shrink-0" />
325
+ </span>
362
326
  </div>
363
327
  )}
364
328
  </div>
@@ -385,11 +349,10 @@ export const TurnGroup = memo(function TurnGroup({
385
349
  isFirst={visibleIdx === 0}
386
350
  isTurnStart={isTurnStart}
387
351
  isOnlyEntry={isSingleLog}
388
- crabIndex={log.id % 12}
389
352
  collapsible={collapsible && isTurnStart}
390
353
  onToggle={toggleCollapse}
391
354
  />
392
- <div className={cn("flex-1 min-w-0 mb-0.5 rounded-lg", bgClass)}>
355
+ <div className={cn("flex-1 min-w-0 mb-0.5 rounded-[8px]", bgClass)}>
393
356
  <LogEntry
394
357
  log={log}
395
358
  displayNumber={entry.sessionLogNumber}
@@ -398,6 +361,7 @@ export const TurnGroup = memo(function TurnGroup({
398
361
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
399
362
  timeDisplayFormat={timeDisplayFormat}
400
363
  cacheTrend={cacheTrends?.get(log.id) ?? null}
364
+ showClientMetadata={showLogClientMetadata}
401
365
  onCompareWithPrevious={
402
366
  comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
403
367
  }
@@ -21,6 +21,8 @@ type TurnGroupListProps = {
21
21
  onCompareWithPrevious: (log: CapturedLog) => void;
22
22
  comparisonPredecessors: Map<number, CapturedLog>;
23
23
  timeDisplayFormat: TimeDisplayFormat;
24
+ /** Show client PID/project metadata on each log row. */
25
+ showLogClientMetadata?: boolean;
24
26
  };
25
27
 
26
28
  type PendingVirtualFocusRequest = PendingTurnFocusRequest & {
@@ -73,6 +75,7 @@ function renderTurnGroup(
73
75
  comparisonPredecessors={props.comparisonPredecessors}
74
76
  turnIndex={turnGroup.turnIndex}
75
77
  pendingFocusRequest={pendingFocusRequest}
78
+ showLogClientMetadata={props.showLogClientMetadata}
76
79
  />
77
80
  );
78
81
  }
@@ -0,0 +1,56 @@
1
+ import { ApiTimeoutError } from "../../lib/apiClient";
2
+
3
+ export type FullBodyHydrationError = {
4
+ title: string;
5
+ detail: string;
6
+ };
7
+
8
+ const DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL =
9
+ "The compact log metadata is still available. Retry if the local server was restarting or the body store was briefly unavailable.";
10
+
11
+ export function formatBytes(bytes: number): string {
12
+ if (bytes < 1024) return `${bytes.toLocaleString()} B`;
13
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
14
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
15
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GiB`;
16
+ }
17
+
18
+ export function formatBodyHydrationFootprint(
19
+ requestBytes: number | null | undefined,
20
+ responseBytes: number | null | undefined,
21
+ ): string | null {
22
+ const parts: string[] = [];
23
+
24
+ if (requestBytes !== null && requestBytes !== undefined) {
25
+ parts.push(`Request ${formatBytes(requestBytes)}`);
26
+ }
27
+
28
+ if (responseBytes !== null && responseBytes !== undefined) {
29
+ parts.push(`Response ${formatBytes(responseBytes)}`);
30
+ }
31
+
32
+ return parts.length === 0 ? null : parts.join(" · ");
33
+ }
34
+
35
+ export function describeFullBodyHydrationError(error: unknown): FullBodyHydrationError {
36
+ if (error instanceof ApiTimeoutError) {
37
+ return {
38
+ title: "Full bodies timed out",
39
+ detail:
40
+ "The log store did not answer before the UI timeout. Compact metadata is still available; retry loading the bodies.",
41
+ };
42
+ }
43
+
44
+ if (error instanceof Error) {
45
+ const message = error.message.trim();
46
+ return {
47
+ title: "Full bodies could not be loaded",
48
+ detail: message === "" ? DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL : message,
49
+ };
50
+ }
51
+
52
+ return {
53
+ title: "Full bodies could not be loaded",
54
+ detail: DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL,
55
+ };
56
+ }
@@ -69,10 +69,6 @@ function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat
69
69
  }
70
70
  }
71
71
 
72
- export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
73
- return standalone || expanded;
74
- }
75
-
76
72
  export function isTurnCollapsible(
77
73
  entryCount: number,
78
74
  isComplete: boolean,
@@ -0,0 +1,2 @@
1
+ export const INSPECTOR_ICON_TRIGGER_CLASS =
2
+ "relative size-8 rounded-md border border-input bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50";