@vtstech/pi-status 1.1.2 → 1.1.4

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/status.js +130 -189
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtstech/pi-status",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "System monitor / status bar extension for Pi Coding Agent",
5
5
  "main": "status.js",
6
6
  "keywords": ["pi-extensions"],
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/VTSTech/pi-coding-agent"
15
15
  },
16
16
  "dependencies": {
17
- "@vtstech/pi-shared": "1.1.2"
17
+ "@vtstech/pi-shared": "1.1.4"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@mariozechner/pi-coding-agent": ">=0.66"
package/status.js CHANGED
@@ -1,33 +1,34 @@
1
1
  // .build-npm/status/status.temp.ts
2
+ import * as fs from "node:fs";
3
+ import { execSync } from "node:child_process";
2
4
  import os from "node:os";
3
- import { execSync as gitExecSync } from "node:child_process";
4
5
  import { getOllamaBaseUrl, fetchModelContextLength, readModelsJson } from "@vtstech/pi-shared/ollama";
5
6
  import { fmtBytes, fmtDur } from "@vtstech/pi-shared/format";
7
+ import { debugLog } from "@vtstech/pi-shared/debug";
8
+ var STATUS_UPDATE_INTERVAL_MS = 5e3;
9
+ var TOOL_TIMER_INTERVAL_MS = 1e3;
6
10
  function status_temp_default(pi) {
7
11
  let lastResponseTime = null;
8
12
  let agentStartTime = null;
9
13
  let updateInterval = null;
14
+ let toolTimerInterval = null;
10
15
  let currentCtx = null;
11
16
  let ctxUi = null;
17
+ let ctxTheme = null;
12
18
  let prevCpuInfo = getCpuSnapshot();
13
19
  let lastPayload = null;
14
- let tuiRef = null;
15
- let gitBranchCache = "";
16
20
  let cpuUsage = 0;
17
21
  let memUsed = 0;
18
22
  let memTotal = 0;
19
23
  let swapUsed = 0;
20
24
  let swapTotal = 0;
21
25
  let hasSwap = false;
22
- let ollamaLoaded = "";
23
26
  let footerModel = "";
24
- let footerThinking = "";
25
- let footerCtxPct = "";
26
27
  let footerNativeCtx = "";
27
28
  let nativeCtxModel = "";
28
29
  let isLocalProvider = true;
29
- let lastUpstream = 0;
30
- let lastDownstream = 0;
30
+ let versionsText = "";
31
+ let cachedPromptText = null;
31
32
  let securityFlashTool = "";
32
33
  let securityFlashUntil = 0;
33
34
  let activeTool = "";
@@ -65,18 +66,20 @@ function status_temp_default(pi) {
65
66
  return { used, total };
66
67
  }
67
68
  function getSwap() {
69
+ if (process.platform !== "linux") {
70
+ debugLog("status", "swap detection skipped: not a Linux platform");
71
+ return null;
72
+ }
68
73
  try {
69
- const out = gitExecSync("cat /proc/meminfo", { encoding: "utf-8", timeout: 3e3 });
74
+ const out = fs.readFileSync("/proc/meminfo", "utf-8");
70
75
  const swapTotal2 = Number(out.match(/SwapTotal:\s+(\d+)/)?.[1]) * 1024;
71
76
  const swapFree = Number(out.match(/SwapFree:\s+(\d+)/)?.[1]) * 1024;
72
77
  if (swapTotal2 > 0) return { used: swapTotal2 - swapFree, total: swapTotal2 };
73
- } catch {
78
+ } catch (err) {
79
+ debugLog("status", "failed to read /proc/meminfo", err);
74
80
  }
75
81
  return null;
76
82
  }
77
- let ollamaLoadedCache = "";
78
- let ollamaLoadedLastCheck = 0;
79
- const OLLAMA_LOADED_INTERVAL = 15e3;
80
83
  function detectLocalProvider(modelsJson) {
81
84
  const isLocalUrl = (url) => url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0");
82
85
  try {
@@ -116,68 +119,70 @@ function status_temp_default(pi) {
116
119
  }
117
120
  return footerNativeCtx;
118
121
  }
119
- async function fetchOllamaLoadedModel() {
120
- try {
121
- const ollamaBase = getOllamaBaseUrl();
122
- const res = await fetch(`${ollamaBase}/api/ps`, {
123
- signal: AbortSignal.timeout(5e3)
124
- });
125
- if (!res.ok) return "";
126
- const data = await res.json();
127
- const models = data?.models || [];
128
- if (Array.isArray(models) && models.length > 0) {
129
- return models[0].name || models[0].model || "";
130
- }
131
- } catch {
132
- }
133
- return "";
134
- }
135
- function getOllamaLoadedModel() {
136
- const now = Date.now();
137
- if (now - ollamaLoadedLastCheck < OLLAMA_LOADED_INTERVAL) return ollamaLoadedCache;
138
- ollamaLoadedLastCheck = now;
139
- fetchOllamaLoadedModel().then((loaded) => {
140
- ollamaLoadedCache = loaded;
141
- }).catch(() => {
142
- ollamaLoadedCache = "";
143
- });
144
- return ollamaLoadedCache;
145
- }
146
122
  function extractParams(payload) {
147
123
  const params = [];
148
124
  if (payload.temperature !== void 0) params.push(`temp:${payload.temperature}`);
149
125
  if (payload.top_p !== void 0) params.push(`top_p:${payload.top_p}`);
150
126
  if (payload.top_k !== void 0) params.push(`top_k:${payload.top_k}`);
151
- if (payload.max_completion_tokens !== void 0) params.push(`max:${payload.max_completion_tokens}`);
152
- else if (payload.max_tokens !== void 0) params.push(`max:${payload.max_tokens}`);
153
127
  if (payload.num_predict !== void 0) params.push(`predict:${payload.num_predict}`);
154
128
  if (payload.num_ctx !== void 0) params.push(`ctx:${payload.num_ctx}`);
155
129
  if (payload.reasoning_effort !== void 0) params.push(`think:${payload.reasoning_effort}`);
156
130
  return params;
157
131
  }
158
- function fmtTk(n) {
159
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
160
- return String(n);
161
- }
162
- function getPwd() {
163
- const cwd = process.cwd();
164
- if (cwd.startsWith(os.homedir())) return "~" + cwd.slice(os.homedir().length);
165
- return cwd;
166
- }
167
- function getGitBranch() {
168
- if (gitBranchCache) return gitBranchCache;
169
- try {
170
- const branch = gitExecSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
171
- encoding: "utf-8",
172
- timeout: 3e3
173
- }).trim();
174
- if (branch) gitBranchCache = branch;
175
- } catch {
132
+ function flushStatus() {
133
+ if (!ctxUi) return;
134
+ const theme = ctxTheme;
135
+ const dim2 = (s) => theme?.fg?.("dim", s) ?? s;
136
+ const green2 = (s) => theme?.fg?.("success", s) ?? s;
137
+ ctxUi.setStatus("status-cpu", isLocalProvider ? `${dim2("CPU")} ${green2(cpuUsage.toFixed(0) + "%")}` : void 0);
138
+ ctxUi.setStatus("status-ram", isLocalProvider ? `${dim2("RAM")} ${green2(fmtBytes(memUsed) + "/" + fmtBytes(memTotal))}` : void 0);
139
+ ctxUi.setStatus(
140
+ "status-swap",
141
+ isLocalProvider && hasSwap && swapUsed > 0 ? `${dim2("Swap")} ${green2(fmtBytes(swapUsed) + "/" + fmtBytes(swapTotal))}` : void 0
142
+ );
143
+ ctxUi.setStatus(
144
+ "status-native-ctx",
145
+ footerNativeCtx ? `${dim2("CtxMax:")}${green2(footerNativeCtx)}` : void 0
146
+ );
147
+ if (lastPayload) {
148
+ const rawMax = lastPayload.max_completion_tokens ?? lastPayload.max_tokens;
149
+ if (rawMax !== void 0) {
150
+ const formatted = rawMax >= 1e3 ? `${(rawMax / 1e3).toFixed(rawMax % 1e3 === 0 ? 0 : 1)}k` : String(rawMax);
151
+ ctxUi.setStatus("status-resp-max", `${dim2("RespMax:")}${green2(formatted)}`);
152
+ } else {
153
+ ctxUi.setStatus("status-resp-max", void 0);
154
+ }
155
+ } else {
156
+ ctxUi.setStatus("status-resp-max", void 0);
157
+ }
158
+ ctxUi.setStatus(
159
+ "status-resp",
160
+ lastResponseTime !== null ? `${dim2("Resp")} ${green2(fmtDur(lastResponseTime))}` : void 0
161
+ );
162
+ if (lastPayload) {
163
+ const params = extractParams(lastPayload);
164
+ ctxUi.setStatus("status-params", params.length > 0 ? dim2(params.join(" ")) : void 0);
165
+ } else {
166
+ ctxUi.setStatus("status-params", void 0);
167
+ }
168
+ const now = Date.now();
169
+ if (securityFlashTool && now < securityFlashUntil) {
170
+ ctxUi.setStatus("status-sec", `${dim2("SEC:")}${green2(String(blockedCount))} ${dim2("(blocked: " + securityFlashTool + ")")}`);
171
+ } else if (blockedCount > 0) {
172
+ ctxUi.setStatus("status-sec", `${dim2("SEC:")}${green2(String(blockedCount))}`);
173
+ } else {
174
+ ctxUi.setStatus("status-sec", void 0);
175
+ }
176
+ if (activeTool && activeToolStart > 0) {
177
+ const elapsed = performance.now() - activeToolStart;
178
+ ctxUi.setStatus("status-tool", `${green2(">")} ${dim2(activeTool + ":")} ${green2(fmtDur(elapsed))}`);
179
+ } else {
180
+ ctxUi.setStatus("status-tool", void 0);
181
+ }
182
+ ctxUi.setStatus("system-prompt", cachedPromptText ?? void 0);
183
+ if (versionsText) {
184
+ ctxUi.setStatus("status-versions", dim2(versionsText));
176
185
  }
177
- return gitBranchCache;
178
- }
179
- function incrementBlockedCount() {
180
- blockedCount++;
181
186
  }
182
187
  function updateMetrics() {
183
188
  cpuUsage = getCpuUsage();
@@ -192,154 +197,79 @@ function status_temp_default(pi) {
192
197
  } else {
193
198
  hasSwap = false;
194
199
  }
195
- ollamaLoaded = getOllamaLoadedModel();
196
200
  const modelsJson = readModelsJson();
197
201
  isLocalProvider = modelsJson ? detectLocalProvider(modelsJson) : false;
198
202
  if (currentCtx) {
199
203
  footerModel = currentCtx.model?.id || "";
200
- footerThinking = pi.getThinkingLevel?.() ?? "";
201
- const usage = currentCtx.getContextUsage?.();
202
- if (usage && usage.contextWindow > 0) {
203
- const pctVal = (usage.tokens / usage.contextWindow * 100).toFixed(1);
204
- footerCtxPct = `${pctVal}%/${(usage.contextWindow / 1e3).toFixed(0)}k`;
205
- } else {
206
- footerCtxPct = "";
207
- }
208
204
  const modelId = currentCtx.model?.id || "";
209
- if (modelId && isLocalProvider) {
205
+ if (modelId) {
210
206
  getNativeModelCtx(modelId);
211
207
  }
212
208
  }
209
+ flushStatus();
213
210
  }
214
211
  pi.on("session_start", async (_event, ctx) => {
215
212
  currentCtx = ctx;
216
213
  ctxUi = ctx.ui;
214
+ ctxTheme = ctx.ui.theme;
217
215
  prevCpuInfo = getCpuSnapshot();
216
+ try {
217
+ const out = execSync("pi -v 2>&1", { encoding: "utf-8", timeout: 5e3 }).trim();
218
+ if (out) versionsText = `pi:${out}`;
219
+ } catch {
220
+ }
218
221
  updateMetrics();
219
- ctx.ui.setFooter((tui, theme, footerData) => {
220
- tuiRef = tui;
221
- const dim = (s) => theme?.fg?.("dim", s) ?? s;
222
- const red = (s) => theme?.fg?.("error", s) ?? s;
223
- const yellow = (s) => theme?.fg?.("yellow", s) ?? s;
224
- const sep = dim(" \xB7 ");
225
- const truncateLine = (line, maxW) => {
226
- const ellipsis = dim("...");
227
- const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
228
- if (visible.length > maxW) {
229
- let vis = 0, cut = 0;
230
- for (let i = 0; i < line.length && vis < maxW - 3; i++) {
231
- if (line[i] === "\x1B") {
232
- while (i < line.length && line[i] !== "m") i++;
233
- } else {
234
- vis++;
235
- }
236
- cut = i + 1;
237
- }
238
- return line.slice(0, cut) + ellipsis;
239
- }
240
- return line;
241
- };
242
- return {
243
- render(width) {
244
- const lines = [];
245
- let branch = "";
246
- try {
247
- branch = footerData?.getGitBranch?.() || "";
248
- } catch {
249
- }
250
- if (!branch) branch = getGitBranch();
251
- const line1Parts = [];
252
- if (footerModel) line1Parts.push(`conf:${footerModel}`);
253
- line1Parts.push(getPwd());
254
- if (footerThinking && footerThinking !== "off") line1Parts.push(dim(footerThinking));
255
- if (isLocalProvider) {
256
- line1Parts.push(dim(`CPU ${cpuUsage.toFixed(0)}%`));
257
- }
258
- let line1 = truncateLine(line1Parts.join(sep), width);
259
- lines.push(line1);
260
- const line2Parts = [];
261
- if (ollamaLoaded) line2Parts.push(`load:${ollamaLoaded}`);
262
- if (footerNativeCtx) line2Parts.push(`M:${footerNativeCtx}`);
263
- if (footerCtxPct) line2Parts.push(`S:${footerCtxPct}`);
264
- if (isLocalProvider) {
265
- line2Parts.push(`RAM ${fmtBytes(memUsed)}/${fmtBytes(memTotal)}`);
266
- if (hasSwap && swapUsed > 0) {
267
- line2Parts.push(`Swap ${fmtBytes(swapUsed)}/${fmtBytes(swapTotal)}`);
268
- }
269
- }
270
- if (lastUpstream > 0 || lastDownstream > 0) {
271
- line2Parts.push(dim(`\u2191${fmtTk(lastUpstream)} \u2193${fmtTk(lastDownstream)}`));
272
- }
273
- if (lastResponseTime !== null) line2Parts.push(`Resp ${fmtDur(lastResponseTime)}`);
274
- if (lastPayload) {
275
- const params = extractParams(lastPayload);
276
- if (params.length > 0) line2Parts.push(...params.map((p) => dim(p)));
277
- }
278
- const now = Date.now();
279
- if (securityFlashTool && now < securityFlashUntil) {
280
- line2Parts.push(red(`BLOCKED:${securityFlashTool}`));
281
- }
282
- if (blockedCount > 0) {
283
- line2Parts.push(red(`SEC:${blockedCount}`));
284
- }
285
- let line2 = truncateLine(line2Parts.join(sep), width);
286
- if (line2) lines.push(line2);
287
- if (activeTool && activeToolStart > 0) {
288
- const elapsed = performance.now() - activeToolStart;
289
- lines.push(`${yellow("\u23F3")} ${activeTool}: ${fmtDur(elapsed)}`);
290
- }
291
- return lines;
292
- },
293
- invalidate() {
294
- },
295
- dispose() {
296
- }
297
- };
298
- });
299
222
  if (updateInterval) clearInterval(updateInterval);
300
- updateInterval = setInterval(() => {
301
- updateMetrics();
302
- if (tuiRef) tuiRef.requestRender();
303
- }, 3e3);
223
+ updateInterval = setInterval(updateMetrics, STATUS_UPDATE_INTERVAL_MS);
304
224
  });
305
- pi.on("session_shutdown", async () => {
306
- if (updateInterval) clearInterval(updateInterval);
307
- updateInterval = null;
308
- tuiRef = null;
309
- if (ctxUi) {
310
- ctxUi.setFooter(void 0);
311
- ctxUi = null;
225
+ pi.on("session_shutdown", async (_event, ctx) => {
226
+ if (updateInterval) {
227
+ clearInterval(updateInterval);
228
+ updateInterval = null;
312
229
  }
230
+ if (toolTimerInterval) {
231
+ clearInterval(toolTimerInterval);
232
+ toolTimerInterval = null;
233
+ }
234
+ ctxUi = null;
313
235
  currentCtx = null;
236
+ const ui = ctx?.ui;
237
+ if (ui) {
238
+ ui.setStatus("status-cpu", void 0);
239
+ ui.setStatus("status-ram", void 0);
240
+ ui.setStatus("status-swap", void 0);
241
+ ui.setStatus("status-native-ctx", void 0);
242
+ ui.setStatus("status-resp", void 0);
243
+ ui.setStatus("status-resp-max", void 0);
244
+ ui.setStatus("status-params", void 0);
245
+ ui.setStatus("system-prompt", void 0);
246
+ ui.setStatus("status-sec", void 0);
247
+ ui.setStatus("status-tool", void 0);
248
+ ui.setStatus("status-versions", void 0);
249
+ }
314
250
  securityFlashTool = "";
315
251
  securityFlashUntil = 0;
316
252
  activeTool = "";
317
253
  activeToolStart = 0;
318
254
  blockedCount = 0;
319
- lastUpstream = 0;
320
- lastDownstream = 0;
255
+ lastResponseTime = null;
256
+ lastPayload = null;
257
+ versionsText = "";
258
+ cachedPromptText = null;
321
259
  });
322
260
  pi.on("before_provider_request", (event) => {
323
261
  lastPayload = event.payload;
324
262
  });
325
- function captureUsage(event) {
326
- if (event?.message?.role !== "assistant") return;
327
- const usage = event?.message?.usage ?? // normalised Pi usage
328
- event?.usage ?? // alternative path
329
- null;
330
- if (!usage) return;
331
- const inp = usage.input ?? usage.promptTokens ?? usage.prompt_tokens;
332
- const out = usage.output ?? usage.completionTokens ?? usage.completion_tokens;
333
- if (inp != null) lastUpstream = inp;
334
- if (out != null) lastDownstream = out;
335
- if (tuiRef) tuiRef.requestRender();
336
- }
337
- pi.on("message_end", captureUsage);
338
- pi.on("turn_end", captureUsage);
339
- pi.on("agent_start", async () => {
263
+ pi.on("agent_start", async (_event, ctx) => {
340
264
  agentStartTime = performance.now();
341
- lastUpstream = 0;
342
- lastDownstream = 0;
265
+ try {
266
+ const prompt = ctx.getSystemPrompt();
267
+ const chr = prompt.length;
268
+ const tok = prompt.split(/\s+/).filter(Boolean).length;
269
+ cachedPromptText = `${dim("Prompt:")} ${green(`${chr} chr ${tok} tok`)}`;
270
+ } catch {
271
+ }
272
+ flushStatus();
343
273
  });
344
274
  pi.on("agent_end", async () => {
345
275
  if (agentStartTime !== null) {
@@ -348,29 +278,40 @@ function status_temp_default(pi) {
348
278
  }
349
279
  activeTool = "";
350
280
  activeToolStart = 0;
281
+ stopToolTimer();
351
282
  updateMetrics();
352
- if (tuiRef) tuiRef.requestRender();
353
283
  });
284
+ function startToolTimer() {
285
+ if (toolTimerInterval) return;
286
+ toolTimerInterval = setInterval(flushStatus, TOOL_TIMER_INTERVAL_MS);
287
+ }
288
+ function stopToolTimer() {
289
+ if (toolTimerInterval) {
290
+ clearInterval(toolTimerInterval);
291
+ toolTimerInterval = null;
292
+ }
293
+ }
354
294
  pi.on("tool_call", (event) => {
355
295
  if (!event) return;
356
296
  const isBlocked = event.blocked === true || event.blocked === "true" || event.result?.blocked === true || event.error?.includes("blocked");
357
297
  if (isBlocked) {
358
298
  securityFlashTool = event.tool ?? event.name ?? "unknown";
359
299
  securityFlashUntil = Date.now() + 3e3;
360
- incrementBlockedCount();
361
- if (tuiRef) tuiRef.requestRender();
300
+ blockedCount++;
301
+ flushStatus();
362
302
  }
363
303
  });
364
304
  pi.on("tool_execution_start", (event) => {
365
305
  if (!event) return;
366
306
  activeTool = event.tool ?? event.name ?? "tool";
367
307
  activeToolStart = performance.now();
368
- if (tuiRef) tuiRef.requestRender();
308
+ startToolTimer();
369
309
  });
370
310
  pi.on("tool_execution_end", () => {
371
311
  activeTool = "";
372
312
  activeToolStart = 0;
373
- if (tuiRef) tuiRef.requestRender();
313
+ stopToolTimer();
314
+ flushStatus();
374
315
  });
375
316
  }
376
317
  export {