Merge pull request #1174 from deepseek-harness/xtr/trajectory-marker-tooltip-fixes

Fix trajectory request markers and disclosure affordances
This commit is contained in:
imccyu
2026-08-03 18:17:06 +08:00
committed by GitHub
3 changed files with 146 additions and 39 deletions

View File

@@ -100,17 +100,12 @@
}
.table tbody tr[data-request-only='true'] td {
height: 1px;
height: 0;
padding-top: 0;
padding-bottom: 0;
border-bottom: 0;
}
.table tbody tr[data-request-only='true']:has(+ tr[data-request-only='true']) td {
/* Keep consecutive boundary markers from painting their halos over one another. */
height: 9px;
}
.table tbody tr[data-request-only='true']:last-child td {
/* Retain the lower half of the 16px boundary marker at the table's end. */
height: 9px;
@@ -130,10 +125,12 @@
}
.requestBoundaryControl {
--request-boundary-base-left: 12px;
position: absolute;
z-index: 6;
top: -8px;
left: 12px;
left: calc(var(--request-boundary-base-left) + var(--request-boundary-offset, 0px));
width: 16px;
height: 16px;
padding: 0;
@@ -198,6 +195,12 @@
box-shadow: 0 0 0 1.5px var(--dsw-alias-brand-primary-new-colorprimary-new-color);
}
.requestBoundaryControl[data-request-status='error']::before,
.requestBoundaryControl[data-request-status='error']:hover::before,
.requestBoundaryControl[data-request-status='error']:focus-visible::before {
background: var(--dsw-alias-state-error-primary);
}
.requestBoundaryControl:hover::after,
.requestBoundaryControl:focus-visible::after {
opacity: 1;
@@ -402,7 +405,7 @@
}
.requestBoundaryControl {
left: 6px;
--request-boundary-base-left: 6px;
}
.kindSlot {
@@ -1245,9 +1248,19 @@
background: transparent;
cursor: pointer;
font: 600 12px/18px var(--dsw-font-family);
gap: 2px;
user-select: none;
}
.thinkingChevron {
flex: none;
transition: transform 120ms var(--ds-ease-in-out);
}
.thinkingToggle[aria-expanded='true'] .thinkingChevron {
transform: rotate(90deg);
}
.thinkingToggle:hover {
color: var(--dsw-alias-label-secondary);
}

View File

