diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css index 7ef67a9d38..1997faaa6f 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css @@ -32,7 +32,7 @@ } .eventColumn { - width: 96px; + width: 104px; } .contentColumn { @@ -91,6 +91,78 @@ background: var(--dsw-alias-interactive-bg-active); } +.requestBoundaryControl { + position: absolute; + z-index: 6; + top: -8px; + left: 6px; + width: 16px; + height: 16px; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; +} + +.requestBoundaryControl::before { + position: absolute; + top: 6px; + left: 6px; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--dsw-alias-label-caption); + box-shadow: + 0 0 0 2px var(--dsw-alias-bg-layer-1), + 0 0 0 3px transparent; + content: ''; + transition: + background 120ms var(--ds-ease-in-out), + transform 120ms var(--ds-ease-in-out); +} + +.requestBoundaryControl::after { + position: absolute; + top: 2px; + left: 17px; + width: max-content; + padding: 0 4px; + border-radius: 2px; + color: var(--dsw-alias-label-secondary); + background: var(--dsw-alias-bg-layer-1); + content: attr(data-label); + font: 9px/12px var(--ds-font-family-code); + opacity: 0; + pointer-events: none; + transform: translateX(-2px); + transition: + opacity 120ms var(--ds-ease-in-out), + transform 120ms var(--ds-ease-in-out); + user-select: none; + white-space: nowrap; +} + +.requestBoundaryControl:hover::before, +.requestBoundaryControl:focus-visible::before, +.requestBoundaryControl[aria-pressed='true']::before { + background: var(--dsw-alias-label-primary); + transform: scale(1.25); +} + +.requestBoundaryControl:hover::after, +.requestBoundaryControl:focus-visible::after { + opacity: 1; + transform: translateX(0); +} + +.requestBoundaryControl:focus-visible { + outline: none; +} + +.table tbody tr:has(.requestBoundaryControl:hover):not([data-selected='true']) { + background: transparent; +} + .event { position: relative; } @@ -143,7 +215,7 @@ .event { overflow: visible !important; padding-right: 4px !important; - padding-left: 10px !important; + padding-left: 18px !important; } .turnLabel { @@ -475,6 +547,19 @@ color: var(--dsw-alias-label-primary); } +.requestDetailsDot { + flex: none; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--dsw-alias-label-secondary); +} + +.requestDetailsName { + flex: none; + font: 500 12px/16px var(--ds-font-family-code); +} + .detailsLocation { min-width: 0; overflow: hidden; @@ -610,6 +695,10 @@ align-items: center; } +.overview > .requestTokenDetail dt { + padding-left: 12px; +} + .overview dt { color: var(--dsw-alias-label-tertiary); } diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index 2cd39ab5a8..8f24b1de78 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -59,6 +59,12 @@ interface ToolCallTextParts { args?: string } +interface SelectedRequest { + turn: number + number: number + group: string +} + interface DetailsResizeDrag { pointerId: number startX: number @@ -81,6 +87,10 @@ const SYSTEM_PROMPT_TABS: readonly DetailTabItem[] = [ { id: 'system-prompt', label: 'System Prompt' }, { id: 'tools', label: 'Tools' }, ] +const REQUEST_TABS: readonly DetailTabItem[] = [ + { id: 'overview', label: 'Summary' }, + { id: 'timing', label: 'Timing' }, +] type TrajectorySplitStyle = CSSProperties & { '--trajectory-tool-request-width': string @@ -213,6 +223,12 @@ function flattenRecords(turns: readonly TrajectoryTurnModel[]): TableRecord[] { }) } +function requestNumber(group: string): number | undefined { + if (!group.startsWith('Step ')) return undefined + const value = Number(group.slice('Step '.length)) + return Number.isInteger(value) && value > 0 ? value : undefined +} + function summarizeTurn(records: readonly TableRecord[]): string { const userText = records .filter(record => record.cell.kind === 'user') @@ -811,6 +827,25 @@ function RecordTiming({ record }: { record: TableRecord }) { ) } +function RequestTiming({ + assistant, + anchor, +}: { + assistant: TableRecord | undefined + anchor: TableRecord | undefined +}) { + if (assistant !== undefined) return + return ( +
+
+
Started
+ +
+
Duration
+
+ ) +} + function RecordPayload({ record, direction, @@ -983,6 +1018,7 @@ export function TrajectoryTable({ onToggleAssistant, }: TrajectoryTableProps) { const [selectedIndex, setSelectedIndex] = useState(null) + const [selectedRequest, setSelectedRequest] = useState(null) const [activeTab, setActiveTab] = useState('overview') const [thinkingExpanded, setThinkingExpanded] = useState(true) const [detailsWidth, setDetailsWidth] = useState(null) @@ -1000,9 +1036,46 @@ export function TrajectoryTable({ const promptSelected = selectedIndex === SYSTEM_PROMPT_INDEX const selected = allRecords.find(record => record.cell.index === selectedIndex) const selectedState = selected === undefined ? undefined : stateOf(selected) - const selectedTabs = promptSelected - ? SYSTEM_PROMPT_TABS - : selected === undefined ? [] : detailTabs(selected) + const selectedRequestRecords = selectedRequest === null + ? [] + : allRecords.filter(record => + record.turn === selectedRequest.turn + && record.group === selectedRequest.group, + ) + const selectedRequestAssistant = selectedRequestRecords.find( + record => record.cell.kind === 'message', + ) + const selectedRequestAnchor = selectedRequestAssistant ?? selectedRequestRecords[0] + const selectedRequestState: RecordState | undefined = selectedRequest === null + ? undefined + : selectedRequestAssistant?.cell.assistantMetrics?.completedTime === null + ? 'running' + : selectedRequestAssistant === undefined + && selectedRequestRecords.some(record => stateOf(record) === 'running') + ? 'running' + : 'complete' + const selectedRequestToolCalls = selectedRequestRecords.filter( + record => record.cell.kind === 'tool', + ).length + const selectedRequestSubtoolCalls = selectedRequestRecords.filter( + record => record.cell.kind === 'subtool', + ).length + const selectedRequestInputTotal = selectedRequestAssistant !== undefined + && ( + selectedRequestAssistant.cell.input !== undefined + || selectedRequestAssistant.cell.cacheRead !== undefined + || selectedRequestAssistant.cell.cacheWrite !== undefined + ) + ? (selectedRequestAssistant.cell.input ?? 0) + + (selectedRequestAssistant.cell.cacheRead ?? 0) + + (selectedRequestAssistant.cell.cacheWrite ?? 0) + : undefined + const activeTurn = selectedRequest?.turn ?? selected?.turn + const selectedTabs = selectedRequest !== null + ? REQUEST_TABS + : promptSelected + ? SYSTEM_PROMPT_TABS + : selected === undefined ? [] : detailTabs(selected) const selectedParents: ParentRecords = selected === undefined ? {} : parentRecords(allRecords, selected) @@ -1022,6 +1095,7 @@ export function TrajectoryTable({ const selectRecord = (index: number) => { const record = allRecords.find(candidate => candidate.cell.index === index) + setSelectedRequest(null) setSelectedIndex(index) if (record === undefined) return const available = new Set(detailTabs(record).map(tab => tab.id)) @@ -1030,10 +1104,17 @@ export function TrajectoryTable({ } const selectSystemPrompt = () => { + setSelectedRequest(null) setSelectedIndex(SYSTEM_PROMPT_INDEX) activateTab('system-prompt') } + const selectRequest = (request: SelectedRequest) => { + setSelectedIndex(null) + setSelectedRequest(request) + activateTab('overview') + } + const openRecordSummary = (target: TableRecord) => { const targetAt = allRecords.findIndex(record => record.cell.index === target.cell.index) if (collapsedTurns.has(target.turn)) onToggleTurn(target.turn) @@ -1046,6 +1127,7 @@ export function TrajectoryTable({ break } } + setSelectedRequest(null) setSelectedIndex(target.cell.index) activateTab('overview') } @@ -1101,13 +1183,18 @@ export function TrajectoryTable({ ? displayText : [toolCallText.name, toolCallText.args].filter(Boolean).join(' ') const isCollapsedSummary = record.collapsedSummary !== undefined + const request = record.groupStart + && !isCollapsedSummary + && !collapsedTurns.has(record.turn) + ? requestNumber(record.group) + : undefined return ( - {selected?.turn === record.turn && ( + {request !== undefined && ( + @@ -1384,6 +1507,101 @@ export function TrajectoryTable({ role="tabpanel" aria-labelledby={`trajectory-detail-${activeTab}`} > + {selectedRequest !== null + && selectedRequestState !== undefined + && activeTab === 'overview' && ( + <> +
+
+
Status
+
{statusLabel(selectedRequestState)}
+
+
+
Started
+ +
+
+
Duration
+
+ {formatElapsedSeconds( + selectedRequestAssistant?.cell.timeSeconds ?? null, + )} +
+
+ {selectedRequestInputTotal !== undefined && ( +
+
Input
+
{selectedRequestInputTotal} tok
+
+ )} + {selectedRequestAssistant?.cell.cacheRead !== undefined && ( +
+
Cached
+
{selectedRequestAssistant.cell.cacheRead} tok
+
+ )} + {selectedRequestAssistant?.cell.cacheWrite !== undefined && ( +
+
Cache created
+
{selectedRequestAssistant.cell.cacheWrite} tok
+
+ )} + {selectedRequestAssistant?.cell.input !== undefined && ( +
+
Other
+
{selectedRequestAssistant.cell.input} tok
+
+ )} + {selectedRequestAssistant?.cell.output !== undefined && ( +
+
Output
+
{selectedRequestAssistant.cell.output} tok
+
+ )} +
+
Tool calls
+
{selectedRequestToolCalls}
+
+ {selectedRequestSubtoolCalls > 0 && ( +
+
Subtool calls
+
{selectedRequestSubtoolCalls}
+
+ )} +
+
+ {selectedRequestAssistant !== undefined && ( + { openRecordSummary(selectedRequestAssistant) }} + > + + + )} + { activateTab('timing') }}> + + +
+ + )} + {selectedRequest !== null && activeTab === 'timing' && ( + + )} {promptSelected && activeTab === 'system-prompt' && ( prompt === undefined ?

Request header not recorded

diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts index b217c67bd8..fcf92429ff 100644 --- a/packages/client/ui-trajectory/src/client/layout.ts +++ b/packages/client/ui-trajectory/src/client/layout.ts @@ -39,6 +39,8 @@ export interface TrajectoryLayoutInput { interface UsageLike { inputTokens?: number + cacheReadTokens?: number + cacheWriteTokens?: number outputTokens?: number reasoningTokens?: number } @@ -498,7 +500,7 @@ function safeImageSource(value: string): string | undefined { function stringifySourceValue(value: unknown): string { const json = JSON.stringify(value, null, 2) - return json ?? String(value) + return json || String(value) } /** @@ -526,6 +528,8 @@ function enclosingUserTurn( function attachUsage(cell: TrajectoryCellProps, usage: UsageLike | undefined): void { if (usage === undefined) return if (usage.inputTokens !== undefined) cell.input = usage.inputTokens + if (usage.cacheReadTokens !== undefined) cell.cacheRead = usage.cacheReadTokens + if (usage.cacheWriteTokens !== undefined) cell.cacheWrite = usage.cacheWriteTokens if (usage.outputTokens !== undefined) cell.output = usage.outputTokens if (usage.reasoningTokens !== undefined) cell.think = usage.reasoningTokens } diff --git a/packages/client/ui-trajectory/src/client/trajectory-record.ts b/packages/client/ui-trajectory/src/client/trajectory-record.ts index 1469adc77a..140ae8fd53 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-record.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-record.ts @@ -60,6 +60,10 @@ export interface TrajectoryCellProps extends HTMLAttributes { startedAt?: number | null /** Message-only prompt token count. */ input?: number + /** Message-only input tokens served from a provider cache. */ + cacheRead?: number + /** Message-only input tokens written into a provider cache. */ + cacheWrite?: number /** Message-only completion token count. */ output?: number /** Message-only reasoning token count. */