opencode-system-metrics-tui 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -15,10 +15,14 @@ The README is available in **Español**, **English**, and **Português**.
15
15
  - GPU: utilización y temperatura.
16
16
  - GPU VRAM: memoria usada, total y porcentaje.
17
17
  - Red: velocidad de descarga y subida en tiempo real.
18
- - Actualización cada 2 segundos.
18
+ - CPU, RAM y red se consultan con una cadencia general de 2 segundos; GPU, VRAM y temperaturas usan una caché expirable de 10 segundos para reducir el coste sin congelar los sensores.
19
19
  - Colores compatibles con el tema activo de OpenCode.
20
- - Iconos Hack Nerd Font con fallback Unicode.
20
+ - Iconos Unicode por defecto en Windows (la detección de fuentes instaladas no cambia la selección); Linux/macOS usan Nerd Font solo si está instalada y mantienen fallback Unicode.
21
+ - En Windows usa iconos Unicode visibles de forma predeterminada, porque OpenCode no puede detectar la fuente activa de la terminal.
21
22
  - Soporte para Linux, macOS y Windows cuando el sistema expone las métricas.
23
+ - En Windows, la temperatura de CPU requiere LibreHardwareMonitor ejecutándose como administrador con **Remote Web Server** activo en `http://127.0.0.1:8085`.
24
+ - En Windows, la velocidad de red usa PowerShell y `Get-NetAdapterStatistics`; no agrega una dependencia npm.
25
+ - Las lecturas opcionales de Windows se hacen bajo demanda, con timeout y caché; no hay timers globales de PowerShell.
22
26
 
23
27
  ### Requisitos
24
28
 
@@ -35,7 +39,7 @@ npm run install-font
35
39
  opencode
36
40
  ```
37
41
 
38
- `npm run install-font` instala **Hack Nerd Font** únicamente para el usuario actual. No se ejecuta automáticamente durante `npm install` y solicita confirmación antes de descargar la fuente oficial.
42
+ `npm run install-font` ejecuta el instalador compilado incluido en `dist/`, instala **Hack Nerd Font** únicamente para el usuario actual, no se ejecuta automáticamente durante `npm install` y solicita confirmación antes de descargar la fuente oficial.
39
43
 
40
44
  Comandos adicionales:
41
45
 
@@ -54,7 +58,7 @@ La fuente se instala solo para el usuario actual:
54
58
  - macOS: `~/Library/Fonts`
55
59
  - Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
56
60
 
57
- Después de instalarla, selecciona **Hack Nerd Font Mono** en la terminal y reinicia la terminal y OpenCode. Si no se selecciona una Nerd Font, el plugin usa iconos Unicode de fallback.
61
+ Después de instalarla, selecciona **Hack Nerd Font Mono** en la terminal y reinicia la terminal y OpenCode. En Windows, para optar explícitamente por iconos Nerd Font define `OPENCODE_MONITOR_NERD_FONT=1`; no se activa solo por detectar archivos instalados.
58
62
 
59
63
  ### Publicación e instalación desde npm
60
64
 
@@ -71,8 +75,16 @@ En otro equipo:
71
75
  opencode plugin opencode-system-metrics-tui
72
76
  ```
73
77
 
78
+ El paquete exporta tanto la raíz (`opencode-system-metrics-tui`) como `./tui`; ambas rutas cargan el mismo bundle TUI.
79
+
74
80
  El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia OpenCode después de instalarlo.
75
81
 
82
+ Publicar en npm e instalar en OpenCode son pasos distintos:
83
+
84
+ - `npm publish --access public` publica el paquete en npm.
85
+ - `opencode plugin opencode-system-metrics-tui` lo instala solo en el proyecto actual.
86
+ - `opencode plugin -g opencode-system-metrics-tui` lo instala globalmente para todos los proyectos.
87
+
76
88
  ### Verificación
77
89
 
78
90
  ```bash
@@ -92,10 +104,14 @@ npm run build
92
104
  - GPU: utilization and temperature.
93
105
  - GPU VRAM: used memory, total memory, and percentage.
94
106
  - Network: live download and upload speeds.
95
- - Refreshes every 2 seconds.
107
+ - CPU, RAM, and network use the 2-second general cadence; GPU, VRAM, and temperatures use an expiring 10-second cache to reduce cost without freezing sensors.
96
108
  - Uses the active OpenCode theme colors.
97
- - Hack Nerd Font icons with Unicode fallback.
109
+ - Windows uses Unicode icons by default (installed-font detection does not change selection); Linux/macOS use Nerd Font only when installed and retain Unicode fallback.
110
+ - On Windows, visible Unicode icons are used by default because OpenCode cannot detect the terminal's active font.
98
111
  - Supports Linux, macOS, and Windows when the operating system exposes the metrics.
112
+ - On Windows, CPU temperature requires LibreHardwareMonitor running as administrator with **Remote Web Server** enabled at `http://127.0.0.1:8085`.
113
+ - On Windows, network speed uses PowerShell and `Get-NetAdapterStatistics`; it adds no npm dependency.
114
+ - Optional Windows reads are on demand, timeout-bounded, and cached; no global PowerShell timers are used.
99
115
 
100
116
  ### Requirements
101
117
 
@@ -112,7 +128,7 @@ npm run install-font
112
128
  opencode
113
129
  ```
114
130
 
115
- `npm run install-font` installs **Hack Nerd Font** for the current user only. It is not executed automatically by `npm install` and asks for confirmation before downloading the official font.
131
+ `npm run install-font` runs the compiled installer included in `dist/`, installs **Hack Nerd Font** for the current user only, is not executed automatically by `npm install`, and asks for confirmation before downloading the official font.
116
132
 
117
133
  Extra commands:
118
134
 
@@ -131,7 +147,7 @@ The font is installed for the current user only:
131
147
  - macOS: `~/Library/Fonts`
132
148
  - Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
133
149
 
134
- After installation, select **Hack Nerd Font Mono** in the terminal and restart the terminal and OpenCode. If a Nerd Font is not selected, the plugin uses Unicode fallback icons.
150
+ After installation, select **Hack Nerd Font Mono** in the terminal and restart the terminal and OpenCode. On Windows, explicitly opt into Nerd Font icons with `OPENCODE_MONITOR_NERD_FONT=1`; installed-font detection alone never enables them.
135
151
 
136
152
  ### Publishing and npm installation
137
153
 
@@ -148,8 +164,16 @@ On another machine:
148
164
  opencode plugin opencode-system-metrics-tui
149
165
  ```
