diff --git a/webui/src/components/MessageBubble.tsx b/webui/src/components/MessageBubble.tsx index 8ffacb0c6..ce372f652 100644 --- a/webui/src/components/MessageBubble.tsx +++ b/webui/src/components/MessageBubble.tsx @@ -368,6 +368,7 @@ export function MessageBubble({ assistantTimestampLabel.length > 0 && (!empty || hasReasoning || media.length > 0); const assistantTimestampTitle = showAssistantTimestamp ? fmtDateTime(assistantTimestamp) : ""; + const showAutomationTrigger = showAssistantTimestamp && automationSourceLabel.length > 0; const showAssistantFooterRow = showCopyButton || showForkButton || showAssistantTimestamp; const showAssistantFooterSlot = message.role === "assistant" @@ -385,12 +386,6 @@ export function MessageBubble({ ) : empty && message.isStreaming ? null : ( <> - {automationSourceLabel ? ( - - ) : null}
{/* A mode switch rebuilds Streamdown's subtree and moves the scroll anchor. */} ) : null} + {showAutomationTrigger ? ( + + ) : null}
) : null} @@ -476,22 +477,23 @@ function UserQuotedContext({ text, label }: { text: string; label: string }) { ); } -function AutomationSourceBadge({ label, triggerLabel }: { label: string; triggerLabel: string }) { +function AutomationTriggerMeta({ label, sourceLabel }: { label: string; sourceLabel: string }) { return ( -
- - {label} - ยท - {triggerLabel} -
+ + + + {label} + + + {sourceLabel} + ); } diff --git a/webui/src/tests/message-bubble.test.tsx b/webui/src/tests/message-bubble.test.tsx index 55ae0a0b9..1804d699c 100644 --- a/webui/src/tests/message-bubble.test.tsx +++ b/webui/src/tests/message-bubble.test.tsx @@ -455,20 +455,39 @@ describe("MessageBubble", () => { expect(screen.getByText(/not @krita/)).toBeInTheDocument(); }); - it("renders a lightweight automation source label for cron replies", () => { + it("places automation metadata after the timestamp and reveals its source on hover", async () => { + const completedAt = Date.UTC(2026, 6, 25, 12, 34, 56); const message: UIMessage = { id: "a-cron", role: "assistant", content: "Time to drink water.", source: { kind: "cron", label: "drink water" }, - createdAt: Date.now(), + completedAt, + createdAt: completedAt - 1_000, }; - render(); + const { container } = render(); - expect(screen.getByText("drink water")).toBeInTheDocument(); - expect(screen.getByText("Triggered automatically")).toBeInTheDocument(); + const footer = container.querySelector("[data-assistant-footer]")!; + const timestamp = footer.querySelector("[data-message-timestamp]")!; + const trigger = footer.querySelector("[data-automation-trigger]")!; + + expect(timestamp).toHaveTextContent(formatMessageEndTime(completedAt)); + expect(trigger).toHaveTextContent("Triggered automatically"); + expect(trigger.previousElementSibling).toBe(timestamp); + expect(trigger).toHaveClass( + "text-[11px]", + "leading-none", + "text-muted-foreground/70", + "tabular-nums", + ); + expect(trigger.className).not.toMatch(/(?:^|\s)(?:border|bg-)/); + expect(trigger.querySelector("svg")).not.toBeInTheDocument(); + expect(screen.queryByText("drink water")).not.toBeInTheDocument(); expect(screen.getByText("Time to drink water.")).toBeInTheDocument(); + + fireEvent.pointerMove(trigger); + expect(await screen.findByRole("tooltip")).toHaveTextContent("drink water"); }); it("renders structured CLI app attachments even without the installed catalog", () => {