@linxiraos/pi-stats 1.1.26 → 1.2.0

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 (125) hide show
  1. package/CHANGELOG.md +12 -5
  2. package/build.ts +4 -0
  3. package/dist/client/favicon.ico +0 -0
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +1 -0
  6. package/dist/client/index.js +96 -96
  7. package/dist/types/aggregator.d.ts +4 -1
  8. package/dist/types/client/api.d.ts +5 -1
  9. package/dist/types/client/app/nav.d.ts +5 -2
  10. package/dist/types/client/data/formatters.d.ts +7 -2
  11. package/dist/types/client/data/live.d.ts +16 -11
  12. package/dist/types/client/data/query-store.d.ts +39 -0
  13. package/dist/types/client/data/query.d.ts +10 -5
  14. package/dist/types/client/data/range.d.ts +4 -3
  15. package/dist/types/client/i18n/format.d.ts +9 -0
  16. package/dist/types/client/i18n/index.d.ts +18 -0
  17. package/dist/types/client/i18n/messages/charts.d.ts +8 -0
  18. package/dist/types/client/i18n/messages/common.d.ts +153 -0
  19. package/dist/types/client/i18n/messages/costs.d.ts +92 -0
  20. package/dist/types/client/i18n/messages/en.d.ts +1255 -0
  21. package/dist/types/client/i18n/messages/errors.d.ts +78 -0
  22. package/dist/types/client/i18n/messages/frustration.d.ts +191 -0
  23. package/dist/types/client/i18n/messages/gain.d.ts +54 -0
  24. package/dist/types/client/i18n/messages/models.d.ts +94 -0
  25. package/dist/types/client/i18n/messages/nav.d.ts +46 -0
  26. package/dist/types/client/i18n/messages/overview.d.ts +80 -0
  27. package/dist/types/client/i18n/messages/projects.d.ts +84 -0
  28. package/dist/types/client/i18n/messages/providers.d.ts +142 -0
  29. package/dist/types/client/i18n/messages/requests.d.ts +72 -0
  30. package/dist/types/client/i18n/messages/tools.d.ts +119 -0
  31. package/dist/types/client/i18n/messages/traces.d.ts +144 -0
  32. package/dist/types/client/i18n/messages/ui.d.ts +44 -0
  33. package/dist/types/client/i18n/messages/zh-CN.d.ts +13 -0
  34. package/dist/types/client/i18n/types.d.ts +15 -0
  35. package/dist/types/client/traces/Minimap.d.ts +2 -0
  36. package/dist/types/client/traces/TimelineCanvas.d.ts +29 -8
  37. package/dist/types/client/traces/TranscriptList.d.ts +3 -1
  38. package/dist/types/db.d.ts +14 -21
  39. package/dist/types/embedded-client.d.ts +5 -0
  40. package/dist/types/frustration.d.ts +18 -9
  41. package/dist/types/live.d.ts +5 -3
  42. package/dist/types/locale.d.ts +16 -0
  43. package/dist/types/parser.d.ts +0 -17
  44. package/dist/types/rollup.d.ts +9 -1
  45. package/dist/types/server.d.ts +6 -0
  46. package/dist/types/shared-types.d.ts +7 -0
  47. package/dist/types/trace.d.ts +18 -6
  48. package/dist/types/types.d.ts +13 -1
  49. package/dist/types/user-metrics.d.ts +23 -1
  50. package/package.json +5 -5
  51. package/src/aggregator.ts +31 -11
  52. package/src/client/api.ts +40 -5
  53. package/src/client/app/LiveChip.tsx +14 -13
  54. package/src/client/app/Shell.tsx +19 -13
  55. package/src/client/app/ThemeToggle.tsx +4 -3
  56. package/src/client/app/nav.ts +19 -16
  57. package/src/client/data/formatters.ts +80 -13
  58. package/src/client/data/live.tsx +85 -45
  59. package/src/client/data/query-store.ts +108 -0
  60. package/src/client/data/query.ts +47 -56
  61. package/src/client/data/range.ts +34 -14
  62. package/src/client/data/series.ts +7 -1
  63. package/src/client/favicon.ico +0 -0
  64. package/src/client/i18n/format.ts +28 -0
  65. package/src/client/i18n/index.ts +53 -0
  66. package/src/client/i18n/messages/charts.ts +8 -0
  67. package/src/client/i18n/messages/common.ts +157 -0
  68. package/src/client/i18n/messages/costs.ts +104 -0
  69. package/src/client/i18n/messages/en.ts +40 -0
  70. package/src/client/i18n/messages/errors.ts +80 -0
  71. package/src/client/i18n/messages/frustration.ts +198 -0
  72. package/src/client/i18n/messages/gain.ts +54 -0
  73. package/src/client/i18n/messages/models.ts +94 -0
  74. package/src/client/i18n/messages/nav.ts +46 -0
  75. package/src/client/i18n/messages/overview.ts +90 -0
  76. package/src/client/i18n/messages/projects.ts +84 -0
  77. package/src/client/i18n/messages/providers.ts +165 -0
  78. package/src/client/i18n/messages/requests.ts +73 -0
  79. package/src/client/i18n/messages/tools.ts +120 -0
  80. package/src/client/i18n/messages/traces.ts +160 -0
  81. package/src/client/i18n/messages/ui.ts +46 -0
  82. package/src/client/i18n/messages/zh-CN.ts +43 -0
  83. package/src/client/i18n/types.ts +19 -0
  84. package/src/client/index.tsx +4 -0
  85. package/src/client/routes/CostsRoute.tsx +61 -48
  86. package/src/client/routes/ErrorsRoute.tsx +71 -47
  87. package/src/client/routes/FrustrationRoute.tsx +131 -116
  88. package/src/client/routes/GainRoute.tsx +42 -30
  89. package/src/client/routes/ModelsRoute.tsx +90 -73
  90. package/src/client/routes/OverviewRoute.tsx +109 -63
  91. package/src/client/routes/ProjectsRoute.tsx +73 -46
  92. package/src/client/routes/ProvidersRoute.tsx +151 -114
  93. package/src/client/routes/RequestsRoute.tsx +69 -46
  94. package/src/client/routes/ToolsRoute.tsx +108 -94
  95. package/src/client/routes/TracesRoute.tsx +25 -23
  96. package/src/client/styles.css +5 -2
  97. package/src/client/traces/AggregatesPanel.tsx +19 -8
  98. package/src/client/traces/Minimap.tsx +65 -27
  99. package/src/client/traces/SpanDrawer.tsx +46 -21
  100. package/src/client/traces/SummaryStrip.tsx +27 -11
  101. package/src/client/traces/TimelineCanvas.tsx +245 -116
  102. package/src/client/traces/TraceView.tsx +51 -36
  103. package/src/client/traces/TranscriptList.tsx +20 -11
  104. package/src/client/traces/traces.css +26 -0
  105. package/src/client/ui/JsonBlock.tsx +3 -2
  106. package/src/client/ui/Modal.tsx +3 -2
  107. package/src/client/ui/RequestDrawer.tsx +33 -30
  108. package/src/client/ui/SearchInput.tsx +2 -1
  109. package/src/client/ui/States.tsx +3 -2
  110. package/src/client/ui/Table.tsx +7 -3
  111. package/src/db.ts +150 -135
  112. package/src/embedded-client.ts +9 -0
  113. package/src/frustration.ts +9 -30
  114. package/src/index.ts +2 -1
  115. package/src/live.ts +36 -11
  116. package/src/locale.ts +29 -0
  117. package/src/parser.ts +37 -72
  118. package/src/port-conflict.ts +1 -1
  119. package/src/prompts/judge/frustration.json +19 -0
  120. package/src/rollup.ts +41 -20
  121. package/src/server.ts +67 -27
  122. package/src/shared-types.ts +7 -0
  123. package/src/trace.ts +54 -40
  124. package/src/types.ts +13 -1
  125. package/src/user-metrics.ts +220 -6
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { formatDurationMs, formatInteger } from "../data/formatters";
7
+ import { t } from "../i18n";
7
8
  import type { TraceToolStat } from "../types";
