diff --git a/ui/src/components/ChatMessage.tsx b/ui/src/components/ChatMessage.tsx
index 3dce53ab..ac5d02e2 100644
--- a/ui/src/components/ChatMessage.tsx
+++ b/ui/src/components/ChatMessage.tsx
@@ -1,12 +1,28 @@
import { ChatMarkdownMessage } from "./ChatMarkdownMessage";
+import { ChatMessageIdentityBar } from "./ChatMessageIdentityBar";
+import { ChatStreamingCursor } from "./ChatStreamingCursor";
import { cn } from "../lib/utils";
+import type { AgentRole } from "@paperclipai/shared";
interface ChatMessageProps {
role: "user" | "assistant" | "system";
content: string;
+ agentName?: string | null;
+ agentIcon?: string | null;
+ agentRole?: AgentRole | null;
+ timestamp?: string;
+ isStreaming?: boolean;
}
-export function ChatMessage({ role, content }: ChatMessageProps) {
+export function ChatMessage({
+ role,
+ content,
+ agentName,
+ agentIcon,
+ agentRole,
+ timestamp,
+ isStreaming,
+}: ChatMessageProps) {
if (role === "user") {
return (
@@ -22,8 +38,18 @@ export function ChatMessage({ role, content }: ChatMessageProps) {
}
// assistant or system
return (
-
+
+ {agentName && (
+
+ )}
+ {isStreaming && }
);
}
diff --git a/ui/src/components/ChatMessageIdentityBar.test.tsx b/ui/src/components/ChatMessageIdentityBar.test.tsx
index de028bd6..25413ba2 100644
--- a/ui/src/components/ChatMessageIdentityBar.test.tsx
+++ b/ui/src/components/ChatMessageIdentityBar.test.tsx
@@ -1,8 +1,66 @@
-import { describe, it } from "vitest";
+// @vitest-environment jsdom
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { ChatMessageIdentityBar } from "./ChatMessageIdentityBar";
+
+// Tell React this environment uses act() for event flushing.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
describe("ChatMessageIdentityBar", () => {
- it.todo("renders agent icon at 16x16px");
- it.todo("renders agent name in semibold text");
- it.todo("renders timestamp in muted text");
- it.todo("applies role-specific color from agentRoleColors");
+ let container: HTMLDivElement;
+ let root: ReturnType
| null = null;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = null;
+ });
+
+ afterEach(() => {
+ if (root) {
+ act(() => {
+ root!.unmount();
+ });
+ root = null;
+ }
+ if (container.parentNode) {
+ container.remove();
+ }
+ });
+
+ function render(props: Parameters[0]) {
+ root = createRoot(container);
+ act(() => {
+ root!.render();
+ });
+ return container;
+ }
+
+ it("renders agent name in semibold text", () => {
+ render({ agentName: "PM Agent" });
+ const nameEl = container.querySelector(".font-semibold");
+ expect(nameEl).toBeDefined();
+ expect(nameEl?.textContent).toBe("PM Agent");
+ });
+
+ it("renders timestamp when provided", () => {
+ render({ agentName: "Test", timestamp: "2026-01-01T12:30:00Z" });
+ const allText = container.textContent ?? "";
+ expect(allText).toContain("12:30");
+ });
+
+ it("applies role-specific color class", () => {
+ render({ agentName: "PM", agentRole: "pm" });
+ const nameEl = container.querySelector(".font-semibold");
+ expect(nameEl?.className).toContain("text-blue-600");
+ expect(nameEl?.className).toContain("dark:text-blue-400");
+ });
+
+ it("shows streaming indicator dot when isStreaming", () => {
+ render({ agentName: "Test", isStreaming: true });
+ const dot = container.querySelector(".animate-pulse");
+ expect(dot).not.toBeNull();
+ });
});
diff --git a/ui/src/components/ChatMessageIdentityBar.tsx b/ui/src/components/ChatMessageIdentityBar.tsx
new file mode 100644
index 00000000..b076937b
--- /dev/null
+++ b/ui/src/components/ChatMessageIdentityBar.tsx
@@ -0,0 +1,38 @@
+import { AgentIcon } from "./AgentIconPicker";
+import { agentRoleColors, agentRoleColorDefault } from "../lib/agent-role-colors";
+import type { AgentRole } from "@paperclipai/shared";
+
+interface ChatMessageIdentityBarProps {
+ agentName: string;
+ agentIcon?: string | null;
+ agentRole?: AgentRole | null;
+ timestamp?: string;
+ isStreaming?: boolean;
+}
+
+export function ChatMessageIdentityBar({
+ agentName,
+ agentIcon,
+ agentRole,
+ timestamp,
+ isStreaming,
+}: ChatMessageIdentityBarProps) {
+ const colorClass = agentRole
+ ? (agentRoleColors[agentRole] ?? agentRoleColorDefault)
+ : agentRoleColorDefault;
+
+ return (
+
+
+
{agentName}
+ {isStreaming && (
+
+ )}
+ {timestamp && (
+
+ {new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
+
+ )}
+
+ );
+}
diff --git a/ui/src/components/ChatStreamingCursor.tsx b/ui/src/components/ChatStreamingCursor.tsx
new file mode 100644
index 00000000..53f5706b
--- /dev/null
+++ b/ui/src/components/ChatStreamingCursor.tsx
@@ -0,0 +1,8 @@
+export function ChatStreamingCursor() {
+ return (
+
+ );
+}