GET /api/0/traces/a3805648/
);
- // Detail lives in the collapsible panel, so it is hidden until the title is toggled.
- const detail = screen.getByText('GET /api/0/traces/a3805648/');
- expect(detail).not.toBeVisible();
-
- await userEvent.click(screen.getByRole('button', {name: /Query spans/}));
-
- expect(detail).toBeVisible();
+ // A tool call is not a disclosure: its detail is not tucked behind a toggle.
+ expect(screen.getByText('GET /api/0/traces/a3805648/')).toBeVisible();
+ expect(screen.queryByRole('button', {name: /Query spans/})).not.toBeInTheDocument();
});
});
diff --git a/static/app/components/core/chat/toolCall.tsx b/static/app/components/core/chat/toolCall.tsx
index 63a5ebfa4c71..778871fb5771 100644
--- a/static/app/components/core/chat/toolCall.tsx
+++ b/static/app/components/core/chat/toolCall.tsx
@@ -2,12 +2,13 @@ import type {MouseEvent, ReactNode} from 'react';
import type {LocationDescriptor} from 'history';
import {Button, LinkButton} from '@sentry/scraps/button';
-import {Disclosure} from '@sentry/scraps/disclosure';
-import {Container, Flex} from '@sentry/scraps/layout';
+import {Container, Flex, Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
import {IconSpan} from 'sentry/icons';
import {t} from 'sentry/locale';
+import {getDuration} from 'sentry/utils/duration/getDuration';
+import {SECOND} from 'sentry/utils/formatters';
import {unreachable} from 'sentry/utils/unreachable';
import {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator';
@@ -46,7 +47,8 @@ export interface ToolCallReference {
interface ToolCallProps {
/**
* Lifecycle status. Drives the leading glyph (spinner while running, semantic
- * icon once settled) via `ToolCallIndicator`.
+ * icon once settled) via `ToolCallIndicator`. A `failure` also surfaces a
+ * trailing `Failed` chip in the result area so the outcome reads on the right.
*/
status: ToolCallStatus;
/**
@@ -54,26 +56,47 @@ interface ToolCallProps {
*/
title: string;
/**
- * Supplementary detail rendered beneath the title and output — e.g. an
- * expandable request/response for the call. Kept in the title's column so it
- * aligns under the headline rather than the status glyph.
+ * Supplementary detail rendered beneath the title — e.g. the request body.
+ * Always visible: a nested tool call has no disclosure of its own.
*/
children?: ReactNode;
+ /**
+ * How long the call took, in milliseconds. Rendered right-aligned in the
+ * trailing meta slot. Omit when the duration is unknown.
+ */
+ durationMs?: number;
+ /**
+ * The trailing danger chip's text when `status` is `failure` (e.g. the HTTP
+ * status code `502`). Defaults to `Failed`.
+ */
+ failureLabel?: string;
+ /**
+ * The call's request, rendered under an `Input:` label. Pass a decomposed
+ * view (e.g. a `FormattedQuery`) so the request reads as its filters rather
+ * than a raw URL string.
+ */
+ input?: ReactNode;
/**
* Short status lines surfaced beneath the call (e.g. "Truncated to 100 rows").
*/
notifications?: string[];
/**
- * The primary result of the call, rendered as a chip under an `Output:` label.
+ * The call's result, rendered under an `Output:` label — a result value, or on
+ * failure the error itself. A slot, mirroring `input`.
*/
- output?: ToolCallReference;
+ output?: ReactNode;
/**
* A trailing chip shown inline with the title. Typically the entity the call
- * acted on.
+ * acted on — the call's result.
*/
reference?: ToolCallReference;
}
+// The leading status glyph and the indent of every row beneath the title are
+// pinned to this width so detail (input chips, notifications, children) aligns
+// under the headline rather than under the glyph.
+const GLYPH_SLOT_WIDTH = '16px';
+
function ChipContent({label, value}: {value: string; label?: string}) {
return label ? (