@raingor/pi-web-switch 0.4.1 → 0.4.3
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/index.html +18 -0
- package/package.json +5 -25
- package/pi-package/index.ts +228 -4
- package/public/trayIconTemplate.png +0 -0
- package/server/pi-reader.ts +251 -3
- package/src/App.tsx +0 -2
- package/src/components/dashboard/DashboardPage.tsx +43 -40
- package/src/components/layout/Sidebar.tsx +1 -3
- package/src/components/providers/ProvidersModelsPage.tsx +56 -4
- package/src/components/sessions/MemoryPage.tsx +2 -1
- package/src/components/settings/SettingsPage.tsx +72 -0
- package/src/lib/translations/en.ts +15 -58
- package/src/lib/translations/ja.ts +15 -58
- package/src/lib/translations/zh-CN.ts +15 -58
- package/src/lib/translations/zh-TW.ts +15 -58
- package/src/types/index.ts +1 -0
- package/vite.config.ts +89 -15
- 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/README.ja.md
CHANGED
|
@@ -88,17 +88,32 @@ npm run build # プロダクションビルド
|
|
|
88
88
|
|
|
89
89
|
## 🖥️ デスクトップアプリ(Electron)
|
|
90
90
|
|
|
91
|
-
pi-web-switch
|
|
91
|
+
pi-web-switch は **macOS メニューバーアプリ**(および Windows/Linux トレイアプリ)としても利用可能 — アプリはメニューバーに常駐し、トレイアイコンのクリックで使用量サマリー(今日の tokens / コスト / リクエスト数 + 7 日間の sparkline + Top プロバイダ)がポップアップします。フルダッシュボードはトレイメニュー or ダブルクリックで開けます。
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
# 開発モードで実行(Vite 開発サーバー + Electron、HMR
|
|
94
|
+
# 開発モードで実行(Vite 開発サーバー + Electron、HMR 寂応)
|
|
95
95
|
npm run electron:dev
|
|
96
96
|
|
|
97
|
-
# インストーラー(DMG / NSIS / AppImage)を release/ にビルド
|
|
97
|
+
# インストーラー(macOS DMG+ZIP / Windows NSIS / Linux AppImage)を release/ にビルド
|
|
98
98
|
npm run electron:build
|
|
99
|
+
|
|
100
|
+
# パッケージ化せずプロダクションビルドをプレビュー
|
|
101
|
+
npm run electron:preview
|
|
99
102
|
```
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
### 🍹 macOS メニューバーモード
|
|
105
|
+
|
|
106
|
+
パッケージ化された macOS アプリは **メニューバー専用アプリ** として実行 — Dock に表示されず、`Cmd+Q` でトレイメニューから終了。`electron-builder` の `extendInfo` で `LSUIElement = true` を設定して実装。開発モード(`electron:dev`)では Dock アイコンが表示されます(この Info.plist キーはパッケージビルドにのみ適用)。トレイアイコンは両モードで動作します。
|
|
107
|
+
|
|
108
|
+
| トレイ操作 | 動作 |
|
|
109
|
+
|------------|------|
|
|
110
|
+
| クリック | 使用量ポップアップの切替(今日 + 7 日 tokens / コスト / リクエスト数、sparkline、Top プロバイダ) |
|
|
111
|
+
| 右クリック | コンテキストメニュー:Dashboard を開く、使用量を更新、終了 |
|
|
112
|
+
| メニュー → "開く Dashboard" | フル React Dashboard ウィンドウを開く |
|
|
113
|
+
|
|
114
|
+
ポップアップは表示中 30 秒ごとに自動更新。トレイアイコンは 32×32 template PNG(`build/trayIconTemplate.png`)で明暗メニューバーに自動適合;`npm run tray:icon` で再生成。
|
|
115
|
+
|
|
116
|
+
アプリアイコンは `build/`(`icon.icns` / `icon.ico` / `icon.png`)にあり;開発モードでは実行時に `public/icon-512.png` から macOS の Dock アイコンを設定します。
|
|
102
117
|
|
|
103
118
|
## 🏗️ 技術スタック
|
|
104
119
|
|
|
@@ -124,9 +139,12 @@ pi-web-switch は **pi コーディングエージェント拡張** としてイ
|
|
|
124
139
|
|
|
125
140
|
| コマンド | 説明 |
|
|
126
141
|
|---------|------|
|
|
127
|
-
| `/pi-
|
|
128
|
-
| `/pi-
|
|
129
|
-
| `/pi-
|
|
142
|
+
| `/pi-switch start` | ダッシュボード起動 http://localhost:5173 |
|
|
143
|
+
| `/pi-switch stop` | サーバー停止 |
|
|
144
|
+
| `/pi-switch status` | 実行状態を確認 |
|
|
145
|
+
| `/pi-usage` | 使用量サマリー(今日 + 7 日)を端末に印字 — tokens / コスト / リクエスト数 / デイリー sparkline、ダッシュボード起動不要 |
|
|
146
|
+
|
|
147
|
+
`/pi-usage` コマンドは `~/.pi/agent/sessions/*.jsonl` を直接読み込み、今日 + 直近 7 日の統計を集約します。どの pi セッションからでも一目で使用量を確認可能。macOS メニューバーポップアップと同じ内容を表示します。
|
|
130
148
|
|
|
131
149
|
### パッケージ構造
|
|
132
150
|
|
|
@@ -134,12 +152,18 @@ pi-web-switch は **pi コーディングエージェント拡張** としてイ
|
|
|
134
152
|
pi-web-switch/
|
|
135
153
|
├── package.json # npm パッケージ + pi.extensions + pi.skills
|
|
136
154
|
├── pi-package/
|
|
137
|
-
│ ├── index.ts # 拡張エントリ:/pi-
|
|
155
|
+
│ ├── index.ts # 拡張エントリ:/pi-switch、/pi-usage コマンド登録
|
|
138
156
|
│ └── skills/
|
|
139
157
|
│ └── pi-web-switch/
|
|
140
158
|
│ └── SKILL.md # 使用ドキュメント
|
|
141
159
|
├── server/
|
|
142
160
|
│ └── pi-reader.ts # サーバーサイド:~/.pi/agent/ 読み取り
|
|
161
|
+
├── electron/ # デスクトップアプリ(macOS メニューバー / トレイ)
|
|
162
|
+
│ ├── main.ts # メインプロセス:トレイ、ポップアップ、IPC、ローカル API サーバー
|
|
163
|
+
│ ├── api-server.ts # ローカル HTTP サーバー(パッケージ版で dist/ + /api/pi/* を提供)
|
|
164
|
+
│ ├── preload.ts # contextBridge:settings/auth/models/usage IPC
|
|
165
|
+
│ ├── popup.html # メニューバー使用量ポップアップ
|
|
166
|
+
│ └── popup-render.ts # ポップアップ描画(今日/7 日 tokens、sparkline、Top プロバイダ)
|
|
143
167
|
└── src/ # React フロントエンド
|
|
144
168
|
```
|
|
145
169
|
|
package/README.md
CHANGED
|
@@ -139,17 +139,32 @@ The dev server automatically serves pi configuration via Vite middleware at `/ap
|
|
|
139
139
|
|
|
140
140
|
## 🖥️ Desktop App (Electron)
|
|
141
141
|
|
|
142
|
-
pi-web-switch also runs as a
|
|
142
|
+
pi-web-switch also runs as a **macOS menu bar app** (and a Windows/Linux tray app) — the app lives in your menu bar, and a single click on the tray icon pops up a compact usage summary (today's tokens / cost / requests + a 7-day sparkline + top providers). The full Dashboard is still available from the tray menu or by double-clicking the tray icon.
|
|
143
143
|
|
|
144
144
|
```bash
|
|
145
145
|
# Run in development (Vite dev server + Electron with HMR)
|
|
146
146
|
npm run electron:dev
|
|
147
147
|
|
|
148
|
-
# Build installers (DMG
|
|
148
|
+
# Build installers (DMG + ZIP for macOS, NSIS for Windows, AppImage for Linux) into release/
|
|
149
149
|
npm run electron:build
|
|
150
|
+
|
|
151
|
+
# Preview the production build without packaging
|
|
152
|
+
npm run electron:preview
|
|
150
153
|
```
|
|
151
154
|
|
|
152
|
-
|
|
155
|
+
### 🍹 macOS menu bar mode
|
|
156
|
+
|
|
157
|
+
The packaged macOS app runs as a **menu bar-only** app — it does not appear in the Dock, and `Cmd+Q` quits from the tray menu. This is wired in via `LSUIElement = true` in `electron-builder`'s `extendInfo`. In dev (`electron:dev`) the Dock icon still shows because that Info.plist key only applies to packaged builds; the tray icon works in both modes.
|
|
158
|
+
|
|
159
|
+
| Tray interaction | What happens |
|
|
160
|
+
|------------------|--------------|
|
|
161
|
+
| Click | Toggle the usage popup (today + 7d tokens / cost / requests, sparkline, top providers) |
|
|
162
|
+
| Right-click | Context menu: open Dashboard, refresh usage, quit |
|
|
163
|
+
| Menu → "打开 Dashboard" | Open the full React Dashboard window |
|
|
164
|
+
|
|
165
|
+
The popup auto-refreshes every 30 seconds while visible. The tray icon is a 16×16 template PNG (`build/trayIconTemplate.png`) that adapts to light/dark menu bars; regenerate it with `npm run tray:icon`.
|
|
166
|
+
|
|
167
|
+
App icons live in `build/` (`icon.icns` / `icon.ico` / `icon.png`); in development the macOS dock icon is set at runtime from `public/icon-512.png`.
|
|
153
168
|
|
|
154
169
|
## 🏗️ Tech Stack
|
|
155
170
|
|
|
@@ -170,10 +185,21 @@ App icons live in `build/` (`icon.icns` / `icon.ico` / `icon.png`) and are wired
|
|
|
170
185
|
pi-web-switch/
|
|
171
186
|
├── index.html
|
|
172
187
|
├── package.json
|
|
173
|
-
├── vite.config.ts # Vite config + pi API plugin (middleware)
|
|
188
|
+
├── vite.config.ts # Vite config + pi API plugin (middleware) + popup entry
|
|
174
189
|
├── tsconfig.json
|
|
175
190
|
├── server/
|
|
176
191
|
│ └── pi-reader.ts # Server-side module: reads ~/.pi/agent/ files + parses sessions
|
|
192
|
+
├── electron/ # Desktop app (macOS menu-bar / tray)
|
|
193
|
+
│ ├── main.ts # Main process: tray, popup window, IPC, local API server
|
|
194
|
+
│ ├── api-server.ts # Local HTTP server serving dist/ + /api/pi/* in packaged mode
|
|
195
|
+
│ ├── preload.ts # contextBridge: settings/auth/models/usage IPC
|
|
196
|
+
│ ├── popup.html # Menu-bar popup UI
|
|
197
|
+
│ └── popup-render.ts # Popup renderer (today/7d tokens, sparkline, top providers)
|
|
198
|
+
├── scripts/
|
|
199
|
+
│ └── generate-tray-icon.mjs # Regenerates build/trayIconTemplate.png
|
|
200
|
+
├── build/
|
|
201
|
+
│ ├── trayIconTemplate.png # 32x32 menu-bar template icon
|
|
202
|
+
│ └── icon.icns / icon.ico / icon.png
|
|
177
203
|
├── public/
|
|
178
204
|
│ └── pi.svg
|
|
179
205
|
└── src/
|
|
@@ -224,7 +250,7 @@ Changes made in the UI are written back to these files in real time — the pi a
|
|
|
224
250
|
|
|
225
251
|
## 🧩 API Routes
|
|
226
252
|
|
|
227
|
-
|
|
253
|
+
These endpoints at `/api/pi/*` are served by the Vite middleware in dev, and by the built-in HTTP server (`electron/api-server.ts`) in the packaged app — so the frontend works identically in both modes:
|
|
228
254
|
|
|
229
255
|
| Method | Path | Description |
|
|
230
256
|
|--------|------|-------------|
|
|
@@ -236,10 +262,29 @@ The Vite dev server exposes these endpoints at `/api/pi/*`:
|
|
|
236
262
|
| POST | `/api/pi/models` | Write `models.json` |
|
|
237
263
|
| GET | `/api/pi/builtin-providers` | List hardcoded built-in providers |
|
|
238
264
|
| GET | `/api/pi/usage` | Aggregated token/cost/request data from sessions |
|
|
239
|
-
| GET | `/api/pi/usage-range` | Date-range filtered usage
|
|
265
|
+
| GET | `/api/pi/usage-range` | Date-range filtered usage (pi sessions) — `?range=today\|7d\|30d\|custom&from=&to=` |
|
|
266
|
+
| GET | `/api/pi/all-usage-range` | Same shape, but combined across **all** sources (pi + cindy + claude + codex + atomcode + copilot) |
|
|
267
|
+
| GET | `/api/pi/{cindy-pi\|claude\|codex\|opencode\|gemini\|grok\|atomcode\|copilot}-usage-range` | Per-source usage range |
|
|
268
|
+
| GET | `/api/pi/copilot-usage-range` | Local Copilot CLI usage from `~/.copilot/session-store.db` (tokens / requests per day per model; no GitHub API or token required) |
|
|
269
|
+
| GET | `/api/pi/copilot-config` | Read Copilot GitHub config (username, token) |
|
|
270
|
+
| POST | `/api/pi/copilot-config` | Write Copilot GitHub config |
|
|
240
271
|
| GET | `/api/pi/sessions` | Session list grouped by project |
|
|
241
|
-
| DELETE | `/api/pi/session?path=` |
|
|
272
|
+
| DELETE | `/api/pi/session?path=` | Move a session file to trash (path must be under sessions/) |
|
|
273
|
+
| POST | `/api/pi/session/trash` | Move a session to trash (body: `{ path }`) |
|
|
274
|
+
| POST | `/api/pi/session/restore` | Restore a session from trash (body: `{ trashPath }`) |
|
|
275
|
+
| GET | `/api/pi/session-preview` | Preview session messages — `?path=` |
|
|
276
|
+
| GET | `/api/pi/trash` | List trashed sessions |
|
|
277
|
+
| DELETE | `/api/pi/trash?path=` | Permanently delete a trashed session |
|
|
242
278
|
| GET | `/api/pi/memory` | Read MEMORY.md, USER.md, failures.md |
|
|
279
|
+
| POST | `/api/pi/memory/delete-entry` | Delete a memory entry (body: `{ filename, text }`) |
|
|
280
|
+
| GET | `/api/pi/subagents` | Read subagent run history |
|
|
281
|
+
| GET | `/api/pi/check-updates` | Check pi package updates |
|
|
282
|
+
| POST | `/api/pi/apply-updates` | Apply package updates (body: `{ names }`) |
|
|
283
|
+
| POST | `/api/pi/provider-test` | Test a provider connection (body: `{ baseUrl, apiKey }`) |
|
|
284
|
+
| POST | `/api/pi/provider-models` | Fetch a provider's live model list (body: `{ baseUrl, apiKey, providerId }`) |
|
|
285
|
+
| POST | `/api/pi/model-test` | Test a model (body: `{ baseUrl, modelId, apiKey, apiType }`) |
|
|
286
|
+
|
|
287
|
+
All `-usage-range` endpoints accept `&refresh=1` to force a rescan, bypassing the 30-second session cache (the Dashboard refresh button sends this).
|
|
243
288
|
|
|
244
289
|
## 📦 Pi Package
|
|
245
290
|
|
|
@@ -263,9 +308,12 @@ Once installed, the following commands are available in your pi session:
|
|
|
263
308
|
|
|
264
309
|
| Command | Description |
|
|
265
310
|
|---------|-------------|
|
|
266
|
-
| `/pi-
|
|
267
|
-
| `/pi-
|
|
268
|
-
| `/pi-
|
|
311
|
+
| `/pi-switch start` | Launch the dashboard at `http://localhost:5173` |
|
|
312
|
+
| `/pi-switch stop` | Stop the server |
|
|
313
|
+
| `/pi-switch status` | Check if the dashboard is running |
|
|
314
|
+
| `/pi-usage` | Print a quick usage summary (today + 7 days) in the terminal — tokens / cost / requests / daily sparkline, without launching the dashboard |
|
|
315
|
+
|
|
316
|
+
The `/pi-usage` command reads `~/.pi/agent/sessions/*.jsonl` directly and aggregates today + last-7-days stats, so you can see your usage at a glance from any pi session. It mirrors what the macOS menu bar popup shows.
|
|
269
317
|
|
|
270
318
|
### Package Structure
|
|
271
319
|
|
package/README.zh-CN.md
CHANGED
|
@@ -126,17 +126,32 @@ npm run preview # 预览生产构建
|
|
|
126
126
|
|
|
127
127
|
## 🖥️ 桌面应用(Electron)
|
|
128
128
|
|
|
129
|
-
pi-web-switch
|
|
129
|
+
pi-web-switch 同样提供 **macOS 菜单栏应用**(以及 Windows/Linux 托盘应用)—— 应用驻留在顶部菜单栏,点击托盘图标即可弹出使用量摘要(今日 tokens / 成本 / 请求数 + 7 天 sparkline + Top 提供商)。完整仪表盘仍可从托盘菜单或双击图标打开。
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
132
|
# 开发模式运行(Vite 开发服务器 + Electron,支持 HMR)
|
|
133
133
|
npm run electron:dev
|
|
134
134
|
|
|
135
|
-
# 构建安装包(DMG / NSIS / AppImage)到 release/
|
|
135
|
+
# 构建安装包(macOS DMG+ZIP / Windows NSIS / Linux AppImage)到 release/
|
|
136
136
|
npm run electron:build
|
|
137
|
+
|
|
138
|
+
# 不打包预览生产构建
|
|
139
|
+
npm run electron:preview
|
|
137
140
|
```
|
|
138
141
|
|
|
139
|
-
|
|
142
|
+
### 🍹 macOS 菜单栏模式
|
|
143
|
+
|
|
144
|
+
打包后的 macOS 应用以 **纯菜单栏应用** 运行 —— 不出现在 Dock 中,`Cmd+Q` 从托盘菜单退出。通过 `electron-builder` 的 `extendInfo` 中 `LSUIElement = true` 实现。开发模式下(`electron:dev`)Dock 图标仍会显示(该 Info.plist 键仅对打包构建生效),托盘图标两种模式都可用。
|
|
145
|
+
|
|
146
|
+
| 托盘交互 | 行为 |
|
|
147
|
+
|----------|------|
|
|
148
|
+
| 点击 | 切换使用量弹窗(今日 + 7 天 tokens / 成本 / 请求数、sparkline、Top 提供商) |
|
|
149
|
+
| 右键 | 上下文菜单:打开 Dashboard、刷新使用量、退出 |
|
|
150
|
+
| 菜单 → "打开 Dashboard" | 打开完整 React Dashboard 窗口 |
|
|
151
|
+
|
|
152
|
+
弹窗可见时每 30 秒自动刷新。托盘图标是 32×32 template PNG(`build/trayIconTemplate.png`),自动适配明暗菜单栏;用 `npm run tray:icon` 重新生成。
|
|
153
|
+
|
|
154
|
+
应用图标位于 `build/`(`icon.icns` / `icon.ico` / `icon.png`);开发模式下会在运行时从 `public/icon-512.png` 设置 macOS 程序坞图标。
|
|
140
155
|
|
|
141
156
|
## 🏗️ 技术栈
|
|
142
157
|
|
|
@@ -162,9 +177,12 @@ pi-web-switch 可安装为 **pi 编码代理扩展**,在 pi 会话中直接启
|
|
|
162
177
|
|
|
163
178
|
| 命令 | 说明 |
|
|
164
179
|
|------|------|
|
|
165
|
-
| `/pi-
|
|
166
|
-
| `/pi-
|
|
167
|
-
| `/pi-
|
|
180
|
+
| `/pi-switch start` | 启动仪表盘 http://localhost:5173 |
|
|
181
|
+
| `/pi-switch stop` | 娶止服务器 |
|
|
182
|
+
| `/pi-switch status` | 查看运行状态 |
|
|
183
|
+
| `/pi-usage` | 在终端打印使用量摘要(今日 + 7 天)—— tokens / 成本 / 请求数 / 每日 sparkline,无需启动仪表盘 |
|
|
184
|
+
|
|
185
|
+
`/pi-usage` 命令直接读取 `~/.pi/agent/sessions/*.jsonl` 并聚合今日 + 最近 7 天统计,让你在任何 pi 会话中一眼看到使用量。它与 macOS 菜单栏弹窗显示的内容一致。
|
|
168
186
|
|
|
169
187
|
### 包结构
|
|
170
188
|
|
|
@@ -172,12 +190,18 @@ pi-web-switch 可安装为 **pi 编码代理扩展**,在 pi 会话中直接启
|
|
|
172
190
|
pi-web-switch/
|
|
173
191
|
├── package.json # npm 包 + pi.extensions + pi.skills
|
|
174
192
|
├── pi-package/
|
|
175
|
-
│ ├── index.ts # 扩展入口:注册 /pi-
|
|
193
|
+
│ ├── index.ts # 扩展入口:注册 /pi-switch、/pi-usage 命令
|
|
176
194
|
│ └── skills/
|
|
177
195
|
│ └── pi-web-switch/
|
|
178
196
|
│ └── SKILL.md # 使用文档
|
|
179
197
|
├── server/
|
|
180
198
|
│ └── pi-reader.ts # 服务端:读取 ~/.pi/agent/ 文件
|
|
199
|
+
├── electron/ # 桌面应用(macOS 菜单栏 / 托盘)
|
|
200
|
+
│ ├── main.ts # 主进程:托盘、弹窗、IPC、本地 API 服务器
|
|
201
|
+
│ ├── api-server.ts # 本地 HTTP 服务器(打包模式提供 dist/ + /api/pi/*)
|
|
202
|
+
│ ├── preload.ts # contextBridge:settings/auth/models/usage IPC
|
|
203
|
+
│ ├── popup.html # 菜单栏使用量弹窗
|
|
204
|
+
│ └── popup-render.ts # 弹窗渲染(今日/7 天 tokens、sparkline、Top 提供商)
|
|
181
205
|
└── src/ # React 前端
|
|
182
206
|
```
|
|
183
207
|
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
+
<title>pi-switch — pi Configuration Manager</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="./pi.svg" />
|
|
8
|
+
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
9
|
+
<link rel="manifest" href="./manifest.webmanifest" />
|
|
10
|
+
<meta name="theme-color" content="#2563eb" />
|
|
11
|
+
<meta name="description" content="Web UI for pi coding agent — configuration management, session browser, and usage dashboard" />
|
|
12
|
+
<script type="module" crossorigin src="./assets/main-eom_4fyA.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/main-M_eVNxKU.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body class="bg-gray-950 text-gray-100 antialiased">
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
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.3",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist
|
|
6
|
+
"main": "dist/index.html",
|
|
7
7
|
"description": "Web UI for pi coding agent — live configuration management, session browser, and memory viewer",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"pi-package",
|
|
@@ -26,10 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"dev": "vite",
|
|
28
28
|
"build": "tsc -b && vite build",
|
|
29
|
-
"preview": "vite preview"
|
|
30
|
-
"electron:dev": "node scripts/electron-dev.mjs",
|
|
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 ."
|
|
29
|
+
"preview": "vite preview"
|
|
33
30
|
},
|
|
34
31
|
"files": [
|
|
35
32
|
"pi-package",
|
|
@@ -68,14 +65,10 @@
|
|
|
68
65
|
"@types/react": "^19.1.2",
|
|
69
66
|
"@types/react-dom": "^19.1.2",
|
|
70
67
|
"@vitejs/plugin-react": "^4.4.1",
|
|
71
|
-
"electron": "^43.2.0",
|
|
72
|
-
"electron-builder": "^26.15.3",
|
|
73
|
-
"electron-vite": "^5.0.0",
|
|
74
68
|
"tailwindcss": "^4.1.4",
|
|
75
69
|
"typescript": "~5.8.3",
|
|
76
70
|
"undici": "^8.9.0",
|
|
77
71
|
"vite": "^6.3.2",
|
|
78
|
-
"vite-plugin-electron": "^1.1.0",
|
|
79
72
|
"vite-plugin-pwa": "^1.3.0"
|
|
80
73
|
},
|
|
81
74
|
"build": {
|
|
@@ -85,20 +78,7 @@
|
|
|
85
78
|
"output": "release"
|
|
86
79
|
},
|
|
87
80
|
"files": [
|
|
88
|
-
"dist/**/*"
|
|
89
|
-
|
|
90
|
-
],
|
|
91
|
-
"mac": {
|
|
92
|
-
"target": "dmg",
|
|
93
|
-
"icon": "build/icon.icns"
|
|
94
|
-
},
|
|
95
|
-
"win": {
|
|
96
|
-
"target": "nsis",
|
|
97
|
-
"icon": "build/icon.ico"
|
|
98
|
-
},
|
|
99
|
-
"linux": {
|
|
100
|
-
"target": "AppImage",
|
|
101
|
-
"icon": "build/icon.png"
|
|
102
|
-
}
|
|
81
|
+
"dist/**/*"
|
|
82
|
+
]
|
|
103
83
|
}
|
|
104
84
|
}
|
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
|