@linxiraos/pi-stats 1.1.27 → 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 (124) hide show
  1. package/CHANGELOG.md +10 -7
  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 +5 -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 +12 -0
  49. package/dist/types/user-metrics.d.ts +23 -1
  50. package/package.json +4 -4
  51. package/src/aggregator.ts +28 -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 +73 -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 +87 -71
  90. package/src/client/routes/OverviewRoute.tsx +107 -62
  91. package/src/client/routes/ProjectsRoute.tsx +71 -45
  92. package/src/client/routes/ProvidersRoute.tsx +149 -113
  93. package/src/client/routes/RequestsRoute.tsx +68 -45
  94. package/src/client/routes/ToolsRoute.tsx +105 -92
  95. package/src/client/routes/TracesRoute.tsx +25 -23
  96. package/src/client/styles.css +4 -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/live.ts +36 -11
  115. package/src/locale.ts +29 -0
  116. package/src/parser.ts +37 -72
  117. package/src/port-conflict.ts +1 -1
  118. package/src/prompts/judge/frustration.json +19 -0
  119. package/src/rollup.ts +41 -20
  120. package/src/server.ts +67 -27
  121. package/src/shared-types.ts +7 -0
  122. package/src/trace.ts +54 -40
  123. package/src/types.ts +12 -0
  124. package/src/user-metrics.ts +220 -6
@@ -1,6 +1,7 @@
1
1
  import { X } from "lucide-react";
2
2
  import { useMemo, useState } from "react";
3
3
  import { getToolDashboardStats } from "../api";
4
+ import { t } from "../i18n";
4
5
  import { Legend, Sparkline, TimeChart, useHiddenSeries } from "../charts";
5
6
  import { buildColorLookup, OTHER_COLOR } from "../data/colors";
