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.
- package/dist/ui-context.js +5 -48
- package/dist/usage-render.js +5 -108
- package/package.json +1 -1
package/dist/ui-context.js
CHANGED
|
@@ -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
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
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() { }
|
package/dist/usage-render.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
}
|