Files
multica/packages/views/onboarding/steps/cli-install-instructions.tsx
Bohan Jiang d6540a1869 fix(clipboard): support copy over http:// via execCommand fallback (#3810)
navigator.clipboard is only exposed in a secure context (https or
localhost). On self-hosted instances served over plain http:// it is
undefined, so every copy / "copy all" / export button silently failed and
left the clipboard empty (GitHub #3781).

Add a shared copyText(text): Promise<boolean> helper in
@multica/ui/lib/clipboard that prefers the async Clipboard API and falls
back to a hidden <textarea> + document.execCommand('copy') for non-secure
contexts. Migrate all direct navigator.clipboard.writeText call sites
(code blocks, agent transcript copy-all, token / webhook / issue-link
copy, etc.) to it, gating success side-effects on the returned boolean,
and remove the now-redundant copyMarkdown wrapper. Secure-context users
keep the native path unchanged.

MUL-3068

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 14:55:23 +08:00

87 lines
2.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { Check, Copy, Terminal } from "lucide-react";
import { Card, CardContent } from "@multica/ui/components/ui/card";
import { CODE_LIGATURE_CLASS } from "@multica/ui/lib/code-style";
import { cn } from "@multica/ui/lib/utils";
import { copyText } from "@multica/ui/lib/clipboard";
import { useT } from "../../i18n";
const INSTALL_CMD =
"curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash";
const SETUP_CMD = "multica setup";
function CopyButton({ text }: { text: string }) {
const { t } = useT("onboarding");
const [copied, setCopied] = useState(false);
const handleCopy = () => {
void copyText(text).then((ok) => {
if (!ok) return;
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
return (
<button
type="button"
onClick={handleCopy}
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label={t(($) => $.cli_install.copy_aria)}
>
{copied ? (
<Check className="h-3.5 w-3.5 text-success" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</button>
);
}
function Step({ n, label, cmd }: { n: number; label: string; cmd: string }) {
return (
<div>
<p className="mb-1.5 text-xs font-medium text-foreground">
{n}. {label}
</p>
<div className="flex items-start gap-2 rounded-lg bg-muted px-3 py-2.5 font-mono text-sm">
<Terminal className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<code
className={cn(
"min-w-0 flex-1 whitespace-pre-wrap break-all",
CODE_LIGATURE_CLASS,
)}
>
{cmd}
</code>
<CopyButton text={cmd} />
</div>
</div>
);
}
/**
* CLI install instructions — two copy-and-run commands. Hardcoded because
* there's nothing environmental to infer: step 1 is the public install
* script, step 2 is the cloud `multica setup` which the CLI itself knows
* the endpoints for. Local development tests a self-host variant by
* typing the extended command directly in the terminal; no need to
* thread env vars through React.
*/
export function CliInstallInstructions() {
const { t } = useT("onboarding");
return (
<Card className="w-full">
<CardContent className="space-y-4 pt-4">
<p className="text-xs leading-[1.55] text-muted-foreground">
{t(($) => $.cli_install.intro)}
</p>
<Step n={1} label={t(($) => $.cli_install.step1_label)} cmd={INSTALL_CMD} />
<Step n={2} label={t(($) => $.cli_install.step2_label)} cmd={SETUP_CMD} />
</CardContent>
</Card>
);
}