diff --git a/static/app/views/navigation/index.mobile.spec.tsx b/static/app/views/navigation/index.mobile.spec.tsx
index 03e81e8f3d26..b18cb57885ad 100644
--- a/static/app/views/navigation/index.mobile.spec.tsx
+++ b/static/app/views/navigation/index.mobile.spec.tsx
@@ -1,9 +1,11 @@
+import {BroadcastFixture} from 'sentry-fixture/broadcast';
import {GroupSearchViewFixture} from 'sentry-fixture/groupSearchView';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {UserFixture} from 'sentry-fixture/user';
import {
render,
+ renderGlobalModal,
screen,
userEvent,
within,
@@ -184,6 +186,34 @@ describe('mobile navigation', () => {
).toBeInTheDocument();
});
+ it("moves the Command Palette into the mobile row and What's New into the Help menu", async () => {
+ const context = navigationContext();
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/broadcasts/',
+ body: [BroadcastFixture({title: 'Mobile Broadcast', hasSeen: true})],
+ });
+
+ render(
+
+
+ ,
+ context
+ );
+ renderGlobalModal({organization: context.organization});
+
+ const mobileHeader = within(screen.getByRole('banner'));
+ expect(
+ mobileHeader.getByRole('button', {name: 'Command Palette'})
+ ).toBeInTheDocument();
+ expect(mobileHeader.getByRole('button', {name: 'Help'})).toBeInTheDocument();
+ expect(screen.queryByRole('button', {name: "What's New"})).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Help'}));
+ await userEvent.click(screen.getByRole('menuitemradio', {name: "What's New"}));
+
+ expect(await screen.findByText('Mobile Broadcast')).toBeInTheDocument();
+ });
+
describe('secondary nav route inference', () => {
it('opens secondary navigation by default when on a sub-view', async () => {
render(
diff --git a/static/app/views/navigation/mobileNavigation.tsx b/static/app/views/navigation/mobileNavigation.tsx
index a72d630b95fd..03021d37663d 100644
--- a/static/app/views/navigation/mobileNavigation.tsx
+++ b/static/app/views/navigation/mobileNavigation.tsx
@@ -7,6 +7,7 @@ import {Flex, type FlexProps, Stack} from '@sentry/scraps/layout';
import {SizeProvider} from '@sentry/scraps/sizeContext';
import {useScrollLock} from '@sentry/scraps/useScrollLock';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {IconMenu} from 'sentry/icons';
import {t} from 'sentry/locale';
import {useOnClickOutside} from 'sentry/utils/useOnClickOutside';
@@ -20,7 +21,12 @@ import {
PrimaryNavigationItems,
} from 'sentry/views/navigation/navigation';
import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
+import {
+ PrimaryNavigationHelpMenu,
+ useWhatsNewHelpMenuOptions,
+} from 'sentry/views/navigation/primary/helpMenu';
import {OrganizationDropdown} from 'sentry/views/navigation/primary/organizationDropdown';
+import {SearchButton} from 'sentry/views/navigation/searchButton';
import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components';
import {SecondaryNavigationContent} from 'sentry/views/navigation/secondary/content';
import {useSecondaryNavigation} from 'sentry/views/navigation/secondaryNavigationContext';
@@ -48,6 +54,15 @@ function MobileNavigationHeader(props: FlexProps<'header'>) {
);
}
+function MobileWhatsNewHelpMenu() {
+ const whatsNewHelpMenuOptions = useWhatsNewHelpMenuOptions();
+ return ;
+}
+
+function MobileHelpMenuFallback() {
+ return ;
+}
+
function MobilePrimaryNavigation() {
const {view} = useSecondaryNavigation();
@@ -136,7 +151,14 @@ export function MobileNavigation() {
/>
-
+
+
+ {buttonProps => }
+
+
+
+
+
diff --git a/static/app/views/navigation/navigation.tsx b/static/app/views/navigation/navigation.tsx
index cc05684feff0..409dc65bb05a 100644
--- a/static/app/views/navigation/navigation.tsx
+++ b/static/app/views/navigation/navigation.tsx
@@ -1,4 +1,4 @@
-import {Fragment, type RefObject, useMemo, useRef} from 'react';
+import {Fragment, type PropsWithChildren, type RefObject, useMemo, useRef} from 'react';
import {mergeProps} from '@react-aria/utils';
import {motion, type MotionProps} from 'framer-motion';
@@ -92,7 +92,12 @@ export function Navigation() {
paddingBottom="md"
>
-
+
+
+
+
+
+
@@ -282,7 +287,7 @@ export function PrimaryNavigationItems({listRef}: PrimaryNavigationItemsProps) {
/**
* Returns the list of items from the footer of the primary navigation
*/
-export function PrimaryNavigationFooterItems() {
+export function PrimaryNavigationFooterItems({children}: PropsWithChildren) {
const organization = useOrganization();
return (
@@ -302,10 +307,7 @@ export function PrimaryNavigationFooterItems() {
-
-
-
-
+ {children}
);
}
diff --git a/static/app/views/navigation/primary/components.tsx b/static/app/views/navigation/primary/components.tsx
index 3f533a8aca57..6d2bea34af1d 100644
--- a/static/app/views/navigation/primary/components.tsx
+++ b/static/app/views/navigation/primary/components.tsx
@@ -324,7 +324,7 @@ function PrimaryNavigationMenu(props: PrimaryNavigationMenuProps) {
>
) => {
if (organization) {
trackAnalytics('navigation.primary_item_clicked', {
@@ -361,7 +361,7 @@ function NavigationButton(props: DistributedOmit) {
const {layout} = usePrimaryNavigation();
return (
-
+
{p => (
) {
{...(layout === 'mobile' ? {variant: 'secondary'} : {variant: props.variant})}
/>
)}
-
+
);
}
+function PrimaryNavigationButtonContainer(props: React.ComponentProps) {
+ return ;
+}
+
/**
* @TODO(JonasBadalic) Scraps buttons have been setting overflow hidden onto the inner surface wrapper ever since
* we inherited that component, and we need to override that to ensure that the indicator is visible as it will
@@ -569,6 +573,7 @@ export const PrimaryNavigation = {
ListItem: PrimaryNavigationListItem,
Link: PrimaryNavigationLink,
Button: PrimaryNavigationButton,
+ ButtonContainer: PrimaryNavigationButtonContainer,
ButtonBar: PrimaryNavigationButtonBar,
Menu: PrimaryNavigationMenu,
ButtonOverlay: PrimaryNavigationButtonOverlay,
diff --git a/static/app/views/navigation/primary/helpMenu.spec.tsx b/static/app/views/navigation/primary/helpMenu.spec.tsx
index 2825044f5d30..9ea03ff5f5c8 100644
--- a/static/app/views/navigation/primary/helpMenu.spec.tsx
+++ b/static/app/views/navigation/primary/helpMenu.spec.tsx
@@ -1,10 +1,25 @@
+import {BroadcastFixture} from 'sentry-fixture/broadcast';
import {OrganizationFixture} from 'sentry-fixture/organization';
-import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+import {
+ render,
+ renderGlobalModal,
+ screen,
+ userEvent,
+} from 'sentry-test/reactTestingLibrary';
import {ConfigStore} from 'sentry/stores/configStore';
+import {ModalStore} from 'sentry/stores/modalStore';
import * as intercom from 'sentry/utils/intercom';
-import {PrimaryNavigationHelpMenu} from 'sentry/views/navigation/primary/helpMenu';
+import {
+ PrimaryNavigationHelpMenu,
+ useWhatsNewHelpMenuOptions,
+} from 'sentry/views/navigation/primary/helpMenu';
+
+function HelpMenuWithWhatsNew() {
+ const whatsNewOptions = useWhatsNewHelpMenuOptions();
+ return ;
+}
jest.mock('sentry/utils/intercom', () => ({
showIntercom: jest.fn(),
@@ -18,6 +33,7 @@ async function expandResourcesSubmenu() {
describe('PrimaryNavigationHelpMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
+ ModalStore.reset();
ConfigStore.set('supportEmail', 'support@sentry.io');
});
@@ -31,4 +47,22 @@ describe('PrimaryNavigationHelpMenu', () => {
expect(intercom.showIntercom).toHaveBeenCalledWith(organization.slug);
});
+
+ it("updates What's New when broadcasts finish loading", async () => {
+ const organization = OrganizationFixture();
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/broadcasts/`,
+ match: [MockApiClient.matchQuery({show: 'latest', limit: '3'})],
+ asyncDelay: 100,
+ body: [BroadcastFixture({id: '1', title: 'New Broadcast', hasSeen: true})],
+ });
+
+ render(, {organization});
+ renderGlobalModal({organization});
+
+ await userEvent.click(screen.getByRole('button', {name: 'Help'}));
+ await userEvent.click(screen.getByRole('menuitemradio', {name: "What's New"}));
+
+ expect(await screen.findByText('New Broadcast')).toBeInTheDocument();
+ });
});
diff --git a/static/app/views/navigation/primary/helpMenu.tsx b/static/app/views/navigation/primary/helpMenu.tsx
index 3a51cb4545f8..45897a987992 100644
--- a/static/app/views/navigation/primary/helpMenu.tsx
+++ b/static/app/views/navigation/primary/helpMenu.tsx
@@ -1,9 +1,12 @@
-import {useEffect} from 'react';
+import {Fragment, useEffect} from 'react';
import {Flex} from '@sentry/scraps/layout';
+import {openModal} from 'sentry/actionCreators/modal';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
+ IconBroadcast,
IconBuilding,
IconDiscord,
IconDocs,
@@ -26,8 +29,20 @@ import {showIntercom} from 'sentry/utils/intercom';
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
import {useOrganization} from 'sentry/utils/useOrganization';
import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
+import {
+ useWhatsNewBroadcasts,
+ WhatsNewContent,
+} from 'sentry/views/navigation/primary/whatsNew';
+
+interface PrimaryNavigationHelpMenuProps {
+ additionalItems?: MenuItemProps[];
+ indicator?: 'accent' | 'danger' | 'warning';
+}
-export function PrimaryNavigationHelpMenu() {
+export function PrimaryNavigationHelpMenu({
+ additionalItems = [],
+ indicator,
+}: PrimaryNavigationHelpMenuProps = {}) {
const organization = useOrganization();
const contactSupportItem = getContactSupportItem(organization);
const openForm = useFeedbackForm();
@@ -38,6 +53,7 @@ export function PrimaryNavigationHelpMenu() {
}, [organization]);
const items: MenuItemProps[] = [
+ ...additionalItems,
{
key: 'resources',
label: t('Resources'),
@@ -199,10 +215,42 @@ export function PrimaryNavigationHelpMenu() {
analyticsKey="help"
label={t('Help')}
icon={}
+ indicator={indicator}
/>
);
}
+export function useWhatsNewHelpMenuOptions(): PrimaryNavigationHelpMenuProps {
+ const {unseenPostIds} = useWhatsNewBroadcasts();
+
+ return {
+ additionalItems: [
+ {
+ key: 'whats-new',
+ label: t("What's New"),
+ leadingItems: (
+
+
+
+ ),
+ onAction() {
+ openModal(({Header, Body}) => (
+
+
+
+
+
+
+
+
+ ));
+ },
+ },
+ ],
+ indicator: unseenPostIds.length > 0 ? 'accent' : undefined,
+ };
+}
+
function getContactSupportItem(organization: Organization): MenuItemProps | null {
const supportEmail = ConfigStore.get('supportEmail');
diff --git a/static/app/views/navigation/primary/whatsNew.tsx b/static/app/views/navigation/primary/whatsNew.tsx
index 6f22ee87cd99..6b3593713de4 100644
--- a/static/app/views/navigation/primary/whatsNew.tsx
+++ b/static/app/views/navigation/primary/whatsNew.tsx
@@ -49,7 +49,7 @@ function BroadcastImage({src, alt}: {alt: string; src: string}) {
);
}
-function WhatsNewContent({
+function WhatsNewBroadcastList({
unseenPostIds,
isPending,
broadcasts = [],
@@ -186,7 +186,15 @@ function WhatsNewContent({
);
}
-export function PrimaryNavigationWhatsNew() {
+/**
+ * Recent broadcasts plus the derived unseen/deduped views of them. Shared by the
+ * standalone What's New button and the help menu entry that replaces it when the
+ * navigation row has no room for a dedicated trigger.
+ *
+ * Pass `enabled: false` from a caller that only conditionally surfaces
+ * broadcasts, so it does not fetch them when it will not show them.
+ */
+export function useWhatsNewBroadcasts({enabled = true}: {enabled?: boolean} = {}) {
const organization = useOrganization();
const {isPending, data: broadcasts} = useApiQuery(
[
@@ -196,6 +204,7 @@ export function PrimaryNavigationWhatsNew() {
{query: {show: 'latest', limit: '3'}},
],
{
+ enabled,
// Five minute stale time prevents window focus frequent refetches
staleTime: 1000 * 60 * 5,
// 10 minutes poll
@@ -224,6 +233,24 @@ export function PrimaryNavigationWhatsNew() {
});
}, [allBroadcasts]);
+ return {isPending, unseenPostIds, uniqueBroadcasts};
+}
+
+export function WhatsNewContent() {
+ const {isPending, unseenPostIds, uniqueBroadcasts} = useWhatsNewBroadcasts();
+
+ return (
+
+ );
+}
+
+export function PrimaryNavigationWhatsNew() {
+ const {unseenPostIds} = useWhatsNewBroadcasts();
+
const {
isOpen,
triggerProps: overlayTriggerProps,
@@ -243,11 +270,7 @@ export function PrimaryNavigationWhatsNew() {
/>
{isOpen && (
-
+
)}
diff --git a/static/app/views/navigation/searchButton.tsx b/static/app/views/navigation/searchButton.tsx
index a68ebb23b21b..77055c88dc70 100644
--- a/static/app/views/navigation/searchButton.tsx
+++ b/static/app/views/navigation/searchButton.tsx
@@ -1,7 +1,4 @@
-import {useTheme} from '@emotion/react';
-import styled from '@emotion/styled';
-
-import {Button} from '@sentry/scraps/button';
+import {Button, type ButtonProps} from '@sentry/scraps/button';
import {Hotkey} from '@sentry/scraps/hotkey';
import {Flex} from '@sentry/scraps/layout';
@@ -12,24 +9,29 @@ import {
} from 'sentry/components/commandPalette/ui/commandPaletteStateContext';
import {IconSearch} from 'sentry/icons';
import {t} from 'sentry/locale';
-import {useMedia} from 'sentry/utils/useMedia';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useSeerExplorerContext} from 'sentry/views/seerExplorer/useSeerExplorerContext';
import {isSeerExplorerEnabled} from 'sentry/views/seerExplorer/utils';
-export function SearchButton() {
- const theme = useTheme();
+export function SearchButton(props: Pick) {
const organization = useOrganization({allowNull: true});
const state = useCommandPaletteState();
const dispatch = useCommandPaletteDispatch();
const {openSeerExplorer} = useSeerExplorerContext();
- const isDesktop = useMedia(`(min-width: ${theme.breakpoints.md})`);
-
return (
- }
- aria-label={t('Search')}
+ aria-label={t('Command Palette')}
+ tooltipProps={{
+ title: (
+
+ {t('Command Palette')}
+
+
+ ),
+ }}
onClick={() => {
if (!organization) {
return;
@@ -43,19 +45,6 @@ export function SearchButton() {
isSeerExplorerEnabled(organization) ? openSeerExplorer : undefined
);
}}
- >
- {isDesktop ? (
-
- {t('Search')}
-
-
- ) : null}
-
+ />
);
}
-
-const StyledButton = styled(Button)`
- > span:last-child {
- overflow: visible;
- }
-`;
diff --git a/static/app/views/navigation/topBar.spec.tsx b/static/app/views/navigation/topBar.spec.tsx
index 09a1cdf8a74d..19baf6c41658 100644
--- a/static/app/views/navigation/topBar.spec.tsx
+++ b/static/app/views/navigation/topBar.spec.tsx
@@ -1,23 +1,50 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ThemeFixture} from 'sentry-fixture/theme';
-import {render, screen, within} from 'sentry-test/reactTestingLibrary';
-import {getEmotionRules} from 'sentry-test/utils';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
import {BreadcrumbList} from '@sentry/scraps/breadcrumbList';
+import {Flex} from '@sentry/scraps/layout';
import {TopBar} from './topBar';
-function renderTopBar() {
- render(
+const theme = ThemeFixture();
+
+jest.mock('sentry/components/feedbackButton/feedbackButton', () => ({
+ FeedbackButton: ({variant}: {variant?: string}) => (
+
+ ),
+}));
+
+jest.mock('sentry/views/seerExplorer/utils', () => ({
+ ...jest.requireActual('sentry/views/seerExplorer/utils'),
+ isSeerExplorerEnabled: () => true,
+}));
+
+function renderTopBar(width?: number) {
+ if (width !== undefined) {
+ jest.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(width);
+ }
+
+ const topBar = (
Page title
- ,
- {organization: OrganizationFixture()}
+
);
+
+ render({topBar}, {
+ organization: OrganizationFixture({
+ features: ['gen-ai-features', 'seer-explorer'],
+ }),
+ });
}
-describe('TopBar title slot', () => {
+describe('TopBar', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
it('renders the title as an h1 by default', () => {
renderTopBar();
@@ -26,20 +53,6 @@ describe('TopBar title slot', () => {
).toBeInTheDocument();
});
- it('hides the empty breadcrumbs outlet when only the title slot is used', () => {
- renderTopBar();
-
- const emptyBreadcrumbsOutlet = Array.from(
- screen.getByRole('banner').querySelectorAll('*')
- ).find(element =>
- getEmotionRules(element).some(
- rule => /display:\s*none/.test(rule) && /flex:\s*0 1 auto/.test(rule)
- )
- );
-
- expect(emptyBreadcrumbsOutlet).toBeDefined();
- });
-
it('keeps BreadcrumbList titles inside the single TopBar heading', () => {
render(
@@ -54,9 +67,35 @@ describe('TopBar title slot', () => {
{organization: OrganizationFixture()}
);
- expect(within(screen.getByRole('banner')).getAllByRole('heading')).toHaveLength(1);
+ expect(screen.getAllByRole('heading', {name: /.+/})).toHaveLength(1);
expect(
screen.getByRole('heading', {name: 'Current Issue', level: 1})
).toBeInTheDocument();
});
+
+ it('uses icon-only actions below sm', () => {
+ renderTopBar(Number.parseFloat(theme.container.sm) - 1);
+
+ expect(screen.queryByText('Command Palette')).not.toBeInTheDocument();
+ expect(screen.queryByText('Ask Seer')).not.toBeInTheDocument();
+ });
+
+ it('shows the Ask Seer label while keeping other actions compact at sm', () => {
+ renderTopBar(Number.parseFloat(theme.container.sm));
+ const askSeerButton = screen.getByRole('button', {name: 'Ask Seer'});
+
+ expect(screen.queryByText('Command Palette')).not.toBeInTheDocument();
+ expect(screen.getByText('Ask Seer')).toBeInTheDocument();
+ expect(screen.queryByText('/')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', {name: 'Give Feedback'})).toHaveAttribute(
+ 'data-variant',
+ 'transparent'
+ );
+
+ expect(screen.getAllByRole('button')).toEqual([
+ askSeerButton,
+ screen.getByRole('button', {name: 'Command Palette'}),
+ screen.getByRole('button', {name: 'Give Feedback'}),
+ ]);
+ });
});
diff --git a/static/app/views/navigation/topBar.tsx b/static/app/views/navigation/topBar.tsx
index ae485690692a..1be82d1a0f02 100644
--- a/static/app/views/navigation/topBar.tsx
+++ b/static/app/views/navigation/topBar.tsx
@@ -11,6 +11,7 @@ import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
import {t} from 'sentry/locale';
import {useOrganization} from 'sentry/utils/useOrganization';
import {SearchButton} from 'sentry/views/navigation/searchButton';
+import {useTopBarActionDisplay} from 'sentry/views/navigation/useTopBarActionDisplay';
import {useTopOffset} from 'sentry/views/navigation/useTopOffset';
import {AskSeerButton} from 'sentry/views/seerExplorer/components/askSeerButton';
import {useSeerExplorerChatState} from 'sentry/views/seerExplorer/seerExplorerChatStateContext';
@@ -33,6 +34,7 @@ function TopBarContent() {
const {pageContentTop} = useTopOffset();
const organization = useOrganization({allowNull: true});
+ const {isSearchInMobileRow} = useTopBarActionDisplay();
useEffect(() => {
document.documentElement.style.setProperty(TOP_BAR_HEIGHT_CSS_VAR, pageContentTop);
@@ -88,16 +90,11 @@ function TopBarContent() {
containerType="inline-size"
>
- {(props, hasConsumers) => (
-
- )}
+ {(props, hasConsumers) =>
+ hasConsumers ? (
+
+ ) : null
+ }
@@ -113,15 +110,19 @@ function TopBarContent() {
- {props => }
+ {(props, hasConsumers) =>
+ hasConsumers ? : null
+ }
- {props => }
+ {(props, hasConsumers) =>
+ hasConsumers ? : null
+ }
-
{isSeerExplorerEnabled(organization) ? : null}
+ {isSearchInMobileRow ? null : }
{props => (
diff --git a/static/app/views/navigation/topBarActions.spec.tsx b/static/app/views/navigation/topBarActions.spec.tsx
new file mode 100644
index 000000000000..f708413d6585
--- /dev/null
+++ b/static/app/views/navigation/topBarActions.spec.tsx
@@ -0,0 +1,58 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ThemeFixture} from 'sentry-fixture/theme';
+
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import {Flex} from '@sentry/scraps/layout';
+
+import {SearchButton} from 'sentry/views/navigation/searchButton';
+import {AskSeerButton} from 'sentry/views/seerExplorer/components/askSeerButton';
+
+const theme = ThemeFixture();
+
+function renderActions(width = 0) {
+ jest.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(width);
+
+ return render(
+
+
+
+ ,
+ {organization: OrganizationFixture()}
+ );
+}
+
+describe('top bar actions', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('shows tooltips for icon-only actions', async () => {
+ renderActions();
+
+ const searchButton = screen.getByRole('button', {name: 'Command Palette'});
+ const askSeerButton = screen.getByRole('button', {name: 'Ask Seer'});
+ expect(screen.queryByText('Command Palette')).not.toBeInTheDocument();
+ expect(screen.queryByText('Ask Seer')).not.toBeInTheDocument();
+
+ await userEvent.hover(searchButton);
+ expect(screen.getByText('Command Palette')).toBeInTheDocument();
+
+ await userEvent.hover(askSeerButton);
+ expect(screen.getByText('Ask Seer')).toBeInTheDocument();
+ });
+
+ it('keeps Command Palette compact and exposes the Ask Seer shortcut in its tooltip at sm', async () => {
+ renderActions(Number.parseFloat(theme.container.sm));
+
+ const askSeerButton = screen.getByRole('button', {name: 'Ask Seer'});
+ expect(screen.getByRole('button', {name: 'Command Palette'})).toBeInTheDocument();
+ expect(screen.queryByText('Command Palette')).not.toBeInTheDocument();
+ expect(screen.getByText('Ask Seer')).toBeInTheDocument();
+ expect(screen.queryByText('K')).not.toBeInTheDocument();
+
+ await userEvent.hover(askSeerButton);
+ expect(screen.getByText('Ctrl', {selector: 'kbd'})).toBeInTheDocument();
+ expect(screen.getByText('/', {selector: 'kbd'})).toBeInTheDocument();
+ });
+});
diff --git a/static/app/views/navigation/useTopBarActionDisplay.tsx b/static/app/views/navigation/useTopBarActionDisplay.tsx
new file mode 100644
index 000000000000..c889b0be69f2
--- /dev/null
+++ b/static/app/views/navigation/useTopBarActionDisplay.tsx
@@ -0,0 +1,21 @@
+import {useResponsivePropValue} from '@sentry/scraps/layout';
+
+import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
+
+export type TopBarActionDisplay = 'icon' | 'label';
+
+export function useTopBarActionDisplay(): {
+ display: TopBarActionDisplay;
+ isSearchInMobileRow: boolean;
+} {
+ const isSearchInMobileRow = usePrimaryNavigation().layout === 'mobile';
+ const responsiveDisplay = useResponsivePropValue({
+ zero: 'icon',
+ sm: 'label',
+ });
+
+ return {
+ display: isSearchInMobileRow ? 'icon' : responsiveDisplay,
+ isSearchInMobileRow,
+ };
+}
diff --git a/static/app/views/seerExplorer/components/askSeerButton.tsx b/static/app/views/seerExplorer/components/askSeerButton.tsx
index 9fcbb7673425..ac5182388938 100644
--- a/static/app/views/seerExplorer/components/askSeerButton.tsx
+++ b/static/app/views/seerExplorer/components/askSeerButton.tsx
@@ -10,12 +10,15 @@ import {Text} from '@sentry/scraps/text';
import {IconSeer} from 'sentry/icons';
import {t} from 'sentry/locale';
+import {useTopBarActionDisplay} from 'sentry/views/navigation/useTopBarActionDisplay';
import {useSeerExplorerContext} from 'sentry/views/seerExplorer/useSeerExplorerContext';
export function AskSeerButton() {
const {isOpen, toggleSeerExplorer, sessionState: state} = useSeerExplorerContext();
+ const {display: actionDisplay} = useTopBarActionDisplay();
const showMessageIndicator = !isOpen && state === 'done-thinking';
const prefersReducedMotion = useReducedMotion();
+ const isIconOnly = actionDisplay === 'icon';
return (
+ {t('Ask Seer')}
+
+
+ ),
+ }}
icon={
-
+
+
+ {showMessageIndicator && isIconOnly ? : null}
+
}
>
-
-
- {t('Ask Seer')}
-
-
-
-
- {state === 'thinking' ? (
-
- {prefersReducedMotion ? (
- {t('Thinking...')}
- ) : (
-
- )}
-
- ) : null}
- {showMessageIndicator ? (
+ {isIconOnly ? null : (
+
-
+ {t('Ask Seer')}
- ) : null}
-
+ {state === 'thinking' ? (
+
+ {prefersReducedMotion ? (
+ {t('Thinking...')}
+ ) : (
+
+ )}
+
+ ) : null}
+ {showMessageIndicator ? : null}
+
+ )}
);
}
+function MessageIndicator() {
+ return (
+
+
+
+ );
+}
+
const SeerLoader = styled(Flex)`
color: ${p => p.theme.tokens.graphics.accent.vibrant};
`;