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.
- package/assets/preview_dashboard_1.png +0 -0
- package/extensions/open-tui/footer.ts +329 -328
- package/extensions/open-tui/icons.ts +207 -207
- package/extensions/open-tui/runtime.ts +174 -174
- package/extensions/open-tui/state.ts +3 -1
- package/extensions/open-tui/telemetry.ts +11 -3
- package/extensions/open-tui/utils.ts +7 -0
- package/package.json +1 -1
|
@@ -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: "
|
|
25
|
-
{ name: "
|
|
26
|
-
{ name: "
|
|
27
|
-
{ name: "
|
|
28
|
-
{ name: "
|
|
29
|
-
{ name: "
|
|
30
|
-
{ name: "
|
|
31
|
-
{ name: "
|
|
32
|
-
{ name: "
|
|
33
|
-
{ name: "
|
|
34
|
-
{ name: "
|
|
35
|
-
{ name: "
|
|
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
|
-
|
|
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
|
-
|
|
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} ${
|
|
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.
|
|
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": {
|