mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'github/master' into xtr/trajectory-inspection-ui
# Conflicts: # packages/client/ui-primitives/src/Menu.tsx # packages/client/ui-trajectory/src/client/TrajectoryCell.tsx # packages/client/ui-trajectory/src/client/TrajectoryView.tsx # packages/client/ui-trajectory/tests/layout.spec.tsx # pnpm-lock.yaml # pnpm-workspace.yaml
This commit is contained in:
@@ -19,18 +19,20 @@ const KIND_LABEL: Record<TrajectoryCellKind, string> = {
|
||||
system: 'System',
|
||||
user: 'User',
|
||||
context: 'Context',
|
||||
compacted: 'Compacted',
|
||||
message: 'Message',
|
||||
tool: 'Tool',
|
||||
subtool: 'Sub',
|
||||
}
|
||||
|
||||
const TAG_CLASS: Record<TrajectoryCellKind, string> = {
|
||||
system: css.tagSystem!,
|
||||
user: css.tagUser!,
|
||||
context: css.tagContext!,
|
||||
message: css.tagMessage!,
|
||||
tool: css.tagTool!,
|
||||
subtool: css.tagSubtool!,
|
||||
const TAG_CLASS: Record<TrajectoryCellKind, string | undefined> = {
|
||||
system: css.tagSystem,
|
||||
user: css.tagUser,
|
||||
context: css.tagContext,
|
||||
compacted: css.tagSystem,
|
||||
message: css.tagMessage,
|
||||
tool: css.tagTool,
|
||||
subtool: css.tagSubtool,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +75,7 @@ export function TrajectoryCell({
|
||||
<div className={rootClass} data-kind={kind} data-selected={selected || undefined} {...rest}>
|
||||
<span className={css.index}>#{index}</span>
|
||||
<span className={css.tagSlot}>
|
||||
<span className={`${css.tag} ${TAG_CLASS[kind]}`}>{KIND_LABEL[kind]}</span>
|
||||
<span className={[css.tag, TAG_CLASS[kind]].filter((c): c is string => c !== undefined).join(' ')}>{KIND_LABEL[kind]}</span>
|
||||
</span>
|
||||
<span className={css.text}>{text}</span>
|
||||
<span className={css.trailing}>
|
||||
|
||||
@@ -14,7 +14,7 @@ import css from './TrajectoryStatsHeader.module.css'
|
||||
export interface TrajectoryStatsHeaderProps { useSession: SnapshotSelectorHook<ConversationSnapshot> }
|
||||
|
||||
export const TrajectoryStatsHeader = memo(function TrajectoryStatsHeader({ useSession }: TrajectoryStatsHeaderProps) {
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const stats = useMemo(() => deriveSpanStats(deriveSpans(nodes)), [nodes])
|
||||
if (stats.turns === 0) return null
|
||||
return <div className={css.root}>{`${stats.turns} turns · ${stats.steps} steps · ${stats.calls} tool calls`}</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.eventColumn {
|
||||
width: 104px;
|
||||
width: 122px;
|
||||
}
|
||||
|
||||
.contentColumn {
|
||||
@@ -111,7 +111,7 @@
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
top: -8px;
|
||||
left: 2px;
|
||||
left: 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
@@ -242,7 +242,7 @@
|
||||
.event {
|
||||
overflow: visible !important;
|
||||
padding-right: 4px !important;
|
||||
padding-left: 18px !important;
|
||||
padding-left: 36px !important;
|
||||
}
|
||||
|
||||
.turnLabel {
|
||||
@@ -835,16 +835,6 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.tokenEquation {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.tokenOperator {
|
||||
margin: 0 2px;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
.overviewSections {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
@@ -220,11 +220,11 @@ export interface TrajectoryTableProps {
|
||||
/** 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
|
||||
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
|
||||
onToggleAssistant: (index: number) => void
|
||||
}
|
||||
|
||||
/** One request identity paired with its session-global number. */
|
||||
@@ -442,20 +442,29 @@ function statusLabel(state: RecordState): string {
|
||||
return 'Completed'
|
||||
}
|
||||
|
||||
function tokenSummary(cell: TrajectoryCellProps): ReactNode {
|
||||
if (cell.kind !== 'message') return '—'
|
||||
if (cell.output === undefined) return '—'
|
||||
if (cell.think === undefined) return String(cell.output)
|
||||
function TokenRows({ cell }: { cell: TrajectoryCellProps }) {
|
||||
const content = cell.output !== undefined && cell.think !== undefined
|
||||
? Math.max(0, cell.output - cell.think)
|
||||
: undefined
|
||||
return (
|
||||
<span className={css.tokenEquation}>
|
||||
<span title="Total output tokens">{cell.output}</span>
|
||||
<span className={css.tokenOperator}>=</span>
|
||||
<span title="Non-reasoning output tokens">
|
||||
{Math.max(0, cell.output - cell.think)}
|
||||
</span>
|
||||
<span className={css.tokenOperator}>+</span>
|
||||
<span title="Reasoning tokens">{cell.think}</span>
|
||||
</span>
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -551,7 +560,7 @@ function RequestOptions({
|
||||
<JsonTree
|
||||
data={options}
|
||||
label="Request options JSON"
|
||||
className={preview ? css.jsonPreview! : css.jsonPayload!}
|
||||
className={preview ? css.jsonPreview : css.jsonPayload}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -560,16 +569,17 @@ function messageOriginLabel(source: unknown): string {
|
||||
if (typeof source !== 'object' || source === null || Array.isArray(source)) {
|
||||
return 'Unknown'
|
||||
}
|
||||
const kind = Reflect.get(source, 'kind')
|
||||
const properties = source as Record<string, unknown>
|
||||
const kind = properties.kind
|
||||
if (kind === 'user') return 'User'
|
||||
if (kind === 'plugin') {
|
||||
const plugin = Reflect.get(source, 'plugin')
|
||||
const plugin = properties.plugin
|
||||
return typeof plugin === 'string' && plugin !== ''
|
||||
? `Plugin · ${plugin}`
|
||||
: 'Plugin'
|
||||
}
|
||||
if (kind === 'goal') {
|
||||
const round = Reflect.get(source, 'round')
|
||||
const round = properties.round
|
||||
return typeof round === 'number' && round > 0
|
||||
? `Goal · Round ${round}`
|
||||
: 'Goal'
|
||||
@@ -591,7 +601,7 @@ function MessageOrigin({ record }: { record: TableRecord }) {
|
||||
<JsonTree
|
||||
data={data}
|
||||
label="Message origin JSON"
|
||||
className={css.jsonPayload!}
|
||||
className={css.jsonPayload}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -736,7 +746,7 @@ function SourceBlocks({
|
||||
onOpenCall,
|
||||
}: {
|
||||
blocks: readonly TrajectorySourceBlock[]
|
||||
onOpenCall(callId: string): void
|
||||
onOpenCall: (callId: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className={css.sourceBlocks}>
|
||||
@@ -744,28 +754,28 @@ function SourceBlocks({
|
||||
<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>
|
||||
)
|
||||
<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>
|
||||
)}
|
||||
<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>}
|
||||
@@ -823,7 +833,7 @@ function AssistantToolCalls({
|
||||
}: {
|
||||
blocks: readonly TrajectorySourceBlock[] | undefined
|
||||
preview: boolean
|
||||
onOpenCall(callId: string): void
|
||||
onOpenCall: (callId: string) => void
|
||||
}) {
|
||||
const calls = blocks?.filter(block => block.type === 'tool-call') ?? []
|
||||
if (calls.length === 0) return null
|
||||
@@ -1031,8 +1041,8 @@ function MarkdownRecordContent({
|
||||
rendered: boolean
|
||||
preview?: boolean
|
||||
thinkingExpanded: boolean
|
||||
onThinkingExpandedChange(expanded: boolean): void
|
||||
onOpenCall(callId: string): void
|
||||
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} />
|
||||
@@ -1046,10 +1056,7 @@ function MarkdownRecordContent({
|
||||
return <MarkdownFragment text={source} rendered={false} preview={preview} />
|
||||
}
|
||||
return (
|
||||
<div className={rendered
|
||||
? `${css.assistantContent} ${css.assistantContentRendered}`
|
||||
: css.assistantContent}
|
||||
>
|
||||
<div className={`${css.assistantContent} ${css.assistantContentRendered}`}>
|
||||
<div className={
|
||||
preview && !record.cell.outputDetail
|
||||
? `${css.thinkingQuote} ${css.thinkingQuoteOnlyPreview}`
|
||||
@@ -1125,12 +1132,12 @@ 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>
|
||||
)
|
||||
<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({
|
||||
@@ -1216,7 +1223,7 @@ function RecordPayload({
|
||||
<JsonTree
|
||||
data={json}
|
||||
label={`${direction === 'input' ? 'Payload' : 'Result'} JSON`}
|
||||
className={preview ? css.jsonPreview! : css.jsonPayload!}
|
||||
className={preview ? css.jsonPreview : css.jsonPayload}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1312,7 +1319,7 @@ function OverviewSection({
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
onOpen(): void
|
||||
onOpen: () => void
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
@@ -1369,9 +1376,9 @@ export function TrajectoryTable({
|
||||
const selectedRequestRecords = selectedRequest === null
|
||||
? []
|
||||
: allRecords.filter(record =>
|
||||
record.turn === selectedRequest.turn
|
||||
record.turn === selectedRequest.turn
|
||||
&& record.group === selectedRequest.group,
|
||||
)
|
||||
)
|
||||
const selectedRequestAssistant = selectedRequestRecords.find(
|
||||
record => record.cell.kind === 'message',
|
||||
)
|
||||
@@ -1401,22 +1408,22 @@ export function TrajectoryTable({
|
||||
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 }),
|
||||
}
|
||||
...(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
|
||||
@@ -1428,16 +1435,18 @@ export function TrajectoryTable({
|
||||
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,
|
||||
}
|
||||
turn: selected.turn,
|
||||
number: selectedAssistantRequest,
|
||||
group: selected.group,
|
||||
}
|
||||
: undefined
|
||||
const hasSelectedHierarchy = selectedAssistantRequestTarget !== undefined
|
||||
|| selectedParents.message !== undefined
|
||||
@@ -1445,8 +1454,8 @@ export function TrajectoryTable({
|
||||
const splitStyle: TrajectorySplitStyle | undefined = toolRequestOffset === null
|
||||
? undefined
|
||||
: {
|
||||
'--trajectory-tool-request-width': `calc(58cqw - ${toolRequestOffset}px)`,
|
||||
}
|
||||
'--trajectory-tool-request-width': `calc(58cqw - ${toolRequestOffset}px)`,
|
||||
}
|
||||
|
||||
const activateTab = (tab: DetailTab) => {
|
||||
tabHistory.current.delete(tab)
|
||||
@@ -1554,11 +1563,11 @@ export function TrajectoryTable({
|
||||
onClick={isRequestOnly
|
||||
? undefined
|
||||
: isCollapsedSummary
|
||||
? () => {
|
||||
? () => {
|
||||
if (record.collapsedSummaryKind === 'turn') onToggleTurn(record.turn)
|
||||
else onToggleAssistant(record.cell.index)
|
||||
}
|
||||
: () => { selectRecord(record.cell.index) }}
|
||||
: () => { selectRecord(record.cell.index) }}
|
||||
onDoubleClick={(event) => {
|
||||
if (isCollapsedSummary || isRequestOnly) return
|
||||
if (collapsedTurns.has(record.turn)) {
|
||||
@@ -1594,94 +1603,94 @@ export function TrajectoryTable({
|
||||
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
|
||||
<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}
|
||||
className={activeTurn === record.turn
|
||||
? `${css.turnLabel} ${css.turnLabelActive}`
|
||||
: css.turnLabel}
|
||||
>
|
||||
<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>
|
||||
Turn {record.turn}
|
||||
</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>
|
||||
)
|
||||
: (
|
||||
<div className={css.eventInner}>
|
||||
{!isCollapsedSummary && !isRequestOnly && (
|
||||
<span
|
||||
className={record.cell.result === undefined ? css.contentText : css.resultPreview}
|
||||
title={record.cell.result === undefined
|
||||
? listDisplayText
|
||||
: `${listDisplayText} → ${record.cell.result}`}
|
||||
className={css.kindSlot}
|
||||
>
|
||||
<span className={record.cell.result === undefined ? undefined : css.resultRequest}>
|
||||
{isToolCallOnly(record.cell)
|
||||
? null
|
||||
: toolCallText === undefined
|
||||
? listDisplayText || '—'
|
||||
: (
|
||||
<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 || '—'}
|
||||
@@ -1693,21 +1702,21 @@ export function TrajectoryTable({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
@@ -1738,7 +1747,7 @@ export function TrajectoryTable({
|
||||
if (event.button !== 0) return
|
||||
const details = event.currentTarget.parentElement
|
||||
const split = details?.parentElement
|
||||
if (details === null || details === undefined || split === null || split === undefined) return
|
||||
if (details === null || split === null) return
|
||||
const splitWidth = split.getBoundingClientRect().width
|
||||
detailsResizeDrag.current = {
|
||||
pointerId: event.pointerId,
|
||||
@@ -1777,7 +1786,7 @@ export function TrajectoryTable({
|
||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
||||
const details = event.currentTarget.parentElement
|
||||
const split = details?.parentElement
|
||||
if (details === null || details === undefined || split === null || split === undefined) return
|
||||
if (details === null || split === null) return
|
||||
const direction = event.key === 'ArrowLeft' ? 1 : -1
|
||||
const currentDetailsWidth = details.getBoundingClientRect().width
|
||||
const splitWidth = split.getBoundingClientRect().width
|
||||
@@ -1800,40 +1809,40 @@ export function TrajectoryTable({
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
<>
|
||||
<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 && (
|
||||
: 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]
|
||||
}`}
|
||||
: 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>
|
||||
@@ -2019,12 +2028,12 @@ export function TrajectoryTable({
|
||||
)}
|
||||
{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>
|
||||
)
|
||||
? <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} />
|
||||
@@ -2045,7 +2054,7 @@ export function TrajectoryTable({
|
||||
</div>
|
||||
<div>
|
||||
<dt>Tokens</dt>
|
||||
<dd>{tokenSummary(selected.cell)}</dd>
|
||||
<dd>—</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{selected.cell.outputDetail !== undefined && (
|
||||
@@ -2109,11 +2118,11 @@ export function TrajectoryTable({
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{selectedParents.message !== undefined && (
|
||||
{selectedParentMessage !== undefined && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.overviewHierarchyNavLink}
|
||||
onClick={() => { openRecordSummary(selectedParents.message!) }}
|
||||
onClick={() => { openRecordSummary(selectedParentMessage) }}
|
||||
>
|
||||
<span>Assistant Message</span>
|
||||
<IconChevronRightOutline14
|
||||
@@ -2122,11 +2131,11 @@ export function TrajectoryTable({
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{selectedParents.tool !== undefined && (
|
||||
{selectedParentTool !== undefined && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.overviewHierarchyNavLink}
|
||||
onClick={() => { openRecordSummary(selectedParents.tool!) }}
|
||||
onClick={() => { openRecordSummary(selectedParentTool) }}
|
||||
>
|
||||
<span>Tool Call</span>
|
||||
<IconChevronRightOutline14
|
||||
@@ -2143,7 +2152,7 @@ export function TrajectoryTable({
|
||||
<dd>{statusLabel(selectedState)}</dd>
|
||||
</div>
|
||||
{selected.cell.kind === 'message' && (
|
||||
<div><dt>Tokens</dt><dd>{tokenSummary(selected.cell)}</dd></div>
|
||||
<TokenRows cell={selected.cell} />
|
||||
)}
|
||||
{(selected.cell.kind === 'user' || selected.cell.kind === 'context') && (
|
||||
<div>
|
||||
@@ -2155,36 +2164,36 @@ export function TrajectoryTable({
|
||||
<div className={css.overviewSections}>
|
||||
{isMarkdownRecord(selected)
|
||||
? (
|
||||
<>
|
||||
<OverviewSection label="Preview" onOpen={() => { activateTab('rendered') }}>
|
||||
<MarkdownRecordContent
|
||||
record={selected}
|
||||
rendered
|
||||
preview
|
||||
thinkingExpanded={thinkingExpanded}
|
||||
onThinkingExpandedChange={setThinkingExpanded}
|
||||
onOpenCall={openCallSummary}
|
||||
/>
|
||||
</OverviewSection>
|
||||
</>
|
||||
)
|
||||
<>
|
||||
<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 />
|
||||
<>
|
||||
{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"
|
||||
|
||||
@@ -8,13 +8,13 @@ export interface TrajectoryToolbarProps {
|
||||
/** Whether every collapsible turn is currently folded. */
|
||||
allTurnsCollapsed: boolean
|
||||
/** Fold or expand every collapsible turn. */
|
||||
onToggleAllTurns(): void
|
||||
onToggleAllTurns: () => void
|
||||
/** Number of assistant messages followed by tool calls. */
|
||||
collapsibleAssistants: number
|
||||
/** Whether every collapsible assistant's tool calls are currently folded. */
|
||||
allAssistantsCollapsed: boolean
|
||||
/** Fold or expand tool calls under every collapsible assistant. */
|
||||
onToggleAllAssistants(): void
|
||||
onToggleAllAssistants: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ export function TrajectoryTurnHeader({ turn }: TrajectoryTurnHeaderProps) {
|
||||
<div className={css.inner}>
|
||||
<span className={css.title}>Turn {turn}</span>
|
||||
<div className={css.columns} aria-hidden="true">
|
||||
{COLUMN_LABELS.map((label) => (
|
||||
{COLUMN_LABELS.map(label => (
|
||||
<span key={label} className={css.column}>{label}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -71,21 +71,21 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
const [collapsedTurns, setCollapsedTurns] = useState<ReadonlySet<number>>(EMPTY_IDS)
|
||||
const [collapsedAssistants, setCollapsedAssistants] =
|
||||
useState<ReadonlySet<number>>(EMPTY_IDS)
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const projectedContexts = useSession((s) => s.contexts)
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const projectedContexts = useSession(s => s.contexts)
|
||||
const compactionRequests = useSession(
|
||||
(s) => s.compactionRequests ?? EMPTY_COMPACTION_REQUESTS,
|
||||
s => s.compactionRequests ?? EMPTY_COMPACTION_REQUESTS,
|
||||
)
|
||||
const requestAttempts = useSession(
|
||||
(s) => s.requestAttempts ?? EMPTY_MODEL_REQUESTS,
|
||||
s => s.requestAttempts ?? EMPTY_MODEL_REQUESTS,
|
||||
)
|
||||
const promptChanges = useSession(
|
||||
(s) => s.promptChanges ?? EMPTY_PROMPT_CHANGES,
|
||||
s => s.promptChanges ?? EMPTY_PROMPT_CHANGES,
|
||||
)
|
||||
const partial = useSession((s) => s.partial)
|
||||
const runningCalls = useSession((s) => s.runningCalls)
|
||||
const callSchemas = useSession((s) => s.callSchemas)
|
||||
const codeDispatches = useSession((s) => s.codeDispatches)
|
||||
const partial = useSession(s => s.partial)
|
||||
const runningCalls = useSession(s => s.runningCalls)
|
||||
const callSchemas = useSession(s => s.callSchemas)
|
||||
const codeDispatches = useSession(s => s.codeDispatches)
|
||||
const contexts = useMemo<readonly ConversationContext[]>(
|
||||
() => projectedContexts === undefined || projectedContexts.length === 0
|
||||
? [{ id: 0, nodes }]
|
||||
@@ -98,7 +98,11 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
)
|
||||
const currentBranch = branches.at(-1)
|
||||
if (currentBranch === undefined) throw new Error('trajectory branch projection must not be empty')
|
||||
const selectedNodes = currentBranch.nodes
|
||||
const selectedNodes = useMemo(() => {
|
||||
const bySeq = new Map(currentBranch.nodes.map(node => [node.seq, node]))
|
||||
for (const node of nodes) bySeq.set(node.seq, node)
|
||||
return [...bySeq.values()].sort((left, right) => left.seq - right.seq)
|
||||
}, [currentBranch, nodes])
|
||||
const globalRequestNumbers = useMemo<readonly TrajectoryRequestNumber[]>(() => {
|
||||
const assistantsByStep = new Map<string, AssistantMessageNode>()
|
||||
for (const context of contexts) {
|
||||
@@ -128,11 +132,11 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
attemptsByStep.has(key)
|
||||
? []
|
||||
: [{
|
||||
seq: node.seq,
|
||||
kind: 'ordinary' as const,
|
||||
request: undefined,
|
||||
node,
|
||||
}],
|
||||
seq: node.seq,
|
||||
kind: 'ordinary' as const,
|
||||
request: undefined,
|
||||
node,
|
||||
}],
|
||||
),
|
||||
...compactionRequests.map(request => ({
|
||||
seq: request.startSeq,
|
||||
|
||||
@@ -24,8 +24,8 @@ export interface WaterfallExtraProps {
|
||||
|
||||
export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & WaterfallExtraProps) {
|
||||
const scale = pxPerNode ?? PX_PER_NODE
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const codeDispatches = useSession((s) => s.codeDispatches)
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const codeDispatches = useSession(s => s.codeDispatches)
|
||||
const spans = useMemo(() => deriveSpans(nodes), [nodes])
|
||||
const subSpans = useMemo(() => deriveSubSpans(nodes, codeDispatches), [nodes, codeDispatches])
|
||||
if (spans.length === 0) return <div className={css.root}><p className={css.empty}>No timing data</p></div>
|
||||
@@ -50,7 +50,7 @@ export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & Waterfa
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(subSpans.get(span.turn) ?? []).map((lane) => (
|
||||
{(subSpans.get(span.turn) ?? []).map(lane => (
|
||||
<div key={lane.callId} className={css.subRow} data-subspan style={{ paddingLeft: i * 12 + 24 }}>
|
||||
<span className={css.subTag}>{lane.name}</span>
|
||||
<span
|
||||
|
||||
@@ -219,7 +219,9 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
}
|
||||
if (entry.kind === 'system') {
|
||||
const { change } = entry
|
||||
const turn = enclosingPromptTurn(nodes, change.seq, partial)
|
||||
const turn = change.kind === 'initial'
|
||||
? firstVisibleTurn(nodes, partial)
|
||||
: enclosingPromptTurn(nodes, change.seq, partial)
|
||||
pushMessage(turn, {
|
||||
absTime: finiteTime(change.time),
|
||||
cell: {
|
||||
@@ -716,6 +718,20 @@ function enclosingPromptTurn(
|
||||
return partial?.turn ?? 1
|
||||
}
|
||||
|
||||
/** Earliest raw turn represented by the selected trajectory branch. */
|
||||
function firstVisibleTurn(
|
||||
nodes: ConversationSnapshot['nodes'],
|
||||
partial: ConversationSnapshot['partial'],
|
||||
): number {
|
||||
const turns = nodes.flatMap(node =>
|
||||
(node.kind === 'assistant' || node.kind === 'steering') && node.turn > 0
|
||||
? [node.turn]
|
||||
: [],
|
||||
)
|
||||
if (partial !== null && partial.turn > 0) turns.push(partial.turn)
|
||||
return turns.length === 0 ? 1 : Math.min(...turns)
|
||||
}
|
||||
|
||||
/** Copy provider usage onto a Message cell when present. */
|
||||
function attachUsage(cell: TrajectoryCellProps, usage: UsageLike | undefined): void {
|
||||
if (usage === undefined) return
|
||||
|
||||
Reference in New Issue
Block a user