mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(trajectory): display sub-second durations as exact milliseconds
formatElapsedSeconds rounded every duration to one decimal, so calls under 50 ms rendered as 0 s. Durations below one second now show integer milliseconds (29 ms); at or above one second the tenth-of-a-second label is unchanged (1.5 s). Step group descriptions reuse the same formatter. The details-panel Duration rows toggle between the readable label and exact milliseconds on click, mirroring the StartedAt toggle; the shared text-selection guard is extracted for both. tabular-nums is dropped from detail values and the toggle buttons because SF's tnum feature widens the decimal point and leaves excessive space after it.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <dd>Not available</dd>
|
||||
@@ -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 <dd>—</dd>
|
||||
return (
|
||||
<dd>
|
||||
<button
|
||||
type="button"
|
||||
className={css.timestampToggle}
|
||||
title={showMillis ? 'Show readable duration' : 'Show exact milliseconds'}
|
||||
onClick={(event) => {
|
||||
if (clickSelectsText(event.currentTarget)) return
|
||||
setShowMillis(current => !current)
|
||||
}}
|
||||
>
|
||||
{showMillis ? `${Math.round(seconds * 1000)} ms` : formatElapsedSeconds(seconds)}
|
||||
</button>
|
||||
</dd>
|
||||
)
|
||||
}
|
||||
|
||||
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 }) {
|
||||
: (
|
||||
<dl className={css.overview}>
|
||||
<div><dt>Started</dt><StartedAtValue timestamp={record.cell.startedAt ?? null} /></div>
|
||||
<div><dt>Duration</dt><dd>{formatElapsedSeconds(record.cell.timeSeconds)}</dd></div>
|
||||
<div><dt>Duration</dt><DurationValue seconds={record.cell.timeSeconds} /></div>
|
||||
<div><dt>Timing source</dt><dd>{record.cell.timeSeconds === null ? 'Not available' : 'Session timestamps'}</dd></div>
|
||||
</dl>
|
||||
)
|
||||
@@ -1376,7 +1399,7 @@ function RequestTiming({
|
||||
return (
|
||||
<dl className={css.overview}>
|
||||
<div><dt>Started</dt><StartedAtValue timestamp={request.startedAt} /></div>
|
||||
<div><dt>Duration</dt><dd>{formatElapsedSeconds(duration)}</dd></div>
|
||||
<div><dt>Duration</dt><DurationValue seconds={duration} /></div>
|
||||
<div>
|
||||
<dt>Timing source</dt>
|
||||
<dd>{duration === null ? 'Session timestamps (running)' : 'Session timestamps'}</dd>
|
||||
@@ -1390,7 +1413,7 @@ function RequestTiming({
|
||||
<dt>Started</dt>
|
||||
<StartedAtValue timestamp={anchor?.cell.startedAt ?? null} />
|
||||
</div>
|
||||
<div><dt>Duration</dt><dd>—</dd></div>
|
||||
<div><dt>Duration</dt><DurationValue seconds={null} /></div>
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -1574,7 +1597,12 @@ function OverviewSection({
|
||||
<IconChevronRightOutline14 className={css.overviewTitleIcon} size={12} />
|
||||
</button>
|
||||
</h3>
|
||||
<div className={css.overviewPreview}>{children}</div>
|
||||
<div
|
||||
className={`${css.overviewPreview} ${css.summaryScrollRegion}`}
|
||||
data-summary-scroll-region=""
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -2563,7 +2591,10 @@ export function TrajectoryTable({
|
||||
&& selectedRequestState !== undefined
|
||||
&& activeTab === 'overview' && (
|
||||
<>
|
||||
<dl className={css.overview}>
|
||||
<dl
|
||||
className={`${css.overview} ${css.summaryScrollRegion}`}
|
||||
data-summary-scroll-region=""
|
||||
>
|
||||
<div>
|
||||
<dt>Status</dt>
|
||||
<dd className={selectedRequestState === 'error' ? css.error : undefined}>
|
||||
@@ -2714,7 +2745,10 @@ export function TrajectoryTable({
|
||||
&& selectedState !== undefined
|
||||
&& activeTab === 'overview' && (
|
||||
<>
|
||||
<dl className={css.overview}>
|
||||
<dl
|
||||
className={`${css.overview} ${css.summaryScrollRegion}`}
|
||||
data-summary-scroll-region=""
|
||||
>
|
||||
<div>
|
||||
<dt>Status</dt>
|
||||
<dd className={selectedState === 'error' ? css.error : undefined}>
|
||||
@@ -2723,7 +2757,7 @@ export function TrajectoryTable({
|
||||
</div>
|
||||
<div>
|
||||
<dt>Duration</dt>
|
||||
<dd>{formatElapsedSeconds(selected.cell.timeSeconds)}</dd>
|
||||
<DurationValue seconds={selected.cell.timeSeconds} />
|
||||
</div>
|
||||
<div>
|
||||
<dt>Tokens</dt>
|
||||
@@ -2731,7 +2765,10 @@ export function TrajectoryTable({
|
||||
</div>
|
||||
</dl>
|
||||
{selected.cell.outputDetail !== undefined && (
|
||||
<div className={css.compactedSummary}>
|
||||
<div
|
||||
className={`${css.compactedSummary} ${css.summaryScrollRegion}`}
|
||||
data-summary-scroll-region=""
|
||||
>
|
||||
<MarkdownRecordContent
|
||||
record={selected}
|
||||
rendered
|
||||
@@ -2749,7 +2786,10 @@ export function TrajectoryTable({
|
||||
&& selectedState !== undefined
|
||||
&& activeTab === 'overview' && (
|
||||
<>
|
||||
<dl className={css.overview}>
|
||||
<dl
|
||||
className={`${css.overview} ${css.summaryScrollRegion}`}
|
||||
data-summary-scroll-region=""
|
||||
>
|
||||
{selected.cell.messageSource !== undefined && (
|
||||
<div>
|
||||
<dt>Origin</dt>
|
||||
@@ -2832,7 +2872,7 @@ export function TrajectoryTable({
|
||||
{(selected.cell.kind === 'user' || selected.cell.kind === 'context') && (
|
||||
<div>
|
||||
<dt>Duration</dt>
|
||||
<dd>{formatElapsedSeconds(selected.cell.timeSeconds)}</dd>
|
||||
<DurationValue seconds={selected.cell.timeSeconds} />
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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('—')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
|
||||
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(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||
fireEvent.click(screen.getByRole('row', { name: /ASSISTANT/ }))
|
||||
|
||||
Reference in New Issue
Block a user