@@ -28,6 +28,7 @@ import { Prompt, type PromptRef } from "@tui/component/prompt"
2828import type { AssistantMessage , Part , ToolPart , UserMessage , TextPart , ReasoningPart } from "@opencode-ai/sdk"
2929import { useLocal } from "@tui/context/local"
3030import { Locale } from "@/util/locale"
31+ import { Token } from "@/util/token"
3132import type { Tool } from "@/tool/tool"
3233import type { ReadTool } from "@/tool/read"
3334import type { WriteTool } from "@/tool/write"
@@ -80,6 +81,7 @@ const context = createContext<{
8081 conceal : ( ) => boolean
8182 showThinking : ( ) => boolean
8283 showTimestamps : ( ) => boolean
84+ showTokens : ( ) => boolean
8385} > ( )
8486
8587function use ( ) {
@@ -106,11 +108,20 @@ export function Session() {
106108 return messages ( ) . findLast ( ( x ) => x . role === "assistant" )
107109 } )
108110
111+ const local = useLocal ( )
112+
113+ const contextLimit = createMemo ( ( ) => {
114+ const c = local . model . current ( )
115+ const provider = sync . data . provider . find ( ( p ) => p . id === c . providerID )
116+ return provider ?. models [ c . modelID ] ?. limit . context ?? 200000
117+ } )
118+
109119 const dimensions = useTerminalDimensions ( )
110120 const [ sidebar , setSidebar ] = createSignal < "show" | "hide" | "auto" > ( kv . get ( "sidebar" , "auto" ) )
111121 const [ conceal , setConceal ] = createSignal ( true )
112122 const [ showThinking , setShowThinking ] = createSignal ( true )
113123 const [ showTimestamps , setShowTimestamps ] = createSignal ( kv . get ( "timestamps" , "hide" ) === "show" )
124+ const [ showTokens , setShowTokens ] = createSignal ( kv . get ( "tokens" , "hide" ) === "show" )
114125
115126 const wide = createMemo ( ( ) => dimensions ( ) . width > 120 )
116127 const sidebarVisible = createMemo ( ( ) => sidebar ( ) === "show" || ( sidebar ( ) === "auto" && wide ( ) ) )
@@ -204,8 +215,6 @@ export function Session() {
204215 } , 50 )
205216 }
206217
207- const local = useLocal ( )
208-
209218 function moveChild ( direction : number ) {
210219 const parentID = session ( ) ?. parentID ?? session ( ) ?. id
211220 let children = sync . data . session
@@ -428,6 +437,19 @@ export function Session() {
428437 dialog . clear ( )
429438 } ,
430439 } ,
440+ {
441+ title : "Toggle tokens" ,
442+ value : "session.toggle.tokens" ,
443+ category : "Session" ,
444+ onSelect : ( dialog ) => {
445+ setShowTokens ( ( prev ) => {
446+ const next = ! prev
447+ kv . set ( "tokens" , next ? "show" : "hide" )
448+ return next
449+ } )
450+ dialog . clear ( )
451+ } ,
452+ } ,
431453 {
432454 title : "Page up" ,
433455 value : "session.page.up" ,
@@ -729,6 +751,7 @@ export function Session() {
729751 conceal,
730752 showThinking,
731753 showTimestamps,
754+ showTokens,
732755 } }
733756 >
734757 < box flexDirection = "row" paddingBottom = { 1 } paddingTop = { 1 } paddingLeft = { 2 } paddingRight = { 2 } gap = { 2 } >
@@ -864,6 +887,7 @@ export function Session() {
864887 last = { lastAssistant ( ) ?. id === message . id }
865888 message = { message as AssistantMessage }
866889 parts = { sync . data . part [ message . id ] ?? [ ] }
890+ contextLimit = { contextLimit ( ) }
867891 />
868892 </ Match >
869893 </ Switch >
@@ -917,6 +941,13 @@ function UserMessage(props: {
917941 const queued = createMemo ( ( ) => props . pending && props . message . id > props . pending )
918942 const color = createMemo ( ( ) => ( queued ( ) ? theme . accent : theme . secondary ) )
919943
944+ const individualTokens = createMemo ( ( ) => {
945+ return props . parts . reduce ( ( sum , part ) => {
946+ if ( part . type === "text" ) return sum + Token . estimate ( part . text )
947+ return sum
948+ } , 0 )
949+ } )
950+
920951 const compaction = createMemo ( ( ) => props . parts . find ( ( x ) => x . type === "compaction" ) )
921952
922953 return (
@@ -977,6 +1008,9 @@ function UserMessage(props: {
9771008 >
9781009 < span style = { { bg : theme . accent , fg : theme . backgroundPanel , bold : true } } > QUEUED </ span >
9791010 </ Show >
1011+ < Show when = { ctx . showTokens ( ) && ! queued ( ) && individualTokens ( ) > 0 } >
1012+ < span style = { { fg : theme . textMuted } } > ⬝~{ individualTokens ( ) . toLocaleString ( ) } tok</ span >
1013+ </ Show >
9801014 </ text >
9811015 </ box >
9821016 </ box >
@@ -994,7 +1028,8 @@ function UserMessage(props: {
9941028 )
9951029}
9961030
997- function AssistantMessage ( props : { message : AssistantMessage ; parts : Part [ ] ; last : boolean } ) {
1031+ function AssistantMessage ( props : { message : AssistantMessage ; parts : Part [ ] ; last : boolean ; contextLimit : number } ) {
1032+ const ctx = use ( )
9981033 const local = useLocal ( )
9991034 const { theme } = useTheme ( )
10001035 const sync = useSync ( )
@@ -1004,12 +1039,71 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
10041039 return props . message . finish && ! [ "tool-calls" , "unknown" ] . includes ( props . message . finish )
10051040 } )
10061041
1042+ // Find the parent user message (reused by duration and token calculations)
1043+ const user = createMemo ( ( ) => messages ( ) . find ( ( x ) => x . role === "user" && x . id === props . message . parentID ) )
1044+
10071045 const duration = createMemo ( ( ) => {
10081046 if ( ! final ( ) ) return 0
10091047 if ( ! props . message . time . completed ) return 0
1010- const user = messages ( ) . find ( ( x ) => x . role === "user" && x . id === props . message . parentID )
1011- if ( ! user || ! user . time ) return 0
1012- return props . message . time . completed - user . time . created
1048+ const u = user ( )
1049+ if ( ! u || ! u . time ) return 0
1050+ return props . message . time . completed - u . time . created
1051+ } )
1052+
1053+ // OUT tokens (sent TO API) - includes user text + tool results from previous assistant
1054+ const outEstimate = createMemo ( ( ) => props . message . sentEstimate )
1055+
1056+ // IN tokens (from API TO computer)
1057+ const inTokens = createMemo ( ( ) => props . message . tokens . output )
1058+ const inEstimate = createMemo ( ( ) => props . message . outputEstimate )
1059+
1060+ // Reasoning tokens (must be defined BEFORE inDisplay)
1061+ const reasoningTokens = createMemo ( ( ) => props . message . tokens . reasoning )
1062+ const reasoningEstimate = createMemo ( ( ) => props . message . reasoningEstimate )
1063+
1064+ const outDisplay = createMemo ( ( ) => {
1065+ const estimate = outEstimate ( )
1066+ if ( estimate !== undefined ) return "~" + estimate . toLocaleString ( )
1067+ const tokens = props . message . tokens . input
1068+ if ( tokens > 0 ) return tokens . toLocaleString ( )
1069+ return "0"
1070+ } )
1071+
1072+ const inDisplay = createMemo ( ( ) => {
1073+ const estimate = inEstimate ( )
1074+ if ( estimate !== undefined ) return "~" + estimate . toLocaleString ( )
1075+ const tokens = inTokens ( )
1076+ if ( tokens > 0 ) return tokens . toLocaleString ( )
1077+ // Show ~0 during streaming when we have reasoning but no output yet
1078+ if ( reasoningEstimate ( ) !== undefined || reasoningTokens ( ) > 0 ) return "~0"
1079+ return undefined
1080+ } )
1081+
1082+ const tokensDisplay = createMemo ( ( ) => {
1083+ const inVal = inDisplay ( )
1084+ if ( ! inVal ) return undefined
1085+ return `${ inVal } ↓/${ outDisplay ( ) } ↑`
1086+ } )
1087+
1088+ const reasoningDisplay = createMemo ( ( ) => {
1089+ const estimate = reasoningEstimate ( )
1090+ if ( estimate !== undefined ) return "~" + estimate . toLocaleString ( )
1091+ const tokens = reasoningTokens ( )
1092+ if ( tokens > 0 ) return tokens . toLocaleString ( )
1093+ return undefined
1094+ } )
1095+
1096+ const contextEstimate = createMemo ( ( ) => props . message . contextEstimate )
1097+
1098+ const cumulativeTokens = createMemo ( ( ) => {
1099+ const estimate = contextEstimate ( )
1100+ if ( estimate !== undefined ) return estimate
1101+ return props . message . tokens . input + props . message . tokens . cache . read + props . message . tokens . cache . write
1102+ } )
1103+
1104+ const percentage = createMemo ( ( ) => {
1105+ if ( ! props . contextLimit ) return 0
1106+ return Math . round ( ( cumulativeTokens ( ) / props . contextLimit ) * 100 )
10131107 } )
10141108
10151109 return (
@@ -1053,6 +1147,22 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
10531147 < Show when = { duration ( ) } >
10541148 < span style = { { fg : theme . textMuted } } > ⬝{ Locale . duration ( duration ( ) ) } </ span >
10551149 </ Show >
1150+ < Show when = { ctx . showTokens ( ) && ( tokensDisplay ( ) || reasoningDisplay ( ) ) } >
1151+ < span style = { { fg : theme . textMuted } } >
1152+ { " " }
1153+ ⬝ { tokensDisplay ( ) } tok
1154+ < Show when = { reasoningDisplay ( ) } >
1155+ { " · " }
1156+ { reasoningDisplay ( ) } think
1157+ </ Show >
1158+ < Show
1159+ when = { cumulativeTokens ( ) > 0 || inEstimate ( ) !== undefined || reasoningEstimate ( ) !== undefined }
1160+ >
1161+ { " · " }
1162+ { cumulativeTokens ( ) . toLocaleString ( ) } context ({ percentage ( ) } %)
1163+ </ Show >
1164+ </ span >
1165+ </ Show >
10561166 </ text >
10571167 </ box >
10581168 </ Match >
0 commit comments