@raingor/pi-web-switch 0.4.0 → 0.4.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/package.json +5 -1
- package/public/sw.js +28 -5
- package/server/agent-session-manager.ts +827 -0
- package/server/chat-api-plugin.ts +488 -0
- package/server/pi-reader.ts +242 -1
- package/src/App.tsx +2 -0
- package/src/components/chat/ChatInput.tsx +863 -0
- package/src/components/chat/ChatPage.tsx +617 -0
- package/src/components/chat/ChatWindow.tsx +338 -0
- package/src/components/chat/MessageView.tsx +595 -0
- package/src/components/dashboard/DashboardPage.tsx +45 -5
- package/src/components/layout/AppShell.tsx +12 -5
- package/src/components/layout/Sidebar.tsx +2 -0
- package/src/components/providers/ProvidersModelsPage.tsx +28 -10
- package/src/components/sessions/SessionsPage.tsx +466 -148
- package/src/hooks/useAgentSession.ts +1104 -0
- package/src/index.css +24 -0
- package/src/lib/translations/en.ts +69 -0
- package/src/lib/translations/ja.ts +69 -0
- package/src/lib/translations/zh-CN.ts +69 -0
- package/src/lib/translations/zh-TW.ts +69 -0
- package/src/main.tsx +4 -2
- package/src/store/config-store.ts +41 -0
- package/src/types/chat.ts +217 -0
- package/vite.config.ts +141 -0
package/server/pi-reader.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, existsSync, statSync, unlinkSync, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
2
|
+
import { homedir, platform } from "os";
|
|
3
3
|
import { join, resolve, dirname, relative, sep } from "path";
|
|
4
4
|
import { spawnSync } from "child_process";
|
|
5
5
|
|
|
6
6
|
const PI_DIR = join(homedir(), ".pi", "agent");
|
|
7
7
|
|
|
8
|
+
// ─── Cindy Pi-Agent Sessions ───────────────────────────
|
|
9
|
+
// When Cindy (the AI assistant) delegates to a pi coding agent, sessions
|
|
10
|
+
// are stored under its own data directory instead of ~/.pi/agent/sessions.
|
|
11
|
+
|
|
12
|
+
function getCindySessionsDir(): string {
|
|
13
|
+
const home = homedir();
|
|
14
|
+
if (platform() === "darwin") {
|
|
15
|
+
return join(home, "Library", "Application Support", "Cindy", "pi-agent-home", "sessions");
|
|
16
|
+
}
|
|
17
|
+
// Linux / Windows fallback
|
|
18
|
+
return join(home, ".config", "cindy", "pi-agent-home", "sessions");
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
// ─── Config File Paths ───────────────────────────────────
|
|
9
22
|
|
|
10
23
|
function piPath(filename: string): string {
|
|
@@ -185,6 +198,234 @@ export function readAllUsage(): UsageRecord[] {
|
|
|
185
198
|
return allRecords;
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
// ─── Cindy Pi-Agent Usage ──────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read usage records from Cindy's pi-agent sessions.
|
|
205
|
+
* Cindy stores pi-agent sessions in its own data directory
|
|
206
|
+
* (~/Library/Application Support/Cindy/pi-agent-home/sessions/)
|
|
207
|
+
* rather than ~/.pi/agent/sessions/. The JSONL format is identical,
|
|
208
|
+
* so we reuse the same parseSessionFile() function.
|
|
209
|
+
*/
|
|
210
|
+
export function readCindyUsage(): UsageRecord[] {
|
|
211
|
+
const allRecords: UsageRecord[] = [];
|
|
212
|
+
const cindyDir = getCindySessionsDir();
|
|
213
|
+
|
|
214
|
+
if (!existsSync(cindyDir)) return allRecords;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// Cindy's sessions are flat (no subdirectory per project)
|
|
218
|
+
const files = readdirSync(cindyDir).filter((f) => f.endsWith(".jsonl"));
|
|
219
|
+
for (const file of files) {
|
|
220
|
+
const filePath = join(cindyDir, file);
|
|
221
|
+
if (!statSync(filePath).isFile()) continue;
|
|
222
|
+
const records = parseSessionFile(filePath);
|
|
223
|
+
allRecords.push(...records);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// skip unreadable directory
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort by date ascending
|
|
230
|
+
allRecords.sort((a, b) => a.date.localeCompare(b.date));
|
|
231
|
+
return allRecords;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── Claude Usage (from Cindy SQLite) ──────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Find all Cindy SQLite database files that may contain usage data.
|
|
238
|
+
* Cindy stores session/usage data in cindy-cms*.db files under its
|
|
239
|
+
* application support directory.
|
|
240
|
+
*/
|
|
241
|
+
function getCindyDbPaths(): string[] {
|
|
242
|
+
const home = homedir();
|
|
243
|
+
let cindyAppDir: string;
|
|
244
|
+
if (platform() === "darwin") {
|
|
245
|
+
cindyAppDir = join(home, "Library", "Application Support", "Cindy");
|
|
246
|
+
} else if (platform() === "win32") {
|
|
247
|
+
cindyAppDir = join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Cindy");
|
|
248
|
+
} else {
|
|
249
|
+
cindyAppDir = join(home, ".config", "Cindy");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!existsSync(cindyAppDir)) return [];
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
return readdirSync(cindyAppDir)
|
|
256
|
+
.filter((f) => f.startsWith("cindy-cms") && f.endsWith(".db"))
|
|
257
|
+
.map((f) => join(cindyAppDir, f));
|
|
258
|
+
} catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read Claude usage records from Cindy's daily_model_usage table.
|
|
265
|
+
* Cindy tracks every agent_kind (claude-code, pi, codex) in the same
|
|
266
|
+
* table; we filter to agent_kind = 'claude-code' for Claude stats.
|
|
267
|
+
*/
|
|
268
|
+
export function readClaudeUsage(): UsageRecord[] {
|
|
269
|
+
const allRecords: UsageRecord[] = [];
|
|
270
|
+
const dbPaths = getCindyDbPaths();
|
|
271
|
+
if (dbPaths.length === 0) return allRecords;
|
|
272
|
+
|
|
273
|
+
for (const dbPath of dbPaths) {
|
|
274
|
+
try {
|
|
275
|
+
const query = "SELECT day, model, cost_usd, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens FROM daily_model_usage WHERE agent_kind = 'claude-code' ORDER BY day";
|
|
276
|
+
const result = spawnSync("sqlite3", [dbPath, "-json", query], {
|
|
277
|
+
encoding: "utf8",
|
|
278
|
+
timeout: 10000,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (result.status !== 0) continue;
|
|
282
|
+
const output = result.stdout?.trim();
|
|
283
|
+
if (!output) continue;
|
|
284
|
+
|
|
285
|
+
const rows = JSON.parse(output) as Array<{
|
|
286
|
+
day: string;
|
|
287
|
+
model: string;
|
|
288
|
+
cost_usd: number;
|
|
289
|
+
input_tokens: number;
|
|
290
|
+
output_tokens: number;
|
|
291
|
+
cache_read_tokens: number;
|
|
292
|
+
cache_create_tokens: number;
|
|
293
|
+
}>;
|
|
294
|
+
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
allRecords.push({
|
|
297
|
+
date: row.day,
|
|
298
|
+
providerId: "claude",
|
|
299
|
+
modelId: row.model,
|
|
300
|
+
inputTokens: row.input_tokens ?? 0,
|
|
301
|
+
outputTokens: row.output_tokens ?? 0,
|
|
302
|
+
cacheReadTokens: row.cache_read_tokens ?? 0,
|
|
303
|
+
cacheWriteTokens: row.cache_create_tokens ?? 0,
|
|
304
|
+
requests: 1,
|
|
305
|
+
cost: row.cost_usd ?? 0,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// skip this db
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Sort by date ascending
|
|
314
|
+
allRecords.sort((a, b) => a.date.localeCompare(b.date));
|
|
315
|
+
return allRecords;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Read Codex usage records from Cindy's daily_model_usage table.
|
|
320
|
+
* Filters to agent_kind = 'codex'.
|
|
321
|
+
*/
|
|
322
|
+
export function readCodexUsage(): UsageRecord[] {
|
|
323
|
+
const allRecords: UsageRecord[] = [];
|
|
324
|
+
const dbPaths = getCindyDbPaths();
|
|
325
|
+
if (dbPaths.length === 0) return allRecords;
|
|
326
|
+
|
|
327
|
+
for (const dbPath of dbPaths) {
|
|
328
|
+
try {
|
|
329
|
+
const query = "SELECT day, model, cost_usd, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens FROM daily_model_usage WHERE agent_kind = 'codex' ORDER BY day";
|
|
330
|
+
const result = spawnSync("sqlite3", [dbPath, "-json", query], {
|
|
331
|
+
encoding: "utf8",
|
|
332
|
+
timeout: 10000,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (result.status !== 0) continue;
|
|
336
|
+
const output = result.stdout?.trim();
|
|
337
|
+
if (!output) continue;
|
|
338
|
+
|
|
339
|
+
const rows = JSON.parse(output) as Array<{
|
|
340
|
+
day: string;
|
|
341
|
+
model: string;
|
|
342
|
+
cost_usd: number;
|
|
343
|
+
input_tokens: number;
|
|
344
|
+
output_tokens: number;
|
|
345
|
+
cache_read_tokens: number;
|
|
346
|
+
cache_create_tokens: number;
|
|
347
|
+
}>;
|
|
348
|
+
|
|
349
|
+
for (const row of rows) {
|
|
350
|
+
allRecords.push({
|
|
351
|
+
date: row.day,
|
|
352
|
+
providerId: "codex",
|
|
353
|
+
modelId: row.model,
|
|
354
|
+
inputTokens: row.input_tokens ?? 0,
|
|
355
|
+
outputTokens: row.output_tokens ?? 0,
|
|
356
|
+
cacheReadTokens: row.cache_read_tokens ?? 0,
|
|
357
|
+
cacheWriteTokens: row.cache_create_tokens ?? 0,
|
|
358
|
+
requests: 1,
|
|
359
|
+
cost: row.cost_usd ?? 0,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
// skip this db
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
allRecords.sort((a, b) => a.date.localeCompare(b.date));
|
|
368
|
+
return allRecords;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Combined All Sources ──────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Combine usage from all sources: local pi, Cindy pi-agent, Claude, Codex.
|
|
375
|
+
* Used for the "All" tab that shows everything in one view.
|
|
376
|
+
*/
|
|
377
|
+
export function readAllCombinedUsage(): UsageRecord[] {
|
|
378
|
+
const all: UsageRecord[] = [
|
|
379
|
+
...readAllUsage(),
|
|
380
|
+
...readCindyUsage(),
|
|
381
|
+
...readClaudeUsage(),
|
|
382
|
+
...readCodexUsage(),
|
|
383
|
+
];
|
|
384
|
+
all.sort((a, b) => a.date.localeCompare(b.date));
|
|
385
|
+
return all;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Provider-Based Filtering ──────────────────────────
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Provider filter patterns. Each provider has a list of regex patterns
|
|
392
|
+
* that match against providerId and modelId to classify records.
|
|
393
|
+
*/
|
|
394
|
+
export interface ProviderFilter {
|
|
395
|
+
id: string;
|
|
396
|
+
label: string;
|
|
397
|
+
patterns: RegExp[];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export const PROVIDER_FILTERS: ProviderFilter[] = [
|
|
401
|
+
{
|
|
402
|
+
id: "opencode",
|
|
403
|
+
label: "OpenCode",
|
|
404
|
+
patterns: [/^opencode$/, /^opencode-go$/i],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: "gemini",
|
|
408
|
+
label: "Gemini",
|
|
409
|
+
patterns: [/^google$/, /gemini/i],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: "grok",
|
|
413
|
+
label: "Grok",
|
|
414
|
+
patterns: [/^xai$/, /grok/i],
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Filter usage records by provider. Matches against both providerId and modelId.
|
|
420
|
+
*/
|
|
421
|
+
export function filterByProvider(records: UsageRecord[], providerId: string): UsageRecord[] {
|
|
422
|
+
const filter = PROVIDER_FILTERS.find((f) => f.id === providerId);
|
|
423
|
+
if (!filter) return records;
|
|
424
|
+
return records.filter((r) =>
|
|
425
|
+
filter.patterns.some((p) => p.test(r.providerId) || p.test(r.modelId))
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
188
429
|
// ─── Aggregation Helpers ────────────────────────────────
|
|
189
430
|
|
|
190
431
|
export function getDailyAggregates(records: UsageRecord[]) {
|
package/src/App.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { MemoryPage } from "@/components/sessions/MemoryPage";
|
|
|
6
6
|
import { ProvidersModelsPage } from "@/components/providers/ProvidersModelsPage";
|
|
7
7
|
import { SubagentsPage } from "@/components/subagents/SubagentsPage";
|
|
8
8
|
import { SettingsPage } from "@/components/settings/SettingsPage";
|
|
9
|
+
import { ChatPage } from "@/components/chat/ChatPage";
|
|
9
10
|
|
|
10
11
|
export default function App() {
|
|
11
12
|
return (
|
|
@@ -18,6 +19,7 @@ export default function App() {
|
|
|
18
19
|
<Route path="/providers" element={<ProvidersModelsPage />} />
|
|
19
20
|
<Route path="/models" element={<ProvidersModelsPage />} />
|
|
20
21
|
<Route path="/subagents" element={<SubagentsPage />} />
|
|
22
|
+
<Route path="/chat" element={<ChatPage />} />
|
|
21
23
|
<Route path="/settings" element={<SettingsPage />} />
|
|
22
24
|
</Route>
|
|
23
25
|
</Routes>
|