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:
@@ -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\)/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user