/* Copyright 2026 Marimo. All rights reserved. */ import { CheckIcon, CopyIcon } from "lucide-react"; import React, { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { copyToClipboard } from "@/utils/copy"; import { Events } from "@/utils/events"; import { Tooltip } from "@/components/ui/tooltip"; import { assertNever } from "@/utils/assertNever"; import { asRemoteURL, useRuntimeManager } from "@/core/runtime/config"; import { API } from "@/core/network/api"; type AgentTab = "claude" | "codex" | "opencode" | "prompt"; const TERMINAL_TABS = ["claude", "codex", "opencode"] as const; function getMarimoCommand(): string { return import.meta.env.DEV ? "uv run marimo" : "uvx marimo@latest"; } function getTerminalCommand( agent: Exclude, url: string, withToken: boolean, ): string { const tokenFlag = withToken ? " --with-token" : ""; const base = `${getMarimoCommand()} pair prompt --url '${url}'${tokenFlag}`; switch (agent) { case "claude": return `claude "$(${base} --claude)"`; case "codex": return `codex "$(${base} --codex)"`; case "opencode": return `opencode --prompt "$(${base} --opencode)"`; default: assertNever(agent); } } function getRawPrompt(url: string, token: string | null): string { const tokenHint = token ? `\n\nUse this auth token when calling \`execute-code.sh\`: \`execute-code.sh --url '${url}' --token '${token}'\`.` : ""; return [ "Use the /marimo-pair skill to pair-program on a running marimo notebook.", "", `Connect to the notebook at: ${url}`, "", `Use \`execute-code.sh --url ${url}\` from the marimo-pair skill to execute code in the notebook.${tokenHint}`, "", "Once you are connected, send a fun toast (mo.status.toast(...)) to the user inside marimo letting them know you're ready to pair.", ].join("\n"); } function maskToken(token: string): string { if (token.length <= 4) { return "****"; } return `${"*".repeat(Math.min(token.length - 4, 8))}${token.slice(-4)}`; } const SKILL_INSTALL = "npx skills add marimo-team/marimo-pair"; const AGENT_LABELS: Record = { claude: "Claude", codex: "Codex", opencode: "OpenCode", prompt: "Prompt", }; function useAuthToken(): string | null { const [token, setToken] = useState(null); useEffect(() => { fetch(asRemoteURL("/auth/token").href, { headers: API.headers(), }) .then((res) => res.ok ? (res.json() as Promise<{ token: string | null }>) : null, ) .then((data) => setToken(data?.token ?? null)) .catch(() => setToken(null)); }, []); return token; } export const PairWithAgentModal: React.FC<{ onClose: () => void; }> = ({ onClose }) => { const [activeTab, setActiveTab] = useState("claude"); const runtimeManager = useRuntimeManager(); const authToken = useAuthToken(); const hasToken = Boolean(authToken); const remoteUrl = runtimeManager.httpURL.toString(); return ( Pair with an agent Use an AI coding agent to pair-program on this notebook.{" "} Learn more .
setActiveTab(v as AgentTab)} > {(["claude", "codex", "opencode", "prompt"] as const).map((tab) => ( {AGENT_LABELS[tab]} ))} {TERMINAL_TABS.map((tab) => ( {hasToken && authToken && ( )} ))}
); }; const Step: React.FC<{ index: number; title: string; hint?: string; children: React.ReactNode; }> = ({ index, title, hint, children }) => (
{index}. {title} {hint && {hint}}
{children}
); const CommandBlock: React.FC<{ command: string; display?: string; multiline?: boolean; }> = ({ command, display, multiline = false }) => { const [copied, setCopied] = useState(false); const copy = Events.stopPropagation(async (e) => { e.preventDefault(); await copyToClipboard(command); setCopied(true); setTimeout(() => setCopied(false), 2000); }); if (multiline) { return (
          {display ?? command}
        
); } return (
{display ?? command}
); };