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 ? (
+
+ ) : (
+
+ );
+
+ return (
+ // Lifted above the header's overlay button, which otherwise swallows the clicks.
+
+ {openInAIProviders ? (
+
+ {mainButton}
+
+
+
+
+
+
+ }
+ >
+ {
+ 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 (
-