diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
index 9219c26..12ba855 100644
--- a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
+++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx
@@ -15,6 +15,7 @@ import { MessageList } from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list";
+import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { Tooltip } from "@/components/workspace/tooltip";
import { useAgent } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks";
@@ -115,6 +116,7 @@ export default function AgentChatPage() {
{t.agents.newChat}
+
diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx
index a447e6f..6241100 100644
--- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx
+++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx
@@ -15,6 +15,7 @@ import { MessageList } from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list";
+import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { Welcome } from "@/components/workspace/welcome";
import { useI18n } from "@/core/i18n/hooks";
import { useNotification } from "@/core/notification/hooks";
@@ -85,7 +86,8 @@ export default function ChatPage() {
+
diff --git a/frontend/src/components/workspace/token-usage-indicator.tsx b/frontend/src/components/workspace/token-usage-indicator.tsx
new file mode 100644
index 0000000..9f0b02f
--- /dev/null
+++ b/frontend/src/components/workspace/token-usage-indicator.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import type { Message } from "@langchain/langgraph-sdk";
+import { CoinsIcon } from "lucide-react";
+import { useMemo } from "react";
+
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useI18n } from "@/core/i18n/hooks";
+import { accumulateUsage, formatTokenCount } from "@/core/messages/usage";
+import { cn } from "@/lib/utils";
+
+interface TokenUsageIndicatorProps {
+ messages: Message[];
+ className?: string;
+}
+
+export function TokenUsageIndicator({
+ messages,
+ className,
+}: TokenUsageIndicatorProps) {
+ const { t } = useI18n();
+
+ const usage = useMemo(() => accumulateUsage(messages), [messages]);
+
+ if (!usage) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
{t.tokenUsage.title}
+
+ {t.tokenUsage.input}
+
+ {formatTokenCount(usage.inputTokens)}
+
+
+
+ {t.tokenUsage.output}
+
+ {formatTokenCount(usage.outputTokens)}
+
+
+
+
+ {t.tokenUsage.total}
+
+ {formatTokenCount(usage.totalTokens)}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts
index 4a99dfd..3617c98 100644
--- a/frontend/src/core/i18n/locales/en-US.ts
+++ b/frontend/src/core/i18n/locales/en-US.ts
@@ -274,6 +274,12 @@ export const enUS: Translations = {
failed: "Subtask failed",
},
+ // Token Usage
+ tokenUsage: {
+ title: "Token Usage",
+ input: "Input",
+ output: "Output",
+ total: "Total",
// Shortcuts
shortcuts: {
searchActions: "Search actions...",
diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts
index b9525e4..cfdb21a 100644
--- a/frontend/src/core/i18n/locales/types.ts
+++ b/frontend/src/core/i18n/locales/types.ts
@@ -211,6 +211,12 @@ export interface Translations {
failed: string;
};
+ // Token Usage
+ tokenUsage: {
+ title: string;
+ input: string;
+ output: string;
+ total: string;
// Shortcuts
shortcuts: {
searchActions: string;
diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts
index 80ab890..8d2e88e 100644
--- a/frontend/src/core/i18n/locales/zh-CN.ts
+++ b/frontend/src/core/i18n/locales/zh-CN.ts
@@ -261,6 +261,12 @@ export const zhCN: Translations = {
failed: "子任务失败",
},
+ // Token Usage
+ tokenUsage: {
+ title: "Token 用量",
+ input: "输入",
+ output: "输出",
+ total: "总计",
// Shortcuts
shortcuts: {
searchActions: "搜索操作...",
diff --git a/frontend/src/core/messages/usage.ts b/frontend/src/core/messages/usage.ts
new file mode 100644
index 0000000..44cee07
--- /dev/null
+++ b/frontend/src/core/messages/usage.ts
@@ -0,0 +1,62 @@
+import type { Message } from "@langchain/langgraph-sdk";
+
+export interface TokenUsage {
+ inputTokens: number;
+ outputTokens: number;
+ totalTokens: number;
+}
+
+/**
+ * Extract usage_metadata from an AI message if present.
+ * The field is added by the backend (PR #1218) but not typed in the SDK.
+ */
+function getUsageMetadata(
+ message: Message,
+): TokenUsage | null {
+ if (message.type !== "ai") {
+ return null;
+ }
+ const usage = (message as Record
).usage_metadata as
+ | { input_tokens?: number; output_tokens?: number; total_tokens?: number }
+ | undefined;
+ if (!usage) {
+ return null;
+ }
+ return {
+ inputTokens: usage.input_tokens ?? 0,
+ outputTokens: usage.output_tokens ?? 0,
+ totalTokens: usage.total_tokens ?? 0,
+ };
+}
+
+/**
+ * Accumulate token usage across all AI messages in a thread.
+ */
+export function accumulateUsage(messages: Message[]): TokenUsage | null {
+ const cumulative: TokenUsage = {
+ inputTokens: 0,
+ outputTokens: 0,
+ totalTokens: 0,
+ };
+ let hasUsage = false;
+ for (const message of messages) {
+ const usage = getUsageMetadata(message);
+ if (usage) {
+ hasUsage = true;
+ cumulative.inputTokens += usage.inputTokens;
+ cumulative.outputTokens += usage.outputTokens;
+ cumulative.totalTokens += usage.totalTokens;
+ }
+ }
+ return hasUsage ? cumulative : null;
+}
+
+/**
+ * Format a token count for display: 1234 -> "1,234", 12345 -> "12.3K"
+ */
+export function formatTokenCount(count: number): string {
+ if (count < 10_000) {
+ return count.toLocaleString();
+ }
+ return `${(count / 1000).toFixed(1)}K`;
+}