6
7
  import {
@@ -44,20 +45,26 @@ export interface ToolsRouteProps {
44
45
  type CallMetric = "calls" | "errors";
45
46
 
46
47
  const METRIC_OPTIONS = [
47
- { value: "calls" as const, label: "Calls" },
48
- { value: "errors" as const, label: "Errors" },
48
+ { value: "calls" as const, label: t("tools.calls") },
49
+ { value: "errors" as const, label: t("tools.errors") },
49
50
  ];
50
51
 
51
52
  /** Tools stacked individually in the calls chart; the rest fold into "Other". */
52
53
  const TOP_TOOLS = 6;
53
54
 
54
- const ATTRIBUTION_NOTE =
55
- "Tokens and API-equivalent cost of each invoking turn, split evenly across that turn's tool calls";
55
+ const ATTRIBUTION_NOTE = t("tools.attribution-note");
56
56
 
57
- const NO_CALLS = <EmptyState title="No tool calls in this range" />;
57
+ /** Bucket granularity label for the calls chart description. */
58
+ function bucketUnit(bucketMs: number): string {
59
+ if (bucketMs < 3_600_000) return t("tools.bucket-5m");
60
+ if (bucketMs < 86_400_000) return t("tools.bucket-hour");
61
+ return t("tools.bucket-day");
62
+ }
63
+
64
+ const NO_CALLS = <EmptyState title={t("tools.no-tool-calls")} />;
58
65
 
59
66
  export function ToolsRoute({ active, range }: ToolsRouteProps) {
60
- const tools = useQuery(["tools", range], () => getToolDashboardStats(range), { enabled: active });
67
+ const tools = useQuery(["tools", range], ({ signal }) => getToolDashboardStats(range, signal), { enabled: active });
61
68
  const [metric, setMetric] = useState<CallMetric>("calls");
62
69
  const [hidden, toggleHidden] = useHiddenSeries();
63
70
  const [pickedTool, setToolFilter] = useState<string | null>(null);
@@ -72,75 +79,78 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
72
79
 
73
80
  return (
74
81
  <div className="page">
75
- <PageHeader
76
- title="Tools"
77
- description={`Which tools zeta called in ${meta.windowLabel}, how often they failed, and what they cost.`}
78
- />
82
+ <PageHeader title={t("tools.title")} description={t("tools.description", { window: t(meta.windowKey) })} />
79
83
 
80
84
  <QueryView query={tools} skeleton={<ChartSkeleton height={112} />}>
81
85
  {() => {
82
- const t = view.totals;
86
+ const totals = view.totals;
83
87
  return (
84
88
  <div data-stale={tools.stale} className="stack" style={{ gap: 16 }}>
85
89
  <StatGrid min={190}>
86
90
  <Stat
87
- label="Tool calls"
88
- value={formatInteger(t.calls)}
89
- hint={`${formatInteger(t.errors)} failed`}
91
+ label={t("tools.tool-calls")}
92
+ value={formatInteger(totals.calls)}
93
+ hint={t("tools.failed-n", { count: formatInteger(totals.errors) })}
90
94
  spark={view.totalCalls}
91
95
  />
92
96
  <Stat
93
- label="Distinct tools"
94
- value={formatInteger(t.tools)}
95
- hint={view.rows[0] ? `Most used: ${view.rows[0].tool}` : undefined}
97
+ label={t("tools.distinct-tools")}
98
+ value={formatInteger(totals.tools)}
99
+ hint={view.rows[0] ? t("tools.most-used", { tool: view.rows[0].tool }) : undefined}
96
100
  />
97
101
  <Stat
98
- label="Error rate"
99
- title="Tool results that came back flagged as errors"
100
- value={formatErrorRate(t.calls > 0 ? t.errors / t.calls : 0)}
101
- hint={`${formatInteger(t.calls - t.errors)} succeeded`}
102
+ label={t("common.error-rate")}
103
+ title={t("tools.error-rate-title")}
104
+ value={formatErrorRate(totals.calls > 0 ? totals.errors / totals.calls : 0)}
105
+ hint={t("tools.succeeded-n", { count: formatInteger(totals.calls - totals.errors) })}
102
106
  spark={view.totalErrors}
103
107
  sparkColor="var(--bad)"
104
108
  />
105
109
  <Stat
106
- label="Attributed tokens"
110
+ label={t("tools.attributed-tokens")}
107
111
  title={ATTRIBUTION_NOTE}
108
- value={formatCompact(Math.round(t.tokens))}
109
- hint={`${formatCompact(Math.round(t.output))} output`}
112
+ value={formatCompact(Math.round(totals.tokens))}
113
+ hint={t("tools.tokens-output", { tokens: formatCompact(Math.round(totals.output)) })}
110
114
  />
111
115
  <Stat
112
- label="Attributed cost"
113
- title={`${ATTRIBUTION_NOTE}; API-equivalent estimate`}
114
- value={formatEstimatedCost(t.cost, t.unpriced)}
116
+ label={t("tools.attributed-cost")}
117
+ title={t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE })}
118
+ value={formatEstimatedCost(totals.cost, totals.unpriced)}
115
119
  hint={
116
- t.unpriced > 0
117
- ? `${formatInteger(Math.round(t.unpriced))} unpriced requests`
118
- : "API-equivalent"
120
+ totals.unpriced > 0
121
+ ? t("tools.unpriced-requests-n", { count: formatInteger(Math.round(totals.unpriced)) })
122
+ : t("tools.api-equivalent")
119
123
  }
120
124
  />
121
125
  </StatGrid>
122
126
  <StatGrid min={150}>
123
127
  <Stat
124
128
  size="sm"
125
- label="Result text"
126
- title="Characters of tool-result text fed back into context"
127
- value={`${formatCompact(t.resultChars)} chars`}
129
+ label={t("tools.result-text")}
130
+ title={t("tools.result-chars-title")}
131
+ value={t("tools.chars", { count: formatCompact(totals.resultChars) })}
128
132
  />
129
133
  <Stat
130
134
  size="sm"
131
- label="Call arguments"
132
- title="Characters of serialized tool-call arguments"
133
- value={`${formatCompact(t.argsChars)} chars`}
135
+ label={t("tools.call-arguments")}
136
+ title={t("tools.args-chars-title")}
137
+ value={t("tools.chars", { count: formatCompact(totals.argsChars) })}
134
138
  />
135
139
  <Stat
136
140
  size="sm"
137
- label="Avg result per call"
138
- value={`${formatCompact(t.calls > 0 ? Math.round(t.resultChars / t.calls) : 0)} chars`}
141
+ label={t("tools.avg-result-per-call")}
142
+ value={t("tools.chars", {
143
+ count: formatCompact(
144
+ totals.calls > 0 ? Math.round(totals.resultChars / totals.calls) : 0,
145
+ ),
146
+ })}
139
147
  />
140
148
  <Stat
141
149
  size="sm"
142
- label="Avg arguments per call"
143
- value={`${formatCompact(t.calls > 0 ? Math.round(t.argsChars / t.calls) : 0)} chars`}
150
+ label={t("tools.avg-args-per-call")}
151
+ value={t("tools.chars", {
152
+ count: formatCompact(totals.calls > 0 ? Math.round(totals.argsChars / totals.calls) : 0),
153
+ })}
144
154
  />
145
155
  </StatGrid>
146
156
  </div>
@@ -150,8 +160,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
150
160
 
151
161
  <Card
152
162
  index={1}
153
- title={metric === "calls" ? "Calls over time" : "Errors over time"}
154
- description={`Per ${meta.bucketMs < 3_600_000 ? "5 minutes" : meta.bucketMs < 86_400_000 ? "hour" : "day"}, top ${TOP_TOOLS} tools stacked`}
163
+ title={metric === "calls" ? t("tools.calls-over-time") : t("tools.errors-over-time")}
164
+ description={t("tools.chart-description", { bucket: bucketUnit(meta.bucketMs), count: TOP_TOOLS })}
155
165
  actions={<Segmented size="sm" options={METRIC_OPTIONS} value={metric} onChange={setMetric} />}
156
166
  stale={tools.stale}
157
167
  >
@@ -165,7 +175,7 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
165
175
  hidden={hidden}
166
176
  height={260}
167
177
  formatTooltip={formatInteger}
168
- emptyLabel={metric === "errors" ? "No tool errors in this range" : undefined}
178
+ emptyLabel={metric === "errors" ? t("tools.no-tool-errors") : undefined}
169
179
  />
170
180
  <Legend
171
181
  items={chartSeries.map(s => ({
@@ -184,8 +194,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
184
194
 
185
195
  <Card
186
196
  index={2}
187
- title="By tool"
188
- description={`${ATTRIBUTION_NOTE}. Select a tool to break it down by model.`}
197
+ title={t("tools.by-tool")}
198
+ description={t("tools.by-tool-description", { note: ATTRIBUTION_NOTE })}
189
199
  flush
190
200
  stale={tools.stale}
191
201
  >
@@ -207,8 +217,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
207
217
 
208
218
  <Card
209
219
  index={3}
210
- title="By tool and model"
211
- description="Which models call which tools, and how often those calls fail"
220
+ title={t("tools.by-tool-and-model")}
221
+ description={t("tools.by-tool-and-model-description")}
212
222
  flush
213
223
  stale={tools.stale}
214
224
  actions={
@@ -217,9 +227,9 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
217
227
  className="input tools-filter"
218
228
  value={toolFilter ?? ""}
219
229
  onChange={e => setToolFilter(e.target.value || null)}
220
- aria-label="Filter by tool"
230
+ aria-label={t("tools.filter-by-tool")}
221
231
  >
222
- <option value="">All tools</option>
232
+ <option value="">{t("tools.all-tools")}</option>
223
233
  {view.toolNames.map(name => (
224
234
  <option key={name} value={name}>
225
235
  {name}
@@ -233,8 +243,8 @@ export function ToolsRoute({ active, range }: ToolsRouteProps) {
233
243
  data-size="sm"
234
244
  data-variant="ghost"
235
245
  data-icon="true"
236
- title="Clear tool filter"
237
- aria-label="Clear tool filter"
246
+ title={t("tools.clear-tool-filter")}
247
+ aria-label={t("tools.clear-tool-filter")}
238
248
  onClick={() => setToolFilter(null)}
239
249
  >
240
250
  <X size={13} />
@@ -271,7 +281,7 @@ function buildToolsView(data: ToolDashboardStats | null, range: TimeRange) {
271
281
  const byTool = data?.byTool ?? [];
272
282
  const points = data?.series ?? [];
273
283
  const rows = buildToolRows(byTool).sort((a, b) => b.calls - a.calls);
274
- const colors = buildColorLookup(byTool.map(t => ({ key: t.tool, weight: t.calls })));
284
+ const colors = buildColorLookup(byTool.map(item => ({ key: item.tool, weight: item.calls })));
275
285
  const buckets = bucketAxis(
276
286
  range,
277
287
  points.map(p => p.timestamp),
@@ -288,15 +298,15 @@ function buildToolsView(data: ToolDashboardStats | null, range: TimeRange) {
288
298
  resultChars: 0,
289
299
  argsChars: 0,
290
300
  };
291
- for (const t of byTool) {
292
- totals.calls += t.calls;
293
- totals.errors += t.errors;
294
- totals.tokens += t.totalTokensShare;
295
- totals.output += t.outputTokensShare;
296
- totals.cost += t.costShare;
297
- totals.unpriced += t.unpricedRequestsShare;
298
- totals.resultChars += t.resultChars;
299
- totals.argsChars += t.argsChars;
301
+ for (const item of byTool) {
302
+ totals.calls += item.calls;
303
+ totals.errors += item.errors;
304
+ totals.tokens += item.totalTokensShare;
305
+ totals.output += item.outputTokensShare;
306
+ totals.cost += item.costShare;
307
+ totals.unpriced += item.unpricedRequestsShare;
308
+ totals.resultChars += item.resultChars;
309
+ totals.argsChars += item.argsChars;
300
310
  }
301
311
 
302
312
  // Every tool keeps its table swatch color in both metrics; zero slots become
@@ -338,7 +348,7 @@ function buildToolColumns(
338
348
  return [
339
349
  {
340
350
  key: "tool",
341
- header: "Tool",
351
+ header: t("tools.tool"),
342
352
  sort: row => row.tool,
343
353
  render: row => (
344
354
  <span className="row" style={{ gap: 8 }}>
@@ -351,8 +361,8 @@ function buildToolColumns(
351
361
  },
352
362
  {
353
363
  key: "trend",
354
- header: "Trend",
355
- title: "Calls per bucket over the range",
364
+ header: t("tools.trend"),
365
+ title: t("tools.trend-title"),
356
366
  render: row => {
357
367
  const values = trends.get(row.tool);
358
368
  return values && values.length > 1 ? (
@@ -364,7 +374,7 @@ function buildToolColumns(
364
374
  },
365
375
  {
366
376
  key: "calls",
367
- header: "Calls",
377
+ header: t("tools.calls"),
368
378
  align: "right",
369
379
  sort: row => row.calls,
370
380
  render: row => (
@@ -372,7 +382,7 @@ function buildToolColumns(
372
382
  value={row.calls}
373
383
  max={maxCalls}
374
384
  display={
375
- <span title={`${formatPercent(row.callFraction)} of all tool calls`}>
385
+ <span title={t("tools.share-of-calls", { percent: formatPercent(row.callFraction) })}>
376
386
  {formatInteger(row.calls)}
377
387
  <span className="dim tools-share">{formatPercent(row.callFraction, 0)}</span>
378
388
  </span>
@@ -382,8 +392,8 @@ function buildToolColumns(
382
392
  },
383
393
  {
384
394
  key: "errorRate",
385
- header: "Errors",
386
- title: "Calls whose result came back flagged as an error",
395
+ header: t("tools.errors"),
396
+ title: t("tools.errors-title"),
387
397
  align: "right",
388
398
  sort: row => row.errorRate,
389
399
  render: row => (
@@ -397,36 +407,36 @@ function buildToolColumns(
397
407
  },
398
408
  {
399
409
  key: "args",
400
- header: "Args",
401
- title: "Characters of serialized tool-call arguments",
410
+ header: t("tools.args"),
411
+ title: t("tools.args-chars-title"),
402
412
  align: "right",
403
413
  sort: row => row.argsChars,
404
414
  render: row => <span className="num">{formatCompact(row.argsChars)}</span>,
405
415
  },
406
416
  {
407
417
  key: "result",
408
- header: "Result",
409
- title: "Characters of tool-result text fed back into context",
418
+ header: t("tools.result"),
419
+ title: t("tools.result-chars-title"),
410
420
  align: "right",
411
421
  sort: row => row.resultChars,
412
422
  render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
413
423
  },
414
424
  {
415
425
  key: "avgResult",
416
- header: "Result / call",
417
- title: "Mean tool-result characters per call",
426
+ header: t("tools.result-per-call"),
427
+ title: t("tools.result-per-call-title"),
418
428
  align: "right",
419
429
  sort: row => row.avgResultChars,
420
430
  render: row => <span className="num muted">{formatCompact(Math.round(row.avgResultChars))}</span>,
421
431
  },
422
432
  {
423
433
  key: "tokens",
424
- header: "Attr. tokens",
434
+ header: t("tools.attr-tokens"),
425
435
  title: ATTRIBUTION_NOTE,
426
436
  align: "right",
427
437
  sort: row => row.totalTokensShare,
428
438
  render: row => (
429
- <span className="num" title={`${formatPercent(row.tokenFraction)} of attributed tokens`}>
439
+ <span className="num" title={t("tools.share-of-tokens", { percent: formatPercent(row.tokenFraction) })}>
430
440
  {formatCompact(Math.round(row.totalTokensShare))}
431
441
  <span className="dim tools-share">{formatPercent(row.tokenFraction, 0)}</span>
432
442
  </span>
@@ -434,12 +444,12 @@ function buildToolColumns(
434
444
  },
435
445
  {
436
446
  key: "cost",
437
- header: "Attr. cost",
438
- title: `${ATTRIBUTION_NOTE}; API-equivalent estimate`,
447
+ header: t("tools.attr-cost"),
448
+ title: t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE }),
439
449
  align: "right",
440
450
  sort: row => row.costShare,
441
451
  render: row => (
442
- <span className="num" title={`${formatPercent(row.costFraction)} of attributed cost`}>
452
+ <span className="num" title={t("tools.share-of-cost", { percent: formatPercent(row.costFraction) })}>
443
453
  {formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}
444
454
  <span className="dim tools-share">{formatPercent(row.costFraction, 0)}</span>
445
455
  </span>
@@ -447,7 +457,7 @@ function buildToolColumns(
447
457
  },
448
458
  {
449
459
  key: "lastUsed",
450
- header: "Last used",
460
+ header: t("tools.last-used"),
451
461
  align: "right",
452
462
  sort: row => row.lastUsed,
453
463
  render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
@@ -485,7 +495,7 @@ function ToolModelTable({
485
495
  return [
486
496
  {
487
497
  key: "tool",
488
- header: "Tool",
498
+ header: t("tools.tool"),
489
499
  sort: row => row.tool,
490
500
  render: row => (
491
501
  <span className="row" style={{ gap: 8 }}>
@@ -498,22 +508,25 @@ function ToolModelTable({
498
508
  },
499
509
  {
500
510
  key: "model",
501
- header: "Model",
511
+ header: t("common.model"),
502
512
  sort: row => row.model,
503
513
  render: row => (
504
- <LabelCell primary={<span className="mono">{row.model || "(unknown)"}</span>} secondary={row.provider} />
514
+ <LabelCell
515
+ primary={<span className="mono">{row.model || t("common.unknown")}</span>}
516
+ secondary={row.provider}
517
+ />
505
518
  ),
506
519
  },
507
520
  {
508
521
  key: "calls",
509
- header: "Calls",
522
+ header: t("tools.calls"),
510
523
  align: "right",
511
524
  sort: row => row.calls,
512
525
  render: row => <MeterCell value={row.calls} max={maxCalls} display={formatInteger(row.calls)} />,
513
526
  },
514
527
  {
515
528
  key: "errorRate",
516
- header: "Errors",
529
+ header: t("tools.errors"),
517
530
  align: "right",
518
531
  sort: row => row.errorRate,
519
532
  render: row => (
@@ -527,15 +540,15 @@ function ToolModelTable({
527
540
  },
528
541
  {
529
542
  key: "result",
530
- header: "Result",
531
- title: "Characters of tool-result text fed back into context",
543
+ header: t("tools.result"),
544
+ title: t("tools.result-chars-title"),
532
545
  align: "right",
533
546
  sort: row => row.resultChars,
534
547
  render: row => <span className="num">{formatCompact(row.resultChars)}</span>,
535
548
  },
536
549
  {
537
550
  key: "tokens",
538
- header: "Attr. tokens",
551
+ header: t("tools.attr-tokens"),
539
552
  title: ATTRIBUTION_NOTE,
540
553
  align: "right",
541
554
  sort: row => row.totalTokensShare,
@@ -543,15 +556,15 @@ function ToolModelTable({
543
556
  },
544
557
  {
545
558
  key: "cost",
546
- header: "Attr. cost",
547
- title: `${ATTRIBUTION_NOTE}; API-equivalent estimate`,
559
+ header: t("tools.attr-cost"),
560
+ title: t("tools.attribution-estimate", { note: ATTRIBUTION_NOTE }),
548
561
  align: "right",
549
562
  sort: row => row.costShare,
550
563
  render: row => <span className="num">{formatEstimatedCost(row.costShare, row.unpricedRequestsShare)}</span>,
551
564
  },
552
565
  {
553
566
  key: "lastUsed",
554
- header: "Last used",
567
+ header: t("tools.last-used"),
555
568
  align: "right",
556
569
  sort: row => row.lastUsed,
557
570
  render: row => <span className="muted">{formatRelativeTime(row.lastUsed)}</span>,
@@ -567,7 +580,7 @@ function ToolModelTable({
567
580
  initialSort={{ key: "calls", dir: "desc" }}
568
581
  limit={25}
569
582
  dense
570
- empty={<EmptyState title={tool ? `No calls to ${tool} in this range` : "No tool calls in this range"} />}
583
+ empty={<EmptyState title={tool ? t("tools.no-calls-to-tool", { tool }) : t("tools.no-tool-calls")} />}
571
584
  />
572
585
  );
573
586
  }
@@ -13,6 +13,7 @@ import {
13
13
  formatRelativeTime,
14
14
  } from "../data/formatters";
15
15
  import { useQuery } from "../data/query";
16
+ import { t } from "../i18n";
16
17
  import { TraceView } from "../traces/TraceView";
17
18
  import type { SessionSummary } from "../types";
18
19
  import {
@@ -49,10 +50,11 @@ function ModelList({ models }: { models: string[] }) {
49
50
 
50
51
  export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps) {
51
52
  const [filter, setFilter] = useState("");
52
- const sessions = useQuery(["sessions", SESSION_LIMIT], () => getSessions(SESSION_LIMIT), {
53
- pollMs: 30000,
54
- enabled: active && session === null,
55
- });
53
+ const sessions = useQuery(
54
+ ["sessions", SESSION_LIMIT],
55
+ ({ signal }) => getSessions(SESSION_LIMIT, undefined, signal),
56
+ { pollMs: 30000, enabled: active && session === null },
57
+ );
56
58
 
57
59
  const filtered = useMemo(() => {
58
60
  const rows = sessions.data ?? [];
@@ -72,7 +74,7 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
72
74
  () => [
73
75
  {
74
76
  key: "title",
75
- header: "Session",
77
+ header: t("trace.session"),
76
78
  width: "32%",
77
79
  render: row => (
78
80
  <LabelCell
@@ -82,10 +84,10 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
82
84
  ),
83
85
  sort: row => (row.title ?? row.file).toLowerCase(),
84
86
  },
85
- { key: "models", header: "Models", render: row => <ModelList models={row.models} /> },
87
+ { key: "models", header: t("common.models"), render: row => <ModelList models={row.models} /> },
86
88
  {
87
89
  key: "started",
88
- header: "Started",
90
+ header: t("common.started"),
89
91
  align: "right",
90
92
  render: row => (
91
93
  <span className="muted" title={new Date(row.startedAt).toLocaleString()}>
@@ -96,28 +98,28 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
96
98
  },
97
99
  {
98
100
  key: "duration",
99
- header: "Duration",
101
+ header: t("common.duration"),
100
102
  align: "right",
101
103
  render: row => <span className="num">{formatElapsed(row.endedAt - row.startedAt)}</span>,
102
104
  sort: row => row.endedAt - row.startedAt,
103
105
  },
104
106
  {
105
107
  key: "requests",
106
- header: "Requests",
108
+ header: t("common.requests"),
107
109
  align: "right",
108
110
  render: row => <span className="num">{formatInteger(row.requests)}</span>,
109
111
  sort: row => row.requests,
110
112
  },
111
113
  {
112
114
  key: "toolCalls",
113
- header: "Tools",
115
+ header: t("trace.tools"),
114
116
  align: "right",
115
117
  render: row => <span className="num">{formatInteger(row.toolCalls)}</span>,
116
118
  sort: row => row.toolCalls,
117
119
  },
118
120
  {
119
121
  key: "subagents",
120
- header: "Agents",
122
+ header: t("trace.agents"),
121
123
  align: "right",
122
124
  render: row =>
123
125
  row.subagents > 0 ? (
@@ -129,14 +131,14 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
129
131
  },
130
132
  {
131
133
  key: "tokens",
132
- header: "Tokens",
134
+ header: t("common.tokens"),
133
135
  align: "right",
134
136
  render: row => <span className="num">{formatCompact(row.totalTokens)}</span>,
135
137
  sort: row => row.totalTokens,
136
138
  },
137
139
  {
138
140
  key: "cost",
139
- header: "Cost",
141
+ header: t("common.cost"),
140
142
  align: "right",
141
143
  width: 150,
142
144
  render: row => (
@@ -159,24 +161,24 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
159
161
  const total = sessions.data?.length ?? 0;
160
162
  return (
161
163
  <div className="stack traces-page">
162
- <PageHeader
163
- title="Traces"
164
- description="Recent sessions with subagent activity folded in. Open one to inspect its timeline."
165
- />
164
+ <PageHeader title={t("trace.title")} description={t("trace.description")} />
166
165
  <Card
167
- title="Sessions"
166
+ title={t("trace.sessions")}
168
167
  description={
169
168
  sessions.data
170
169
  ? filter.trim()
171
- ? `${formatInteger(filtered.length)} of ${formatInteger(total)} most recent`
172
- : `${formatInteger(total)} most recent`
170
+ ? t("trace.most-recent-filtered", {
171
+ shown: formatInteger(filtered.length),
172
+ total: formatInteger(total),
173
+ })
174
+ : t("trace.most-recent", { count: formatInteger(total) })
173
175
  : undefined
174
176
  }
175
177
  actions={
176
178
  <SearchInput
177
179
  value={filter}
178
180
  onChange={setFilter}
179
- placeholder="Filter by title, project, model…"
181
+ placeholder={t("trace.filter-placeholder")}
180
182
  width={260}
181
183
  />
182
184
  }
@@ -188,7 +190,7 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
188
190
  query={sessions}
189
191
  skeleton={<TableSkeleton rows={10} />}
190
192
  isEmpty={rows => rows.length === 0}
191
- empty={<EmptyState title="No sessions found" hint="Run a sync to index recent activity." />}
193
+ empty={<EmptyState title={t("trace.empty-title")} hint={t("trace.empty-hint")} />}
192
194
  >
193
195
  {() => (
194
196
  <Table
@@ -198,7 +200,7 @@ export function TracesRoute({ active, session, onOpenSession }: TracesRouteProps
198
200
  onRowClick={row => onOpenSession(row.file)}
199
201
  initialSort={{ key: "started", dir: "desc" }}
200
202
  limit={50}
201
- empty={<EmptyState title="No matching sessions" hint="Try a different filter." />}
203
+ empty={<EmptyState title={t("trace.no-matching-title")} hint={t("trace.no-matching-hint")} />}
202
204
  />
203
205
  )}
204
206
  </QueryView>
@@ -460,8 +460,10 @@ kbd {
460
460
  gap: 8px;
461
461
  }
462
462
 
463
- .topbar-menu {
464
- display: none;
463
+ @media (min-width: 901px) {
464
+ .btn.topbar-menu {
465
+ display: none;
466
+ }
465
467
  }
466
468
 
467
469
  /* Sync progress hairline along the bottom edge of the topbar. */