From d47d801609c1bdfa639ecb6545bcb2c6fd68b89f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:58:44 +0000 Subject: [PATCH] Add 'continue in your own agent' CTA to dashboard AI chats Co-Authored-By: Konstantin Wohlwend --- .../src/components/assistant-ui/thread.tsx | 102 +++++++++++++++++- .../src/components/commands/ask-ai.tsx | 1 + .../hexclave-companion/ai-chat-widget.tsx | 1 + 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/src/components/assistant-ui/thread.tsx b/apps/dashboard/src/components/assistant-ui/thread.tsx index 933b9ccea..5c25a1b4b 100644 --- a/apps/dashboard/src/components/assistant-ui/thread.tsx +++ b/apps/dashboard/src/components/assistant-ui/thread.tsx @@ -1,6 +1,7 @@ import { MarkdownText } from "@/components/assistant-ui/markdown-text"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Button, useToast } from "@/components/ui"; +import { getPublicEnvVar } from "@/lib/env"; import { cn } from "@/lib/utils"; import { validateComposerImageCount, @@ -19,13 +20,14 @@ import { useMessage, useThreadRuntime, type PendingAttachment, + type ThreadMessage, } from "@assistant-ui/react"; -import { ArrowClockwiseIcon, ArrowDownIcon, CaretLeftIcon, CaretRightIcon, CheckIcon, CopyIcon, ImageIcon, PaperPlaneRightIcon, PencilSimpleIcon, WarningCircle, XIcon } from "@phosphor-icons/react"; +import { ArrowClockwiseIcon, ArrowDownIcon, CaretLeftIcon, CaretRightIcon, CheckIcon, CopyIcon, ImageIcon, PaperPlaneRightIcon, PencilSimpleIcon, PlugsConnectedIcon, WarningCircle, XIcon } from "@phosphor-icons/react"; import { MAX_IMAGES_PER_MESSAGE, } from "@hexclave/shared/dist/ai/image-limits"; import { runAsynchronously } from "@hexclave/shared/dist/utils/promises"; -import { createContext, useContext, useEffect, useMemo, useRef, useState, type ComponentProps, type FC, type ReactNode } from "react"; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ComponentProps, type FC, type ReactNode } from "react"; type AssistantContentComponents = ComponentProps["components"]; const AssistantContentComponentsContext = createContext(undefined); @@ -68,7 +70,14 @@ export const Thread: FC<{ /** Overrides for the assistant message content slots (Text / tools / etc.). */ assistantContentComponents?: AssistantContentComponents, autoFocusComposer?: boolean, -}> = ({ useOffWhiteLightMode = false, composerPlaceholder, hideMessageActions = false, runningStatusMessages, composerAttachments = false, attachmentAdapter, welcome, assistantContentComponents, autoFocusComposer = true }) => { + /** + * Shows a subtle line beneath the composer inviting the user to continue the + * conversation in their own coding agent (via the Hexclave MCP server) or to + * copy the chat transcript so they can paste it elsewhere. Enable this on the + * general-purpose dashboard AI chats (command center, companion widget). + */ + agentEjectFooter?: boolean, +}> = ({ useOffWhiteLightMode = false, composerPlaceholder, hideMessageActions = false, runningStatusMessages, composerAttachments = false, attachmentAdapter, welcome, assistantContentComponents, autoFocusComposer = true, agentEjectFooter = false }) => { return ( @@ -111,6 +120,7 @@ export const Thread: FC<{
+ {agentEjectFooter && }
@@ -547,6 +557,92 @@ const Composer: FC<{ placeholder?: ComposerPlaceholder, autoFocus?: boolean }> = ); }; +function serializeThreadToMarkdown(messages: readonly ThreadMessage[]): string { + const blocks: string[] = []; + for (const message of messages) { + if (message.role !== "user" && message.role !== "assistant") continue; + const text = (typeof message.content === "string" + ? message.content + : message.content.map((part) => (part.type === "text" ? part.text : "")).join("") + ).trim(); + if (text.length === 0) continue; + blocks.push(`## ${message.role === "user" ? "User" : "Assistant"}\n\n${text}`); + } + return blocks.join("\n\n"); +} + +/** + * Subtle call-to-action beneath the composer that lets a user "eject" a chat + * they started in the dashboard into their own coding agent — either by wiring + * up the Hexclave MCP server (live docs + tools) or by copying the transcript + * to paste wherever they like. + */ +const AgentEjectFooter: FC = () => { + const threadRuntime = useThreadRuntime(); + const { toast } = useToast(); + const [copied, setCopied] = useState(false); + const [messageCount, setMessageCount] = useState(() => threadRuntime.getState().messages.length); + + useEffect(() => { + const sync = () => setMessageCount(threadRuntime.getState().messages.length); + sync(); + return threadRuntime.subscribe(sync); + }, [threadRuntime]); + + const mcpDocsUrl = useMemo(() => { + const base = (getPublicEnvVar("NEXT_PUBLIC_STACK_DOCS_BASE_URL") ?? "https://docs.hexclave.com").replace(/\/$/, ""); + return `${base}/guides/getting-started/ai-integration#option-3-mcp`; + }, []); + + const hasMessages = messageCount > 0; + + const handleCopy = useCallback(async () => { + const markdown = serializeThreadToMarkdown(threadRuntime.getState().messages); + if (markdown.length === 0) { + toast({ description: "No chat history to copy yet." }); + return; + } + await navigator.clipboard.writeText(markdown); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }, [threadRuntime, toast]); + + const linkClass = "inline-flex items-center gap-1 font-medium underline decoration-foreground/20 underline-offset-2 transition-colors hover:transition-none hover:text-foreground hover:decoration-foreground/40"; + + return ( +
+ Want to continue in your own agent? + + + Install our MCP server + + or + +
+ ); +}; + const ComposerAction: FC = () => { return (
diff --git a/apps/dashboard/src/components/commands/ask-ai.tsx b/apps/dashboard/src/components/commands/ask-ai.tsx index 2a0b525a5..8b159f388 100644 --- a/apps/dashboard/src/components/commands/ask-ai.tsx +++ b/apps/dashboard/src/components/commands/ask-ai.tsx @@ -88,6 +88,7 @@ const AIChatPreviewInner = memo(function AIChatPreview({ composerAttachments attachmentAdapter={attachmentAdapter} autoFocusComposer={false} + agentEjectFooter />
diff --git a/apps/dashboard/src/components/hexclave-companion/ai-chat-widget.tsx b/apps/dashboard/src/components/hexclave-companion/ai-chat-widget.tsx index 5586107cd..3143c42be 100644 --- a/apps/dashboard/src/components/hexclave-companion/ai-chat-widget.tsx +++ b/apps/dashboard/src/components/hexclave-companion/ai-chat-widget.tsx @@ -527,6 +527,7 @@ function AIChatWidgetInner({ welcome={} composerAttachments attachmentAdapter={attachmentAdapter} + agentEjectFooter />