refactor(ai-chat): drop thinking-token text from typing indicator
The live typing placeholder now shows only the bouncing dots; the "Thinking… · N tokens" line is removed. Clean up the dead plumbing: - typing-indicator: remove thinkingTokens prop, thinkingLine and the <Text> line; keep the animated dots and the dimmed name label - message-list: remove tailThinkingTokens helper, the thinkingTokens prop pass-through, and the now-unused liveTurnTokens import - delete tail-thinking-tokens.test.ts (tested the removed helper) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 TypingIndicator from "@/features/ai-chat/components/typing-indicator.tsx";
|
||||||
import { isToolPart, toolRunState, ToolUiPart } from "@/features/ai-chat/utils/tool-parts.tsx";
|
import { isToolPart, toolRunState, ToolUiPart } from "@/features/ai-chat/utils/tool-parts.tsx";
|
||||||
import { assistantMessageHasVisibleContent } from "@/features/ai-chat/utils/message-content.ts";
|
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";
|
import classes from "@/features/ai-chat/components/ai-chat.module.css";
|
||||||
|
|
||||||
interface MessageListProps {
|
interface MessageListProps {
|
||||||
@@ -95,19 +94,6 @@ export function typingIndicatorShowsName(messages: UIMessage[]): boolean {
|
|||||||
return !assistantMessageHasVisibleContent(last);
|
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,
|
* 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
|
* but only while the user is pinned to the bottom — if they scrolled up to read
|
||||||
@@ -208,7 +194,6 @@ export default function MessageList({
|
|||||||
<TypingIndicator
|
<TypingIndicator
|
||||||
assistantName={assistantName}
|
assistantName={assistantName}
|
||||||
showName={typingIndicatorShowsName(messages)}
|
showName={typingIndicatorShowsName(messages)}
|
||||||
thinkingTokens={tailThinkingTokens(messages)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -16,12 +16,6 @@ interface TypingIndicatorProps {
|
|||||||
* assistant row above already shows the same name, to avoid a duplicate label.
|
* assistant row above already shows the same name, to avoid a duplicate label.
|
||||||
*/
|
*/
|
||||||
showName?: boolean;
|
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
|
* 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
|
* as the assistant's bubble taking shape. The dimmed label uses the configured
|
||||||
* identity name when provided (otherwise the generic "AI agent"), while the
|
* identity name when provided (otherwise the generic "AI agent"); below it the
|
||||||
* typing line is always the generic "Thinking…" (it never includes the
|
* animated dots stand in for the nascent bubble until content arrives.
|
||||||
* role/identity name).
|
|
||||||
*/
|
*/
|
||||||
export default function TypingIndicator({ assistantName, showName = true, thinkingTokens }: TypingIndicatorProps) {
|
export default function TypingIndicator({ assistantName, showName = true }: TypingIndicatorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const name = resolveAssistantName(assistantName);
|
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 (
|
return (
|
||||||
<Box className={classes.messageRow}>
|
<Box className={classes.messageRow}>
|
||||||
@@ -58,9 +46,6 @@ export default function TypingIndicator({ assistantName, showName = true, thinki
|
|||||||
<span />
|
<span />
|
||||||
<span />
|
<span />
|
||||||
</span>
|
</span>
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{thinkingLine}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user