diff --git a/src/components/AIViewer.tsx b/src/components/AIViewer.tsx
index c4bc2f5..56eb1b2 100644
--- a/src/components/AIViewer.tsx
+++ b/src/components/AIViewer.tsx
@@ -6,7 +6,7 @@
* Powered by ChatSessionManager for multi-window support.
*/
-import { useState, useEffect, useCallback, useRef, memo } from "react";
+import { useState, useEffect, useCallback, useRef, memo, useMemo } from "react";
import {
Loader2,
PanelLeft,
@@ -17,7 +17,6 @@ import {
Brain,
Settings2,
MessageSquare,
- RefreshCw,
Play,
Sparkles,
AlertCircle,
@@ -121,46 +120,58 @@ const MessageBubble = memo(function MessageBubble({
// Assistant message
const assistantMsg = message as AssistantMessage;
+ // Format cost info for display
+ const costInfo = formatMessageCost(assistantMsg);
+
return (
-
- {/* Reasoning content (collapsible) */}
- {assistantMsg.reasoning_content && (
-
-
-
- Reasoning
-
-
- {assistantMsg.reasoning_content}
-
-
- )}
-
- {/* Tool calls */}
- {assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0 && (
-
- {assistantMsg.tool_calls.map((tc) => (
-
-
-
- {tc.function.name}
-
-
- {formatToolArgs(tc.function.arguments)}
-
+
+
+ {/* Reasoning content (collapsible) */}
+ {assistantMsg.reasoning_content && (
+
+
+
+ Reasoning
+
+
+ {assistantMsg.reasoning_content}
- ))}
-
- )}
+
+ )}
- {/* Regular content */}
- {content && (
-
-
+ {/* Tool calls */}
+ {assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0 && (
+
+ {assistantMsg.tool_calls.map((tc) => (
+
+
+
+ {tc.function.name}
+
+
+ {formatToolArgs(tc.function.arguments)}
+
+
+ ))}
+
+ )}
+
+ {/* Regular content */}
+ {content && (
+
+
+
+ )}
+
+
+ {/* Cost info footer */}
+ {costInfo && (
+
+ {costInfo}
)}
@@ -180,6 +191,48 @@ function formatToolArgs(args: string): string {
}
}
+/**
+ * Format message cost info for display.
+ * Returns null if no cost info available.
+ */
+function formatMessageCost(msg: AssistantMessage): string | null {
+ const parts: string[] = [];
+
+ // Model name (clean it up for display)
+ if (msg.model) {
+ const modelName = msg.model
+ .replace(/^(openai\/|anthropic\/|google\/|meta-llama\/|mistralai\/)/, "")
+ .replace(/-\d{4}-\d{2}-\d{2}$/, "");
+ parts.push(modelName);
+ }
+
+ // Token count
+ if (msg.usage) {
+ const total = msg.usage.promptTokens + msg.usage.completionTokens;
+ parts.push(`${total.toLocaleString()} tokens`);
+ }
+
+ // Cost
+ if (msg.cost !== undefined && msg.cost > 0) {
+ parts.push(formatCost(msg.cost));
+ }
+
+ return parts.length > 0 ? parts.join(" • ") : null;
+}
+
+/**
+ * Format cost in USD.
+ */
+function formatCost(cost: number): string {
+ if (cost < 0.0001) {
+ return "<$0.0001";
+ }
+ if (cost < 0.01) {
+ return `$${cost.toFixed(4)}`;
+ }
+ return `$${cost.toFixed(2)}`;
+}
+
// ─────────────────────────────────────────────────────────────
// Thinking Indicator
// ─────────────────────────────────────────────────────────────
@@ -273,6 +326,16 @@ function ChatPanel({
// Prompt options for selector
const promptOptions = usePromptOptions();
+ // Calculate total conversation cost from messages
+ const conversationCost = useMemo(() => {
+ return messages.reduce((total, msg) => {
+ if (msg.role === "assistant" && "cost" in msg && msg.cost) {
+ return total + msg.cost;
+ }
+ return total;
+ }, 0);
+ }, [messages]);
+
// Local UI state
const [input, setInput] = useState("");
const [pendingUserMessage, setPendingUserMessage] = useState
(
@@ -499,27 +562,37 @@ function ChatPanel({
-