({ turn }: { turn: TurnNode })
| 232 | } |
| 233 | |
| 234 | function TurnCard({ turn }: { turn: TurnNode }) { |
| 235 | const [open, setOpen] = useState(turn.index === 0); |
| 236 | const totalTokens = turn.tokens.inputOther + turn.tokens.output + turn.tokens.inputCacheRead + turn.tokens.inputCacheCreation; |
| 237 | return ( |
| 238 | <div className="border border-border bg-surface-0"> |
| 239 | <button |
| 240 | type="button" |
| 241 | onClick={() => { setOpen((v) => !v); }} |
| 242 | className="flex w-full flex-wrap items-center gap-2 px-3 py-2 text-left hover:bg-surface-1" |
| 243 | > |
| 244 | <span className="text-fg-3">{open ? '▾' : '▸'}</span> |
| 245 | <Pill tone={turn.trigger === 'steer' ? 'turn' : 'conversation'} variant="outline"> |
| 246 | turn {turn.index}{turn.trigger === 'steer' ? ' (steer)' : ''} |
| 247 | </Pill> |
| 248 | {turn.originKind && turn.originKind !== 'user' ? ( |
| 249 | <Pill tone="meta" variant="outline">{turn.originKind}</Pill> |
| 250 | ) : null} |
| 251 | {turn.cancelled ? <Pill tone="warning">cancelled</Pill> : null} |
| 252 | {turn.toolErrorCount > 0 ? <Pill tone="error">{turn.toolErrorCount} err</Pill> : null} |
| 253 | <span className="min-w-0 flex-1 truncate font-mono text-[12px] text-fg-1" title={turn.promptText}> |
| 254 | {turn.promptText || '(no prompt text)'} |
| 255 | </span> |
| 256 | <span className="flex shrink-0 items-center gap-3 font-mono text-[11px] text-fg-3 tabular"> |
| 257 | <span>{turn.steps.length} steps</span> |
| 258 | <span>{turn.toolCallCount} tools</span> |
| 259 | <span title="total tokens processed this turn">{formatTokens(totalTokens)} tok</span> |
| 260 | <span title="active execution time">{formatDuration(turn.durationMs)}</span> |
| 261 | </span> |
| 262 | </button> |
| 263 | |
| 264 | {open ? ( |
| 265 | <div className="border-t border-border px-3 py-2"> |
| 266 | {turn.waitBeforeMs !== undefined && turn.waitBeforeMs >= 1000 ? ( |
| 267 | <div className="mb-2 font-mono text-[10px] text-fg-3"> |
| 268 | ⏱ waited {formatDuration(turn.waitBeforeMs)} before this turn |
| 269 | </div> |
| 270 | ) : null} |
| 271 | <div className="flex flex-col gap-1.5"> |
| 272 | {turn.steps.map((step) => ( |
| 273 | <StepRow key={step.uuid} step={step} turnDurationMs={turn.durationMs} /> |
| 274 | ))} |
| 275 | </div> |
| 276 | </div> |
| 277 | ) : null} |
| 278 | </div> |
| 279 | ); |
| 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; |
nothing calls this directly
no test coverage detected