Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions static/app/components/core/chat/thinkingBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function ThinkingBlock({title, startTime, endTime, children}: ThinkingBlo
size="sm"
variant="outline"
flex={1}
minWidth={0}
>
<Global styles={streamingAnimationStyles} />
<Disclosure.Title
Expand Down
92 changes: 71 additions & 21 deletions static/app/components/core/chat/toolCall.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: ToolCall
description: A single agent tool call — a quiet read or an expanded query.
description: A single agent tool call — its status, result, timing, and inputs.
category: chat
source: '@sentry/scraps/chat'
resources:
Expand All @@ -16,27 +16,31 @@ import * as Storybook from 'sentry/stories';
export const documentation = import('!!type-loader!@sentry/scraps/chat/toolCall');

The `ToolCall` renders a single step an agent took, within a `ThinkingBlock`.
Like `ThinkingBlock`, it is an outline `Disclosure`: a leading glyph communicates
lifecycle `status` (a spinner while running, a semantic icon once settled — see
`ToolCallIndicator`), the `title` is the toggle, and any `children` expand into
the panel below.
Unlike the collapsible `ThinkingBlock` it lives in, a tool call is **not** itself
a disclosure — it has no chevron and its detail is always visible. A leading
glyph communicates lifecycle `status` (a spinner while running, a semantic icon
once settled — see `ToolCallIndicator`), an optional `reference` chip trails the
title, and a `durationMs` reads right-aligned in the meta slot.

## Anatomy

A tool call collapses to a single title line, with an optional trailing
`reference` chip pointing at the entity the call acted on.
A tool call is a single title line: the status glyph, the `title`, an optional
trailing `reference` chip pointing at the entity the call acted on, and the
elapsed `durationMs`.

<Storybook.Demo align="stretch">
<Container width="100%" background="secondary" radius="md" padding="lg">
<ToolCall
title="Read trace waterfall"
status="success"
reference={{label: 'Trace', value: 'a3805648'}}
durationMs={6400}
/>
<ToolCall
title="Read trace with span details"
status="success"
reference={{label: 'Span', value: 'a469c305'}}
durationMs={6600}
/>
</Container>
</Storybook.Demo>
Expand All @@ -46,20 +50,29 @@ A tool call collapses to a single title line, with an optional trailing
title="Read trace waterfall"
status="success"
reference={{label: 'Trace', value: 'a3805648'}}
durationMs={6400}
/>
```

## Output
## Input

When a call produces a primary result, pass `output` to surface it as a chip
under an `Output:` label.
Pass `input` to render the call's request under an `Input:` label. It is a slot,
so give it a decomposed view — typically a `FormattedQuery`, which parses a
Sentry search string (including boolean and parenthesized grouping) into query
chips.

<Storybook.Demo align="stretch">
<Container width="100%" background="secondary" radius="md" padding="lg">
<ToolCall
title="Query spans"
status="success"
output={{label: 'Trace', value: 'a3805648'}}
reference={{label: 'Trace', value: 'a3805648'}}
durationMs={9400}
input={
<Text size="sm" monospace>
dataset:spans project:ml-service span.description:DSL
</Text>
}
/>
</Container>
</Storybook.Demo>
Expand All @@ -68,20 +81,58 @@ under an `Output:` label.
<ToolCall
title="Query spans"
status="success"
output={{label: 'Trace', value: 'a3805648'}}
input={<FormattedQuery query="dataset:spans project:ml-service" />}
/>
```

## Output

Pass `output` to render the call's result under an `Output:` label — a result
value, or on failure the error itself. Like `input`, it is a slot.

<Storybook.Demo align="stretch">
<Container width="100%" background="secondary" radius="md" padding="lg">
<ToolCall
title="Query spans"
status="failure"
failureLabel="502"
output={
<Text size="sm" variant="danger" monospace>
Returned HTTP 502
</Text>
}
/>
</Container>
</Storybook.Demo>

```jsx
<ToolCall
title="Query spans"
status="failure"
failureLabel="502"
output={<Text variant="danger">Returned HTTP 502</Text>}
/>
```

## Status

The leading glyph reflects the call's lifecycle.
The leading glyph reflects the call's lifecycle. A `failure` keeps that glyph and
additionally hoists a chip into the trailing result slot — where a successful
call shows its `reference` — carrying the `failureLabel` (typically the HTTP
status code, e.g. `502`), so the outcome reads on the right rather than only as a
small glyph on the far left.

<Storybook.Demo align="stretch">
<Container width="100%" background="secondary" radius="md" padding="lg">
<ToolCall title="Running query" status="loading" />
<ToolCall title="Awaiting approval" status="pending" />
<ToolCall title="Query succeeded" status="success" />
<ToolCall title="Query failed" status="failure" />
<ToolCall title="Query succeeded" status="success" durationMs={9400} />
<ToolCall
title="Query failed"
status="failure"
failureLabel="502"
durationMs={9400}
/>
</Container>
</Storybook.Demo>

Expand All @@ -101,9 +152,8 @@ Pass `notifications` to surface short status lines beneath a call.

## Links

Give a `reference` or `output` chip a `to` to render it as a real link (an
anchor supporting middle/cmd-click and keyboard access) rather than an
`onClick` button.
Give a `reference` chip a `to` to render it as a real link (an anchor supporting
middle/cmd-click and keyboard access) rather than an `onClick` button.

<Storybook.Demo align="stretch">
<Container width="100%" background="secondary" radius="md" padding="lg">
Expand All @@ -125,9 +175,9 @@ anchor supporting middle/cmd-click and keyboard access) rather than an

