feat(ui): expand request trajectory inspection
This commit is contained in:
@@ -28,7 +28,8 @@ export type {
|
||||
EngineStoreHandle, EngineStoreInstance, ObservableSnapshot, SnapshotStore,
|
||||
} from './contract/store.ts'
|
||||
export type {
|
||||
AssistantBlock, AssistantMessageNode, AssistantTiming, CodeSubCall, ComposerPhase, ContextMessageNode,
|
||||
AssistantBlock, AssistantMessageNode, AssistantProvenanceView, AssistantRequestConfig,
|
||||
AssistantTiming, CodeSubCall, ComposerPhase, ContextMessageNode,
|
||||
ConversationContext, ConversationContextOriginKind, ConversationNode, ConversationPromptSnapshot,
|
||||
ConversationSnapshot, QueuedMessage, RunningToolCall,
|
||||
SteeringMessageNode, ToolResultNode, UnknownSurfaceNode, UserMessageNode,
|
||||
|
||||
@@ -60,6 +60,23 @@ export interface AssistantTiming {
|
||||
completedTime: number
|
||||
}
|
||||
|
||||
/** Request configuration recorded in the effective header for one assistant response. */
|
||||
export interface AssistantRequestConfig {
|
||||
provider: string
|
||||
model: string
|
||||
thinking?: string
|
||||
reasoningEffort?: string
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
stop?: readonly string[]
|
||||
}
|
||||
|
||||
/** Stable provider/model identity attached to one assistant response. */
|
||||
export interface AssistantProvenanceView {
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
|
||||
/** A finalized (or interruption-frozen) assistant message. */
|
||||
export interface AssistantMessageNode {
|
||||
kind: 'assistant'
|
||||
@@ -70,6 +87,8 @@ export interface AssistantMessageNode {
|
||||
step: number
|
||||
blocks: readonly AssistantBlock[]
|
||||
usage?: unknown
|
||||
provenance?: AssistantProvenanceView
|
||||
requestConfig?: AssistantRequestConfig
|
||||
/** Timing derived from the recorded step/chunk/message event sequence. */
|
||||
timing?: AssistantTiming
|
||||
/** Frozen partial of an aborted turn (no finalize ever arrives): rendered with a 已停止 marker.
|
||||
@@ -211,6 +230,8 @@ export type ConversationContextOriginKind = 'compaction' | 'rewind' | 'rewrite'
|
||||
|
||||
/** Latest complete model request header in force within one context generation. */
|
||||
export interface ConversationPromptSnapshot {
|
||||
/** Provider/model and sampling configuration from the latest effective request header. */
|
||||
config?: AssistantRequestConfig
|
||||
/** Rendered system prompt text; empty when the request had no system prompt. */
|
||||
system: string
|
||||
/** Complete tool catalog sent with the request, including tools that were never called. */
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@deepseek-ai/dsh-session/surface'
|
||||
import type { ToolCallView, ToolEventView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {
|
||||
AssistantTiming, ConversationContext, ConversationContextOriginKind, ConversationNode,
|
||||
AssistantRequestConfig, AssistantTiming, ConversationContext, ConversationContextOriginKind, ConversationNode,
|
||||
ConversationPromptSnapshot,
|
||||
} from './conversation.ts'
|
||||
import { toAssistantBlocks } from './conversation.ts'
|
||||
@@ -43,6 +43,7 @@ function materializeNode(
|
||||
callIndex: ReadonlyMap<string, CallIndexEntry>,
|
||||
resultView: ToolResultView | null,
|
||||
assistantTiming?: AssistantTiming,
|
||||
requestConfig?: AssistantRequestConfig,
|
||||
): ConversationNode {
|
||||
switch (event.type) {
|
||||
case 'user/message':
|
||||
@@ -64,6 +65,11 @@ function materializeNode(
|
||||
kind: 'assistant', seq: event.seq, time: event.time,
|
||||
turn: event.data.turn, step: event.data.step,
|
||||
blocks: toAssistantBlocks(event.data.content), usage: event.data.usage,
|
||||
provenance: {
|
||||
provider: event.data.provenance.provider,
|
||||
model: event.data.provenance.model,
|
||||
},
|
||||
...(requestConfig === undefined ? {} : { requestConfig }),
|
||||
...(assistantTiming !== undefined ? { timing: assistantTiming } : {}),
|
||||
}
|
||||
case 'steering/message':
|
||||
@@ -204,6 +210,7 @@ export class FoldAdapter {
|
||||
this.callIdx,
|
||||
this.resultViews.get(seq) ?? null,
|
||||
event.type === 'assistant/message' ? this.assistantTiming(event) : undefined,
|
||||
event.type === 'assistant/message' ? this.assistantRequestConfig(event) : undefined,
|
||||
)
|
||||
this.nodeCache.set(seq, node)
|
||||
out.push(node)
|
||||
@@ -280,6 +287,7 @@ export class FoldAdapter {
|
||||
this.callIdx,
|
||||
this.resultViews.get(seq) ?? null,
|
||||
event.type === 'assistant/message' ? this.assistantTiming(event) : undefined,
|
||||
event.type === 'assistant/message' ? this.assistantRequestConfig(event) : undefined,
|
||||
)
|
||||
this.nodeCache.set(seq, node)
|
||||
return node
|
||||
@@ -312,6 +320,17 @@ export class FoldAdapter {
|
||||
return { stepStartTime, firstTokenTime, completedTime: event.time }
|
||||
}
|
||||
|
||||
private assistantRequestConfig(
|
||||
event: SessionEvent<'assistant/message'>,
|
||||
): AssistantRequestConfig | undefined {
|
||||
for (let i = event.seq; i >= this.baseSeq; i--) {
|
||||
const candidate = this.padded[i]
|
||||
if (candidate?.type !== 'request/header') continue
|
||||
return candidate.data.header.config
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private indexCall(event: SessionEvent, view?: ToolEventView): void {
|
||||
if (event.type === 'tool/result') {
|
||||
if (view?.for === 'result') this.resultViews.set(event.seq, view.view)
|
||||
@@ -336,6 +355,7 @@ export class FoldAdapter {
|
||||
}
|
||||
if (event.type !== 'request/header') return
|
||||
this.activePrompt = {
|
||||
config: event.data.header.config,
|
||||
system: event.data.header.system ?? '',
|
||||
tools: event.data.header.tools ?? [],
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import css from './TrajectoryGroupHeader.module.css'
|
||||
export interface TrajectoryGroupHeaderProps {
|
||||
/** Group title (`Message`, `Step 1`, …). */
|
||||
title: string
|
||||
/** Secondary summary (`49s`, `2.2s skill`, …). */
|
||||
/** Secondary summary (`49 s`, `2.2 s skill`, …). */
|
||||
description?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -699,6 +699,26 @@
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.usagePanel {
|
||||
padding: 4px 0 10px;
|
||||
}
|
||||
|
||||
.usageGroup + .usageGroup {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.usageHeading {
|
||||
margin: 0;
|
||||
padding: 4px 14px 1px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xs-strong-13);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.usageGroup .overview {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.overview dt {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import type { CSSProperties, ReactNode } from 'react'
|
||||
import {
|
||||
extractMarkdownPlainText, IconChevronRightOutline14, JsonTree, MarkdownText,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ConversationPromptSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
AssistantRequestConfig, ConversationPromptSnapshot,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
AssistantMetricDetail, TrajectoryCellKind, TrajectoryCellProps, TrajectorySourceBlock,
|
||||
} from './trajectory-record.ts'
|
||||
@@ -41,6 +43,8 @@ type DetailTab =
|
||||
| 'input'
|
||||
| 'output'
|
||||
| 'schema'
|
||||
| 'options'
|
||||
| 'usage'
|
||||
| 'timing'
|
||||
type RecordState = 'complete' | 'running' | 'error'
|
||||
|
||||
@@ -89,6 +93,8 @@ const SYSTEM_PROMPT_TABS: readonly DetailTabItem[] = [
|
||||
]
|
||||
const REQUEST_TABS: readonly DetailTabItem[] = [
|
||||
{ id: 'overview', label: 'Summary' },
|
||||
{ id: 'options', label: 'Options' },
|
||||
{ id: 'usage', label: 'Usage' },
|
||||
{ id: 'timing', label: 'Timing' },
|
||||
]
|
||||
|
||||
@@ -172,14 +178,6 @@ function AssistantTimingPanel({ metrics }: { metrics: AssistantMetricDetail }) {
|
||||
<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>
|
||||
<div>
|
||||
<dt>Output tokens</dt>
|
||||
<dd>
|
||||
{!metrics.usageProvided
|
||||
? 'Usage unavailable'
|
||||
: metrics.outputTokens ?? 'Output tokens unavailable'}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -188,6 +186,8 @@ function AssistantTimingPanel({ metrics }: { metrics: AssistantMetricDetail }) {
|
||||
export interface TrajectoryTableProps {
|
||||
/** Latest model request header in force for the selected context. */
|
||||
prompt?: ConversationPromptSnapshot
|
||||
/** Session-global request numbers for the request groups visible in this context. */
|
||||
requestNumbers?: readonly TrajectoryRequestNumber[]
|
||||
/** Grouped records in display order. */
|
||||
turns: readonly TrajectoryTurnModel[]
|
||||
/** Turn ids whose rows after the first are folded into a summary. */
|
||||
@@ -200,6 +200,27 @@ export interface TrajectoryTableProps {
|
||||
onToggleAssistant(index: number): void
|
||||
}
|
||||
|
||||
/** One context-local request identity paired with its session-global number. */
|
||||
export interface TrajectoryRequestNumber {
|
||||
turn: number
|
||||
step: number
|
||||
number: 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
|
||||
@@ -223,12 +244,35 @@ function flattenRecords(turns: readonly TrajectoryTurnModel[]): TableRecord[] {
|
||||
})
|
||||
}
|
||||
|
||||
function requestNumber(group: string): number | undefined {
|
||||
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, `Step ${request.step}`), 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 userText = records
|
||||
.filter(record => record.cell.kind === 'user')
|
||||
@@ -381,6 +425,94 @@ function tokenSummary(cell: TrajectoryCellProps): ReactNode {
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
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>
|
||||
)}
|
||||
</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 isMarkdownRecord(record: TableRecord): boolean {
|
||||
return record.cell.kind === 'user'
|
||||
|| record.cell.kind === 'context'
|
||||
@@ -435,7 +567,6 @@ function detailTabs(record: TableRecord): readonly DetailTabItem[] {
|
||||
{ id: 'overview', label: 'Summary' },
|
||||
{ id: 'rendered', label: 'Preview' },
|
||||
{ id: 'source', label: 'Source' },
|
||||
{ id: 'timing', label: 'Timing' },
|
||||
]
|
||||
}
|
||||
return [
|
||||
@@ -1011,6 +1142,7 @@ function OverviewSection({
|
||||
*/
|
||||
export function TrajectoryTable({
|
||||
prompt,
|
||||
requestNumbers: sessionRequestNumbers,
|
||||
turns,
|
||||
collapsedTurns,
|
||||
onToggleTurn,
|
||||
@@ -1026,6 +1158,7 @@ export function TrajectoryTable({
|
||||
const detailsResizeDrag = useRef<DetailsResizeDrag | null>(null)
|
||||
const tabHistory = useRef<Set<DetailTab>>(new Set(['overview']))
|
||||
const allRecords = flattenRecords(turns)
|
||||
const requestNumbers = indexRequestNumbers(allRecords, sessionRequestNumbers)
|
||||
const turnRecords = collapseTurnRecords(allRecords, collapsedTurns)
|
||||
const records = collapseAssistantRecords(turnRecords, collapsedAssistants)
|
||||
const systemPromptPreview = prompt === undefined
|
||||
@@ -1060,26 +1193,55 @@ export function TrajectoryTable({
|
||||
const selectedRequestSubtoolCalls = selectedRequestRecords.filter(
|
||||
record => record.cell.kind === 'subtool',
|
||||
).length
|
||||
const selectedRequestInputTotal = selectedRequestAssistant !== undefined
|
||||
&& (
|
||||
selectedRequestAssistant.cell.input !== undefined
|
||||
|| selectedRequestAssistant.cell.cacheRead !== undefined
|
||||
|| selectedRequestAssistant.cell.cacheWrite !== undefined
|
||||
)
|
||||
? (selectedRequestAssistant.cell.input ?? 0)
|
||||
+ (selectedRequestAssistant.cell.cacheRead ?? 0)
|
||||
+ (selectedRequestAssistant.cell.cacheWrite ?? 0)
|
||||
: undefined
|
||||
const selectedRequestInfo = selectedRequest === null
|
||||
? undefined
|
||||
: sessionRequestNumbers?.find(request => request.number === selectedRequest.number)
|
||||
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
|
||||
? REQUEST_TABS.filter(tab => tab.id !== 'options' || selectedRequestOptions !== undefined)
|
||||
: promptSelected
|
||||
? SYSTEM_PROMPT_TABS
|
||||
: selected === undefined ? [] : detailTabs(selected)
|
||||
const selectedParents: ParentRecords = selected === undefined
|
||||
? {}
|
||||
: parentRecords(allRecords, selected)
|
||||
const hasSelectedParents = selectedParents.message !== undefined
|
||||
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
|
||||
@@ -1109,10 +1271,13 @@ export function TrajectoryTable({
|
||||
activateTab('system-prompt')
|
||||
}
|
||||
|
||||
const selectRequest = (request: SelectedRequest) => {
|
||||
const selectRequest = (
|
||||
request: SelectedRequest,
|
||||
tab: 'overview' | 'timing' = 'overview',
|
||||
) => {
|
||||
setSelectedIndex(null)
|
||||
setSelectedRequest(request)
|
||||
activateTab('overview')
|
||||
activateTab(tab)
|
||||
}
|
||||
|
||||
const openRecordSummary = (target: TableRecord) => {
|
||||
@@ -1186,7 +1351,7 @@ export function TrajectoryTable({
|
||||
const request = record.groupStart
|
||||
&& !isCollapsedSummary
|
||||
&& !collapsedTurns.has(record.turn)
|
||||
? requestNumber(record.group)
|
||||
? requestNumbers.get(requestKey(record.turn, record.group))
|
||||
: undefined
|
||||
return (
|
||||
<tr
|
||||
@@ -1516,48 +1681,24 @@ export function TrajectoryTable({
|
||||
<dt>Status</dt>
|
||||
<dd>{statusLabel(selectedRequestState)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Started</dt>
|
||||
<StartedAtValue
|
||||
timestamp={selectedRequestAnchor?.cell.startedAt ?? null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Duration</dt>
|
||||
<dd>
|
||||
{formatElapsedSeconds(
|
||||
selectedRequestAssistant?.cell.timeSeconds ?? null,
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
{selectedRequestInputTotal !== undefined && (
|
||||
{(selectedRequestInfo?.provider
|
||||
?? selectedRequestInfo?.requestConfig?.provider) !== undefined && (
|
||||
<div>
|
||||
<dt>Input</dt>
|
||||
<dd>{selectedRequestInputTotal} tok</dd>
|
||||
<dt>Provider</dt>
|
||||
<dd>
|
||||
{selectedRequestInfo?.provider
|
||||
?? selectedRequestInfo?.requestConfig?.provider}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{selectedRequestAssistant?.cell.cacheRead !== undefined && (
|
||||
<div className={css.requestTokenDetail}>
|
||||
<dt>Cached</dt>
|
||||
<dd>{selectedRequestAssistant.cell.cacheRead} tok</dd>
|
||||
</div>
|
||||
)}
|
||||
{selectedRequestAssistant?.cell.cacheWrite !== undefined && (
|
||||
<div className={css.requestTokenDetail}>
|
||||
<dt>Cache created</dt>
|
||||
<dd>{selectedRequestAssistant.cell.cacheWrite} tok</dd>
|
||||
</div>
|
||||
)}
|
||||
{selectedRequestAssistant?.cell.input !== undefined && (
|
||||
<div className={css.requestTokenDetail}>
|
||||
<dt>Other</dt>
|
||||
<dd>{selectedRequestAssistant.cell.input} tok</dd>
|
||||
</div>
|
||||
)}
|
||||
{selectedRequestAssistant?.cell.output !== undefined && (
|
||||
{(selectedRequestInfo?.model
|
||||
?? selectedRequestInfo?.requestConfig?.model) !== undefined && (
|
||||
<div>
|
||||
<dt>Output</dt>
|
||||
<dd>{selectedRequestAssistant.cell.output} tok</dd>
|
||||
<dt>Model</dt>
|
||||
<dd>
|
||||
{selectedRequestInfo?.model
|
||||
?? selectedRequestInfo?.requestConfig?.model}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
@@ -1570,23 +1711,36 @@ export function TrajectoryTable({
|
||||
<dd>{selectedRequestSubtoolCalls}</dd>
|
||||
</div>
|
||||
)}
|
||||
{selectedRequestAssistant !== undefined && (
|
||||
<div>
|
||||
<dt>Result</dt>
|
||||
<dd className={css.overviewParentLinks}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.overviewHierarchyNavLink}
|
||||
onClick={() => {
|
||||
openRecordSummary(selectedRequestAssistant)
|
||||
}}
|
||||
>
|
||||
<span>Assistant Message</span>
|
||||
<IconChevronRightOutline14
|
||||
className={css.overviewHierarchyJumpIconTight}
|
||||
size={11}
|
||||
/>
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
<div className={css.overviewSections}>
|
||||
{selectedRequestAssistant !== undefined && (
|
||||
<OverviewSection
|
||||
label="Result"
|
||||
onOpen={() => { openRecordSummary(selectedRequestAssistant) }}
|
||||
>
|
||||
<MarkdownRecordContent
|
||||
record={selectedRequestAssistant}
|
||||
rendered
|
||||
preview
|
||||
thinkingExpanded={thinkingExpanded}
|
||||
onThinkingExpandedChange={setThinkingExpanded}
|
||||
onOpenCall={openCallSummary}
|
||||
/>
|
||||
{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}
|
||||
@@ -1596,6 +1750,15 @@ export function TrajectoryTable({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{selectedRequest !== null && activeTab === 'options' && (
|
||||
<RequestOptions options={selectedRequestOptions} />
|
||||
)}
|
||||
{selectedRequest !== null && activeTab === 'usage' && (
|
||||
<RequestUsagePanel
|
||||
usage={selectedRequestUsage}
|
||||
cumulative={selectedRequestCumulativeUsage}
|
||||
/>
|
||||
)}
|
||||
{selectedRequest !== null && activeTab === 'timing' && (
|
||||
<RequestTiming
|
||||
assistant={selectedRequestAssistant}
|
||||
@@ -1621,17 +1784,32 @@ export function TrajectoryTable({
|
||||
{!promptSelected && selected !== undefined && selectedState !== undefined && activeTab === 'overview' && (
|
||||
<>
|
||||
<dl className={css.overview}>
|
||||
{hasSelectedParents && (
|
||||
{hasSelectedHierarchy && (
|
||||
<div>
|
||||
<dt>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>
|
||||
)}
|
||||
{selectedParents.message !== undefined && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.overviewHierarchyNavLink}
|
||||
onClick={() => { openRecordSummary(selectedParents.message!) }}
|
||||
>
|
||||
<span>Parent Message</span>
|
||||
<span>Assistant Message</span>
|
||||
<IconChevronRightOutline14
|
||||
className={css.overviewHierarchyJumpIconTight}
|
||||
size={11}
|
||||
@@ -1644,7 +1822,7 @@ export function TrajectoryTable({
|
||||
className={css.overviewHierarchyNavLink}
|
||||
onClick={() => { openRecordSummary(selectedParents.tool!) }}
|
||||
>
|
||||
<span>Parent Tool Call</span>
|
||||
<span>Tool Call</span>
|
||||
<IconChevronRightOutline14
|
||||
className={css.overviewHierarchyJumpIconTight}
|
||||
size={11}
|
||||
@@ -1661,7 +1839,12 @@ export function TrajectoryTable({
|
||||
{selected.cell.kind === 'message' && (
|
||||
<div><dt>Tokens</dt><dd>{tokenSummary(selected.cell)}</dd></div>
|
||||
)}
|
||||
<div><dt>Duration</dt><dd>{formatElapsedSeconds(selected.cell.timeSeconds)}</dd></div>
|
||||
{(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)
|
||||
@@ -1696,9 +1879,21 @@ export function TrajectoryTable({
|
||||
</OverviewSection>
|
||||
</>
|
||||
)}
|
||||
<OverviewSection label="Timing" onOpen={() => { activateTab('timing') }}>
|
||||
<RecordTiming record={selected} />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,15 +2,65 @@
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { ConvViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { ConversationContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
AssistantMessageNode, ConversationContext,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { ContextsPanel, contextLabel } from './ContextsPanel.tsx'
|
||||
import { TrajectoryTable } from './TrajectoryTable.tsx'
|
||||
import {
|
||||
TrajectoryTable,
|
||||
type TrajectoryRequestNumber,
|
||||
type TrajectoryUsage,
|
||||
} from './TrajectoryTable.tsx'
|
||||
import { TrajectoryToolbar } from './TrajectoryToolbar.tsx'
|
||||
import { deriveTrajectoryLayout } from './layout.ts'
|
||||
import css from './views.module.css'
|
||||
|
||||
const EMPTY_IDS: ReadonlySet<number> = new Set()
|
||||
|
||||
interface UsageLike {
|
||||
inputTokens?: number
|
||||
cacheReadTokens?: number
|
||||
cacheWriteTokens?: number
|
||||
outputTokens?: number
|
||||
reasoningTokens?: number
|
||||
}
|
||||
|
||||
function requestUsage(value: unknown): TrajectoryUsage | undefined {
|
||||
const usage = value as UsageLike | undefined
|
||||
if (usage === undefined) return undefined
|
||||
return {
|
||||
...(usage.inputTokens === undefined ? {} : { input: usage.inputTokens }),
|
||||
...(usage.cacheReadTokens === undefined ? {} : { cacheRead: usage.cacheReadTokens }),
|
||||
...(usage.cacheWriteTokens === undefined ? {} : { cacheWrite: usage.cacheWriteTokens }),
|
||||
...(usage.outputTokens === undefined ? {} : { output: usage.outputTokens }),
|
||||
...(usage.reasoningTokens === undefined ? {} : { reasoning: usage.reasoningTokens }),
|
||||
}
|
||||
}
|
||||
|
||||
function addUsage(
|
||||
total: TrajectoryUsage | undefined,
|
||||
usage: TrajectoryUsage | undefined,
|
||||
): TrajectoryUsage | undefined {
|
||||
if (usage === undefined) return total
|
||||
return {
|
||||
...(total?.input === undefined && usage.input === undefined
|
||||
? {}
|
||||
: { input: (total?.input ?? 0) + (usage.input ?? 0) }),
|
||||
...(total?.cacheRead === undefined && usage.cacheRead === undefined
|
||||
? {}
|
||||
: { cacheRead: (total?.cacheRead ?? 0) + (usage.cacheRead ?? 0) }),
|
||||
...(total?.cacheWrite === undefined && usage.cacheWrite === undefined
|
||||
? {}
|
||||
: { cacheWrite: (total?.cacheWrite ?? 0) + (usage.cacheWrite ?? 0) }),
|
||||
...(total?.output === undefined && usage.output === undefined
|
||||
? {}
|
||||
: { output: (total?.output ?? 0) + (usage.output ?? 0) }),
|
||||
...(total?.reasoning === undefined && usage.reasoning === undefined
|
||||
? {}
|
||||
: { reasoning: (total?.reasoning ?? 0) + (usage.reasoning ?? 0) }),
|
||||
}
|
||||
}
|
||||
|
||||
function currentContextOf(contexts: readonly ConversationContext[]): ConversationContext {
|
||||
const context = contexts.at(-1)
|
||||
if (context === undefined) throw new Error('trajectory context projection must not be empty')
|
||||
@@ -43,6 +93,72 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
: contexts.find(context => context.id === selectedContextId) ?? currentContext
|
||||
const viewingCurrent = selectedContext.id === currentContext.id
|
||||
const selectedNodes = viewingCurrent ? nodes : selectedContext.nodes
|
||||
const requestNumbers = useMemo<readonly TrajectoryRequestNumber[]>(() => {
|
||||
const requestsBySeq = new Map<number, AssistantMessageNode>()
|
||||
for (const context of contexts) {
|
||||
for (const node of context.nodes) {
|
||||
if (node.kind !== 'assistant' || node.step <= 0) continue
|
||||
requestsBySeq.set(node.seq, node)
|
||||
}
|
||||
}
|
||||
for (const node of nodes) {
|
||||
if (node.kind !== 'assistant' || node.step <= 0) continue
|
||||
requestsBySeq.set(node.seq, node)
|
||||
}
|
||||
const orderedRequests = [...requestsBySeq.values()]
|
||||
.sort((left, right) => left.seq - right.seq)
|
||||
const requestBySeq = new Map<number, TrajectoryRequestNumber>()
|
||||
let cumulativeUsage: TrajectoryUsage | undefined
|
||||
for (const [index, node] of orderedRequests.entries()) {
|
||||
const usage = requestUsage(node.usage)
|
||||
cumulativeUsage = addUsage(cumulativeUsage, usage)
|
||||
requestBySeq.set(node.seq, {
|
||||
turn: node.turn,
|
||||
step: node.step,
|
||||
number: index + 1,
|
||||
...(node.provenance?.provider === undefined
|
||||
? {}
|
||||
: { provider: node.provenance.provider }),
|
||||
...(node.provenance?.model === undefined
|
||||
? {}
|
||||
: { model: node.provenance.model }),
|
||||
...(node.requestConfig === undefined ? {} : { requestConfig: node.requestConfig }),
|
||||
...(usage === undefined ? {} : { usage }),
|
||||
...(cumulativeUsage === undefined ? {} : { cumulativeUsage }),
|
||||
})
|
||||
}
|
||||
|
||||
const selected: TrajectoryRequestNumber[] = []
|
||||
const selectedKeys = new Set<string>()
|
||||
for (const node of selectedNodes) {
|
||||
if (node.kind !== 'assistant' || node.step <= 0) continue
|
||||
const request = requestBySeq.get(node.seq)
|
||||
if (request === undefined) continue
|
||||
selected.push(request)
|
||||
selectedKeys.add(`${node.turn}\u0000${node.step}`)
|
||||
}
|
||||
if (viewingCurrent && partial !== null && partial.step > 0) {
|
||||
const key = `${partial.turn}\u0000${partial.step}`
|
||||
if (!selectedKeys.has(key)) {
|
||||
selected.push({
|
||||
turn: partial.turn,
|
||||
step: partial.step,
|
||||
number: orderedRequests.length + 1,
|
||||
...(currentContext.prompt?.config?.provider === undefined
|
||||
? {}
|
||||
: { provider: currentContext.prompt.config.provider }),
|
||||
...(currentContext.prompt?.config?.model === undefined
|
||||
? {}
|
||||
: { model: currentContext.prompt.config.model }),
|
||||
...(currentContext.prompt?.config === undefined
|
||||
? {}
|
||||
: { requestConfig: currentContext.prompt.config }),
|
||||
...(cumulativeUsage === undefined ? {} : { cumulativeUsage }),
|
||||
})
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}, [contexts, currentContext.prompt, nodes, partial, selectedNodes, viewingCurrent])
|
||||
const collapsedTurns = collapsedTurnsByContext.get(selectedContext.id) ?? EMPTY_IDS
|
||||
const collapsedAssistants = collapsedAssistantsByContext.get(selectedContext.id) ?? EMPTY_IDS
|
||||
const turns = useMemo(
|
||||
@@ -164,6 +280,7 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
<TrajectoryTable
|
||||
key={selectedContext.id}
|
||||
{...selectedContext.prompt === undefined ? {} : { prompt: selectedContext.prompt }}
|
||||
requestNumbers={requestNumbers}
|
||||
turns={turns}
|
||||
collapsedTurns={collapsedTurns}
|
||||
onToggleTurn={toggleTurn}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & Waterfa
|
||||
}}
|
||||
title={lane.timing === 'measured'
|
||||
/* durationMs is non-null exactly when timing is measured. */
|
||||
? `${lane.name} · ${((lane.durationMs ?? 0) / 1000).toFixed(2)}s`
|
||||
? `${lane.name} · ${((lane.durationMs ?? 0) / 1000).toFixed(2)} s`
|
||||
: lane.timing === 'running' ? `${lane.name} · running` : `${lane.name} · duration unknown`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,15 @@ interface LaidCell {
|
||||
callId?: string
|
||||
}
|
||||
|
||||
interface LaidGroup {
|
||||
title: string
|
||||
laid: LaidCell[]
|
||||
}
|
||||
|
||||
interface TurnBucket {
|
||||
groups: LaidGroup[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold a snapshot into turn → Message/Step groups with expanded cells.
|
||||
* @param input - nodes plus in-flight partial/runningCalls.
|
||||
@@ -70,7 +79,7 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
const startedAt = finiteTime(call.time)
|
||||
if (startedAt !== null) callStartById.set(call.callId, startedAt)
|
||||
}
|
||||
const turns = new Map<number, { message: LaidCell[]; steps: Map<number, LaidCell[]> }>()
|
||||
const turns = new Map<number, TurnBucket>()
|
||||
let index = 0
|
||||
let prevAbsTime: number | null = null
|
||||
let lastAssistantTurn: number | null = null
|
||||
@@ -78,20 +87,31 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
const bucket = (turn: number) => {
|
||||
let entry = turns.get(turn)
|
||||
if (entry === undefined) {
|
||||
entry = { message: [], steps: new Map() }
|
||||
entry = { groups: [] }
|
||||
turns.set(turn, entry)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
const pushMessage = (turn: number, laid: LaidCell) => {
|
||||
bucket(turn).message.push(laid)
|
||||
const groups = bucket(turn).groups
|
||||
const last = groups.at(-1)
|
||||
if (last?.title === 'Message') {
|
||||
last.laid.push(laid)
|
||||
return
|
||||
}
|
||||
groups.push({ title: 'Message', laid: [laid] })
|
||||
}
|
||||
const pushStep = (turn: number, step: number, laid: LaidCell) => {
|
||||
const steps = bucket(turn).steps
|
||||
const list = steps.get(step) ?? []
|
||||
list.push(laid)
|
||||
steps.set(step, list)
|
||||
const pushStep = (turn: number, step: number, laid: readonly LaidCell[]) => {
|
||||
if (laid.length === 0) return
|
||||
const groups = bucket(turn).groups
|
||||
const title = `Step ${step}`
|
||||
const existing = groups.find(group => group.title === title)
|
||||
if (existing !== undefined) {
|
||||
existing.laid.push(...laid)
|
||||
return
|
||||
}
|
||||
groups.push({ title, laid: [...laid] })
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
@@ -123,10 +143,8 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
expandAssistant(node, index + 1, prevAbsTime, resultByCall, callStartById),
|
||||
codeDispatches,
|
||||
)
|
||||
for (const laid of laidList) {
|
||||
if (node.step > 0) pushStep(node.turn, node.step, laid)
|
||||
else pushMessage(node.turn, laid)
|
||||
}
|
||||
if (node.step > 0) pushStep(node.turn, node.step, laidList)
|
||||
else for (const laid of laidList) pushMessage(node.turn, laid)
|
||||
const last = laidList[laidList.length - 1]
|
||||
if (last !== undefined) index = last.cell.index
|
||||
prevAbsTime = finiteTime(node.time) ?? prevAbsTime
|
||||
@@ -153,7 +171,7 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
if (node.kind === 'tool-result') {
|
||||
if (!callEmittedInAssistant(nodes, node.callId)) {
|
||||
const toolName = node.call?.name
|
||||
pushStep(0, 1, {
|
||||
const laidList: LaidCell[] = [{
|
||||
absTime: finiteTime(node.callTime ?? node.time),
|
||||
...(toolName !== undefined ? { toolName } : {}),
|
||||
callId: node.callId,
|
||||
@@ -172,11 +190,12 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
timeSeconds: durationSeconds(node.time, node.callTime),
|
||||
startedAt: finiteTime(node.callTime),
|
||||
},
|
||||
})
|
||||
}]
|
||||
for (const laid of expandSubCalls(codeDispatches.get(node.callId), index)) {
|
||||
pushStep(0, 1, laid)
|
||||
laidList.push(laid)
|
||||
index = laid.cell.index
|
||||
}
|
||||
pushStep(0, 1, laidList)
|
||||
}
|
||||
prevAbsTime = finiteTime(node.time) ?? prevAbsTime
|
||||
}
|
||||
@@ -195,10 +214,8 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
callStartById,
|
||||
{ streaming: true },
|
||||
)
|
||||
for (const laid of laidList) {
|
||||
if (partial.step > 0) pushStep(partial.turn, partial.step, laid)
|
||||
else pushMessage(partial.turn, laid)
|
||||
}
|
||||
if (partial.step > 0) pushStep(partial.turn, partial.step, laidList)
|
||||
else for (const laid of laidList) pushMessage(partial.turn, laid)
|
||||
const last = laidList[laidList.length - 1]
|
||||
if (last !== undefined) index = last.cell.index
|
||||
}
|
||||
@@ -206,7 +223,7 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
const seenCalls = collectCallIds(turns)
|
||||
for (const call of runningCalls) {
|
||||
if (seenCalls.has(call.callId)) continue
|
||||
pushStep(call.turn, call.step > 0 ? call.step : 1, {
|
||||
const laidList: LaidCell[] = [{
|
||||
absTime: null,
|
||||
toolName: call.name,
|
||||
callId: call.callId,
|
||||
@@ -219,34 +236,27 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
timeSeconds: null,
|
||||
startedAt: finiteTime(call.time),
|
||||
},
|
||||
})
|
||||
}]
|
||||
for (const laid of expandSubCalls(codeDispatches.get(call.callId), index)) {
|
||||
pushStep(call.turn, call.step > 0 ? call.step : 1, laid)
|
||||
laidList.push(laid)
|
||||
index = laid.cell.index
|
||||
}
|
||||
pushStep(call.turn, call.step > 0 ? call.step : 1, laidList)
|
||||
}
|
||||
|
||||
// Orphan turn-0 cells (orphaned tools / steering turn 0) fold into Turn 1.
|
||||
const prologue = turns.get(0)
|
||||
if (prologue !== undefined) {
|
||||
turns.delete(0)
|
||||
const emptyTurn = (): { message: LaidCell[]; steps: Map<number, LaidCell[]> } => ({
|
||||
message: [],
|
||||
steps: new Map(),
|
||||
})
|
||||
const emptyTurn = (): TurnBucket => ({ groups: [] })
|
||||
const first = turns.get(1) ?? emptyTurn()
|
||||
first.message = [...prologue.message, ...first.message]
|
||||
for (const [step, cells] of prologue.steps) {
|
||||
const existing = first.steps.get(step) ?? []
|
||||
first.steps.set(step, [...cells, ...existing])
|
||||
}
|
||||
first.groups = [...prologue.groups, ...first.groups]
|
||||
turns.set(1, first)
|
||||
}
|
||||
|
||||
for (const entry of turns.values()) {
|
||||
for (const laid of entry.message) attachToolSchema(laid, callSchemas)
|
||||
for (const laid of entry.steps.values()) {
|
||||
for (const cell of laid) attachToolSchema(cell, callSchemas)
|
||||
for (const group of entry.groups) {
|
||||
for (const laid of group.laid) attachToolSchema(laid, callSchemas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,30 +277,20 @@ function attachToolSchema(
|
||||
|
||||
function toTurnModel(
|
||||
turn: number,
|
||||
entry: { message: LaidCell[]; steps: Map<number, LaidCell[]> },
|
||||
entry: TurnBucket,
|
||||
): TrajectoryTurnModel {
|
||||
const groups: TrajectoryGroupModel[] = []
|
||||
if (entry.message.length > 0) {
|
||||
const description = groupDescription(entry.message)
|
||||
groups.push({
|
||||
title: 'Message',
|
||||
...(description !== undefined ? { description } : {}),
|
||||
cells: entry.message.map(l => l.cell),
|
||||
})
|
||||
}
|
||||
for (const step of [...entry.steps.keys()].sort((a, b) => a - b)) {
|
||||
const laid = entry.steps.get(step) ?? []
|
||||
const groups = entry.groups.map(({ title, laid }): TrajectoryGroupModel => {
|
||||
const description = groupDescription(laid)
|
||||
groups.push({
|
||||
title: `Step ${step}`,
|
||||
return {
|
||||
title,
|
||||
...(description !== undefined ? { description } : {}),
|
||||
cells: laid.map(l => l.cell),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return { turn, groups }
|
||||
}
|
||||
|
||||
/** Wall-span duration + tool histogram, e.g. `1.5s bash×6`. */
|
||||
/** Wall-span duration + tool histogram, e.g. `1.5 s bash×6`. */
|
||||
function groupDescription(laid: readonly LaidCell[]): string | undefined {
|
||||
const parts: string[] = []
|
||||
// Tool rows contribute start (absTime) and end (start + own duration) so a
|
||||
@@ -325,8 +325,8 @@ function groupDescription(laid: readonly LaidCell[]): string | undefined {
|
||||
function formatGroupDuration(seconds: number): string | undefined {
|
||||
if (!Number.isFinite(seconds)) return undefined
|
||||
const rounded = Math.round(seconds * 10) / 10
|
||||
if (Number.isInteger(rounded)) return `${rounded}s`
|
||||
return `${rounded.toFixed(1)}s`
|
||||
if (Number.isInteger(rounded)) return `${rounded} s`
|
||||
return `${rounded.toFixed(1)} s`
|
||||
}
|
||||
|
||||
/** Own-duration seconds from two epoch-ms stamps; null when either is unusable. */
|
||||
@@ -551,15 +551,12 @@ function callEmittedInAssistant(nodes: ConversationSnapshot['nodes'], callId: st
|
||||
}
|
||||
|
||||
function collectCallIds(
|
||||
turns: Map<number, { message: LaidCell[]; steps: Map<number, LaidCell[]> }>,
|
||||
turns: Map<number, TurnBucket>,
|
||||
): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
for (const entry of turns.values()) {
|
||||
for (const laid of entry.message) {
|
||||
if (laid.callId !== undefined) ids.add(laid.callId)
|
||||
}
|
||||
for (const list of entry.steps.values()) {
|
||||
for (const laid of list) {
|
||||
for (const group of entry.groups) {
|
||||
for (const laid of group.laid) {
|
||||
if (laid.callId !== undefined) ids.add(laid.callId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,6 @@ export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
|
||||
export function formatElapsedSeconds(seconds: number | null): string {
|
||||
if (seconds === null || !Number.isFinite(seconds)) return '—'
|
||||
const rounded = Math.round(seconds * 10) / 10
|
||||
if (Number.isInteger(rounded)) return `${rounded}s`
|
||||
return `${rounded.toFixed(1)}s`
|
||||
if (Number.isInteger(rounded)) return `${rounded} s`
|
||||
return `${rounded.toFixed(1)} s`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user