Origin
@@ -2832,7 +2872,7 @@ export function TrajectoryTable({
{(selected.cell.kind === 'user' || selected.cell.kind === 'context') && (
Duration
- {formatElapsedSeconds(selected.cell.timeSeconds)}
+
)}
diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts
index aa1a9f5e11..832c8cd488 100644
--- a/packages/client/ui-trajectory/src/client/layout.ts
+++ b/packages/client/ui-trajectory/src/client/layout.ts
@@ -17,6 +17,7 @@ import type {
TrajectoryCellProps,
TrajectorySourceBlock,
} from './trajectory-record.ts'
+import { formatElapsedSeconds } from './trajectory-record.ts'
/** One Message or Step group inside a turn. */
export interface TrajectoryGroupModel {
@@ -603,9 +604,7 @@ function groupDescription(laid: readonly LaidCell[]): string | undefined {
function formatGroupDuration(seconds: number): string | undefined {
if (!Number.isFinite(seconds)) return undefined
- const rounded = Math.round(seconds * 10) / 10
- if (Number.isInteger(rounded)) return `${rounded} s`
- return `${rounded.toFixed(1)} s`
+ return formatElapsedSeconds(seconds)
}
/** Own-duration seconds from two epoch-ms stamps; null when either is unusable. */
diff --git a/packages/client/ui-trajectory/src/client/trajectory-record.ts b/packages/client/ui-trajectory/src/client/trajectory-record.ts
index 63cf35fcd7..87cd2e9b21 100644
--- a/packages/client/ui-trajectory/src/client/trajectory-record.ts
+++ b/packages/client/ui-trajectory/src/client/trajectory-record.ts
@@ -106,12 +106,14 @@ export function trajectoryRecordId(cell: TrajectoryCellProps): string {
}
/**
- * Format own-duration for the trailing time column.
+ * Format a duration with a precision that matches its magnitude.
* @param seconds - Duration seconds, or `null` when absent.
- * @returns `—` when unknown, otherwise a seconds label.
+ * @returns `—` when unknown, otherwise an integer-millisecond label
+ * below one second and a tenth-of-a-second label at or above it.
*/
export function formatElapsedSeconds(seconds: number | null): string {
if (seconds === null || !Number.isFinite(seconds)) return '—'
+ if (seconds < 1) return `${Math.round(seconds * 1000)} ms`
const rounded = Math.round(seconds * 10) / 10
if (Number.isInteger(rounded)) return `${rounded} s`
return `${rounded.toFixed(1)} s`
diff --git a/packages/client/ui-trajectory/tests/cell.spec.tsx b/packages/client/ui-trajectory/tests/cell.spec.tsx
index 30f5814998..69e4d638af 100644
--- a/packages/client/ui-trajectory/tests/cell.spec.tsx
+++ b/packages/client/ui-trajectory/tests/cell.spec.tsx
@@ -20,7 +20,10 @@ describe('formatElapsedSeconds', () => {
expect(formatElapsedSeconds(235.0)).toBe('235 s')
expect(formatElapsedSeconds(235.2)).toBe('235.2 s')
expect(formatElapsedSeconds(235.25)).toBe('235.3 s')
- expect(formatElapsedSeconds(0)).toBe('0 s')
+ expect(formatElapsedSeconds(0)).toBe('0 ms')
+ expect(formatElapsedSeconds(0.029)).toBe('29 ms')
+ expect(formatElapsedSeconds(0.5)).toBe('500 ms')
+ expect(formatElapsedSeconds(1.5)).toBe('1.5 s')
expect(formatElapsedSeconds(Number.NaN)).toBe('—')
})
})
diff --git a/packages/client/ui-trajectory/tests/table.spec.tsx b/packages/client/ui-trajectory/tests/table.spec.tsx
index 3bfeaf02de..184466ccb2 100644
--- a/packages/client/ui-trajectory/tests/table.spec.tsx
+++ b/packages/client/ui-trajectory/tests/table.spec.tsx
@@ -97,6 +97,31 @@ describe('TrajectoryTable', () => {
expect(screen.getByText('20.0 tok/s')).toBeTruthy()
})
+ it('toggles a tool record Duration between readable and exact milliseconds', () => {
+ const turns: readonly TrajectoryTurnModel[] = [{
+ turn: 1,
+ groups: [{
+ title: 'Step 1',
+ cells: [{
+ index: 1,
+ kind: 'tool',
+ text: 'bash · {"command":"pwd"}',
+ inputDetail: '{"command":"pwd"}',
+ timeSeconds: 1.5,
+ }],
+ }],
+ }]
+
+ render(
)
+ fireEvent.click(screen.getByRole('row', { name: /TOOL/ }))
+
+ const readable = screen.getByRole('button', { name: '1.5 s' })
+ fireEvent.click(readable)
+ expect(screen.getByRole('button', { name: '1500 ms' })).toBeTruthy()
+ fireEvent.click(screen.getByRole('button', { name: '1500 ms' }))
+ expect(screen.getByRole('button', { name: '1.5 s' })).toBeTruthy()
+ })
+
it('breaks output tokens into labeled reasoning and content rows', () => {
render(
)
fireEvent.click(screen.getByRole('row', { name: /ASSISTANT/ }))