150
166
 
167
+ The package exports both the root (`opencode-system-metrics-tui`) and `./tui`; both paths load the same TUI bundle.
168
+
151
169
  The OpenCode installer configures the TUI plugin automatically. Restart OpenCode after installation.
152
170
 
171
+ Publishing to npm and installing in OpenCode are separate steps:
172
+
173
+ - `npm publish --access public` publishes the package to npm.
174
+ - `opencode plugin opencode-system-metrics-tui` installs it for the current project only.
175
+ - `opencode plugin -g opencode-system-metrics-tui` installs it globally for all projects.
176
+
153
177
  ### Verification
154
178
 
155
179
  ```bash
@@ -169,10 +193,14 @@ npm run build
169
193
  - GPU: utilização e temperatura.
170
194
  - GPU VRAM: memória usada, total e porcentagem.
171
195
  - Rede: velocidades de download e upload em tempo real.
172
- - Atualização a cada 2 segundos.
196
+ - CPU, RAM e rede usam a cadência geral de 2 segundos; GPU, VRAM e temperaturas usam uma cache expirável de 10 segundos para reduzir o custo sem congelar os sensores.
173
197
  - Usa as cores do tema ativo do OpenCode.
174
- - Ícones Hack Nerd Font com fallback Unicode.
198
+ - No Windows, os ícones Unicode são usados por padrão (detectar fontes instaladas não muda a seleção); Linux/macOS usam Nerd Font apenas quando instalada e mantêm fallback Unicode.
199
+ - No Windows, ícones Unicode visíveis são usados por padrão porque o OpenCode não consegue detectar a fonte ativa do terminal.
175
200
  - Suporte para Linux, macOS e Windows quando o sistema disponibiliza as métricas.
201
+ - No Windows, a temperatura da CPU requer o LibreHardwareMonitor executado como administrador com o **Remote Web Server** ativo em `http://127.0.0.1:8085`.
202
+ - No Windows, a velocidade da rede usa o PowerShell e `Get-NetAdapterStatistics`; nenhuma dependência npm é adicionada.
203
+ - As leituras opcionais do Windows são sob demanda, têm timeout e cache; não há timers globais do PowerShell.
176
204
 
177
205
  ### Requisitos
178
206
 
@@ -189,7 +217,7 @@ npm run install-font
189
217
  opencode
190
218
  ```
191
219
 
192
- `npm run install-font` instala a **Hack Nerd Font** somente para o usuário atual. O comando não é executado automaticamente durante `npm install` e pede confirmação antes de baixar a fonte oficial.
220
+ `npm run install-font` executa o instalador compilado incluído em `dist/`, instala a **Hack Nerd Font** somente para o usuário atual, não é executado automaticamente durante `npm install` e pede confirmação antes de baixar a fonte oficial.
193
221
 
194
222
  Comandos adicionais:
195
223
 
@@ -208,7 +236,7 @@ A fonte é instalada somente para o usuário atual:
208
236
  - macOS: `~/Library/Fonts`
209
237
  - Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
210
238
 
211
- Depois da instalação, selecione **Hack Nerd Font Mono** no terminal e reinicie o terminal e o OpenCode. Se uma Nerd Font não estiver selecionada, o plugin usará ícones Unicode de fallback.
239
+ Depois da instalação, selecione **Hack Nerd Font Mono** no terminal e reinicie o terminal e o OpenCode. No Windows, opte explicitamente pelos ícones Nerd Font com `OPENCODE_MONITOR_NERD_FONT=1`; detectar arquivos instalados sozinho nunca os ativa.
212
240
 
213
241
  ### Publicação e instalação pelo npm
214
242
 
@@ -225,8 +253,16 @@ Em outro computador:
225
253
  opencode plugin opencode-system-metrics-tui
226
254
  ```
227
255
 
256
+ O pacote exporta tanto a raiz (`opencode-system-metrics-tui`) quanto `./tui`; os dois caminhos carregam o mesmo bundle TUI.
257
+
228
258
  O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o OpenCode após a instalação.
229
259
 
260
+ Publicar no npm e instalar no OpenCode são etapas diferentes:
261
+
262
+ - `npm publish --access public` publica o pacote no npm.
263
+ - `opencode plugin opencode-system-metrics-tui` instala o plugin apenas no projeto atual.
264
+ - `opencode plugin -g opencode-system-metrics-tui` instala o plugin globalmente para todos os projetos.
265
+
230
266
  ### Verificação
231
267
 
