@raingor/pi-web-switch 0.4.1 → 0.4.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/README.ja.md +32 -8
- package/README.md +58 -10
- package/README.zh-CN.md +31 -7
- package/dist-electron/main/main.cjs +2453 -0
- package/package.json +18 -5
- package/pi-package/index.ts +228 -4
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/pi.svg +6 -41
- package/public/trayIconTemplate.png +0 -0
- package/server/pi-reader.ts +470 -31
- package/src/App.tsx +0 -2
- package/src/components/dashboard/DashboardPage.tsx +45 -13
- package/src/components/layout/Sidebar.tsx +15 -6
- package/src/components/providers/ProvidersModelsPage.tsx +56 -4
- package/src/components/sessions/SessionsPage.tsx +36 -2
- package/src/components/settings/SettingsPage.tsx +72 -0
- package/src/lib/translations/en.ts +20 -58
- package/src/lib/translations/ja.ts +20 -58
- package/src/lib/translations/zh-CN.ts +20 -58
- package/src/lib/translations/zh-TW.ts +20 -58
- package/src/main.tsx +29 -5
- package/src/types/index.ts +2 -0
- package/vite.config.ts +106 -8
- package/server/agent-session-manager.ts +0 -827
- package/server/chat-api-plugin.ts +0 -488
- package/src/components/chat/ChatInput.tsx +0 -863
- package/src/components/chat/ChatPage.tsx +0 -617
- package/src/components/chat/ChatWindow.tsx +0 -338
- package/src/components/chat/MessageView.tsx +0 -595
- package/src/hooks/useAgentSession.ts +0 -1104
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raingor/pi-web-switch",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist-electron/main.
|
|
6
|
+
"main": "dist-electron/main/main.cjs",
|
|
7
7
|
"description": "Web UI for pi coding agent — live configuration management, session browser, and memory viewer",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"pi-package",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"preview": "vite preview",
|
|
30
30
|
"electron:dev": "node scripts/electron-dev.mjs",
|
|
31
31
|
"electron:build": "tsc -b && vite build && vite build --config vite.electron.config.ts && vite build --config vite.preload.config.ts && electron-builder",
|
|
32
|
-
"electron:preview": "vite build && vite build --config vite.electron.config.ts && vite build --config vite.preload.config.ts && env -u ELECTRON_RUN_AS_NODE npx electron ."
|
|
32
|
+
"electron:preview": "vite build && vite build --config vite.electron.config.ts && vite build --config vite.preload.config.ts && env -u ELECTRON_RUN_AS_NODE npx electron .",
|
|
33
|
+
"tray:icon": "node scripts/generate-tray-icon.mjs"
|
|
33
34
|
},
|
|
34
35
|
"files": [
|
|
35
36
|
"pi-package",
|
|
@@ -89,8 +90,20 @@
|
|
|
89
90
|
"dist-electron/**/*"
|
|
90
91
|
],
|
|
91
92
|
"mac": {
|
|
92
|
-
"target":
|
|
93
|
-
|
|
93
|
+
"target": [
|
|
94
|
+
{
|
|
95
|
+
"target": "dmg"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"target": "zip"
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"icon": "build/icon.icns",
|
|
102
|
+
"extendInfo": {
|
|
103
|
+
"LSMinimumSystemVersion": "11.0.0",
|
|
104
|
+
"CFBundleName": "pi-web-switch"
|
|
105
|
+
},
|
|
106
|
+
"category": "public.app-category.developer-tools"
|
|
94
107
|
},
|
|
95
108
|
"win": {
|
|
96
109
|
"target": "nsis",
|
package/pi-package/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { join, resolve, dirname } from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
const PI_SWITCH_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const PI_DIR = join(homedir(), ".pi", "agent");
|
|
11
12
|
|
|
12
13
|
let serverProcess: ReturnType<typeof spawn> | null = null;
|
|
13
14
|
|
|
@@ -17,8 +18,188 @@ function getPackageManager(): "npm" | "pnpm" | "yarn" {
|
|
|
17
18
|
return "npm";
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// ─── Usage reader ────────────────────────────────────────
|
|
22
|
+
// Reads ~/.pi/agent/sessions/*.jsonl directly and aggregates today / 7d stats,
|
|
23
|
+
// so the user can see usage at a glance without launching the dashboard.
|
|
24
|
+
|
|
25
|
+
interface UsageRecord {
|
|
26
|
+
date: string;
|
|
27
|
+
hour?: number;
|
|
28
|
+
providerId: string;
|
|
29
|
+
modelId: string;
|
|
30
|
+
inputTokens: number;
|
|
31
|
+
outputTokens: number;
|
|
32
|
+
cacheReadTokens: number;
|
|
33
|
+
cacheWriteTokens: number;
|
|
34
|
+
requests: number;
|
|
35
|
+
cost: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CN_TZ = "Asia/Shanghai";
|
|
39
|
+
|
|
40
|
+
function cnDateParts(ts: string | number): { date: string; hour: number } {
|
|
41
|
+
const d = new Date(ts);
|
|
42
|
+
if (isNaN(d.getTime())) return { date: "unknown", hour: 0 };
|
|
43
|
+
const date = new Intl.DateTimeFormat("en-CA", {
|
|
44
|
+
timeZone: CN_TZ,
|
|
45
|
+
year: "numeric",
|
|
46
|
+
month: "2-digit",
|
|
47
|
+
day: "2-digit",
|
|
48
|
+
}).format(d);
|
|
49
|
+
const hour = Number(
|
|
50
|
+
new Intl.DateTimeFormat("en-US", {
|
|
51
|
+
timeZone: CN_TZ,
|
|
52
|
+
hour: "2-digit",
|
|
53
|
+
hour12: false,
|
|
54
|
+
}).format(d)
|
|
55
|
+
);
|
|
56
|
+
return { date, hour: hour === 24 ? 0 : hour };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseSessionFile(filePath: string): UsageRecord[] {
|
|
60
|
+
const records: UsageRecord[] = [];
|
|
61
|
+
try {
|
|
62
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
63
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
64
|
+
|
|
65
|
+
let currentProvider = "unknown";
|
|
66
|
+
let currentModel = "unknown";
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
try {
|
|
70
|
+
const obj = JSON.parse(line);
|
|
71
|
+
const type = obj.type;
|
|
72
|
+
|
|
73
|
+
if (type === "model_change") {
|
|
74
|
+
currentProvider = obj.provider || currentProvider;
|
|
75
|
+
currentModel = obj.modelId || currentModel;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (type === "message" && obj.message?.role === "assistant") {
|
|
80
|
+
const usage = obj.message.usage;
|
|
81
|
+
if (!usage || !usage.input) continue;
|
|
82
|
+
|
|
83
|
+
const timestamp = obj.timestamp || obj.message.timestamp;
|
|
84
|
+
const { date, hour } = cnDateParts(timestamp);
|
|
85
|
+
|
|
86
|
+
records.push({
|
|
87
|
+
date,
|
|
88
|
+
hour,
|
|
89
|
+
providerId: obj.message.provider || currentProvider,
|
|
90
|
+
modelId: obj.message.model || currentModel,
|
|
91
|
+
inputTokens: usage.input ?? 0,
|
|
92
|
+
outputTokens: usage.output ?? 0,
|
|
93
|
+
cacheReadTokens: usage.cacheRead ?? 0,
|
|
94
|
+
cacheWriteTokens: usage.cacheWrite ?? 0,
|
|
95
|
+
requests: 1,
|
|
96
|
+
cost: usage.cost?.total ?? 0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// skip malformed lines
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// skip unreadable files
|
|
105
|
+
}
|
|
106
|
+
return records;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readAllUsage(): UsageRecord[] {
|
|
110
|
+
const sessionsPath = join(PI_DIR, "sessions");
|
|
111
|
+
const { readdirSync, existsSync: exists, statSync } = require("node:fs");
|
|
112
|
+
if (!exists(sessionsPath)) return [];
|
|
113
|
+
|
|
114
|
+
const dirs = readdirSync(sessionsPath)
|
|
115
|
+
.filter((name: string) => name.startsWith("--"))
|
|
116
|
+
.map((name: string) => join(sessionsPath, name))
|
|
117
|
+
.filter((dir: string) => statSync(dir).isDirectory());
|
|
118
|
+
|
|
119
|
+
const allRecords: UsageRecord[] = [];
|
|
120
|
+
for (const dir of dirs) {
|
|
121
|
+
try {
|
|
122
|
+
const files = readdirSync(dir).filter((f: string) => f.endsWith(".jsonl"));
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
const records = parseSessionFile(join(dir, file));
|
|
125
|
+
allRecords.push(...records);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// skip unreadable directories
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return allRecords;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function aggregateSummary() {
|
|
135
|
+
const records = readAllUsage();
|
|
136
|
+
// pi-reader buckets dates in Asia/Shanghai (CN_TZ); use the same timezone
|
|
137
|
+
// so "today" lines up with the session data around midnight UTC.
|
|
138
|
+
const cnDate = (d: Date) =>
|
|
139
|
+
new Intl.DateTimeFormat("en-CA", {
|
|
140
|
+
timeZone: "Asia/Shanghai",
|
|
141
|
+
year: "numeric",
|
|
142
|
+
month: "2-digit",
|
|
143
|
+
day: "2-digit",
|
|
144
|
+
}).format(d);
|
|
145
|
+
const today = cnDate(new Date());
|
|
146
|
+
const sevenDaysAgo = cnDate(new Date(Date.now() - 6 * 24 * 60 * 60 * 1000));
|
|
147
|
+
|
|
148
|
+
const todayRecs = records.filter((r) => r.date === today);
|
|
149
|
+
const sevenDayRecs = records.filter((r) => r.date >= sevenDaysAgo);
|
|
150
|
+
|
|
151
|
+
const sum = (recs: UsageRecord[]) => {
|
|
152
|
+
let tokens = 0,
|
|
153
|
+
cost = 0,
|
|
154
|
+
requests = 0;
|
|
155
|
+
for (const r of recs) {
|
|
156
|
+
tokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
|
|
157
|
+
cost += r.cost;
|
|
158
|
+
requests += r.requests;
|
|
159
|
+
}
|
|
160
|
+
return { tokens, cost, requests };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Per-day breakdown for last 7 days
|
|
164
|
+
const dailyMap = new Map<string, { tokens: number; cost: number; requests: number }>();
|
|
165
|
+
for (const r of sevenDayRecs) {
|
|
166
|
+
const d = dailyMap.get(r.date) ?? { tokens: 0, cost: 0, requests: 0 };
|
|
167
|
+
d.tokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
|
|
168
|
+
d.cost += r.cost;
|
|
169
|
+
d.requests += r.requests;
|
|
170
|
+
dailyMap.set(r.date, d);
|
|
171
|
+
}
|
|
172
|
+
const daily = Array.from(dailyMap.entries())
|
|
173
|
+
.map(([date, v]) => ({ date, ...v }))
|
|
174
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
today: sum(todayRecs),
|
|
178
|
+
sevenDays: sum(sevenDayRecs),
|
|
179
|
+
daily,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatTokens(n: number): string {
|
|
184
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
185
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
186
|
+
return n.toLocaleString();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatCost(n: number): string {
|
|
190
|
+
if (n === 0) return "$0.00";
|
|
191
|
+
if (n < 0.01) return `$${n.toFixed(4)}`;
|
|
192
|
+
return `$${n.toFixed(2)}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function shortDate(iso: string): string {
|
|
196
|
+
const parts = iso.split("-");
|
|
197
|
+
if (parts.length !== 3) return iso;
|
|
198
|
+
return `${parts[1]}/${parts[2]}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
20
201
|
export default function (api: ExtensionAPI, ctx: ExtensionContext) {
|
|
21
|
-
//
|
|
202
|
+
// /pi-switch start|stop|status — launch the dashboard web UI
|
|
22
203
|
api.registerCommand({
|
|
23
204
|
name: "pi-switch",
|
|
24
205
|
description: "Start or stop the pi-web-switch dashboard",
|
|
@@ -41,7 +222,7 @@ export default function (api: ExtensionAPI, ctx: ExtensionContext) {
|
|
|
41
222
|
return ctx.say(
|
|
42
223
|
<Box borderStyle="round" borderColor="yellow" paddingLeft={1} paddingRight={1}>
|
|
43
224
|
<Text>pi-web-switch is not running.</Text>
|
|
44
|
-
<Text>Use `/pi-
|
|
225
|
+
<Text>Use `/pi-switch start` to launch the dashboard.</Text>
|
|
45
226
|
</Box>
|
|
46
227
|
);
|
|
47
228
|
}
|
|
@@ -84,14 +265,57 @@ export default function (api: ExtensionAPI, ctx: ExtensionContext) {
|
|
|
84
265
|
|
|
85
266
|
serverProcess.unref();
|
|
86
267
|
|
|
87
|
-
// Wait a moment then check
|
|
88
268
|
await new Promise((r) => setTimeout(r, 2000));
|
|
89
269
|
|
|
90
270
|
return ctx.say(
|
|
91
271
|
<Box borderStyle="round" borderColor="green" paddingLeft={1} paddingRight={1}>
|
|
92
272
|
<Text>pi-web-switch started!</Text>
|
|
93
273
|
<Text>Dashboard: http://localhost:{port}</Text>
|
|
94
|
-
<Text>Use `/pi-
|
|
274
|
+
<Text>Use `/pi-switch stop` to stop the server.</Text>
|
|
275
|
+
</Box>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// /pi-usage — quick usage summary (today + 7d) in the terminal
|
|
282
|
+
api.registerCommand({
|
|
283
|
+
name: "pi-usage",
|
|
284
|
+
description: "Show pi usage summary (today / 7 days) without launching the dashboard",
|
|
285
|
+
params: Type.Object({}),
|
|
286
|
+
execute: async () => {
|
|
287
|
+
try {
|
|
288
|
+
const s = aggregateSummary();
|
|
289
|
+
const spark = s.daily
|
|
290
|
+
.map((d) => {
|
|
291
|
+
const max = Math.max(1, ...s.daily.map((x) => x.tokens));
|
|
292
|
+
const bars = Math.round((d.tokens / max) * 8);
|
|
293
|
+
return `${shortDate(d.date)} ${"█".repeat(bars)}${"░".repeat(8 - bars)} ${formatTokens(d.tokens)}`;
|
|
294
|
+
})
|
|
295
|
+
.join("\n");
|
|
296
|
+
|
|
297
|
+
return ctx.say(
|
|
298
|
+
<Box borderStyle="round" borderColor="green" paddingLeft={1} paddingRight={1}>
|
|
299
|
+
<Text bold>📊 pi usage summary</Text>
|
|
300
|
+
<Text> </Text>
|
|
301
|
+
<Text bold>Today ({new Date().toISOString().slice(0, 10)})</Text>
|
|
302
|
+
<Text> Tokens: {formatTokens(s.today.tokens)}</Text>
|
|
303
|
+
<Text> Cost: {formatCost(s.today.cost)}</Text>
|
|
304
|
+
<Text> Requests: {s.today.requests}</Text>
|
|
305
|
+
<Text> </Text>
|
|
306
|
+
<Text bold>Last 7 days</Text>
|
|
307
|
+
<Text> Tokens: {formatTokens(s.sevenDays.tokens)}</Text>
|
|
308
|
+
<Text> Cost: {formatCost(s.sevenDays.cost)}</Text>
|
|
309
|
+
<Text> Requests: {s.sevenDays.requests}</Text>
|
|
310
|
+
<Text> </Text>
|
|
311
|
+
<Text bold>Daily trend</Text>
|
|
312
|
+
<Text>{spark}</Text>
|
|
313
|
+
</Box>
|
|
314
|
+
);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
return ctx.say(
|
|
317
|
+
<Box borderStyle="round" borderColor="red" paddingLeft={1} paddingRight={1}>
|
|
318
|
+
<Text>Failed to read usage: {String(err)}</Text>
|
|
95
319
|
</Box>
|
|
96
320
|
);
|
|
97
321
|
}
|
|
Binary file
|
package/public/icon-192.png
CHANGED
|
Binary file
|
package/public/icon-512.png
CHANGED
|
Binary file
|
package/public/pi.svg
CHANGED
|
@@ -1,47 +1,12 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none">
|
|
2
2
|
<defs>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<stop offset="
|
|
6
|
-
<stop offset="0.5" stop-color="#2563eb"/>
|
|
7
|
-
<stop offset="1" stop-color="#1e40af"/>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="0.3" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#3b82f6"/>
|
|
5
|
+
<stop offset="1" stop-color="#1d4ed8"/>
|
|
8
6
|
</linearGradient>
|
|
9
|
-
<!-- Soft top gloss for a tactile, premium feel -->
|
|
10
|
-
<linearGradient id="gloss" x1="0" y1="0" x2="0" y2="1">
|
|
11
|
-
<stop offset="0" stop-color="#ffffff" stop-opacity="0.28"/>
|
|
12
|
-
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
|
|
13
|
-
</linearGradient>
|
|
14
|
-
<!-- Subtle vertical shading on the pi glyph -->
|
|
15
|
-
<linearGradient id="piGrad" x1="0" y1="0" x2="0" y2="1">
|
|
16
|
-
<stop offset="0" stop-color="#ffffff"/>
|
|
17
|
-
<stop offset="1" stop-color="#d9e6ff"/>
|
|
18
|
-
</linearGradient>
|
|
19
|
-
<!-- Toggle knob highlight -->
|
|
20
|
-
<radialGradient id="knob" cx="0.35" cy="0.3" r="0.85">
|
|
21
|
-
<stop offset="0" stop-color="#ffffff"/>
|
|
22
|
-
<stop offset="1" stop-color="#cdd9f2"/>
|
|
23
|
-
</radialGradient>
|
|
24
|
-
<filter id="softShadow" x="-30%" y="-30%" width="160%" height="160%">
|
|
25
|
-
<feDropShadow dx="0" dy="7" stdDeviation="11" flood-color="#0a2a6b" flood-opacity="0.38"/>
|
|
26
|
-
</filter>
|
|
27
|
-
<filter id="knobShadow" x="-60%" y="-60%" width="220%" height="220%">
|
|
28
|
-
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#0a2a6b" flood-opacity="0.35"/>
|
|
29
|
-
</filter>
|
|
30
7
|
</defs>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<rect x="0" y="0" width="512" height="512" rx="115" fill="url(#bg)"/>
|
|
34
|
-
<!-- Top gloss highlight -->
|
|
35
|
-
<rect x="0" y="0" width="512" height="250" rx="115" fill="url(#gloss)"/>
|
|
36
|
-
|
|
37
|
-
<!-- Toggle switch track (behind the pi) -->
|
|
38
|
-
<rect x="92" y="217" width="328" height="78" rx="39" fill="#ffffff" opacity="0.20"/>
|
|
39
|
-
<rect x="92" y="217" width="328" height="78" rx="39" fill="none" stroke="#ffffff" stroke-opacity="0.25" stroke-width="2"/>
|
|
40
|
-
<!-- Toggle knob in the "on" position -->
|
|
41
|
-
<circle cx="378" cy="256" r="30" fill="url(#knob)" filter="url(#knobShadow)"/>
|
|
42
|
-
|
|
43
|
-
<!-- Pi glyph -->
|
|
44
|
-
<text x="256" y="256" text-anchor="middle" dominant-baseline="central"
|
|
8
|
+
<rect x="0" y="0" width="512" height="512" rx="113" fill="url(#bg)"/>
|
|
9
|
+
<text x="256" y="270" text-anchor="middle" dominant-baseline="central"
|
|
45
10
|
font-family="-apple-system, 'SF Pro Display', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
|
46
|
-
font-size="
|
|
11
|
+
font-size="240" font-weight="500" fill="#ffffff">π</text>
|
|
47
12
|
</svg>
|
|
Binary file
|