Merge pull request #1174 from deepseek-harness/xtr/trajectory-marker-tooltip-fixes
Fix trajectory request markers and disclosure affordances
This commit is contained in:
@@ -100,17 +100,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table tbody tr[data-request-only='true'] td {
|
.table tbody tr[data-request-only='true'] td {
|
||||||
height: 1px;
|
height: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
border-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 {
|
.table tbody tr[data-request-only='true']:last-child td {
|
||||||
/* Retain the lower half of the 16px boundary marker at the table's end. */
|
/* Retain the lower half of the 16px boundary marker at the table's end. */
|
||||||
height: 9px;
|
height: 9px;
|
||||||
@@ -130,10 +125,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.requestBoundaryControl {
|
.requestBoundaryControl {
|
||||||
|
--request-boundary-base-left: 12px;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
top: -8px;
|
top: -8px;
|
||||||
left: 12px;
|
left: calc(var(--request-boundary-base-left) + var(--request-boundary-offset, 0px));
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -198,6 +195,12 @@
|
|||||||
box-shadow: 0 0 0 1.5px var(--dsw-alias-brand-primary-new-colorprimary-new-color);
|
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:hover::after,
|
||||||
.requestBoundaryControl:focus-visible::after {
|
.requestBoundaryControl:focus-visible::after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -402,7 +405,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.requestBoundaryControl {
|
.requestBoundaryControl {
|
||||||
left: 6px;
|
--request-boundary-base-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kindSlot {
|
.kindSlot {
|
||||||
@@ -1245,9 +1248,19 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: 600 12px/18px var(--dsw-font-family);
|
font: 600 12px/18px var(--dsw-font-family);
|
||||||
|
gap: 2px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thinkingChevron {
|
||||||
|
flex: none;
|
||||||
|
transition: transform 120ms var(--ds-ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinkingToggle[aria-expanded='true'] .thinkingChevron {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.thinkingToggle:hover {
|
.thinkingToggle:hover {
|
||||||
color: var(--dsw-alias-label-secondary);
|
color: var(--dsw-alias-label-secondary);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,10 @@ type TrajectorySplitStyle = CSSProperties & {
|
|||||||
'--trajectory-tool-request-width': string
|
'--trajectory-tool-request-width': string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestBoundaryStyle = CSSProperties & {
|
||||||
|
'--request-boundary-offset': string
|
||||||
|
}
|
||||||
|
|
||||||
function clampDetailsWidth(width: number, splitWidth: number): number {
|
function clampDetailsWidth(width: number, splitWidth: number): number {
|
||||||
const maxWidth = Math.max(
|
const maxWidth = Math.max(
|
||||||
DETAILS_MIN_WIDTH,
|
DETAILS_MIN_WIDTH,
|
||||||
@@ -453,6 +457,22 @@ function indexRequestNumbers(
|
|||||||
return numbers
|
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 {
|
function summarizeTurn(records: readonly TableRecord[]): string {
|
||||||
const steps = new Set(
|
const steps = new Set(
|
||||||
records
|
records
|
||||||
@@ -1212,7 +1232,8 @@ function MarkdownRecordContent({
|
|||||||
aria-expanded={thinkingExpanded}
|
aria-expanded={thinkingExpanded}
|
||||||
onClick={() => { onThinkingExpandedChange(!thinkingExpanded) }}
|
onClick={() => { onThinkingExpandedChange(!thinkingExpanded) }}
|
||||||
>
|
>
|
||||||
{thinkingExpanded ? 'Thinking' : 'Thinking ...'}
|
Thinking
|
||||||
|
<IconChevronRightOutline14 className={css.thinkingChevron} size={12} />
|
||||||
</button>
|
</button>
|
||||||
{thinkingExpanded && (
|
{thinkingExpanded && (
|
||||||
<MarkdownFragment
|
<MarkdownFragment
|
||||||
@@ -1545,6 +1566,7 @@ export function TrajectoryTable({
|
|||||||
collapsedAssistants,
|
collapsedAssistants,
|
||||||
)
|
)
|
||||||
: filterRecords(allRecords, searchMatchIndexes)
|
: filterRecords(allRecords, searchMatchIndexes)
|
||||||
|
const requestBoundaryRuns = indexRequestBoundaryRuns(records)
|
||||||
const selected = allRecords.find(record => record.cell.index === selectedIndex)
|
const selected = allRecords.find(record => record.cell.index === selectedIndex)
|
||||||
const selectedPrompt = selected?.cell.kind === 'system'
|
const selectedPrompt = selected?.cell.kind === 'system'
|
||||||
? selected.cell.promptDetail
|
? selected.cell.promptDetail
|
||||||
@@ -1791,6 +1813,12 @@ export function TrajectoryTable({
|
|||||||
const requestInfo = request === undefined
|
const requestInfo = request === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: sessionRequestNumbers?.find(candidate => candidate.number === request)
|
: 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
|
const requestLabel = request === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: `Request #${request}${requestInfo?.purpose === 'compaction' ? ' · Compaction' : ''}`
|
: `Request #${request}${requestInfo?.purpose === 'compaction' ? ' · Compaction' : ''}`
|
||||||
@@ -1882,6 +1910,9 @@ export function TrajectoryTable({
|
|||||||
aria-label={requestLabel}
|
aria-label={requestLabel}
|
||||||
aria-pressed={requestSelected}
|
aria-pressed={requestSelected}
|
||||||
data-label={requestLabel}
|
data-label={requestLabel}
|
||||||
|
data-request-run-index={requestRunIndex}
|
||||||
|
data-request-status={requestStatus}
|
||||||
|
style={requestBoundaryStyle}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
selectRequest({
|
selectRequest({
|
||||||
@@ -1930,36 +1961,36 @@ export function TrajectoryTable({
|
|||||||
<span
|
<span
|
||||||
className={css.kindSlot}
|
className={css.kindSlot}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<span
|
||||||
label={KIND_LABEL[record.cell.kind]}
|
className={`${css.kindTag} ${
|
||||||
side="right"
|
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
|
<Tooltip
|
||||||
className={`${css.kindTag} ${
|
label={KIND_LABEL[record.cell.kind]}
|
||||||
record.cell.kind === 'system'
|
side="right"
|
||||||
? 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.kindTagIcon} aria-hidden="true">
|
<span className={css.kindTagIcon} aria-hidden="true">
|
||||||
{KIND_ICON[record.cell.kind]}
|
{KIND_ICON[record.cell.kind]}
|
||||||
</span>
|
</span>
|
||||||
<span className={css.kindTagLabel}>
|
</Tooltip>
|
||||||
{KIND_LABEL[record.cell.kind]}
|
<span className={css.kindTagLabel}>
|
||||||
</span>
|
{KIND_LABEL[record.cell.kind]}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2497,7 +2528,7 @@ export function TrajectoryTable({
|
|||||||
)}
|
)}
|
||||||
{selectedAssistantRequestTarget !== undefined && (
|
{selectedAssistantRequestTarget !== undefined && (
|
||||||
<OverviewSection
|
<OverviewSection
|
||||||
label="Timing"
|
label="Request Timing"
|
||||||
onOpen={() => {
|
onOpen={() => {
|
||||||
selectRequest(selectedAssistantRequestTarget, 'timing')
|
selectRequest(selectedAssistantRequestTarget, 'timing')
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('TrajectoryTable', () => {
|
|||||||
it('shows assistant timing facts after keyboard selection', () => {
|
it('shows assistant timing facts after keyboard selection', () => {
|
||||||
render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||||
fireEvent.keyDown(screen.getByRole('row', { name: /ASSISTANT/ }), { key: 'Enter' })
|
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('500 ms')).toBeTruthy()
|
||||||
expect(screen.getByText('1.00 s')).toBeTruthy()
|
expect(screen.getByText('1.00 s')).toBeTruthy()
|
||||||
@@ -101,10 +101,13 @@ describe('TrajectoryTable', () => {
|
|||||||
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
|
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('row', { name: /ASSISTANT/ }))
|
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()
|
expect(screen.queryByText(thinking)).toBeNull()
|
||||||
|
|
||||||
fireEvent.click(toggle)
|
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)
|
expect(toggle.parentElement?.textContent?.length).toBeGreaterThan(thinking.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -222,19 +225,79 @@ describe('TrajectoryTable', () => {
|
|||||||
expect(errorResult.closest('[class*="errorPayload"]')).toBeTruthy()
|
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 view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||||
const toolTag = view.container.querySelector<HTMLElement>('[data-role-kind="tool"]')
|
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).not.toBeNull()
|
||||||
expect(toolTag?.getAttribute('title')).toBeNull()
|
expect(toolTag?.getAttribute('title')).toBeNull()
|
||||||
expect(toolTag?.querySelector('[data-role-icon="wrench"]')).toBeTruthy()
|
expect(toolIcon).toBeTruthy()
|
||||||
|
|
||||||
fireEvent.mouseEnter(toolTag as HTMLElement)
|
fireEvent.mouseEnter(toolTag as HTMLElement)
|
||||||
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
||||||
|
fireEvent.mouseEnter(toolIcon as HTMLElement)
|
||||||
const tooltip = screen.getByRole('tooltip')
|
const tooltip = screen.getByRole('tooltip')
|
||||||
expect(tooltip.textContent).toBe('TOOL')
|
expect(tooltip.textContent).toBe('TOOL')
|
||||||
expect(tooltip.getAttribute('data-side')).toBe('right')
|
expect(tooltip.getAttribute('data-side')).toBe('right')
|
||||||
fireEvent.mouseLeave(toolTag as HTMLElement)
|
fireEvent.mouseLeave(toolIcon as HTMLElement)
|
||||||
expect(screen.queryByRole('tooltip')).toBeNull()
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user