232
268
  ```bash
@@ -0,0 +1,156 @@
1
+ // scripts/install-hack-font.ts
2
+ import { cp, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { execFile as execFileCallback } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ // src/font.ts
9
+ import { existsSync, readdirSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join as posixJoin, win32 } from "node:path";
12
+ var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
13
+ function getFontDirectories(platform, home = homedir(), env = process.env) {
14
+ if (platform === "darwin")
15
+ return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
16
+ if (platform === "win32") {
17
+ const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
18
+ const windows = env.WINDIR ?? "C:\\Windows";
19
+ return [win32.join(localAppData, "Microsoft", "Windows", "Fonts"), win32.join(windows, "Fonts")];
20
+ }
21
+ return [posixJoin(home, ".local", "share", "fonts"), posixJoin(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
22
+ }
23
+ function isNerdFontFile(fileName) {
24
+ return NERD_FONT_FILE_PATTERN.test(fileName);
25
+ }
26
+ var isHackNerdFontFile = isNerdFontFile;
27
+ function hasNerdFont(platform = process.platform, home = homedir(), env = process.env) {
28
+ const containsFont = (directory) => {
29
+ if (!existsSync(directory))
30
+ return false;
31
+ try {
32
+ return readdirSync(directory, { withFileTypes: true }).some((entry) => {
33
+ const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
34
+ return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
35
+ });
36
+ } catch {
37
+ return false;
38
+ }
39
+ };
40
+ return getFontDirectories(platform, home, env).some(containsFont);
41
+ }
42
+ var hasHackNerdFont = hasNerdFont;
43
+ var nerdFontIcons = {
44
+ title: "\uDB80\uDF79",
45
+ cpu: "\uDB83\uDEE0",
46
+ ram: "\uDB81\uDE1A",
47
+ gpu: "\uDB82\uDCAE",
48
+ vram: "\uDB80\uDF5B",
49
+ thermometer: "",
50
+ network: "\uDB81\uDEF3"
51
+ };
52
+ var unicodeIcons = { title: "▦", cpu: "▣", ram: "▤", gpu: "◆", vram: "◈", thermometer: "♨", network: "⇅" };
53
+ function getMetricIcons(useNerdFont) {
54
+ return useNerdFont ? nerdFontIcons : unicodeIcons;
55
+ }
56
+ function shouldUseNerdFont(platform, detected, env = process.env) {
57
+ if (platform === "win32")
58
+ return env.OPENCODE_MONITOR_NERD_FONT === "1";
59
+ return detected;
60
+ }
61
+
62
+ // scripts/install-hack-font.ts
63
+ var execFile = promisify(execFileCallback);
64
+ var RELEASE_URL = "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/Hack.zip";
65
+ function printHelp() {
66
+ console.log(`Install Hack Nerd Font for the current user.
67
+
68
+ Usage:
69
+ npm run install-font Ask before downloading and installing
70
+ npm run install-font --check Detect only; never downloads or changes files
71
+ npm run install-font --help Show this help
72
+ npm run install-font --yes Skip the download confirmation`);
73
+ }
74
+ async function confirm() {
75
+ if (!process.stdin.isTTY)
76
+ return false;
77
+ process.stdout.write("Download Hack Nerd Font from the official Nerd Fonts release? [y/N] ");
78
+ for await (const chunk of process.stdin)
79
+ return /^y(es)?$/i.test(String(chunk).trim());
80
+ return false;
81
+ }
82
+ async function findFontFiles(directory) {
83
+ const files = [];
84
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
85
+ const path = join(directory, entry.name);
86
+ if (entry.isDirectory())
87
+ files.push(...await findFontFiles(path));
88
+ else if (entry.isFile() && isHackNerdFontFile(entry.name))
89
+ files.push(path);
90
+ }
91
+ return files;
92
+ }
93
+ async function extractZip(zipPath, destination, platform) {
94
+ try {
95
+ if (platform === "win32") {
96
+ await execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", zipPath, destination]);
97
+ } else {
98
+ await execFile("unzip", ["-q", "-o", zipPath, "-d", destination]);
99
+ }
100
+ } catch (error) {
101
+ throw new Error(`Could not extract the ZIP archive. Install unzip (Linux/macOS) or use PowerShell (Windows). ${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ }
104
+ async function registerWindowsFonts(fontFiles) {
105
+ const script = "$key='HKCU:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts'; New-Item -Path $key -Force | Out-Null; foreach ($font in $args) { New-ItemProperty -Path $key -Name ([IO.Path]::GetFileNameWithoutExtension($font)) -Value $font -PropertyType String -Force | Out-Null }";
106
+ await execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, ...fontFiles]);
107
+ }
108
+ async function install() {
109
+ const platform = process.platform;
110
+ const supportedPlatforms = ["linux", "darwin", "win32"];
111
+ if (!supportedPlatforms.includes(platform))
112
+ throw new Error(`Unsupported platform: ${process.platform}`);
113
+ if (hasHackNerdFont(platform))
114
+ return console.log("Hack Nerd Font is already installed; nothing to do.");
115
+ if (!(process.argv.includes("--yes") || await confirm()))
116
+ return console.log("Installation cancelled. Use --yes to confirm non-interactively.");
117
+ const temporaryDirectory = await mkdtemp(join(tmpdir(), "opencode-monitor-font-"));
118
+ try {
119
+ console.log(`Downloading ${RELEASE_URL}`);
120
+ const response = await fetch(RELEASE_URL);
121
+ if (!response.ok)
122
+ throw new Error(`Download failed with HTTP ${response.status} ${response.statusText}`);
123
+ const zipPath = join(temporaryDirectory, "Hack.zip");
124
+ await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
125
+ const extractedDirectory = join(temporaryDirectory, "extracted");
126
+ await mkdir(extractedDirectory);
127
+ await extractZip(zipPath, extractedDirectory, platform);
128
+ const fontFiles = await findFontFiles(extractedDirectory);
129
+ if (fontFiles.length === 0)
130
+ throw new Error("The downloaded archive did not contain Hack Nerd Font files.");
131
+ const targetDirectory = getFontDirectories(platform)[0];
132
+ await mkdir(targetDirectory, { recursive: true });
133
+ const installedFiles = fontFiles.map((file) => join(targetDirectory, basename(file)));
134
+ for (let index = 0;index < fontFiles.length; index += 1)
135
+ await cp(fontFiles[index], installedFiles[index], { force: true });
136
+ if (platform === "win32") {
137
+ try {
138
+ await registerWindowsFonts(installedFiles);
139
+ } catch {
140
+ console.warn("Fonts were copied, but Windows font registration failed. Select Hack Nerd Font in the terminal manually.");
141
+ }
142
+ }
143
+ console.log(`Installed ${fontFiles.length} font files in ${targetDirectory}. Select Hack Nerd Font in your terminal and restart OpenCode.`);
144
+ } finally {
145
+ await rm(temporaryDirectory, { recursive: true, force: true });
146
+ }
147
+ }
148
+ if (process.argv.includes("--help"))
149
+ printHelp();
150
+ else if (process.argv.includes("--check"))
151
+ console.log(hasHackNerdFont() ? "Hack Nerd Font is installed." : "Hack Nerd Font is not installed.");
152
+ else
153
+ install().catch((error) => {
154
+ console.error(`Hack Nerd Font installation could not be completed: ${error instanceof Error ? error.message : String(error)}`);
155
+ process.exitCode = 1;
156
+ });
package/dist/tui.js CHANGED
@@ -1,4 +1,3 @@
1
- // @bun
2
1
  // src/tui.tsx
3
2
  import { createComponent as _$createComponent } from "@opentui/solid";
4
3
  import { effect as _$effect } from "@opentui/solid";
@@ -13,19 +12,19 @@ import { TextAttributes } from "@opentui/core";
13
12
  // src/format.ts
14
13
  var BYTES_PER_GIB = 1024 ** 3;
