feat(FN-3530): add modal-based agent error details and UI/docs updates

- Add AgentErrorDetailsModal and wire agent list/detail views to open full error diagnostics while keeping inline errors compact
- Add line-number gutter toggle in FileEditor and Files modal with dashboard persistence coverage
- Align project memory scope guidance across core logic, CLI tool docs, and dashboard docs
- Add regression coverage for runtime plugin alias behavior and update related engine/heartbeat tests
- Include changesets for FN-3530 UI improvements and FN-3485 memory guidance updates

Fusion-Task-Id: FN-3530
This commit is contained in:
Fusion
2026-05-05 17:00:41 -07:00
committed by gsxdsm
parent ff66c20417
commit bb6169a89b
10 changed files with 344 additions and 83 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Improve dashboard agent error UX by replacing inline stack-trace dumps with compact error indicators that open a shared details modal, including copy-to-clipboard and a prefilled GitHub issue shortcut.

View File

@@ -264,6 +264,7 @@ Manage AI agents with a dedicated control surface accessible from the main dashb
- **View Modes**: Board (compact grid) and list (detailed card) layouts, persisted to localStorage
- **Agent CRUD**: Create agents with name and role (create form's text input and role/type select both use tokenized styling — `var(--surface)`, `var(--text)`, `var(--border)`, `var(--radius-sm)`, `var(--focus-ring)` — for consistent theme-aware rendering across all color themes and light/dark modes), change state, update roles inline, delete idle and terminated agents (active and paused agents must be stopped/terminated first)
- **Health Monitoring**: Heartbeat-based health status (Healthy, Unresponsive, Starting, Paused, Terminated) using CSS variable references for theme consistency
- **Agent Error Details**: Agent collection views now show a compact inline error indicator (instead of raw stack traces) that opens a shared error-details modal with full text, copy action, and a prefilled "Report on GitHub" shortcut
- **Agent Detail**: Click any agent card to open a detail modal with full agent information. In list view, each agent card also provides an explicit **View Details** action button in the card actions row for clearer discoverability, while the existing clickable identity/header area remains supported. The modal features a compact header with clear visual hierarchy:
- **Identity area** (left): Agent icon, name, and state/health badges
- **Lifecycle controls** (center): Compact action buttons for state transitions (Start, Pause, Resume, Retry, Stop, Delete)

View File

@@ -27,6 +27,7 @@ import { useConfirm } from "../hooks/useConfirm";
import { useModalResizePersist } from "../hooks/useModalResizePersist";
import { AgentImportModal } from "./AgentImportModal";
import { AgentAvatar } from "./AgentAvatar";
import { AgentErrorIndicator } from "./AgentErrorDetailsModal";
/**
* Simple className utility - joins class names conditionally
@@ -1511,7 +1512,19 @@ function RunsTab({
{detailRun.stderrExcerpt && (
<div className="run-output-section">
<div className="run-output-label run-output-label--error">Errors</div>
<pre className="run-output-panel run-output-panel--error">{detailRun.stderrExcerpt}</pre>
<AgentErrorIndicator
errorText={detailRun.stderrExcerpt}
summaryPrefix="Run error"
issueContext={{
surface: "AgentDetailView runs",
agentId,
agentName,
agentState,
runId: detailRun.id,
taskId: undefined,
timestamp: detailRun.startedAt,
}}
/>
</div>
)}

View File

@@ -0,0 +1,51 @@
.agent-error-indicator {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
width: 100%;
border: var(--btn-border-width) solid color-mix(in srgb, var(--color-error) 30%, var(--border));
background: color-mix(in srgb, var(--color-error) 10%, transparent);
color: var(--color-error);
border-radius: var(--radius-sm);
padding: var(--space-xs) var(--space-sm);
cursor: pointer;
text-align: left;
}
.agent-error-indicator__label {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: var(--font-mono);
font-size: var(--space-md);
}
.agent-error-modal {
max-width: min(calc(var(--space-2xl) * 20), 100%);
}
.agent-error-modal__content {
padding: 0 var(--space-lg) var(--space-md);
}
.agent-error-modal__error {
margin: 0;
padding: var(--space-sm);
border-radius: var(--radius-sm);
border: var(--btn-border-width) solid var(--border);
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
font-size: var(--space-md);
max-height: calc(var(--space-2xl) * 8);
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 768px) {
.agent-error-modal {
width: calc(100% - var(--space-lg));
}
}

View File

@@ -0,0 +1,124 @@
import "./AgentErrorDetailsModal.css";
import { useMemo, useState } from "react";
import { AlertCircle, Check, Copy, ExternalLink } from "lucide-react";
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 issueUrl = useMemo(() => buildAgentErrorIssueUrl(errorText, issueContext), [errorText, issueContext]);
if (!open) {
return null;
}
return (
<div className="modal-overlay open" onClick={(event) => event.target === event.currentTarget && onClose()} role="dialog" aria-modal="true" aria-label="Agent error details">
<div className="modal agent-error-modal">
<div className="modal-header">
<h2 className="modal-title">
<AlertCircle size={16} />
Agent Error Details
</h2>
<button className="modal-close" onClick={onClose} aria-label="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={() => {
void navigator.clipboard.writeText(errorText).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
}}
aria-label={copied ? "Copied error to clipboard" : "Copy error to clipboard"}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
{copied ? "Copied" : "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} />
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);
return (
<>
<button type="button" className="agent-error-indicator" onClick={() => setOpen(true)} aria-label="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} />
</>
);
}

View File

@@ -12,8 +12,8 @@ import { getAgentHealthStatus } from "../utils/agentHealth";
import { getErrorMessage } from "@fusion/core";
import type { AgentHealthStatus } from "../utils/agentHealth";
import { useConfirm } from "../hooks/useConfirm";
import { CollapsibleErrorDisplay } from "./AgentsView";
import { AgentAvatar } from "./AgentAvatar";
import { AgentErrorIndicator } from "./AgentErrorDetailsModal";
interface AgentListModalProps {
isOpen: boolean;
@@ -223,6 +223,17 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
return "muted";
};
const getHealthSummary = (agent: Agent, health: AgentHealthStatus): { title: string | undefined; label: string | null } => {
if (agent.state === "error") {
return { title: undefined, label: "Error" };
}
return {
title: health.reason ?? health.label,
label: health.stateDerived ? null : health.label,
};
};
if (!isOpen) return null;
return (
@@ -339,6 +350,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
// Board view: compact grid layout
displayAgents.map(agent => {
const health = getHealthStatus(agent);
const healthSummary = getHealthSummary(agent, health);
const healthTone = getHealthTone(health);
return (
<div key={agent.id} className="agent-board-card" data-state={agent.state}>
@@ -350,7 +362,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
>
{agent.state}
</span>
<span className="agent-board-health" data-health={healthTone} title={health.reason ?? health.label}>
<span className="agent-board-health" data-health={healthTone} title={healthSummary.title}>
{health.icon}
</span>
</div>
@@ -482,6 +494,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
// List view: detailed card layout
displayAgents.map(agent => {
const health = getHealthStatus(agent);
const healthSummary = getHealthSummary(agent, health);
const healthTone = getHealthTone(health);
return (
<div key={agent.id} className="agent-card" data-state={agent.state}>
@@ -531,8 +544,8 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
>
{agent.state}
</span>
<span className="badge agent-list-health-badge" data-health={healthTone} title={health.reason ?? health.label}>
{health.icon}{!health.stateDerived && ` ${health.label}`}
<span className="badge agent-list-health-badge" data-health={healthTone} title={healthSummary.title}>
{health.icon}{healthSummary.label ? ` ${healthSummary.label}` : ""}
</span>
<span className="badge text-secondary">
{getRoleLabel(agent.role)}
@@ -542,7 +555,16 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
<div className="agent-card-body">
{agent.state === "error" && agent.lastError ? (
<CollapsibleErrorDisplay errorText={agent.lastError} />
<AgentErrorIndicator
errorText={agent.lastError}
issueContext={{
surface: "AgentListModal list",
agentId: agent.id,
agentName: agent.name,
agentState: agent.state,
taskId: agent.taskId,
}}
/>
) : null}
{agent.taskId && (
<div className="agent-task">

View File

@@ -1,6 +1,6 @@
import "./AgentsView.css";
import { useState, useEffect, useCallback, useRef, useMemo, useId, lazy, Suspense, type CSSProperties } from "react";
import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, List, ChevronRight, ChevronDown, ChevronUp, Filter, Upload, Network, SlidersHorizontal, Copy, Check, ZoomIn, ZoomOut, Minimize2 } from "lucide-react";
import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, List, ChevronRight, Filter, Upload, Network, SlidersHorizontal, ZoomIn, ZoomOut, Minimize2 } from "lucide-react";
import type { Agent, AgentCapability, AgentOnboardingSummary, AgentState, OrgTreeNode } from "../api";
import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree, fetchSettings, updateSettings } from "../api";
@@ -28,6 +28,7 @@ import { isEphemeralAgent, getErrorMessage } from "@fusion/core";
import { formatAgentSkillBadgeLabel } from "../utils/agentSkills";
import { resolveOrgChartLayoutMode, type OrgChartLayoutMode } from "./agentsOrgChartLayout";
import { AgentAvatar } from "./AgentAvatar";
import { AgentErrorIndicator } from "./AgentErrorDetailsModal";
export interface AgentsViewProps {
addToast: (message: string, type?: "success" | "error") => void;
@@ -89,58 +90,6 @@ function getStateCardClass(
}
}
export function CollapsibleErrorDisplay({
errorText,
className,
}: {
errorText: string;
className?: string;
}) {
const [expanded, setExpanded] = useState(false);
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(errorText);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Ignore clipboard errors
}
}, [errorText]);
return (
<div className={`agent-card-error${className ? ` ${className}` : ""}`}>
<div className="agent-card-error-header">
<span className="agent-card-error-preview" title={errorText}>
{errorText}
</span>
<div className="agent-card-error-actions">
<button
type="button"
className="btn-icon touch-target agent-card-error-copy-btn"
onClick={() => void handleCopy()}
title={copied ? "Copied" : "Copy error"}
aria-label={copied ? "Copied error to clipboard" : "Copy error to clipboard"}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
<button
type="button"
className="btn-icon touch-target agent-card-error-toggle"
onClick={() => setExpanded((value) => !value)}
title={expanded ? "Collapse error" : "Expand error"}
aria-label={expanded ? "Collapse error" : "Expand error"}
aria-expanded={expanded}
>
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
</div>
</div>
{expanded ? <pre className="agent-card-error-full">{errorText}</pre> : null}
</div>
);
}
function getOrgChartLeafCount(node: OrgTreeNode): number {
if (node.children.length === 0) {
@@ -150,6 +99,17 @@ function getOrgChartLeafCount(node: OrgTreeNode): number {
return node.children.reduce((sum, child) => sum + getOrgChartLeafCount(child), 0);
}
function getHealthSummary(agent: Agent, health: AgentHealthStatus): { title: string | undefined; label: string | null } {
if (agent.state === "error") {
return { title: undefined, label: "Error" };
}
return {
title: health.reason ?? health.label,
label: health.stateDerived ? null : health.label,
};
}
function OrgChartNode({
node,
onSelect,
@@ -165,6 +125,7 @@ function OrgChartNode({
}) {
const { agent, children } = node;
const health = getHealthStatus(agent);
const healthSummary = getHealthSummary(agent, health);
const stateBadgeClass = getStateBadgeClass(agent.state);
const stateNodeClass = getStateCardClass("org-chart-node-card", agent.state);
const subtreeLeafCount = getOrgChartLeafCount(node);
@@ -199,9 +160,9 @@ function OrgChartNode({
>
{agent.state}
</span>
<span className="org-chart-node__health" style={{ color: health.color }} title={health.reason ?? health.label}>
<span className="org-chart-node__health" style={{ color: health.color }} title={healthSummary.title}>
{health.icon}
{!health.stateDerived && <span className="text-secondary">{health.label}</span>}
{healthSummary.label && <span className="text-secondary">{healthSummary.label}</span>}
</span>
</div>
</div>
@@ -1109,6 +1070,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
) : (
displayAgents.map((agent) => {
const health = getHealthStatus(agent);
const healthSummary = getHealthSummary(agent, health);
const stateBadgeClass = getStateBadgeClass(agent.state);
const stateCardClass = getStateCardClass("agent-board-card", agent.state);
return (
@@ -1134,8 +1096,8 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
</div>
<div className="agent-board-name">{agent.name}</div>
<div className="agent-board-id">{agent.id}</div>
<div className="agent-board-health" style={{ color: health.color }} title={health.reason ?? health.label}>
{health.icon}{!health.stateDerived && ` ${health.label}`}
<div className="agent-board-health" style={{ color: health.color }} title={healthSummary.title}>
{health.icon}{healthSummary.label ? ` ${healthSummary.label}` : ""}
</div>
</div>
</div>
@@ -1151,6 +1113,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
// List view: detailed card layout
displayAgents.map(agent => {
const health = getHealthStatus(agent);
const healthSummary = getHealthSummary(agent, health);
const stateBadgeClass = getStateBadgeClass(agent.state);
const stateCardClass = getStateCardClass("agent-card", agent.state);
const configuredIntervalMs = resolveHeartbeatIntervalMs(agent.runtimeConfig?.heartbeatIntervalMs);
@@ -1221,8 +1184,8 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
>
{agent.state}
</span>
<span className="badge" style={{ color: health.color }} title={health.reason ?? health.label}>
{health.icon}{!health.stateDerived && ` ${health.label}`}
<span className="badge" style={{ color: health.color }} title={healthSummary.title}>
{health.icon}{healthSummary.label ? ` ${healthSummary.label}` : ""}
</span>
<span className="badge text-secondary">
{getRoleLabel(agent.role)}
@@ -1247,7 +1210,16 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<div className="agent-card-body">
{agent.state === "error" && agent.lastError ? (
<CollapsibleErrorDisplay errorText={agent.lastError} />
<AgentErrorIndicator
errorText={agent.lastError}
issueContext={{
surface: "AgentsView list",
agentId: agent.id,
agentName: agent.name,
agentState: agent.state,
taskId: agent.taskId,
}}
/>
) : null}
{agent.taskId && (
<div className="agent-task">

View File

@@ -1297,6 +1297,58 @@ describe("AgentDetailView", () => {
});
});
it("shows run error in modal and launches prefilled GitHub issue", async () => {
const runId = "run-error";
mockFetchAgentRuns.mockResolvedValueOnce([
{
id: runId,
agentId: "agent-001",
startedAt: "2024-01-01T00:00:00.000Z",
endedAt: "2024-01-01T00:01:00.000Z",
status: "failed",
} as AgentHeartbeatRun,
]);
mockFetchAgentRunLogs.mockResolvedValueOnce([]);
mockFetchAgentRunDetail.mockResolvedValueOnce({
id: runId,
agentId: "agent-001",
startedAt: "2024-01-01T00:00:00.000Z",
endedAt: "2024-01-01T00:01:00.000Z",
status: "failed",
stderrExcerpt: "fatal: exploded",
} as AgentHeartbeatRun);
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
initialTab="runs"
initialRunId={runId}
/>,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: "Open error details" })).toBeInTheDocument();
});
expect(screen.queryByText("fatal: exploded")).toBeNull();
expect(screen.queryByLabelText("Agent error details")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Open error details" }));
expect(screen.getByLabelText("Agent error details")).toBeInTheDocument();
expect(screen.getByText("fatal: exploded")).toBeInTheDocument();
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
fireEvent.click(screen.getByRole("link", { name: "Report on GitHub" }));
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining("https://github.com/Runfusion/Fusion/issues/new?"),
"_blank",
"noopener,noreferrer",
);
expect(openSpy.mock.calls[0]?.[0]).toContain("run-error");
openSpy.mockRestore();
});
it("auto-expands the active run when opened from running control context", async () => {
const activeRunId = "run-001";
mockFetchAgentRunLogs.mockResolvedValueOnce([
@@ -1333,8 +1385,9 @@ describe("AgentDetailView", () => {
});
await waitFor(() => {
expect(screen.getByText("Active run log line")).toBeInTheDocument();
expect(screen.getByText("System Prompt")).toBeInTheDocument();
const viewer = screen.getByTestId("agent-log-viewer");
expect(viewer.textContent).toContain("Active run log line");
});
});
@@ -1382,8 +1435,9 @@ describe("AgentDetailView", () => {
expect(screen.getByText("Latest run · run-1001")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("First entry")).toBeInTheDocument();
expect(screen.getByText("Second entry")).toBeInTheDocument();
const viewer = screen.getByTestId("agent-log-viewer");
expect(viewer.textContent).toContain("First entry");
expect(viewer.textContent).toContain("Second entry");
});
});

View File

@@ -318,7 +318,7 @@ describe("AgentListModal", () => {
});
});
it("renders collapsible error display for error agents in list view", async () => {
it("renders compact error indicator and opens modal with actions for error agents", async () => {
mockFetchAgents.mockResolvedValueOnce([
{
...mockAgents[0],
@@ -338,17 +338,26 @@ describe("AgentListModal", () => {
);
await waitFor(() => {
expect(screen.getAllByText("modal failure").length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: "Open error details" })).toBeTruthy();
});
fireEvent.click(screen.getByRole("button", { name: "Expand error" }));
expect(screen.getByRole("button", { name: "Collapse error" })).toBeTruthy();
expect(screen.queryByText("modal failure")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Open error details" }));
expect(screen.getByLabelText("Agent error details")).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Copy error to clipboard" }));
await waitFor(() => {
expect(mockClipboardWriteText).toHaveBeenCalledWith("modal failure");
});
expect(screen.getByRole("button", { name: "Copied error to clipboard" })).toBeTruthy();
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
fireEvent.click(screen.getByRole("link", { name: "Report on GitHub" }));
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining("https://github.com/Runfusion/Fusion/issues/new?"),
"_blank",
"noopener,noreferrer",
);
openSpy.mockRestore();
});
it("renders health badges via data attributes instead of inline color styles", async () => {

View File

@@ -1010,7 +1010,7 @@ describe("AgentsView", () => {
);
});
it("renders collapsible error display and supports expand/copy", async () => {
it("renders compact error indicator and opens modal with copy/github actions", async () => {
const errorAgent: Agent = {
...mockAgents[0],
id: "agent-error",
@@ -1024,20 +1024,30 @@ describe("AgentsView", () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getAllByText("something broke").length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: "Open error details" })).toBeTruthy();
});
const expandButton = screen.getByRole("button", { name: "Expand error" });
fireEvent.click(expandButton);
expect(screen.getByRole("button", { name: "Collapse error" })).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Collapse error" }));
expect(screen.getByRole("button", { name: "Expand error" })).toBeTruthy();
expect(screen.queryByText("something broke")).toBeNull();
expect(screen.queryByLabelText("Agent error details")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Open error details" }));
expect(screen.getByLabelText("Agent error details")).toBeTruthy();
expect(screen.getAllByText("something broke").length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole("button", { name: "Copy error to clipboard" }));
await waitFor(() => {
expect(mockClipboardWriteText).toHaveBeenCalledWith("something broke");
});
expect(screen.getByRole("button", { name: "Copied error to clipboard" })).toBeTruthy();
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
fireEvent.click(screen.getByRole("link", { name: "Report on GitHub" }));
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining("https://github.com/Runfusion/Fusion/issues/new?"),
"_blank",
"noopener,noreferrer",
);
expect(openSpy.mock.calls[0]?.[0]).toContain("Surface%3A+AgentsView+list");
openSpy.mockRestore();
});
it("does not render error display without error state and lastError", async () => {
@@ -1055,7 +1065,7 @@ describe("AgentsView", () => {
});
expect(screen.queryByText("should not show")).toBeNull();
expect(screen.queryByRole("button", { name: "Copy error to clipboard" })).toBeNull();
expect(screen.queryByRole("button", { name: "Open error details" })).toBeNull();
});
it("shows refresh button", async () => {