8
9
  import { Badge, Card, type Column, MeterCell, Table } from "../ui";
9
10
 
@@ -16,21 +17,26 @@ export function AggregatesPanel({ toolStats, index }: AggregatesPanelProps) {
16
17
  if (toolStats.length === 0) return null;
17
18
  const maxTotal = Math.max(...toolStats.map(stat => stat.totalMs));
18
19
  const columns: Column<TraceToolStat>[] = [
19
- { key: "tool", header: "Tool", render: row => <span className="mono">{row.tool}</span>, sort: row => row.tool },
20
+ {
21
+ key: "tool",
22
+ header: t("trace.tool"),
23
+ render: row => <span className="mono">{row.tool}</span>,
24
+ sort: row => row.tool,
25
+ },
20
26
  {
21
27
  key: "calls",
22
- header: "Calls",
28
+ header: t("trace.calls"),
23
29
  align: "right",
24
30
  render: row => <span className="num">{formatInteger(row.calls)}</span>,
25
31
  sort: row => row.calls,
26
32
  },
27
33
  {
28
34
  key: "errors",
29
- header: "Errors",
35
+ header: t("trace.errors"),
30
36
  align: "right",
31
37
  render: row =>
32
38
  row.errors > 0 ? (
33
- <Badge tone="bad">{formatInteger(row.errors)} failed</Badge>
39
+ <Badge tone="bad">{t("trace.failed-n", { count: formatInteger(row.errors) })}</Badge>
34
40
  ) : (
35
41
  <span className="num dim">0</span>
36
42
  ),
@@ -38,7 +44,7 @@ export function AggregatesPanel({ toolStats, index }: AggregatesPanelProps) {
38
44
  },
39
45
  {
40
46
  key: "total",
41
- header: "Total",
47
+ header: t("common.total"),
42
48
  align: "right",
43
49
  width: 200,
44
50
  render: row => (
@@ -48,21 +54,26 @@ export function AggregatesPanel({ toolStats, index }: AggregatesPanelProps) {
48
54
  },
49
55
  {
50
56
  key: "avg",
51
- header: "Avg",
57
+ header: t("trace.avg"),
52
58
  align: "right",
53
59
  render: row => <span className="num">{formatDurationMs(row.calls > 0 ? row.totalMs / row.calls : 0)}</span>,
54
60
  sort: row => (row.calls > 0 ? row.totalMs / row.calls : 0),
55
61
  },
56
62
  {
57
63
  key: "max",
58
- header: "Max",
64
+ header: t("trace.max"),
59
65
  align: "right",
60
66
  render: row => <span className="num">{formatDurationMs(row.maxMs)}</span>,
61
67
  sort: row => row.maxMs,
62
68
  },
63
69
  ];
64
70
  return (
65
- <Card title="Tool aggregates" description={`${toolStats.length} tools, by total time`} flush index={index}>
71
+ <Card
72
+ title={t("trace.tool-aggregates")}
73
+ description={t("trace.tool-aggregates-desc", { count: toolStats.length })}
74
+ flush
75
+ index={index}
76
+ >
66
77
  <Table columns={columns} rows={toolStats} rowKey={row => row.tool} dense limit={12} />
67
78
  </Card>
68
79
  );
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * Timeline overview strip: per-category density over the full virtual domain
3
3
  * plus a draggable/resizable viewport brush, mirroring DevTools' overview.
4
+ * The density layer is painted once per trace/scale/size/theme into an
5
+ * offscreen canvas; pan and zoom only blit it and redraw the brush.
4
6
  */
5
7
 
6
- import { useCallback, useEffect, useRef, useState } from "react";
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
+ import { t } from "../i18n";
7
10
  import type { TraceSpanKind, TraceTrack } from "../types";
8
11
  import type { TimelineViewport } from "./TimelineCanvas";
9
12
  import type { TraceScale } from "./time-scale";
10
- import { useTraceTheme } from "./trace-colors";
13
+ import { type TraceTheme, useTraceTheme } from "./trace-colors";
11
14
 
12
15
  export interface MinimapProps {
13
16
  tracks: TraceTrack[];
@@ -23,6 +26,59 @@ const MIN_WINDOW_U = 10;
23
26
 
24
27
  type DragMode = "move" | "left" | "right" | "create";
25
28
 
29
+ /** Paint the density strips (bucketed span coverage per pixel per category) at device resolution. */
30
+ function paintDensity(
31
+ tracks: TraceTrack[],
32
+ scale: TraceScale,
33
+ width: number,
34
+ colors: TraceTheme,
35
+ ): OffscreenCanvas | HTMLCanvasElement {
36
+ const dpr = devicePixelRatio;
37
+ const pixelWidth = Math.floor(width * dpr);
38
+ const pixelHeight = Math.floor(HEIGHT * dpr);
39
+ let layer: OffscreenCanvas | HTMLCanvasElement;
40
+ let ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D | null;
41
+ if (typeof OffscreenCanvas === "undefined") {
42
+ const canvas = document.createElement("canvas");
43
+ canvas.width = pixelWidth;
44
+ canvas.height = pixelHeight;
45
+ layer = canvas;
46
+ ctx = canvas.getContext("2d");
47
+ } else {
48
+ const canvas = new OffscreenCanvas(pixelWidth, pixelHeight);
49
+ layer = canvas;
50
+ ctx = canvas.getContext("2d");
51
+ }
52
+ if (!ctx) return layer;
53
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
54
+
55
+ const [d0, d1] = scale.domain;
56
+ const dSpan = Math.max(d1 - d0, 1e-9);
57
+ const buckets = STRIP_KINDS.map(() => new Float32Array(width));
58
+ for (const track of tracks) {
59
+ for (const span of track.spans) {
60
+ const k = STRIP_KINDS.indexOf(span.kind);
61
+ if (k === -1) continue;
62
+ const strip = buckets[k];
63
+ const x0 = Math.max(0, Math.floor(((scale.toU(span.start) - d0) / dSpan) * width));
64
+ const x1 = Math.min(width - 1, Math.ceil(((scale.toU(span.end) - d0) / dSpan) * width));
65
+ for (let x = x0; x <= x1; x++) strip[x] = Math.min(1, strip[x] + 0.34);
66
+ }
67
+ }
68
+ const stripH = HEIGHT / STRIP_KINDS.length;
69
+ for (let k = 0; k < STRIP_KINDS.length; k++) {
70
+ const strip = buckets[k];
71
+ ctx.fillStyle = colors.category[STRIP_KINDS[k]];
72
+ for (let x = 0; x < width; x++) {
73
+ if (strip[x] <= 0) continue;
74
+ ctx.globalAlpha = 0.25 + strip[x] * 0.75;
75
+ ctx.fillRect(x, k * stripH + 1, 1, stripH - 2);
76
+ }
77
+ ctx.globalAlpha = 1;
78
+ }
79
+ return layer;
80
+ }
81
+
26
82
  export function Minimap({ tracks, scale, viewport, onViewportChange }: MinimapProps) {
27
83
  const colors = useTraceTheme();
28
84
  const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -44,36 +100,18 @@ export function Minimap({ tracks, scale, viewport, onViewportChange }: MinimapPr
44
100
  const toX = useCallback((u: number) => ((u - d0) / dSpan) * width, [d0, dSpan, width]);
45
101
  const toU = useCallback((x: number) => d0 + (x / Math.max(width, 1)) * dSpan, [d0, dSpan, width]);
46
102
 
103
+ const density = useMemo(() => paintDensity(tracks, scale, width, colors), [tracks, scale, width, colors]);
104
+
47
105
  useEffect(() => {
48
106
  const canvas = canvasRef.current;
49
107
  if (!canvas) return;
50
108
  const ctx = canvas.getContext("2d");
51
109
  if (!ctx) return;
52
110
  const dpr = devicePixelRatio;
111
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
112
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
113
+ ctx.drawImage(density, 0, 0);
53
114
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
54
- ctx.clearRect(0, 0, width, HEIGHT);
55
-
56
- // Density strips: bucket span coverage per pixel per category.
57
- const stripH = HEIGHT / STRIP_KINDS.length;
58
- for (let k = 0; k < STRIP_KINDS.length; k++) {
59
- const kind = STRIP_KINDS[k];
60
- const buckets = new Float32Array(width);
61
- for (const track of tracks) {
62
- for (const span of track.spans) {
63
- if (span.kind !== kind) continue;
64
- const x0 = Math.max(0, Math.floor(toX(scale.toU(span.start))));
65
- const x1 = Math.min(width - 1, Math.ceil(toX(scale.toU(span.end))));
66
- for (let x = x0; x <= x1; x++) buckets[x] = Math.min(1, buckets[x] + 0.34);
67
- }
68
- }
69
- ctx.fillStyle = colors.category[kind];
70
- for (let x = 0; x < width; x++) {
71
- if (buckets[x] <= 0) continue;
72
- ctx.globalAlpha = 0.25 + buckets[x] * 0.75;
73
- ctx.fillRect(x, k * stripH + 1, 1, stripH - 2);
74
- }
75
- ctx.globalAlpha = 1;
76
- }
77
115
 
78
116
  // Viewport brush.
79
117
  const vx0 = toX(viewport.u0);
@@ -85,7 +123,7 @@ export function Minimap({ tracks, scale, viewport, onViewportChange }: MinimapPr
85
123
  ctx.fillStyle = colors.selection;
86
124
  ctx.fillRect(vx0, 0, 2, HEIGHT);
87
125
  ctx.fillRect(vx1 - 2, 0, 2, HEIGHT);
88
- }, [tracks, scale, viewport, colors, width, toX]);
126
+ }, [density, viewport, colors, toX]);
89
127
 
90
128
  const handlePointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {
91
129
  const rect = event.currentTarget.getBoundingClientRect();
@@ -146,7 +184,7 @@ export function Minimap({ tracks, scale, viewport, onViewportChange }: MinimapPr
146
184
  className="traces-minimap"
147
185
  style={{ height: HEIGHT }}
148
186
  role="slider"
149
- aria-label="Timeline overview brush"
187
+ aria-label={t("trace.minimap-aria")}
150
188
  aria-valuemin={0}
151
189
  aria-valuemax={100}
152
190
  aria-valuenow={Math.round(((viewport.u0 - d0) / dSpan) * 100)}
@@ -8,6 +8,7 @@ import { Check, Copy } from "lucide-react";
8
8
  import { type ReactNode, useEffect, useRef, useState } from "react";
9
9
  import { getSessionEntryDetail } from "../api";
10
10
  import { formatDurationMs, formatInteger } from "../data/formatters";
11
+ import { t } from "../i18n";
11
12
  import type { TraceSpan, TraceTrack } from "../types";
12
13
  import { Badge, Drawer, ErrorState, JsonBlock, KeyValues, Skeleton } from "../ui";
13
14
 
@@ -112,32 +113,44 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
112
113
  const metrics: Array<{ key: string; label: ReactNode; value: ReactNode }> = [
113
114
  {
114
115
  key: "duration",
115
- label: "Duration",
116
+ label: t("common.duration"),
116
117
  value: (
117
118
  <span className="num">
118
119
  {formatDurationMs(span.end - span.start)}
119
- {span.unterminated && <span className="muted"> · unterminated</span>}
120
+ {span.unterminated && <span className="muted"> · {t("trace.unterminated")}</span>}
120
121
  </span>
121
122
  ),
122
123
  },
123
- { key: "start", label: "Start", value: <span className="num">{new Date(span.start).toLocaleTimeString()}</span> },
124
- { key: "track", label: "Track", value: <span className="mono">{track?.label ?? "-"}</span> },
124
+ {
125
+ key: "start",
126
+ label: t("trace.start"),
127
+ value: <span className="num">{new Date(span.start).toLocaleTimeString()}</span>,
128
+ },
129
+ { key: "track", label: t("trace.track"), value: <span className="mono">{track?.label ?? "-"}</span> },
125
130
  ];
126
131
  if (span.kind === "model") {
127
132
  metrics.push(
128
133
  {
129
134
  key: "tokens",
130
- label: "Tokens",
135
+ label: t("common.tokens"),
131
136
  value: <span className="num">{formatInteger(span.tokens ?? 0)}</span>,
132
137
  },
133
- { key: "cost", label: "Cost", value: <span className="num">${(span.cost ?? 0).toFixed(4)}</span> },
134
- { key: "ttft", label: "TTFT", value: <span className="num">{formatDurationMs(span.ttft ?? null)}</span> },
135
- { key: "model", label: "Model", value: <span className="mono">{msg?.model ?? span.model ?? "-"}</span> },
138
+ { key: "cost", label: t("common.cost"), value: <span className="num">${(span.cost ?? 0).toFixed(4)}</span> },
139
+ {
140
+ key: "ttft",
141
+ label: t("trace.ttft"),
142
+ value: <span className="num">{formatDurationMs(span.ttft ?? null)}</span>,
143
+ },
144
+ {
145
+ key: "model",
146
+ label: t("common.model"),
147
+ value: <span className="mono">{msg?.model ?? span.model ?? "-"}</span>,
148
+ },
136
149
  );
137
150
  if (usage)
138
151
  metrics.push({
139
152
  key: "split",
140
- label: "Input / output / cached",
153
+ label: t("trace.io-cached"),
141
154
  value: (
142
155
  <span className="num">
143
156
  {formatInteger(usage.input ?? 0)} / {formatInteger(usage.output ?? 0)} /{" "}
@@ -146,9 +159,17 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
146
159
  ),
147
160
  });
148
161
  if (msg?.provider)
149
- metrics.push({ key: "provider", label: "Provider", value: <span className="mono">{msg.provider}</span> });
162
+ metrics.push({
163
+ key: "provider",
164
+ label: t("common.provider"),
165
+ value: <span className="mono">{msg.provider}</span>,
166
+ });
150
167
  if (msg?.stopReason)
151
- metrics.push({ key: "stop", label: "Stop reason", value: <span className="mono">{msg.stopReason}</span> });
168
+ metrics.push({
169
+ key: "stop",
170
+ label: t("trace.stop-reason"),
171
+ value: <span className="mono">{msg.stopReason}</span>,
172
+ });
152
173
  }
153
174
 
154
175
  return (
@@ -159,7 +180,7 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
159
180
  subtitle={
160
181
  <span className="row" style={{ gap: 6 }}>
161
182
  <Badge tone={span.isError ? "bad" : "neutral"}>
162
- {span.isError ? `${span.kind} · error` : span.kind}
183
+ {span.isError ? `${span.kind} · ${t("common.error")}` : span.kind}
163
184
  </Badge>
164
185
  </span>
165
186
  }
@@ -170,8 +191,8 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
170
191
  data-variant="ghost"
171
192
  data-icon="true"
172
193
  onClick={handleCopy}
173
- aria-label="Copy raw JSON"
174
- title="Copy raw JSON"
194
+ aria-label={t("trace.copy-json")}
195
+ title={t("trace.copy-json")}
175
196
  >
176
197
  {copied ? <Check size={15} /> : <Copy size={15} />}
177
198
  </button>
@@ -182,7 +203,7 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
182
203
 
183
204
  {msg?.errorMessage && (
184
205
  <div className="stack" style={{ gap: 6 }}>
185
- <div className="section-label tone-bad">Error message</div>
206
+ <div className="section-label tone-bad">{t("trace.error-message")}</div>
186
207
  <pre className="code-block traces-pre">{msg.errorMessage}</pre>
187
208
  </div>
188
209
  )}
@@ -191,7 +212,7 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
191
212
  <div className="stack" style={{ gap: 8 }}>
192
213
  {span.detail && (
193
214
  <>
194
- <div className="section-label">Task</div>
215
+ <div className="section-label">{t("trace.task")}</div>
195
216
  <pre className="code-block traces-pre">{span.detail}</pre>
196
217
  </>
197
218
  )}
@@ -203,7 +224,7 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
203
224
  data-size="sm"
204
225
  onClick={() => onOpenChildTrack(span.childTrackId ?? "")}
205
226
  >
206
- Open child track <span className="mono">{span.childTrackId}</span>
227
+ {t("trace.open-child-track")} <span className="mono">{span.childTrackId}</span>
207
228
  </button>
208
229
  </div>
209
230
  )}
@@ -212,7 +233,7 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
212
233
 
213
234
  {span.kind === "tool" && span.detail && (
214
235
  <div className="stack" style={{ gap: 6 }}>
215
- <div className="section-label">Args</div>
236
+ <div className="section-label">{t("trace.args")}</div>
216
237
  <pre className="code-block traces-pre">{span.detail}</pre>
217
238
  </div>
218
239
  )}
@@ -223,20 +244,24 @@ export function SpanDrawer({ span, track, onClose, onOpenChildTrack }: SpanDrawe
223
244
  <Skeleton height={160} />
224
245
  </div>
225
246
  )}
226
- {error && <ErrorState error={`Failed to load entry: ${error.message}`} />}
247
+ {error && <ErrorState error={t("trace.load-failed", { message: error.message })} />}
227
248
 
228
249
  {!loading &&
229
250
  entry !== null &&
230
251
  textBlocks(msg?.content).map((block, index) => (
231
252
  <div key={`${block.kind}-${index}`} className="stack" style={{ gap: 6 }}>
232
- <div className="section-label">{block.kind === "thinking" ? "Thinking" : "Text"}</div>
253
+ <div className="section-label">
254
+ {block.kind === "thinking" ? t("trace.thinking") : t("trace.text")}
255
+ </div>
233
256
  <pre className="code-block traces-pre" data-scroll="true">
234
257
  {block.text}
235
258
  </pre>
236
259
  </div>
237
260
  ))}
238
261
 
239
- {!loading && entry !== null && <JsonBlock data={entry} title="Raw entry" initialCollapsed={true} />}
262
+ {!loading && entry !== null && (
263
+ <JsonBlock data={entry} title={t("trace.raw-entry")} initialCollapsed={true} />
264
+ )}
240
265
  </div>
241
266
  </Drawer>
242
267
  );
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { formatCompact, formatElapsed, formatEstimatedCost, formatInteger, formatPercent } from "../data/formatters";
7
+ import { t } from "../i18n";
7
8
  import type { TraceSummary } from "../types";
8
9
  import { Stat, StatGrid } from "../ui";
9
10
 
@@ -12,23 +13,38 @@ export interface SummaryStripProps {
12
13
  }
13
14
 
14
15
  export function SummaryStrip({ summary }: SummaryStripProps) {
15
- const share = (ms: number) => (summary.wallMs > 0 ? `${formatPercent(ms / summary.wallMs)} of wall` : undefined);
16
+ const share = (ms: number) =>
17
+ summary.wallMs > 0 ? t("trace.of-wall", { percent: formatPercent(ms / summary.wallMs) }) : undefined;
16
18
  return (
17
19
  <StatGrid min={112}>
18
- <Stat size="sm" label="Wall time" value={formatElapsed(summary.wallMs)} />
19
- <Stat size="sm" label="Model time" value={formatElapsed(summary.modelMs)} hint={share(summary.modelMs)} />
20
- <Stat size="sm" label="Tool time" value={formatElapsed(summary.toolMs)} hint={share(summary.toolMs)} />
21
- <Stat size="sm" label="Idle" value={formatElapsed(summary.idleMs)} hint={share(summary.idleMs)} />
22
- <Stat size="sm" label="Turns" value={formatInteger(summary.turns)} />
20
+ <Stat size="sm" label={t("trace.wall-time")} value={formatElapsed(summary.wallMs)} />
23
21
  <Stat
24
22
  size="sm"
25
- label="Requests"
23
+ label={t("trace.model-time")}
24
+ value={formatElapsed(summary.modelMs)}
25
+ hint={share(summary.modelMs)}
26
+ />
27
+ <Stat
28
+ size="sm"
29
+ label={t("trace.tool-time")}
30
+ value={formatElapsed(summary.toolMs)}
31
+ hint={share(summary.toolMs)}
32
+ />
33
+ <Stat size="sm" label={t("trace.idle")} value={formatElapsed(summary.idleMs)} hint={share(summary.idleMs)} />
34
+ <Stat size="sm" label={t("trace.turns")} value={formatInteger(summary.turns)} />
35
+ <Stat
36
+ size="sm"
37
+ label={t("common.requests")}
26
38
  value={formatInteger(summary.requests)}
27
- hint={`${formatInteger(summary.toolCalls)} tool calls`}
39
+ hint={t("trace.tool-calls-n", { count: formatInteger(summary.toolCalls) })}
40
+ />
41
+ <Stat size="sm" label={t("trace.agents")} value={formatInteger(summary.subagents)} />
42
+ <Stat size="sm" label={t("common.tokens")} value={formatCompact(summary.totalTokens)} />
43
+ <Stat
44
+ size="sm"
45
+ label={t("common.cost")}
46
+ value={formatEstimatedCost(summary.costTotal, summary.unpricedRequests)}
28
47
  />
29
- <Stat size="sm" label="Agents" value={formatInteger(summary.subagents)} />
30
- <Stat size="sm" label="Tokens" value={formatCompact(summary.totalTokens)} />
31
- <Stat size="sm" label="Cost" value={formatEstimatedCost(summary.costTotal, summary.unpricedRequests)} />
32
48
  </StatGrid>
33
49
  );
34
50
  }