15
14
  function formatPercent(value) {
16
- return value === null || !Number.isFinite(value) ? "\u2014" : `${Math.round(value)}%`;
15
+ return value === null || !Number.isFinite(value) ? "" : `${Math.round(value)}%`;
17
16
  }
18
17
  function formatTemperature(value) {
19
- return value === null || !Number.isFinite(value) ? "\u2014" : `${Math.round(value)}\xB0C`;
18
+ return value === null || !Number.isFinite(value) ? "" : `${Math.round(value)}°C`;
20
19
  }
21
20
  function formatGiB(bytes) {
22
21
  if (bytes === null || !Number.isFinite(bytes))
23
- return "\u2014";
22
+ return "";
24
23
  return `${(bytes / BYTES_PER_GIB).toFixed(1)} GiB`;
25
24
  }
26
25
  function formatRate(bytesPerSecond) {
27
26
  if (bytesPerSecond === null || !Number.isFinite(bytesPerSecond))
28
- return "\u2014";
27
+ return "";
29
28
  const units = ["B/s", "KiB/s", "MiB/s", "GiB/s"];
30
29
  let value = Math.max(0, bytesPerSecond);
31
30
  let unit = 0;
@@ -37,7 +36,50 @@ function formatRate(bytesPerSecond) {
37
36
  }
38
37
 
39
38
  // src/metrics.ts
39
+ import { execFile } from "node:child_process";
40
+ import { promisify } from "node:util";
40
41
  import si from "systeminformation";
42
+ var execFileAsync = promisify(execFile);
43
+ function parseMonitorTemperature(value) {
44
+ if (value === undefined)
45
+ return;
46
+ const parsed = Number(value.replace(",", ".").replace(/[^\d.-]/g, ""));
47
+ return Number.isFinite(parsed) ? parsed : undefined;
48
+ }
49
+ async function readLibreHardwareMonitorTemperature() {
50
+ const response = await fetch("http://127.0.0.1:8085/data.json", { signal: AbortSignal.timeout(2000) });
51
+ if (!response.ok)
52
+ return;
53
+ const root = await response.json();
54
+ const sensors = [];
55
+ const visit = (node) => {
56
+ if (node.Type === "Temperature" && node.Text !== undefined)
57
+ sensors.push(node);
58
+ for (const child of node.Children ?? [])
59
+ visit(child);
60
+ };
61
+ visit(root);
62
+ const cpuSensor = sensors.find((sensor) => sensor.Text === "CPU Package") ?? sensors.find((sensor) => sensor.Text === "Core Average") ?? sensors.find((sensor) => sensor.Text === "Core Max");
63
+ return parseMonitorTemperature(cpuSensor?.Value);
64
+ }
65
+ function parseWindowsNetworkOutput(stdout) {
66
+ const parsed = JSON.parse(stdout.trim());
67
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
68
+ return rows.map((item) => ({
69
+ iface: item.Name,
70
+ rx_bytes: finite(item.ReceivedBytes) ?? 0,
71
+ tx_bytes: finite(item.SentBytes) ?? 0
72
+ }));
73
+ }
74
+ async function readWindowsNetworkSample() {
75
+ const { stdout } = await execFileAsync("powershell.exe", [
76
+ "-NoProfile",
77
+ "-NonInteractive",
78
+ "-Command",
79
+ "Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress"
80
+ ], { windowsHide: true, timeout: 15000, maxBuffer: 1024 * 1024 });
81
+ return parseWindowsNetworkOutput(stdout);
82
+ }
41
83
  var previousNetwork;
42
84
  var unavailable = {
43
85
  cpuPercent: null,
@@ -70,62 +112,139 @@ async function withTimeout(promise, timeoutMs) {
70
112
  clearTimeout(timeout);
71
113
  }
72
114
  }
115
+ var SLOW_METRIC_CACHE_MS = 1e4;
116
+ var TEMPERATURE_CACHE_MS = 1e4;
117
+ var isWindows = process.platform === "win32";
118
+ var NETWORK_CACHE_MS = 2000;
119
+ function selectGpuMetrics(graphics) {
120
+ const empty = {
121
+ gpuPercent: null,
122
+ gpuTemperatureCelsius: null,
123
+ gpuMemoryUsedBytes: null,
124
+ gpuMemoryTotalBytes: null,
125
+ gpuMemoryPercent: null
126
+ };
127
+ if (graphics === undefined)
128
+ return empty;
129
+ const controller = graphics.controllers.map((item) => {
130
+ const utilization = finite(item.utilizationGpu);
131
+ const temperature = finite(item.temperatureGpu);
132
+ const memoryTotal = finite(item.memoryTotal);
133
+ const memoryFree = finite(item.memoryFree);
134
+ const memoryUsed = finite(item.memoryUsed) ?? (memoryTotal !== null && memoryFree !== null ? Math.max(0, memoryTotal - memoryFree) : null);
135
+ const hasMemory = memoryUsed !== null && memoryTotal !== null && memoryTotal > 0;
136
+ return { utilization, temperature, memoryUsed, memoryTotal, hasMemory };
137
+ }).filter(({ utilization, temperature, hasMemory }) => utilization !== null || temperature !== null || hasMemory).sort((left, right) => {
138
+ const score = (value) => (value.hasMemory ? 2 : 0) + (value.utilization !== null ? 1 : 0);
139
+ return score(right) - score(left) || (right.utilization ?? -Infinity) - (left.utilization ?? -Infinity);
140
+ })[0];
141
+ if (controller === undefined)
142
+ return empty;
143
+ const result = {
144
+ gpuPercent: controller.utilization ?? 0,
145
+ gpuTemperatureCelsius: controller.temperature,
146
+ gpuMemoryUsedBytes: null,
147
+ gpuMemoryTotalBytes: null,
148
+ gpuMemoryPercent: null
149
+ };
150
+ if (controller.hasMemory) {
151
+ result.gpuMemoryUsedBytes = controller.memoryUsed * 1024 ** 2;
152
+ result.gpuMemoryTotalBytes = controller.memoryTotal * 1024 ** 2;
153
+ result.gpuMemoryPercent = controller.memoryUsed / controller.memoryTotal * 100;
154
+ }
155
+ return result;
156
+ }
157
+ function readCachedMetric(state, reader, cacheMs = SLOW_METRIC_CACHE_MS, now = Date.now()) {
158
+ if (state.inFlight !== undefined)
159
+ return state.inFlight;
160
+ if (now - state.lastAttemptAt < cacheMs)
161
+ return Promise.resolve(state.value);
162
+ state.lastAttemptAt = now;
163
+ state.inFlight = reader().then((value) => {
164
+ state.value = value;
165
+ return value;
166
+ }).catch(() => state.value).finally(() => {
167
+ state.inFlight = undefined;
168
+ });
169
+ return state.inFlight;
170
+ }
171
+ var cachedCpuTemperature = {
172
+ value: undefined,
173
+ lastAttemptAt: 0,
174
+ inFlight: undefined
175
+ };
176
+ var cachedGraphics = {
177
+ value: undefined,
178
+ lastAttemptAt: 0,
179
+ inFlight: undefined
180
+ };
181
+ var cachedNetwork = {
182
+ value: undefined,
183
+ lastAttemptAt: 0,
184
+ inFlight: undefined
185
+ };
186
+ var previousWindowsNetwork;
187
+ async function readWindowsNetworkMetrics() {
188
+ const samples = await readWindowsNetworkSample();
189
+ const timestamp = Date.now();
190
+ const rx = samples.reduce((total, item) => total + (finite(item.rx_bytes) ?? 0), 0);
191
+ const tx = samples.reduce((total, item) => total + (finite(item.tx_bytes) ?? 0), 0);
192
+ const seconds = previousWindowsNetwork === undefined ? undefined : Math.max((timestamp - previousWindowsNetwork.timestamp) / 1000, 0.001);
193
+ const result = [{
194
+ rx_bytes: rx,
195
+ tx_bytes: tx,
196
+ rx_sec: seconds === undefined ? undefined : Math.max(0, rx - previousWindowsNetwork.rx) / seconds,
197
+ tx_sec: seconds === undefined ? undefined : Math.max(0, tx - previousWindowsNetwork.tx) / seconds
198
+ }];
199
+ previousWindowsNetwork = { timestamp, rx, tx };
200
+ return result;
201
+ }
202
+ async function readWindowsCpuTemperature() {
203
+ let value;
204
+ try {
205
+ value = await readLibreHardwareMonitorTemperature();
206
+ } catch {}
207
+ if (value === undefined) {
208
+ try {
209
+ const fallback = await si.cpuTemperature();
210
+ value = finite(fallback.main) ?? finite(fallback.max) ?? undefined;
211
+ } catch {}
212
+ }
213
+ return value === undefined ? undefined : { main: value };
214
+ }
73
215
  async function readMetrics() {
74
216
  const [load, memory] = await Promise.allSettled([
75
- si.currentLoad(),
76
- si.mem()
217
+ withTimeout(si.currentLoad(), 1500),
218
+ withTimeout(si.mem(), 1500)
77
219
  ]);
78
220
  const [temperature, graphics, network] = await Promise.allSettled([
79
- withTimeout(si.cpuTemperature(), 1500),
80
- withTimeout(si.graphics(), 1500),
81
- withTimeout(si.networkStats("*"), 1500)
221
+ withTimeout(readCachedMetric(cachedCpuTemperature, isWindows ? readWindowsCpuTemperature : () => si.cpuTemperature(), TEMPERATURE_CACHE_MS), 2500),
222
+ withTimeout(readCachedMetric(cachedGraphics, () => si.graphics(), SLOW_METRIC_CACHE_MS), 2500),
223
+ withTimeout(readCachedMetric(cachedNetwork, isWindows ? readWindowsNetworkMetrics : () => si.networkStats("*"), isWindows ? NETWORK_CACHE_MS : 2000), 1500)
82
224
  ]);
83
225
  const cpuPercent = load.status === "fulfilled" ? finite(load.value.currentLoad) : null;
84
226
  const memoryTotalBytes = memory.status === "fulfilled" ? finite(memory.value.total) : null;
85
227
  const memoryAvailableBytes = memory.status === "fulfilled" ? finite(memory.value.available) : null;
86
228
  const memoryUsedBytes = memoryTotalBytes !== null && memoryAvailableBytes !== null ? Math.max(0, memoryTotalBytes - memoryAvailableBytes) : memory.status === "fulfilled" ? finite(memory.value.used) : null;
87
229
  const memoryPercent = memoryUsedBytes !== null && memoryTotalBytes !== null && memoryTotalBytes > 0 ? memoryUsedBytes / memoryTotalBytes * 100 : null;
88
- let gpuPercent = null;
89
- let gpuTemperatureCelsius = null;
90
- let gpuMemoryUsedBytes = null;
91
- let gpuMemoryTotalBytes = null;
92
- let gpuMemoryPercent = null;
93
- if (graphics.status === "fulfilled") {
94
- const controller = graphics.value.controllers.map((item) => {
95
- const utilization = finite(item.utilizationGpu);
96
- const temperature = finite(item.temperatureGpu);
97
- const memoryUsed = finite(item.memoryUsed);
98
- const memoryTotal = finite(item.memoryTotal);
99
- const hasMemory = memoryUsed !== null && memoryTotal !== null && memoryTotal > 0;
100
- return { item, utilization, temperature, memoryUsed, memoryTotal, hasMemory };
101
- }).filter(({ utilization, temperature, hasMemory }) => utilization !== null || temperature !== null || hasMemory).sort((left, right) => {
102
- const score = (value) => (value.hasMemory ? 2 : 0) + (value.utilization !== null ? 1 : 0);
103
- const scoreDifference = score(right) - score(left);
104
- if (scoreDifference !== 0)
105
- return scoreDifference;
106
- return (right.utilization ?? -Infinity) - (left.utilization ?? -Infinity);
107
- })[0];
108
- if (controller !== undefined) {
109
- gpuPercent = controller.utilization ?? (controller.hasMemory ? 0 : null);
110
- gpuTemperatureCelsius = controller.temperature;
111
- if (controller.hasMemory) {
112
- gpuMemoryUsedBytes = controller.memoryUsed * 1024 ** 2;
113
- gpuMemoryTotalBytes = controller.memoryTotal * 1024 ** 2;
114
- gpuMemoryPercent = controller.memoryUsed / controller.memoryTotal * 100;
115
- }
116
- }
117
- }
230
+ const gpu = selectGpuMetrics(graphics.status === "fulfilled" ? graphics.value : undefined);
118
231
  let downloadBytesPerSecond = null;
119
232
  let uploadBytesPerSecond = null;
120
- if (network.status === "fulfilled") {
233
+ if (network.status === "fulfilled" && network.value !== undefined) {
121
234
  const samples = network.value;
122
235
  const timestamp = Date.now();
123
236
  const rx = samples.reduce((total, item) => total + (finite(item.rx_bytes) ?? 0), 0);
124
237
  const tx = samples.reduce((total, item) => total + (finite(item.tx_bytes) ?? 0), 0);
125
- if (previousNetwork !== undefined) {
238
+ if (isWindows) {
239
+ downloadBytesPerSecond = finite(samples[0]?.rx_sec) ?? null;
240
+ uploadBytesPerSecond = finite(samples[0]?.tx_sec) ?? null;
241
+ } else if (previousNetwork !== undefined) {
126
242
  const seconds = Math.max((timestamp - previousNetwork.timestamp) / 1000, 0.001);
127
243
  downloadBytesPerSecond = Math.max(0, rx - previousNetwork.rx) / seconds;
128
244
  uploadBytesPerSecond = Math.max(0, tx - previousNetwork.tx) / seconds;
245
+ } else {
246
+ downloadBytesPerSecond = finite(samples[0]?.rx_sec) ?? null;
247
+ uploadBytesPerSecond = finite(samples[0]?.tx_sec) ?? null;
129
248
  }
130
249
  previousNetwork = { timestamp, rx, tx };
131
250
  }
@@ -135,22 +254,22 @@ async function readMetrics() {
135
254
  memoryUsedBytes,
136
255
  memoryTotalBytes,
137
256
  memoryPercent,
138
- gpuPercent,
139
- cpuTemperatureCelsius: temperature.status === "fulfilled" ? finite(temperature.value.main) ?? finite(temperature.value.max) : null,
140
- gpuTemperatureCelsius,
141
- gpuMemoryUsedBytes,
142
- gpuMemoryTotalBytes,
143
- gpuMemoryPercent,
257
+ gpuPercent: gpu.gpuPercent,
258
+ cpuTemperatureCelsius: temperature.status === "fulfilled" && temperature.value !== undefined ? finite(temperature.value.main) ?? ("max" in temperature.value ? finite(temperature.value.max) : null) : null,
259
+ gpuTemperatureCelsius: gpu.gpuTemperatureCelsius,
260
+ gpuMemoryUsedBytes: gpu.gpuMemoryUsedBytes,
261
+ gpuMemoryTotalBytes: gpu.gpuMemoryTotalBytes,
262
+ gpuMemoryPercent: gpu.gpuMemoryPercent,
144
263
  downloadBytesPerSecond,
145
264
  uploadBytesPerSecond
146
265
  };
147
266
  }
148
267
 
149
268
  // src/font.ts
150
- import { existsSync, readdirSync } from "fs";
151
- import { homedir } from "os";
152
- import { join as posixJoin, win32 } from "path";
153
- var FONT_FILE_PATTERN = /^HackNerdFont(?:Mono|Propo|Console)?-[A-Za-z0-9-]+\.(?:ttf|otf)$/i;
269
+ import { existsSync, readdirSync } from "node:fs";
270
+ import { homedir } from "node:os";
271
+ import { join as posixJoin, win32 } from "node:path";
272
+ var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
154
273
  function getFontDirectories(platform, home = homedir(), env = process.env) {
155
274
  if (platform === "darwin")
156
275
  return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
@@ -161,17 +280,18 @@ function getFontDirectories(platform, home = homedir(), env = process.env) {
161
280
  }
162
281
  return [posixJoin(home, ".local", "share", "fonts"), posixJoin(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
163
282
  }
164
- function isHackNerdFontFile(fileName) {
165
- return FONT_FILE_PATTERN.test(fileName);
283
+ function isNerdFontFile(fileName) {
284
+ return NERD_FONT_FILE_PATTERN.test(fileName);
166
285
  }
167
- function hasHackNerdFont(platform = process.platform, home = homedir(), env = process.env) {
286
+ var isHackNerdFontFile = isNerdFontFile;
287
+ function hasNerdFont(platform = process.platform, home = homedir(), env = process.env) {
168
288
  const containsFont = (directory) => {
169
289
  if (!existsSync(directory))
170
290
  return false;
171
291
  try {
172
292
  return readdirSync(directory, { withFileTypes: true }).some((entry) => {
173
293
  const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
174
- return entry.isFile() ? isHackNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
294
+ return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
175
295
  });
176
296
  } catch {
177
297
  return false;
@@ -179,71 +299,87 @@ function hasHackNerdFont(platform = process.platform, home = homedir(), env = pr
179
299
  };
180
300
  return getFontDirectories(platform, home, env).some(containsFont);
181
301
  }
302
+ var hasHackNerdFont = hasNerdFont;
182
303
  var nerdFontIcons = {
183
304
  title: "\uDB80\uDF79",
184
305
  cpu: "\uDB83\uDEE0",
185
306
  ram: "\uDB81\uDE1A",
186
307
  gpu: "\uDB82\uDCAE",
187
308
  vram: "\uDB80\uDF5B",
188
- thermometer: "\uF2C7",
309
+ thermometer: "",
189
310
  network: "\uDB81\uDEF3"
190
311
  };
191
- var unicodeIcons = { title: "\u25A6", cpu: "\u25A3", ram: "\u25A4", gpu: "\u25C6", vram: "\u25C8", thermometer: "\u2668", network: "\u21C5" };
312
+ var unicodeIcons = { title: "", cpu: "", ram: "", gpu: "", vram: "", thermometer: "", network: "" };
192
313
  function getMetricIcons(useNerdFont) {
193
314
  return useNerdFont ? nerdFontIcons : unicodeIcons;
194
315
  }
316
+ function shouldUseNerdFont(platform, detected, env = process.env) {
317
+ if (platform === "win32")
318
+ return env.OPENCODE_MONITOR_NERD_FONT === "1";
319
+ return detected;
320
+ }
195
321
 
196
322
  // src/tui.tsx
197
323
  var REFRESH_INTERVAL_MS = 2000;
198
- var icons = getMetricIcons(hasHackNerdFont());
324
+ var icons = getMetricIcons(shouldUseNerdFont(process.platform, hasNerdFont()));
325
+ var initialMetrics = {
326
+ cpuPercent: null,
327
+ memoryUsedBytes: null,
328
+ memoryTotalBytes: null,
329
+ memoryPercent: null,
330
+ gpuPercent: null,
331
+ cpuTemperatureCelsius: null,
332
+ gpuTemperatureCelsius: null,
333
+ gpuMemoryUsedBytes: null,
334
+ gpuMemoryTotalBytes: null,
335
+ gpuMemoryPercent: null,
336
+ downloadBytesPerSecond: null,
337
+ uploadBytesPerSecond: null
338
+ };
339
+ var [sharedMetrics, setSharedMetrics] = createSignal(initialMetrics);
340
+ var metricsTimer;
341
+ var metricsConsumers = 0;
342
+ var refreshing = false;
343
+ var refreshMetrics = async () => {
344
+ if (refreshing)
345
+ return;
346
+ refreshing = true;
347
+ try {
348
+ setSharedMetrics(await readMetrics());
349
+ } finally {
350
+ refreshing = false;
351
+ }
352
+ };
353
+ function startMetricsPolling() {
354
+ metricsConsumers += 1;
355
+ if (metricsConsumers !== 1)
356
+ return;
357
+ refreshMetrics();
358
+ metricsTimer = setInterval(() => void refreshMetrics(), REFRESH_INTERVAL_MS);
359
+ }
360
+ function stopMetricsPolling() {
361
+ metricsConsumers = Math.max(0, metricsConsumers - 1);
362
+ if (metricsConsumers !== 0 || metricsTimer === undefined)
363
+ return;
364
+ clearInterval(metricsTimer);
365
+ metricsTimer = undefined;
366
+ }
199
367
  function systemMetricsTitle() {
200
368
  const locale = process.env.LC_ALL ?? process.env.LANGUAGE?.split(":")[0] ?? process.env.LANG ?? Intl.DateTimeFormat().resolvedOptions().locale;
201
369
  const language = locale.split(/[-_.]/)[0].toLowerCase();
202
370
  return {
203
371
  de: "Systemmetriken",
204
- es: "M\xE9tricas del sistema",
205
- fr: "M\xE9triques syst\xE8me",
372
+ es: "Métricas del sistema",
373
+ fr: "Métriques système",
206
374
  it: "Metriche di sistema",
207
- pt: "M\xE9tricas do sistema"
375
+ pt: "Métricas do sistema"
208
376
  }[language] ?? "System metrics";
209
377
  }
210
378
  function MetricsPanel(props) {
211
- const [metrics, setMetrics] = createSignal({
212
- cpuPercent: null,
213
- memoryUsedBytes: null,
214
- memoryTotalBytes: null,
215
- memoryPercent: null,
216
- gpuPercent: null,
217
- cpuTemperatureCelsius: null,
218
- gpuTemperatureCelsius: null,
219
- gpuMemoryUsedBytes: null,
220
- gpuMemoryTotalBytes: null,
221
- gpuMemoryPercent: null,
222
- downloadBytesPerSecond: null,
223
- uploadBytesPerSecond: null
224
- });
225
- let refreshing = false;
226
- const refresh = async () => {
227
- if (refreshing)
228
- return;
229
- refreshing = true;
230
- try {
231
- setMetrics(await readMetrics());
232
- } finally {
233
- refreshing = false;
234
- }
235
- };
236
- let timer;
237
- onMount(() => {
238
- refresh();
239
- timer = setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
240
- });
241
- onCleanup(() => {
242
- if (timer !== undefined)
243
- clearInterval(timer);
244
- });
379
+ onMount(startMetricsPolling);
380
+ onCleanup(stopMetricsPolling);
245
381
  return (() => {
246
- var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createTextNode(` \xB7 `), _el$0 = _$createTextNode(` `), _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("text"), _el$13 = _$createElement("text"), _el$14 = _$createTextNode(` / `), _el$15 = _$createTextNode(` (`), _el$16 = _$createTextNode(`)`), _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` \xB7 `), _el$23 = _$createTextNode(` `), _el$24 = _$createElement("box"), _el$25 = _$createElement("text"), _el$26 = _$createElement("text"), _el$28 = _$createElement("text"), _el$29 = _$createTextNode(` / `), _el$30 = _$createTextNode(` (`), _el$31 = _$createTextNode(`)`), _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$34 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createTextNode(`\u2193 `), _el$38 = _$createTextNode(` \u2191 `);
382
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createTextNode(` · `), _el$0 = _$createTextNode(` `), _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("text"), _el$13 = _$createElement("text"), _el$14 = _$createTextNode(` / `), _el$15 = _$createTextNode(` (`), _el$16 = _$createTextNode(`)`), _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` · `), _el$23 = _$createTextNode(` `), _el$24 = _$createElement("box"), _el$25 = _$createElement("text"), _el$26 = _$createElement("text"), _el$28 = _$createElement("text"), _el$29 = _$createTextNode(` / `), _el$30 = _$createTextNode(` (`), _el$31 = _$createTextNode(`)`), _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$34 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createTextNode(`↓ `), _el$38 = _$createTextNode(` `);
247
383
  _$insertNode(_el$, _el$2);
248
384
  _$insertNode(_el$, _el$4);
249
385
  _$insertNode(_el$, _el$1);
@@ -264,9 +400,9 @@ function MetricsPanel(props) {
264
400
  _$insertNode(_el$6, _$createTextNode(` CPU `));
265
401
  _$insertNode(_el$8, _el$9);
266
402
  _$insertNode(_el$8, _el$0);
267
- _$insert(_el$8, () => formatPercent(metrics().cpuPercent), _el$9);
403
+ _$insert(_el$8, () => formatPercent(sharedMetrics().cpuPercent), _el$9);
268
404
  _$insert(_el$8, () => icons.thermometer, _el$0);
269
- _$insert(_el$8, () => formatTemperature(metrics().cpuTemperatureCelsius), null);
405
+ _$insert(_el$8, () => formatTemperature(sharedMetrics().cpuTemperatureCelsius), null);
270
406
  _$insertNode(_el$1, _el$10);
271
407
  _$insertNode(_el$1, _el$11);
272
408
  _$insertNode(_el$1, _el$13);
@@ -276,9 +412,9 @@ function MetricsPanel(props) {
276
412
  _$insertNode(_el$13, _el$14);
277
413
  _$insertNode(_el$13, _el$15);
278
414
  _$insertNode(_el$13, _el$16);
279
- _$insert(_el$13, () => formatGiB(metrics().memoryUsedBytes), _el$14);
280
- _$insert(_el$13, () => formatGiB(metrics().memoryTotalBytes), _el$15);
281
- _$insert(_el$13, () => formatPercent(metrics().memoryPercent), _el$16);
415
+ _$insert(_el$13, () => formatGiB(sharedMetrics().memoryUsedBytes), _el$14);
416
+ _$insert(_el$13, () => formatGiB(sharedMetrics().memoryTotalBytes), _el$15);
417
+ _$insert(_el$13, () => formatPercent(sharedMetrics().memoryPercent), _el$16);
282
418
  _$insertNode(_el$17, _el$18);
283
419
  _$insertNode(_el$17, _el$19);
284
420
  _$insertNode(_el$17, _el$21);
@@ -287,9 +423,9 @@ function MetricsPanel(props) {
287
423
  _$insertNode(_el$19, _$createTextNode(` GPU `));
288
424
  _$insertNode(_el$21, _el$22);
289
425
  _$insertNode(_el$21, _el$23);
290
- _$insert(_el$21, () => formatPercent(metrics().gpuPercent), _el$22);
426
+ _$insert(_el$21, () => formatPercent(sharedMetrics().gpuPercent), _el$22);
291
427
  _$insert(_el$21, () => icons.thermometer, _el$23);
292
- _$insert(_el$21, () => formatTemperature(metrics().gpuTemperatureCelsius), null);
428
+ _$insert(_el$21, () => formatTemperature(sharedMetrics().gpuTemperatureCelsius), null);
293
429
  _$insertNode(_el$24, _el$25);
294
430
  _$insertNode(_el$24, _el$26);
295
431
  _$insertNode(_el$24, _el$28);
@@ -299,9 +435,9 @@ function MetricsPanel(props) {
299
435
  _$insertNode(_el$28, _el$29);
300
436
  _$insertNode(_el$28, _el$30);
301
437
  _$insertNode(_el$28, _el$31);
302
- _$insert(_el$28, () => formatGiB(metrics().gpuMemoryUsedBytes), _el$29);
303
- _$insert(_el$28, () => formatGiB(metrics().gpuMemoryTotalBytes), _el$30);
304
- _$insert(_el$28, () => formatPercent(metrics().gpuMemoryPercent), _el$31);
438
+ _$insert(_el$28, () => formatGiB(sharedMetrics().gpuMemoryUsedBytes), _el$29);
439
+ _$insert(_el$28, () => formatGiB(sharedMetrics().gpuMemoryTotalBytes), _el$30);
440
+ _$insert(_el$28, () => formatPercent(sharedMetrics().gpuMemoryPercent), _el$31);
305
441
  _$insertNode(_el$32, _el$33);
306
442
  _$insertNode(_el$32, _el$34);
307
443
  _$insertNode(_el$32, _el$36);
@@ -310,8 +446,8 @@ function MetricsPanel(props) {
310
446
  _$insertNode(_el$34, _$createTextNode(` NET `));
311
447
  _$insertNode(_el$36, _el$37);
312
448
  _$insertNode(_el$36, _el$38);
313
- _$insert(_el$36, () => formatRate(metrics().downloadBytesPerSecond), _el$38);
314
- _$insert(_el$36, () => formatRate(metrics().uploadBytesPerSecond), null);
449
+ _$insert(_el$36, () => formatRate(sharedMetrics().downloadBytesPerSecond), _el$38);
450
+ _$insert(_el$36, () => formatRate(sharedMetrics().uploadBytesPerSecond), null);
315
451
  _$effect((_p$) => {
316
452
  var _v$ = props.theme.text, _v$2 = TextAttributes.BOLD, _v$3 = props.theme.success, _v$4 = props.theme.text, _v$5 = props.theme.textMuted, _v$6 = props.theme.success, _v$7 = props.theme.text, _v$8 = props.theme.textMuted, _v$9 = props.theme.success, _v$0 = props.theme.text, _v$1 = props.theme.textMuted, _v$10 = props.theme.success, _v$11 = props.theme.text, _v$12 = props.theme.textMuted, _v$13 = props.theme.success, _v$14 = props.theme.text, _v$15 = props.theme.textMuted;
317
453
  _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "opencode-system-metrics-tui",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OpenCode TUI sidebar plugin for live system metrics",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
8
+ ".": {
9
+ "types": "./dist/tui.d.ts",
10
+ "import": "./dist/tui.js"
11
+ },
8
12
  "./tui": {
9
13
  "types": "./dist/tui.d.ts",
10
14
  "import": "./dist/tui.js"
@@ -17,9 +21,9 @@
17
21
  ],
18
22
  "scripts": {
19
23
  "build": "bun scripts/build.ts && tsc --emitDeclarationOnly --declaration --outDir dist",
20
- "test": "tsx --test test/format.test.ts",
24
+ "test": "tsx --test test/*.test.ts",
21
25
  "typecheck": "tsc --noEmit",
22
- "install-font": "tsx scripts/install-hack-font.ts"
26
+ "install-font": "node dist/install-hack-font.js"
23
27
  },
24
28
  "engines": {
25
29
  "node": ">=18"
package/scripts/build.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import solidPlugin from "@opentui/solid/bun-plugin"
2
2
 
3
3
  const result = await Bun.build({
4
- entrypoints: ["src/tui.tsx"],
4
+ entrypoints: ["src/tui.tsx", "scripts/install-hack-font.ts"],
5
5
  outdir: "dist",
6
- target: "bun",
6
+ target: "node",
7
7
  format: "esm",
8
8
  plugins: [solidPlugin],
9
9
  external: ["@opencode-ai/plugin/tui", "@opentui/core", "@opentui/solid", "solid-js", "systeminformation"],
10
- naming: "tui.js",
10
+ naming: "[name].js",
11
11
  })
12
12
 
13
13
  if (!result.success) {