{
+ setDetailsWidth(null)
+ setToolRequestOffset(null)
+ }}
+ onPointerDown={(event) => {
+ if (event.button !== 0) return
+ const details = event.currentTarget.parentElement
+ const split = details?.parentElement
+ if (details === null || details === undefined || split === null || split === undefined) 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
+ const split = details?.parentElement
+ if (details === null || details === undefined || split === null || split === undefined) 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()
+ }}
+ />
+
+
+
+ {KIND_LABEL[selected.cell.kind]}
+
+ {`Turn ${selected.turn} · ${selected.group}`}
+
+
+
+
+ {selectedTabs.map(tab => (
+
+ ))}
+
+
+ {activeTab === 'overview' && (
+ <>
+
+ {hasSelectedParents && (
+
+
- Hierarchy
+ -
+ {selectedParents.message !== undefined && (
+
+ )}
+ {selectedParents.tool !== undefined && (
+
+ )}
+
+
+ )}
+
+
- Status
+ - {statusLabel(selectedState)}
+
+ {selected.cell.kind === 'message' && (
+ - Tokens
- {tokenSummary(selected.cell)}
+ )}
+ - Duration
- {formatElapsedSeconds(selected.cell.timeSeconds)}
+
+
+ {isMarkdownRecord(selected)
+ ? (
+ <>
+ { activateTab('rendered') }}>
+
+
+ >
+ )
+ : (
+ <>
+ {selected.cell.inputDetail && (
+ { activateTab('input') }}>
+
+
+ )}
+ {selected.cell.outputDetail && (
+ { activateTab('output') }}>
+
+
+ )}
+ { activateTab('schema') }}>
+
+
+ >
+ )}
+ { activateTab('timing') }}>
+
+
+
+ >
+ )}
+ {activeTab === 'rendered' && (
+
+ )}
+ {activeTab === 'source' && (
+
+ )}
+ {activeTab === 'input' && (
+
+ )}
+ {activeTab === 'output' && (
+
+ )}
+ {activeTab === 'schema' && (
+
+ )}
+ {activeTab === 'timing' && (
+
+ )}
+
+
+ )}
+
+ )
+}
diff --git a/packages/client/ui-trajectory/src/client/TrajectoryToolbar.module.css b/packages/client/ui-trajectory/src/client/TrajectoryToolbar.module.css
new file mode 100644
index 0000000000..d5d81521c2
--- /dev/null
+++ b/packages/client/ui-trajectory/src/client/TrajectoryToolbar.module.css
@@ -0,0 +1,85 @@
+.root {
+ position: sticky;
+ top: 0;
+ z-index: 4;
+ box-sizing: border-box;
+ width: 100%;
+ height: var(--dsh-trajectory-toolbar-height);
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
+ background: var(--dsw-alias-bg-layer-1);
+}
+
+.inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ padding: 0 14px 0 16px;
+}
+
+.summary {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ gap: 10px;
+}
+
+.title {
+ flex: none;
+ color: var(--dsw-alias-label-primary);
+ font: var(--dsw-font-xs-strong-13);
+}
+
+.actions {
+ display: flex;
+ flex: none;
+ align-items: center;
+ gap: 2px;
+}
+
+.action {
+ display: inline-flex;
+ flex: none;
+ align-items: center;
+ box-sizing: border-box;
+ height: 26px;
+ padding: 0 7px;
+ gap: 6px;
+ border: 0;
+ border-radius: 4px;
+ color: var(--dsw-alias-label-tertiary);
+ background: transparent;
+ cursor: pointer;
+ font: var(--dsw-font-xs-13);
+ transition:
+ color 120ms var(--ds-ease-in-out),
+ background-color 120ms var(--ds-ease-in-out);
+}
+
+.action:hover:not(:disabled) {
+ color: var(--dsw-alias-label-primary);
+ background: var(--dsw-alias-interactive-bg-hover);
+}
+
+.action:focus-visible {
+ outline: 1px solid var(--dsw-alias-state-business-primary);
+ outline-offset: 2px;
+}
+
+.action:disabled {
+ color: var(--dsw-alias-label-dimmed);
+ cursor: not-allowed;
+}
+
+.actionIcon {
+ color: var(--dsw-alias-label-tertiary);
+ font: 13px/13px var(--ds-font-family-code);
+}
+
+@media (max-width: 720px) {
+ .summary {
+ gap: 7px;
+ }
+}
diff --git a/packages/client/ui-trajectory/src/client/TrajectoryToolbar.tsx b/packages/client/ui-trajectory/src/client/TrajectoryToolbar.tsx
new file mode 100644
index 0000000000..f063377a00
--- /dev/null
+++ b/packages/client/ui-trajectory/src/client/TrajectoryToolbar.tsx
@@ -0,0 +1,66 @@
+/** Trajectory toolbar: view identity, record totals, and the ledger fold control. */
+
+import css from './TrajectoryToolbar.module.css'
+
+export interface TrajectoryToolbarProps {
+ /** Number of turns containing more than one row. */
+ collapsibleTurns: number
+ /** Whether every collapsible turn is currently folded. */
+ allTurnsCollapsed: boolean
+ /** Fold or expand every collapsible turn. */
+ 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
+}
+
+/**
+ * Render the sticky trajectory toolbar.
+ * @param props - rendered counts and whole-list fold state.
+ * @returns the toolbar element.
+ */
+export function TrajectoryToolbar({
+ collapsibleTurns,
+ allTurnsCollapsed,
+ onToggleAllTurns,
+ collapsibleAssistants,
+ allAssistantsCollapsed,
+ onToggleAllAssistants,
+}: TrajectoryToolbarProps) {
+ return (
+