diff --git a/ui/src/components/task-chat/TaskChatBubble.test.tsx b/ui/src/components/task-chat/TaskChatBubble.test.tsx index 99416ae0d17..efa46befe8a 100644 --- a/ui/src/components/task-chat/TaskChatBubble.test.tsx +++ b/ui/src/components/task-chat/TaskChatBubble.test.tsx @@ -9,6 +9,7 @@ import { ThemeProvider } from "@/context/ThemeContext"; import { IssueGalleryContext } from "@/context/IssueGalleryContext"; import { MemoryRouter, useLocation } from "@/lib/router"; import { TaskChatAgentIdentity, TaskChatBubble } from "./TaskChatBubble"; +import { copyTextToClipboard } from "@/lib/clipboard"; import { paperclipHumanAvatarUrl } from "@/lib/issue-chat-human-author"; import type { TaskChatMessageItem } from "./task-chat-model"; @@ -17,6 +18,8 @@ vi.mock("@/context/CompanyContext", async (importOriginal) => ({ useCompany: () => ({ selectedCompany: { issuePrefix: "TES" } }), })); +vi.mock("@/lib/clipboard", () => ({ copyTextToClipboard: vi.fn().mockResolvedValue(undefined) })); + function CurrentPath() { return {useLocation().pathname}; } @@ -144,6 +147,36 @@ describe("TaskChatBubble attachment chips", () => { expect(container.textContent).not.toContain("Sent from iMessage"); }); + it("copies the viewer's original Markdown beside the timestamp, including attachment links", async () => { + const text = "**My message**\n\n[report.txt](/api/attachments/report/content)"; + flushSync(() => root!.render( + + + , + )); + const toolbar = container.querySelector('[data-testid="task-chat-bubble-actions"]')!; + expect(toolbar.parentElement?.parentElement?.textContent).toContain("15m ago"); + const button = toolbar.querySelector('button[aria-label="Copy message"]') as HTMLButtonElement; + await act(async () => button.click()); + expect(copyTextToClipboard).toHaveBeenLastCalledWith(text); + expect(button.querySelector(".lucide-check")).not.toBeNull(); + }); + + it.each([ + { author: "human" as const, isCurrentUser: false }, + { author: "human" as const, isCurrentUser: undefined }, + { author: "agent" as const, isCurrentUser: true }, + { author: "human" as const, isCurrentUser: true, optimistic: "pending" as const }, + { author: "human" as const, isCurrentUser: true, optimistic: "queued" as const }, + ])("omits the own-message toolbar for %j", (state) => { + flushSync(() => root!.render( + + + , + )); + expect(container.querySelector('[data-testid="task-chat-bubble-actions"]')).toBeNull(); + }); + it("opens attachment images in the shared task gallery", () => { const openGallery = vi.fn(() => true); const contentPath = "/api/attachments/shared-image/content"; diff --git a/ui/src/components/task-chat/TaskChatBubble.tsx b/ui/src/components/task-chat/TaskChatBubble.tsx index 6df836a443c..da67905b546 100644 --- a/ui/src/components/task-chat/TaskChatBubble.tsx +++ b/ui/src/components/task-chat/TaskChatBubble.tsx @@ -42,6 +42,7 @@ import { stripStandaloneImageEmbeds, type AttachmentRef, } from "./task-chat-attachments"; +import { TaskChatBubbleActions } from "./TaskChatBubbleActions"; import { TaskChatSystemNotice } from "./TaskChatSystemNotice"; import type { TaskChatMessageItem } from "./task-chat-model"; @@ -431,6 +432,17 @@ function TaskChatBubbleContent({ > {attachedTurn} + ) : isOwnMessage ? ( +
+ + {sentFromIMessage ? "Sent from iMessage" : null} + {sentFromIMessage && timestamp ? " ยท " : null} + {timestamp} + +
+ +
+
) : actions ? ( streamlined ? (