fix(ui-trajectory): distinguish overlapping request markers

This commit is contained in:
_Kerman
2026-08-03 15:01:35 +08:00
parent ef7637a5d8
commit a533cb6ce4
3 changed files with 83 additions and 8 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 {

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,23 @@ function indexRequestNumbers(
return numbers
}
function indexRequestBoundaryRuns(records: readonly TableRecord[]): ReadonlyMap<number, number> {
const indexes = new Map<number, number>()
let previous: TableRecord | undefined
let runIndex = 0
for (const record of records) {
if (record.cell.requestOnly !== true) {
previous = record
runIndex = 0
continue
}
runIndex = previous?.cell.requestOnly === true ? runIndex + 1 : 0
indexes.set(record.cell.index, runIndex)
previous = record
}
return indexes
}
function summarizeTurn(records: readonly TableRecord[]): string {
const steps = new Set(
records
@@ -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({

View File

@@ -222,6 +222,47 @@ describe('TrajectoryTable', () => {
expect(errorResult.closest('[class*="errorPayload"]')).toBeTruthy()
})
it('marks failed requests and lays coincident request markers left to right', () => {
const turns: readonly TrajectoryTurnModel[] = [
{
turn: null,
groups: [{
title: 'Step 1',
cells: [{
index: 1,
kind: 'message',
text: '',
requestOnly: true,
isError: true,
timeSeconds: 0.1,
}],
}],
},
{
turn: null,
groups: [{
title: 'Step 2',
cells: [{
index: 2,
kind: 'message',
text: '',
requestOnly: true,
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' })
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')
})
it('renders responsive role icons with a custom tooltip', () => {
const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
const toolTag = view.container.querySelector<HTMLElement>('[data-role-kind="tool"]')