pi-sdk-web 0.5.0 → 0.5.1
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/usage-render.js +204 -85
- 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/usage-render.js
CHANGED
|
@@ -1,99 +1,215 @@
|
|
|
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, readFileSync, statSync } from "node:fs";
|
|
17
|
+
import { existsSync, readdirSync, readFileSync, statSync } 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;
|
|
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;
|
|
68
|
+
}
|
|
69
|
+
if (typeof entryTimestamp === "string") {
|
|
70
|
+
const n = new Date(entryTimestamp).getTime();
|
|
71
|
+
if (!Number.isNaN(n))
|
|
72
|
+
return n;
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
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
|
+
};
|
|
91
|
+
}
|
|
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;
|
|
53
105
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
content = readFileSync(path, "utf8");
|
|
107
|
+
}
|
|
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;
|
|
148
|
+
}
|
|
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
|
+
}
|
|
83
172
|
}
|
|
173
|
+
break;
|
|
84
174
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
sessionId: f.sessionId ?? "",
|
|
88
|
-
cwd: f.cwd ?? "",
|
|
89
|
-
parentSession: f.parentSession ?? "",
|
|
90
|
-
});
|
|
175
|
+
default:
|
|
176
|
+
break;
|
|
91
177
|
}
|
|
92
|
-
return { files, names };
|
|
93
178
|
}
|
|
94
|
-
|
|
95
|
-
|
|
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);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
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
|
+
}
|
|
96
211
|
}
|
|
212
|
+
return sum;
|
|
97
213
|
}
|
|
98
214
|
function startOfDay(ts) {
|
|
99
215
|
const d = new Date(ts);
|
|
@@ -124,9 +240,9 @@ function fmtCost(n) {
|
|
|
124
240
|
}
|
|
125
241
|
/** Render a Markdown usage summary (today / this week / all time). */
|
|
126
242
|
export function renderUsageSummary() {
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
return "## Usage\n\nNo usage data found
|
|
243
|
+
const files = collectUsage();
|
|
244
|
+
if (files.length === 0) {
|
|
245
|
+
return "## Usage\n\nNo usage data found yet.";
|
|
130
246
|
}
|
|
131
247
|
const now = Date.now();
|
|
132
248
|
const todayStart = startOfDay(now);
|
|
@@ -135,7 +251,7 @@ export function renderUsageSummary() {
|
|
|
135
251
|
const week = emptyTotals();
|
|
136
252
|
const all = emptyTotals();
|
|
137
253
|
const byModel = new Map();
|
|
138
|
-
for (const file of
|
|
254
|
+
for (const file of files) {
|
|
139
255
|
for (const m of file.messages) {
|
|
140
256
|
const t = m.timestamp;
|
|
141
257
|
const targets = [all];
|
|
@@ -237,12 +353,15 @@ function periodStart(key, now) {
|
|
|
237
353
|
return 0; // allTime
|
|
238
354
|
}
|
|
239
355
|
/**
|
|
240
|
-
* Aggregate
|
|
241
|
-
* Returns null when no
|
|
356
|
+
* Aggregate collected session usage into the structured payload the web
|
|
357
|
+
* panel renders. Returns null when no usage data exists. When sessionId
|
|
358
|
+
* is given, only that session's usage is aggregated (scope: current
|
|
359
|
+
* session); otherwise all sessions.
|
|
242
360
|
*/
|
|
243
|
-
export function buildUsageData() {
|
|
244
|
-
const
|
|
245
|
-
|
|
361
|
+
export function buildUsageData(sessionId) {
|
|
362
|
+
const files = collectUsage();
|
|
363
|
+
const scoped = sessionId ? files.filter((f) => f.sessionId === sessionId) : files;
|
|
364
|
+
if (scoped.length === 0)
|
|
246
365
|
return null;
|
|
247
366
|
const now = Date.now();
|
|
248
367
|
// per-tab accumulators
|
|
@@ -253,7 +372,7 @@ export function buildUsageData() {
|
|
|
253
372
|
// hourly buckets (all providers, global) - the render side filters per tab
|
|
254
373
|
const hourly = new Map();
|
|
255
374
|
const hourStart = (ts) => Math.floor(ts / 3600_000) * 3600_000;
|
|
256
|
-
for (const file of
|
|
375
|
+
for (const file of scoped) {
|
|
257
376
|
for (const m of file.messages) {
|
|
258
377
|
const ts = m.timestamp;
|
|
259
378
|
{
|
|
@@ -317,7 +436,7 @@ export function buildUsageData() {
|
|
|
317
436
|
}
|
|
318
437
|
}
|
|
319
438
|
}
|
|
320
|
-
const payload = { tabs: {}, hourly: [], tabWindow: {}, collectedAt:
|
|
439
|
+
const payload = { tabs: {}, hourly: [], tabWindow: {}, collectedAt: sessionsStamp() };
|
|
321
440
|
// Graph x-axis windows per tab: [start, end]. today: midnight->now;
|
|
322
441
|
// thisWeek: monday->now; lastWeek: monday->next Monday; last30Days:
|
|
323
442
|
// start->now; allTime: 0 -> now (frontend clips to first data).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sdk-web",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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"
|