diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css
index 7ef67a9d38..1997faaa6f 100644
--- a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css
+++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css
@@ -32,7 +32,7 @@
}
.eventColumn {
- width: 96px;
+ width: 104px;
}
.contentColumn {
@@ -91,6 +91,78 @@
background: var(--dsw-alias-interactive-bg-active);
}
+.requestBoundaryControl {
+ position: absolute;
+ z-index: 6;
+ top: -8px;
+ left: 6px;
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+}
+
+.requestBoundaryControl::before {
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: var(--dsw-alias-label-caption);
+ box-shadow:
+ 0 0 0 2px var(--dsw-alias-bg-layer-1),
+ 0 0 0 3px transparent;
+ content: '';
+ transition:
+ background 120ms var(--ds-ease-in-out),
+ transform 120ms var(--ds-ease-in-out);
+}
+
+.requestBoundaryControl::after {
+ position: absolute;
+ top: 2px;
+ left: 17px;
+ width: max-content;
+ padding: 0 4px;
+ border-radius: 2px;
+ color: var(--dsw-alias-label-secondary);
+ background: var(--dsw-alias-bg-layer-1);
+ content: attr(data-label);
+ font: 9px/12px var(--ds-font-family-code);
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(-2px);
+ transition:
+ opacity 120ms var(--ds-ease-in-out),
+ transform 120ms var(--ds-ease-in-out);
+ user-select: none;
+ white-space: nowrap;
+}
+
+.requestBoundaryControl:hover::before,
+.requestBoundaryControl:focus-visible::before,
+.requestBoundaryControl[aria-pressed='true']::before {
+ background: var(--dsw-alias-label-primary);
+ transform: scale(1.25);
+}
+
+.requestBoundaryControl:hover::after,
+.requestBoundaryControl:focus-visible::after {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.requestBoundaryControl:focus-visible {
+ outline: none;
+}
+
+.table tbody tr:has(.requestBoundaryControl:hover):not([data-selected='true']) {
+ background: transparent;
+}
+
.event {
position: relative;
}
@@ -143,7 +215,7 @@
.event {
overflow: visible !important;
padding-right: 4px !important;
- padding-left: 10px !important;
+ padding-left: 18px !important;
}
.turnLabel {
@@ -475,6 +547,19 @@
color: var(--dsw-alias-label-primary);
}
+.requestDetailsDot {
+ flex: none;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--dsw-alias-label-secondary);
+}
+
+.requestDetailsName {
+ flex: none;
+ font: 500 12px/16px var(--ds-font-family-code);
+}
+
.detailsLocation {
min-width: 0;
overflow: hidden;
@@ -610,6 +695,10 @@
align-items: center;
}
+.overview > .requestTokenDetail dt {
+ padding-left: 12px;
+}
+
.overview dt {
color: var(--dsw-alias-label-tertiary);
}
diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
index 2cd39ab5a8..8f24b1de78 100644
--- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
+++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx
@@ -59,6 +59,12 @@ interface ToolCallTextParts {
args?: string
}
+interface SelectedRequest {
+ turn: number
+ number: number
+ group: string
+}
+
interface DetailsResizeDrag {
pointerId: number
startX: number
@@ -81,6 +87,10 @@ const SYSTEM_PROMPT_TABS: readonly DetailTabItem[] = [
{ id: 'system-prompt', label: 'System Prompt' },
{ id: 'tools', label: 'Tools' },
]
+const REQUEST_TABS: readonly DetailTabItem[] = [
+ { id: 'overview', label: 'Summary' },
+ { id: 'timing', label: 'Timing' },
+]
type TrajectorySplitStyle = CSSProperties & {
'--trajectory-tool-request-width': string
@@ -213,6 +223,12 @@ function flattenRecords(turns: readonly TrajectoryTurnModel[]): TableRecord[] {
})
}
+function requestNumber(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 summarizeTurn(records: readonly TableRecord[]): string {
const userText = records
.filter(record => record.cell.kind === 'user')
@@ -811,6 +827,25 @@ function RecordTiming({ record }: { record: TableRecord }) {
)
}
+function RequestTiming({
+ assistant,
+ anchor,
+}: {
+ assistant: TableRecord | undefined
+ anchor: TableRecord | undefined
+}) {
+ if (assistant !== undefined) return