fix(operation-trace): repair pageOf key placement, numbered pager, right column

- pageOf was inserted under the wrong namespace (the locale files already had a
  hideDetails in toolCalls), so the UI rendered the raw key. Removed it and
  replaced the dropdown with a numbered pager (first/last always visible, a
  one-page window around the current page, gaps as an ellipsis).
- Restore the right-hand column (duration + record hash) that the previous
  declutter removed; the digest is still opt-in, but the hash stays visible.
- Hide the status code on success: a 200 is the default, so only >=400 shows.
This commit is contained in:
TyperBody
2026-09-27 02:39:09 +08:00
parent 67bc80e3bc
commit a8daa99884
9 changed files with 80 additions and 47 deletions
@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
@@ -139,6 +140,32 @@ function groupRecords(records: OperationLogRecord[]): GroupedRecord[] {
return grouped;
}
/**
* Build a compact page list: the first and last page are always shown, with a
* one-page window around the current page and gaps collapsed to an ellipsis.
* ``'…'`` entries are rendered as non-clickable separators.
*/
function pageWindow(
current: number,
count: number,
span = 1,
): (number | '…')[] {
if (count <= 0) return [];
const pages = new Set<number>([1, count]);
for (let page = current - span; page <= current + span; page += 1) {
if (page >= 1 && page <= count) pages.add(page);
}
const sorted = [...pages].sort((a, b) => a - b);
const result: (number | '…')[] = [];
let previous = 0;
for (const page of sorted) {
if (previous && page - previous > 1) result.push('…');
result.push(page);
previous = page;
}
return result;
}
/** Compact timestamp: time-only for today, date+time otherwise. */
function formatTimestamp(value: string | null): string {
if (!value) return '';
@@ -744,7 +771,10 @@ export default function OperationTracePanel({
<span aria-hidden="true">·</span>
<span className="font-mono">
{record.http_method}
{record.status_code !== null
{/* A successful 2xx is the default; only surface the
status when it carries a signal (an error). */}
{record.status_code !== null &&
record.status_code >= 400
? ` ${record.status_code}`
: ''}
</span>
@@ -793,21 +823,29 @@ export default function OperationTracePanel({
redactedLabel={t('operationTrace.redacted')}
/>
))}
<div className="flex items-center gap-2 pt-1 text-[10px] text-muted-foreground">
<span>{record.duration_ms}ms</span>
{record.record_hash && (
<span
className="flex items-center gap-1 font-mono"
title={record.record_hash}
>
<Fingerprint className="size-3" />
{record.record_hash.slice(0, 8)}
</span>
)}
</div>
</div>
)}
</ItemContent>
{/* Restore the always-visible right column: each row is
anchored by its duration and tamper-evidence hash, so the
digest stays one click away instead of being the only way
to see it. */}
<ItemActions className="max-sm:hidden">
<div className="flex flex-col items-end gap-0.5 text-right">
<span className="text-[10px] text-muted-foreground">
{record.duration_ms}ms
</span>
{record.record_hash && (
<span
className="flex items-center gap-1 font-mono text-[10px] text-muted-foreground"
title={record.record_hash}
>
<Fingerprint className="size-3" />
{record.record_hash.slice(0, 8)}
</span>
)}
</div>
</ItemActions>
</Item>
);
})}
@@ -836,32 +874,35 @@ export default function OperationTracePanel({
>
<ChevronLeft className="size-4" />
</Button>
{/* Jump straight to a page instead of only stepping one at a
time; with hundreds of records the arrows alone are painful. */}
<Select
value={String(pageIndex)}
onValueChange={(value) =>
setQuery((prev) => ({
...prev,
offset: (Number(value) - 1) * PAGE_SIZE,
}))
}
disabled={loading || pageCount <= 1}
>
<SelectTrigger size="sm" className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: pageCount }, (_, index) => (
<SelectItem key={index} value={String(index + 1)}>
{t('operationTrace.pageOf', {
page: index + 1,
total: pageCount,
})}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Numbered pager: the first and last page stay visible with an
ellipsis between, so any page is one click away without the
arrows filling the bar with "Page x / y" rows. */}
{pageWindow(pageIndex, pageCount).map((item, index) =>
item === '…' ? (
<span
key={`gap-${index}`}
className="px-1 text-xs text-muted-foreground"
>
…
</span>
) : (
<Button
key={item}
size="sm"
variant={item === pageIndex ? 'default' : 'outline'}
className="h-8 min-w-8 px-2 font-mono text-xs"
disabled={loading}
onClick={() =>
setQuery((prev) => ({
...prev,
offset: (item - 1) * PAGE_SIZE,
}))
}
>
{item}
</Button>
),
)}
<Button
size="icon"
variant="outline"
-1
View File
@@ -2548,7 +2548,6 @@ const enUS = {
noToolCalls: 'No tool calls recorded',
showDetails: 'Show details',
hideDetails: 'Hide details',
pageOf: 'Page {{page}} / {{total}}',
},
tokens: {
totalTokens: 'Total Tokens',
-1
View File
@@ -2602,7 +2602,6 @@ const esES = {
noToolCalls: 'No se registraron llamadas de herramientas',
showDetails: 'Mostrar detalles',
hideDetails: 'Ocultar detalles',
pageOf: 'Página {{page}} / {{total}}',
},
tokens: {
totalTokens: 'Tokens totales',
-1
View File
@@ -2565,7 +2565,6 @@ const jaJP = {
noToolCalls: 'ツール呼び出しは記録されていません',
showDetails: '詳細を表示',
hideDetails: '詳細を非表示',
pageOf: '{{page}} / {{total}} ページ',
},
tokens: {
totalTokens: '総トークン数',
-1
View File
@@ -2573,7 +2573,6 @@ const ruRU = {
noToolCalls: 'Вызовы инструментов не записаны',
showDetails: 'Показать детали',
hideDetails: 'Скрыть детали',
pageOf: 'Стр. {{page}} / {{total}}',
},
tokens: {
totalTokens: 'Всего токенов',
-1
View File
@@ -2499,7 +2499,6 @@ const thTH = {
noToolCalls: 'ยังไม่มีการบันทึกการเรียกใช้เครื่องมือ',
showDetails: 'แสดงรายละเอียด',
hideDetails: 'ซ่อนรายละเอียด',
pageOf: 'หน้า {{page}} / {{total}}',
},
tokens: {
totalTokens: 'Token ทั้งหมด',
-1
View File
@@ -2534,7 +2534,6 @@ const viVN = {
noToolCalls: 'Chưa ghi nhận lượt gọi công cụ',
showDetails: 'Hiện chi tiết',
hideDetails: 'Ẩn chi tiết',
pageOf: 'Trang {{page}} / {{total}}',
},
tokens: {
totalTokens: 'Tổng số Token',
-1
View File
@@ -2399,7 +2399,6 @@ const zhHans = {
noToolCalls: '未记录工具调用',
showDetails: '查看详情',
hideDetails: '隐藏详情',
pageOf: '第 {{page}} / {{total}} 页',
},
tokens: {
totalTokens: '总 Token 数',
-1
View File
@@ -2400,7 +2400,6 @@ const zhHant = {
noToolCalls: '未記錄工具呼叫',
showDetails: '查看詳情',
hideDetails: '隱藏詳情',
pageOf: '第 {{page}} / {{total}} 頁',
},
tokens: {
totalTokens: '總 Token 數',