Skip to content

Commit df95336

Browse files
authored
feat(explorer): add UI for creating PRs (#104500)
adds UI for creating pull requests <img width="834" height="488" alt="Screenshot 2025-12-06 at 11 35 46 AM" src="https://github.com/user-attachments/assets/50c2355e-094e-41a6-bb7b-838fee0b31ac" /> <img width="962" height="409" alt="Screenshot 2025-12-06 at 11 38 24 AM" src="https://github.com/user-attachments/assets/26695550-4c46-4eb0-b989-37a8204cb000" /> requires getsentry/seer#4187 part of AIML-1695
1 parent 83421d9 commit df95336

File tree

7 files changed

+572
-67
lines changed

7 files changed

+572
-67
lines changed

static/app/views/seerExplorer/explorerMenu.tsx

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import TimeSince from 'sentry/components/timeSince';
66
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
77
import {useExplorerSessions} from 'sentry/views/seerExplorer/hooks/useExplorerSessions';
88

9-
type MenuMode = 'slash-commands-keyboard' | 'session-history' | 'hidden';
9+
type MenuMode = 'slash-commands-keyboard' | 'session-history' | 'pr-widget' | 'hidden';
1010

1111
interface ExplorerMenuProps {
1212
clearInput: () => void;
@@ -23,9 +23,12 @@ interface ExplorerMenuProps {
2323
textAreaRef: React.RefObject<HTMLTextAreaElement | null>;
2424
inputAnchorRef?: React.RefObject<HTMLElement | null>;
2525
menuAnchorRef?: React.RefObject<HTMLElement | null>;
26+
prWidgetAnchorRef?: React.RefObject<HTMLElement | null>;
27+
prWidgetFooter?: React.ReactNode;
28+
prWidgetItems?: MenuItemProps[];
2629
}
2730

28-
interface MenuItemProps {
31+
export interface MenuItemProps {
2932
description: string | React.ReactNode;
3033
handler: () => void;
3134
key: string;
@@ -43,6 +46,9 @@ export function useExplorerMenu({
4346
onChangeSession,
4447
menuAnchorRef,
4548
inputAnchorRef,
49+
prWidgetAnchorRef,
50+
prWidgetItems,
51+
prWidgetFooter,
4652
}: ExplorerMenuProps) {
4753
const [menuMode, setMenuMode] = useState<MenuMode>('hidden');
4854
const [menuPosition, setMenuPosition] = useState<{
@@ -74,10 +80,12 @@ export function useExplorerMenu({
7480
return filteredSlashCommands;
7581
case 'session-history':
7682
return sessionItems;
83+
case 'pr-widget':
84+
return prWidgetItems ?? [];
7785
default:
7886
return [];
7987
}
80-
}, [menuMode, filteredSlashCommands, sessionItems]);
88+
}, [menuMode, filteredSlashCommands, sessionItems, prWidgetItems]);
8189

8290
const close = useCallback(() => {
8391
setMenuMode('hidden');
@@ -210,7 +218,11 @@ export function useExplorerMenu({
210218
}
211219

212220
const anchorRef =
213-
menuMode === 'slash-commands-keyboard' ? inputAnchorRef : menuAnchorRef;
221+
menuMode === 'slash-commands-keyboard'
222+
? inputAnchorRef
223+
: menuMode === 'pr-widget'
224+
? prWidgetAnchorRef
225+
: menuAnchorRef;
214226
const isSlashCommand = menuMode === 'slash-commands-keyboard';
215227

216228
if (!anchorRef?.current) {
@@ -233,24 +245,31 @@ export function useExplorerMenu({
233245
const spacing = 8;
234246
const relativeTop = rect.top - panelRect.top;
235247
const relativeLeft = rect.left - panelRect.left;
248+
const relativeRight = panelRect.right - rect.right;
236249

237-
setMenuPosition(
238-
isSlashCommand
239-
? {
240-
bottom: `${panelRect.height - relativeTop + spacing}px`,
241-
left: `${relativeLeft}px`,
242-
}
243-
: {
244-
top: `${relativeTop + rect.height + spacing}px`,
245-
left: `${relativeLeft}px`,
246-
}
247-
);
248-
}, [isVisible, menuMode, menuAnchorRef, inputAnchorRef]);
250+
if (isSlashCommand) {
251+
setMenuPosition({
252+
bottom: `${panelRect.height - relativeTop + spacing}px`,
253+
left: `${relativeLeft}px`,
254+
});
255+
} else if (menuMode === 'pr-widget') {
256+
// Position below anchor, aligned to right edge
257+
setMenuPosition({
258+
top: `${relativeTop + rect.height + spacing}px`,
259+
right: `${relativeRight}px`,
260+
});
261+
} else {
262+
setMenuPosition({
263+
top: `${relativeTop + rect.height + spacing}px`,
264+
left: `${relativeLeft}px`,
265+
});
266+
}
267+
}, [isVisible, menuMode, menuAnchorRef, inputAnchorRef, prWidgetAnchorRef]);
249268

250269
const menu = (
251270
<Activity mode={isVisible ? 'visible' : 'hidden'}>
252271
<MenuPanel panelSize={panelSize} style={menuPosition} data-seer-menu-panel="">
253-
{menuItems.map((item, index) => (
272+
{menuItems.map((item: MenuItemProps, index: number) => (
254273
<MenuItem
255274
key={item.key}
256275
ref={el => {
@@ -274,6 +293,7 @@ export function useExplorerMenu({
274293
</ItemName>
275294
</MenuItem>
276295
)}
296+
{menuMode === 'pr-widget' && prWidgetFooter}
277297
</MenuPanel>
278298
</Activity>
279299
);
@@ -288,12 +308,22 @@ export function useExplorerMenu({
288308
}
289309
}, [menuMode, close, refetchSessions]);
290310

311+
// Handler for opening PR widget from button
312+
const openPRWidget = useCallback(() => {
313+
if (menuMode === 'pr-widget') {
314+
close();
315+
} else {
316+
setMenuMode('pr-widget');
317+
}
318+
}, [menuMode, close]);
319+
291320
return {
292321
menu,
293322
menuMode,
294323
isMenuOpen: menuMode !== 'hidden',
295324
closeMenu: close,
296325
openSessionHistory,
326+
openPRWidget,
297327
};
298328
}
299329

@@ -370,20 +400,22 @@ function useSessions({
370400
return [];
371401
}
372402

373-
return data.data.map(session => ({
374-
title: session.title,
375-
key: session.run_id.toString(),
376-
description: (
377-
<TimeSince
378-
tooltipPrefix="Last updated"
379-
date={moment.utc(session.last_triggered_at).toDate()}
380-
suffix="ago"
381-
/>
382-
),
383-
handler: () => {
384-
onChangeSession(session.run_id);
385-
},
386-
}));
403+
return data.data.map(
404+
(session: {last_triggered_at: moment.MomentInput; run_id: number; title: any}) => ({
405+
title: session.title,
406+
key: session.run_id.toString(),
407+
description: (
408+
<TimeSince
409+
tooltipPrefix="Last updated"
410+
date={moment.utc(session.last_triggered_at).toDate()}
411+
suffix="ago"
412+
/>
413+
),
414+
handler: () => {
415+
onChangeSession(session.run_id);
416+
},
417+
})
418+
);
387419
}, [data, isPending, isError, onChangeSession]);
388420

389421
return {

static/app/views/seerExplorer/explorerPanel.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ describe('ExplorerPanel', () => {
147147
runId: null,
148148
setRunId: jest.fn(),
149149
respondToUserInput: jest.fn(),
150+
createPR: jest.fn(),
150151
});
151152

152153
render(<ExplorerPanel isVisible />, {organization});

static/app/views/seerExplorer/explorerPanel.tsx

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import InputSection from 'sentry/views/seerExplorer/inputSection';
1616
import PanelContainers, {
1717
BlocksContainer,
1818
} from 'sentry/views/seerExplorer/panelContainers';
19+
import {usePRWidgetData} from 'sentry/views/seerExplorer/prWidget';
1920
import TopBar from 'sentry/views/seerExplorer/topBar';
2021
import type {Block, ExplorerPanelProps} from 'sentry/views/seerExplorer/types';
2122

@@ -36,8 +37,8 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
3637
const userScrolledUpRef = useRef<boolean>(false);
3738
const allowHoverFocusChange = useRef<boolean>(true);
3839
const sessionHistoryButtonRef = useRef<HTMLButtonElement>(null);
40+
const prWidgetButtonRef = useRef<HTMLButtonElement>(null);
3941

40-
// Custom hooks
4142
const {panelSize, handleMaxSize, handleMedSize} = usePanelSizing();
4243
const {
4344
sessionData,
@@ -49,11 +50,25 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
4950
interruptRequested,
5051
setRunId,
5152
respondToUserInput,
53+
createPR,
5254
} = useSeerExplorer();
5355

56+
// Extract repo_pr_states from session
57+
const repoPRStates = useMemo(
58+
() => sessionData?.repo_pr_states ?? {},
59+
[sessionData?.repo_pr_states]
60+
);
61+
5462
// Get blocks from session data or empty array
5563
const blocks = useMemo(() => sessionData?.blocks || [], [sessionData]);
5664

65+
// Get PR widget data for menu
66+
const {menuItems: prWidgetItems, menuFooter: prWidgetFooter} = usePRWidgetData({
67+
blocks,
68+
repoPRStates,
69+
onCreatePR: createPR,
70+
});
71+
5772
// Find the index of the last block that has todos (for special rendering)
5873
const latestTodoBlockIndex = useMemo(() => {
5974
for (let i = blocks.length - 1; i >= 0; i--) {
@@ -209,22 +224,26 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
209224

210225
const openFeedbackForm = useFeedbackForm();
211226

212-
const {menu, isMenuOpen, menuMode, closeMenu, openSessionHistory} = useExplorerMenu({
213-
clearInput: () => setInputValue(''),
214-
inputValue,
215-
focusInput,
216-
textAreaRef: textareaRef,
217-
panelSize,
218-
panelVisible: isVisible,
219-
slashCommandHandlers: {
220-
onMaxSize: handleMaxSize,
221-
onMedSize: handleMedSize,
222-
onNew: startNewSession,
223-
},
224-
onChangeSession: setRunId,
225-
menuAnchorRef: sessionHistoryButtonRef,
226-
inputAnchorRef: textareaRef,
227-
});
227+
const {menu, isMenuOpen, menuMode, closeMenu, openSessionHistory, openPRWidget} =
228+
useExplorerMenu({
229+
clearInput: () => setInputValue(''),
230+
inputValue,
231+
focusInput,
232+
textAreaRef: textareaRef,
233+
panelSize,
234+
panelVisible: isVisible,
235+
slashCommandHandlers: {
236+
onMaxSize: handleMaxSize,
237+
onMedSize: handleMedSize,
238+
onNew: startNewSession,
239+
},
240+
onChangeSession: setRunId,
241+
menuAnchorRef: sessionHistoryButtonRef,
242+
inputAnchorRef: textareaRef,
243+
prWidgetAnchorRef: prWidgetButtonRef,
244+
prWidgetItems,
245+
prWidgetFooter,
246+
});
228247

229248
const handlePanelBackgroundClick = useCallback(() => {
230249
setIsMinimized(false);
@@ -241,10 +260,11 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
241260
const target = event.target as Node;
242261
const menuElement = document.querySelector('[data-seer-menu-panel]');
243262

244-
// Don't close if clicking on the menu itself or the button
263+
// Don't close if clicking on the menu itself or the trigger buttons
245264
if (
246265
menuElement?.contains(target) ||
247-
sessionHistoryButtonRef.current?.contains(target)
266+
sessionHistoryButtonRef.current?.contains(target) ||
267+
prWidgetButtonRef.current?.contains(target)
248268
) {
249269
return;
250270
}
@@ -426,14 +446,19 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
426446
onUnminimize={handleUnminimize}
427447
>
428448
<TopBar
449+
blocks={blocks}
429450
isEmptyState={isEmptyState}
430451
isPolling={isPolling}
431452
isSessionHistoryOpen={isMenuOpen && menuMode === 'session-history'}
453+
onCreatePR={createPR}
432454
onFeedbackClick={handleFeedbackClick}
433455
onNewChatClick={startNewSession}
456+
onPRWidgetClick={openPRWidget}
434457
onSessionHistoryClick={openSessionHistory}
435458
onSizeToggleClick={handleSizeToggle}
436459
panelSize={panelSize}
460+
prWidgetButtonRef={prWidgetButtonRef}
461+
repoPRStates={repoPRStates}
437462
sessionHistoryButtonRef={sessionHistoryButtonRef}
438463
/>
439464
{menu}

0 commit comments

Comments
 (0)