diff --git a/apps/client/src/features/ai-chat/components/message-list.tsx b/apps/client/src/features/ai-chat/components/message-list.tsx
index f9debc57..fca37b9b 100644
--- a/apps/client/src/features/ai-chat/components/message-list.tsx
+++ b/apps/client/src/features/ai-chat/components/message-list.tsx
@@ -77,6 +77,17 @@ export function showTypingIndicator(messages: UIMessage[], isStreaming: boolean)
return true;
}
+/**
+ * Whether the standalone typing indicator should render its own assistant-name
+ * label. True only when it stands in for a not-yet-started assistant row (it IS
+ * the nascent bubble). False once an assistant row exists at the tail, because
+ * that row's MessageItem already shows the same name — avoids a duplicate label.
+ */
+export function typingIndicatorShowsName(messages: UIMessage[]): boolean {
+ const last = messages[messages.length - 1];
+ return !(last && last.role === "assistant");
+}
+
/**
* Scrollable transcript. Auto-scrolls to the newest message as it streams in,
* but only while the user is pinned to the bottom — if they scrolled up to read
@@ -173,7 +184,7 @@ export default function MessageList({
assistantName={assistantName}
/>
))}
- {typing && }
+ {typing && }
);
diff --git a/apps/client/src/features/ai-chat/components/typing-indicator-shows-name.test.ts b/apps/client/src/features/ai-chat/components/typing-indicator-shows-name.test.ts
new file mode 100644
index 00000000..1d48a28a
--- /dev/null
+++ b/apps/client/src/features/ai-chat/components/typing-indicator-shows-name.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from "vitest";
+import type { UIMessage } from "@ai-sdk/react";
+import { typingIndicatorShowsName } from "@/features/ai-chat/components/message-list.tsx";
+
+/**
+ * Pure-helper tests for whether the standalone "Thinking…" indicator renders its
+ * own dimmed assistant-name label. It should only show the name while it stands
+ * in for a not-yet-started assistant row; once an assistant row exists at the
+ * tail, that row's MessageItem already shows the same name, so the indicator
+ * must show only the dots to avoid a duplicate stacked label.
+ */
+const msg = (
+ role: "user" | "assistant",
+ parts: UIMessage["parts"],
+): UIMessage => ({ id: Math.random().toString(), role, parts }) as UIMessage;
+
+describe("typingIndicatorShowsName", () => {
+ it("shows the name with no messages yet (standalone, just submitted)", () => {
+ expect(typingIndicatorShowsName([])).toBe(true);
+ });
+
+ it("shows the name when the last message is still the user's", () => {
+ expect(
+ typingIndicatorShowsName([msg("user", [{ type: "text", text: "q" }])]),
+ ).toBe(true);
+ });
+
+ it("hides the name when an assistant row exists at the tail", () => {
+ expect(
+ typingIndicatorShowsName([msg("assistant", [{ type: "text", text: "" }])]),
+ ).toBe(false);
+ });
+
+ it("hides the name when the assistant row's last part is a tool", () => {
+ const doneTool = { type: "tool-getPage", state: "output-available" } as unknown as UIMessage["parts"][number];
+ expect(
+ typingIndicatorShowsName([msg("assistant", [doneTool])]),
+ ).toBe(false);
+ });
+});
diff --git a/apps/client/src/features/ai-chat/components/typing-indicator.tsx b/apps/client/src/features/ai-chat/components/typing-indicator.tsx
index fce0dcee..c811c2bd 100644
--- a/apps/client/src/features/ai-chat/components/typing-indicator.tsx
+++ b/apps/client/src/features/ai-chat/components/typing-indicator.tsx
@@ -10,6 +10,12 @@ interface TypingIndicatorProps {
* (agent role) name.
*/
assistantName?: string;
+ /**
+ * Whether to render the dimmed assistant-name label. Defaults to true
+ * (standalone behavior preserved). Set false between agent steps where the
+ * assistant row above already shows the same name, to avoid a duplicate label.
+ */
+ showName?: boolean;
}
/**
@@ -24,15 +30,17 @@ interface TypingIndicatorProps {
* typing line is always the generic "Thinking…" (it never includes the
* role/identity name).
*/
-export default function TypingIndicator({ assistantName }: TypingIndicatorProps) {
+export default function TypingIndicator({ assistantName, showName = true }: TypingIndicatorProps) {
const { t } = useTranslation();
const name = resolveAssistantName(assistantName);
return (
-
- {name ?? t("AI agent")}
-
+ {showName !== false && (
+
+ {name ?? t("AI agent")}
+
+ )}