Skip to content

Commit 423f37a

Browse files
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 <noreply@anthropic.com>
1 parent 8eb4dae commit 423f37a

File tree

4 files changed

+127
-3
lines changed

4 files changed

+127
-3
lines changed

static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import {PageFiltersContainer} from 'sentry/components/pageFilters/container';
88
import {PageFiltersStore} from 'sentry/components/pageFilters/store';
99
import {OrganizationStore} from 'sentry/stores/organizationStore';
1010
import {ProjectsStore} from 'sentry/stores/projectsStore';
11+
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
1112
import {WidgetBuilderV2} from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder';
13+
import {
14+
LLMContextProvider,
15+
useLLMContext,
16+
} from 'sentry/views/seerExplorer/contexts/llmContext';
17+
import type {LLMContextSnapshot} from 'sentry/views/seerExplorer/contexts/llmContextTypes';
1218

1319
const organization = OrganizationFixture({
1420
features: ['open-membership', 'visibility-explore-view'],
@@ -294,4 +300,92 @@ describe('NewWidgetBuilder', () => {
294300

295301
expect(await screen.findByText('Select a widget to preview')).toBeInTheDocument();
296302
});
303+
304+
it('populates LLM context with builder state', async () => {
305+
const {ContextCapture, getSnapshot} = makeContextCapture();
306+
307+
render(
308+
<LLMContextProvider>
309+
<ContextCapture />
310+
<WidgetBuilderV2
311+
isOpen
312+
onClose={onCloseMock}
313+
onSave={onSaveMock}
314+
dashboard={DashboardFixture([])}
315+
dashboardFilters={{}}
316+
openWidgetTemplates={false}
317+
setOpenWidgetTemplates={jest.fn()}
318+
/>
319+
</LLMContextProvider>,
320+
{
321+
organization,
322+
initialRouterConfig: {
323+
location: {
324+
pathname: '/organizations/org-slug/dashboard/1/widget-builder/widget/new/',
325+
query: {
326+
displayType: DisplayType.LINE,
327+
dataset: WidgetType.ERRORS,
328+
title: 'My Widget',
329+
yAxis: 'count()',
330+
field: 'browser.name',
331+
query: 'browser.name:Firefox',
332+
},
333+
},
334+
},
335+
}
336+
);
337+
338+
await waitFor(() => {
339+
const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder');
340+
expect(node).toBeDefined();
341+
});
342+
343+
const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder')!;
344+
const data = node.data as Record<string, unknown>;
345+
expect(data.mode).toBe('creating');
346+
expect(data.title).toBe('My Widget');
347+
expect(data.dataset).toBe(WidgetType.ERRORS);
348+
expect(data.displayType).toBe(DisplayType.LINE);
349+
expect(data.visualize).toEqual(['count()']);
350+
expect(data.fields).toEqual(['browser.name']);
351+
expect(data.query).toEqual(['browser.name:Firefox']);
352+
});
353+
354+
it('does not register LLM context when the builder is closed', () => {
355+
const {ContextCapture, getSnapshot} = makeContextCapture();
356+
357+
render(
358+
<LLMContextProvider>
359+
<ContextCapture />
360+
<WidgetBuilderV2
361+
isOpen={false}
362+
onClose={onCloseMock}
363+
onSave={onSaveMock}
364+
dashboard={DashboardFixture([])}
365+
dashboardFilters={{}}
366+
openWidgetTemplates={false}
367+
setOpenWidgetTemplates={jest.fn()}
368+
/>
369+
</LLMContextProvider>
370+
);
371+
372+
const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder');
373+
expect(node).toBeUndefined();
374+
});
297375
});
376+
377+
function makeContextCapture() {
378+
const ref: {current: (() => LLMContextSnapshot) | null} = {current: null};
379+
function ContextCapture() {
380+
const {getLLMContext} = useLLMContext();
381+
ref.current = getLLMContext;
382+
return null;
383+
}
384+
return {
385+
ContextCapture,
386+
getSnapshot: () => {
387+
if (!ref.current) throw new Error('ContextCapture not mounted');
388+
return ref.current();
389+
},
390+
};
391+
}

