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: diff --git a/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx b/frontend/src/lib/components/AgentPromptButton/AgentPromptButton.tsx index 8339b76c08d5..097238592cca 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,11 @@ 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 + onOpenChange?: (open: boolean) => void size?: AgentPromptButtonSize variant?: NonNullable /** Renders the dropdown open on first paint. Useful for visual regression snapshots. */ @@ -269,6 +275,9 @@ export function AgentPromptButton({ defaultAgentKey, agentKeys, agentSelectionMode = 'select', + labelMode = 'action', + menuClassName, + onOpenChange, size = 'default', variant = 'default', defaultOpen = false, @@ -304,7 +313,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 }) @@ -337,7 +349,13 @@ export function AgentPromptButton({ } return ( - + { + setOpen(nextOpen) + onOpenChange?.(nextOpen) + }} + > - + {actions.length > 1 && ( <> Content 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 e60ed4d7148a..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 { MCPHintActions } from './MCPHintActions' import { mcpHintLogic } from './mcpHintLogic' -import { MCPInstallCommand } from './MCPInstallCommand' 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/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.tsx b/frontend/src/lib/components/MCPHint/MCPUseCaseCard.tsx index e07a07caca7b..09579b88648b 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 { MCPHintActions } from './MCPHintActions' 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..1fb86b3cebb8 100644 --- a/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx +++ b/frontend/src/lib/components/MCPHint/mcpHintLogic.tsx @@ -14,9 +14,10 @@ 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' + function reportMCPHintDismissed(dismissType: 'surface' | 'all', surfaceKey?: string): void { posthog.capture('mcp hint dismissed', { dismiss_type: dismissType, @@ -98,6 +99,9 @@ export interface mcpHintLogicActions { | 'surveys.create' | 'workflows.create' } + keepHintOpen: (toastId: string) => { + toastId: string + } loadTopEvents: () => any loadTopEventsFailure: ( error: string, @@ -119,6 +123,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 +225,13 @@ 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 }), + keepHintOpen: (toastId: string) => ({ toastId }), }), reducers({ lastShownAt: [ @@ -232,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 @@ -241,9 +300,15 @@ 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 { - toast.info(, { - toastId: `mcp-hint-${surfaceKey}-${now}`, + // 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, autoClose: AUTO_DISMISS_MS, closeOnClick: false, draggable: false, @@ -265,7 +330,6 @@ export const mcpHintLogic = kea([ /> ), }) - actions.recordShown(now) actions.reportMCPHintShown(surfaceKey) } catch (error) { console.warn('[mcpHint] toast render failed', { surfaceKey, error }) @@ -282,5 +346,16 @@ 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 }) + }, + 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 }) + }, })), ]) 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.` +}