pi-open-tui 0.2.15 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,174 +1,174 @@
1
- import { execFile } from "node:child_process";
2
- import { existsSync, readdirSync, statSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { promisify } from "node:util";
5
-
6
- const execFileAsync = promisify(execFile);
7
- const VERSION_TIMEOUT_MS = 2500;
8
-
9
- export interface RuntimeInfo {
10
- name: string;
11
- version?: string;
12
- }
13
-
14
- interface RuntimeDef {
15
- name: string;
16
- files: readonly string[];
17
- folders?: readonly string[];
18
- extensions?: readonly string[];
19
- env?: string;
20
- versionCommand?: { cmd: string; args?: string[]; pattern?: RegExp };
21
- }
22
-
23
- const RUNTIMES: readonly RuntimeDef[] = [
24
- { name: "nodejs", files: ["package.json", ".nvmrc", ".node-version"], versionCommand: { cmd: "node", args: ["--version"], pattern: /v(\d+\.\d+\.\d+)/ } },
25
- { name: "rust", files: ["Cargo.toml"], versionCommand: { cmd: "rustc", args: ["--version"], pattern: /rustc\s+(\d+\.\d+\.\d+)/ } },
26
- { name: "go", files: ["go.mod"], versionCommand: { cmd: "go", args: ["version"], pattern: /go(\d+\.\d+\.\d+)/ } },
27
- { name: "python", files: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile", ".python-version"], versionCommand: { cmd: "python3", args: ["--version"], pattern: /Python\s+(\d+\.\d+\.\d+)/ } },
28
- { name: "ruby", files: ["Gemfile", ".ruby-version"], versionCommand: { cmd: "ruby", args: ["--version"], pattern: /ruby\s+(\d+\.\d+\.\d+)/ } },
29
- { name: "java", files: ["pom.xml", "build.gradle", "build.gradle.kts", ".java-version"], versionCommand: { cmd: "java", args: ["-version"], pattern: /version\s+"(\d+\.\d+[\.\d]*)"/ } },
30
- { name: "swift", files: ["Package.swift"], versionCommand: { cmd: "swift", args: ["--version"], pattern: /Swift\s+(\d+\.\d+)/ } },
31
- { name: "kotlin", files: ["build.gradle.kts", "settings.gradle.kts"] },
32
- { name: "cpp", files: ["CMakeLists.txt", "Makefile"] },
33
- { name: "c", files: ["Makefile", "CMakeLists.txt"] },
34
- { name: "deno", files: ["deno.json", "deno.jsonc", "deno.lock"], versionCommand: { cmd: "deno", args: ["--version"], pattern: /deno\s+(\d+\.\d+\.\d+)/ } },
35
- { name: "bun", files: ["bun.lock", "bun.lockb"], versionCommand: { cmd: "bun", args: ["--version"], pattern: /(\d+\.\d+\.\d+)/ } },
36
- { name: "php", files: ["composer.json"], versionCommand: { cmd: "php", args: ["--version"], pattern: /PHP\s+(\d+\.\d+\.\d+)/ } },
37
- { name: "haskell", files: ["stack.yaml", "cabal.project", ".cabal"], versionCommand: { cmd: "ghc", args: ["--version"], pattern: /(\d+\.\d+\.\d+)/ } },
38
- { name: "julia", files: ["Project.toml", "Manifest.toml"], versionCommand: { cmd: "julia", args: ["--version"], pattern: /julia\s+(\d+\.\d+\.\d+)/ } },
39
- { name: "lua", files: ["stylua.toml", ".luarc.json"], versionCommand: { cmd: "lua", args: ["-v"], pattern: /Lua\s+(\d+\.\d+)/ } },
40
- { name: "elixir", files: ["mix.exs"], versionCommand: { cmd: "elixir", args: ["--version"], pattern: /Elixir\s+(\d+\.\d+\.\d+)/ } },
41
- { name: "erlang", files: ["rebar.config", "erlang.mk"] },
42
- { name: "gleam", files: ["gleam.toml"], versionCommand: { cmd: "gleam", args: ["--version"], pattern: /gleam\s+(\d+\.\d+\.\d+)/ } },
43
- { name: "crystal", files: ["shard.yml"], versionCommand: { cmd: "crystal", args: ["--version"], pattern: /Crystal\s+(\d+\.\d+\.\d+)/ } },
44
- { name: "dart", files: ["pubspec.yaml"], versionCommand: { cmd: "dart", args: ["--version"], pattern: /Dart\s+SDK\s+version:\s+(\d+\.\d+\.\d+)/ } },
45
- { name: "nim", files: ["nim.cfg", ".nimble"] },
46
- { name: "zig", files: ["build.zig"], versionCommand: { cmd: "zig", args: ["version"], pattern: /(\d+\.\d+\.\d+)/ } },
47
- { name: "ocaml", files: [".opam", "dune", "dune-project"] },
48
- { name: "clojure", files: ["project.clj", "deps.edn"] },
49
- { name: "scala", files: ["build.sbt", ".scala", ".metals"] },
50
- { name: "perl", files: ["Makefile.PL", "cpanfile"] },
51
- { name: "r", files: [".Rproj", "DESCRIPTION"] },
52
- { name: "elm", files: ["elm.json"] },
53
- { name: "haxe", files: ["haxelib.json", ".haxerc"] },
54
- { name: "vagrant", files: ["Vagrantfile"] },
55
- { name: "terraform", files: ["main.tf", "variables.tf"], folders: [".terraform"] },
56
- { name: "helm", files: ["Chart.yaml", "helmfile.yaml"] },
57
- { name: "solidity", files: [], extensions: [".sol"] },
58
- { name: "fortran", files: ["fpm.toml"], extensions: [".f", ".f90", ".f95"] },
59
- { name: "mojo", files: [], extensions: [".mojo"] },
60
- { name: "red", files: [], extensions: [".red", ".reds"] },
61
- { name: "raku", files: ["META6.json"], extensions: [".raku", ".rakumod"] },
62
- { name: "purescript", files: ["spago.dhall", "spago.yaml"] },
63
- { name: "fennel", files: [], extensions: [".fnl"] },
64
- { name: "odin", files: [], extensions: [".odin"] },
65
- { name: "v", files: ["v.mod", "vpkg.json"], extensions: [".v"] },
66
- { name: "xmake", files: ["xmake.lua"] },
67
- { name: "gradle", files: ["build.gradle", "build.gradle.kts"], folders: ["gradle"] },
68
- { name: "maven", files: ["pom.xml"] },
69
- { name: "cmake", files: ["CMakeLists.txt", "CMakeCache.txt"] },
70
- { name: "meson", files: ["meson.build"], env: "MESON_DEVENV" },
71
- { name: "nix", files: ["flake.nix", "shell.nix"], env: "IN_NIX_SHELL" },
72
- { name: "guix", files: [], env: "GUIX_ENVIRONMENT" },
73
- { name: "conda", files: [], env: "CONDA_DEFAULT_ENV" },
74
- { name: "pixi", files: ["pixi.toml", "pixi.lock"], env: "PIXI_ENVIRONMENT_NAME" },
75
- { name: "spack", files: [], env: "SPACK_ENV" },
76
- { name: "pulumi", files: ["Pulumi.yaml", "Pulumi.yml"] },
77
- { name: "typst", files: ["template.typ"], extensions: [".typ"] },
78
- { name: "buf", files: ["buf.yaml", "buf.gen.yaml", "buf.work.yaml"] },
79
- { name: "dotnet", files: [".csproj", ".fsproj", "global.json", "Directory.Build.props"] },
80
- { name: "cobol", files: [], extensions: [".cbl", ".cob"] },
81
- ];
82
-
83
- interface CacheEntry {
84
- fingerprint: string;
85
- runtime: RuntimeInfo | null;
86
- }
87
-
88
- const cache = new Map<string, CacheEntry>();
89
- const CACHE_MAX = 32;
90
-
91
- function fingerprint(cwd: string, def: RuntimeDef): string {
92
- const parts: string[] = [];
93
- for (const f of def.files) {
94
- try {
95
- const stat = statSync(join(cwd, f));
96
- parts.push(`${f}:${stat.mtimeMs}`);
97
- } catch { /* ignore */ }
98
- }
99
- if (def.extensions || def.folders) {
100
- try {
101
- const entries = readdirSync(cwd);
102
- parts.push(...entries.slice().sort());
103
- } catch { /* ignore */ }
104
- }
105
- if (def.env && process.env[def.env]) {
106
- parts.push(`${def.env}=${process.env[def.env]}`);
107
- }
108
- return parts.join("\0");
109
- }
110
-
111
- function matchesDef(cwd: string, def: RuntimeDef): boolean {
112
- if (def.env && process.env[def.env]) return true;
113
- if (def.files.some((f) => existsSync(join(cwd, f)))) return true;
114
- if (def.folders?.some((f) => existsSync(join(cwd, f)))) return true;
115
- if (def.extensions) {
116
- try {
117
- const entries = readdirSync(cwd);
118
- if (entries.some((e) => def.extensions!.some((ext) => e.endsWith(ext)))) return true;
119
- } catch { /* ignore */ }
120
- }
121
- return false;
122
- }
123
-
124
- async function fetchVersion(def: RuntimeDef, cwd: string): Promise<string | undefined> {
125
- if (!def.versionCommand) return undefined;
126
- try {
127
- const { stdout } = await execFileAsync(def.versionCommand.cmd, def.versionCommand.args ?? [], {
128
- cwd,
129
- timeout: VERSION_TIMEOUT_MS,
130
- maxBuffer: 64 * 1024,
131
- });
132
- if (def.versionCommand.pattern) {
133
- const match = stdout.match(def.versionCommand.pattern);
134
- return match?.[1];
135
- }
136
- return stdout.trim() || undefined;
137
- } catch {
138
- return undefined;
139
- }
140
- }
141
-
142
- export async function readRuntimeInfo(cwd: string): Promise<RuntimeInfo | null> {
143
- for (const def of RUNTIMES) {
144
- if (!matchesDef(cwd, def)) continue;
145
- const fp = fingerprint(cwd, def);
146
- const cacheKey = `${cwd}\0${def.name}`;
147
- const cached = cache.get(cacheKey);
148
- if (cached && cached.fingerprint === fp) {
149
- return cached.runtime;
150
- }
151
-
152
- for (const key of cache.keys()) {
153
- if (key === cacheKey || key.startsWith(`${cwd}\0`)) cache.delete(key);
154
- }
155
-
156
- const version = await fetchVersion(def, cwd);
157
- const info: RuntimeInfo = {
158
- name: def.name,
159
- version,
160
- };
161
- cache.set(cacheKey, { fingerprint: fp, runtime: info });
162
- while (cache.size > CACHE_MAX) {
163
- const oldest = cache.keys().next().value;
164
- if (oldest === undefined) break;
165
- cache.delete(oldest);
166
- }
167
- return info;
168
- }
169
- return null;
170
- }
171
-
172
- export function clearRuntimeCache(): void {
173
- cache.clear();
174
- }
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+ const VERSION_TIMEOUT_MS = 2500;
8
+
9
+ export interface RuntimeInfo {
10
+ name: string;
11
+ version?: string;
12
+ }
13
+
14
+ interface RuntimeDef {
15
+ name: string;
16
+ files: readonly string[];
17
+ folders?: readonly string[];
18
+ extensions?: readonly string[];
19
+ env?: string;
20
+ versionCommand?: { cmd: string; args?: string[]; pattern?: RegExp };
21
+ }
22
+
23
+ const RUNTIMES: readonly RuntimeDef[] = [
24
+ { name: "bun", files: ["bun.lock", "bun.lockb"], versionCommand: { cmd: "bun", args: ["--version"], pattern: /(\d+\.\d+\.\d+)/ } },
25
+ { name: "nodejs", files: ["package.json", ".nvmrc", ".node-version"], versionCommand: { cmd: "node", args: ["--version"], pattern: /v(\d+\.\d+\.\d+)/ } },
26
+ { name: "rust", files: ["Cargo.toml"], versionCommand: { cmd: "rustc", args: ["--version"], pattern: /rustc\s+(\d+\.\d+\.\d+)/ } },
27
+ { name: "go", files: ["go.mod"], versionCommand: { cmd: "go", args: ["version"], pattern: /go(\d+\.\d+\.\d+)/ } },
28
+ { name: "python", files: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile", ".python-version"], versionCommand: { cmd: "python3", args: ["--version"], pattern: /Python\s+(\d+\.\d+\.\d+)/ } },
29
+ { name: "ruby", files: ["Gemfile", ".ruby-version"], versionCommand: { cmd: "ruby", args: ["--version"], pattern: /ruby\s+(\d+\.\d+\.\d+)/ } },
30
+ { name: "java", files: ["pom.xml", "build.gradle", "build.gradle.kts", ".java-version"], versionCommand: { cmd: "java", args: ["-version"], pattern: /version\s+"(\d+\.\d+[\.\d]*)"/ } },
31
+ { name: "swift", files: ["Package.swift"], versionCommand: { cmd: "swift", args: ["--version"], pattern: /Swift\s+(\d+\.\d+)/ } },
32
+ { name: "kotlin", files: ["build.gradle.kts", "settings.gradle.kts"] },
33
+ { name: "cpp", files: ["CMakeLists.txt", "Makefile"] },
34
+ { name: "c", files: ["Makefile", "CMakeLists.txt"] },
35
+ { name: "deno", files: ["deno.json", "deno.jsonc", "deno.lock"], versionCommand: { cmd: "deno", args: ["--version"], pattern: /deno\s+(\d+\.\d+\.\d+)/ } },
36
+ { name: "php", files: ["composer.json"], versionCommand: { cmd: "php", args: ["--version"], pattern: /PHP\s+(\d+\.\d+\.\d+)/ } },
37
+ { name: "haskell", files: ["stack.yaml", "cabal.project", ".cabal"], versionCommand: { cmd: "ghc", args: ["--version"], pattern: /(\d+\.\d+\.\d+)/ } },
38
+ { name: "julia", files: ["Project.toml", "Manifest.toml"], versionCommand: { cmd: "julia", args: ["--version"], pattern: /julia\s+(\d+\.\d+\.\d+)/ } },
39
+ { name: "lua", files: ["stylua.toml", ".luarc.json"], versionCommand: { cmd: "lua", args: ["-v"], pattern: /Lua\s+(\d+\.\d+)/ } },
40
+ { name: "elixir", files: ["mix.exs"], versionCommand: { cmd: "elixir", args: ["--version"], pattern: /Elixir\s+(\d+\.\d+\.\d+)/ } },
41
+ { name: "erlang", files: ["rebar.config", "erlang.mk"] },
42
+ { name: "gleam", files: ["gleam.toml"], versionCommand: { cmd: "gleam", args: ["--version"], pattern: /gleam\s+(\d+\.\d+\.\d+)/ } },
43
+ { name: "crystal", files: ["shard.yml"], versionCommand: { cmd: "crystal", args: ["--version"], pattern: /Crystal\s+(\d+\.\d+\.\d+)/ } },
44
+ { name: "dart", files: ["pubspec.yaml"], versionCommand: { cmd: "dart", args: ["--version"], pattern: /Dart\s+SDK\s+version:\s+(\d+\.\d+\.\d+)/ } },
45
+ { name: "nim", files: ["nim.cfg", ".nimble"] },
46
+ { name: "zig", files: ["build.zig"], versionCommand: { cmd: "zig", args: ["version"], pattern: /(\d+\.\d+\.\d+)/ } },
47
+ { name: "ocaml", files: [".opam", "dune", "dune-project"] },
48
+ { name: "clojure", files: ["project.clj", "deps.edn"] },
49
+ { name: "scala", files: ["build.sbt", ".scala", ".metals"] },
50
+ { name: "perl", files: ["Makefile.PL", "cpanfile"] },
51
+ { name: "r", files: [".Rproj", "DESCRIPTION"] },
52
+ { name: "elm", files: ["elm.json"] },
53
+ { name: "haxe", files: ["haxelib.json", ".haxerc"] },
54
+ { name: "vagrant", files: ["Vagrantfile"] },
55
+ { name: "terraform", files: ["main.tf", "variables.tf"], folders: [".terraform"] },
56
+ { name: "helm", files: ["Chart.yaml", "helmfile.yaml"] },
57
+ { name: "solidity", files: [], extensions: [".sol"] },
58
+ { name: "fortran", files: ["fpm.toml"], extensions: [".f", ".f90", ".f95"] },
59
+ { name: "mojo", files: [], extensions: [".mojo"] },
60
+ { name: "red", files: [], extensions: [".red", ".reds"] },
61
+ { name: "raku", files: ["META6.json"], extensions: [".raku", ".rakumod"] },
62
+ { name: "purescript", files: ["spago.dhall", "spago.yaml"] },
63
+ { name: "fennel", files: [], extensions: [".fnl"] },
64
+ { name: "odin", files: [], extensions: [".odin"] },
65
+ { name: "v", files: ["v.mod", "vpkg.json"], extensions: [".v"] },
66
+ { name: "xmake", files: ["xmake.lua"] },
67
+ { name: "gradle", files: ["build.gradle", "build.gradle.kts"], folders: ["gradle"] },
68
+ { name: "maven", files: ["pom.xml"] },
69
+ { name: "cmake", files: ["CMakeLists.txt", "CMakeCache.txt"] },
70
+ { name: "meson", files: ["meson.build"], env: "MESON_DEVENV" },
71
+ { name: "nix", files: ["flake.nix", "shell.nix"], env: "IN_NIX_SHELL" },
72
+ { name: "guix", files: [], env: "GUIX_ENVIRONMENT" },
73
+ { name: "conda", files: [], env: "CONDA_DEFAULT_ENV" },
74
+ { name: "pixi", files: ["pixi.toml", "pixi.lock"], env: "PIXI_ENVIRONMENT_NAME" },
75
+ { name: "spack", files: [], env: "SPACK_ENV" },
76
+ { name: "pulumi", files: ["Pulumi.yaml", "Pulumi.yml"] },
77
+ { name: "typst", files: ["template.typ"], extensions: [".typ"] },
78
+ { name: "buf", files: ["buf.yaml", "buf.gen.yaml", "buf.work.yaml"] },
79
+ { name: "dotnet", files: [".csproj", ".fsproj", "global.json", "Directory.Build.props"] },
80
+ { name: "cobol", files: [], extensions: [".cbl", ".cob"] },
81
+ ];
82
+
83
+ interface CacheEntry {
84
+ fingerprint: string;
85
+ runtime: RuntimeInfo | null;
86
+ }
87
+
88
+ const cache = new Map<string, CacheEntry>();
89
+ const CACHE_MAX = 32;
90
+
91
+ function fingerprint(cwd: string, def: RuntimeDef): string {
92
+ const parts: string[] = [];
93
+ for (const f of def.files) {
94
+ try {
95
+ const stat = statSync(join(cwd, f));
96
+ parts.push(`${f}:${stat.mtimeMs}`);
97
+ } catch { /* ignore */ }
98
+ }
99
+ if (def.extensions || def.folders) {
100
+ try {
101
+ const entries = readdirSync(cwd);
102
+ parts.push(...entries.slice().sort());
103
+ } catch { /* ignore */ }
104
+ }
105
+ if (def.env && process.env[def.env]) {
106
+ parts.push(`${def.env}=${process.env[def.env]}`);
107
+ }
108
+ return parts.join("\0");
109
+ }
110
+
111
+ function matchesDef(cwd: string, def: RuntimeDef): boolean {
112
+ if (def.env && process.env[def.env]) return true;
113
+ if (def.files.some((f) => existsSync(join(cwd, f)))) return true;
114
+ if (def.folders?.some((f) => existsSync(join(cwd, f)))) return true;
115
+ if (def.extensions) {
116
+ try {
117
+ const entries = readdirSync(cwd);
118
+ if (entries.some((e) => def.extensions!.some((ext) => e.endsWith(ext)))) return true;
119
+ } catch { /* ignore */ }
120
+ }
121
+ return false;
122
+ }
123
+
124
+ async function fetchVersion(def: RuntimeDef, cwd: string): Promise<string | undefined> {
125
+ if (!def.versionCommand) return undefined;
126
+ try {
127
+ const { stdout } = await execFileAsync(def.versionCommand.cmd, def.versionCommand.args ?? [], {
128
+ cwd,
129
+ timeout: VERSION_TIMEOUT_MS,
130
+ maxBuffer: 64 * 1024,
131
+ });
132
+ if (def.versionCommand.pattern) {
133
+ const match = stdout.match(def.versionCommand.pattern);
134
+ return match?.[1];
135
+ }
136
+ return stdout.trim() || undefined;
137
+ } catch {
138
+ return undefined;
139
+ }
140
+ }
141
+
142
+ export async function readRuntimeInfo(cwd: string): Promise<RuntimeInfo | null> {
143
+ for (const def of RUNTIMES) {
144
+ if (!matchesDef(cwd, def)) continue;
145
+ const fp = fingerprint(cwd, def);
146
+ const cacheKey = `${cwd}\0${def.name}`;
147
+ const cached = cache.get(cacheKey);
148
+ if (cached && cached.fingerprint === fp) {
149
+ return cached.runtime;
150
+ }
151
+
152
+ for (const key of cache.keys()) {
153
+ if (key === cacheKey || key.startsWith(`${cwd}\0`)) cache.delete(key);
154
+ }
155
+
156
+ const version = await fetchVersion(def, cwd);
157
+ const info: RuntimeInfo = {
158
+ name: def.name,
159
+ version,
160
+ };
161
+ cache.set(cacheKey, { fingerprint: fp, runtime: info });
162
+ while (cache.size > CACHE_MAX) {
163
+ const oldest = cache.keys().next().value;
164
+ if (oldest === undefined) break;
165
+ cache.delete(oldest);
166
+ }
167
+ return info;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ export function clearRuntimeCache(): void {
173
+ cache.clear();
174
+ }
@@ -46,7 +46,9 @@ export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
46
46
  const input = finiteOrZero(u.input);
47
47
  const cacheRead = finiteOrZero(u.cacheRead);
48
48
  const cacheWrite = finiteOrZero(u.cacheWrite);
49
- totals.input += input;
49
+ // input matches /session's "uncached" total: cacheWrite is billed near full
50
+ // price (fresh content), only cacheRead is discounted repeat content.
51
+ totals.input += input + cacheWrite;
50
52
  totals.output += finiteOrZero(u.output);
51
53
  totals.cacheRead += cacheRead;
52
54
  totals.cacheWrite += cacheWrite;
@@ -11,7 +11,7 @@ import type {
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import type { IconMode, TelemetryConfig } from "./config.ts";
13
13
  import { resolveGlyphs } from "./icons.ts";
14
- import { finiteOrZero, fmtTokens, formatDuration } from "./utils.ts";
14
+ import { finiteOrZero, fmtTokens, formatDuration, formatInputBreakdown } from "./utils.ts";
15
15
 
16
16
  const STALL_THRESHOLD_MS = 1000;
17
17
 
@@ -49,6 +49,7 @@ export interface TurnTelemetry {
49
49
  totalMs: number;
50
50
  inputTokens: number;
51
51
  outputTokens: number;
52
+ cacheReadTokens: number;
52
53
  stallMs: number;
53
54
  stallCount: number;
54
55
  rateUsdPerMTokens: number | null;
@@ -190,11 +191,15 @@ export class TurnTelemetryTracker {
190
191
  const endMs = this.now();
191
192
  let inputTokens = 0;
192
193
  let outputTokens = 0;
194
+ let cacheReadTokens = 0;
193
195
  let totalTokens = 0;
194
196
  let costUsd = 0;
195
197
  for (const message of turn.messages) {
196
- inputTokens += finiteOrZero(message.usage?.input);
198
+ // match /session's "uncached" total: cacheWrite is fresh, near-full-price
199
+ // content; only cacheRead is discounted repeat content.
200
+ inputTokens += finiteOrZero(message.usage?.input) + finiteOrZero(message.usage?.cacheWrite);
197
201
  outputTokens += finiteOrZero(message.usage?.output);
202
+ cacheReadTokens += finiteOrZero(message.usage?.cacheRead);
198
203
  totalTokens += finiteOrZero(message.usage?.totalTokens);
199
204
  costUsd += finiteOrZero(message.usage?.cost?.total);
200
205
  }
@@ -211,6 +216,7 @@ export class TurnTelemetryTracker {
211
216
  totalMs: endMs - turn.startMs,
212
217
  inputTokens,
213
218
  outputTokens,
219
+ cacheReadTokens,
214
220
  stallMs: turn.stallMs,
215
221
  stallCount: turn.stallCount,
216
222
  rateUsdPerMTokens: validCost && validTokens
@@ -232,6 +238,7 @@ export class TurnTelemetryTracker {
232
238
 
233
239
  const outputTokens = turns.reduce((sum, turn) => sum + turn.outputTokens, 0);
234
240
  const inputTokens = turns.reduce((sum, turn) => sum + turn.inputTokens, 0);
241
+ const cacheReadTokens = turns.reduce((sum, turn) => sum + turn.cacheReadTokens, 0);
235
242
  const totalTokens = turns.reduce((sum, turn) => sum + turn.totalTokens, 0);
236
243
  const costUsd = turns.reduce((sum, turn) => sum + turn.costUsd, 0);
237
244
  const stallMs = turns.reduce((sum, turn) => sum + turn.stallMs, 0);
@@ -248,6 +255,7 @@ export class TurnTelemetryTracker {
248
255
  totalMs: this.now() - startMs,
249
256
  inputTokens,
250
257
  outputTokens,
258
+ cacheReadTokens,
251
259
  stallMs,
252
260
  stallCount,
253
261
  rateUsdPerMTokens: validRate ? round(costUsd / (totalTokens / 1_000_000), 2) : null,
@@ -282,7 +290,7 @@ export function formatTurnTelemetry(
282
290
  parts.push(theme.fg("success", `${glyphs.done} ${formatTurnDuration(telemetry.totalMs)}`));
283
291
  }
284
292
  if (config.tokens) {
285
- parts.push(theme.fg("accent", `${glyphs.input} ${fmtTokens(telemetry.inputTokens)}`));
293
+ parts.push(theme.fg("accent", `${glyphs.input} ${formatInputBreakdown(telemetry.inputTokens, telemetry.cacheReadTokens)}`));
286
294
  parts.push(theme.fg("success", `${glyphs.output} ${fmtTokens(telemetry.outputTokens)}`));
287
295
  }
288
296
  if (config.stalls && telemetry.stallMs > 0) {
@@ -67,6 +67,13 @@ export function fmtTokens(n: number): string {
67
67
  return `${Math.round(n / 1_000_000)}M`;
68
68
  }
69
69
 
70
+ export function formatInputBreakdown(uncached: number, cacheRead: number): string {
71
+ const total = fmtTokens(uncached + cacheRead);
72
+ return cacheRead > 0
73
+ ? `${total} (U ${fmtTokens(uncached)} + R ${fmtTokens(cacheRead)})`
74
+ : total;
75
+ }
76
+
70
77
  export function formatDuration(ms: number): string {
71
78
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
72
79
  if (totalSeconds < 60) return `${totalSeconds}s`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-tui",
3
- "version": "0.2.15",
3
+ "version": "0.3.0",
4
4
  "description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages.",
5
5
  "type": "module",
6
6
  "repository": {