pi-sdk-web 0.5.1 → 0.5.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.
@@ -200,54 +200,11 @@ export class WebUIContext {
200
200
  setHeader() { }
201
201
  custom() {
202
202
  // Pi's custom() shows an extension-drawn TUI component. Web has no TUI
203
- // renderer, so the component can't be *displayed* - but the factory is
204
- // still invoked with no-op stubs so the extension's own logic runs
205
- // (e.g. /usage starts its data collection inside the factory), then we
206
- // settle immediately: display-only panels (user-driven close) would
207
- // otherwise hang the command forever, and data-producing panels
208
- // (loader -> done(value)) have their result read from the extension's
209
- // own cache file afterwards when needed (see usage-render.ts). So
210
- // custom() never blocks the command.
211
- // Extensions that branch on hasUI before custom (magic-context
212
- // ctx-status) are routed to their text fallback by executeCommand
213
- // (hasUI:false) and never reach this.
214
- try {
215
- // Arguments are (factory, options) at runtime; the declared
216
- // signature stays interface-compatible, read them dynamically.
217
- const args = arguments;
218
- const factory = args[0];
219
- const options = args[1];
220
- if (typeof factory === "function") {
221
- // No-op TUI stub: enough surface for factories that need a render
222
- // handle; rendering itself is never performed on Web.
223
- const stubTui = {
224
- requestRender: () => { },
225
- invalidate: () => { },
226
- setFocus: () => { },
227
- getWidth: () => 100,
228
- };
229
- // Theme stub: color helpers degrade to plain text.
230
- const stubTheme = {
231
- fg: (_k, s) => s,
232
- bold: (s) => s,
233
- dim: (s) => s,
234
- get theme() {
235
- return undefined;
236
- },
237
- };
238
- const component = factory(stubTui, stubTheme, {}, () => { });
239
- // Component built and its logic ran (data collection started);
240
- // NOT disposed - async work inside may still be running and own
241
- // its resources. settle immediately.
242
- void component;
243
- }
244
- if (options && typeof options.onHandle === "function") {
245
- options.onHandle({ setHidden: () => { }, focus: () => { } });
246
- }
247
- }
248
- catch {
249
- // factory threw - ignore, still settle
250
- }
203
+ // renderer, so the component can't be displayed - same headless stub as
204
+ // Pi's RPC mode (rpc-mode.ts: "Custom UI not supported in RPC mode"):
205
+ // settle immediately so commands awaiting the panel don't hang. This is
206
+ // now purely internal: /usage (which used to collect data inside the
207
+ // factory) collects natively from Pi session files instead.
251
208
  return Promise.resolve(undefined);
252
209
  }
253
210
  pasteToEditor() { }
@@ -14,7 +14,7 @@
14
14
  * (5 time tabs x provider/model x metrics + insights + global hourly
15
15
  * series + tab windows); the frontend renders.
16
16
  */
17
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
17
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { homedir } from "node:os";
20
20
  const SESSIONS_DIRS = [
@@ -193,111 +193,6 @@ export function collectUsage() {
193
193
  }
194
194
  return out;
195
195
  }
196
- /** A coarse freshness stamp: sum of session-file mtimes, for cheap invalidation. */
197
- export function sessionsStamp() {
198
- const root = sessionsDir();
199
- if (!root)
200
- return 0;
201
- const files = [];
202
- collectSessionFiles(root, files);
203
- let sum = 0;
204
- for (const f of files) {
205
- try {
206
- sum += statSync(f).mtimeMs;
207
- }
208
- catch {
209
- // ignore
210
- }
211
- }
212
- return sum;
213
- }
214
- function startOfDay(ts) {
215
- const d = new Date(ts);
216
- d.setHours(0, 0, 0, 0);
217
- return d.getTime();
218
- }
219
- function startOfWeek(ts) {
220
- const d = new Date(ts);
221
- const day = d.getDay() === 0 ? 6 : d.getDay() - 1;
222
- d.setHours(0, 0, 0, 0);
223
- d.setDate(d.getDate() - day);
224
- return d.getTime();
225
- }
226
- function emptyTotals() {
227
- return { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, messages: 0, sessions: new Set() };
228
- }
229
- function fmt(n) {
230
- return n >= 1e9
231
- ? `${(n / 1e9).toFixed(2)}B`
232
- : n >= 1e6
233
- ? `${(n / 1e6).toFixed(2)}M`
234
- : n >= 1e3
235
- ? `${(n / 1e3).toFixed(1)}K`
236
- : `${Math.round(n)}`;
237
- }
238
- function fmtCost(n) {
239
- return `$${n.toFixed(n >= 1 ? 2 : 4)}`;
240
- }
241
- /** Render a Markdown usage summary (today / this week / all time). */
242
- export function renderUsageSummary() {
243
- const files = collectUsage();
244
- if (files.length === 0) {
245
- return "## Usage\n\nNo usage data found yet.";
246
- }
247
- const now = Date.now();
248
- const todayStart = startOfDay(now);
249
- const weekStart = startOfWeek(now);
250
- const today = emptyTotals();
251
- const week = emptyTotals();
252
- const all = emptyTotals();
253
- const byModel = new Map();
254
- for (const file of files) {
255
- for (const m of file.messages) {
256
- const t = m.timestamp;
257
- const targets = [all];
258
- if (t >= todayStart)
259
- targets.push(today);
260
- if (t >= weekStart)
261
- targets.push(week);
262
- for (const target of targets) {
263
- target.cost += m.cost;
264
- target.input += m.input;
265
- target.output += m.output;
266
- target.cacheRead += m.cacheRead;
267
- target.cacheWrite += m.cacheWrite;
268
- target.messages += 1;
269
- target.sessions.add(file.sessionId);
270
- }
271
- const key = `${m.provider}/${m.model}`;
272
- const byModelEntry = byModel.get(key) ?? emptyTotals();
273
- byModelEntry.cost += m.cost;
274
- byModelEntry.input += m.input;
275
- byModelEntry.output += m.output;
276
- byModelEntry.cacheRead += m.cacheRead;
277
- byModelEntry.cacheWrite += m.cacheWrite;
278
- byModelEntry.messages += 1;
279
- byModel.set(key, byModelEntry);
280
- }
281
- }
282
- const row = (label, t) => `| ${label} | ${fmt(t.input)} | ${fmt(t.output)} | ${fmt(t.cacheRead)} | ${fmt(t.cacheWrite)} | $${t.cost.toFixed(4)} | ${t.messages} | ${t.sessions.size} |`;
283
- const lines = [
284
- "## Usage",
285
- "",
286
- "| Period | Input | Output | Cache R | Cache W | Cost | Msgs | Sessions |",
287
- "| --- | --- | --- | --- | --- | --- | --- | --- |",
288
- row("Today", today),
289
- row("This week", week),
290
- row("All time", all),
291
- ];
292
- if (byModel.size > 0) {
293
- const sorted = [...byModel.entries()].sort((a, b) => b[1].cost - a[1].cost).slice(0, 10);
294
- lines.push("", "### By model (all time)", "", "| Model | Input | Output | Cost | Msgs |", "| --- | --- | --- | --- | --- |");
295
- for (const [key, t] of sorted) {
296
- lines.push(`| ${key} | ${fmt(t.input)} | ${fmt(t.output)} | $${t.cost.toFixed(4)} | ${t.messages} |`);
297
- }
298
- }
299
- return lines.join("\n");
300
- }
301
196
  const TAB_KEYS = ["today", "thisWeek", "lastWeek", "last30Days", "allTime"];
302
197
  function emptyTokens() {
303
198
  return { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
@@ -436,7 +331,7 @@ export function buildUsageData(sessionId) {
436
331
  }
437
332
  }
438
333
  }
439
- const payload = { tabs: {}, hourly: [], tabWindow: {}, collectedAt: sessionsStamp() };
334
+ const payload = { tabs: {}, hourly: [], tabWindow: {}, collectedAt: Date.now() };
440
335
  // Graph x-axis windows per tab: [start, end]. today: midnight->now;
441
336
  // thisWeek: monday->now; lastWeek: monday->next Monday; last30Days:
442
337
  // start->now; allTime: 0 -> now (frontend clips to first data).
@@ -494,7 +389,9 @@ export function buildUsageData(sessionId) {
494
389
  insights.push({
495
390
  kind: "alarm",
496
391
  stat: formatUsageCost(topModel.cost),
497
- headline: `${topModel.name} is the costliest model (${((topModel.cost / totalCost) * 100).toFixed(0)}% of total)`,
392
+ // Model names can repeat across providers (e.g. deepseek-v4-
393
+ // flash on several providers) - show provider/model.
394
+ headline: `${top.name}/${topModel.name} is the costliest model (${((topModel.cost / totalCost) * 100).toFixed(0)}% of total)`,
498
395
  advice: "Check whether its output quality justifies the price.",
499
396
  });
500
397
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sdk-web",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Browser Web access for Pi (AI coding agent) via the Pi SDK - standalone module, zero modification to Pi itself",
5
5
  "type": "module",
6
6
  "bin": {