Files
deepseek-harness/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
T

2330 lines
83 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Turn-aware trajectory event ledger with a local record inspector. */
import { useEffect, useRef, useState } from 'react'
import type { CSSProperties, ReactNode } from 'react'
import {
extractMarkdownPlainText, IconChevronRightOutline14, JsonTree, MarkdownText,
} from '@deepseek-ai/dsh-client-ui-primitives'
import { structuredPatch } from 'diff'
import type {
AssistantRequestConfig, ConversationPromptSnapshot,
} from '@deepseek-ai/dsh-client-runtime/client'
import type {
AssistantMetricDetail, TrajectoryCellKind, TrajectoryCellProps, TrajectorySourceBlock,
} from './trajectory-record.ts'
import { formatElapsedSeconds } from './trajectory-record.ts'
import type { TrajectoryTurnModel } from './layout.ts'
import css from './TrajectoryTable.module.css'
const KIND_LABEL: Record<TrajectoryCellKind, string> = {
system: 'SYSTEM',
user: 'USER',
context: 'CONTEXT',
compacted: 'COMPACTED',
message: 'ASSISTANT',
tool: 'TOOL',
subtool: 'SUBTOOL',
}
interface TableRecord {
turn: number
group: string
groupStart: boolean
turnStart: boolean
cell: TrajectoryCellProps
turnEnd: boolean
collapsedSummary?: string
collapsedSummaryKind?: 'turn' | 'assistant'
}
type DetailTab =
| 'system-prompt'
| 'tools'
| 'overview'
| 'rendered'
| 'source'
| 'origin'
| 'input'
| 'output'
| 'schema'
| 'options'
| 'usage'
| 'timing'
| 'diff'
type RecordState = 'complete' | 'running' | 'error'
interface DetailTabItem {
id: DetailTab
label: string
}
interface ParentRecords {
message?: TableRecord
tool?: TableRecord
}
interface ToolCallTextParts {
name: string
args?: string
}
interface SelectedRequest {
turn: number
number: number
group: string
}
interface DetailsResizeDrag {
pointerId: number
startX: number
startWidth: number
splitWidth: number
startToolRequestOffset: number
}
const DETAILS_MIN_WIDTH = 320
const DETAILS_MAX_WIDTH = 720
const TABLE_MIN_WIDTH = 280
const DETAILS_RESIZE_STEP = 16
const TOOL_REQUEST_SHARE = 0.58
const TOOL_REQUEST_MIN_WIDTH = 180
const TOOL_REQUEST_MAX_WIDTH = 480
const DEFAULT_TOOL_REQUEST_SHARE = 0.36
const DEFAULT_TOOL_REQUEST_OFFSET = 56
const SYSTEM_PROMPT_TABS: readonly DetailTabItem[] = [
{ id: 'system-prompt', label: 'System Prompt' },
{ id: 'tools', label: 'Tools' },
]
const SYSTEM_UPDATE_TABS: readonly DetailTabItem[] = [
{ id: 'diff', label: 'Diff' },
...SYSTEM_PROMPT_TABS,
]
const REQUEST_TABS: readonly DetailTabItem[] = [
{ id: 'overview', label: 'Summary' },
{ id: 'options', label: 'Options' },
{ id: 'usage', label: 'Usage' },
{ id: 'timing', label: 'Timing' },
]
type TrajectorySplitStyle = CSSProperties & {
'--trajectory-tool-request-width': string
}
function clampDetailsWidth(width: number, splitWidth: number): number {
const maxWidth = Math.max(
DETAILS_MIN_WIDTH,
Math.min(DETAILS_MAX_WIDTH, splitWidth - TABLE_MIN_WIDTH),
)
return Math.round(Math.min(Math.max(width, DETAILS_MIN_WIDTH), maxWidth))
}
function defaultToolRequestWidth(splitWidth: number): number {
return Math.min(
Math.max(
splitWidth * DEFAULT_TOOL_REQUEST_SHARE - DEFAULT_TOOL_REQUEST_OFFSET,
TOOL_REQUEST_MIN_WIDTH,
),
TOOL_REQUEST_MAX_WIDTH,
)
}
function formatDurationMs(milliseconds: number): string {
if (milliseconds < 1_000) return `${Math.round(milliseconds)} ms`
return `${(milliseconds / 1_000).toFixed(milliseconds < 10_000 ? 2 : 1)} s`
}
function formatStartedAt(timestamp: number | null): string {
if (timestamp === null || !Number.isFinite(timestamp)) return 'Not available'
const date = new Date(timestamp)
const two = (value: number) => String(value).padStart(2, '0')
const three = (value: number) => String(value).padStart(3, '0')
const time = `${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())}.${three(date.getMilliseconds())}`
const day = `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())}`
return `${day} ${time}`
}
function StartedAtValue({ timestamp }: { timestamp: number | null }) {
const [showUnix, setShowUnix] = useState(false)
if (timestamp === null || !Number.isFinite(timestamp)) return <dd>Not available</dd>
return (
<dd>
<button
type="button"
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
setShowUnix(current => !current)
}}
>
{showUnix ? (timestamp / 1_000).toFixed(3) : formatStartedAt(timestamp)}
</button>
</dd>
)
}
function totalTime(metrics: AssistantMetricDetail): string {
if (!metrics.timingRecorded) return 'Not recorded'
if (metrics.stepStartTime === null) return 'Step start unavailable'
if (metrics.completedTime === null) return 'Pending'
return formatDurationMs(Math.max(0, metrics.completedTime - metrics.stepStartTime))
}
function ttft(metrics: AssistantMetricDetail): string {
if (!metrics.timingRecorded) return 'Not recorded'
if (metrics.stepStartTime === null) return 'Step start unavailable'
if (metrics.firstTokenTime === null) return 'First token unavailable'
return formatDurationMs(Math.max(0, metrics.firstTokenTime - metrics.stepStartTime))
}
function generationTime(metrics: AssistantMetricDetail): string {
if (!metrics.timingRecorded || metrics.firstTokenTime === null) return 'First token unavailable'
if (metrics.completedTime === null) return 'Pending'
return formatDurationMs(Math.max(0, metrics.completedTime - metrics.firstTokenTime))
}
function throughput(metrics: AssistantMetricDetail): string {
if (!metrics.usageProvided) return 'Usage unavailable'
if (metrics.outputTokens === null) return 'Output tokens unavailable'
if (!metrics.timingRecorded || metrics.firstTokenTime === null) return 'First token unavailable'
if (metrics.completedTime === null) return 'Pending'
const generationSeconds = (metrics.completedTime - metrics.firstTokenTime) / 1_000
if (generationSeconds <= 0) return 'Duration too short'
return `${(metrics.outputTokens / generationSeconds).toFixed(1)} tok/s`
}
function AssistantTimingPanel({ metrics }: { metrics: AssistantMetricDetail }) {
return (
<dl className={css.overview}>
<div><dt>Started</dt><StartedAtValue timestamp={metrics.stepStartTime} /></div>
<div><dt>Total duration</dt><dd>{totalTime(metrics)}</dd></div>
<div><dt>TTFT</dt><dd>{ttft(metrics)}</dd></div>
<div><dt>Generation</dt><dd>{generationTime(metrics)}</dd></div>
<div><dt>Throughput</dt><dd>{throughput(metrics)}</dd></div>
</dl>
)
}
/** Props for the trajectory ledger. */
export interface TrajectoryTableProps {
/** Session-global request numbers for the request groups visible in this context. */
requestNumbers?: readonly TrajectoryRequestNumber[]
/** Grouped records in display order. */
turns: readonly TrajectoryTurnModel[]
/** Record indexes emphasized by the active timeline focus. */
timelineFocusIndexes?: ReadonlySet<number> | null
/** Record indexes retained by the active live search, or null without a query. */
searchMatchIndexes?: ReadonlySet<number> | null
/** Report the record currently selected in the local inspector. */
onSelectedIndexChange?: (index: number | null) => void
/** Report a direct user selection from a ledger row. */
onRecordSelect?: (index: number) => void
/** Clear selection state owned by the ledger host. */
onClearSelection?: () => void
/** Turn ids whose rows after the first are folded into a summary. */
collapsedTurns: ReadonlySet<number>
/** Toggle one turn between folded and expanded. */
onToggleTurn: (turn: number) => void
/** Assistant record indexes whose tool calls are folded. */
collapsedAssistants: ReadonlySet<number>
/** Toggle tool calls under one assistant record. */
onToggleAssistant: (index: number) => void
}
/** One request identity paired with its session-global number. */
export interface TrajectoryRequestNumber {
/** Request anchor event sequence; absent for the currently streaming ordinary request. */
seq?: number
turn: number
step: number
group: string
number: number
purpose?: 'compaction'
status?: 'complete' | 'running' | 'error'
startedAt?: number
completedAt?: number | null
error?: string
retry?: number
maxRetries?: number
retryDelayMs?: number
resultSeq?: number
provider?: string
model?: string
requestConfig?: AssistantRequestConfig
usage?: TrajectoryUsage
cumulativeUsage?: TrajectoryUsage
}
/** Disjoint provider token buckets for one request or a session prefix. */
export interface TrajectoryUsage {
input?: number
cacheRead?: number
cacheWrite?: number
output?: number
reasoning?: number
}
function flattenRecords(turns: readonly TrajectoryTurnModel[]): TableRecord[] {
return turns.flatMap((turn) => {
let firstInTurn = true
const records = turn.groups.flatMap((group) => {
return group.cells.map((cell, index) => {
const turnStart = firstInTurn
&& cell.requestOnly !== true
&& cell.kind !== 'system'
&& cell.kind !== 'compacted'
if (turnStart) firstInTurn = false
return {
turn: turn.turn,
group: group.title,
groupStart: index === 0,
turnStart,
cell,
turnEnd: false,
}
})
})
const last = records.at(-1)
if (last !== undefined) last.turnEnd = true
return records
})
}
function filterRecords(
records: readonly TableRecord[],
matches: ReadonlySet<number>,
): TableRecord[] {
const filtered = records
.filter(record =>
record.cell.requestOnly !== true && matches.has(record.cell.index),
)
.map(record => ({ ...record, groupStart: false, turnStart: false, turnEnd: false }))
const startedTurns = new Set<number>()
for (const [index, record] of filtered.entries()) {
const previous = filtered[index - 1]
const next = filtered[index + 1]
record.groupStart = previous === undefined
|| previous.turn !== record.turn
|| previous.group !== record.group
record.turnStart = !startedTurns.has(record.turn)
&& record.cell.kind !== 'system'
&& record.cell.kind !== 'compacted'
if (record.turnStart) startedTurns.add(record.turn)
record.turnEnd = next === undefined || next.turn !== record.turn
}
return filtered
}
function requestStep(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 requestKey(turn: number, group: string): string {
return `${turn}\u0000${group}`
}
function indexRequestNumbers(
records: readonly TableRecord[],
sessionNumbers: readonly TrajectoryRequestNumber[] | undefined,
): ReadonlyMap<string, number> {
const numbers = new Map<string, number>()
for (const request of sessionNumbers ?? []) {
numbers.set(requestKey(request.turn, request.group), request.number)
}
let next = Math.max(0, ...numbers.values()) + 1
const boundaries = records
.filter(record => record.groupStart && requestStep(record.group) !== undefined)
.sort((left, right) => left.cell.index - right.cell.index)
for (const record of boundaries) {
const key = requestKey(record.turn, record.group)
if (!numbers.has(key)) numbers.set(key, next++)
}
return numbers
}
function summarizeTurn(records: readonly TableRecord[]): string {
const steps = new Set(
records
.map(record => record.group)
.filter(group => group.startsWith('Step ')),
).size
const toolCalls = records.filter(record =>
record.cell.kind === 'tool' || record.cell.kind === 'subtool',
).length
return [
`${steps} ${steps === 1 ? 'step' : 'steps'}`,
`${toolCalls} tool ${toolCalls === 1 ? 'call' : 'calls'}`,
].join(' · ')
}
function collapseTurnRecords(
records: readonly TableRecord[],
collapsedTurns: ReadonlySet<number>,
): TableRecord[] {
if (collapsedTurns.size === 0) return [...records]
const recordsByTurn = new Map<number, TableRecord[]>()
for (const record of records) {
const turnRecords = recordsByTurn.get(record.turn) ?? []
turnRecords.push(record)
recordsByTurn.set(record.turn, turnRecords)
}
return records.flatMap((record) => {
if (!collapsedTurns.has(record.turn)) return [record]
const turnRecords = recordsByTurn.get(record.turn) ?? [record]
if (record.cell.requestOnly === true || record.cell.kind === 'system') return [record]
const contentRecords = turnRecords.filter(candidate =>
candidate.cell.requestOnly !== true && candidate.cell.kind !== 'system')
if (contentRecords.length <= 1) return [record]
if (record.cell.index !== contentRecords[0]?.cell.index) return []
return [
{ ...record, turnEnd: false },
{
...record,
groupStart: false,
turnStart: false,
turnEnd: true,
collapsedSummary: summarizeTurn(contentRecords.slice(1)),
collapsedSummaryKind: 'turn',
},
]
})
}
function assistantToolCalls(
records: readonly TableRecord[],
assistantIndex: number,
): readonly TableRecord[] {
const at = records.findIndex(record => record.cell.index === assistantIndex)
if (at === -1 || records[at]?.cell.kind !== 'message') return []
const calls: TableRecord[] = []
for (let i = at + 1; i < records.length; i++) {
const record = records[i]
if (record === undefined) break
if (record.cell.kind !== 'tool' && record.cell.kind !== 'subtool') break
calls.push(record)
}
return calls
}
function summarizeAssistantTools(records: readonly TableRecord[]): string {
const names = [...new Set(records.map((record) => {
const separator = record.cell.text.indexOf(' · ')
return separator === -1 ? record.cell.text : record.cell.text.slice(0, separator)
}).filter(name => name !== ''))]
const count = records.length
const summary = `${count} tool ${count === 1 ? 'call' : 'calls'}`
return names.length > 0 ? `${summary} · ${names.join(', ')}` : summary
}
function collapseAssistantRecords(
records: readonly TableRecord[],
collapsedAssistants: ReadonlySet<number>,
): TableRecord[] {
if (collapsedAssistants.size === 0) return [...records]
const out: TableRecord[] = []
for (let i = 0; i < records.length; i++) {
const record = records[i]
if (record === undefined) continue
out.push(record)
if (record.cell.kind !== 'message' || !collapsedAssistants.has(record.cell.index)) continue
const calls: TableRecord[] = []
for (let j = i + 1; j < records.length; j++) {
const candidate = records[j]
if (
candidate === undefined
|| candidate.collapsedSummary !== undefined
|| (candidate.cell.kind !== 'tool' && candidate.cell.kind !== 'subtool')
) break
calls.push(candidate)
}
if (calls.length === 0) continue
const last = calls.at(-1)
out[out.length - 1] = { ...record, turnEnd: false }
out.push({
...record,
groupStart: false,
turnStart: false,
turnEnd: last?.turnEnd ?? false,
collapsedSummary: summarizeAssistantTools(calls),
collapsedSummaryKind: 'assistant',
})
i += calls.length
}
return out
}
function stateOf(record: TableRecord): RecordState {
if (record.cell.isError) return 'error'
if (record.cell.kind === 'compacted' && record.cell.timeSeconds === null) return 'running'
if (
(record.cell.kind === 'tool' || record.cell.kind === 'subtool')
&& record.cell.outputDetail === undefined
) return 'running'
return 'complete'
}
function statusLabel(state: RecordState): string {
if (state === 'error') return 'Failed'
if (state === 'running') return 'Pending'
return 'Completed'
}
function TokenRows({ cell }: { cell: TrajectoryCellProps }) {
const content = cell.output !== undefined && cell.think !== undefined
? Math.max(0, cell.output - cell.think)
: undefined
return (
<>
<div>
<dt>Tokens</dt>
<dd>{cell.output === undefined ? '—' : `${cell.output} tok`}</dd>
</div>
{cell.think !== undefined && (
<div className={css.requestTokenDetail}>
<dt>Reasoning</dt>
<dd>{cell.think} tok</dd>
</div>
)}
{content !== undefined && (
<div className={css.requestTokenDetail}>
<dt>Content</dt>
<dd>{content} tok</dd>
</div>
)}
</>
)
}
function inputTotal(usage: TrajectoryUsage): number | undefined {
if (
usage.input === undefined
&& usage.cacheRead === undefined
&& usage.cacheWrite === undefined
) return undefined
return (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0)
}
function UsageRows({ usage }: { usage: TrajectoryUsage | undefined }) {
if (usage === undefined) return <p className={css.noPayload}>Usage not reported</p>
const totalInput = inputTotal(usage)
const otherOutput = usage.output !== undefined && usage.reasoning !== undefined
? usage.output - usage.reasoning
: undefined
return (
<dl className={css.overview}>
{totalInput !== undefined && (
<div><dt>Input</dt><dd>{totalInput} tok</dd></div>
)}
{usage.cacheRead !== undefined && (
<div className={css.requestTokenDetail}>
<dt>Cached</dt>
<dd>{usage.cacheRead} tok</dd>
</div>
)}
{usage.cacheWrite !== undefined && (
<div className={css.requestTokenDetail}>
<dt>Cache created</dt>
<dd>{usage.cacheWrite} tok</dd>
</div>
)}
{usage.input !== undefined && (
<div className={css.requestTokenDetail}>
<dt>Other</dt>
<dd>{usage.input} tok</dd>
</div>
)}
{usage.output !== undefined && (
<div><dt>Output</dt><dd>{usage.output} tok</dd></div>
)}
{usage.reasoning !== undefined && (
<div className={css.requestTokenDetail}>
<dt>Reasoning</dt>
<dd>{usage.reasoning} tok</dd>
</div>
)}
{otherOutput !== undefined && (
<div className={css.requestTokenDetail}>
<dt>Content</dt>
<dd>{otherOutput} tok</dd>
</div>
)}
</dl>
)
}
function RequestUsagePanel({
usage,
cumulative,
}: {
usage: TrajectoryUsage | undefined
cumulative: TrajectoryUsage | undefined
}) {
return (
<div className={css.usagePanel}>
<section className={css.usageGroup}>
<h4 className={css.usageHeading}>This request</h4>
<UsageRows usage={usage} />
</section>
<section className={css.usageGroup}>
<h4 className={css.usageHeading}>Session cumulative</h4>
<UsageRows usage={cumulative} />
</section>
</div>
)
}
function RequestOptions({
options,
preview = false,
}: {
options: AssistantRequestConfig | undefined
preview?: boolean
}) {
if (options === undefined) {
return <p className={css.noPayload}>Options not recorded</p>
}
return (
<JsonTree
data={options}
label="Request options JSON"
className={preview ? css.jsonPreview : css.jsonPayload}
/>
)
}
function messageOriginLabel(source: unknown): string {
if (typeof source !== 'object' || source === null || Array.isArray(source)) {
return 'Unknown'
}
const properties = source as Record<string, unknown>
const kind = properties.kind
if (kind === 'user') return 'User'
if (kind === 'plugin') {
const plugin = properties.plugin
return typeof plugin === 'string' && plugin !== ''
? `Plugin · ${plugin}`
: 'Plugin'
}
if (kind === 'goal') {
const round = properties.round
return typeof round === 'number' && round > 0
? `Goal · Round ${round}`
: 'Goal'
}
if (typeof kind !== 'string' || kind === '') return 'Unknown'
return `${kind[0]?.toUpperCase() ?? ''}${kind.slice(1)}`
}
function MessageOrigin({ record }: { record: TableRecord }) {
const source = record.cell.messageSource
if (source === undefined) return <p className={css.noPayload}>Origin not recorded</p>
const data = typeof source === 'object' && source !== null
? source
: { value: source }
return (
<JsonTree
data={data}
label="Message origin JSON"
className={css.jsonPayload}
/>
)
}
function isMarkdownRecord(record: TableRecord): boolean {
return record.cell.kind === 'user'
|| record.cell.kind === 'context'
|| record.cell.kind === 'message'
}
function parentRecords(
records: readonly TableRecord[],
record: TableRecord,
): ParentRecords {
if (record.cell.kind !== 'tool' && record.cell.kind !== 'subtool') return {}
const at = records.findIndex(candidate => candidate.cell.index === record.cell.index)
if (at === -1) return {}
let tool: TableRecord | undefined
if (record.cell.kind === 'subtool') {
for (let i = at - 1; i >= 0; i--) {
const candidate = records[i]
if (
candidate === undefined
|| candidate.turn !== record.turn
|| candidate.group !== record.group
) break
if (candidate.cell.kind === 'tool') {
tool = candidate
break
}
}
}
const parentCallId = tool?.cell.callId ?? record.cell.callId
let message: TableRecord | undefined
if (parentCallId !== undefined) {
message = records.find(candidate =>
candidate.turn === record.turn
&& candidate.cell.kind === 'message'
&& candidate.cell.sourceBlocks?.some(block => block.callId === parentCallId) === true,
)
}
return { ...(message === undefined ? {} : { message }), ...(tool === undefined ? {} : { tool }) }
}
function markdownSource(record: TableRecord): string | undefined {
if (record.cell.kind === 'user' || record.cell.kind === 'context') {
return record.cell.inputDetail
}
if (record.cell.kind === 'message' || record.cell.kind === 'compacted') {
return record.cell.outputDetail
}
return undefined
}
function detailTabs(record: TableRecord): readonly DetailTabItem[] {
if (record.cell.kind === 'system') {
return record.cell.previousPromptDetail === undefined
? SYSTEM_PROMPT_TABS
: SYSTEM_UPDATE_TABS
}
if (record.cell.kind === 'compacted') {
return [
{ id: 'overview', label: 'Summary' },
{ id: 'source', label: 'Raw Output' },
]
}
if (isMarkdownRecord(record)) {
return [
{ id: 'overview', label: 'Summary' },
{ id: 'rendered', label: 'Preview' },
{ id: 'source', label: 'Source' },
...(record.cell.messageSource === undefined
? []
: [{ id: 'origin', label: 'Origin' } as const]),
]
}
return [
{ id: 'overview', label: 'Summary' },
...(record.cell.inputDetail ? [{ id: 'input', label: 'Payload' } as const] : []),
...(record.cell.outputDetail ? [{ id: 'output', label: 'Result' } as const] : []),
{ id: 'schema', label: 'Schema' },
{ id: 'timing', label: 'Timing' },
]
}
function recordDisplayText(cell: TrajectoryCellProps): string {
if (isToolCallOnly(cell)) return ''
const markdown = cell.kind === 'user' || cell.kind === 'context'
? cell.inputDetail
: cell.kind === 'message'
? cell.outputDetail ?? cell.thinkingDetail
: undefined
if (!markdown) return cell.text
return extractMarkdownPlainText(markdown).replace(/\s+/g, ' ').trim()
}
function toolCallTextParts(
kind: TrajectoryCellKind,
text: string,
): ToolCallTextParts | undefined {
if (kind !== 'tool' && kind !== 'subtool') return undefined
const separator = text.indexOf(' · ')
if (separator === -1) return { name: text }
return {
name: text.slice(0, separator),
args: text.slice(separator + 3),
}
}
function isToolCallOnly(cell: TrajectoryCellProps): boolean {
return cell.kind === 'message'
&& !cell.outputDetail
&& !cell.thinkingDetail
&& cell.text === 'Tool call only'
}
function MarkdownFragment({
text,
rendered,
preview,
}: {
text: string
rendered: boolean
preview: boolean
}) {
if (rendered) {
return (
<div className={preview ? css.markdownPreview : css.markdownPayload}>
<MarkdownText text={text} />
</div>
)
}
return (
<pre className={`${css.payload} ${preview ? css.payloadPreview : ''}`}>
{text}
</pre>
)
}
function SourceBlocks({
blocks,
onOpenCall,
}: {
blocks: readonly TrajectorySourceBlock[]
onOpenCall: (callId: string) => void
}) {
return (
<div className={css.sourceBlocks}>
{blocks.map((block, index) => (
<section className={css.sourceBlock} key={index}>
{block.callId !== undefined
? (
<button
type="button"
className={css.sourceBlockJumpTarget}
aria-label={`Open Block #${index + 1} tool call summary`}
title="Open tool call summary"
onClick={() => {
if (block.callId !== undefined) onOpenCall(block.callId)
}}
>
<span className={css.sourceBlockLabel}>
{`Block #${index + 1} ${block.type}`}
</span>
<IconChevronRightOutline14 className={css.sourceBlockJumpIcon} size={12} />
</button>
)
: (
<div className={css.sourceBlockHeader}>
<span className={css.sourceBlockLabel}>
{`Block #${index + 1} ${block.type}`}
</span>
</div>
)}
{block.imageSrc !== undefined
? <PanelImage block={block} />
: <pre className={css.sourceBlockContent}>{block.content}</pre>}
</section>
))}
</div>
)
}
function PanelImage({
block,
preview = false,
}: {
block: TrajectorySourceBlock
preview?: boolean
}) {
if (block.imageSrc === undefined) return null
return (
<a
className={preview ? `${css.panelImageLink} ${css.panelImageLinkPreview}` : css.panelImageLink}
href={block.imageSrc}
target="_blank"
rel="noopener noreferrer"
title="Open image"
>
<img
className={css.panelImage}
src={block.imageSrc}
alt={block.imageAlt ?? ''}
/>
</a>
)
}
function MessageImages({
blocks,
preview,
}: {
blocks: readonly TrajectorySourceBlock[] | undefined
preview: boolean
}) {
const images = blocks?.filter(block => block.imageSrc !== undefined) ?? []
if (images.length === 0) return null
return (
<div className={preview ? `${css.messageImages} ${css.messageImagesPreview}` : css.messageImages}>
{images.map((block, index) => <PanelImage block={block} preview={preview} key={index} />)}
</div>
)
}
function AssistantToolCalls({
blocks,
preview,
onOpenCall,
}: {
blocks: readonly TrajectorySourceBlock[] | undefined
preview: boolean
onOpenCall: (callId: string) => void
}) {
const calls = blocks?.filter(block => block.type === 'tool-call') ?? []
if (calls.length === 0) return null
return (
<ul className={preview
? `${css.assistantToolCalls} ${css.assistantToolCallsPreview}`
: css.assistantToolCalls}
>
{calls.map((call, index) => (
<li key={call.callId ?? index}>
<button
type="button"
className={css.assistantToolCallButton}
title="Open tool call summary"
onClick={() => {
if (call.callId !== undefined) onOpenCall(call.callId)
}}
>
<svg
className={css.assistantToolCallIcon}
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94z"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className={css.assistantToolCallText}>
<span className={css.assistantToolCallName}>
{call.toolName ?? 'tool-call'}
</span>
{call.content !== '' && (
<span className={css.assistantToolCallArgs}>{call.content}</span>
)}
</span>
</button>
</li>
))}
</ul>
)
}
function ToolGlyph() {
return (
<svg
className={css.toolCatalogIcon}
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94z"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function ToolCatalog({ tools }: { tools: ConversationPromptSnapshot['tools'] }) {
if (tools.length === 0) return <p className={css.noPayload}>No tools in this request</p>
return (
<div className={css.toolCatalog}>
{tools.map((tool, index) => (
<details className={css.toolCatalogItem} key={`${tool.name}:${index}`}>
<summary className={css.toolCatalogSummary}>
<IconChevronRightOutline14 className={css.toolCatalogChevron} size={12} />
<ToolGlyph />
<span className={css.toolCatalogName}>{tool.name}</span>
<span className={css.toolCatalogDescription}>{tool.description}</span>
</summary>
<div className={css.toolCatalogDefinition}>
{tool.description !== '' && (
<p className={css.toolCatalogFullDescription}>{tool.description}</p>
)}
<JsonTree
data={tool.parameters}
label={`${tool.name} parameters JSON`}
className={css.toolCatalogTree}
/>
</div>
</details>
))}
</div>
)
}
interface PromptDiffLine {
kind: 'meta' | 'context' | 'added' | 'removed'
text: string
}
function promptDiffLines(before: string, after: string): readonly PromptDiffLine[] {
const patch = structuredPatch('', '', before, after, undefined, undefined, { context: 3 })
return patch.hunks.flatMap((hunk, hunkIndex) => [
...(hunkIndex === 0 ? [] : [{ kind: 'meta' as const, text: '' }]),
{
kind: 'meta' as const,
text: `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`,
},
...hunk.lines.flatMap((line): PromptDiffLine[] => {
if (line.startsWith('\\')) return []
if (line.startsWith('+')) return [{ kind: 'added', text: line }]
if (line.startsWith('-')) return [{ kind: 'removed', text: line }]
return [{ kind: 'context', text: line }]
}),
])
}
function PromptDiffSection({
title,
before,
after,
}: {
title: string
before: string
after: string
}) {
const lines = promptDiffLines(before, after)
if (lines.length === 0) return null
return (
<section className={css.promptDiffSection}>
<h3 className={css.promptDiffTitle}>{title}</h3>
<pre className={css.promptDiff}>
{lines.map((line, index) => (
<span className={css[`promptDiffLine${line.kind}`]} key={index}>
{line.text || ' '}
{'\n'}
</span>
))}
</pre>
</section>
)
}
function SystemPromptDiff({
before,
after,
}: {
before: ConversationPromptSnapshot
after: ConversationPromptSnapshot
}) {
const toolsBefore = JSON.stringify(before.tools, null, 2)
const toolsAfter = JSON.stringify(after.tools, null, 2)
return (
<div className={css.promptDiffSections}>
{before.system !== after.system && (
<PromptDiffSection
title="System Prompt"
before={before.system}
after={after.system}
/>
)}
{toolsBefore !== toolsAfter && (
<PromptDiffSection
title="Tools"
before={toolsBefore}
after={toolsAfter}
/>
)}
</div>
)
}
function ToolOutputBlocks({
blocks,
preview,
}: {
blocks: readonly TrajectorySourceBlock[]
preview: boolean
}) {
return (
<div className={preview ? `${css.resultBlocks} ${css.resultBlocksPreview}` : css.resultBlocks}>
{blocks.map((block, index) => (
block.imageSrc !== undefined
? <PanelImage block={block} preview={preview} key={index} />
: block.content !== ''
? <pre className={css.resultBlockText} key={index}>{block.content}</pre>
: null
))}
</div>
)
}
function MarkdownRecordContent({
record,
rendered,
preview = false,
thinkingExpanded,
onThinkingExpandedChange,
onOpenCall,
}: {
record: TableRecord
rendered: boolean
preview?: boolean
thinkingExpanded: boolean
onThinkingExpandedChange: (expanded: boolean) => void
onOpenCall: (callId: string) => void
}) {
if (!rendered && record.cell.sourceBlocks && record.cell.sourceBlocks.length > 0) {
return <SourceBlocks blocks={record.cell.sourceBlocks} onOpenCall={onOpenCall} />
}
if (record.cell.thinkingDetail) {
if (!rendered) {
const source = [
record.cell.thinkingDetail,
record.cell.outputDetail,
].filter((value): value is string => value !== undefined && value !== '').join('\n\n')
return <MarkdownFragment text={source} rendered={false} preview={preview} />
}
return (
<div className={`${css.assistantContent} ${css.assistantContentRendered}`}>
<div className={
preview && !record.cell.outputDetail
? `${css.thinkingQuote} ${css.thinkingQuoteOnlyPreview}`
: css.thinkingQuote
}
>
<button
type="button"
className={css.thinkingToggle}
aria-expanded={thinkingExpanded}
onClick={() => { onThinkingExpandedChange(!thinkingExpanded) }}
>
{thinkingExpanded ? 'Thinking' : 'Thinking ...'}
</button>
{thinkingExpanded && (
<MarkdownFragment
text={record.cell.thinkingDetail}
rendered={rendered}
preview={preview}
/>
)}
</div>
{record.cell.outputDetail && (
<div className={css.assistantOutput}>
<MarkdownFragment
text={record.cell.outputDetail}
rendered={rendered}
preview={preview}
/>
</div>
)}
<AssistantToolCalls
blocks={record.cell.sourceBlocks}
preview={preview}
onOpenCall={onOpenCall}
/>
<MessageImages
blocks={record.cell.sourceBlocks}
preview={preview}
/>
</div>
)
}
const source = markdownSource(record)
const hasImages = record.cell.sourceBlocks?.some(block => block.imageSrc !== undefined) === true
const hasToolCalls = record.cell.kind === 'message'
&& record.cell.sourceBlocks?.some(block => block.type === 'tool-call') === true
if (!source && !hasImages && !hasToolCalls) {
const emptyLabel = isToolCallOnly(record.cell)
? 'Tool call only'
: record.cell.text || 'No content'
return <p className={css.noPayload}>{emptyLabel}</p>
}
if (!rendered || (!hasImages && !hasToolCalls)) {
return <MarkdownFragment text={source ?? ''} rendered={rendered} preview={preview} />
}
return (
<div>
{source && <MarkdownFragment text={source} rendered preview={preview} />}
{record.cell.kind === 'message' && (
<AssistantToolCalls
blocks={record.cell.sourceBlocks}
preview={preview}
onOpenCall={onOpenCall}
/>
)}
<MessageImages blocks={record.cell.sourceBlocks} preview={preview} />
</div>
)
}
function RecordTiming({ record }: { record: TableRecord }) {
return record.cell.kind === 'message' && record.cell.assistantMetrics !== undefined
? <AssistantTimingPanel metrics={record.cell.assistantMetrics} />
: (
<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>Timing source</dt><dd>{record.cell.timeSeconds === null ? 'Not available' : 'Session timestamps'}</dd></div>
</dl>
)
}
function RequestTiming({
assistant,
anchor,
request,
}: {
assistant: TableRecord | undefined
anchor: TableRecord | undefined
request: TrajectoryRequestNumber | undefined
}) {
if (assistant !== undefined) return <RecordTiming record={assistant} />
if (request?.startedAt !== undefined) {
const duration = request.completedAt === null || request.completedAt === undefined
? null
: Math.max(0, (request.completedAt - request.startedAt) / 1000)
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>Timing source</dt>
<dd>{duration === null ? 'Session timestamps (running)' : 'Session timestamps'}</dd>
</div>
</dl>
)
}
return (
<dl className={css.overview}>
<div>
<dt>Started</dt>
<StartedAtValue timestamp={anchor?.cell.startedAt ?? null} />
</div>
<div><dt>Duration</dt><dd></dd></div>
</dl>
)
}
function RecordPayload({
record,
direction,
preview = false,
}: {
record: TableRecord
direction: 'input' | 'output'
preview?: boolean
}) {
const value = direction === 'input' ? record.cell.inputDetail : record.cell.outputDetail
const missing = direction === 'input'
? 'No payload captured'
: 'No result captured'
if (!value) return <p className={css.noPayload}>{missing}</p>
const json = parseJsonContainer(value)
const singleTextResult = direction === 'output'
&& record.cell.outputBlocks?.length === 1
&& record.cell.outputBlocks[0]?.type === 'text'
if (singleTextResult && json !== undefined) {
return (
<JsonTree
data={json}
label="Result JSON"
className={preview ? css.jsonPreview : css.jsonPayload}
/>
)
}
if (
direction === 'output'
&& record.cell.outputBlocks?.some(block =>
block.imageSrc !== undefined || block.content !== '') === true
) {
return (
<ToolOutputBlocks
blocks={record.cell.outputBlocks}
preview={preview}
/>
)
}
const markdown = (
direction === 'input'
&& (record.cell.kind === 'user' || record.cell.kind === 'context')
) || (
direction === 'output' && record.cell.kind === 'message'
)
if (markdown) {
return (
<div className={preview ? css.markdownPreview : css.markdownPayload}>
<MarkdownText text={value} />
</div>
)
}
if (json !== undefined) {
return (
<JsonTree
data={json}
label={`${direction === 'input' ? 'Payload' : 'Result'} JSON`}
className={preview ? css.jsonPreview : css.jsonPayload}
/>
)
}
return (
<pre className={[
css.payload,
preview ? css.payloadPreview : undefined,
record.cell.isError ? css.error : undefined,
value === 'No output' ? css.noOutputText : undefined,
].filter((value): value is string => value !== undefined).join(' ')}
>
{value}
</pre>
)
}
function RecordSchema({
record,
preview = false,
}: {
record: TableRecord
preview?: boolean
}) {
if (!record.cell.schemaDetail) {
return <p className={css.noPayload}>Schema unavailable</p>
}
const schema = parseToolSchema(record.cell.schemaDetail)
if (schema !== undefined) {
return (
<div className={preview ? `${css.schema} ${css.schemaPreview}` : css.schema}>
<header className={css.schemaIntro}>
<h3 className={css.schemaName}>{schema.name}</h3>
<p className={css.schemaDescription}>{schema.description}</p>
</header>
<section className={css.schemaParameters}>
<h4 className={css.schemaParametersTitle}>Parameters</h4>
<JsonTree
data={schema.parameters}
label={`${schema.name} parameters JSON`}
className={css.schemaTree}
/>
</section>
</div>
)
}
return (
<pre className={`${css.payload} ${preview ? css.payloadPreview : ''}`}>
{record.cell.schemaDetail}
</pre>
)
}
interface ParsedToolSchema {
name: string
description: string
parameters: object
}
function parseToolSchema(value: string): ParsedToolSchema | undefined {
try {
const parsed: unknown = JSON.parse(value)
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined
const schema = parsed as Record<string, unknown>
if (
typeof schema.name !== 'string'
|| typeof schema.description !== 'string'
|| typeof schema.parameters !== 'object'
|| schema.parameters === null
|| Array.isArray(schema.parameters)
) return undefined
return {
name: schema.name,
description: schema.description,
parameters: schema.parameters,
}
} catch {
return undefined
}
}
function parseJsonContainer(value: string): object | undefined {
try {
const parsed: unknown = JSON.parse(value)
return typeof parsed === 'object' && parsed !== null ? parsed : undefined
} catch {
return undefined
}
}
function OverviewSection({
label,
onOpen,
children,
}: {
label: string
onOpen: () => void
children: ReactNode
}) {
return (
<section className={css.overviewSection}>
<h3 className={css.overviewHeading}>
<button
type="button"
className={css.overviewTitle}
onClick={onOpen}
>
<span>{label}</span>
<IconChevronRightOutline14 className={css.overviewTitleIcon} size={12} />
</button>
</h3>
<div className={css.overviewPreview}>{children}</div>
</section>
)
}
/**
* Render trajectory events as a dense ledger with turn and step separators.
* Clicking ledger whitespace clears the active record or request selection.
* @param props - Grouped trajectory data and whole-ledger fold state.
* @returns The ledger and an optional local record inspector.
*/
export function TrajectoryTable({
requestNumbers: sessionRequestNumbers,
turns,
timelineFocusIndexes = null,
searchMatchIndexes = null,
onSelectedIndexChange,
onRecordSelect,
onClearSelection,
collapsedTurns,
onToggleTurn,
collapsedAssistants,
onToggleAssistant,
}: TrajectoryTableProps) {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
const [selectedRequest, setSelectedRequest] = useState<SelectedRequest | null>(null)
const [activeTab, setActiveTab] = useState<DetailTab>('overview')
const [thinkingExpanded, setThinkingExpanded] = useState(true)
const [detailsWidth, setDetailsWidth] = useState<number | null>(null)
const [toolRequestOffset, setToolRequestOffset] = useState<number | null>(null)
const detailsResizeDrag = useRef<DetailsResizeDrag | null>(null)
const tabHistory = useRef<Set<DetailTab>>(new Set(['overview']))
useEffect(() => {
onSelectedIndexChange?.(selectedIndex)
}, [onSelectedIndexChange, selectedIndex])
const allRecords = flattenRecords(turns)
const requestNumbers = indexRequestNumbers(allRecords, sessionRequestNumbers)
const records = searchMatchIndexes === null
? collapseAssistantRecords(
collapseTurnRecords(allRecords, collapsedTurns),
collapsedAssistants,
)
: filterRecords(allRecords, searchMatchIndexes)
const selected = allRecords.find(record => record.cell.index === selectedIndex)
const selectedPrompt = selected?.cell.kind === 'system'
? selected.cell.promptDetail
: undefined
const selectedPreviousPrompt = selected?.cell.kind === 'system'
? selected.cell.previousPromptDetail
: undefined
const promptSelected = selectedPrompt !== undefined
const selectedState = selected === undefined ? undefined : stateOf(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 selectedRequestInfo = selectedRequest === null
? undefined
: sessionRequestNumbers?.find(request => request.number === selectedRequest.number)
const selectedRequestState: RecordState | undefined = selectedRequest === null
? undefined
: selectedRequestInfo?.status
?? (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 selectedRequestResult = selectedRequestInfo?.resultSeq === undefined
? selectedRequestAssistant
: allRecords.find(record => record.cell.sourceSeq === selectedRequestInfo.resultSeq)
const selectedRequestUsage = selectedRequestInfo?.usage ?? (
selectedRequestAssistant === undefined
? undefined
: {
...(selectedRequestAssistant.cell.input === undefined
? {}
: { input: selectedRequestAssistant.cell.input }),
...(selectedRequestAssistant.cell.cacheRead === undefined
? {}
: { cacheRead: selectedRequestAssistant.cell.cacheRead }),
...(selectedRequestAssistant.cell.cacheWrite === undefined
? {}
: { cacheWrite: selectedRequestAssistant.cell.cacheWrite }),
...(selectedRequestAssistant.cell.output === undefined
? {}
: { output: selectedRequestAssistant.cell.output }),
...(selectedRequestAssistant.cell.think === undefined
? {}
: { reasoning: selectedRequestAssistant.cell.think }),
}
)
const selectedRequestCumulativeUsage =
selectedRequestInfo?.cumulativeUsage ?? selectedRequestUsage
const selectedRequestOptions = selectedRequestInfo?.requestConfig
const activeTurn = selectedRequest?.turn ?? selected?.turn
const selectedTabs = selectedRequest !== null
? REQUEST_TABS.filter(tab => tab.id !== 'options' || selectedRequestOptions !== undefined)
: selected === undefined ? [] : detailTabs(selected)
const selectedParents: ParentRecords = selected === undefined
? {}
: parentRecords(allRecords, selected)
const selectedParentMessage = selectedParents.message
const selectedParentTool = selectedParents.tool
const selectedAssistantRequest = selected?.cell.kind === 'message'
? requestNumbers.get(requestKey(selected.turn, selected.group))
: undefined
const selectedAssistantRequestTarget: SelectedRequest | undefined =
selected !== undefined && selectedAssistantRequest !== undefined
? {
turn: selected.turn,
number: selectedAssistantRequest,
group: selected.group,
}
: undefined
const hasSelectedHierarchy = selectedAssistantRequestTarget !== undefined
|| selectedParents.message !== undefined
|| selectedParents.tool !== undefined
const splitStyle: TrajectorySplitStyle | undefined = toolRequestOffset === null
? undefined
: {
'--trajectory-tool-request-width': `calc(58cqw - ${toolRequestOffset}px)`,
}
const activateTab = (tab: DetailTab) => {
tabHistory.current.delete(tab)
tabHistory.current.add(tab)
setActiveTab(tab)
}
const clearInspectorSelection = () => {
setSelectedIndex(null)
setSelectedRequest(null)
}
const clearAllSelections = () => {
clearInspectorSelection()
onClearSelection?.()
}
const selectRecord = (index: number) => {
const record = allRecords.find(candidate => candidate.cell.index === index)
onRecordSelect?.(index)
setSelectedRequest(null)
setSelectedIndex(index)
if (record === undefined) return
const tabs = detailTabs(record)
const available = new Set(tabs.map(tab => tab.id))
const recent = [...tabHistory.current].reverse().find(tab => available.has(tab))
setActiveTab(recent ?? tabs[0]?.id ?? 'overview')
}
const selectRequest = (
request: SelectedRequest,
tab: 'overview' | 'timing' = 'overview',
) => {
setSelectedIndex(null)
setSelectedRequest(request)
activateTab(tab)
}
const openRecordSummary = (target: TableRecord) => {
const targetAt = allRecords.findIndex(record => record.cell.index === target.cell.index)
if (collapsedTurns.has(target.turn)) onToggleTurn(target.turn)
if (target.cell.kind === 'tool' || target.cell.kind === 'subtool') {
for (let i = targetAt - 1; i >= 0; i--) {
const candidate = allRecords[i]
if (candidate === undefined || candidate.turn !== target.turn) break
if (candidate.cell.kind !== 'message') continue
if (collapsedAssistants.has(candidate.cell.index)) onToggleAssistant(candidate.cell.index)
break
}
}
setSelectedRequest(null)
setSelectedIndex(target.cell.index)
activateTab('overview')
}
const openCallSummary = (callId: string) => {
const target = allRecords.find(record => record.cell.callId === callId)
if (target !== undefined) openRecordSummary(target)
}
return (
<div className={css.split} style={splitStyle}>
<div
className={css.tablePane}
onClick={(event) => {
if (event.target === event.currentTarget) clearAllSelections()
}}
>
<table className={css.table}>
<colgroup>
<col className={css.eventColumn} />
<col className={css.contentColumn} />
</colgroup>
<tbody>
{records.map((record) => {
const displayText = recordDisplayText(record.cell)
const toolCallText = toolCallTextParts(record.cell.kind, displayText)
const listDisplayText = toolCallText === undefined
? displayText
: [toolCallText.name, toolCallText.args].filter(Boolean).join(' ')
const isCollapsedSummary = record.collapsedSummary !== undefined
const isRequestOnly = record.cell.requestOnly === true
const isInitialSystem = record.cell.kind === 'system'
&& record.cell.index === allRecords[0]?.cell.index
const request = record.groupStart
&& !isCollapsedSummary
&& !collapsedTurns.has(record.turn)
? requestNumbers.get(requestKey(record.turn, record.group))
: undefined
const requestInfo = request === undefined
? undefined
: sessionRequestNumbers?.find(candidate => candidate.number === request)
const requestLabel = request === undefined
? undefined
: `Request #${request}${requestInfo?.purpose === 'compaction' ? ' · Compaction' : ''}`
const requestSelected = request !== undefined
&& selectedRequest?.turn === record.turn
&& selectedRequest.number === request
return (
<tr
key={`${record.cell.index}:${record.collapsedSummaryKind ?? 'record'}`}
tabIndex={isRequestOnly ? -1 : 0}
aria-label={isCollapsedSummary
? `Collapsed ${record.collapsedSummaryKind} summary, ${record.collapsedSummary}`
: isRequestOnly
? `Request ${request ?? ''}, compaction`
: `${request === undefined ? '' : `Request ${request}, `}${KIND_LABEL[record.cell.kind]}, ${listDisplayText || 'no content'}`}
aria-selected={!isCollapsedSummary && !isRequestOnly && selectedIndex === record.cell.index}
data-kind={record.cell.kind}
data-record-index={!isCollapsedSummary && !isRequestOnly
? record.cell.index
: undefined}
data-request-only={isRequestOnly || undefined}
data-group-start={record.groupStart || undefined}
data-turn-start={record.turnStart || undefined}
data-error={record.cell.isError || undefined}
data-running={stateOf(record) === 'running' || undefined}
data-turn-end={record.turnEnd || undefined}
data-collapsed-summary={record.collapsedSummaryKind}
data-selected={!isCollapsedSummary && selectedIndex === record.cell.index || undefined}
data-timeline-focus={isCollapsedSummary || timelineFocusIndexes === null
? undefined
: timelineFocusIndexes.has(record.cell.index) ? 'inside' : 'outside'}
onClick={isRequestOnly
? undefined
: isCollapsedSummary
? () => {
if (record.collapsedSummaryKind === 'turn') onToggleTurn(record.turn)
else onToggleAssistant(record.cell.index)
}
: () => { selectRecord(record.cell.index) }}
onDoubleClick={(event) => {
if (isCollapsedSummary || isRequestOnly) return
if (collapsedTurns.has(record.turn)) {
event.preventDefault()
onToggleTurn(record.turn)
return
}
if (
record.cell.kind === 'message'
&& assistantToolCalls(allRecords, record.cell.index).length > 0
) {
event.preventDefault()
onToggleAssistant(record.cell.index)
return
}
if (!record.turnStart) return
if (allRecords.filter(candidate =>
candidate.turn === record.turn
&& candidate.cell.requestOnly !== true
&& candidate.cell.kind !== 'system').length <= 1) return
event.preventDefault()
onToggleTurn(record.turn)
}}
onKeyDown={(event) => {
if (isRequestOnly) return
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
if (isCollapsedSummary) {
if (record.collapsedSummaryKind === 'turn') onToggleTurn(record.turn)
else onToggleAssistant(record.cell.index)
return
}
selectRecord(record.cell.index)
}}
>
<td className={css.event}>
{request !== undefined && (
<button
type="button"
className={requestSelected
? `${css.requestBoundaryControl} ${css.requestBoundaryControlActive}`
: css.requestBoundaryControl}
aria-label={requestLabel}
aria-pressed={requestSelected}
data-label={requestLabel}
onClick={(event) => {
event.stopPropagation()
selectRequest({
turn: record.turn,
number: request,
group: record.group,
})
}}
onDoubleClick={(event) => { event.stopPropagation() }}
/>
)}
{activeTurn === record.turn && !isInitialSystem && (
<span className={css.turnRail} aria-hidden="true" />
)}
{!isCollapsedSummary && selectedIndex === record.cell.index && (
<span className={css.selectionRail} aria-hidden="true" />
)}
{!isCollapsedSummary
&& !isRequestOnly
&& record.turnStart && (
<span
className={activeTurn === record.turn
? `${css.turnLabel} ${css.turnLabelActive}`
: css.turnLabel}
>
Turn {record.turn}
</span>
)}
<div className={css.eventInner}>
{!isCollapsedSummary && !isRequestOnly && (
<span
className={css.kindSlot}
>
<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]
}`}
>
{KIND_LABEL[record.cell.kind]}
</span>
</span>
)}
</div>
</td>
<td className={css.content}>
{isRequestOnly
? null
: record.collapsedSummary !== undefined
? (
<span className={css.collapsedTurnContent} title={record.collapsedSummary}>
<span className={css.collapsedTurnEllipsis}></span>
<span className={css.collapsedTurnText}>{record.collapsedSummary}</span>
</span>
)
: (
<span
className={record.cell.result === undefined ? css.contentText : css.resultPreview}
title={record.cell.result === undefined
? listDisplayText
: `${listDisplayText}${record.cell.result}`}
>
<span className={record.cell.result === undefined ? undefined : css.resultRequest}>
{isToolCallOnly(record.cell)
? null
: toolCallText === undefined
? listDisplayText || '—'
: (
<>
<span className={css.toolCallNameTypeface}>
{toolCallText.name || '—'}
</span>
{toolCallText.args !== undefined && (
<span className={css.toolCallPayload}>
{toolCallText.args}
</span>
)}
</>
)}
</span>
{record.cell.result !== undefined && (
<span className={record.cell.isError ? `${css.inlineResult} ${css.error}` : css.inlineResult}>
<span className={css.arrow}></span>
<span className={record.cell.result === 'No output'
? `${css.inlineResultText} ${css.noOutputText}`
: css.inlineResultText}
>
{record.cell.result}
</span>
</span>
)}
</span>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{(selectedRequest !== null
|| promptSelected
|| (selected !== undefined && selectedState !== undefined)) && (
<aside
className={css.details}
aria-label="Event details"
style={detailsWidth === null ? undefined : { width: detailsWidth }}
>
<div
className={css.detailsResizeHandle}
role="separator"
aria-label="Resize event details"
aria-controls="trajectory-detail-panel"
aria-orientation="vertical"
tabIndex={0}
title="Drag to resize. Double-click to reset."
onDoubleClick={() => {
setDetailsWidth(null)
setToolRequestOffset(null)
}}
onPointerDown={(event) => {
if (event.button !== 0) return
const details = event.currentTarget.parentElement
if (details === null) return
const split = details.parentElement
if (split === null) return
const splitWidth = split.getBoundingClientRect().width
detailsResizeDrag.current = {
pointerId: event.pointerId,
startX: event.clientX,
startWidth: details.getBoundingClientRect().width,
splitWidth,
startToolRequestOffset: toolRequestOffset ?? (
splitWidth * TOOL_REQUEST_SHARE - defaultToolRequestWidth(splitWidth)
),
}
event.currentTarget.setPointerCapture(event.pointerId)
event.preventDefault()
}}
onPointerMove={(event) => {
const drag = detailsResizeDrag.current
if (drag === null || drag.pointerId !== event.pointerId) return
const nextDetailsWidth = clampDetailsWidth(
drag.startWidth + drag.startX - event.clientX,
drag.splitWidth,
)
setDetailsWidth(nextDetailsWidth)
setToolRequestOffset(
drag.startToolRequestOffset
+ (nextDetailsWidth - drag.startWidth) * TOOL_REQUEST_SHARE,
)
}}
onPointerUp={(event) => {
if (detailsResizeDrag.current?.pointerId !== event.pointerId) return
detailsResizeDrag.current = null
event.currentTarget.releasePointerCapture(event.pointerId)
}}
onPointerCancel={() => {
detailsResizeDrag.current = null
}}
onKeyDown={(event) => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
const details = event.currentTarget.parentElement
if (details === null) return
const split = details.parentElement
if (split === null) return
const direction = event.key === 'ArrowLeft' ? 1 : -1
const currentDetailsWidth = details.getBoundingClientRect().width
const splitWidth = split.getBoundingClientRect().width
const nextDetailsWidth = clampDetailsWidth(
currentDetailsWidth + direction * DETAILS_RESIZE_STEP,
splitWidth,
)
const currentToolRequestOffset = toolRequestOffset ?? (
splitWidth * TOOL_REQUEST_SHARE - defaultToolRequestWidth(splitWidth)
)
setDetailsWidth(nextDetailsWidth)
setToolRequestOffset(
currentToolRequestOffset
+ (nextDetailsWidth - currentDetailsWidth) * TOOL_REQUEST_SHARE,
)
event.preventDefault()
}}
/>
<div className={css.detailsHeader}>
<div className={css.detailsTitle}>
{selectedRequest !== null
? (
<>
<span className={css.requestDetailsDot} aria-hidden="true" />
<span className={css.requestDetailsName}>
Request #{selectedRequest.number}
</span>
<span className={css.detailsLocation}>
{selectedRequestInfo?.purpose === 'compaction'
? `Compaction · Turn ${selectedRequest.turn}`
: `Turn ${selectedRequest.turn}`}
</span>
</>
)
: promptSelected
? (
<>
<span className={`${css.kindTag} ${css.systemNeutral}`}>SYSTEM</span>
<span className={css.detailsLocation}>{selected?.cell.text}</span>
</>
)
: selected !== undefined && (
<>
<span className={`${css.kindTag} ${
selected.cell.kind === 'context'
? css.contextGreen
: selected.cell.kind === 'compacted'
? css.compacted
: selected.cell.kind === 'tool'
? css.toolAmber
: selected.cell.kind === 'message'
? css.assistantVioletBright
: selected.cell.kind === 'subtool'
? css.subtoolAmber
: css[selected.cell.kind]
}`}
>
{KIND_LABEL[selected.cell.kind]}
</span>
<span className={css.detailsLocation}>
{selected.cell.kind === 'compacted'
? `Turn ${selected.turn}`
: `Turn ${selected.turn} · ${selected.group}`}
</span>
</>
)}
</div>
<button
type="button"
className={css.close}
aria-label="Close details"
onClick={clearInspectorSelection}
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className={css.detailTabs} role="tablist" aria-label="Event details">
{selectedTabs.map(tab => (
<button
key={tab.id}
id={`trajectory-detail-${tab.id}`}
type="button"
role="tab"
aria-controls="trajectory-detail-panel"
aria-selected={activeTab === tab.id}
className={activeTab === tab.id ? `${css.detailTab} ${css.detailTabActive}` : css.detailTab}
onClick={() => { activateTab(tab.id) }}
>
{tab.label}
</button>
))}
</div>
<div
id="trajectory-detail-panel"
className={activeTab === 'overview'
? `${css.detailBody} ${css.detailBodySummary}`
: css.detailBody}
role="tabpanel"
aria-labelledby={`trajectory-detail-${activeTab}`}
>
{selectedRequest !== null
&& selectedRequestState !== undefined
&& activeTab === 'overview' && (
<>
<dl className={css.overview}>
<div>
<dt>Status</dt>
<dd>{statusLabel(selectedRequestState)}</dd>
</div>
{selectedRequestInfo?.purpose === 'compaction' && (
<div>
<dt>Purpose</dt>
<dd>Compaction</dd>
</div>
)}
{(selectedRequestInfo?.provider
?? selectedRequestInfo?.requestConfig?.provider) !== undefined && (
<div>
<dt>Provider</dt>
<dd>
{selectedRequestInfo?.provider
?? selectedRequestInfo?.requestConfig?.provider}
</dd>
</div>
)}
{(selectedRequestInfo?.model
?? selectedRequestInfo?.requestConfig?.model) !== undefined && (
<div>
<dt>Model</dt>
<dd>
{selectedRequestInfo?.model
?? selectedRequestInfo?.requestConfig?.model}
</dd>
</div>
)}
<div>
<dt>Tool calls</dt>
<dd>{selectedRequestToolCalls}</dd>
</div>
{selectedRequestSubtoolCalls > 0 && (
<div>
<dt>Subtool calls</dt>
<dd>{selectedRequestSubtoolCalls}</dd>
</div>
)}
{selectedRequestInfo?.error !== undefined && (
<div>
<dt>Error</dt>
<dd>{selectedRequestInfo.error}</dd>
</div>
)}
{selectedRequestInfo?.retry !== undefined && (
<div>
<dt>Retry</dt>
<dd>
Scheduled {selectedRequestInfo.retry}
{selectedRequestInfo.maxRetries === undefined
? ''
: ` of ${selectedRequestInfo.maxRetries}`}
</dd>
</div>
)}
{selectedRequestInfo?.retryDelayMs !== undefined && (
<div>
<dt>Retry delay</dt>
<dd>{formatDurationMs(selectedRequestInfo.retryDelayMs)}</dd>
</div>
)}
{selectedRequestResult !== undefined && (
<div>
<dt>Result</dt>
<dd className={css.overviewParentLinks}>
<button
type="button"
className={css.overviewHierarchyNavLink}
onClick={() => {
openRecordSummary(selectedRequestResult)
}}
>
<span>
{selectedRequestInfo?.purpose === 'compaction'
? 'Compacted'
: 'Assistant Message'}
</span>
<IconChevronRightOutline14
className={css.overviewHierarchyJumpIconTight}
size={11}
/>
</button>
</dd>
</div>
)}
</dl>
<div className={css.overviewSections}>
{selectedRequestOptions !== undefined && (
<OverviewSection label="Options" onOpen={() => { activateTab('options') }}>
<RequestOptions options={selectedRequestOptions} preview />
</OverviewSection>
)}
<OverviewSection label="Usage" onOpen={() => { activateTab('usage') }}>
<UsageRows usage={selectedRequestUsage} />
</OverviewSection>
<OverviewSection label="Timing" onOpen={() => { activateTab('timing') }}>
<RequestTiming
assistant={selectedRequestAssistant}
anchor={selectedRequestAnchor}
request={selectedRequestInfo}
/>
</OverviewSection>
</div>
</>
)}
{selectedRequest !== null && activeTab === 'options' && (
<RequestOptions options={selectedRequestOptions} />
)}
{selectedRequest !== null && activeTab === 'usage' && (
<RequestUsagePanel
usage={selectedRequestUsage}
cumulative={selectedRequestCumulativeUsage}
/>
)}
{selectedRequest !== null && activeTab === 'timing' && (
<RequestTiming
assistant={selectedRequestAssistant}
anchor={selectedRequestAnchor}
request={selectedRequestInfo}
/>
)}
{promptSelected
&& selectedPreviousPrompt !== undefined
&& activeTab === 'diff' && (
<SystemPromptDiff
before={selectedPreviousPrompt}
after={selectedPrompt}
/>
)}
{promptSelected && activeTab === 'system-prompt' && (
selectedPrompt.system === ''
? <p className={css.noPayload}>No system prompt in this request</p>
: (
<div className={`${css.markdownPayload} ${css.systemPrompt}`}>
<MarkdownText text={selectedPrompt.system} />
</div>
)
)}
{promptSelected && activeTab === 'tools' && (
<ToolCatalog tools={selectedPrompt.tools} />
)}
{!promptSelected
&& selected?.cell.kind === 'compacted'
&& selectedState !== undefined
&& activeTab === 'overview' && (
<>
<dl className={css.overview}>
<div>
<dt>Status</dt>
<dd>{statusLabel(selectedState)}</dd>
</div>
<div>
<dt>Duration</dt>
<dd>{formatElapsedSeconds(selected.cell.timeSeconds)}</dd>
</div>
<div>
<dt>Tokens</dt>
<dd></dd>
</div>
</dl>
{selected.cell.outputDetail !== undefined && (
<div className={css.compactedSummary}>
<MarkdownRecordContent
record={selected}
rendered
thinkingExpanded={thinkingExpanded}
onThinkingExpandedChange={setThinkingExpanded}
onOpenCall={openCallSummary}
/>
</div>
)}
</>
)}
{!promptSelected
&& selected !== undefined
&& selected.cell.kind !== 'compacted'
&& selectedState !== undefined
&& activeTab === 'overview' && (
<>
<dl className={css.overview}>
{selected.cell.messageSource !== undefined && (
<div>
<dt>Origin</dt>
<dd className={css.overviewParentLinks}>
<button
type="button"
className={css.overviewHierarchyNavLink}
onClick={() => { activateTab('origin') }}
>
<span>{messageOriginLabel(selected.cell.messageSource)}</span>
<IconChevronRightOutline14
className={css.overviewHierarchyJumpIconTight}
size={11}
/>
</button>
</dd>
</div>
)}
{hasSelectedHierarchy && (
<div>
<dt>
{selectedAssistantRequestTarget !== undefined
? 'Origin'
: 'Hierarchy'}
</dt>
<dd className={css.overviewParentLinks}>
{selectedAssistantRequestTarget !== undefined && (
<button
type="button"
className={css.overviewHierarchyNavLink}
onClick={() => {
selectRequest(selectedAssistantRequestTarget)
}}
>
<span>Request #{selectedAssistantRequestTarget.number}</span>
<IconChevronRightOutline14
className={css.overviewHierarchyJumpIconTight}
size={11}
/>
</button>
)}
{selectedParentMessage !== undefined && (
<button
type="button"
className={css.overviewHierarchyNavLink}
onClick={() => { openRecordSummary(selectedParentMessage) }}
>
<span>Assistant Message</span>
<IconChevronRightOutline14
className={css.overviewHierarchyJumpIconTight}
size={11}
/>
</button>
)}
{selectedParentTool !== undefined && (
<button
type="button"
className={css.overviewHierarchyNavLink}
onClick={() => { openRecordSummary(selectedParentTool) }}
>
<span>Tool Call</span>
<IconChevronRightOutline14
className={css.overviewHierarchyJumpIconTight}
size={11}
/>
</button>
)}
</dd>
</div>
)}
<div>
<dt>Status</dt>
<dd>{statusLabel(selectedState)}</dd>
</div>
{selected.cell.kind === 'message' && (
<TokenRows cell={selected.cell} />
)}
{(selected.cell.kind === 'user' || selected.cell.kind === 'context') && (
<div>
<dt>Duration</dt>
<dd>{formatElapsedSeconds(selected.cell.timeSeconds)}</dd>
</div>
)}
</dl>
<div className={css.overviewSections}>
{isMarkdownRecord(selected)
? (
<>
<OverviewSection label="Preview" onOpen={() => { activateTab('rendered') }}>
<MarkdownRecordContent
record={selected}
rendered
preview
thinkingExpanded={thinkingExpanded}
onThinkingExpandedChange={setThinkingExpanded}
onOpenCall={openCallSummary}
/>
</OverviewSection>
</>
)
: (
<>
{selected.cell.inputDetail && (
<OverviewSection label="Payload" onOpen={() => { activateTab('input') }}>
<RecordPayload record={selected} direction="input" preview />
</OverviewSection>
)}
{selected.cell.outputDetail && (
<OverviewSection label="Result" onOpen={() => { activateTab('output') }}>
<RecordPayload record={selected} direction="output" preview />
</OverviewSection>
)}
<OverviewSection label="Schema" onOpen={() => { activateTab('schema') }}>
<RecordSchema record={selected} preview />
</OverviewSection>
</>
)}
{selectedAssistantRequestTarget !== undefined && (
<OverviewSection
label="Timing"
onOpen={() => {
selectRequest(selectedAssistantRequestTarget, 'timing')
}}
>
<RecordTiming record={selected} />
</OverviewSection>
)}
{(selected.cell.kind === 'tool' || selected.cell.kind === 'subtool') && (
<OverviewSection label="Timing" onOpen={() => { activateTab('timing') }}>
<RecordTiming record={selected} />
</OverviewSection>
)}
</div>
</>
)}
{!promptSelected && selected !== undefined && activeTab === 'rendered' && (
<MarkdownRecordContent
record={selected}
rendered
thinkingExpanded={thinkingExpanded}
onThinkingExpandedChange={setThinkingExpanded}
onOpenCall={openCallSummary}
/>
)}
{!promptSelected && selected !== undefined && activeTab === 'source' && (
<MarkdownRecordContent
record={selected}
rendered={false}
thinkingExpanded={thinkingExpanded}
onThinkingExpandedChange={setThinkingExpanded}
onOpenCall={openCallSummary}
/>
)}
{!promptSelected && selected !== undefined && activeTab === 'origin' && (
<MessageOrigin record={selected} />
)}
{!promptSelected && selected !== undefined && activeTab === 'input' && (
<RecordPayload record={selected} direction="input" />
)}
{!promptSelected && selected !== undefined && activeTab === 'output' && (
<RecordPayload record={selected} direction="output" />
)}
{!promptSelected && selected !== undefined && activeTab === 'schema' && (
<RecordSchema record={selected} />
)}
{!promptSelected && selected !== undefined && activeTab === 'timing' && (
<RecordTiming record={selected} />
)}
</div>
</aside>
)}
</div>
)
}