Browse Source

feat(tui): add verbose turn token usage (#39281)

Simon Klee 2 weeks ago
parent
commit
4d59b059ee

+ 11 - 0
packages/tui/src/component/devtools-bar.tsx

@@ -77,6 +77,7 @@ export function DevToolsBar() {
   const runtime = createMemo(() => runtimeStatus(frontendSamples()))
   const timing = () => config.data.debug?.timing ?? false
   const turnTokens = () => config.data.debug?.turn_tokens ?? false
+  const verboseTurnTokens = () => turnTokens() === "verbose"
 
   const offEscape = keymap.intercept(
     "key",
@@ -380,6 +381,16 @@ export function DevToolsBar() {
               >
                 {turnTokens() ? "[x]" : "[ ]"} Turn token usage
               </Action>
+              <Action
+                onClick={() =>
+                  void config.update((draft) => {
+                    draft.debug = { ...draft.debug, turn_tokens: verboseTurnTokens() ? true : "verbose" }
+                  })
+                }
+                hoverBackground
+              >
+                {verboseTurnTokens() ? "[x]" : "[ ]"} Turn token usage (verbose)
+              </Action>
             </box>
             <For each={groups()}>
               {(group) => (

+ 2 - 2
packages/tui/src/component/dialog-config.tsx

@@ -254,8 +254,8 @@ export const settings: Setting[] = [
     category: "Debug",
     path: ["debug", "turn_tokens"],
     default: false,
-    values: [false, true],
-    labels: ["off", "on"],
+    values: [false, true, "verbose"],
+    labels: ["off", "on", "verbose"],
     keywords: ["tokens", "usage", "debug"],
   },
 ]

+ 3 - 1
packages/tui/src/config/index.tsx

@@ -159,7 +159,9 @@ export const Info = Schema.Struct({
     Schema.Struct({
       devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools debug bar" }),
       timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }),
-      turn_tokens: Schema.optional(Schema.Boolean).annotate({ description: "Show per-turn token usage diagnostics" }),
+      turn_tokens: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("verbose")])).annotate({
+        description: "Show per-turn token usage diagnostics, optionally with tool call inputs",
+      }),
     }),
   ).annotate({ description: "Debugging settings" }),
   animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }),

+ 53 - 2
packages/tui/src/routes/session/index.tsx

@@ -1104,6 +1104,7 @@ function TurnTokenUsage(props: {
 }) {
   const config = useConfig()
   const { themeV2 } = useTheme()
+  const verbose = () => config.data.debug?.turn_tokens === "verbose"
   const steps = createMemo(() => {
     let previousCache = props.previousCache
     return props.messageIDs.flatMap((messageID) => {
@@ -1123,6 +1124,7 @@ function TurnTokenUsage(props: {
       return [
         {
           finish: message.finish === "tool-calls" ? "tool-call" : (message.finish ?? "unknown"),
+          tools: verbose() ? message.content.filter((part) => part.type === "tool") : [],
           newTokens,
           cached: message.tokens.cache.read,
           total,
@@ -1138,7 +1140,7 @@ function TurnTokenUsage(props: {
     total: Math.max("Total".length, ...steps().map((item) => item.total.toLocaleString().length)),
   }))
   return (
-    <Show when={config.data.debug?.turn_tokens === true && steps().length > 0}>
+    <Show when={Boolean(config.data.debug?.turn_tokens) && steps().length > 0}>
       <box paddingLeft={3} flexDirection="column">
         <box flexDirection="row">
           <text width={INLINE_TOOL_ICON_WIDTH} fg={themeV2.text.subdued}>
@@ -1161,7 +1163,7 @@ function TurnTokenUsage(props: {
         <For each={steps()}>
           {(item) => (
             <box paddingLeft={INLINE_TOOL_ICON_WIDTH} flexDirection="column">
-              <text fg={themeV2.text.subdued}>
+              <text fg={verbose() && item.finish === "tool-call" ? undefined : themeV2.text.subdued}>
                 {item.finish.padEnd(columns().step + 2)}
                 <span style={{ attributes: TextAttributes.BOLD }}>
                   {item.newTokens.toLocaleString().padStart(columns().newTokens)}
@@ -1171,6 +1173,7 @@ function TurnTokenUsage(props: {
                 {"  "}
                 {item.total.toLocaleString().padStart(columns().total)}
               </text>
+              <TurnTokenToolCalls tools={item.tools} />
               <Show when={item.reuseDrop !== undefined}>
                 <text fg={themeV2.text.feedback.warning.default}>
                   ! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step
@@ -1184,6 +1187,54 @@ function TurnTokenUsage(props: {
   )
 }
 
+function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) {
+  const { themeV2 } = useTheme()
+  const nameWidth = () => Math.max(0, ...props.tools.map((tool) => tool.name.length)) + 2
+  return (
+    <Show when={props.tools.length > 0}>
+      <box paddingLeft={2} flexDirection="column">
+        <For each={props.tools}>
+          {(tool) => (
+            <box flexDirection="row">
+              <text
+                width={nameWidth()}
+                flexShrink={0}
+                fg={themeV2.text.subdued}
+                attributes={TextAttributes.BOLD}
+              >
+                {tool.name}
+              </text>
+              <text
+                fg={themeV2.text.subdued}
+                attributes={TextAttributes.DIM}
+                wrapMode="word"
+                flexGrow={1}
+                minWidth={0}
+              >
+                {turnTokenToolSummary(tool)}
+              </text>
+            </box>
+          )}
+        </For>
+      </box>
+    </Show>
+  )
+}
+
+function turnTokenToolSummary(tool: SessionMessageAssistantTool) {
+  const data = tool.state.input
+  if (typeof data === "string") return data
+  const primaryKey = ["command", "id", "pattern", "url", "query", "path", "description", "code"].find(
+    (key) => key in data,
+  )
+  const input = Object.entries(data).filter(([, value]) =>
+    ["string", "number", "boolean"].includes(typeof value),
+  )
+  const primary = input.find(([key]) => key === primaryKey)?.[1]
+  const details = input.filter(([key]) => key !== primaryKey).map(([key, value]) => `${key}: ${String(value)}`)
+  return [primary === undefined ? "" : String(primary), ...details].filter(Boolean).join("  ")
+}
+
 function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
   const { themeV2 } = useTheme()
   const shortcut = Keymap.useShortcut("session.background")

+ 1 - 1
packages/tui/src/routes/session/rows.ts

@@ -41,7 +41,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
   const config = useConfig()
   const [rows, setRows] = createStore<SessionRow[]>([])
   const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
-  const turnTokens = () => config.data.debug?.turn_tokens === true
+  const turnTokens = () => Boolean(config.data.debug?.turn_tokens)
 
   function reduce() {
     const messages = data.session.message.list(sessionID())