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 ;
}
@@ -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 ? (