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) : "--"}
+
+
+
+