## Detail

Pass `children` to tuck supplementary detail beneath the call — for example an
expandable request/response. It lives in the collapsible panel, revealed by
toggling the title.
Pass `children` to tuck supplementary detail beneath the call — for example a
request body. It renders inline, indented under the title; a tool call has no
disclosure of its own.

<Storybook.Demo align="stretch">
<Container width="100%" background="secondary" radius="md" padding="lg">
Expand Down
55 changes: 42 additions & 13 deletions static/app/components/core/chat/toolCall.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {render, screen} from 'sentry-test/reactTestingLibrary';

import {ToolCall} from '@sentry/scraps/chat';

Expand All @@ -17,18 +17,18 @@ describe('ToolCall', () => {
expect(screen.queryByText('Output:')).not.toBeInTheDocument();
});

it('renders an output chip when output is provided', () => {
it('renders an output slot under an Output label', () => {
render(
<ToolCall
title="Query spans"
status="success"
output={{label: 'Trace', value: 'a3805648'}}
status="failure"
output={<span>Returned HTTP 502</span>}
/>
);

expect(screen.getByText('Query spans')).toBeInTheDocument();
expect(screen.getByText('Output:')).toBeInTheDocument();
expect(screen.getByText('a3805648')).toBeInTheDocument();
expect(screen.getByText('Returned HTTP 502')).toBeInTheDocument();
});

it('communicates status via the leading glyph', () => {
Expand All @@ -42,6 +42,39 @@ describe('ToolCall', () => {
expect(screen.getByLabelText('Running')).toBeInTheDocument();
});

it('keeps the leading glyph and hoists a Failed chip beside the result on failure', () => {
render(<ToolCall title="Query spans" status="failure" />);

// Leading glyph (accessible label) plus a visible trailing chip.
expect(screen.getByLabelText('Failed')).toBeInTheDocument();
expect(screen.getByText('Failed')).toBeInTheDocument();
});

it('shows the failureLabel (e.g. HTTP status) in the trailing chip', () => {
render(<ToolCall title="Query spans" status="failure" failureLabel="502" />);

expect(screen.getByText('502')).toBeInTheDocument();
expect(screen.queryByText('Failed')).not.toBeInTheDocument();
});

it('renders a duration in the meta slot', () => {
render(<ToolCall title="Query spans" status="success" durationMs={9400} />);
expect(screen.getByText('9.4s')).toBeInTheDocument();
});

it('renders the input slot under an Input label', () => {
render(
<ToolCall
title="Query spans"
status="success"
input={<span>dataset is spans</span>}
/>
);

expect(screen.getByText('Input:')).toBeInTheDocument();
expect(screen.getByText('dataset is spans')).toBeInTheDocument();
});

it('surfaces notifications', () => {
render(
<ToolCall
Expand Down Expand Up @@ -69,19 +102,15 @@ describe('ToolCall', () => {
);
});

it('reveals supplementary detail children when expanded', async () => {
it('renders supplementary detail children inline, always visible', () => {
render(
<ToolCall title="Query spans" status="success">
<div>GET /api/0/traces/a3805648/</div>
</ToolCall>
);

// 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();
});
});
Loading
Loading