From 39316fc2055b46cd1e13ff22ae9a36fbd08834f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Mon, 14 Sep 2026 12:01:04 +0200 Subject: [PATCH] Add agent actions to the prompt block (#4610) Co-authored-by: Claude Opus 5 --- .changeset/eighty-donkeys-tease.md | 5 + .../DocumentView/Prompt/PromptActions.tsx | 156 ++++++++++++++++++ .../DocumentView/Prompt/PromptClient.tsx | 110 +----------- .../DocumentView/Prompt/promptAction.ts | 95 +++++++++++ .../src/components/utils/getURLForLLM.ts | 4 +- packages/gitbook/src/lib/ai-agents.test.ts | 48 ++++++ packages/gitbook/src/lib/ai-agents.ts | 70 ++++++++ 7 files changed, 377 insertions(+), 111 deletions(-) create mode 100644 .changeset/eighty-donkeys-tease.md create mode 100644 packages/gitbook/src/components/DocumentView/Prompt/PromptActions.tsx create mode 100644 packages/gitbook/src/components/DocumentView/Prompt/promptAction.ts create mode 100644 packages/gitbook/src/lib/ai-agents.test.ts create mode 100644 packages/gitbook/src/lib/ai-agents.ts diff --git a/.changeset/eighty-donkeys-tease.md b/.changeset/eighty-donkeys-tease.md new file mode 100644 index 000000000..c1936d346 --- /dev/null +++ b/.changeset/eighty-donkeys-tease.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Give the prompt block a default "Copy prompt" action alongside "Open in Claude", "Open in Codex" and "Open in Cursor", and remember the visitor's last pick across every prompt block. diff --git a/packages/gitbook/src/components/DocumentView/Prompt/PromptActions.tsx b/packages/gitbook/src/components/DocumentView/Prompt/PromptActions.tsx new file mode 100644 index 000000000..f37e01b49 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/Prompt/PromptActions.tsx @@ -0,0 +1,156 @@ +'use client'; + +import React from 'react'; + +import { Icon } from '@gitbook/icons'; + +import { type PromptActionId, setPromptAction, usePromptAction } from './promptAction'; +import { + Button, + ButtonGroup, + DropdownMenu, + DropdownMenuItem, + ToggleChevron, +} from '@/components/primitives'; +import { tString, useLanguage } from '@/intl/client'; +import { AI_AGENTS, getAIAgent } from '@/lib/ai-agents'; +import { tcls } from '@/lib/tailwind'; + +/** How long the copy button shows its confirmation. */ +const COPIED_MESSAGE_DURATION = 1000; + +/** + * Actions of a prompt block: copying the prompt, or handing it to a coding agent. The visitor's + * last pick becomes the main button, here and in every other prompt block they come across. + */ +export function PromptActions(props: { prompt: string; openInAIProviders: boolean }) { + const { prompt, openInAIProviders } = props; + const language = useLanguage(); + const selectedAction = usePromptAction(); + const [copied, setCopied] = React.useState(false); + + React.useEffect(() => { + if (!copied) { + return; + } + + const timeout = setTimeout(() => { + setCopied(false); + }, COPIED_MESSAGE_DURATION); + + return () => { + clearTimeout(timeout); + }; + }, [copied]); + + const copyPrompt = () => { + navigator.clipboard.writeText(prompt); + setCopied(true); + }; + + // The pick follows the visitor from site to site, so fall back to copying wherever the agent + // actions are turned off — and with nothing to hand over, an agent link would open an agent on + // an empty prompt. + const action: PromptActionId = openInAIProviders && prompt ? selectedAction : 'copy'; + const agent = action === 'copy' ? null : getAIAgent(action); + + const mainButton = agent ? ( + + } + > + { + setPromptAction('copy'); + copyPrompt(); + }} + > + {tString(language, 'prompt_copy')} + + {AI_AGENTS.map((item) => ( + setPromptAction(item.id)} + > + {tString(language, 'open_in', item.label)} + + ))} + + + ) : ( + mainButton + )} + + ); +} + +/** + * The agents on offer, overlapped into a stack, so the menu advertises what it holds without + * spelling out three names next to a button that already has one. + */ +function AgentIconStack() { + return ( + + {AI_AGENTS.map((agent, index) => ( + 0 && '-ms-1.5' + )} + > + + + ))} + + ); +} diff --git a/packages/gitbook/src/components/DocumentView/Prompt/PromptClient.tsx b/packages/gitbook/src/components/DocumentView/Prompt/PromptClient.tsx index fa130041a..6bca4f152 100644 --- a/packages/gitbook/src/components/DocumentView/Prompt/PromptClient.tsx +++ b/packages/gitbook/src/components/DocumentView/Prompt/PromptClient.tsx @@ -5,14 +5,11 @@ import React from 'react'; import type { DocumentBlockPrompt } from '@gitbook/api'; import { Icon, type IconName } from '@gitbook/icons'; -import { Button, DropdownMenu, DropdownMenuItem, ToggleChevron } from '@/components/primitives'; -import { getURLForLLM } from '@/components/utils'; +import { PromptActions } from './PromptActions'; +import { ToggleChevron } from '@/components/primitives'; import { tString, useLanguage } from '@/intl/client'; import { tcls } from '@/lib/tailwind'; -const OPEN_IN_AI_PROVIDERS = ['claude', 'chatgpt', 'cursor'] as const; -type AIProviders = (typeof OPEN_IN_AI_PROVIDERS)[number]; - type PromptClientProps = DocumentBlockPrompt['data'] & { contentIcon: IconName | null; prompt: string; @@ -88,106 +85,3 @@ export function PromptClient(props: PromptClientProps) { ); } - -function PromptActions(props: { prompt: string; openInAIProviders: boolean }) { - const { prompt, openInAIProviders } = props; - - return ( -
- - {openInAIProviders ? : null} -
- ); -} - -// time in milliseconds to show the "Copied" message after copying a prompt -const COPIED_MESSAGE_DURATION = 1000; - -function CopyPromptButton(props: { prompt: string }) { - const { prompt } = props; - const language = useLanguage(); - const [copied, setCopied] = React.useState(false); - - React.useEffect(() => { - if (!copied) { - return; - } - - const timeout = setTimeout(() => { - setCopied(false); - }, COPIED_MESSAGE_DURATION); - - return () => { - clearTimeout(timeout); - }; - }, [copied]); - - return ( -