From d8d2389621fe2dc527725d9ab167ce6afeb2916a Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 13 Jul 2026 10:44:31 +0200 Subject: [PATCH 01/14] feat(scraps): Add chat primitives (Spinner, ChatMessageBubble, ToolCallStatusIcon) First step of extracting Seer Explorer's chat block components into reusable building blocks so Dashboards AI, Autofix, and other agent surfaces can share them instead of importing straight from seerExplorer. This adds the presentational, dependency-free primitives under @sentry/scraps/chat: - Spinner: small inline activity spinner (distinct from the full-width IndeterminateLoader) with a size prop and reduced-motion handling. - ChatMessageBubble: a single message bubble for the sender's own messages, presentation only (caller handles alignment). - ToolCallStatusIcon: status -> icon/spinner + tooltip for a group of tool calls, with default labels and an override. These carry no analytics, feature flags, markdown renderer, or seerExplorer utils/types; that behavior is injected by the composite layer in a follow-up. Each primitive ships an MDX story. The action-bar shell is intentionally left out of scraps since it composes from the existing RevealOnHover + ButtonBar. Co-Authored-By: Claude --- .../core/chat/chatMessageBubble.mdx | 62 +++++++++++ .../core/chat/chatMessageBubble.tsx | 42 +++++++ static/app/components/core/chat/index.tsx | 3 + static/app/components/core/chat/spinner.mdx | 73 ++++++++++++ static/app/components/core/chat/spinner.tsx | 48 ++++++++ .../core/chat/toolCallStatusIcon.mdx | 71 ++++++++++++ .../core/chat/toolCallStatusIcon.tsx | 104 ++++++++++++++++++ 7 files changed, 403 insertions(+) create mode 100644 static/app/components/core/chat/chatMessageBubble.mdx create mode 100644 static/app/components/core/chat/chatMessageBubble.tsx create mode 100644 static/app/components/core/chat/index.tsx create mode 100644 static/app/components/core/chat/spinner.mdx create mode 100644 static/app/components/core/chat/spinner.tsx create mode 100644 static/app/components/core/chat/toolCallStatusIcon.mdx create mode 100644 static/app/components/core/chat/toolCallStatusIcon.tsx diff --git a/static/app/components/core/chat/chatMessageBubble.mdx b/static/app/components/core/chat/chatMessageBubble.mdx new file mode 100644 index 000000000000..e76d7d900062 --- /dev/null +++ b/static/app/components/core/chat/chatMessageBubble.mdx @@ -0,0 +1,62 @@ +--- +title: ChatMessageBubble +description: A single message bubble for the sender's own messages in an agent conversation. +category: display +source: '@sentry/scraps/chat' +resources: + js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/chatMessageBubble.tsx +--- + +import {Flex} from '@sentry/scraps/layout'; +import {ChatMessageBubble} from '@sentry/scraps/chat'; + +import * as Storybook from 'sentry/stories'; + +export const documentation = + import('!!type-loader!@sentry/scraps/chat/chatMessageBubble'); + +The `ChatMessageBubble` renders a single message bubble, styled for the +sender's own messages in an agent conversation (Seer Explorer, Dashboards AI, +Autofix, …). It is presentation only — alignment within the conversation is the +caller's responsibility. + +## Usage + +Wrap the bubble in a right-aligned row for user messages so it sits opposite the +agent's responses. + + + + How many errors happened in the last 24 hours? + + + +```jsx + + How many errors happened in the last 24 hours? + +``` + +## Width + +The bubble grows to fit its content up to `maxWidth` (default `80%`), so it +never spans the full conversation width. Override `maxWidth` for narrower or +wider surfaces. + + + + + + A longer question that wraps onto multiple lines once it reaches the max width of + the bubble. + + + + + +```jsx + +``` + +Whitespace and long unbroken tokens are preserved and wrapped, so pasted URLs +or stack frames won't overflow the bubble. diff --git a/static/app/components/core/chat/chatMessageBubble.tsx b/static/app/components/core/chat/chatMessageBubble.tsx new file mode 100644 index 000000000000..2630dc4ffc99 --- /dev/null +++ b/static/app/components/core/chat/chatMessageBubble.tsx @@ -0,0 +1,42 @@ +import styled from '@emotion/styled'; + +interface ChatMessageBubbleProps extends React.HTMLAttributes { + children: React.ReactNode; + /** + * Caps how wide the bubble can grow relative to its container. Defaults to + * `80%` so a bubble never spans the full conversation width. + */ + maxWidth?: React.CSSProperties['maxWidth']; +} + +/** + * A single chat message bubble, styled for the sender's own messages in an + * agent conversation (Seer Explorer, Dashboards AI, Autofix, …). + * + * Presentation only — alignment within the conversation is the caller's + * responsibility (wrap it in a right-aligned row for user messages). + */ +export function ChatMessageBubble({ + children, + maxWidth = '80%', + ...props +}: ChatMessageBubbleProps) { + return ( + + {children} + + ); +} + +const Bubble = styled('div')<{maxWidth: React.CSSProperties['maxWidth']}>` + max-width: ${p => p.maxWidth}; + min-width: 0; + padding: ${p => p.theme.space.xs} ${p => p.theme.space.md}; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: anywhere; + color: ${p => p.theme.tokens.content.primary}; + background: ${p => p.theme.tokens.background.secondary}; + border: 1px solid ${p => p.theme.tokens.border.primary}; + border-radius: ${p => p.theme.radius.md}; +`; diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx new file mode 100644 index 000000000000..79a1fd8041d0 --- /dev/null +++ b/static/app/components/core/chat/index.tsx @@ -0,0 +1,3 @@ +export {ChatMessageBubble} from './chatMessageBubble'; +export {Spinner} from './spinner'; +export {ToolCallStatusIcon, type ToolCallStatus} from './toolCallStatusIcon'; diff --git a/static/app/components/core/chat/spinner.mdx b/static/app/components/core/chat/spinner.mdx new file mode 100644 index 000000000000..6a04375997d2 --- /dev/null +++ b/static/app/components/core/chat/spinner.mdx @@ -0,0 +1,73 @@ +--- +title: Spinner +description: A small, inline activity spinner for chat and agent surfaces. +category: status +source: '@sentry/scraps/chat' +resources: + js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/spinner.tsx +--- + +import {Flex} from '@sentry/scraps/layout'; +import {Spinner} from '@sentry/scraps/chat'; +import {Text} from '@sentry/scraps/text'; + +import * as Storybook from 'sentry/stories'; + +export const documentation = import('!!type-loader!@sentry/scraps/chat/spinner'); + +The `Spinner` is a small circular activity indicator for agent conversation +surfaces — shown next to a message that is streaming or a tool call that is +still running. For full-width, indeterminate progress use the +[`IndeterminateLoader`](?name=IndeterminateLoader) instead. + +## Sizes + + + + + + + + + +```jsx + + + +``` + +## Usage + + + + + Running… + + + +```jsx + + + Running… + +``` + +## Accessibility + +The spinner is decorative by default (`aria-hidden`) — reach for that when +adjacent text already communicates that work is in progress. When the spinner +is the only signal, pass an `aria-label` (and `role="status"`) so screen +readers announce it. + +```jsx +// Decorative — the label already says what is happening + + + Running… + + +// Meaningful — the spinner is the only signal + +``` + +When the user prefers reduced motion, the spinner slows to a gentle rotation. diff --git a/static/app/components/core/chat/spinner.tsx b/static/app/components/core/chat/spinner.tsx new file mode 100644 index 000000000000..d5e5e7b6ade7 --- /dev/null +++ b/static/app/components/core/chat/spinner.tsx @@ -0,0 +1,48 @@ +import {keyframes} from '@emotion/react'; +import styled from '@emotion/styled'; + +interface SpinnerProps extends React.HTMLAttributes { + /** + * Diameter of the spinner in pixels. + */ + size?: number; +} + +/** + * A small, inline activity spinner for chat and agent surfaces — used next to a + * message that is streaming or a tool call that is still running. + * + * Decorative by default (`aria-hidden`). Pass an `aria-label` (and typically + * `role="status"`) when the spinner is the only signal that work is in progress. + */ +export function Spinner({size = 12, ...props}: SpinnerProps) { + return ( + + ); +} + +const spin = keyframes` + to { + transform: rotate(360deg); + } +`; + +const Ring = styled('span')<{size: number}>` + box-sizing: border-box; + display: inline-block; + width: ${p => p.size}px; + height: ${p => p.size}px; + border-radius: ${p => p.theme.radius.full}; + border: 1.5px solid ${p => p.theme.tokens.border.primary}; + border-left-color: ${p => p.theme.tokens.border.accent.vibrant}; + animation: ${spin} 0.6s linear infinite; + flex-shrink: 0; + + @media (prefers-reduced-motion: reduce) { + animation-duration: 2.4s; + } +`; diff --git a/static/app/components/core/chat/toolCallStatusIcon.mdx b/static/app/components/core/chat/toolCallStatusIcon.mdx new file mode 100644 index 000000000000..9b590a9de408 --- /dev/null +++ b/static/app/components/core/chat/toolCallStatusIcon.mdx @@ -0,0 +1,71 @@ +--- +title: ToolCallStatusIcon +description: A compact status icon for a group of agent tool calls. +category: status +source: '@sentry/scraps/chat' +resources: + js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/toolCallStatusIcon.tsx +--- + +import {Flex, Grid} from '@sentry/scraps/layout'; +import {ToolCallStatusIcon} from '@sentry/scraps/chat'; +import {Text} from '@sentry/scraps/text'; + +import * as Storybook from 'sentry/stories'; + +export const documentation = + import('!!type-loader!@sentry/scraps/chat/toolCallStatusIcon'); + +The `ToolCallStatusIcon` communicates the aggregate status of a message's tool +calls in an agent conversation: a spinner while they run and a semantic icon +once they settle. Shared across agent surfaces (Seer Explorer, Dashboards AI, +Autofix, …). + +## Statuses + + + + + loading — work in progress + + + pending — waiting for approval + + + success — all tool calls succeeded + + + mixed — some succeeded, some failed + + + failure — all tool calls failed + + + + +```jsx + + + + + +``` + +The `content` status (a message with no tool calls, only content) renders +nothing, so it is safe to pass a status derived from any message. + +## Labels + +Each status carries a default tooltip and accessible label. Override it with the +`label` prop when the surrounding context needs more specific wording. + + + + + Custom label + + + +```jsx + +``` diff --git a/static/app/components/core/chat/toolCallStatusIcon.tsx b/static/app/components/core/chat/toolCallStatusIcon.tsx new file mode 100644 index 000000000000..8f8b63701ec2 --- /dev/null +++ b/static/app/components/core/chat/toolCallStatusIcon.tsx @@ -0,0 +1,104 @@ +import {Text} from '@sentry/scraps/text'; +import {Tooltip} from '@sentry/scraps/tooltip'; + +import {IconCheckmark, IconClose, IconWarning} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {unreachable} from 'sentry/utils/unreachable'; + +import {Spinner} from './spinner'; + +/** + * Aggregate status of a message's tool calls. + * + * - `loading` / `pending`: work is in progress (spinner) + * - `success` / `failure` / `mixed`: terminal outcomes (icon) + * - `content`: the message has no tool calls, only content — renders nothing + */ +export type ToolCallStatus = + | 'loading' + | 'pending' + | 'success' + | 'failure' + | 'mixed' + | 'content'; + +interface ToolCallStatusIconProps { + status: ToolCallStatus; + /** + * Overrides the default tooltip / accessible label for the status. + */ + label?: string; +} + +/** + * A compact status icon for a group of agent tool calls: a spinner while they + * run and a semantic icon once they settle. Shared across agent conversation + * surfaces (Seer Explorer, Dashboards AI, Autofix, …). + * + * Placement and sizing of the surrounding slot are the caller's responsibility. + */ +export function ToolCallStatusIcon({status, label}: ToolCallStatusIconProps) { + const defaultLabel = getDefaultLabel(status); + + if (status === 'content') { + return null; + } + + const title = label ?? defaultLabel; + + return ( + + + + ); +} + +function ToolCallStatusGlyph({status, label}: {label: string; status: ToolCallStatus}) { + switch (status) { + case 'loading': + case 'pending': + return ; + case 'failure': + return ( + + + + ); + case 'mixed': + return ( + + + + ); + case 'success': + return ( + + + + ); + case 'content': + return null; + default: + return unreachable(status); + } +} + +function getDefaultLabel(status: ToolCallStatus): string { + switch (status) { + case 'loading': + return t('Running...'); + case 'pending': + return t('Waiting for approval'); + case 'failure': + return t('All tool calls failed'); + case 'mixed': + return t('Some tool calls succeeded and some failed'); + case 'success': + return t('All tool calls succeeded'); + case 'content': + return ''; + default: + unreachable(status); + return ''; + } +} From 4f63a56fb5773f7f48b47794cd8f555228be669a Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 13 Jul 2026 12:13:59 +0200 Subject: [PATCH 02/14] ref(scraps): Build ChatMessageBubble on Container instead of styled div Prefer the Container layout primitive over a hand-rolled styled div, per the design-system convention. Background, border, radius, padding, max-width, min-width, and white-space now come from Container props; only overflow-wrap and the text color token remain in a minimal css escape hatch since Container does not expose them. Co-Authored-By: Claude --- .../core/chat/chatMessageBubble.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/static/app/components/core/chat/chatMessageBubble.tsx b/static/app/components/core/chat/chatMessageBubble.tsx index 2630dc4ffc99..5235e59ec8e5 100644 --- a/static/app/components/core/chat/chatMessageBubble.tsx +++ b/static/app/components/core/chat/chatMessageBubble.tsx @@ -1,4 +1,6 @@ -import styled from '@emotion/styled'; +import {css} from '@emotion/react'; + +import {Container} from '@sentry/scraps/layout'; interface ChatMessageBubbleProps extends React.HTMLAttributes { children: React.ReactNode; @@ -22,21 +24,26 @@ export function ChatMessageBubble({ ...props }: ChatMessageBubbleProps) { return ( - + {children} - + ); } -const Bubble = styled('div')<{maxWidth: React.CSSProperties['maxWidth']}>` - max-width: ${p => p.maxWidth}; - min-width: 0; - padding: ${p => p.theme.space.xs} ${p => p.theme.space.md}; - white-space: pre-wrap; +// `overflow-wrap`/`word-wrap` are not layout props; they keep long unbroken +// tokens (URLs, stack frames) from overflowing the bubble. +const wrapStyles = (theme: import('@emotion/react').Theme) => css` + color: ${theme.tokens.content.primary}; word-wrap: break-word; overflow-wrap: anywhere; - color: ${p => p.theme.tokens.content.primary}; - background: ${p => p.theme.tokens.background.secondary}; - border: 1px solid ${p => p.theme.tokens.border.primary}; - border-radius: ${p => p.theme.radius.md}; `; From 0f1b28462f9bcde609f3f6e4184824f8dc5f88b1 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 13 Jul 2026 14:26:04 +0200 Subject: [PATCH 03/14] ref(scraps): Group chat stories and drop product names from primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dedicated "Chat" story category so ChatMessageBubble, Spinner, and ToolCallStatusIcon group together instead of scattering across Display and Status, mirroring how MUI/Chakra give the chat kit its own section. Also strip the (Seer Explorer, Dashboards AI, Autofix, …) product enumeration from the primitives' docs and JSDoc. The primitives are product-agnostic and should not name the surfaces that consume them; the enumeration was also copy-pasted across files and would drift. Co-Authored-By: Claude --- static/app/components/core/chat/chatMessageBubble.mdx | 7 +++---- static/app/components/core/chat/chatMessageBubble.tsx | 2 +- static/app/components/core/chat/spinner.mdx | 2 +- static/app/components/core/chat/toolCallStatusIcon.mdx | 5 ++--- static/app/components/core/chat/toolCallStatusIcon.tsx | 3 +-- static/app/stories/frontmatter.ts | 1 + static/app/stories/view/storyTree.tsx | 2 ++ 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/static/app/components/core/chat/chatMessageBubble.mdx b/static/app/components/core/chat/chatMessageBubble.mdx index e76d7d900062..9aa5aa17e1de 100644 --- a/static/app/components/core/chat/chatMessageBubble.mdx +++ b/static/app/components/core/chat/chatMessageBubble.mdx @@ -1,7 +1,7 @@ --- title: ChatMessageBubble description: A single message bubble for the sender's own messages in an agent conversation. -category: display +category: chat source: '@sentry/scraps/chat' resources: js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/chatMessageBubble.tsx @@ -16,9 +16,8 @@ export const documentation = import('!!type-loader!@sentry/scraps/chat/chatMessageBubble'); The `ChatMessageBubble` renders a single message bubble, styled for the -sender's own messages in an agent conversation (Seer Explorer, Dashboards AI, -Autofix, …). It is presentation only — alignment within the conversation is the -caller's responsibility. +sender's own messages in an agent conversation. It is presentation only — +alignment within the conversation is the caller's responsibility. ## Usage diff --git a/static/app/components/core/chat/chatMessageBubble.tsx b/static/app/components/core/chat/chatMessageBubble.tsx index 5235e59ec8e5..11476c7261ad 100644 --- a/static/app/components/core/chat/chatMessageBubble.tsx +++ b/static/app/components/core/chat/chatMessageBubble.tsx @@ -13,7 +13,7 @@ interface ChatMessageBubbleProps extends React.HTMLAttributes { /** * A single chat message bubble, styled for the sender's own messages in an - * agent conversation (Seer Explorer, Dashboards AI, Autofix, …). + * agent conversation. * * Presentation only — alignment within the conversation is the caller's * responsibility (wrap it in a right-aligned row for user messages). diff --git a/static/app/components/core/chat/spinner.mdx b/static/app/components/core/chat/spinner.mdx index 6a04375997d2..f8b983da57a9 100644 --- a/static/app/components/core/chat/spinner.mdx +++ b/static/app/components/core/chat/spinner.mdx @@ -1,7 +1,7 @@ --- title: Spinner description: A small, inline activity spinner for chat and agent surfaces. -category: status +category: chat source: '@sentry/scraps/chat' resources: js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/spinner.tsx diff --git a/static/app/components/core/chat/toolCallStatusIcon.mdx b/static/app/components/core/chat/toolCallStatusIcon.mdx index 9b590a9de408..4154ca8d44cc 100644 --- a/static/app/components/core/chat/toolCallStatusIcon.mdx +++ b/static/app/components/core/chat/toolCallStatusIcon.mdx @@ -1,7 +1,7 @@ --- title: ToolCallStatusIcon description: A compact status icon for a group of agent tool calls. -category: status +category: chat source: '@sentry/scraps/chat' resources: js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/toolCallStatusIcon.tsx @@ -18,8 +18,7 @@ export const documentation = The `ToolCallStatusIcon` communicates the aggregate status of a message's tool calls in an agent conversation: a spinner while they run and a semantic icon -once they settle. Shared across agent surfaces (Seer Explorer, Dashboards AI, -Autofix, …). +once they settle. ## Statuses diff --git a/static/app/components/core/chat/toolCallStatusIcon.tsx b/static/app/components/core/chat/toolCallStatusIcon.tsx index 8f8b63701ec2..39d0d289fa79 100644 --- a/static/app/components/core/chat/toolCallStatusIcon.tsx +++ b/static/app/components/core/chat/toolCallStatusIcon.tsx @@ -32,8 +32,7 @@ interface ToolCallStatusIconProps { /** * A compact status icon for a group of agent tool calls: a spinner while they - * run and a semantic icon once they settle. Shared across agent conversation - * surfaces (Seer Explorer, Dashboards AI, Autofix, …). + * run and a semantic icon once they settle. * * Placement and sizing of the surrounding slot are the caller's responsibility. */ diff --git a/static/app/stories/frontmatter.ts b/static/app/stories/frontmatter.ts index 02e4b15502e7..63689b2738c8 100644 --- a/static/app/stories/frontmatter.ts +++ b/static/app/stories/frontmatter.ts @@ -9,6 +9,7 @@ type ComponentCategory = | 'navigation' | 'status' | 'display' + | 'chat' | 'overlays' | 'utilities' | 'shared'; diff --git a/static/app/stories/view/storyTree.tsx b/static/app/stories/view/storyTree.tsx index c593e0cdc6cd..5aa8ee7e4808 100644 --- a/static/app/stories/view/storyTree.tsx +++ b/static/app/stories/view/storyTree.tsx @@ -157,6 +157,7 @@ export const COMPONENT_SUBCATEGORY_CONFIG: Record< navigation: {label: 'Navigation'}, status: {label: 'Status'}, display: {label: 'Display'}, + chat: {label: 'Chat'}, overlays: {label: 'Overlays'}, utilities: {label: 'Utilities'}, shared: {label: 'Shared'}, @@ -179,6 +180,7 @@ const COMPONENT_SUBCATEGORY_ORDER: ComponentSubcategory[] = [ 'navigation', 'status', 'display', + 'chat', 'overlays', 'utilities', 'shared', From 550454fba469018b3759fdbd01ed5ed8fc97ca16 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Jul 2026 10:53:40 +0200 Subject: [PATCH 04/14] ref(scraps): Keep chat Spinner internal to ToolCallStatusIcon Stop exporting Spinner and drop its story. It stays as ToolCallStatusIcon's running-state helper, but is no longer a public primitive. The general small-spinner vs LoadingIndicator consolidation is a separate, already-designed effort (Loading States patterns doc); shipping a public Spinner here would front-run it and risk the exact "which one?" ambiguity we want that work to resolve. Co-Authored-By: Claude --- static/app/components/core/chat/index.tsx | 1 - static/app/components/core/chat/spinner.mdx | 73 --------------------- static/app/components/core/chat/spinner.tsx | 3 + 3 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 static/app/components/core/chat/spinner.mdx diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx index 79a1fd8041d0..468c02ce9554 100644 --- a/static/app/components/core/chat/index.tsx +++ b/static/app/components/core/chat/index.tsx @@ -1,3 +1,2 @@ export {ChatMessageBubble} from './chatMessageBubble'; -export {Spinner} from './spinner'; export {ToolCallStatusIcon, type ToolCallStatus} from './toolCallStatusIcon'; diff --git a/static/app/components/core/chat/spinner.mdx b/static/app/components/core/chat/spinner.mdx deleted file mode 100644 index f8b983da57a9..000000000000 --- a/static/app/components/core/chat/spinner.mdx +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Spinner -description: A small, inline activity spinner for chat and agent surfaces. -category: chat -source: '@sentry/scraps/chat' -resources: - js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/spinner.tsx ---- - -import {Flex} from '@sentry/scraps/layout'; -import {Spinner} from '@sentry/scraps/chat'; -import {Text} from '@sentry/scraps/text'; - -import * as Storybook from 'sentry/stories'; - -export const documentation = import('!!type-loader!@sentry/scraps/chat/spinner'); - -The `Spinner` is a small circular activity indicator for agent conversation -surfaces — shown next to a message that is streaming or a tool call that is -still running. For full-width, indeterminate progress use the -[`IndeterminateLoader`](?name=IndeterminateLoader) instead. - -## Sizes - - - - - - - - - -```jsx - - - -``` - -## Usage - - - - - Running… - - - -```jsx - - - Running… - -``` - -## Accessibility - -The spinner is decorative by default (`aria-hidden`) — reach for that when -adjacent text already communicates that work is in progress. When the spinner -is the only signal, pass an `aria-label` (and `role="status"`) so screen -readers announce it. - -```jsx -// Decorative — the label already says what is happening - - - Running… - - -// Meaningful — the spinner is the only signal - -``` - -When the user prefers reduced motion, the spinner slows to a gentle rotation. diff --git a/static/app/components/core/chat/spinner.tsx b/static/app/components/core/chat/spinner.tsx index d5e5e7b6ade7..cd48b2adb11e 100644 --- a/static/app/components/core/chat/spinner.tsx +++ b/static/app/components/core/chat/spinner.tsx @@ -12,6 +12,9 @@ interface SpinnerProps extends React.HTMLAttributes { * A small, inline activity spinner for chat and agent surfaces — used next to a * message that is streaming or a tool call that is still running. * + * Internal to the chat primitives (not exported) — the general spinner/loading + * consolidation is tracked separately. + * * Decorative by default (`aria-hidden`). Pass an `aria-label` (and typically * `role="status"`) when the spinner is the only signal that work is in progress. */ From 622857ad24babb4c344b03700e744aa5009dd247 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Jul 2026 12:24:24 +0200 Subject: [PATCH 05/14] ref(scraps): Rename chat primitives to MessageBubble and ToolCallIndicator Drop the redundant "Chat" prefix (the components are already namespaced under @sentry/scraps/chat) and use an *Indicator suffix instead of *Icon, since the component renders a spinner or an icon depending on state. Mirrors MUI X Chat's naming. No behavior change; the ToolCallStatus type name is unchanged. Co-Authored-By: Claude --- static/app/components/core/chat/index.tsx | 4 +-- ...hatMessageBubble.mdx => messageBubble.mdx} | 21 ++++++------ ...hatMessageBubble.tsx => messageBubble.tsx} | 6 ++-- ...llStatusIcon.mdx => toolCallIndicator.mdx} | 34 +++++++++---------- ...llStatusIcon.tsx => toolCallIndicator.tsx} | 6 ++-- 5 files changed, 35 insertions(+), 36 deletions(-) rename static/app/components/core/chat/{chatMessageBubble.mdx => messageBubble.mdx} (68%) rename static/app/components/core/chat/{chatMessageBubble.tsx => messageBubble.tsx} (89%) rename static/app/components/core/chat/{toolCallStatusIcon.mdx => toolCallIndicator.mdx} (60%) rename static/app/components/core/chat/{toolCallStatusIcon.tsx => toolCallIndicator.tsx} (92%) diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx index 468c02ce9554..59dc249c0893 100644 --- a/static/app/components/core/chat/index.tsx +++ b/static/app/components/core/chat/index.tsx @@ -1,2 +1,2 @@ -export {ChatMessageBubble} from './chatMessageBubble'; -export {ToolCallStatusIcon, type ToolCallStatus} from './toolCallStatusIcon'; +export {MessageBubble} from './messageBubble'; +export {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator'; diff --git a/static/app/components/core/chat/chatMessageBubble.mdx b/static/app/components/core/chat/messageBubble.mdx similarity index 68% rename from static/app/components/core/chat/chatMessageBubble.mdx rename to static/app/components/core/chat/messageBubble.mdx index 9aa5aa17e1de..0fd6236f6fc5 100644 --- a/static/app/components/core/chat/chatMessageBubble.mdx +++ b/static/app/components/core/chat/messageBubble.mdx @@ -1,21 +1,20 @@ --- -title: ChatMessageBubble +title: MessageBubble description: A single message bubble for the sender's own messages in an agent conversation. category: chat source: '@sentry/scraps/chat' resources: - js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/chatMessageBubble.tsx + js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/messageBubble.tsx --- import {Flex} from '@sentry/scraps/layout'; -import {ChatMessageBubble} from '@sentry/scraps/chat'; +import {MessageBubble} from '@sentry/scraps/chat'; import * as Storybook from 'sentry/stories'; -export const documentation = - import('!!type-loader!@sentry/scraps/chat/chatMessageBubble'); +export const documentation = import('!!type-loader!@sentry/scraps/chat/messageBubble'); -The `ChatMessageBubble` renders a single message bubble, styled for the +The `MessageBubble` renders a single message bubble, styled for the sender's own messages in an agent conversation. It is presentation only — alignment within the conversation is the caller's responsibility. @@ -26,13 +25,13 @@ agent's responses. - How many errors happened in the last 24 hours? + How many errors happened in the last 24 hours? ```jsx - How many errors happened in the last 24 hours? + How many errors happened in the last 24 hours? ``` @@ -45,16 +44,16 @@ wider surfaces. - + A longer question that wraps onto multiple lines once it reaches the max width of the bubble. - + ```jsx - + ``` Whitespace and long unbroken tokens are preserved and wrapped, so pasted URLs diff --git a/static/app/components/core/chat/chatMessageBubble.tsx b/static/app/components/core/chat/messageBubble.tsx similarity index 89% rename from static/app/components/core/chat/chatMessageBubble.tsx rename to static/app/components/core/chat/messageBubble.tsx index 11476c7261ad..501302bb3926 100644 --- a/static/app/components/core/chat/chatMessageBubble.tsx +++ b/static/app/components/core/chat/messageBubble.tsx @@ -2,7 +2,7 @@ import {css} from '@emotion/react'; import {Container} from '@sentry/scraps/layout'; -interface ChatMessageBubbleProps extends React.HTMLAttributes { +interface MessageBubbleProps extends React.HTMLAttributes { children: React.ReactNode; /** * Caps how wide the bubble can grow relative to its container. Defaults to @@ -18,11 +18,11 @@ interface ChatMessageBubbleProps extends React.HTMLAttributes { * Presentation only — alignment within the conversation is the caller's * responsibility (wrap it in a right-aligned row for user messages). */ -export function ChatMessageBubble({ +export function MessageBubble({ children, maxWidth = '80%', ...props -}: ChatMessageBubbleProps) { +}: MessageBubbleProps) { return ( - + loading — work in progress - + pending — waiting for approval - + success — all tool calls succeeded - + mixed — some succeeded, some failed - + failure — all tool calls failed ```jsx - - - - - + + + + + ``` The `content` status (a message with no tool calls, only content) renders @@ -60,11 +60,11 @@ Each status carries a default tooltip and accessible label. Override it with the - + Custom label ```jsx - + ``` diff --git a/static/app/components/core/chat/toolCallStatusIcon.tsx b/static/app/components/core/chat/toolCallIndicator.tsx similarity index 92% rename from static/app/components/core/chat/toolCallStatusIcon.tsx rename to static/app/components/core/chat/toolCallIndicator.tsx index 39d0d289fa79..6a5e5b2a3828 100644 --- a/static/app/components/core/chat/toolCallStatusIcon.tsx +++ b/static/app/components/core/chat/toolCallIndicator.tsx @@ -22,7 +22,7 @@ export type ToolCallStatus = | 'mixed' | 'content'; -interface ToolCallStatusIconProps { +interface ToolCallIndicatorProps { status: ToolCallStatus; /** * Overrides the default tooltip / accessible label for the status. @@ -31,12 +31,12 @@ interface ToolCallStatusIconProps { } /** - * A compact status icon for a group of agent tool calls: a spinner while they + * A compact status indicator for a group of agent tool calls: a spinner while they * run and a semantic icon once they settle. * * Placement and sizing of the surrounding slot are the caller's responsibility. */ -export function ToolCallStatusIcon({status, label}: ToolCallStatusIconProps) { +export function ToolCallIndicator({status, label}: ToolCallIndicatorProps) { const defaultLabel = getDefaultLabel(status); if (status === 'content') { From 12af92d5aaaac175e1331a4c2c9db57ea5bfda2c Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Jul 2026 12:32:17 +0200 Subject: [PATCH 06/14] fix(scraps): Color ToolCallIndicator icons via the icon variant prop Pass variant directly to the status icons instead of wrapping them in Text. The warning icon now uses the icon system's brighter graphics.warning.vibrant token (which the Text path skipped in favor of the darker content.warning that design engineering flagged as too dark for icons). Also drops three Text wrappers and the now-unused import. Co-Authored-By: Claude --- .../core/chat/toolCallIndicator.tsx | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/static/app/components/core/chat/toolCallIndicator.tsx b/static/app/components/core/chat/toolCallIndicator.tsx index 6a5e5b2a3828..e47fcfa0cd5d 100644 --- a/static/app/components/core/chat/toolCallIndicator.tsx +++ b/static/app/components/core/chat/toolCallIndicator.tsx @@ -1,4 +1,3 @@ -import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {IconCheckmark, IconClose, IconWarning} from 'sentry/icons'; @@ -58,23 +57,11 @@ function ToolCallStatusGlyph({status, label}: {label: string; status: ToolCallSt case 'pending': return ; case 'failure': - return ( - - - - ); + return ; case 'mixed': - return ( - - - - ); + return ; case 'success': - return ( - - - - ); + return ; case 'content': return null; default: From 18d8c14885340ff3388619426998954a21284050 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Jul 2026 12:37:19 +0200 Subject: [PATCH 07/14] ref(scraps): Use Stack instead of Flex direction=column in MessageBubble story Co-Authored-By: Claude --- static/app/components/core/chat/messageBubble.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/components/core/chat/messageBubble.mdx b/static/app/components/core/chat/messageBubble.mdx index 0fd6236f6fc5..22ecbccb387c 100644 --- a/static/app/components/core/chat/messageBubble.mdx +++ b/static/app/components/core/chat/messageBubble.mdx @@ -7,7 +7,7 @@ resources: js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/messageBubble.tsx --- -import {Flex} from '@sentry/scraps/layout'; +import {Flex, Stack} from '@sentry/scraps/layout'; import {MessageBubble} from '@sentry/scraps/chat'; import * as Storybook from 'sentry/stories'; @@ -42,14 +42,14 @@ never spans the full conversation width. Override `maxWidth` for narrower or wider surfaces. - + A longer question that wraps onto multiple lines once it reaches the max width of the bubble. - + ```jsx From 7362e3bd4c14df66dc002835f65886becb268f83 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Jul 2026 12:40:40 +0200 Subject: [PATCH 08/14] ref(scraps): Mark chat index exports @public for knip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ToolCallStatus type has no consumers yet (stories use string literals), so knip flagged its re-export. Tag the entry-point exports @public—the repo's convention for intentional API surface—matching how other core components export their prop types. Co-Authored-By: Claude --- static/app/components/core/chat/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx index 59dc249c0893..be6f5334a43c 100644 --- a/static/app/components/core/chat/index.tsx +++ b/static/app/components/core/chat/index.tsx @@ -1,2 +1,4 @@ +/** @public */ export {MessageBubble} from './messageBubble'; +/** @public */ export {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator'; From fc4c28983f7415ba55bb3f027f480d817e158d02 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Jul 2026 13:10:26 +0200 Subject: [PATCH 09/14] ref: Adopt chat primitives in Seer Explorer and rename to UserBubble Wire the new primitives into the code they were extracted from so they have real production consumers (previously story-only, which failed knip:prod under --production): - seerExplorer UserBlock renders UserBubble instead of a local styled div - seerExplorer tool-call rows render ToolCallIndicator instead of the local BlockStatusIndicator (identical tooltips/labels; warning icon now uses the icon system's brighter token) Also rename MessageBubble -> UserBubble: the component only carries the user's own-message styling, so the generic name over-promised. The now internal-only seerExplorer Spinner is de-exported. No behavior change. Co-Authored-By: Claude --- static/app/components/core/chat/index.tsx | 2 +- .../{messageBubble.mdx => userBubble.mdx} | 20 +++---- .../{messageBubble.tsx => userBubble.tsx} | 8 +-- .../seerExplorer/components/chat/shared.tsx | 2 +- .../seerExplorer/components/chat/toolUse.tsx | 52 ++----------------- .../seerExplorer/components/chat/user.tsx | 16 +----- 6 files changed, 18 insertions(+), 82 deletions(-) rename static/app/components/core/chat/{messageBubble.mdx => userBubble.mdx} (73%) rename static/app/components/core/chat/{messageBubble.tsx => userBubble.tsx} (86%) diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx index be6f5334a43c..f14f27196323 100644 --- a/static/app/components/core/chat/index.tsx +++ b/static/app/components/core/chat/index.tsx @@ -1,4 +1,4 @@ /** @public */ -export {MessageBubble} from './messageBubble'; +export {UserBubble} from './userBubble'; /** @public */ export {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator'; diff --git a/static/app/components/core/chat/messageBubble.mdx b/static/app/components/core/chat/userBubble.mdx similarity index 73% rename from static/app/components/core/chat/messageBubble.mdx rename to static/app/components/core/chat/userBubble.mdx index 22ecbccb387c..82a8eb0b79b1 100644 --- a/static/app/components/core/chat/messageBubble.mdx +++ b/static/app/components/core/chat/userBubble.mdx @@ -1,20 +1,20 @@ --- -title: MessageBubble +title: UserBubble description: A single message bubble for the sender's own messages in an agent conversation. category: chat source: '@sentry/scraps/chat' resources: - js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/messageBubble.tsx + js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/userBubble.tsx --- import {Flex, Stack} from '@sentry/scraps/layout'; -import {MessageBubble} from '@sentry/scraps/chat'; +import {UserBubble} from '@sentry/scraps/chat'; import * as Storybook from 'sentry/stories'; -export const documentation = import('!!type-loader!@sentry/scraps/chat/messageBubble'); +export const documentation = import('!!type-loader!@sentry/scraps/chat/userBubble'); -The `MessageBubble` renders a single message bubble, styled for the +The `UserBubble` renders a single message bubble, styled for the sender's own messages in an agent conversation. It is presentation only — alignment within the conversation is the caller's responsibility. @@ -25,13 +25,13 @@ agent's responses. - How many errors happened in the last 24 hours? + How many errors happened in the last 24 hours? ```jsx - How many errors happened in the last 24 hours? + How many errors happened in the last 24 hours? ``` @@ -44,16 +44,16 @@ wider surfaces. - + A longer question that wraps onto multiple lines once it reaches the max width of the bubble. - + ```jsx - + ``` Whitespace and long unbroken tokens are preserved and wrapped, so pasted URLs diff --git a/static/app/components/core/chat/messageBubble.tsx b/static/app/components/core/chat/userBubble.tsx similarity index 86% rename from static/app/components/core/chat/messageBubble.tsx rename to static/app/components/core/chat/userBubble.tsx index 501302bb3926..04e1c38a4174 100644 --- a/static/app/components/core/chat/messageBubble.tsx +++ b/static/app/components/core/chat/userBubble.tsx @@ -2,7 +2,7 @@ import {css} from '@emotion/react'; import {Container} from '@sentry/scraps/layout'; -interface MessageBubbleProps extends React.HTMLAttributes { +interface UserBubbleProps extends React.HTMLAttributes { children: React.ReactNode; /** * Caps how wide the bubble can grow relative to its container. Defaults to @@ -18,11 +18,7 @@ interface MessageBubbleProps extends React.HTMLAttributes { * Presentation only — alignment within the conversation is the caller's * responsibility (wrap it in a right-aligned row for user messages). */ -export function MessageBubble({ - children, - maxWidth = '80%', - ...props -}: MessageBubbleProps) { +export function UserBubble({children, maxWidth = '80%', ...props}: UserBubbleProps) { return ( - {blockStatus && } + {blockStatus && } {hasLink ? ( @@ -251,51 +250,6 @@ function TodoList({todos}: {todos: TodoItem[]}) { ); } -function BlockStatusIndicator({status}: {status: BlockStatus}) { - switch (status) { - case 'loading': - return ( - - - - ); - case 'pending': - return ( - - - - ); - case 'failure': - return ( - - - - - - ); - case 'mixed': - return ( - - - - - - ); - case 'success': - return ( - - - - - - ); - case 'content': - return null; - default: - return unreachable(status); - } -} - const ToolCallText = styled(Text)` white-space: normal; overflow: visible; diff --git a/static/app/views/seerExplorer/components/chat/user.tsx b/static/app/views/seerExplorer/components/chat/user.tsx index 2c8d8df2238b..2940d8278929 100644 --- a/static/app/views/seerExplorer/components/chat/user.tsx +++ b/static/app/views/seerExplorer/components/chat/user.tsx @@ -1,5 +1,4 @@ -import styled from '@emotion/styled'; - +import {UserBubble} from '@sentry/scraps/chat'; import {Flex} from '@sentry/scraps/layout'; import type {UserBlockProps} from './shared'; @@ -11,16 +10,3 @@ export function UserBlock({block}: UserBlockProps) { ); } - -const UserBubble = styled('div')` - max-width: 80%; - padding: ${p => p.theme.space.xs} ${p => p.theme.space.md}; - white-space: pre-wrap; - word-wrap: break-word; - overflow-wrap: anywhere; - min-width: 0; - color: ${p => p.theme.tokens.content.primary}; - background: ${p => p.theme.tokens.background.secondary}; - border: 1px solid ${p => p.theme.tokens.border.primary}; - border-radius: 6px; -`; From b8081e1671b20958d773e4be400e51732c911e81 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Jul 2026 14:30:13 +0200 Subject: [PATCH 10/14] ref: Reuse ToolCallStatus in Seer Explorer and inline UserBubble styles Delete the duplicate BlockStatus type in seerExplorer; getBlockStatus and the tool-call rows now use the exported ToolCallStatus. That gives the type a real consumer, so the @public knip tags on the chat barrel are no longer needed and are removed. Also inline UserBubble's wrapStyles css block into the css prop. No behavior change. Co-Authored-By: Claude --- static/app/components/core/chat/index.tsx | 2 -- static/app/components/core/chat/userBubble.tsx | 16 +++++++--------- .../seerExplorer/components/chat/shared.tsx | 11 ++--------- .../seerExplorer/components/chat/toolUse.tsx | 11 +++-------- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx index f14f27196323..48103bb19bb3 100644 --- a/static/app/components/core/chat/index.tsx +++ b/static/app/components/core/chat/index.tsx @@ -1,4 +1,2 @@ -/** @public */ export {UserBubble} from './userBubble'; -/** @public */ export {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator'; diff --git a/static/app/components/core/chat/userBubble.tsx b/static/app/components/core/chat/userBubble.tsx index 04e1c38a4174..3f951a2bbedd 100644 --- a/static/app/components/core/chat/userBubble.tsx +++ b/static/app/components/core/chat/userBubble.tsx @@ -28,18 +28,16 @@ export function UserBubble({children, maxWidth = '80%', ...props}: UserBubblePro border="primary" radius="md" whiteSpace="pre-wrap" - css={wrapStyles} + // `overflow-wrap`/`word-wrap` are not layout props; they keep long + // unbroken tokens (URLs, stack frames) from overflowing the bubble. + css={theme => css` + color: ${theme.tokens.content.primary}; + word-wrap: break-word; + overflow-wrap: anywhere; + `} {...props} > {children} ); } - -// `overflow-wrap`/`word-wrap` are not layout props; they keep long unbroken -// tokens (URLs, stack frames) from overflowing the bubble. -const wrapStyles = (theme: import('@emotion/react').Theme) => css` - color: ${theme.tokens.content.primary}; - word-wrap: break-word; - overflow-wrap: anywhere; -`; diff --git a/static/app/views/seerExplorer/components/chat/shared.tsx b/static/app/views/seerExplorer/components/chat/shared.tsx index 1fba196d9fc6..227c86c67693 100644 --- a/static/app/views/seerExplorer/components/chat/shared.tsx +++ b/static/app/views/seerExplorer/components/chat/shared.tsx @@ -1,6 +1,7 @@ import {keyframes} from '@emotion/react'; import styled from '@emotion/styled'; +import type {ToolCallStatus} from '@sentry/scraps/chat'; import {Flex} from '@sentry/scraps/layout'; import {SeerMarkdown} from 'sentry/components/seer/markdown'; @@ -25,15 +26,7 @@ export interface ToolUseBlockProps extends BlockVariantProps { showThinking?: boolean; } -export type BlockStatus = - | 'loading' - | 'content' - | 'success' - | 'failure' - | 'mixed' - | 'pending'; - -export function getBlockStatus(block: Block): BlockStatus { +export function getBlockStatus(block: Block): ToolCallStatus { if (block.loading) { return 'loading'; } diff --git a/static/app/views/seerExplorer/components/chat/toolUse.tsx b/static/app/views/seerExplorer/components/chat/toolUse.tsx index 3ef58c962d25..1a1fbc3bf834 100644 --- a/static/app/views/seerExplorer/components/chat/toolUse.tsx +++ b/static/app/views/seerExplorer/components/chat/toolUse.tsx @@ -1,7 +1,7 @@ import {useMemo} from 'react'; import styled from '@emotion/styled'; -import {ToolCallIndicator} from '@sentry/scraps/chat'; +import {ToolCallIndicator, type ToolCallStatus} from '@sentry/scraps/chat'; import {Checkbox} from '@sentry/scraps/checkbox'; import {Disclosure} from '@sentry/scraps/disclosure'; import {Flex, Stack} from '@sentry/scraps/layout'; @@ -23,12 +23,7 @@ import { } from 'sentry/views/seerExplorer/utils'; import type {ToolUseBlockProps} from './shared'; -import { - type BlockStatus, - MessagePlaceholder, - getBlockStatus, - hasValidContent, -} from './shared'; +import {MessagePlaceholder, getBlockStatus, hasValidContent} from './shared'; export function ToolUseBlock({ block, @@ -185,7 +180,7 @@ function ToolCallRow({ onLinkClick, todos, }: { - blockStatus: BlockStatus | undefined; + blockStatus: ToolCallStatus | undefined; failureTooltip: string | null; todos: TodoItem[] | null; toolString: string; From 9a8e5990087d536a1208b67052318157bacfe9d5 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 15 Jul 2026 11:44:58 +0200 Subject: [PATCH 11/14] ref(scraps): Drop redundant Stack wrapper in UserBubble story --- static/app/components/core/chat/userBubble.mdx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/static/app/components/core/chat/userBubble.mdx b/static/app/components/core/chat/userBubble.mdx index 82a8eb0b79b1..a9a1558f34bc 100644 --- a/static/app/components/core/chat/userBubble.mdx +++ b/static/app/components/core/chat/userBubble.mdx @@ -7,7 +7,7 @@ resources: js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/userBubble.tsx --- -import {Flex, Stack} from '@sentry/scraps/layout'; +import {Flex} from '@sentry/scraps/layout'; import {UserBubble} from '@sentry/scraps/chat'; import * as Storybook from 'sentry/stories'; @@ -42,14 +42,12 @@ never spans the full conversation width. Override `maxWidth` for narrower or wider surfaces. - - - - A longer question that wraps onto multiple lines once it reaches the max width of - the bubble. - - - + + + A longer question that wraps onto multiple lines once it reaches the max width of + the bubble. + + ```jsx From 4108258ab9c502b4b93136fff87664a6a65c04df Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 15 Jul 2026 12:49:00 +0200 Subject: [PATCH 12/14] ref(scraps): Reuse chat Spinner primitive in Seer Explorer --- static/app/components/core/chat/index.tsx | 1 + static/app/components/core/chat/spinner.tsx | 4 ++-- .../seerExplorer/components/chat/shared.tsx | 20 +------------------ 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx index 48103bb19bb3..f9a891bc7201 100644 --- a/static/app/components/core/chat/index.tsx +++ b/static/app/components/core/chat/index.tsx @@ -1,2 +1,3 @@ export {UserBubble} from './userBubble'; export {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator'; +export {Spinner} from './spinner'; diff --git a/static/app/components/core/chat/spinner.tsx b/static/app/components/core/chat/spinner.tsx index cd48b2adb11e..d6859657bd8e 100644 --- a/static/app/components/core/chat/spinner.tsx +++ b/static/app/components/core/chat/spinner.tsx @@ -12,8 +12,8 @@ interface SpinnerProps extends React.HTMLAttributes { * A small, inline activity spinner for chat and agent surfaces — used next to a * message that is streaming or a tool call that is still running. * - * Internal to the chat primitives (not exported) — the general spinner/loading - * consolidation is tracked separately. + * Scoped to the chat primitives — the general spinner/loading consolidation is + * tracked separately. * * Decorative by default (`aria-hidden`). Pass an `aria-label` (and typically * `role="status"`) when the spinner is the only signal that work is in progress. diff --git a/static/app/views/seerExplorer/components/chat/shared.tsx b/static/app/views/seerExplorer/components/chat/shared.tsx index 227c86c67693..3fa5fafb7f79 100644 --- a/static/app/views/seerExplorer/components/chat/shared.tsx +++ b/static/app/views/seerExplorer/components/chat/shared.tsx @@ -1,7 +1,4 @@ -import {keyframes} from '@emotion/react'; -import styled from '@emotion/styled'; - -import type {ToolCallStatus} from '@sentry/scraps/chat'; +import {Spinner, type ToolCallStatus} from '@sentry/scraps/chat'; import {Flex} from '@sentry/scraps/layout'; import {SeerMarkdown} from 'sentry/components/seer/markdown'; @@ -84,19 +81,4 @@ export function MessagePlaceholder({content}: {content?: string}) { ); } -const spin = keyframes` - to { transform: rotate(360deg); } -`; - -const Spinner = styled('div')` - box-sizing: border-box; - width: 12px; - height: 12px; - border-radius: 50%; - border: 1.5px solid ${p => p.theme.tokens.border.primary}; - border-left-color: ${p => p.theme.tokens.border.accent.vibrant}; - animation: ${spin} 0.6s linear infinite; - flex-shrink: 0; -`; - export const BLOCK_WRAPPER_SELECTOR = '[data-block-wrapper]'; From 494e7ea0aee86dbf782c7aac466bb334f2a3a98e Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 15 Jul 2026 13:03:01 +0200 Subject: [PATCH 13/14] fix(scraps): Label placeholder spinner and stop size prop DOM leak --- static/app/components/core/chat/spinner.tsx | 8 ++++---- static/app/views/seerExplorer/components/chat/shared.tsx | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/static/app/components/core/chat/spinner.tsx b/static/app/components/core/chat/spinner.tsx index d6859657bd8e..c79e06cde4b7 100644 --- a/static/app/components/core/chat/spinner.tsx +++ b/static/app/components/core/chat/spinner.tsx @@ -21,7 +21,7 @@ interface SpinnerProps extends React.HTMLAttributes { export function Spinner({size = 12, ...props}: SpinnerProps) { return ( @@ -34,11 +34,11 @@ const spin = keyframes` } `; -const Ring = styled('span')<{size: number}>` +const Ring = styled('span')<{$size: number}>` box-sizing: border-box; display: inline-block; - width: ${p => p.size}px; - height: ${p => p.size}px; + width: ${p => p.$size}px; + height: ${p => p.$size}px; border-radius: ${p => p.theme.radius.full}; border: 1.5px solid ${p => p.theme.tokens.border.primary}; border-left-color: ${p => p.theme.tokens.border.accent.vibrant}; diff --git a/static/app/views/seerExplorer/components/chat/shared.tsx b/static/app/views/seerExplorer/components/chat/shared.tsx index 3fa5fafb7f79..a3efe3dcd423 100644 --- a/static/app/views/seerExplorer/components/chat/shared.tsx +++ b/static/app/views/seerExplorer/components/chat/shared.tsx @@ -2,6 +2,7 @@ import {Spinner, type ToolCallStatus} from '@sentry/scraps/chat'; import {Flex} from '@sentry/scraps/layout'; import {SeerMarkdown} from 'sentry/components/seer/markdown'; +import {t} from 'sentry/locale'; import type {Block, SeerExplorerRunId} from 'sentry/views/seerExplorer/types'; interface BlockVariantProps { @@ -74,7 +75,7 @@ export function MessagePlaceholder({content}: {content?: string}) { height="12px" flexShrink={0} > - + {hasValidContent(content) && } From bb51e4167d875ec4f4fba78d548820cd04d65715 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 15 Jul 2026 14:03:46 +0200 Subject: [PATCH 14/14] ref(scraps): Add MessageRow chat primitive and adopt in Seer Explorer --- static/app/components/core/chat/index.tsx | 1 + .../app/components/core/chat/messageRow.mdx | 49 +++++++++++++++++++ .../app/components/core/chat/messageRow.tsx | 29 +++++++++++ .../seerExplorer/components/chat/shared.tsx | 6 +-- .../seerExplorer/components/chat/user.tsx | 7 ++- 5 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 static/app/components/core/chat/messageRow.mdx create mode 100644 static/app/components/core/chat/messageRow.tsx diff --git a/static/app/components/core/chat/index.tsx b/static/app/components/core/chat/index.tsx index f9a891bc7201..9d967d6768a0 100644 --- a/static/app/components/core/chat/index.tsx +++ b/static/app/components/core/chat/index.tsx @@ -1,3 +1,4 @@ export {UserBubble} from './userBubble'; export {ToolCallIndicator, type ToolCallStatus} from './toolCallIndicator'; export {Spinner} from './spinner'; +export {MessageRow} from './messageRow'; diff --git a/static/app/components/core/chat/messageRow.mdx b/static/app/components/core/chat/messageRow.mdx new file mode 100644 index 000000000000..29c1b4007857 --- /dev/null +++ b/static/app/components/core/chat/messageRow.mdx @@ -0,0 +1,49 @@ +--- +title: MessageRow +description: The full-width row that positions a single message turn in a conversation. +category: chat +source: '@sentry/scraps/chat' +resources: + js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/chat/messageRow.tsx +--- + +import {MessageRow, UserBubble} from '@sentry/scraps/chat'; +import {Container} from '@sentry/scraps/layout'; + +import * as Storybook from 'sentry/stories'; + +export const documentation = import('!!type-loader!@sentry/scraps/chat/messageRow'); + +The `MessageRow` lays out a single message turn within a conversation. It is +presentation only — it owns alignment and the consistent gutter around a turn, +while the bubble or content is the caller's responsibility. + +## Alignment + +Use `justify="end"` for the sender's own messages and the default `justify="start"` +for the agent's responses, so the two sides sit opposite each other. + + + + + Which of my issues are getting worse? + + Here are the three escalating issues I found. + + + +```jsx + + Which of my issues are getting worse? + +Here are the three escalating issues I found. +``` + +## Padding + +The row applies a consistent gutter (`md xl`) around each turn. Override +`padding` for surfaces that need a different gutter. + +```jsx + +``` diff --git a/static/app/components/core/chat/messageRow.tsx b/static/app/components/core/chat/messageRow.tsx new file mode 100644 index 000000000000..64ee9cbc852d --- /dev/null +++ b/static/app/components/core/chat/messageRow.tsx @@ -0,0 +1,29 @@ +import {Flex, type FlexProps} from '@sentry/scraps/layout'; + +interface MessageRowProps extends Omit { + children: React.ReactNode; + /** + * Which side of the conversation the message sits on: `end` for the sender's + * own messages, `start` (default) for the agent's responses. + */ + justify?: 'start' | 'end'; +} + +/** + * The full-width row that positions a single message turn within a conversation. + * + * Presentation only — it owns alignment and the consistent gutter around a turn; + * the bubble or content is the caller's responsibility. + */ +export function MessageRow({ + children, + justify = 'start', + padding = 'md xl', + ...props +}: MessageRowProps) { + return ( + + {children} + + ); +} diff --git a/static/app/views/seerExplorer/components/chat/shared.tsx b/static/app/views/seerExplorer/components/chat/shared.tsx index a3efe3dcd423..b8e000bcdc33 100644 --- a/static/app/views/seerExplorer/components/chat/shared.tsx +++ b/static/app/views/seerExplorer/components/chat/shared.tsx @@ -1,4 +1,4 @@ -import {Spinner, type ToolCallStatus} from '@sentry/scraps/chat'; +import {MessageRow, Spinner, type ToolCallStatus} from '@sentry/scraps/chat'; import {Flex} from '@sentry/scraps/layout'; import {SeerMarkdown} from 'sentry/components/seer/markdown'; @@ -66,7 +66,7 @@ export function hasValidContent(content: string | null | undefined): content is export function MessagePlaceholder({content}: {content?: string}) { return ( - + {hasValidContent(content) && } - + ); } diff --git a/static/app/views/seerExplorer/components/chat/user.tsx b/static/app/views/seerExplorer/components/chat/user.tsx index 2940d8278929..9a3b4eb4238a 100644 --- a/static/app/views/seerExplorer/components/chat/user.tsx +++ b/static/app/views/seerExplorer/components/chat/user.tsx @@ -1,12 +1,11 @@ -import {UserBubble} from '@sentry/scraps/chat'; -import {Flex} from '@sentry/scraps/layout'; +import {MessageRow, UserBubble} from '@sentry/scraps/chat'; import type {UserBlockProps} from './shared'; export function UserBlock({block}: UserBlockProps) { return ( - + {block.message.content ?? ''} - + ); }