feat(FN-3712): add provider response copy action to chat bubble

The merge delivers four distinct features: a copy-to-clipboard action on provider responses in the chat bubble (FN-3712, with styling and tests across ChatView), richer agent onboarding drafts documented and wired into the dashboard onboarding modal (FN-3555), assigned-agent triage inheritance so ag

Fusion-Task-Id: FN-3712
This commit is contained in:
Fusion
2026-05-07 11:36:10 -07:00
committed by gsxdsm
parent af0fc5fc95
commit 011ae14aa7
3 changed files with 218 additions and 0 deletions

View File

@@ -24,6 +24,7 @@ const mockUseChat = vi.mocked(useChatModule.useChat);
const mockFetchDiscoveredSkills = vi.mocked(apiModule.fetchDiscoveredSkills);
const mockCreateObjectURL = vi.fn();
const mockRevokeObjectURL = vi.fn();
const mockClipboardWriteText = vi.fn();
// Mock lucide-react icons - spread actual module and override specific icons
vi.mock("lucide-react", async (importOriginal) => {
@@ -45,6 +46,8 @@ vi.mock("lucide-react", async (importOriginal) => {
EyeOff: ({ "data-testid": testId, ...props }: any) => <svg data-testid={testId || "icon-eye-off"} {...props} />,
Paperclip: ({ "data-testid": testId, ...props }: any) => <svg data-testid={testId || "icon-paperclip"} {...props} />,
File: ({ "data-testid": testId, ...props }: any) => <svg data-testid={testId || "icon-file"} {...props} />,
Copy: ({ "data-testid": testId, ...props }: any) => <svg data-testid={testId || "icon-copy"} {...props} />,
Check: ({ "data-testid": testId, ...props }: any) => <svg data-testid={testId || "icon-check"} {...props} />,
};
});
@@ -178,6 +181,11 @@ beforeEach(() => {
mockCreateObjectURL.mockImplementation((file: File) => `blob:${file.name}`);
Object.defineProperty(URL, "createObjectURL", { value: mockCreateObjectURL, writable: true });
Object.defineProperty(URL, "revokeObjectURL", { value: mockRevokeObjectURL, writable: true });
mockClipboardWriteText.mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
value: { writeText: mockClipboardWriteText },
configurable: true,
});
});
afterEach(() => {
@@ -920,6 +928,99 @@ describe("ChatView", () => {
});
});
it("shows copy actions only for assistant responses in provider/model chats", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_agent__",
status: "active",
title: "Fusion Chat",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
createdAt: "2026-04-08T00:00:00.000Z",
updatedAt: "2026-04-08T00:00:00.000Z",
},
messages: [
{ id: "msg-user", sessionId: "session-001", role: "user", content: "Question", createdAt: "2026-04-08T00:00:00.000Z" },
{ id: "msg-assistant", sessionId: "session-001", role: "assistant", content: "Answer", createdAt: "2026-04-08T00:00:01.000Z" },
],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(screen.getByTestId("chat-copy-response-msg-assistant")).toBeInTheDocument();
expect(screen.queryByTestId("chat-copy-response-msg-user")).not.toBeInTheDocument();
});
it("copies raw provider response content and shows feedback for success/failure", async () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_agent__",
status: "active",
title: "Fusion Chat",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
createdAt: "2026-04-08T00:00:00.000Z",
updatedAt: "2026-04-08T00:00:00.000Z",
},
messages: [
{ id: "msg-assistant", sessionId: "session-001", role: "assistant", content: "**Raw** output", createdAt: "2026-04-08T00:00:01.000Z" },
],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const copyButton = screen.getByTestId("chat-copy-response-msg-assistant");
expect(copyButton).not.toHaveTextContent("Copy");
await userEvent.click(copyButton);
expect(mockClipboardWriteText).toHaveBeenCalledWith("**Raw** output");
expect(screen.getByLabelText("Response copied")).toBeInTheDocument();
mockClipboardWriteText.mockRejectedValueOnce(new Error("denied"));
await userEvent.click(screen.getByTestId("chat-copy-response-msg-assistant"));
expect(screen.getByLabelText("Copy failed")).toBeInTheDocument();
});
it("shows streaming copy action for provider chats", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_agent__",
status: "active",
title: "Fusion Chat",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
createdAt: "2026-04-08T00:00:00.000Z",
updatedAt: "2026-04-08T00:00:00.000Z",
},
messages: [],
isStreaming: true,
streamingText: "Live answer",
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(screen.getByTestId("chat-copy-response-streaming")).toBeInTheDocument();
});
it("does not show copy actions for non-provider sessions", () => {
setupMockChat({
activeSession: activeSessionFixture,
messages: [
{ id: "msg-assistant", sessionId: "session-001", role: "assistant", content: "Answer", createdAt: "2026-04-08T00:00:01.000Z" },
],
isStreaming: true,
streamingText: "Live answer",
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(screen.queryByTestId("chat-copy-response-msg-assistant")).not.toBeInTheDocument();
expect(screen.queryByTestId("chat-copy-response-streaming")).not.toBeInTheDocument();
});
it("shows resolved agent name in streaming assistant avatar", async () => {
setupMockChat({
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Agent Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
@@ -3148,4 +3249,10 @@ describe("ChatView mobile CSS contract", () => {
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.chat-thread-header-identity\s*\{[^}]*flex:\s*1\s+1\s+auto[^}]*white-space:\s*nowrap/);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.chat-thread-header-render-toggle\s*\{[^}]*flex-shrink:\s*0/);
});
it("mobile keeps response copy action visible and touch-friendly", () => {
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.chat-message-copy-action\s*\{[^}]*opacity:\s*1/);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.chat-message-copy-action\s*\{[^}]*min-width:\s*calc\(var\(--space-lg\)\s*\*\s*2\.25\)/);
expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.chat-message-copy-action\s*\{[^}]*min-height:\s*calc\(var\(--space-lg\)\s*\*\s*2\.25\)/);
});
});