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 d9995cda..f04ca2ab 100644
--- a/apps/client/src/features/ai-chat/components/message-list.tsx
+++ b/apps/client/src/features/ai-chat/components/message-list.tsx
@@ -6,7 +6,6 @@ import MessageItem from "@/features/ai-chat/components/message-item.tsx";
import TypingIndicator from "@/features/ai-chat/components/typing-indicator.tsx";
import { isToolPart, toolRunState, ToolUiPart } from "@/features/ai-chat/utils/tool-parts.tsx";
import { assistantMessageHasVisibleContent } from "@/features/ai-chat/utils/message-content.ts";
-import { liveTurnTokens } from "@/features/ai-chat/utils/count-stream-tokens.ts";
import classes from "@/features/ai-chat/components/ai-chat.module.css";
interface MessageListProps {
@@ -95,19 +94,6 @@ export function typingIndicatorShowsName(messages: UIMessage[]): boolean {
return !assistantMessageHasVisibleContent(last);
}
-/**
- * The live thinking-token count to show on the standalone typing indicator. It
- * is the reasoning split of the tail assistant message (estimate while streaming,
- * authoritative once the server attaches usage at a step/turn boundary). Returns
- * 0 when the turn has produced no reasoning yet — the indicator then shows the
- * plain "Thinking…" line.
- */
-export function tailThinkingTokens(messages: UIMessage[]): number {
- const last = messages[messages.length - 1];
- if (!last || last.role !== "assistant") return 0;
- return liveTurnTokens(last).reasoning;
-}
-
/**
* 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
@@ -208,7 +194,6 @@ export default function MessageList({
)}
diff --git a/apps/client/src/features/ai-chat/components/tail-thinking-tokens.test.ts b/apps/client/src/features/ai-chat/components/tail-thinking-tokens.test.ts
deleted file mode 100644
index 5f421aec..00000000
--- a/apps/client/src/features/ai-chat/components/tail-thinking-tokens.test.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { describe, expect, it } from "vitest";
-import type { UIMessage } from "@ai-sdk/react";
-import { tailThinkingTokens } from "@/features/ai-chat/components/message-list.tsx";
-
-/**
- * Pure-helper tests for `tailThinkingTokens`: the live thinking-token count the
- * standalone typing indicator shows. It is the reasoning split of the tail
- * assistant message (estimate while streaming, authoritative once usage arrives).
- */
-const msg = (
- role: "user" | "assistant",
- parts: unknown[],
- metadata?: unknown,
-): UIMessage =>
- ({ id: Math.random().toString(), role, parts, metadata }) as UIMessage;
-
-describe("tailThinkingTokens", () => {
- it("is 0 when there are no messages", () => {
- expect(tailThinkingTokens([])).toBe(0);
- });
-
- it("is 0 when the tail message is the user's", () => {
- expect(tailThinkingTokens([msg("user", [{ type: "text", text: "q" }])])).toBe(0);
- });
-
- it("is 0 when the assistant has produced no reasoning yet", () => {
- expect(
- tailThinkingTokens([msg("assistant", [{ type: "text", text: "answer" }])]),
- ).toBe(0);
- });
-
- it("estimates reasoning tokens from streamed reasoning text", () => {
- // 8 chars -> 2 tokens.
- expect(
- tailThinkingTokens([
- msg("assistant", [{ type: "reasoning", text: "12345678" }]),
- ]),
- ).toBe(2);
- });
-
- it("uses authoritative usage.reasoningTokens once the server attaches it", () => {
- expect(
- tailThinkingTokens([
- msg("assistant", [{ type: "reasoning", text: "x" }], {
- usage: { outputTokens: 100, reasoningTokens: 42 },
- }),
- ]),
- ).toBe(42);
- });
-});
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 72ac3179..a3e9f937 100644
--- a/apps/client/src/features/ai-chat/components/typing-indicator.tsx
+++ b/apps/client/src/features/ai-chat/components/typing-indicator.tsx
@@ -16,12 +16,6 @@ interface TypingIndicatorProps {
* assistant row above already shows the same name, to avoid a duplicate label.
*/
showName?: boolean;
- /**
- * Live thinking/reasoning token count for the in-flight turn. When > 0 the
- * typing line becomes `Thinking… · {count} tokens` (like Claude Code). Omitted
- * / 0 keeps the plain `Thinking…` line.
- */
- thinkingTokens?: number;
}
/**
@@ -32,18 +26,12 @@ interface TypingIndicatorProps {
*
* Mirrors the assistant row layout in MessageItem (the dimmed label), so it reads
* as the assistant's bubble taking shape. The dimmed label uses the configured
- * identity name when provided (otherwise the generic "AI agent"), while the
- * typing line is always the generic "Thinking…" (it never includes the
- * role/identity name).
+ * identity name when provided (otherwise the generic "AI agent"); below it the
+ * animated dots stand in for the nascent bubble until content arrives.
*/
-export default function TypingIndicator({ assistantName, showName = true, thinkingTokens }: TypingIndicatorProps) {
+export default function TypingIndicator({ assistantName, showName = true }: TypingIndicatorProps) {
const { t } = useTranslation();
const name = resolveAssistantName(assistantName);
- // Show the running thinking-token count only once there is something to count.
- const thinkingLine =
- thinkingTokens && thinkingTokens > 0
- ? t("Thinking… · {{count}} tokens", { count: thinkingTokens })
- : t("Thinking…");
return (
@@ -58,9 +46,6 @@ export default function TypingIndicator({ assistantName, showName = true, thinki
-
- {thinkingLine}
-
);