From 423f37a66f19c47eeb83f6a51025c9a1ee562278 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Fri, 10 Apr 2026 11:12:28 -0700 Subject: [PATCH] feat(seer): Add LLM context to widget builder page Register a 'widget-builder' node in the LLM context tree so the Seer Explorer agent understands what widget the user is configuring. Pushes builder state (dataset, displayType, visualize, fields, query, sort, thresholds, legendAlias) with readable condition transforms. Also adds the widget builder routes to STRUCTURED_CONTEXT_ROUTES so structured context is sent instead of ASCII when the builder is open. Co-Authored-By: Claude Opus 4.6 --- .../components/newWidgetBuilder.spec.tsx | 94 +++++++++++++++++++ .../components/widgetBuilderSlideout.tsx | 28 +++++- .../seerExplorer/contexts/llmContextTypes.ts | 2 +- .../seerExplorer/hooks/useSeerExplorer.tsx | 6 +- 4 files changed, 127 insertions(+), 3 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx index 61597a096803bb..391d6278b8cd80 100644 --- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx @@ -8,7 +8,13 @@ import {PageFiltersContainer} from 'sentry/components/pageFilters/container'; import {PageFiltersStore} from 'sentry/components/pageFilters/store'; import {OrganizationStore} from 'sentry/stores/organizationStore'; import {ProjectsStore} from 'sentry/stores/projectsStore'; +import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import {WidgetBuilderV2} from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder'; +import { + LLMContextProvider, + useLLMContext, +} from 'sentry/views/seerExplorer/contexts/llmContext'; +import type {LLMContextSnapshot} from 'sentry/views/seerExplorer/contexts/llmContextTypes'; const organization = OrganizationFixture({ features: ['open-membership', 'visibility-explore-view'], @@ -294,4 +300,92 @@ describe('NewWidgetBuilder', () => { expect(await screen.findByText('Select a widget to preview')).toBeInTheDocument(); }); + + it('populates LLM context with builder state', async () => { + const {ContextCapture, getSnapshot} = makeContextCapture(); + + render( + + + + , + { + organization, + initialRouterConfig: { + location: { + pathname: '/organizations/org-slug/dashboard/1/widget-builder/widget/new/', + query: { + displayType: DisplayType.LINE, + dataset: WidgetType.ERRORS, + title: 'My Widget', + yAxis: 'count()', + field: 'browser.name', + query: 'browser.name:Firefox', + }, + }, + }, + } + ); + + await waitFor(() => { + const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder'); + expect(node).toBeDefined(); + }); + + const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder')!; + const data = node.data as Record; + expect(data.mode).toBe('creating'); + expect(data.title).toBe('My Widget'); + expect(data.dataset).toBe(WidgetType.ERRORS); + expect(data.displayType).toBe(DisplayType.LINE); + expect(data.visualize).toEqual(['count()']); + expect(data.fields).toEqual(['browser.name']); + expect(data.query).toEqual(['browser.name:Firefox']); + }); + + it('does not register LLM context when the builder is closed', () => { + const {ContextCapture, getSnapshot} = makeContextCapture(); + + render( + + + + + ); + + const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder'); + expect(node).toBeUndefined(); + }); }); + +function makeContextCapture() { + const ref: {current: (() => LLMContextSnapshot) | null} = {current: null}; + function ContextCapture() { + const {getLLMContext} = useLLMContext(); + ref.current = getLLMContext; + return null; + } + return { + ContextCapture, + getSnapshot: () => { + if (!ref.current) throw new Error('ContextCapture not mounted'); + return ref.current(); + }, + }; +} diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 9f2a3bd39d9bd6..12bb5e1ddd3b73 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -26,6 +26,7 @@ import {IconClose} from 'sentry/icons'; import {t, tctCode} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents'; +import {generateFieldAsString} from 'sentry/utils/discover/fields'; import {useLocation} from 'sentry/utils/useLocation'; import {useMedia} from 'sentry/utils/useMedia'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -70,7 +71,10 @@ import {useSegmentSpanWidgetState} from 'sentry/views/dashboards/widgetBuilder/h import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget'; import {convertWidgetToBuilderState} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams'; import type {OnDataFetchedParams} from 'sentry/views/dashboards/widgetCard'; +import {readableConditions} from 'sentry/views/dashboards/widgetCard/widgetLLMContext'; import {getTopNConvertedDefaultWidgets} from 'sentry/views/dashboards/widgetLibrary/data'; +import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext'; +import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext'; type WidgetBuilderSlideoutProps = { dashboard: DashboardDetails; @@ -86,7 +90,7 @@ type WidgetBuilderSlideoutProps = { thresholdMetaState?: ThresholdMetaState; }; -export function WidgetBuilderSlideout({ +function WidgetBuilderSlideoutInner({ onClose, onSave, onQueryConditionChange, @@ -108,6 +112,23 @@ export function WidgetBuilderSlideout({ const [error, setError] = useState>({}); const theme = useTheme(); const isEditing = useIsEditingWidget(); + + // Push widget builder state into the LLM context tree for Seer Explorer. + useLLMContext({ + contextHint: + 'Sentry widget builder. The user is configuring a dashboard widget. visualize is the y-axis metrics (timeseries) or the aggregate (big number/table). fields are group-by columns (timeseries) or visible columns (table). query filters the data and sort controls ordering.', + mode: isEditing ? 'editing' : 'creating', + title: state.title, + description: state.description, + dataset: state.dataset, + displayType: state.displayType, + visualize: state.yAxis?.map(generateFieldAsString), + fields: state.fields?.map(generateFieldAsString), + query: state.query?.map(readableConditions), + sort: state.sort?.map(s => (s.kind === 'desc' ? `-${s.field}` : s.field)), + thresholds: state.thresholds, + legendAlias: state.legendAlias, + }); const source = useDashboardWidgetSource(); const {cacheBuilderState} = useCacheBuilderState(); const {setSegmentSpanBuilderState} = useSegmentSpanWidgetState(); @@ -508,6 +529,11 @@ export function WidgetBuilderSlideout({ ); } +export const WidgetBuilderSlideout = registerLLMContext( + 'widget-builder', + WidgetBuilderSlideoutInner +); + function Section({children}: {children: React.ReactNode}) { return ( diff --git a/static/app/views/seerExplorer/contexts/llmContextTypes.ts b/static/app/views/seerExplorer/contexts/llmContextTypes.ts index 1120072a3a6667..5c4eb10e6120cf 100644 --- a/static/app/views/seerExplorer/contexts/llmContextTypes.ts +++ b/static/app/views/seerExplorer/contexts/llmContextTypes.ts @@ -17,7 +17,7 @@ * Known node types for the LLM context tree. * Add new types here as new context-aware components are registered. */ -export type LLMContextNodeType = 'chart' | 'dashboard' | 'widget'; +export type LLMContextNodeType = 'chart' | 'dashboard' | 'widget' | 'widget-builder'; /** * A single node in the flat registry. diff --git a/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx b/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx index d52ba16425657b..a628d39c773bd3 100644 --- a/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx +++ b/static/app/views/seerExplorer/hooks/useSeerExplorer.tsx @@ -52,7 +52,11 @@ type SeerExplorerChatResponse = { const POLL_INTERVAL = 500; // Poll every 500ms /** Routes where the LLMContext tree provides structured page context. */ -const STRUCTURED_CONTEXT_ROUTES = new Set(['/dashboard/:dashboardId/']); +const STRUCTURED_CONTEXT_ROUTES = new Set([ + '/dashboard/:dashboardId/', + '/dashboard/:dashboardId/widget-builder/widget/new/', + '/dashboard/:dashboardId/widget-builder/widget/:widgetIndex/edit/', +]); const OPTIMISTIC_ASSISTANT_TEXTS = [ 'Looking around...',