pi-sdk-web 0.5.0 → 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/server.js +51 -25
- package/dist/static/app.js +28 -4
- package/dist/ui-context.js +5 -48
- package/dist/usage-render.js +182 -166
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -359,6 +359,13 @@ export class PiWebServer {
|
|
|
359
359
|
for (const s of this.session.resourceLoader.getSkills().skills) {
|
|
360
360
|
commands.push({ name: `skill:${s.name}`, description: s.description, source: "skill", sourceInfo: s.sourceInfo });
|
|
361
361
|
}
|
|
362
|
+
// pi-sdk-web built-ins: commands provided by this package itself
|
|
363
|
+
// (not tied to any extension), e.g. /usage which collects Pi session
|
|
364
|
+
// usage natively (usage-render.ts) instead of running the extension's
|
|
365
|
+
// command. Source "maxdai" = the package author's npm identity.
|
|
366
|
+
if (!commands.some((c) => c.name === "usage")) {
|
|
367
|
+
commands.push({ name: "usage", description: "Show usage statistics (tokens/cost, all sessions)", source: "maxdai", sourceInfo: { path: "pi-sdk-web", source: "maxdai", scope: "global", origin: "cli" } });
|
|
368
|
+
}
|
|
362
369
|
return commands;
|
|
363
370
|
}
|
|
364
371
|
catch {
|
|
@@ -622,6 +629,13 @@ export class PiWebServer {
|
|
|
622
629
|
await this.executeCommand(name, args);
|
|
623
630
|
break;
|
|
624
631
|
}
|
|
632
|
+
case "usage": {
|
|
633
|
+
// /usage panel scope switch (session | all) - re-render with the
|
|
634
|
+
// chosen data range.
|
|
635
|
+
const scope = data.scope === "all" ? "all" : "session";
|
|
636
|
+
await this.handleUsageCommand(scope);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
625
639
|
case "session":
|
|
626
640
|
// Read-only session info (TUI /session equivalent)
|
|
627
641
|
this.broadcastSessionInfo();
|
|
@@ -752,6 +766,13 @@ export class PiWebServer {
|
|
|
752
766
|
* modal (mirroring the TUI dialog behavior).
|
|
753
767
|
*/
|
|
754
768
|
async executeCommand(name, args) {
|
|
769
|
+
// /usage is now a pi-web-native feature: collect usage directly from
|
|
770
|
+
// Pi's session files (usage-render.ts) and show the panel - the
|
|
771
|
+
// pi-usage-extension is no longer required (it reads the same files).
|
|
772
|
+
if (name === "usage") {
|
|
773
|
+
await this.handleUsageCommand("session");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
755
776
|
const cmd = this.session.extensionRunner.getCommand(name);
|
|
756
777
|
if (!cmd)
|
|
757
778
|
throw new Error(`Unknown command: ${name}`);
|
|
@@ -817,34 +838,39 @@ export class PiWebServer {
|
|
|
817
838
|
notifyType: "info",
|
|
818
839
|
});
|
|
819
840
|
}
|
|
820
|
-
//
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
|
-
this.broadcast({
|
|
834
|
-
type: "extension_ui_request",
|
|
835
|
-
id: crypto.randomUUID(),
|
|
836
|
-
method: "notify",
|
|
837
|
-
title: "/usage",
|
|
838
|
-
message: "No usage data found. Run `/usage` in the TUI first to build the usage cache.",
|
|
839
|
-
notifyType: "info",
|
|
840
|
-
});
|
|
841
|
-
}
|
|
841
|
+
// --- /usage (pi-web-native) handled early in executeCommand ---
|
|
842
|
+
}
|
|
843
|
+
/** Collect usage from Pi session files and broadcast the panel payload.
|
|
844
|
+
* scope "session" = current session only; "all" = every session. */
|
|
845
|
+
async handleUsageCommand(scope = "session") {
|
|
846
|
+
try {
|
|
847
|
+
const { buildUsageData } = await import("./usage-render.js");
|
|
848
|
+
const sessionId = scope === "session" ? String(this.session.sessionId ?? "") : undefined;
|
|
849
|
+
const data = buildUsageData(sessionId);
|
|
850
|
+
if (data) {
|
|
851
|
+
this.broadcast({ type: "usage_data", data });
|
|
842
852
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
853
|
+
else {
|
|
854
|
+
this.broadcast({
|
|
855
|
+
type: "extension_ui_request",
|
|
856
|
+
id: crypto.randomUUID(),
|
|
857
|
+
method: "notify",
|
|
858
|
+
title: "/usage",
|
|
859
|
+
message: scope === "session" ? "No usage data for this session yet." : "No usage data found yet (no Pi session files with usage).",
|
|
860
|
+
notifyType: "info",
|
|
861
|
+
});
|
|
846
862
|
}
|
|
847
863
|
}
|
|
864
|
+
catch (e) {
|
|
865
|
+
this.broadcast({
|
|
866
|
+
type: "extension_ui_request",
|
|
867
|
+
id: crypto.randomUUID(),
|
|
868
|
+
method: "notify",
|
|
869
|
+
title: "/usage",
|
|
870
|
+
message: `Usage collection failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
871
|
+
notifyType: "error",
|
|
872
|
+
});
|
|
873
|
+
}
|
|
848
874
|
}
|
|
849
875
|
buildCommandContext(hasUI) {
|
|
850
876
|
const sm = this.session.sessionManager;
|
package/dist/static/app.js
CHANGED
|
@@ -1972,10 +1972,15 @@ class PiWebClient {
|
|
|
1972
1972
|
|
|
1973
1973
|
renderUsagePanel(data) {
|
|
1974
1974
|
if (!data || !data.tabs) return;
|
|
1975
|
+
// Preserve the user's current choices (tab/view/scope/expanded) across
|
|
1976
|
+
// data refresh (e.g. scope switch Session|All must not reset them);
|
|
1977
|
+
// defaults are set only on first open. usageExpanded must exist for the
|
|
1978
|
+
// tab/provider click handlers (created once, kept afterwards).
|
|
1979
|
+
if (!this.usageTab) this.usageTab = 'thisWeek';
|
|
1980
|
+
if (!this.usageView) this.usageView = 'table';
|
|
1981
|
+
if (!this.usageScope) this.usageScope = 'session';
|
|
1982
|
+
if (!this.usageExpanded) this.usageExpanded = new Set();
|
|
1975
1983
|
this.usageData = data;
|
|
1976
|
-
this.usageTab = 'thisWeek';
|
|
1977
|
-
this.usageView = 'table';
|
|
1978
|
-
this.usageExpanded = new Set();
|
|
1979
1984
|
const overlay = document.getElementById('usage-overlay');
|
|
1980
1985
|
if (!overlay) return;
|
|
1981
1986
|
overlay.style.display = 'flex';
|
|
@@ -1983,6 +1988,14 @@ class PiWebClient {
|
|
|
1983
1988
|
this.renderUsageBody();
|
|
1984
1989
|
}
|
|
1985
1990
|
|
|
1991
|
+
// Switch the usage scope (session | all) - re-fetch from the server.
|
|
1992
|
+
setUsageScope(scope) {
|
|
1993
|
+
if (this.usageScope === scope) return;
|
|
1994
|
+
this.usageScope = scope;
|
|
1995
|
+
this.renderUsageTabs();
|
|
1996
|
+
this.send({ type: 'usage', scope });
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1986
1999
|
closeUsagePanel() {
|
|
1987
2000
|
const overlay = document.getElementById('usage-overlay');
|
|
1988
2001
|
if (overlay) overlay.style.display = 'none';
|
|
@@ -2011,7 +2024,7 @@ class PiWebClient {
|
|
|
2011
2024
|
});
|
|
2012
2025
|
tabsEl.appendChild(tab);
|
|
2013
2026
|
}
|
|
2014
|
-
// View switcher: Table / Insights / Graph
|
|
2027
|
+
// View switcher: Table / Insights / Graph + scope (Session | All)
|
|
2015
2028
|
const viewsEl = document.getElementById('usage-views');
|
|
2016
2029
|
if (viewsEl) {
|
|
2017
2030
|
viewsEl.innerHTML = '';
|
|
@@ -2026,6 +2039,17 @@ class PiWebClient {
|
|
|
2026
2039
|
});
|
|
2027
2040
|
viewsEl.appendChild(v);
|
|
2028
2041
|
}
|
|
2042
|
+
const scopeSep = document.createElement('span');
|
|
2043
|
+
scopeSep.className = 'usage-scope-sep';
|
|
2044
|
+
scopeSep.textContent = '|';
|
|
2045
|
+
viewsEl.appendChild(scopeSep);
|
|
2046
|
+
for (const [key, label] of [['session', 'Session'], ['all', 'All']]) {
|
|
2047
|
+
const s = document.createElement('span');
|
|
2048
|
+
s.className = 'usage-view' + (key === this.usageScope ? ' active' : '');
|
|
2049
|
+
s.textContent = label;
|
|
2050
|
+
s.addEventListener('click', () => this.setUsageScope(key));
|
|
2051
|
+
viewsEl.appendChild(s);
|
|
2052
|
+
}
|
|
2029
2053
|
}
|
|
2030
2054
|
}
|
|
2031
2055
|
|
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
|
@@ -1,186 +1,197 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Usage
|
|
2
|
+
* Usage statistics for the pi-web panel (standalone module).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Data is collected directly from Pi's own session files
|
|
5
|
+
* (~/.pi/agent/sessions/**\/*.jsonl): each assistant message carries
|
|
6
|
+
* provider/model/timestamp/usage{input,output,cacheRead,cacheWrite,
|
|
7
|
+
* reasoning,cost}, and auxiliary entries (compaction/branch_summary)
|
|
8
|
+
* carry usage too. The collection mirrors the semantics used by the
|
|
9
|
+
* pi-usage-extension (which reads the same session files and normalizes
|
|
10
|
+
* them to a cache) - but this module is independent: it parses the
|
|
11
|
+
* session files itself, so /usage works without the extension installed.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* message tuple: [provider, model, cost, inputTokens, outputTokens,
|
|
16
|
-
* cacheReadTokens, cacheWriteTokens, timestampMs, thinkingLevel,
|
|
17
|
-
* reasoningTokens, afterCompaction(0|1), source(0 assistant|1 auxiliary),
|
|
18
|
-
* sourceId]
|
|
13
|
+
* The module aggregates the messages into the structured UsageDataPayload
|
|
14
|
+
* (5 time tabs x provider/model x metrics + insights + global hourly
|
|
15
|
+
* series + tab windows); the frontend renders.
|
|
19
16
|
*/
|
|
20
|
-
import { existsSync,
|
|
17
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
21
18
|
import { join } from "node:path";
|
|
22
19
|
import { homedir } from "node:os";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
join(
|
|
26
|
-
join(homedir(), ".pi", "agent"),
|
|
20
|
+
const SESSIONS_DIRS = [
|
|
21
|
+
join(process.env.PI_HOME?.replace(/^~/, homedir()) ?? homedir(), ".pi", "agent", "sessions"),
|
|
22
|
+
join(homedir(), ".pi", "agent", "sessions"),
|
|
27
23
|
];
|
|
28
|
-
function
|
|
29
|
-
for (const dir of
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return p;
|
|
24
|
+
function sessionsDir() {
|
|
25
|
+
for (const dir of SESSIONS_DIRS) {
|
|
26
|
+
if (existsSync(dir))
|
|
27
|
+
return dir;
|
|
33
28
|
}
|
|
34
29
|
return null;
|
|
35
30
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!path)
|
|
40
|
-
return null;
|
|
31
|
+
/** Recursively collect all .jsonl session files under dir (sorted). */
|
|
32
|
+
function collectSessionFiles(dir, out) {
|
|
33
|
+
let entries;
|
|
41
34
|
try {
|
|
42
|
-
|
|
35
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
43
36
|
}
|
|
44
37
|
catch {
|
|
45
|
-
return
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
const p = join(dir, e.name);
|
|
42
|
+
if (e.isDirectory())
|
|
43
|
+
collectSessionFiles(p, out);
|
|
44
|
+
else if (e.isFile() && e.name.endsWith(".jsonl"))
|
|
45
|
+
out.push(p);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!path)
|
|
48
|
+
function parseUsageAmount(value) {
|
|
49
|
+
const u = value;
|
|
50
|
+
if (!u || typeof u !== "object")
|
|
52
51
|
return null;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
messages.push({
|
|
70
|
-
provider,
|
|
71
|
-
model,
|
|
72
|
-
thinkingLevel,
|
|
73
|
-
cost: Number(tuple[2]) || 0,
|
|
74
|
-
input: Number(tuple[3]) || 0,
|
|
75
|
-
output: Number(tuple[4]) || 0,
|
|
76
|
-
cacheRead: Number(tuple[5]) || 0,
|
|
77
|
-
cacheWrite: Number(tuple[6]) || 0,
|
|
78
|
-
timestamp: Number(tuple[7]) || 0,
|
|
79
|
-
reasoning: Number(tuple[9]) || 0,
|
|
80
|
-
afterCompaction: tuple[10] === 1,
|
|
81
|
-
source: tuple[11] === 1 ? "auxiliary" : "assistant",
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
files.push({
|
|
86
|
-
messages,
|
|
87
|
-
sessionId: f.sessionId ?? "",
|
|
88
|
-
cwd: f.cwd ?? "",
|
|
89
|
-
parentSession: f.parentSession ?? "",
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
return { files, names };
|
|
52
|
+
return {
|
|
53
|
+
cost: Number(u.cost?.total) || 0,
|
|
54
|
+
input: Number(u.input) || 0,
|
|
55
|
+
output: Number(u.output) || 0,
|
|
56
|
+
cacheRead: Number(u.cacheRead) || 0,
|
|
57
|
+
cacheWrite: Number(u.cacheWrite) || 0,
|
|
58
|
+
reasoning: Number(u.reasoning) || 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function tsOf(messageTimestamp, entryTimestamp) {
|
|
62
|
+
if (typeof messageTimestamp === "number")
|
|
63
|
+
return messageTimestamp;
|
|
64
|
+
if (typeof messageTimestamp === "string") {
|
|
65
|
+
const n = new Date(messageTimestamp).getTime();
|
|
66
|
+
if (!Number.isNaN(n))
|
|
67
|
+
return n;
|
|
93
68
|
}
|
|
94
|
-
|
|
95
|
-
|
|
69
|
+
if (typeof entryTimestamp === "string") {
|
|
70
|
+
const n = new Date(entryTimestamp).getTime();
|
|
71
|
+
if (!Number.isNaN(n))
|
|
72
|
+
return n;
|
|
96
73
|
}
|
|
74
|
+
return 0;
|
|
97
75
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
function fmt(n) {
|
|
114
|
-
return n >= 1e9
|
|
115
|
-
? `${(n / 1e9).toFixed(2)}B`
|
|
116
|
-
: n >= 1e6
|
|
117
|
-
? `${(n / 1e6).toFixed(2)}M`
|
|
118
|
-
: n >= 1e3
|
|
119
|
-
? `${(n / 1e3).toFixed(1)}K`
|
|
120
|
-
: `${Math.round(n)}`;
|
|
76
|
+
function auxMessage(usage, ts, sourceId) {
|
|
77
|
+
return {
|
|
78
|
+
provider: "aux",
|
|
79
|
+
model: "auxiliary",
|
|
80
|
+
thinkingLevel: undefined,
|
|
81
|
+
cost: usage.cost,
|
|
82
|
+
input: usage.input,
|
|
83
|
+
output: usage.output,
|
|
84
|
+
cacheRead: usage.cacheRead,
|
|
85
|
+
cacheWrite: usage.cacheWrite,
|
|
86
|
+
timestamp: ts,
|
|
87
|
+
reasoning: usage.reasoning,
|
|
88
|
+
afterCompaction: false,
|
|
89
|
+
source: "auxiliary",
|
|
90
|
+
};
|
|
121
91
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Parse one session jsonl into its usage messages (the same semantics as
|
|
94
|
+
* the pi-usage-extension: assistant messages contribute their own usage,
|
|
95
|
+
* compaction/branch_summary entries contribute auxiliary usage).
|
|
96
|
+
*/
|
|
97
|
+
function parseSessionFile(path) {
|
|
98
|
+
let sessionId = "";
|
|
99
|
+
let cwd = "";
|
|
100
|
+
let parentSession = "";
|
|
101
|
+
let thinkingLevel;
|
|
102
|
+
let compactionPending = false;
|
|
103
|
+
const messages = [];
|
|
104
|
+
let content;
|
|
105
|
+
try {
|
|
106
|
+
content = readFileSync(path, "utf8");
|
|
130
107
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
108
|
+
catch {
|
|
109
|
+
return { sessionId, cwd, parentSession, messages };
|
|
110
|
+
}
|
|
111
|
+
for (const line of content.split("\n")) {
|
|
112
|
+
if (!line.trim())
|
|
113
|
+
continue;
|
|
114
|
+
let entry;
|
|
115
|
+
try {
|
|
116
|
+
entry = JSON.parse(line);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue; // malformed line
|
|
120
|
+
}
|
|
121
|
+
switch (entry.type) {
|
|
122
|
+
case "session": {
|
|
123
|
+
if (typeof entry.id === "string")
|
|
124
|
+
sessionId = entry.id;
|
|
125
|
+
if (typeof entry.cwd === "string")
|
|
126
|
+
cwd = entry.cwd;
|
|
127
|
+
if (typeof entry.parentSession === "string")
|
|
128
|
+
parentSession = entry.parentSession;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "thinking_level_change": {
|
|
132
|
+
if (typeof entry.thinkingLevel === "string")
|
|
133
|
+
thinkingLevel = entry.thinkingLevel;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "compaction": {
|
|
137
|
+
const usage = parseUsageAmount(entry.usage);
|
|
138
|
+
if (usage)
|
|
139
|
+
messages.push(auxMessage(usage, tsOf(undefined, entry.timestamp), typeof entry.id === "string" ? entry.id : ""));
|
|
140
|
+
compactionPending = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "branch_summary": {
|
|
144
|
+
const usage = parseUsageAmount(entry.usage);
|
|
145
|
+
if (usage)
|
|
146
|
+
messages.push(auxMessage(usage, tsOf(undefined, entry.timestamp), typeof entry.id === "string" ? entry.id : ""));
|
|
147
|
+
break;
|
|
154
148
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
149
|
+
case "message": {
|
|
150
|
+
const msg = entry.message;
|
|
151
|
+
if (!msg)
|
|
152
|
+
break;
|
|
153
|
+
if (msg.role === "assistant" && msg.usage && msg.provider && msg.model) {
|
|
154
|
+
const usage = parseUsageAmount(msg.usage);
|
|
155
|
+
if (usage) {
|
|
156
|
+
messages.push({
|
|
157
|
+
provider: msg.provider,
|
|
158
|
+
model: msg.model,
|
|
159
|
+
thinkingLevel,
|
|
160
|
+
cost: usage.cost,
|
|
161
|
+
input: usage.input,
|
|
162
|
+
output: usage.output,
|
|
163
|
+
cacheRead: usage.cacheRead,
|
|
164
|
+
cacheWrite: usage.cacheWrite,
|
|
165
|
+
timestamp: tsOf(msg.timestamp, entry.timestamp),
|
|
166
|
+
reasoning: usage.reasoning,
|
|
167
|
+
afterCompaction: compactionPending,
|
|
168
|
+
source: "assistant",
|
|
169
|
+
});
|
|
170
|
+
compactionPending = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
break;
|
|
164
177
|
}
|
|
165
178
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
];
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
lines.push(`| ${key} | ${fmt(t.input)} | ${fmt(t.output)} | $${t.cost.toFixed(4)} | ${t.messages} |`);
|
|
181
|
-
}
|
|
179
|
+
return { sessionId, cwd, parentSession, messages };
|
|
180
|
+
}
|
|
181
|
+
/** Collect usage across all session files. Empty array when none found. */
|
|
182
|
+
export function collectUsage() {
|
|
183
|
+
const root = sessionsDir();
|
|
184
|
+
if (!root)
|
|
185
|
+
return [];
|
|
186
|
+
const files = [];
|
|
187
|
+
collectSessionFiles(root, files);
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const f of files) {
|
|
190
|
+
const parsed = parseSessionFile(f);
|
|
191
|
+
if (parsed.messages.length > 0 || parsed.sessionId)
|
|
192
|
+
out.push(parsed);
|
|
182
193
|
}
|
|
183
|
-
return
|
|
194
|
+
return out;
|
|
184
195
|
}
|
|
185
196
|
const TAB_KEYS = ["today", "thisWeek", "lastWeek", "last30Days", "allTime"];
|
|
186
197
|
function emptyTokens() {
|
|
@@ -237,12 +248,15 @@ function periodStart(key, now) {
|
|
|
237
248
|
return 0; // allTime
|
|
238
249
|
}
|
|
239
250
|
/**
|
|
240
|
-
* Aggregate
|
|
241
|
-
* Returns null when no
|
|
251
|
+
* Aggregate collected session usage into the structured payload the web
|
|
252
|
+
* panel renders. Returns null when no usage data exists. When sessionId
|
|
253
|
+
* is given, only that session's usage is aggregated (scope: current
|
|
254
|
+
* session); otherwise all sessions.
|
|
242
255
|
*/
|
|
243
|
-
export function buildUsageData() {
|
|
244
|
-
const
|
|
245
|
-
|
|
256
|
+
export function buildUsageData(sessionId) {
|
|
257
|
+
const files = collectUsage();
|
|
258
|
+
const scoped = sessionId ? files.filter((f) => f.sessionId === sessionId) : files;
|
|
259
|
+
if (scoped.length === 0)
|
|
246
260
|
return null;
|
|
247
261
|
const now = Date.now();
|
|
248
262
|
// per-tab accumulators
|
|
@@ -253,7 +267,7 @@ export function buildUsageData() {
|
|
|
253
267
|
// hourly buckets (all providers, global) - the render side filters per tab
|
|
254
268
|
const hourly = new Map();
|
|
255
269
|
const hourStart = (ts) => Math.floor(ts / 3600_000) * 3600_000;
|
|
256
|
-
for (const file of
|
|
270
|
+
for (const file of scoped) {
|
|
257
271
|
for (const m of file.messages) {
|
|
258
272
|
const ts = m.timestamp;
|
|
259
273
|
{
|
|
@@ -317,7 +331,7 @@ export function buildUsageData() {
|
|
|
317
331
|
}
|
|
318
332
|
}
|
|
319
333
|
}
|
|
320
|
-
const payload = { tabs: {}, hourly: [], tabWindow: {}, collectedAt:
|
|
334
|
+
const payload = { tabs: {}, hourly: [], tabWindow: {}, collectedAt: Date.now() };
|
|
321
335
|
// Graph x-axis windows per tab: [start, end]. today: midnight->now;
|
|
322
336
|
// thisWeek: monday->now; lastWeek: monday->next Monday; last30Days:
|
|
323
337
|
// start->now; allTime: 0 -> now (frontend clips to first data).
|
|
@@ -375,7 +389,9 @@ export function buildUsageData() {
|
|
|
375
389
|
insights.push({
|
|
376
390
|
kind: "alarm",
|
|
377
391
|
stat: formatUsageCost(topModel.cost),
|
|
378
|
-
|
|
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)`,
|
|
379
395
|
advice: "Check whether its output quality justifies the price.",
|
|
380
396
|
});
|
|
381
397
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sdk-web",
|
|
3
|
-
"version": "0.5.
|
|
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": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "tsc -p tsconfig.json && node -e \"require('node:fs').cpSync('../static', 'dist/static', { recursive: true }); require('node:fs').mkdirSync('dist/pi-bin', { recursive: true }); require('node:fs').renameSync('dist/pii-cli.js', 'dist/pi-bin/pii-cli.js'); require('node:fs').chmodSync('dist/pi-bin/pii-cli.js', 0o755); require('node:fs').cpSync('../pii/pii', 'dist/pi-bin/pii'); require('node:fs').cpSync('../server', 'dist/pi-bin/server', { recursive: true, filter: (s) => !s.includes('__pycache__') })\"",
|
|
14
|
+
"build": "tsc -p tsconfig.json && node -e \"require('node:fs').cpSync('../static', 'dist/static', { recursive: true }); require('node:fs').mkdirSync('dist/pi-bin', { recursive: true }); require('node:fs').chmodSync('dist/cli.js', 0o755); require('node:fs').renameSync('dist/pii-cli.js', 'dist/pi-bin/pii-cli.js'); require('node:fs').chmodSync('dist/pi-bin/pii-cli.js', 0o755); require('node:fs').cpSync('../pii/pii', 'dist/pi-bin/pii'); require('node:fs').cpSync('../server', 'dist/pi-bin/server', { recursive: true, filter: (s) => !s.includes('__pycache__') })\"",
|
|
15
15
|
"dev": "tsx src/cli.ts",
|
|
16
16
|
"verify": "tsx src/verify-sdk.ts",
|
|
17
17
|
"prepublishOnly": "npm run build"
|