diff --git a/packages/ui/src/components/message-item.tsx b/packages/ui/src/components/message-item.tsx index a1b7e78c..64cb3432 100644 --- a/packages/ui/src/components/message-item.tsx +++ b/packages/ui/src/components/message-item.tsx @@ -1,6 +1,7 @@ -import { For, Show } from "solid-js" +import { For, Show, createMemo } from "solid-js" import type { Message, SDKPart, MessageInfo, ClientPart } from "../types/message" import { partHasRenderableText } from "../types/message" +import { formatTokenTotal } from "../lib/formatters" import MessagePart from "./message-part" interface MessageItemProps { @@ -138,6 +139,41 @@ export default function MessageItem(props: MessageItemProps) { isUser() ? "message-item-base bg-[var(--message-user-bg)] border-l-4 border-[var(--message-user-border)]" : "message-item-base assistant-message bg-[var(--message-assistant-bg)] border-l-4 border-[var(--message-assistant-border)]" + + const statChipClass = + "inline-flex items-center gap-1 rounded-full border border-[var(--border-base)] px-2 py-0.5 text-[10px]" + const statLabelClass = "uppercase text-[9px] tracking-wide text-[var(--text-muted)]" + const statValueClass = "font-semibold text-[var(--text-primary)]" + + const usageStats = createMemo(() => { + const info = props.messageInfo + if (!info || info.role !== "assistant" || !info.tokens) { + return null + } + + const tokens = info.tokens + const input = tokens.input ?? 0 + const output = tokens.output ?? 0 + const reasoning = tokens.reasoning ?? 0 + if (input === 0 && output === 0 && reasoning === 0) { + return null + } + + return { + input, + output, + reasoning, + cacheRead: tokens.cache?.read ?? 0, + cacheWrite: tokens.cache?.write ?? 0, + cost: info.cost ?? 0, + } + }) + + const formatCostValue = (value: number) => { + if (!value) return "$0.00" + if (value < 0.01) return `$${value.toPrecision(2)}` + return `$${value.toFixed(2)}` + } const agentIdentifier = () => { if (isUser()) return "" @@ -193,6 +229,36 @@ export default function MessageItem(props: MessageItemProps) { Fork + + {(usage) => ( +
+
+ Input + {formatTokenTotal(usage().input)} +
+
+ Output + {formatTokenTotal(usage().output)} +
+
+ Reasoning + {formatTokenTotal(usage().reasoning)} +
+
+ Cache Read + {formatTokenTotal(usage().cacheRead)} +
+
+ Cache Write + {formatTokenTotal(usage().cacheWrite)} +
+
+ Cost + {formatCostValue(usage().cost)} +
+
+ )} +
{timestamp()} diff --git a/packages/ui/src/components/message-stream.tsx b/packages/ui/src/components/message-stream.tsx index 94adf54e..16b477c9 100644 --- a/packages/ui/src/components/message-stream.tsx +++ b/packages/ui/src/components/message-stream.tsx @@ -32,6 +32,7 @@ import { sseManager } from "../lib/sse-manager" import Kbd from "./kbd" import { useConfig } from "../stores/preferences" import { getSessionInfo, computeDisplayParts, sessions, setActiveSession, setActiveParentSession } from "../stores/sessions" +import { formatTokenTotal } from "../lib/formatters" import { setActiveInstanceId } from "../stores/instances" import { showCommandPalette } from "../stores/command-palette" @@ -72,26 +73,9 @@ function navigateToTaskSession(location: TaskSessionLocation) { } } -// Format tokens like TUI (e.g., "110K", "1.2M") +// Format tokens like session sidebar (comma-separated totals) function formatTokens(tokens: number): string { - if (tokens >= 1000000) { - return `${(tokens / 1000000).toFixed(1)}M` - } else if (tokens >= 1000) { - return `${(tokens / 1000).toFixed(0)}K` - } - return tokens.toString() -} - -// Format session info for the session view header -function formatSessionInfo(usageTokens: number, contextWindow: number, usagePercent: number | null): string { - if (contextWindow > 0) { - const windowStr = formatTokens(contextWindow) - const usageStr = formatTokens(usageTokens) - const percent = usagePercent ?? Math.min(100, Math.max(0, Math.round((usageTokens / contextWindow) * 100))) - return `${usageStr} of ${windowStr} (${percent}%)` - } - - return formatTokens(usageTokens) + return formatTokenTotal(tokens) } interface MessageStreamProps { @@ -206,18 +190,27 @@ export default function MessageStream(props: MessageStreamProps) { const sessionInfo = createMemo(() => getSessionInfo(props.instanceId, props.sessionId) ?? { - tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false, - contextUsageTokens: 0, - contextUsagePercent: null, + inputTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + actualUsageTokens: 0, + modelOutputLimit: 0, + contextAvailableTokens: null, }, ) - const formattedSessionInfo = createMemo(() => { + const tokenStats = createMemo(() => { const info = sessionInfo() - return formatSessionInfo(info.contextUsageTokens, info.contextWindow, info.contextUsagePercent) + return { + input: info.inputTokens ?? 0, + output: info.outputTokens ?? 0, + cost: info.cost ?? 0, + used: info.actualUsageTokens ?? 0, + avail: info.contextAvailableTokens, + } }) function isNearBottom(element: HTMLDivElement, offset = SCROLL_OFFSET) { @@ -552,10 +545,20 @@ export default function MessageStream(props: MessageStreamProps) { return (
-
-
- {formattedSessionInfo()} -
+
+
+
+ Used + {formatTokens(sessionInfo().actualUsageTokens ?? 0)} +
+
+ Avail + + {sessionInfo().contextAvailableTokens !== null ? formatTokens(sessionInfo().contextAvailableTokens ?? 0) : "--"} + +
+
+