From f4994ff237c8828e7a59848557389fc0573597e2 Mon Sep 17 00:00:00 2001 From: Harshil Vasoya Date: Wed, 23 Sep 2026 12:15:21 +0530 Subject: [PATCH 1/2] Fix: Sandbox tool calls name with intent and tool call grouping --- .changeset/fuzzy-sandboxes-explain.md | 5 ++ .../AgentSessionEventTimelineChart.tsx | 87 +++++++++++++++---- .../AgentSessionTimelineTooltip.tsx | 15 +++- .../src/utils/sessionEventTimelineChart.ts | 2 +- .../src/utils/sessionTimelineEvents.ts | 3 +- .../AgentSessionEventTimelineChart.test.tsx | 53 +++++++++++ .../test/utils/sessionTimelineEvents.test.ts | 8 +- 7 files changed, 146 insertions(+), 27 deletions(-) create mode 100644 .changeset/fuzzy-sandboxes-explain.md diff --git a/.changeset/fuzzy-sandboxes-explain.md b/.changeset/fuzzy-sandboxes-explain.md new file mode 100644 index 000000000..47151e0da --- /dev/null +++ b/.changeset/fuzzy-sandboxes-explain.md @@ -0,0 +1,5 @@ +--- +'@truefoundry/trueforge-ui': patch +--- + +Improve session timeline tooltips with sandbox tool details and grouped sub-agent tool calls. diff --git a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx index 22a2cc1d0..97af07801 100644 --- a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx +++ b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx @@ -205,6 +205,28 @@ export function AgentSessionEventTimelineChart({ () => getSubAgentLanes({ subAgentSegments, threadSegments: durationSegments, minWidthMs: MARKER_PX * msPerPx }), [durationSegments, msPerPx, subAgentSegments], ); + const subAgentToolCallGroups = useMemo( + () => + subAgentLanes.flatMap(lane => + groupOverlappingToolCalls(lane.segments.filter(segment => segment.type === 'tool_call')) + .filter(group => group.segments.length > 1) + .map(group => ({ group, lane: lane.lane, subAgentLabel: lane.track.description })), + ), + [subAgentLanes], + ); + const groupedSubAgentToolCallIds = useMemo( + () => new Set(subAgentToolCallGroups.flatMap(({ group }) => group.segments.map(segment => segment.id))), + [subAgentToolCallGroups], + ); + const subAgentEventSegments = useMemo( + () => + subAgentLanes.flatMap(lane => + lane.segments + .filter(segment => !groupedSubAgentToolCallIds.has(segment.id)) + .map(segment => ({ segment, lane: lane.lane })), + ), + [groupedSubAgentToolCallIds, subAgentLanes], + ); const mainCandidates = useMemo(() => { return [...durationSegments.filter(segment => segment.threadId === MAIN_THREAD_ID), ...mainSubAgents].sort( (left, right) => left.startMs - right.startMs || left.endMs - right.endMs, @@ -242,8 +264,15 @@ export function AgentSessionEventTimelineChart({ () => [ ...turnBars.map((bar): TimelineHoverTarget => ({ type: TIMELINE_TYPE.turn, bar })), ...markerGroups.map((group): TimelineHoverTarget => ({ type: TIMELINE_TYPE.markerGroup, group })), - ...subAgentLanes.flatMap(lane => - lane.segments.map((segment): TimelineHoverTarget => ({ type: TIMELINE_TYPE.event, segment })), + ...subAgentEventSegments.map( + ({ segment }): TimelineHoverTarget => ({ type: TIMELINE_TYPE.event, segment }), + ), + ...subAgentToolCallGroups.map( + ({ group, subAgentLabel }): TimelineHoverTarget => ({ + type: TIMELINE_TYPE.toolCallGroup, + group, + subAgentLabel, + }), ), ...mainEventSegments.map((segment): TimelineHoverTarget | null => { if (segment.type !== 'sub_agent') return { type: TIMELINE_TYPE.event, segment }; @@ -252,7 +281,15 @@ export function AgentSessionEventTimelineChart({ }), ...toolCallGroups.map((group): TimelineHoverTarget => ({ type: TIMELINE_TYPE.toolCallGroup, group })), ], - [mainEventSegments, markerGroups, subAgentGroups, subAgentLanes, toolCallGroups, turnBars], + [ + mainEventSegments, + markerGroups, + subAgentEventSegments, + subAgentGroups, + subAgentToolCallGroups, + toolCallGroups, + turnBars, + ], ); const barDataset = ({ @@ -319,19 +356,29 @@ export function AgentSessionEventTimelineChart({ order: 0, } satisfies ChartDataset<'scatter', MarkerPoint[]>; }), - ...subAgentLanes.flatMap(lane => - lane.segments.map(segment => - barDataset({ - label: `${getSessionEventLabel(segment.type)}: ${segment.title}`, - range: segment, - y: centers[eventRow + 1 + lane.lane] ?? 0, - color: getSessionEventColor(segment.type, isDark), - hover: getSessionEventHoverColor(segment.type, isDark), - thickness: 12, - order: eventOrder(segment.type), - inflateAmount: 0.5, - }), - ), + ...subAgentEventSegments.map(({ segment, lane }) => + barDataset({ + label: `${getSessionEventLabel(segment.type)}: ${segment.title}`, + range: segment, + y: centers[eventRow + 1 + lane] ?? 0, + color: getSessionEventColor(segment.type, isDark), + hover: getSessionEventHoverColor(segment.type, isDark), + thickness: 12, + order: eventOrder(segment.type), + inflateAmount: 0.5, + }), + ), + ...subAgentToolCallGroups.map(({ group, lane }) => + barDataset({ + label: 'Parallel tool calls', + range: group, + y: centers[eventRow + 1 + lane] ?? 0, + color: getSessionEventColor('tool_call', isDark), + hover: getSessionEventHoverColor('tool_call', isDark), + thickness: 12, + order: eventOrder('tool_call'), + inflateAmount: 0.5, + }), ), ...mainEventSegments.map(segment => barDataset({ @@ -366,7 +413,8 @@ export function AgentSessionEventTimelineChart({ mainEventSegments, markerGroups, markerRow, - subAgentLanes, + subAgentEventSegments, + subAgentToolCallGroups, toolCallGroups, turnBars, turnFill, @@ -512,7 +560,10 @@ export function AgentSessionEventTimelineChart({ } /> ) : tooltipTarget?.type === TIMELINE_TYPE.toolCallGroup ? ( - + ) : tooltipTarget?.type === TIMELINE_TYPE.markerGroup ? ( ) : tooltipTarget?.type === TIMELINE_TYPE.subAgentGroup ? ( diff --git a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionTimelineTooltip.tsx b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionTimelineTooltip.tsx index ee30e6580..88cbad032 100644 --- a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionTimelineTooltip.tsx +++ b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionTimelineTooltip.tsx @@ -102,14 +102,23 @@ export function SessionTurnTooltip({ ); } -export function SessionToolCallGroupTooltip({ group }: { group: TimelineToolCallGroup }) { +export function SessionToolCallGroupTooltip({ + group, + subAgentLabel, +}: { + group: TimelineToolCallGroup; + subAgentLabel?: string; +}) { return (
-
+ {subAgentLabel != null ? ( +
{`Sub-Agent: ${subAgentLabel}`}
+ ) : null} +
Tool calls {formatTimelineDuration(group.endMs - group.startMs)}
-
+
{group.segments.map(segment => (
diff --git a/packages/trueforge-ui/src/utils/sessionEventTimelineChart.ts b/packages/trueforge-ui/src/utils/sessionEventTimelineChart.ts index 6a1d94fb8..a5f911b14 100644 --- a/packages/trueforge-ui/src/utils/sessionEventTimelineChart.ts +++ b/packages/trueforge-ui/src/utils/sessionEventTimelineChart.ts @@ -72,7 +72,7 @@ export type TimelineHoverTarget = | { type: typeof TIMELINE_TYPE.turn; bar: TimelineTurnBar } | { type: typeof TIMELINE_TYPE.event; segment: SessionEventTimelineSegment } | { type: typeof TIMELINE_TYPE.markerGroup; group: TimelineMarkerGroup } - | { type: typeof TIMELINE_TYPE.toolCallGroup; group: TimelineToolCallGroup } + | { type: typeof TIMELINE_TYPE.toolCallGroup; group: TimelineToolCallGroup; subAgentLabel?: string } | { type: typeof TIMELINE_TYPE.subAgentGroup; group: TimelineSubAgentGroup }; /** Stable identity used to avoid replacing tooltip state while hovering the same bar. */ diff --git a/packages/trueforge-ui/src/utils/sessionTimelineEvents.ts b/packages/trueforge-ui/src/utils/sessionTimelineEvents.ts index 973095f42..55401f63d 100644 --- a/packages/trueforge-ui/src/utils/sessionTimelineEvents.ts +++ b/packages/trueforge-ui/src/utils/sessionTimelineEvents.ts @@ -132,7 +132,8 @@ export function toolCallDescription(toolCall: Record): string { const argumentsJson = typeof fn?.arguments === 'string' ? fn.arguments : undefined; if (SANDBOX_TOOL_NAMES.has(name) || name === 'code_sandbox') { const intent = parseSandboxArgs(argumentsJson).intent?.trim(); - return intent != null && intent.length > 0 ? intent : name; + const sandboxTool = `Sandbox: ${name}`; + return intent != null && intent.length > 0 ? `${sandboxTool} - ${intent}` : sandboxTool; } // Deferred MCP wrappers store the real tool in args; surface it like ToolCallContainer. if (MCP_META_TOOLS.has(name)) { diff --git a/packages/trueforge-ui/test/atoms/AgentSessionEventTimelineChart.test.tsx b/packages/trueforge-ui/test/atoms/AgentSessionEventTimelineChart.test.tsx index 5d4f1822e..2939c73a6 100644 --- a/packages/trueforge-ui/test/atoms/AgentSessionEventTimelineChart.test.tsx +++ b/packages/trueforge-ui/test/atoms/AgentSessionEventTimelineChart.test.tsx @@ -152,6 +152,59 @@ describe('AgentSessionEventTimelineChart', () => { expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); }); + it('groups overlapping tool calls within a sub-agent lane', async () => { + const subAgentSegments: SessionEventTimelineSegment[] = [ + ...segments, + { + id: 'sub-agent', + type: 'sub_agent', + title: 'thread.created', + description: 'Researcher', + startMs: 100, + endMs: 900, + turnIndex: 0, + threadId: 'sub-agent-thread', + }, + { + id: 'sub-tool-1', + type: 'tool_call', + title: 'tool.call', + description: 'search', + startMs: 200, + endMs: 600, + turnIndex: 0, + threadId: 'sub-agent-thread', + }, + { + id: 'sub-tool-2', + type: 'tool_call', + title: 'tool.call', + description: 'fetch', + startMs: 300, + endMs: 700, + turnIndex: 0, + threadId: 'sub-agent-thread', + }, + ]; + render(); + + const datasets = Reflect.get(capturedData ?? {}, 'datasets'); + if (!Array.isArray(datasets)) throw new Error('Expected chart datasets'); + const groupedDatasetIndex = datasets.findIndex(dataset => Reflect.get(dataset, 'label') === 'Parallel tool calls'); + expect(groupedDatasetIndex).toBeGreaterThanOrEqual(0); + expect(datasets.some(dataset => Reflect.get(dataset, 'label') === 'Tool call: search')).toBe(false); + expect(datasets.some(dataset => Reflect.get(dataset, 'label') === 'Tool call: fetch')).toBe(false); + + const onHover = Reflect.get(capturedOptions ?? {}, 'onHover'); + if (typeof onHover !== 'function') throw new Error('Expected Chart.js onHover callback'); + act(() => onHover({ native: null }, [{ datasetIndex: groupedDatasetIndex }])); + + expect(await screen.findByText('Sub-Agent: Researcher')).toBeInTheDocument(); + expect(await screen.findByText('Tool calls')).toBeInTheDocument(); + expect(screen.getByText('search').parentElement?.parentElement).not.toHaveClass('border-t'); + expect(screen.getByText('fetch')).toBeInTheDocument(); + }); + it('summarizes every sub-agent represented by a shared bar', () => { render( { - it('shows sandbox intent instead of the exec tool name', () => { + it('shows the sandbox tool name and intent', () => { expect( toolCallDescription({ function: { @@ -11,16 +11,16 @@ describe('toolCallDescription', () => { arguments: JSON.stringify({ command: 'ls', intent: 'List workspace files' }), }, }), - ).toBe('List workspace files'); + ).toBe('Sandbox: exec - List workspace files'); expect( toolCallDescription({ function: { name: 'sandbox_exec', arguments: '{"intent":"Generate a PDF"}' }, }), - ).toBe('Generate a PDF'); + ).toBe('Sandbox: sandbox_exec - Generate a PDF'); }); it('falls back to the tool name when intent is missing', () => { - expect(toolCallDescription({ function: { name: 'exec', arguments: '{"command":"ls"}' } })).toBe('exec'); + expect(toolCallDescription({ function: { name: 'exec', arguments: '{"command":"ls"}' } })).toBe('Sandbox: exec'); expect(toolCallDescription({ function: { name: 'search', arguments: '{"q":"x"}' } })).toBe('search'); }); From 442d81d050f1a5f1d357eda257ac995509664fcd Mon Sep 17 00:00:00 2001 From: Harshil Vasoya Date: Wed, 23 Sep 2026 12:26:55 +0530 Subject: [PATCH 2/2] refactor: streamline timeline hover target mapping in AgentSessionEventTimelineChart --- .../AgentSessionEventTimelineChart.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx index 97af07801..6a4490e40 100644 --- a/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx +++ b/packages/trueforge-ui/src/atoms/agent-details/AgentSessionEventTimelineChart.tsx @@ -264,16 +264,12 @@ export function AgentSessionEventTimelineChart({ () => [ ...turnBars.map((bar): TimelineHoverTarget => ({ type: TIMELINE_TYPE.turn, bar })), ...markerGroups.map((group): TimelineHoverTarget => ({ type: TIMELINE_TYPE.markerGroup, group })), - ...subAgentEventSegments.map( - ({ segment }): TimelineHoverTarget => ({ type: TIMELINE_TYPE.event, segment }), - ), - ...subAgentToolCallGroups.map( - ({ group, subAgentLabel }): TimelineHoverTarget => ({ - type: TIMELINE_TYPE.toolCallGroup, - group, - subAgentLabel, - }), - ), + ...subAgentEventSegments.map(({ segment }): TimelineHoverTarget => ({ type: TIMELINE_TYPE.event, segment })), + ...subAgentToolCallGroups.map(({ group, subAgentLabel }): TimelineHoverTarget => ({ + type: TIMELINE_TYPE.toolCallGroup, + group, + subAgentLabel, + })), ...mainEventSegments.map((segment): TimelineHoverTarget | null => { if (segment.type !== 'sub_agent') return { type: TIMELINE_TYPE.event, segment }; const group = subAgentGroups.find(candidate => candidate.barId === segment.id);