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", () => {