({ step, turnDurationMs }: { step: StepNode; turnDurationMs?: number })
| 280 | } |
| 281 | |
| 282 | function StepRow({ step, turnDurationMs }: { step: StepNode; turnDurationMs?: number }) { |
| 283 | const widthPct = turnDurationMs && step.durationMs ? Math.max(2, (step.durationMs / turnDurationMs) * 100) : 0; |
| 284 | return ( |
| 285 | <div className="border-l-2 pl-2" style={{ borderColor: step.isError ? 'var(--color-sev-error)' : 'var(--color-border)' }}> |
| 286 | <div className="flex flex-wrap items-center gap-2 font-mono text-[11px]"> |
| 287 | <span className="text-fg-2">step {step.step}</span> |
| 288 | {step.finishReason ? ( |
| 289 | <span className={step.isError ? 'text-[var(--color-sev-error)]' : 'text-fg-3'}>{step.finishReason}</span> |
| 290 | ) : null} |
| 291 | <span className="text-fg-3 tabular" title="step wall-clock duration">{formatDuration(step.durationMs)}</span> |
| 292 | {step.llmFirstTokenLatencyMs !== undefined ? ( |
| 293 | <span |
| 294 | className="text-fg-3 tabular" |
| 295 | title={ |
| 296 | step.llmServerFirstTokenMs !== undefined && step.llmRequestBuildMs !== undefined |
| 297 | ? `time to first token (api ${step.llmServerFirstTokenMs}ms + client ${step.llmRequestBuildMs}ms)` |
| 298 | : 'time to first token' |
| 299 | } |
| 300 | > |
| 301 | ttft {step.llmFirstTokenLatencyMs}ms |
| 302 | {step.llmServerFirstTokenMs !== undefined && step.llmRequestBuildMs !== undefined |
| 303 | ? ` (api ${step.llmServerFirstTokenMs} + client ${step.llmRequestBuildMs})` |
| 304 | : ''} |
| 305 | </span> |
| 306 | ) : null} |
| 307 | {step.llmServerDecodeMs !== undefined && step.llmClientConsumeMs !== undefined ? ( |
| 308 | <span |
| 309 | className="text-fg-3 tabular" |
| 310 | title="decode window split (server awaiting parts + client processing parts)" |
| 311 | > |
| 312 | decode {step.llmServerDecodeMs}+{step.llmClientConsumeMs}ms |
| 313 | </span> |
| 314 | ) : null} |
| 315 | {step.contextTokens !== undefined ? ( |
| 316 | <span className="text-fg-3 tabular" title="context-window fill after step">ctx {formatTokens(step.contextTokens)}</span> |
| 317 | ) : null} |
| 318 | {step.content.thinkChars > 0 ? <span className="text-[var(--color-cat-meta)]" title="reasoning chars">💭 {step.content.thinkChars}</span> : null} |
| 319 | </div> |
| 320 | {widthPct > 0 ? ( |
| 321 | <div className="mt-0.5 h-1 w-full bg-surface-2"> |
| 322 | <div className="h-1" style={{ width: `${widthPct}%`, backgroundColor: step.isError ? 'var(--color-sev-error)' : 'var(--color-cat-conversation)' }} /> |
| 323 | </div> |
| 324 | ) : null} |
| 325 | {step.toolCalls.length > 0 ? ( |
| 326 | <div className="mt-1 flex flex-col gap-0.5"> |
| 327 | {step.toolCalls.map((tc) => ( |
| 328 | <ToolRow key={tc.toolCallId} tc={tc} stepDurationMs={step.durationMs} /> |
| 329 | ))} |
| 330 | </div> |
| 331 | ) : null} |
| 332 | </div> |
| 333 | ); |
| 334 | } |
| 335 | |
| 336 | function ToolRow({ tc, stepDurationMs }: { tc: ToolCallNode; stepDurationMs?: number }) { |
| 337 | const widthPct = stepDurationMs && tc.durationMs ? Math.max(2, (tc.durationMs / stepDurationMs) * 100) : 0; |
nothing calls this directly
no test coverage detected