Skip to content

Commit 8bf6941

Browse files
authored
feat(investigations): Focus active investigation progress (#122682)
Investigation notebooks now focus on work that has started: auto-run cells waiting on dependencies remain hidden and appear once their execution begins. Active cells automatically open their live Seer steps, while generated query evidence starts collapsed. Manual query cells keep their existing expanded default, and closing an active trace remains respected until a new execution starts.
1 parent da6911b commit 8bf6941

3 files changed

Lines changed: 348 additions & 145 deletions

File tree

static/app/views/investigations/detail/cell.tsx

Lines changed: 142 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {TextArea} from '@sentry/scraps/textarea';
1111

1212
import {addErrorMessage} from 'sentry/actionCreators/indicator';
1313
import {openConfirmModal} from 'sentry/components/confirm';
14-
import {DropdownMenu} from 'sentry/components/dropdownMenu';
14+
import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
1515
import {Duration} from 'sentry/components/duration';
1616
import {SeerMarkdown} from 'sentry/components/seer/markdown';
1717
import {ChartContent} from 'sentry/components/seer/markdown/embeds/components/chart';
@@ -21,7 +21,6 @@ import {
2121
IconChevron,
2222
IconClose,
2323
IconEllipsis,
24-
IconRefresh,
2524
IconReturn,
2625
IconSeer,
2726
} from 'sentry/icons';
@@ -62,9 +61,15 @@ export function InvestigationCell({
6261
investigation,
6362
}: InvestigationCellProps) {
6463
const organizationSlug = useOrganization().slug;
65-
const [panelOpen, setPanelOpen] = useState(false);
66-
const [traceExecutionId, setTraceExecutionId] = useState<string | null>(null);
67-
const [showPrompt, setShowPrompt] = useState(true);
64+
const activeExecutionId = isExecutionActive(block.currentExecution?.status)
65+
? (block.currentExecution?.id ?? null)
66+
: null;
67+
const autoOpenedExecutionId = useRef(activeExecutionId);
68+
const [panelOpen, setPanelOpen] = useState(Boolean(activeExecutionId));
69+
const [traceExecutionId, setTraceExecutionId] = useState<string | null>(
70+
activeExecutionId
71+
);
72+
const [showPrompt, setShowPrompt] = useState(!activeExecutionId);
6873
const [prompt, setPrompt] = useState(() =>
6974
block.outputStatus === 'notRun' ? block.generationPrompt : ''
7075
);
@@ -108,6 +113,16 @@ export function InvestigationCell({
108113
{onError: () => addErrorMessage(t('Unable to delete this cell.'))}
109114
);
110115

116+
useEffect(() => {
117+
if (!activeExecutionId || autoOpenedExecutionId.current === activeExecutionId) {
118+
return;
119+
}
120+
autoOpenedExecutionId.current = activeExecutionId;
121+
setPanelOpen(true);
122+
setTraceExecutionId(activeExecutionId);
123+
setShowPrompt(false);
124+
}, [activeExecutionId]);
125+
111126
function openPanel() {
112127
setPanelOpen(true);
113128
if (block.currentExecution && isExecutionActive(block.currentExecution.status)) {
@@ -128,38 +143,56 @@ export function InvestigationCell({
128143
setPanelOpen(true);
129144
setTraceExecutionId(execution.id);
130145
setShowPrompt(false);
146+
autoOpenedExecutionId.current = execution.id;
131147
} catch {
132148
// The mutation owns user-facing error handling.
133149
}
134150
}
135151

136-
const refinementButton = (
137-
<Button
138-
size="xs"
139-
variant="transparent"
140-
icon={<IconSeer size="xs" />}
141-
aria-label={t('Ask Seer about %s', displayTitle)}
142-
disabled={waitingForDependencies}
143-
onClick={panelOpen ? () => setPanelOpen(false) : openPanel}
144-
/>
152+
const actionItems: MenuItemProps[] = [];
153+
if (block.kind === 'query') {
154+
actionItems.push({
155+
key: 'rerun',
156+
label: t('Rerun'),
157+
disabled:
158+
!canRun ||
159+
rerunMutation.isPending ||
160+
isExecutionActive(block.currentExecution?.status) ||
161+
!(block.generationPrompt || block.content).trim(),
162+
onAction: () => void rerun(),
163+
});
164+
}
165+
actionItems.push(
166+
{
167+
key: 'refine',
168+
label: t('Refine'),
169+
disabled: waitingForDependencies,
170+
onAction: openPanel,
171+
},
172+
{
173+
key: 'delete',
174+
label: t('Delete'),
175+
priority: 'danger',
176+
disabled:
177+
!canRun ||
178+
deleteMutation.isPending ||
179+
isExecutionActive(block.currentExecution?.status),
180+
onAction: () =>
181+
openConfirmModal({
182+
message: t('Are you sure you want to delete this cell?'),
183+
priority: 'danger',
184+
confirmText: t('Delete'),
185+
onConfirm: () =>
186+
deleteMutation.mutate({
187+
block,
188+
investigationVersion: investigation.version,
189+
}),
190+
}),
191+
}
145192
);
146193

147-
const queryHeaderActions = (
148-
<Flex align="center" gap="xs" flexShrink={0}>
149-
{refinementButton}
150-
<Button
151-
size="xs"
152-
variant="transparent"
153-
icon={<IconRefresh size="xs" />}
154-
aria-label={t('Rerun %s', displayTitle)}
155-
busy={rerunMutation.isPending}
156-
disabled={
157-
!canRun ||
158-
isExecutionActive(block.currentExecution?.status) ||
159-
!(block.generationPrompt || block.content).trim()
160-
}
161-
onClick={() => void rerun()}
162-
/>
194+
const cellActions = (
195+
<CellActions flexShrink={0}>
163196
<DropdownMenu
164197
position="bottom-end"
165198
usePortal
@@ -170,30 +203,9 @@ export function InvestigationCell({
170203
icon: <IconEllipsis size="xs" />,
171204
'aria-label': t('Cell actions for %s', displayTitle),
172205
}}
173-
items={[
174-
{
175-
key: 'delete',
176-
label: t('Delete'),
177-
priority: 'danger',
178-
disabled:
179-
!canRun ||
180-
deleteMutation.isPending ||
181-
isExecutionActive(block.currentExecution?.status),
182-
onAction: () =>
183-
openConfirmModal({
184-
message: t('Are you sure you want to delete this cell?'),
185-
priority: 'danger',
186-
confirmText: t('Delete'),
187-
onConfirm: () =>
188-
deleteMutation.mutate({
189-
block,
190-
investigationVersion: investigation.version,
191-
}),
192-
}),
193-
},
194-
]}
206+
items={actionItems}
195207
/>
196-
</Flex>
208+
</CellActions>
197209
);
198210

199211
const panel = panelOpen ? (
@@ -223,7 +235,7 @@ export function InvestigationCell({
223235
{block.kind === 'query' ? (
224236
<Fragment>
225237
<QueryResult
226-
actions={queryHeaderActions}
238+
actions={cellActions}
227239
block={block}
228240
progressState={progressState}
229241
/>
@@ -233,8 +245,8 @@ export function InvestigationCell({
233245
<Fragment>
234246
<CellResult
235247
block={block}
248+
actions={cellActions}
236249
progressState={progressState}
237-
refinementButton={refinementButton}
238250
streamedMarkdown={
239251
isExecutionActive(block.currentExecution?.status)
240252
? streamedTextQuery.data?.partialMarkdown
@@ -249,20 +261,20 @@ export function InvestigationCell({
249261
}
250262

251263
function CellResult({
264+
actions,
252265
block,
253266
progressState,
254-
refinementButton,
255267
streamedMarkdown,
256268
}: {
269+
actions: React.ReactNode;
257270
block: InvestigationBlock;
258271
progressState: CellProgressState;
259-
refinementButton: React.ReactNode;
260272
streamedMarkdown?: string | null;
261273
}) {
262274
const markdown =
263275
streamedMarkdown ?? getTextOutput(block.output) ?? (block.content.trim() || null);
264276
return (
265-
<Stack
277+
<CellHoverSurface
266278
position="relative"
267279
flex={1}
268280
minWidth={0}
@@ -271,15 +283,15 @@ function CellResult({
271283
data-cell-variant="unbordered"
272284
>
273285
<Container position="absolute" top={0} right={0}>
274-
{refinementButton}
286+
{actions}
275287
</Container>
276288
<CellExecutionAlert block={block} />
277289
{markdown ? (
278290
<SeerMarkdown raw={markdown} />
279291
) : (
280292
<CellProgress state={progressState} />
281293
)}
282-
</Stack>
294+
</CellHoverSurface>
283295
);
284296
}
285297

@@ -292,7 +304,7 @@ function QueryResult({
292304
block: InvestigationBlock;
293305
progressState: CellProgressState;
294306
}) {
295-
const [expanded, setExpanded] = useState(true);
307+
const [expanded, setExpanded] = useState(block.config.autoRun !== true);
296308
const output = getQueryOutput(block.output);
297309
const chart =
298310
output?.preferredView === 'chart' ? getRenderableChart(output.chart) : null;
@@ -304,19 +316,22 @@ function QueryResult({
304316
getChartMetadata(chart);
305317

306318
return (
307-
<Stack width="100%" gap="sm">
308-
<QueryDisclosureButton
309-
size="sm"
310-
variant="transparent"
311-
icon={<IconChevron direction={expanded ? 'down' : 'right'} size="xs" />}
312-
aria-label={t('Toggle %s', title)}
313-
aria-expanded={expanded}
314-
onClick={() => setExpanded(value => !value)}
315-
>
316-
<Text data-test-id="query-cell-title" size="sm" tabular>
317-
{title}
318-
</Text>
319-
</QueryDisclosureButton>
319+
<CellHoverSurface width="100%" gap="sm">
320+
<Flex width="100%" align="center" gap="xs" data-test-id="query-cell-toolbar">
321+
<QueryDisclosureButton
322+
size="sm"
323+
variant="transparent"
324+
icon={<IconChevron direction={expanded ? 'down' : 'right'} size="xs" />}
325+
aria-label={t('Toggle %s', title)}
326+
aria-expanded={expanded}
327+
onClick={() => setExpanded(value => !value)}
328+
>
329+
<Text data-test-id="query-cell-title" size="sm" tabular>
330+
{title}
331+
</Text>
332+
</QueryDisclosureButton>
333+
{actions}
334+
</Flex>
320335
{expanded ? (
321336
<Stack
322337
width="100%"
@@ -348,7 +363,6 @@ function QueryResult({
348363
</Text>
349364
) : null}
350365
</Stack>
351-
{actions}
352366
</Flex>
353367
<Container width="100%" overflow="hidden" padding={chart ? 'md lg' : '0'}>
354368
<CellExecutionAlert block={block} />
@@ -364,7 +378,7 @@ function QueryResult({
364378
</Container>
365379
</Stack>
366380
) : null}
367-
</Stack>
381+
</CellHoverSurface>
368382
);
369383
}
370384

@@ -449,6 +463,15 @@ function getCellProgressState(
449463
return 'waiting';
450464
}
451465

466+
export function shouldDisplayInvestigationBlock(
467+
block: InvestigationBlock,
468+
blocks: InvestigationBlock[]
469+
) {
470+
// Waiting cells have no useful content yet. Dependency failures and cancellations
471+
// remain visible so users can understand why downstream work stopped.
472+
return getCellProgressState(block, blocks) !== 'waiting';
473+
}
474+
452475
export function shouldPollInvestigationBlocks(blocks: InvestigationBlock[]) {
453476
return blocks.some(
454477
block =>
@@ -689,7 +712,7 @@ function RefinementPanel({
689712

690713
return (
691714
<RefinementDisclosure defaultExpanded size="sm">
692-
<Disclosure.Title
715+
<AgentActivityDisclosureTitle
693716
leadingItems={<IconSeer size="xs" animation={active ? 'waiting' : undefined} />}
694717
trailingItems={
695718
<Flex align="center" gap="sm">
@@ -704,8 +727,8 @@ function RefinementPanel({
704727
</Flex>
705728
}
706729
>
707-
<Text monospace>{getExecutionTitle(status)}</Text>
708-
</Disclosure.Title>
730+
<AgentActivityTitle monospace>{getExecutionTitle(status)}</AgentActivityTitle>
731+
</AgentActivityDisclosureTitle>
709732
<RefinementDisclosureContent>
710733
<Stack gap="md">
711734
<Transcript
@@ -1102,11 +1125,32 @@ function getSeriesName(series: {label: string} | {name: string}) {
11021125
}
11031126

11041127
const QueryDisclosureButton = styled(Button)`
1105-
width: 100%;
1128+
flex: 1;
11061129
justify-content: flex-start;
1130+
padding-inline: ${p => p.theme.space.xs};
11071131
text-align: left;
11081132
`;
11091133

1134+
const CellActions = styled(Flex)`
1135+
opacity: 0;
1136+
pointer-events: none;
1137+
`;
1138+
1139+
const CellHoverSurface = styled(Stack)`
1140+
&:hover ${CellActions},
1141+
&:focus-within ${CellActions} {
1142+
opacity: 1;
1143+
pointer-events: auto;
1144+
}
1145+
1146+
@media (hover: none) {
1147+
${CellActions} {
1148+
opacity: 1;
1149+
pointer-events: auto;
1150+
}
1151+
}
1152+
`;
1153+
11101154
const QueryTable = styled('table')`
11111155
min-width: 100%;
11121156
border-collapse: collapse;
@@ -1115,6 +1159,24 @@ const QueryTable = styled('table')`
11151159
const RefinementDisclosure = styled(Disclosure)`
11161160
width: 100%;
11171161
margin-top: ${p => p.theme.space.lg};
1162+
1163+
& > div:first-child {
1164+
padding-inline: ${p => p.theme.space['2xs']};
1165+
}
1166+
`;
1167+
1168+
const AgentActivityDisclosureTitle = styled(Disclosure.Title)`
1169+
padding-inline: 0;
1170+
`;
1171+
1172+
const AgentActivityTitle = styled(Text)`
1173+
font-size: ${p => p.theme.font.size.sm};
1174+
font-style: normal;
1175+
font-weight: 700;
1176+
line-height: ${p => p.theme.font.lineHeight.fixed};
1177+
letter-spacing: 0;
1178+
vertical-align: middle;
1179+
font-variant-numeric: lining-nums tabular-nums;
11181180
`;
11191181

11201182
const RefinementPrompt = styled('div')`

0 commit comments

Comments
 (0)