Replaces the last direct navigator.clipboard.writeText() call sites across dashboard components and the reports plugin with the shared copyTextToClipboard helper, fixing copy actions that crashed or silently failed on non-secure origins (HTTP/mobile). - Migrated AgentDetailView, AgentErrorDetailsModal, CliBinaryPanel, GitManagerModal, LoginInstructions, PrPanel, SecretsView, and StashConflictModal to use copyTextToClipboard (secure-context guard + execCommand fallback, boolean result handling) instead of calling navigator.clipboard directly. - Migrated the fusion-plugin-reports ShareBlocksPanel to the same helper and added a vitest alias so the plugin's subpath import resolves to the dashboard's copyToClipboard util instead of collapsing to its package root. - Added ./app/utils/copyToClipboard subpath export to @fusion/dashboard's package.json. - Added/extended tests covering copy success, fallback, and failure paths for AgentDetailView, CliBinaryPanel, AgentErrorDetailsModal, GitManagerModal, SecretsView, StashConflictModal, and ShareBlocksPanel. - Added a patch changeset documenting the fix for @runfusion/fusion. Files changed: .changeset/fn-7885-clipboard-migration.md | 7 ++ packages/dashboard/app/components/AgentDetailView.tsx | 16 ++++- packages/dashboard/app/components/AgentErrorDetailsModal.tsx | 5 +- packages/dashboard/app/components/CliBinaryPanel.tsx | 16 +++-- packages/dashboard/app/components/GitManagerModal.tsx | 16 ++++- packages/dashboard/app/components/LoginInstructions.tsx | 21 +++--- packages/dashboard/app/components/PrPanel.tsx | 9 ++- packages/dashboard/app/components/SecretsView.tsx | 11 ++- packages/dashboard/app/components/StashConflictModal.tsx | 13 ++-- packages/dashboard/app/components/__tests__/AgentDetailView.copy.test.tsx | 56 +++++++++++++++ packages/dashboard/app/components/__tests__/AgentErrorDetailsModal.test.tsx | 18 +++++ packages/dashboard/app/components/__tests__/CliBinaryPanel.copy.test.tsx | 68 ++++++++++++++++++ packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx | 23 ++++++ packages/dashboard/app/components/__tests__/SecretsView.test.tsx | 82 ++++++++++++++++++++++ packages/dashboard/app/components/__tests__/StashConflictModal.test.tsx | 25 ++++++- packages/dashboard/package.json | 4 ++ plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx | 5 +- plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx | 43 +++++++++++- plugins/fusion-plugin-reports/vitest.config.ts | 2 + 19 files changed, 402 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-7885 Fusion-Task-Lineage: 122a54ea-962b-4ae1-98ac-c28e03f4f8ca Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
import "./AgentErrorDetailsModal.css";
|
|
import { useMemo, useState } from "react";
|
|
import { AlertCircle, Check, Copy, ExternalLink } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
|
import { copyTextToClipboard } from "../utils/copyToClipboard";
|
|
|
|
const DEFAULT_ISSUE_URL = "https://github.com/Runfusion/Fusion/issues/new";
|
|
|
|
export interface AgentErrorIssueContext {
|
|
surface: string;
|
|
agentId?: string;
|
|
agentName?: string;
|
|
agentState?: string;
|
|
runId?: string;
|
|
taskId?: string;
|
|
timestamp?: string;
|
|
}
|
|
|
|
interface AgentErrorDetailsModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
errorText: string;
|
|
issueContext: AgentErrorIssueContext;
|
|
}
|
|
|
|
export function buildAgentErrorIssueUrl(errorText: string, context: AgentErrorIssueContext): string {
|
|
const title = `[Agent Error] ${context.surface}${context.agentName ? ` - ${context.agentName}` : ""}`;
|
|
const bodyLines = [
|
|
"## Agent Error Report",
|
|
"",
|
|
`- Surface: ${context.surface}`,
|
|
`- Agent ID: ${context.agentId ?? "unknown"}`,
|
|
`- Agent Name: ${context.agentName ?? "unknown"}`,
|
|
`- Agent State: ${context.agentState ?? "unknown"}`,
|
|
`- Run ID: ${context.runId ?? "n/a"}`,
|
|
`- Task ID: ${context.taskId ?? "n/a"}`,
|
|
`- Timestamp: ${context.timestamp ?? new Date().toISOString()}`,
|
|
"",
|
|
"## Error",
|
|
"```text",
|
|
errorText,
|
|
"```",
|
|
];
|
|
|
|
const params = new URLSearchParams({
|
|
title,
|
|
body: bodyLines.join("\n"),
|
|
});
|
|
|
|
return `${DEFAULT_ISSUE_URL}?${params.toString()}`;
|
|
}
|
|
|
|
export function AgentErrorDetailsModal({ open, onClose, errorText, issueContext }: AgentErrorDetailsModalProps) {
|
|
const [copied, setCopied] = useState(false);
|
|
const { t } = useTranslation("app");
|
|
const issueUrl = useMemo(() => buildAgentErrorIssueUrl(errorText, issueContext), [errorText, issueContext]);
|
|
const overlayDismissProps = useOverlayDismiss(onClose);
|
|
|
|
if (!open) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="modal-overlay open" {...overlayDismissProps} role="dialog" aria-modal="true" aria-label={t("agentError.dialogLabel", "Agent error details")}>
|
|
<div className="modal agent-error-modal">
|
|
<div className="modal-header">
|
|
<h2 className="modal-title">
|
|
<AlertCircle size={16} />
|
|
{t("agentError.title", "Agent Error Details")}
|
|
</h2>
|
|
<button className="modal-close" onClick={onClose} aria-label={t("common.close", "Close")}>×</button>
|
|
</div>
|
|
<div className="agent-error-modal__content">
|
|
<pre className="agent-error-modal__error">{errorText}</pre>
|
|
</div>
|
|
<div className="modal-actions">
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm"
|
|
onClick={() => {
|
|
/* FNXC:Clipboard 2026-07-12-00:00: Direct navigator.clipboard.writeText crashes or mis-reports on non-secure origins such as mobile http://fusionstudio:4040; copyTextToClipboard centralizes the secure-context guard and execCommand fallback. */
|
|
void copyTextToClipboard(errorText).then((copiedToClipboard) => {
|
|
if (!copiedToClipboard) return;
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1500);
|
|
});
|
|
}}
|
|
aria-label={copied ? t("agentError.copiedLabel", "Copied error to clipboard") : t("agentError.copyLabel", "Copy error to clipboard")}
|
|
>
|
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
{copied ? t("agentError.copied", "Copied") : t("agentError.copy", "Copy")}
|
|
</button>
|
|
<a
|
|
className="btn btn-sm btn-warning"
|
|
href={issueUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
window.open(issueUrl, "_blank", "noopener,noreferrer");
|
|
}}
|
|
>
|
|
<ExternalLink size={14} />
|
|
{t("agentError.reportOnGithub", "Report on GitHub")}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface AgentErrorIndicatorProps {
|
|
errorText: string;
|
|
issueContext: AgentErrorIssueContext;
|
|
summaryPrefix?: string;
|
|
}
|
|
|
|
export function AgentErrorIndicator({ errorText, issueContext, summaryPrefix = "Error" }: AgentErrorIndicatorProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const { t } = useTranslation("app");
|
|
|
|
return (
|
|
<>
|
|
<button type="button" className="agent-error-indicator" onClick={() => setOpen(true)} aria-label={t("agentError.openDetails", "Open error details")}>
|
|
<AlertCircle size={14} />
|
|
<span className="agent-error-indicator__label">{summaryPrefix}</span>
|
|
</button>
|
|
<AgentErrorDetailsModal open={open} onClose={() => setOpen(false)} errorText={errorText} issueContext={issueContext} />
|
|
</>
|
|
);
|
|
}
|