Files
fusion/packages/dashboard/app/components/AgentErrorDetailsModal.tsx
gsxdsm 6ea53966f6 FN-7885: migrate remaining direct clipboard callers to shared copyTextToClipboard helper
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>
2026-07-12 17:54:21 -07:00

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")}>&times;</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} />
</>
);
}