From d4c095c776f448ffe1e0a29ed2b089f7a9ef6377 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Tue, 6 Oct 2026 02:56:18 +0100 Subject: [PATCH 1/4] feat(mcp-hint): open the hint prompt in an agent with one button Replace the MCP install command in the MCP hint toast and the inline use case card with an agent button. It opens the surface's example prompt in Claude Code, Claude Desktop, VS Code, Cursor, Codex or PostHog Desktop, or copies it. The prompt names the project and tells the agent to ask for the install command when the PostHog MCP server is not connected. Raise the menu above react-toastify so it is visible from the toast. Track agent opens and use case card impressions. Co-Authored-By: Claude Opus 5.5 --- .../AgentPromptButton/AgentPromptButton.tsx | 14 ++++- .../lib/components/MCPHint/MCPAgentButton.tsx | 62 ++++++++++++++++++ .../lib/components/MCPHint/MCPHintToast.tsx | 4 +- .../components/MCPHint/MCPInstallCommand.tsx | 38 ----------- .../MCPHint/MCPUseCaseCard.test.tsx | 11 ++-- .../lib/components/MCPHint/MCPUseCaseCard.tsx | 12 +++- .../lib/components/MCPHint/mcpHintLogic.tsx | 63 +++++++++++++++++++ .../lib/components/MCPHint/prompts.test.ts | 18 +++++- .../src/lib/components/MCPHint/prompts.ts | 12 ++++ 9 files changed, 181 insertions(+), 53 deletions(-) create mode 100644 frontend/src/lib/components/MCPHint/MCPAgentButton.tsx delete mode 100644 frontend/src/lib/components/MCPHint/MCPInstallCommand.tsx diff --git a/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx b/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx index 8339b76c08d5..83cf6d97dcec 100644 --- a/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx +++ b/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx @@ -24,6 +24,7 @@ import { type ButtonProps as QuillButtonProps, } from 'lib/ui/quill' import { copyToClipboard } from 'lib/utils/copyToClipboard' +import { cn } from 'lib/utils/css-classes' import { maxGlobalLogic } from 'scenes/max/maxGlobalLogic' import { todayShellLogic } from '~/layout/today/todayShellLogic' @@ -69,6 +70,10 @@ export interface AgentPromptButtonProps { defaultAgentKey?: AgentPromptDestination agentKeys?: AgentPromptDestination[] agentSelectionMode?: 'select' | 'run' + /** `destination` names the agent on the main button ("Open in Cursor") instead of the prompt ("Open Fix prompt"). */ + labelMode?: 'action' | 'destination' + /** Extra classes for the dropdown menu, e.g. a higher z-index when the button sits inside a toast. */ + menuClassName?: string size?: AgentPromptButtonSize variant?: NonNullable /** Renders the dropdown open on first paint. Useful for visual regression snapshots. */ @@ -269,6 +274,8 @@ export function AgentPromptButton({ defaultAgentKey, agentKeys, agentSelectionMode = 'select', + labelMode = 'action', + menuClassName, size = 'default', variant = 'default', defaultOpen = false, @@ -304,7 +311,10 @@ export function AgentPromptButton({ ? defaultAgent : ((remembered?.agentKey ? availableAgents.find((a) => a.key === remembered.agentKey) : null) ?? defaultAgent) - const buttonLabel = `${activeAgent.verb} ${activeAction.label}` + const buttonLabel = + labelMode === 'destination' && activeAgent.key !== 'clipboard' + ? `Open in ${activeAgent.name}` + : `${activeAgent.verb} ${activeAction.label}` const selectAction = (actionKey: string): void => { setRemembered({ actionKey, agentKey: remembered?.agentKey ?? null }) @@ -365,7 +375,7 @@ export function AgentPromptButton({ - + {actions.length > 1 && ( <> Content diff --git a/frontend/src/lib/components/MCPHint/MCPAgentButton.tsx b/frontend/src/lib/components/MCPHint/MCPAgentButton.tsx new file mode 100644 index 000000000000..44bfcc5fcbe9 --- /dev/null +++ b/frontend/src/lib/components/MCPHint/MCPAgentButton.tsx @@ -0,0 +1,62 @@ +import { useActions, useValues } from 'kea' + +import { AgentPromptButton, type AgentPromptDestination } from 'lib/components/AgentPromptButton' +import { preflightLogic } from 'lib/logic/preflightLogic' +import { projectLogic } from 'scenes/projectLogic' + +import { type MCPHintPlacement, mcpHintLogic } from './mcpHintLogic' +import { buildMCPAgentPrompt, type SurfaceKey } from './prompts' + +// PostHog AI already runs inside the app, and a cloud session on claude.ai cannot reach a locally configured MCP server. +const MCP_AGENT_KEYS: AgentPromptDestination[] = [ + 'claude-code', + 'claude-desktop', + 'claude-code-vscode', + 'cursor', + 'codex', + 'posthog-code', + 'clipboard', +] + +export function MCPAgentButton({ + surfaceKey, + example, + placement, +}: { + surfaceKey: SurfaceKey + /** The example prompt shown on the surface. The agent receives it with MCP setup instructions. */ + example: string + placement: MCPHintPlacement +}): JSX.Element | null { + const { isCloudOrDev } = useValues(preflightLogic) + const { currentProjectId } = useValues(projectLogic) + const { reportAgentOpened } = useActions(mcpHintLogic) + + // The PostHog MCP server only serves cloud and dev instances, so a self-hosted user has nothing to connect to. + if (!isCloudOrDev) { + return null + } + + return ( + buildMCPAgentPrompt(example, currentProjectId), + }, + ]} + agentKeys={MCP_AGENT_KEYS} + defaultAgentKey="claude-code" + agentSelectionMode="run" + labelMode="destination" + size="sm" + variant="outline" + onRun={({ agentKey }) => reportAgentOpened(surfaceKey, placement, agentKey)} + // Radix menus sit below react-toastify's container, so a menu opened from the toast would render behind it. + menuClassName={placement === 'toast' ? 'z-[calc(var(--toastify-z-index)+1)]' : undefined} + data-attr={`mcp-hint-${placement}-agent-button`} + /> + ) +} diff --git a/frontend/src/lib/components/MCPHint/MCPHintToast.tsx b/frontend/src/lib/components/MCPHint/MCPHintToast.tsx index e60ed4d7148a..9b2c3ed76d51 100644 --- a/frontend/src/lib/components/MCPHint/MCPHintToast.tsx +++ b/frontend/src/lib/components/MCPHint/MCPHintToast.tsx @@ -3,8 +3,8 @@ import { useValues } from 'kea' import { IconSparkles } from '@posthog/icons' import { AgentBadgeRotator } from './AgentBadgeRotator' +import { MCPAgentButton } from './MCPAgentButton' import { mcpHintLogic } from './mcpHintLogic' -import { MCPInstallCommand } from './MCPInstallCommand' import { type SurfaceKey, formatDerivedToastPrompt, getSurfacePrompts } from './prompts' export function MCPHintToast({ @@ -29,7 +29,7 @@ export function MCPHintToast({
{prompt}
- + ) } diff --git a/frontend/src/lib/components/MCPHint/MCPInstallCommand.tsx b/frontend/src/lib/components/MCPHint/MCPInstallCommand.tsx deleted file mode 100644 index 7b1c80e8239d..000000000000 --- a/frontend/src/lib/components/MCPHint/MCPInstallCommand.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useValues } from 'kea' - -import { CommandBlock } from 'lib/components/CommandBlock/CommandBlock' -import { preflightLogic } from 'lib/logic/preflightLogic' -import { cn } from 'lib/utils/css-classes' - -import { MCP_INSTALL_COMMAND } from './constants' - -type Size = 'sm' | 'md' - -export function MCPInstallCommand({ - size = 'sm', - className, - silentCopy = false, -}: { - size?: Size - className?: string - /** Skip the "Copied … to clipboard" toast (used when the parent is itself a toast). */ - silentCopy?: boolean -}): JSX.Element | null { - const { isCloudOrDev } = useValues(preflightLogic) - - if (!isCloudOrDev) { - return null - } - - return ( - - ) -} diff --git a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx index 0f9b81b620b1..cf758db709b0 100644 --- a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx +++ b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx @@ -2,7 +2,7 @@ import '@testing-library/jest-dom' import { cleanup, fireEvent, render, screen } from '@testing-library/react' -import { copyToClipboard } from 'lib/utils/copyToClipboard' +import * as clipboard from 'lib/utils/copyToClipboard' import { initKeaTests } from '~/test/init' @@ -10,26 +10,23 @@ import { mcpHintLogic } from './mcpHintLogic' import { MCPUseCaseCard } from './MCPUseCaseCard' import { getSurfacePrompts } from './prompts' -jest.mock('lib/utils/copyToClipboard', () => ({ - copyToClipboard: jest.fn().mockResolvedValue(true), -})) - describe('MCPUseCaseCard', () => { beforeEach(() => { initKeaTests() - jest.clearAllMocks() + jest.restoreAllMocks() mcpHintLogic.mount() }) afterEach(cleanup) it('copies a surface configured to display a prompt', () => { + const copySpy = jest.spyOn(clipboard, 'copyToClipboard').mockResolvedValue(true) const [prompt] = getSurfacePrompts('ai_observability_evaluations.create').examples render() fireEvent.click(screen.getByLabelText('Copy to clipboard')) - expect(copyToClipboard).toHaveBeenCalledWith(prompt, 'prompt') + expect(copySpy).toHaveBeenCalledWith(prompt, 'prompt') }) it('keeps a single dynamic SQL example in the list display', () => { diff --git a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx index e07a07caca7b..12bcec4d1e7a 100644 --- a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx +++ b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx @@ -7,8 +7,8 @@ import { CodeSnippet } from 'lib/components/CodeSnippet/CodeSnippet' import { cn } from 'lib/utils/css-classes' import { AgentBadgeRotator } from './AgentBadgeRotator' +import { MCPAgentButton } from './MCPAgentButton' import { mcpHintLogic } from './mcpHintLogic' -import { MCPInstallCommand } from './MCPInstallCommand' import { getSurfacePrompts, type SurfaceKey } from './prompts' const FIRST_SEEN_KEY_PREFIX = 'mcp-use-case-card-first-seen:' @@ -43,7 +43,7 @@ export function MCPUseCaseCard({ forceDisplay?: boolean }): JSX.Element | null { const { effectiveOptOut, userRole, topEvents } = useValues(mcpHintLogic) - const { loadTopEvents } = useActions(mcpHintLogic) + const { loadTopEvents, reportUseCaseCardShown } = useActions(mcpHintLogic) const [expired] = useState(() => (expiresAfterMs ? getExpiryState(surfaceKey, expiresAfterMs).expired : false)) const triedLoadingEvents = useRef(false) @@ -58,6 +58,12 @@ export function MCPUseCaseCard({ } }, [willRender, surfaceKey, topEvents.length, loadTopEvents]) + useEffect(() => { + if (willRender) { + reportUseCaseCardShown(surfaceKey) + } + }, [willRender, surfaceKey, reportUseCaseCardShown]) + if (!willRender) { return null } @@ -90,7 +96,7 @@ export function MCPUseCaseCard({ )}
- +
) diff --git a/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx b/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx index 5b17335b883b..f69b2f81f0b6 100644 --- a/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx +++ b/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx @@ -17,6 +17,8 @@ import type { FeatureFlagsSet } from '../../logic/featureFlagLogic' import { MCPHintToast } from './MCPHintToast' import type { SurfaceKey } from './prompts' +export type MCPHintPlacement = 'toast' | 'card' + function reportMCPHintDismissed(dismissType: 'surface' | 'all', surfaceKey?: string): void { posthog.capture('mcp hint dismissed', { dismiss_type: dismissType, @@ -119,6 +121,54 @@ export interface mcpHintLogicActions { reenable: () => { value: true } + reportAgentOpened: ( + surfaceKey: SurfaceKey, + placement: MCPHintPlacement, + agentKey: string + ) => { + agentKey: string + placement: MCPHintPlacement + surfaceKey: + | 'actions.create' + | 'ai_observability_evaluations.create' + | 'alerts.create' + | 'annotations.create' + | 'cohorts.create' + | 'dashboards.create' + | 'data_warehouse_sources.create' + | 'data_warehouse_sources.update' + | 'early_access_features.create' + | 'error_tracking.assign' + | 'experiments.create' + | 'experiments.launch' + | 'feature_flags.create' + | 'feature_flags.update' + | 'insights.create' + | 'sql.execute' + | 'surveys.create' + | 'workflows.create' + } + reportUseCaseCardShown: (surfaceKey: SurfaceKey) => { + surfaceKey: + | 'actions.create' + | 'ai_observability_evaluations.create' + | 'alerts.create' + | 'annotations.create' + | 'cohorts.create' + | 'dashboards.create' + | 'data_warehouse_sources.create' + | 'data_warehouse_sources.update' + | 'early_access_features.create' + | 'error_tracking.assign' + | 'experiments.create' + | 'experiments.launch' + | 'feature_flags.create' + | 'feature_flags.update' + | 'insights.create' + | 'sql.execute' + | 'surveys.create' + | 'workflows.create' + } tryShowHint: ( surfaceKey: SurfaceKey, derivedPrompt?: string @@ -173,6 +223,12 @@ export const mcpHintLogic = kea([ dismissSurface: (surfaceKey: SurfaceKey) => ({ surfaceKey }), dismissAll: true, reenable: true, + reportAgentOpened: (surfaceKey: SurfaceKey, placement: MCPHintPlacement, agentKey: string) => ({ + surfaceKey, + placement, + agentKey, + }), + reportUseCaseCardShown: (surfaceKey: SurfaceKey) => ({ surfaceKey }), }), reducers({ lastShownAt: [ @@ -282,5 +338,12 @@ export const mcpHintLogic = kea([ reenable: () => { actions.updateUser({ hide_mcp_hints: false }) }, + // pinned: analytics event names and properties + reportAgentOpened: ({ surfaceKey, placement, agentKey }) => { + posthog.capture('mcp hint agent opened', { surface_key: surfaceKey, placement, agent: agentKey }) + }, + reportUseCaseCardShown: ({ surfaceKey }) => { + posthog.capture('mcp use case card shown', { surface_key: surfaceKey }) + }, })), ]) diff --git a/frontend/src/lib/components/MCPHint/prompts.test.ts b/frontend/src/lib/components/MCPHint/prompts.test.ts index 8ffddf33be8e..b59dc6aabe9e 100644 --- a/frontend/src/lib/components/MCPHint/prompts.test.ts +++ b/frontend/src/lib/components/MCPHint/prompts.test.ts @@ -1,6 +1,7 @@ import { UserRole } from '~/types' -import { FALLBACK_PROMPTS, formatDerivedToastPrompt, getSurfacePrompts } from './prompts' +import { MCP_INSTALL_COMMAND } from './constants' +import { buildMCPAgentPrompt, FALLBACK_PROMPTS, formatDerivedToastPrompt, getSurfacePrompts } from './prompts' describe('prompts', () => { describe('getSurfacePrompts', () => { @@ -51,4 +52,19 @@ describe('prompts', () => { expect(formatDerivedToastPrompt(' foo ')).toBe('"foo"') }) }) + + describe('buildMCPAgentPrompt', () => { + it.each([ + [42, 'Use the PostHog MCP server in PostHog project 42.'], + [null, 'Use the PostHog MCP server.'], + ])( + 'sends the task without display quotes and tells the agent how to connect (project %s)', + (projectId, mcpLine) => { + const prompt = buildMCPAgentPrompt('"Disable beta-banner flag"', projectId) + expect(prompt.startsWith('Disable beta-banner flag\n')).toBe(true) + expect(prompt).toContain(mcpLine) + expect(prompt).toContain(MCP_INSTALL_COMMAND) + } + ) + }) }) diff --git a/frontend/src/lib/components/MCPHint/prompts.ts b/frontend/src/lib/components/MCPHint/prompts.ts index b12768e4d027..f81958d26066 100644 --- a/frontend/src/lib/components/MCPHint/prompts.ts +++ b/frontend/src/lib/components/MCPHint/prompts.ts @@ -1,5 +1,7 @@ import { UserRole } from '~/types' +import { MCP_INSTALL_COMMAND } from './constants' + export const SURFACE_KEYS = [ 'feature_flags.create', 'feature_flags.update', @@ -564,3 +566,13 @@ export function formatDerivedToastPrompt(prompt: string): string { const trimmed = prompt.trim().replace(/^["']|["']$/g, '') return `"${trimmed}"` } + +/** + * Turns a displayed example into the prompt an external agent receives. The agent may not have the + * PostHog MCP server yet, so the prompt tells it to ask for the install instead of guessing. + */ +export function buildMCPAgentPrompt(example: string, projectId: number | null): string { + const task = example.trim().replace(/^["']|["']$/g, '') + const project = projectId ? ` in PostHog project ${projectId}` : '' + return `${task}\n\nUse the PostHog MCP server${project}. If the PostHog MCP server is not connected, stop and ask me to run \`${MCP_INSTALL_COMMAND}\` in a terminal, then try again.` +} From a3fbe14aac5dd4b6009cae5f53659397460cc23d Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Tue, 6 Oct 2026 03:08:20 +0100 Subject: [PATCH 2/4] feat(mcp-hint): offer the install command next to the agent button Show the install command and the agent button side by side, joined by "or", so a person can set up the MCP server directly or open the prompt in an agent. Opening the agent menu turns off the toast's auto-dismiss, because the menu renders outside the toast and hovering it does not pause the timer. Co-Authored-By: Claude Opus 5.5 --- .../AgentPromptButton/AgentPromptButton.tsx | 10 ++- .../lib/components/MCPHint/MCPAgentButton.tsx | 62 -------------- .../lib/components/MCPHint/MCPHintActions.tsx | 82 +++++++++++++++++++ .../lib/components/MCPHint/MCPHintToast.tsx | 14 +++- .../lib/components/MCPHint/MCPUseCaseCard.tsx | 4 +- .../lib/components/MCPHint/mcpHintLogic.tsx | 13 ++- 6 files changed, 115 insertions(+), 70 deletions(-) delete mode 100644 frontend/src/lib/components/MCPHint/MCPAgentButton.tsx create mode 100644 frontend/src/lib/components/MCPHint/MCPHintActions.tsx diff --git a/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx b/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx index 83cf6d97dcec..097238592cca 100644 --- a/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx +++ b/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx @@ -74,6 +74,7 @@ export interface AgentPromptButtonProps { labelMode?: 'action' | 'destination' /** Extra classes for the dropdown menu, e.g. a higher z-index when the button sits inside a toast. */ menuClassName?: string + onOpenChange?: (open: boolean) => void size?: AgentPromptButtonSize variant?: NonNullable /** Renders the dropdown open on first paint. Useful for visual regression snapshots. */ @@ -276,6 +277,7 @@ export function AgentPromptButton({ agentSelectionMode = 'select', labelMode = 'action', menuClassName, + onOpenChange, size = 'default', variant = 'default', defaultOpen = false, @@ -347,7 +349,13 @@ export function AgentPromptButton({ } return ( - + { + setOpen(nextOpen) + onOpenChange?.(nextOpen) + }} + > buildMCPAgentPrompt(example, currentProjectId), - }, - ]} - agentKeys={MCP_AGENT_KEYS} - defaultAgentKey="claude-code" - agentSelectionMode="run" - labelMode="destination" - size="sm" - variant="outline" - onRun={({ agentKey }) => reportAgentOpened(surfaceKey, placement, agentKey)} - // Radix menus sit below react-toastify's container, so a menu opened from the toast would render behind it. - menuClassName={placement === 'toast' ? 'z-[calc(var(--toastify-z-index)+1)]' : undefined} - data-attr={`mcp-hint-${placement}-agent-button`} - /> - ) -} diff --git a/frontend/src/lib/components/MCPHint/MCPHintActions.tsx b/frontend/src/lib/components/MCPHint/MCPHintActions.tsx new file mode 100644 index 000000000000..bb35446e1445 --- /dev/null +++ b/frontend/src/lib/components/MCPHint/MCPHintActions.tsx @@ -0,0 +1,82 @@ +import { useActions, useValues } from 'kea' + +import { AgentPromptButton, type AgentPromptDestination } from 'lib/components/AgentPromptButton' +import { CommandBlock } from 'lib/components/CommandBlock/CommandBlock' +import { preflightLogic } from 'lib/logic/preflightLogic' +import { projectLogic } from 'scenes/projectLogic' + +import { MCP_INSTALL_COMMAND } from './constants' +import { type MCPHintPlacement, mcpHintLogic } from './mcpHintLogic' +import { buildMCPAgentPrompt, type SurfaceKey } from './prompts' + +// PostHog AI already runs inside the app, and a cloud session on claude.ai cannot reach a locally configured MCP server. +const MCP_AGENT_KEYS: AgentPromptDestination[] = [ + 'claude-code', + 'claude-desktop', + 'claude-code-vscode', + 'cursor', + 'codex', + 'posthog-code', + 'clipboard', +] + +export function MCPHintActions({ + surfaceKey, + example, + placement, + onMenuOpen, +}: { + surfaceKey: SurfaceKey + /** The example prompt shown on the surface. The agent receives it with MCP setup instructions. */ + example: string + placement: MCPHintPlacement + onMenuOpen?: () => void +}): JSX.Element | null { + const { isCloudOrDev } = useValues(preflightLogic) + const { currentProjectId } = useValues(projectLogic) + const { reportAgentOpened } = useActions(mcpHintLogic) + + // The PostHog MCP server only serves cloud and dev instances, so a self-hosted user has nothing to connect to. + if (!isCloudOrDev) { + return null + } + + return ( +
+ +
+ or + buildMCPAgentPrompt(example, currentProjectId), + }, + ]} + agentKeys={MCP_AGENT_KEYS} + defaultAgentKey="claude-code" + agentSelectionMode="run" + labelMode="destination" + size="sm" + variant="outline" + onRun={({ agentKey }) => reportAgentOpened(surfaceKey, placement, agentKey)} + onOpenChange={(open) => open && onMenuOpen?.()} + // Radix menus sit below react-toastify's container, so a menu opened from the toast would render behind it. + menuClassName={placement === 'toast' ? 'z-[calc(var(--toastify-z-index)+1)]' : undefined} + data-attr={`mcp-hint-${placement}-agent-button`} + /> +
+
+ ) +} diff --git a/frontend/src/lib/components/MCPHint/MCPHintToast.tsx b/frontend/src/lib/components/MCPHint/MCPHintToast.tsx index 9b2c3ed76d51..4d990c088162 100644 --- a/frontend/src/lib/components/MCPHint/MCPHintToast.tsx +++ b/frontend/src/lib/components/MCPHint/MCPHintToast.tsx @@ -1,21 +1,24 @@ -import { useValues } from 'kea' +import { useActions, useValues } from 'kea' import { IconSparkles } from '@posthog/icons' import { AgentBadgeRotator } from './AgentBadgeRotator' -import { MCPAgentButton } from './MCPAgentButton' +import { MCPHintActions } from './MCPHintActions' import { mcpHintLogic } from './mcpHintLogic' import { type SurfaceKey, formatDerivedToastPrompt, getSurfacePrompts } from './prompts' export function MCPHintToast({ surfaceKey, derivedPrompt, + toastId, }: { surfaceKey: SurfaceKey /** If provided, replaces the per-surface default toast prompt with this action-derived string. */ derivedPrompt?: string + toastId?: string }): JSX.Element { const { userRole } = useValues(mcpHintLogic) + const { keepHintOpen } = useActions(mcpHintLogic) const prompt = derivedPrompt ? formatDerivedToastPrompt(derivedPrompt) : getSurfacePrompts(surfaceKey, { role: userRole }).toast @@ -29,7 +32,12 @@ export function MCPHintToast({
{prompt}
- + keepHintOpen(toastId) : undefined} + /> ) } diff --git a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx index 12bcec4d1e7a..09579b88648b 100644 --- a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx +++ b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx @@ -7,7 +7,7 @@ import { CodeSnippet } from 'lib/components/CodeSnippet/CodeSnippet' import { cn } from 'lib/utils/css-classes' import { AgentBadgeRotator } from './AgentBadgeRotator' -import { MCPAgentButton } from './MCPAgentButton' +import { MCPHintActions } from './MCPHintActions' import { mcpHintLogic } from './mcpHintLogic' import { getSurfacePrompts, type SurfaceKey } from './prompts' @@ -96,7 +96,7 @@ export function MCPUseCaseCard({ )}
- +
) diff --git a/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx b/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx index f69b2f81f0b6..72a47415867e 100644 --- a/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx +++ b/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx @@ -100,6 +100,9 @@ export interface mcpHintLogicActions { | 'surveys.create' | 'workflows.create' } + keepHintOpen: (toastId: string) => { + toastId: string + } loadTopEvents: () => any loadTopEventsFailure: ( error: string, @@ -229,6 +232,7 @@ export const mcpHintLogic = kea([ agentKey, }), reportUseCaseCardShown: (surfaceKey: SurfaceKey) => ({ surfaceKey }), + keepHintOpen: (toastId: string) => ({ toastId }), }), reducers({ lastShownAt: [ @@ -298,8 +302,9 @@ export const mcpHintLogic = kea([ } try { - toast.info(, { - toastId: `mcp-hint-${surfaceKey}-${now}`, + const toastId = `mcp-hint-${surfaceKey}-${now}` + toast.info(, { + toastId, autoClose: AUTO_DISMISS_MS, closeOnClick: false, draggable: false, @@ -342,6 +347,10 @@ export const mcpHintLogic = kea([ reportAgentOpened: ({ surfaceKey, placement, agentKey }) => { posthog.capture('mcp hint agent opened', { surface_key: surfaceKey, placement, agent: agentKey }) }, + keepHintOpen: ({ toastId }) => { + // The agent menu renders outside the toast, so hovering it does not pause the auto-dismiss timer. + toast.update(toastId, { autoClose: false }) + }, reportUseCaseCardShown: ({ surfaceKey }) => { posthog.capture('mcp use case card shown', { surface_key: surfaceKey }) }, From e928337e707f8eea20d873d53da272325e2fd67a Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Tue, 6 Oct 2026 03:48:34 +0100 Subject: [PATCH 3/4] fix(mcp-hint): load the hint toast lazily to keep the logic light Many logics import mcpHintLogic to call tryShowMCPHint, and Jest setup reaches it through superpowersLogic. The static toast import pulled the agent prompt button and its dependencies into all of them, so modules that tests mock were already loaded for real. Import the toast when a hint shows, and record the hint before the import so two quick actions cannot both show one. Restore the use case card test to its module mock. Co-Authored-By: Claude Opus 5.5 --- .../lib/components/MCPHint/MCPUseCaseCard.test.tsx | 11 +++++++---- frontend/src/lib/components/MCPHint/mcpHintLogic.tsx | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx index cf758db709b0..0f9b81b620b1 100644 --- a/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx +++ b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.test.tsx @@ -2,7 +2,7 @@ import '@testing-library/jest-dom' import { cleanup, fireEvent, render, screen } from '@testing-library/react' -import * as clipboard from 'lib/utils/copyToClipboard' +import { copyToClipboard } from 'lib/utils/copyToClipboard' import { initKeaTests } from '~/test/init' @@ -10,23 +10,26 @@ import { mcpHintLogic } from './mcpHintLogic' import { MCPUseCaseCard } from './MCPUseCaseCard' import { getSurfacePrompts } from './prompts' +jest.mock('lib/utils/copyToClipboard', () => ({ + copyToClipboard: jest.fn().mockResolvedValue(true), +})) + describe('MCPUseCaseCard', () => { beforeEach(() => { initKeaTests() - jest.restoreAllMocks() + jest.clearAllMocks() mcpHintLogic.mount() }) afterEach(cleanup) it('copies a surface configured to display a prompt', () => { - const copySpy = jest.spyOn(clipboard, 'copyToClipboard').mockResolvedValue(true) const [prompt] = getSurfacePrompts('ai_observability_evaluations.create').examples render() fireEvent.click(screen.getByLabelText('Copy to clipboard')) - expect(copySpy).toHaveBeenCalledWith(prompt, 'prompt') + expect(copyToClipboard).toHaveBeenCalledWith(prompt, 'prompt') }) it('keeps a single dynamic SQL example in the list display', () => { diff --git a/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx b/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx index 72a47415867e..1fb86b3cebb8 100644 --- a/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx +++ b/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx @@ -14,7 +14,6 @@ import { userLogic } from 'scenes/userLogic' import { UserType } from '~/types' import type { FeatureFlagsSet } from '../../logic/featureFlagLogic' -import { MCPHintToast } from './MCPHintToast' import type { SurfaceKey } from './prompts' export type MCPHintPlacement = 'toast' | 'card' @@ -292,7 +291,7 @@ export const mcpHintLogic = kea([ userRole: [(s) => [s.user], (user: UserType | null): string | null => user?.role_at_organization ?? null], }), listeners(({ values, actions }) => ({ - tryShowHint: ({ surfaceKey, derivedPrompt }) => { + tryShowHint: async ({ surfaceKey, derivedPrompt }) => { const now = Date.now() const sinceLast = values.lastShownAt ? now - values.lastShownAt : Infinity const cooldownActive = values.lastShownAt !== null && sinceLast < COOLDOWN_MS @@ -301,7 +300,12 @@ export const mcpHintLogic = kea([ return } + // Record the hint before the toast module loads, so a second action in that gap cannot show a second hint. + actions.recordShown(now) try { + // Many logics import this one to call tryShowMCPHint, so a static import would load the + // toast and the agent prompt button with every one of them, including in Jest setup. + const { MCPHintToast } = await import('./MCPHintToast') const toastId = `mcp-hint-${surfaceKey}-${now}` toast.info(, { toastId, @@ -326,7 +330,6 @@ export const mcpHintLogic = kea([ /> ), }) - actions.recordShown(now) actions.reportMCPHintShown(surfaceKey) } catch (error) { console.warn('[mcpHint] toast render failed', { surfaceKey, error }) From 1e69b2e913ea969c5798f2c6a4c09e9daad57735 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 03:37:25 +0000 Subject: [PATCH 4/4] chore(visual): update storybook baselines 25 updated Run: 1a7a28b0-df60-4790-afa9-dee55505e427 --- frontend/snapshots.yml | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/frontend/snapshots.yml b/frontend/snapshots.yml index 36e1756d4fe8..5bda355c528a 100644 --- a/frontend/snapshots.yml +++ b/frontend/snapshots.yml @@ -1709,53 +1709,53 @@ snapshots: components-markdown-notebook--view-mode--light: hash: v1.k794b7964.6e095c601e0f2eeb7a171bdcaf26cae50d4f59de68401eb89190912f26c17b77.IIbj_YIF2cO5RWtf6FhWbHrJIgQaGTTN6IWiRY36TlM components-mcp-hint-toast--annotations-create--dark: - hash: v1.k794b7964.eec2ed99e26c3e17bb863aee733524cf25a5ae76808f594bdc41c0eddf501b8f.x88L0pkIlEBj6PedsMSeWEWk04MeGS8PbIdYCJmYYjs + hash: v1.k794b7964.20e4661ed904460e9556f166b27aa5617174c5097f972484cfd0c4260856d062.q2R5_obK0HAAIO_W-akuUKwgFWHClbfrnniZQD1h0yQ components-mcp-hint-toast--annotations-create--light: - hash: v1.k794b7964.9268330fdbac65ef8a7944d6b7842d75fc24b65be0b6161aef1cc09e91137455.34awtLqUjMyV-e6oq1J7EZMTHRayyp7SaN1ckBfiRgQ + hash: v1.k794b7964.484c0e32888e43f94622441c7d7445f94597b7de72ddc7a4f7eecb3ba08a305d.pYDboq-S60-huq4qb3i4EoQVZUQxviQOShHlas6HNcU components-mcp-hint-toast--dashboards-create--dark: - hash: v1.k794b7964.8b0cb4193951738490a5828f9f0ee6098f64d230938fb472bf48efa14f7d1b04.s_3ppuxHf5j2rlmlHO-gEaVNt2SUyHVLHrquBY7YLQs + hash: v1.k794b7964.50a4b770d7fae82aaded43658f90f2aae4683249077f3710e14d119250fadafc.TeQItSeEg6UAYkkK5w2M-NF6mc1k0XHtFCPgzoQxNUo components-mcp-hint-toast--dashboards-create--light: - hash: v1.k794b7964.d435b1d7309aba3fa45ca0e77cd56434cd18d9192f892bff103b39af1630f9b6.rOGJNpfxSGNEG01itN3MAz-RuqXPoRKQS1kYd2p8DAE + hash: v1.k794b7964.96bd59f4e479ae35fc993cdc5f671060823509b87397274a0447e9437f875ee8.yejJdm7gBrEQvsT3tlTgRyMR3Q5ZRvmebbevazUWs8s components-mcp-hint-toast--experiments-create--dark: - hash: v1.k794b7964.4a565562ed3d671eaa2fc5ca17409379641fc5cdc06b875e06890b835fcd0df0.FoDB7oJzWF0dz0NTodUVhMnK8y_p83GijBSkhhBzQhg + hash: v1.k794b7964.8f0e8a8c38fae005ff0983b379892ddcb462aaaeb4da465c4abd53f478e31aed.5I9_JMpy_O8WL1JFXsGeDek1941rX2gdEq-rZu_CDlo components-mcp-hint-toast--experiments-create--light: - hash: v1.k794b7964.e0594102361f47e92ae32e064db951096618d3a427e08bef3a67af226c5b4713.jQASTcXuItAMOeoG769HCPopYEba1RI8eQG4S-bsfqE + hash: v1.k794b7964.5a6e0f13f2719e7508163d6bd4f3623d8f1418ad1c09edaf3997c4019e1b2227.VzdiFD3XpOQO1f9J7dJeZfK8Dc5K_Eex_0Me6F7iVi0 components-mcp-hint-toast--feature-flags-create--dark: - hash: v1.k794b7964.35db2c43cab2ed8173493ee17e9079170b259e56cada44145e224eb6d6d65cab.kpPpFg1dUT7XewfDblelk6-s9Yab2AOa5G6Txsd-vjM + hash: v1.k794b7964.0ca508677ba24a686cea0a72ba372669aeb5ff529b9a2db17e16e238888bf5c6.U2aehNkIeoJrYD0L5DMFKjlR16rRMyTCdKT447HUVyU components-mcp-hint-toast--feature-flags-create--light: - hash: v1.k794b7964.a656a5ca3bb672e1cdcab7dce7bcb8b2cbb70f0c5e61c0794aaee8504f8cc73c.Ff7x6fzxWa1s2JJ47jAU9HnfnA3P1ynwRD4ifkBbdHg + hash: v1.k794b7964.08bc945ae2637aa41d04c718e8dd65282a75810e6de20d34ba4a2cca6a856ed8.rGR-fMyxFRn19Q9bClwU_0kO7_68h1CAUXJP2Yjzo8Q components-mcp-hint-toast--feature-flags-update-with-derived-prompt--dark: - hash: v1.k794b7964.986a1d15152c36e917d9eacfc49a562b8b9f80f4dfdcc2fad2a2271964efb342._JiKbOmw9B9mTl8fOXAR6B4Vlq-iBqeoCyrodyPORTk + hash: v1.k794b7964.fd1db23f770fa047cd20ae3d57243e88de82182875c20a957734875949f2c792.BRx6z-LwNM1TgtmO3uuFjDeVU1CN1oF5PdYAePo62Xc components-mcp-hint-toast--feature-flags-update-with-derived-prompt--light: - hash: v1.k794b7964.68a7cf74686a3b5b1699e05e2890eb325db60ed9ebc84b058aa6f99b392e6cb2.NjBOm-cmRuJ-cDGkX3F29hgtaogrhTAXrazZGgzr__o + hash: v1.k794b7964.1e8bb1ede5957e8098785a9d2100729c190d4579fd2e730e453d59543ce79273.5KHHXriwqUVo16IcHmw-6c4GGyQbV5lxuyA-yg7EGiA components-mcp-hint-toast--sql-execute--dark: - hash: v1.k794b7964.e5991874972a9382c736026b9be0ba23f478f5cad7fb1a6c0b95ad697549b2b0.MlZppqkWPobycZrk6InXacMCB_Otr0Mh5rfsEaZl-6k + hash: v1.k794b7964.4d003cb82dc563496292ef97c51e268e2214fafaed5baa621fcc100563073928.bWjplBoGIvHVAgVQA0R_H5y3oL4xvxPiikLPnVsZsVY components-mcp-hint-toast--sql-execute--light: - hash: v1.k794b7964.0f1ab3bce13fe8842d1c1082aa3958d4f3036f45e22b446492de1ea952a1ddf2.I1aMoYXTWQHQ7HLM_sBl7E_ivm0w1OS4uyBXusN5xkQ + hash: v1.k794b7964.a9eb6891d53135b0c099fe4a4232df4ef4a02e1ca7f1d244f6fa63b444c15bb0.7lsmDPcqI3ahdsUMbvbyMLY1uy0W0QWxqQzVFPsDTMo components-mcp-use-case-card--ai-observability-evaluations-create--dark: - hash: v1.k794b7964.4c2ec3fdb53e346b6d7b85968a96f05e8bef9cf0e4ed0d3e59774d544f420c3e.AyRsIVMIdQVCIuvWbROr4r4C7cLb1thsT0ngYxja9V4 + hash: v1.k794b7964.f86242069939c409acae687f0931a7f1b223e54fbcf2903714000481afc23d1f.-aBPUrcSJiY1bKdAPUBiJphFrhVu3czkmRY7YVqTYdo components-mcp-use-case-card--ai-observability-evaluations-create--light: - hash: v1.k794b7964.c9f282f6b43684e6efc0b463e80d48cdaf6f86a548beb804fdc89c328e97c2ef.cafG-QYpXNUv3DfQrSVoLaOCAAhjVdHoJoQlBGT0IyQ + hash: v1.k794b7964.98cd4238199ab1e59d191934d23e5e8b1fca78bd30a359e846b80347c512aa74.cy0WTDMjyW3jvlUx5T3G7TzSGAWlWzjCufKHdCUBv1U components-mcp-use-case-card--dashboards-create--dark: - hash: v1.k794b7964.f11cb9fd9e4bf81c9809fca5a6e01b60feb1504d84a6a49eef90ed5313f66063.Nqxa-pxylBl5wjNLEyblqWq8gIqrUty24HUzaMxa9tU + hash: v1.k794b7964.3470949040a03ed1ce2d05116fbf2675961446bc52006513522314b60ad99a83.sPNQM-nzb7rlwVUPt4AAvPB41kPpTrmzX6k_lt-JjRQ components-mcp-use-case-card--dashboards-create--light: - hash: v1.k794b7964.e5bcd0b150dad00f41b10599b720a7b17d53de2aa9c701f132f3e5e0b1b76bf4.2-1hPkuiSoNh9I9YWblocq8HHNQsJ9qnBqW2VvQ50Rc + hash: v1.k794b7964.cdc89952e4e29b300f3a303a3c0efa92644835eb14045510634917284bae707a.w9hK0AB2gWt6UKUO684mjn9H0Ig1oW93efytPMapNcE components-mcp-use-case-card--experiments-create--dark: - hash: v1.k794b7964.e24fc589aa2645f8bb662eadd648609583b0cfa168cc870742aa52908c9f86f1.kxVh0EEyMMv-ZnghvAfryZ55Ce3c3cfUnBwbBd8ySP8 + hash: v1.k794b7964.3701a1dff85b657167d396c19ec7f5a589387c4bbd5649078c19d072894586b2.zVSxgesdgl8r9neWB6VyDO7cbUg-pE0NKif8puPVY30 components-mcp-use-case-card--experiments-create--light: - hash: v1.k794b7964.34e391df2000b13da2cb95ad96a774d37b79f7b50e93bc44da0b80895acb2dd0.K9uPX9I52fokIINNP-StL_9MsZ-OKzr0J6_Wufz8rKM + hash: v1.k794b7964.4a28e055eabc0d1a63e6d03d291952c331c64db4c4565bb51acf90962e640268.j-1_6BXCygll4xwk6qtlv64jGDJrZ_H6OxybHyYQc50 components-mcp-use-case-card--feature-flags-create--dark: - hash: v1.k794b7964.1878de18cc57905b859d36d770a94d383898c874c532b1b22e489f30a2faf11c.8hKFkkIp0HfDfm4aWQuS6-BRMZhI6MBZPYMnvsQgWXQ + hash: v1.k794b7964.3eebffb9cfdf80dd11593aacb351c46920fe06d882e77b3ac3ca9254d19aa49e.dLzBsJ-GD5ymvy4cNHKvf8t6dnwTqLqekMXvskXb60E components-mcp-use-case-card--feature-flags-create--light: - hash: v1.k794b7964.ad2a63f23858ee6a91fae1f9ce7439c1c49b32310e0158a9f9887989861636eb.dStLTLAgE7xIh2sG6I7DJzjCTMyOnJoPcuVeW4BOD8Q + hash: v1.k794b7964.15e53ba2dbdc244d2ede188f96152b19967b0f294ac9dc57cd969d795fba21b8.1TP2ipY4e7kuaoBnNcrnRLUfpsLI2MpGHo1QImxCruE components-mcp-use-case-card--insights-create--dark: - hash: v1.k794b7964.337bde2569c967a7d82923d46da99c3aeb8bfa5107ca1c046598fefd86661429.SPrZ5dGO026IDKquMRJhrPE7pibd864Akm1bdiget70 + hash: v1.k794b7964.ee1d2ca2c21553e049b5b6314a7ca35e5e68131228871a4e7b1db3f8bb171ba4.pu-eHTmCUWNebuMoFTq1uA0F5gEucHShAMSBiQ8G1Io components-mcp-use-case-card--insights-create--light: - hash: v1.k794b7964.36015a48bc7c7557b16b61e661a358549df5c39542fde9360954257005873ae5.BcftWNDC7Y4she2FkhwEUkJ69V58TGKis1IfCBjqU20 + hash: v1.k794b7964.456c9698ab1e19938b11cd9aa3a17688e927b7ec4b29ea2bdd424ef8dd941f74.pNUYI9bT3kwuxB7O_fr3vxt4IeH-3vajG7ZgzB-iWhY components-mcp-use-case-card--surveys-create--dark: - hash: v1.k794b7964.695db537c58523ccddbc27ca55efd0c26869b819dd1b61d68c8f62568f95662c.Elw0IPXosIZQX-CTjUA53qCNvx5pzjeDQcVO8ukReTs + hash: v1.k794b7964.15fbabb08961435e861f16da95aaefeb3304c30b466197488268e540b8e20533.jfPtk0lZ5lhpRemspY5PLR53qxrhMfyR_WYQDEYWuvo components-mcp-use-case-card--surveys-create--light: - hash: v1.k794b7964.693a3cba67de29bb7ecea8b07f3ae05b0201c2785fa2f2d4975e481b6fdc349e.ROQjP-mA4zsLzJifJv9uIKLgwuM0h84NwoFZJ90rSyc + hash: v1.k794b7964.2ce59a009bfce14f01a88f688627cf4b1def20c28ce5124288a54f9c62ffd3a6.wIhKopBY00iVCPnC9GB4QncFt-dCm8Gq9Uz6FYXBZ68 components-navpanelproductpush--all-products--dark: hash: v1.k794b7964.d6cc4da0e30950f4e3bf7338d86856529e48c45785bae5a85ccf4fd3ed7c3ad0.8n61Z9x6l3I3qfdAzk46yr7fAFHoYNdo3tjEKyJlcIo components-navpanelproductpush--all-products--light: @@ -6353,7 +6353,7 @@ snapshots: products-dashboards-add-insight-to-dashboard-modal--empty--dark: hash: v1.k794b7964.1f1953518f382e674f6f335a3d040ce9470a4e7a807f49b6d74ad44c37e2c633.Ye8sjd3P1ji8bG-FU5M0fzon3CiLEr0uZiGK0hF6qO0 products-dashboards-add-insight-to-dashboard-modal--empty--light: - hash: v1.k794b7964.346d66b5b410401624c9bf4a7cfad079fd12b9b9cb3dec759cfd9a8718afbb2a.0VdaNvoY6_cNL2PPHWpNFiIf7eJDyFaHf0m6dI1Dk-Q + hash: v1.k794b7964.e3ea78f7a9f66b25409d03711062966236f9a72653c3c42942a03f3393ff7d46.rQJ1xBzLABRtCPxO_V9_xDB1dBT_NG0DlSeRP8pnz8c products-dashboards-add-insight-to-dashboard-modal--with-more-insight-types--dark: hash: v1.k794b7964.010da65c2bb2e55daed38244a9918ab578c84f020a1c8df2ed847a4670005aec.M8-1Oet6RSJ7ulOk_R2yYMfKo4IUctrRZQulmG15BVk products-dashboards-add-insight-to-dashboard-modal--with-more-insight-types--light: