diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css index 78fe478afc..b1d9be9930 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css @@ -1096,7 +1096,6 @@ margin: 0; overflow: hidden; color: var(--dsw-alias-label-primary); - font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; } @@ -1106,7 +1105,6 @@ color: inherit; cursor: pointer; font: inherit; - font-variant-numeric: tabular-nums; user-select: text; } diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index 6ce0e22de6..fb62d82e8d 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -270,6 +270,15 @@ function formatStartedAt(timestamp: number | null): string { return `${day} ${time}` } +/** Whether a click lands on an active text selection and should keep it. */ +function clickSelectsText(target: Node): boolean { + const selection = window.getSelection() + return selection !== null + && !selection.isCollapsed + && selection.rangeCount > 0 + && selection.getRangeAt(0).intersectsNode(target) +} + function StartedAtValue({ timestamp }: { timestamp: number | null }) { const [showUnix, setShowUnix] = useState(false) if (timestamp === null || !Number.isFinite(timestamp)) return
Not available
@@ -280,13 +289,7 @@ function StartedAtValue({ timestamp }: { timestamp: number | null }) { className={css.timestampToggle} title={showUnix ? 'Show local time' : 'Show Unix timestamp'} onClick={(event) => { - const selection = window.getSelection() - if ( - selection !== null - && !selection.isCollapsed - && selection.rangeCount > 0 - && selection.getRangeAt(0).intersectsNode(event.currentTarget) - ) return + if (clickSelectsText(event.currentTarget)) return setShowUnix(current => !current) }} > @@ -296,6 +299,26 @@ function StartedAtValue({ timestamp }: { timestamp: number | null }) { ) } +function DurationValue({ seconds }: { seconds: number | null }) { + const [showMillis, setShowMillis] = useState(false) + if (seconds === null || !Number.isFinite(seconds)) return
+ return ( +
+ +
+ ) +} + function totalTime(metrics: AssistantMetricDetail): string { if (!metrics.timingRecorded) return 'Not recorded' if (metrics.stepStartTime === null) return 'Step start unavailable' @@ -1353,7 +1376,7 @@ function RecordTiming({ record }: { record: TableRecord }) { : (
Started
-
Duration
{formatElapsedSeconds(record.cell.timeSeconds)}
+
Duration
Timing source
{record.cell.timeSeconds === null ? 'Not available' : 'Session timestamps'}
) @@ -1376,7 +1399,7 @@ function RequestTiming({ return (
Started
-
Duration
{formatElapsedSeconds(duration)}
+
Duration
Timing source
{duration === null ? 'Session timestamps (running)' : 'Session timestamps'}
@@ -1390,7 +1413,7 @@ function RequestTiming({
Started
-
Duration
+
Duration
) } @@ -1574,7 +1597,12 @@ function OverviewSection({ -
{children}
+
+ {children} +
) } @@ -2563,7 +2591,10 @@ export function TrajectoryTable({ && selectedRequestState !== undefined && activeTab === 'overview' && ( <> -
+
Status
@@ -2714,7 +2745,10 @@ export function TrajectoryTable({ && selectedState !== undefined && activeTab === 'overview' && ( <> -
+
Status
@@ -2723,7 +2757,7 @@ export function TrajectoryTable({
Duration
-
{formatElapsedSeconds(selected.cell.timeSeconds)}
+
Tokens
@@ -2731,7 +2765,10 @@ export function TrajectoryTable({
{selected.cell.outputDetail !== undefined && ( -
+
-
+
{selected.cell.messageSource !== undefined && (
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/ }))