@@ -191,6 +191,10 @@ type TrajectorySplitStyle = CSSProperties & {
'--trajectory-tool-request-width': string
}
type RequestBoundaryStyle = CSSProperties & {
'--request-boundary-offset': string
}
function clampDetailsWidth(width: number, splitWidth: number): number {
const maxWidth = Math.max(
DETAILS_MIN_WIDTH,
@@ -453,6 +457,22 @@ function indexRequestNumbers(
return numbers
}
function indexRequestBoundaryRuns(records: readonly TableRecord[]): ReadonlyMap<number, number> {
const indexes = new Map<number, number>()
let runLength = 0
for (const record of records) {
if (record.cell.requestOnly === true) {
indexes.set(record.cell.index, runLength++)
continue
}
if (runLength > 0 && record.groupStart && requestStep(record.group) !== undefined) {
indexes.set(record.cell.index, runLength)
}
runLength = 0
}
return indexes
}
function summarizeTurn(records: readonly TableRecord[]): string {
const steps = new Set(
records
@@ -1212,7 +1232,8 @@ function MarkdownRecordContent({
aria-expanded={thinkingExpanded}
onClick={() => { onThinkingExpandedChange(!thinkingExpanded) }}
>
{thinkingExpanded ? 'Thinking' : 'Thinking ...'}
Thinking
<IconChevronRightOutline14 className={css.thinkingChevron} size={12} />
</button>
{thinkingExpanded && (
<MarkdownFragment
@@ -1545,6 +1566,7 @@ export function TrajectoryTable({
collapsedAssistants,
)
: filterRecords(allRecords, searchMatchIndexes)
const requestBoundaryRuns = indexRequestBoundaryRuns(records)
const selected = allRecords.find(record => record.cell.index === selectedIndex)
const selectedPrompt = selected?.cell.kind === 'system'
? selected.cell.promptDetail
@@ -1791,6 +1813,12 @@ export function TrajectoryTable({
const requestInfo = request === undefined
? undefined
: sessionRequestNumbers?.find(candidate => candidate.number === request)
const requestStatus = requestInfo?.status
?? (record.cell.isError === true ? 'error' : undefined)
const requestRunIndex = requestBoundaryRuns.get(record.cell.index) ?? 0
const requestBoundaryStyle: RequestBoundaryStyle = {
'--request-boundary-offset': `${requestRunIndex * 8}px`,
}
const requestLabel = request === undefined
? undefined
: `Request #${request}${requestInfo?.purpose === 'compaction' ? ' · Compaction' : ''}`
@@ -1882,6 +1910,9 @@ export function TrajectoryTable({
aria-label={requestLabel}
aria-pressed={requestSelected}
data-label={requestLabel}
data-request-run-index={requestRunIndex}
data-request-status={requestStatus}
style={requestBoundaryStyle}
onClick={(event) => {
event.stopPropagation()
selectRequest({
@@ -1930,36 +1961,36 @@ export function TrajectoryTable({
<span
className={css.kindSlot}
>
<Tooltip
label={KIND_LABEL[record.cell.kind]}
side="right"
<span
className={`${css.kindTag} ${
record.cell.kind === 'system'
? css.systemNeutral
: record.cell.kind === 'context'
? css.contextGreen
: record.cell.kind === 'compacted'
? css.compacted
: record.cell.kind === 'tool'
? css.toolAmber
: record.cell.kind === 'message'
? css.assistantVioletBright
: record.cell.kind === 'subtool'
? css.subtoolAmber
: css[record.cell.kind]
}`}
data-role-kind={record.cell.kind}
>
<span
className={`${css.kindTag} ${
record.cell.kind === 'system'
? css.systemNeutral
: record.cell.kind === 'context'
? css.contextGreen
: record.cell.kind === 'compacted'
? css.compacted
: record.cell.kind === 'tool'
? css.toolAmber
: record.cell.kind === 'message'
? css.assistantVioletBright
: record.cell.kind === 'subtool'
? css.subtoolAmber
: css[record.cell.kind]
}`}
data-role-kind={record.cell.kind}
<Tooltip
label={KIND_LABEL[record.cell.kind]}
side="right"
>
<span className={css.kindTagIcon} aria-hidden="true">
{KIND_ICON[record.cell.kind]}
</span>
<span className={css.kindTagLabel}>
{KIND_LABEL[record.cell.kind]}
</span>
</Tooltip>
<span className={css.kindTagLabel}>
{KIND_LABEL[record.cell.kind]}
</span>
</Tooltip>
</span>
</span>
)}
</div>
@@ -2497,7 +2528,7 @@ export function TrajectoryTable({
)}
{selectedAssistantRequestTarget !== undefined && (
<OverviewSection
label="Timing"
label="Request Timing"
onOpen={() => {
selectRequest(selectedAssistantRequestTarget, 'timing')
}}

View File

@@ -64,7 +64,7 @@ describe('TrajectoryTable', () => {
it('shows assistant timing facts after keyboard selection', () => {
render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
fireEvent.keyDown(screen.getByRole('row', { name: /ASSISTANT/ }), { key: 'Enter' })
fireEvent.click(screen.getByRole('button', { name: 'Timing' }))
fireEvent.click(screen.getByRole('button', { name: 'Request Timing' }))
expect(screen.getByText('500 ms')).toBeTruthy()
expect(screen.getByText('1.00 s')).toBeTruthy()
@@ -101,10 +101,13 @@ describe('TrajectoryTable', () => {
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
fireEvent.click(screen.getByRole('row', { name: /ASSISTANT/ }))
const toggle = screen.getByRole('button', { name: 'Thinking ...' })
const toggle = screen.getByRole('button', { name: 'Thinking' })
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(screen.queryByText(thinking)).toBeNull()
fireEvent.click(toggle)
expect(screen.getByRole('button', { name: 'Thinking' })).toBe(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('true')
expect(toggle.parentElement?.textContent?.length).toBeGreaterThan(thinking.length)
})
@@ -222,19 +225,79 @@ describe('TrajectoryTable', () => {
expect(errorResult.closest('[class*="errorPayload"]')).toBeTruthy()
})
it('renders responsive role icons with a custom tooltip', () => {
it('marks failed requests and lays coincident request markers left to right', () => {
const turns: readonly TrajectoryTurnModel[] = [
{
turn: 1,
groups: [{
title: 'Step 1',
cells: [{
index: 1,
kind: 'message',
text: '',
requestOnly: true,
isError: true,
timeSeconds: 0.1,
}],
}],
},
{
turn: 2,
groups: [{
title: 'Step 1',
cells: [{
index: 2,
kind: 'message',
text: '',
requestOnly: true,
isError: true,
timeSeconds: 0.1,
}],
}],
},
{
turn: 3,
groups: [{
title: 'Step 1',
cells: [{
index: 3,
kind: 'message',
text: 'Recovered response',
timeSeconds: 0.1,
}],
}],
},
]
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
const failed = screen.getByRole('button', { name: 'Request #1' })
const retry = screen.getByRole('button', { name: 'Request #2' })
const recovered = screen.getByRole('button', { name: 'Request #3' })
expect(failed.getAttribute('data-request-status')).toBe('error')
expect(failed.getAttribute('data-request-run-index')).toBe('0')
expect(failed.style.getPropertyValue('--request-boundary-offset')).toBe('0px')
expect(retry.getAttribute('data-request-run-index')).toBe('1')
expect(retry.style.getPropertyValue('--request-boundary-offset')).toBe('8px')
expect(recovered.getAttribute('data-request-run-index')).toBe('2')
expect(recovered.style.getPropertyValue('--request-boundary-offset')).toBe('16px')
})
it('shows the custom role tooltip only from the responsive icon', () => {
const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
const toolTag = view.container.querySelector<HTMLElement>('[data-role-kind="tool"]')
const toolIcon = toolTag?.querySelector<HTMLElement>('[data-role-icon="wrench"]')
expect(toolTag).not.toBeNull()
expect(toolTag?.getAttribute('title')).toBeNull()
expect(toolTag?.querySelector('[data-role-icon="wrench"]')).toBeTruthy()
expect(toolIcon).toBeTruthy()
fireEvent.mouseEnter(toolTag as HTMLElement)
expect(screen.queryByRole('tooltip')).toBeNull()
fireEvent.mouseEnter(toolIcon as HTMLElement)
const tooltip = screen.getByRole('tooltip')
expect(tooltip.textContent).toBe('TOOL')
expect(tooltip.getAttribute('data-side')).toBe('right')
fireEvent.mouseLeave(toolTag as HTMLElement)
fireEvent.mouseLeave(toolIcon as HTMLElement)
expect(screen.queryByRole('tooltip')).toBeNull()
})