static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {IconClose} from 'sentry/icons';
2626
import {t, tctCode} from 'sentry/locale';
2727
import {trackAnalytics} from 'sentry/utils/analytics';
2828
import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
29+
import {generateFieldAsString} from 'sentry/utils/discover/fields';
2930
import {useLocation} from 'sentry/utils/useLocation';
3031
import {useMedia} from 'sentry/utils/useMedia';
3132
import {useOrganization} from 'sentry/utils/useOrganization';
@@ -70,7 +71,10 @@ import {useSegmentSpanWidgetState} from 'sentry/views/dashboards/widgetBuilder/h
7071
import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget';
7172
import {convertWidgetToBuilderState} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
7273
import type {OnDataFetchedParams} from 'sentry/views/dashboards/widgetCard';
74+
import {readableConditions} from 'sentry/views/dashboards/widgetCard/widgetLLMContext';
7375
import {getTopNConvertedDefaultWidgets} from 'sentry/views/dashboards/widgetLibrary/data';
76+
import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext';
77+
import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext';
7478

7579
type WidgetBuilderSlideoutProps = {
7680
dashboard: DashboardDetails;
@@ -86,7 +90,7 @@ type WidgetBuilderSlideoutProps = {
8690
thresholdMetaState?: ThresholdMetaState;
8791
};
8892

89-
export function WidgetBuilderSlideout({
93+
function WidgetBuilderSlideoutInner({
9094
onClose,
9195
onSave,
9296
onQueryConditionChange,
@@ -108,6 +112,23 @@ export function WidgetBuilderSlideout({
108112
const [error, setError] = useState<Record<string, any>>({});
109113
const theme = useTheme();
110114
const isEditing = useIsEditingWidget();
115+
116+
// Push widget builder state into the LLM context tree for Seer Explorer.
117+
useLLMContext({
118+
contextHint:
119+
'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.',
120+
mode: isEditing ? 'editing' : 'creating',
121+
title: state.title,
122+
description: state.description,
123+
dataset: state.dataset,
124+
displayType: state.displayType,
125+
visualize: state.yAxis?.map(generateFieldAsString),
126+
fields: state.fields?.map(generateFieldAsString),
127+
query: state.query?.map(readableConditions),
128+
sort: state.sort?.map(s => (s.kind === 'desc' ? `-${s.field}` : s.field)),
129+
thresholds: state.thresholds,
130+
legendAlias: state.legendAlias,
131+
});
111132
const source = useDashboardWidgetSource();
112133
const {cacheBuilderState} = useCacheBuilderState();
113134
const {setSegmentSpanBuilderState} = useSegmentSpanWidgetState();
@@ -508,6 +529,11 @@ export function WidgetBuilderSlideout({
508529
);
509530
}
510531

532+
export const WidgetBuilderSlideout = registerLLMContext(
533+
'widget-builder',
534+
WidgetBuilderSlideoutInner
535+
);
536+
511537
function Section({children}: {children: React.ReactNode}) {
512538
return (
513539
<SectionWrapper>

static/app/views/seerExplorer/contexts/llmContextTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* Known node types for the LLM context tree.
1818
* Add new types here as new context-aware components are registered.
1919
*/
20-
export type LLMContextNodeType = 'chart' | 'dashboard' | 'widget';
20+
export type LLMContextNodeType = 'chart' | 'dashboard' | 'widget' | 'widget-builder';
2121

2222
/**
2323
* A single node in the flat registry.

static/app/views/seerExplorer/hooks/useSeerExplorer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ type SeerExplorerChatResponse = {
5252
const POLL_INTERVAL = 500; // Poll every 500ms
5353

5454
/** Routes where the LLMContext tree provides structured page context. */
55-
const STRUCTURED_CONTEXT_ROUTES = new Set(['/dashboard/:dashboardId/']);
55+
const STRUCTURED_CONTEXT_ROUTES = new Set([
56+
'/dashboard/:dashboardId/',
57+
'/dashboard/:dashboardId/widget-builder/widget/new/',
58+
'/dashboard/:dashboardId/widget-builder/widget/:widgetIndex/edit/',
59+
]);
5660

5761
const OPTIMISTIC_ASSISTANT_TEXTS = [
5862
'Looking around...',

0 commit comments

Comments
 (0)