@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.
@@ -0,0 +1,217 @@
1
+ // Chat-related type definitions — ported and simplified from pi-web's lib/types.ts
2
+
3
+ export interface TextContent {
4
+ type: "text";
5
+ text: string;
6
+ }
7
+
8
+ export interface ImageContent {
9
+ type: "image";
10
+ source: {
11
+ type: "base64" | "url";
12
+ media_type?: string;
13
+ data?: string;
14
+ url?: string;
15
+ };
16
+ }
17
+
18
+ export interface ThinkingContent {
19
+ type: "thinking";
20
+ thinking: string;
21
+ deferred?: boolean;
22
+ }
23
+
24
+ export interface ToolCallContent {
25
+ type: "toolCall";
26
+ toolCallId: string;
27
+ toolName: string;
28
+ input: Record<string, unknown>;
29
+ }
30
+
31
+ export type AssistantContentBlock = TextContent | ImageContent | ThinkingContent | ToolCallContent;
32
+
33
+ export interface UserMessage {
34
+ role: "user";
35
+ content: string | (TextContent | ImageContent)[];
36
+ timestamp?: number;
37
+ }
38
+
39
+ export interface AssistantMessage {
40
+ role: "assistant";
41
+ content: AssistantContentBlock[];
42
+ model: string;
43
+ provider: string;
44
+ stopReason?: string;
45
+ errorMessage?: string;
46
+ timestamp?: number;
47
+ usage?: {
48
+ input: number;
49
+ output: number;
50
+ cacheRead: number;
51
+ cacheWrite: number;
52
+ cost: {
53
+ input: number;
54
+ output: number;
55
+ cacheRead: number;
56
+ cacheWrite: number;
57
+ total: number;
58
+ };
59
+ };
60
+ }
61
+
62
+ export interface ToolResultMessage {
63
+ role: "toolResult";
64
+ toolCallId: string;
65
+ toolName?: string;
66
+ content: (TextContent | ImageContent)[];
67
+ isError?: boolean;
68
+ details?: unknown;
69
+ timestamp?: number;
70
+ }
71
+
72
+ export interface CustomMessage {
73
+ role: "custom";
74
+ customType: string;
75
+ content: string | (TextContent | ImageContent)[];
76
+ display: boolean;
77
+ details?: unknown;
78
+ timestamp?: number;
79
+ }
80
+
81
+ export interface BashExecutionMessage {
82
+ role: "bashExecution";
83
+ command: string;
84
+ output: string;
85
+ exitCode?: number;
86
+ cancelled?: boolean;
87
+ truncated?: boolean;
88
+ fullOutputPath?: string;
89
+ excludeFromContext?: boolean;
90
+ timestamp?: number;
91
+ }
92
+
93
+ export type AgentMessage = UserMessage | AssistantMessage | ToolResultMessage | CustomMessage | BashExecutionMessage;
94
+
95
+ export interface SessionInfo {
96
+ path: string;
97
+ id: string;
98
+ cwd: string;
99
+ name?: string;
100
+ created: string;
101
+ modified: string;
102
+ messageCount: number;
103
+ firstMessage: string;
104
+ parentSessionId?: string;
105
+ projectRoot?: string;
106
+ worktreeBranch?: string;
107
+ }
108
+
109
+ export interface SessionContext {
110
+ messages: AgentMessage[];
111
+ entryIds: string[];
112
+ thinkingLevel: string;
113
+ model: { provider: string; modelId: string } | null;
114
+ }
115
+
116
+ export interface SessionTreeNode {
117
+ entry: { id: string; type: string };
118
+ children: SessionTreeNode[];
119
+ label?: string;
120
+ compressedEntryIds?: string[];
121
+ }
122
+
123
+ export interface SessionData {
124
+ sessionId: string;
125
+ filePath: string;
126
+ tree: SessionTreeNode[];
127
+ leafId: string | null;
128
+ context: SessionContext;
129
+ }
130
+
131
+ export interface ContextUsage {
132
+ percent: number | null;
133
+ contextWindow: number;
134
+ tokens: number | null;
135
+ }
136
+
137
+ export interface SessionStatsInfo {
138
+ sessionFile?: string;
139
+ sessionId: string;
140
+ sessionName?: string;
141
+ userMessages: number;
142
+ assistantMessages: number;
143
+ toolCalls: number;
144
+ toolResults: number;
145
+ totalMessages: number;
146
+ tokens: {
147
+ input: number;
148
+ output: number;
149
+ cacheRead: number;
150
+ cacheWrite: number;
151
+ total: number;
152
+ };
153
+ cost: number;
154
+ contextUsage?: ContextUsage;
155
+ }
156
+
157
+ export interface AttachedImage {
158
+ data: string;
159
+ mimeType: string;
160
+ previewUrl: string;
161
+ }
162
+
163
+ export interface ChatInputHandle {
164
+ insertText: (text: string) => void;
165
+ insertIfEmpty: (content: string) => void;
166
+ prependText: (text: string) => void;
167
+ addImages: (files: File[]) => void;
168
+ }
169
+
170
+ export interface ModelEntry {
171
+ id: string;
172
+ name: string;
173
+ provider: string;
174
+ }
175
+
176
+ export interface ModelsResponse {
177
+ models: Record<string, string>;
178
+ modelList?: ModelEntry[];
179
+ defaultModel?: { provider: string; modelId: string } | null;
180
+ thinkingLevels?: Record<string, string[]>;
181
+ modelError?: string;
182
+ }
183
+
184
+ export interface AgentStateResponse {
185
+ isStreaming?: boolean;
186
+ isPromptRunning?: boolean;
187
+ isBashRunning?: boolean;
188
+ isCompacting?: boolean;
189
+ contextUsage?: ContextUsage | null;
190
+ systemPrompt?: string;
191
+ thinkingLevel?: string;
192
+ model?: { id: string; provider: string };
193
+ }
194
+
195
+ export interface NoticeItem {
196
+ id: string;
197
+ message: string;
198
+ type: "info" | "success" | "warning" | "error";
199
+ exiting?: boolean;
200
+ }
201
+
202
+ export type AgentPhase =
203
+ | { kind: "waiting_model" }
204
+ | { kind: "running_command" }
205
+ | { kind: "running_tools"; tools: { id: string; name: string }[] }
206
+ | null;
207
+
208
+ export interface QueuedMessages {
209
+ steering: string[];
210
+ followUp: string[];
211
+ }
212
+
213
+ export interface SlashCommandInfo {
214
+ name: string;
215
+ description?: string;
216
+ source: "extension" | "prompt" | "skill";
217
+ }
package/vite.config.ts CHANGED
@@ -3,6 +3,7 @@ import react from "@vitejs/plugin-react";
3
3
  import tailwindcss from "@tailwindcss/vite";
4
4
  import path from "path";
5
5
  import type { Connect } from "vite";
6
+ import { chatApiPlugin } from "./server/chat-api-plugin";
6
7
 
7
8
  // ─── Pi Config API Plugin ───────────────────────────────
8
9
 
@@ -306,6 +307,145 @@ function piApiPlugin(): Plugin {
306
307
  return res.end(JSON.stringify(usage));
307
308
  }
308
309
 
310
+ // Handle GET /api/pi/cindy-usage-range?range=today|7d|30d|custom&from=...&to=...
311
+ if (method === "GET" && pathOnly === "/api/pi/cindy-usage-range") {
312
+ const parsedUrl = new URL(url, "http://localhost");
313
+ const range = parsedUrl.searchParams.get("range") || "today";
314
+ const fromParam = parsedUrl.searchParams.get("from") || "";
315
+ const toParam = parsedUrl.searchParams.get("to") || "";
316
+
317
+ const now = new Date();
318
+ const localDateStr = (dt: Date) =>
319
+ `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
320
+ let fromDate: string;
321
+ let toDate = localDateStr(now);
322
+
323
+ if (range === "today") {
324
+ fromDate = toDate;
325
+ } else if (range === "7d") {
326
+ const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
327
+ } else if (range === "30d") {
328
+ const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
329
+ } else if (range === "custom" && fromParam) {
330
+ fromDate = fromParam;
331
+ if (toParam) toDate = toParam;
332
+ } else {
333
+ fromDate = toDate;
334
+ }
335
+
336
+ const allRecords = pi.readCindyUsage();
337
+ const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
338
+ res.setHeader("Content-Type", "application/json");
339
+ return res.end(JSON.stringify(usage));
340
+ }
341
+
342
+ // Handle GET /api/pi/claude-usage-range?range=today|7d|30d|custom&from=...&to=...
343
+ if (method === "GET" && pathOnly === "/api/pi/claude-usage-range") {
344
+ const parsedUrl = new URL(url, "http://localhost");
345
+ const range = parsedUrl.searchParams.get("range") || "today";
346
+ const fromParam = parsedUrl.searchParams.get("from") || "";
347
+ const toParam = parsedUrl.searchParams.get("to") || "";
348
+
349
+ const now = new Date();
350
+ const localDateStr = (dt: Date) =>
351
+ `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
352
+ let fromDate: string;
353
+ let toDate = localDateStr(now);
354
+
355
+ if (range === "today") {
356
+ fromDate = toDate;
357
+ } else if (range === "7d") {
358
+ const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
359
+ } else if (range === "30d") {
360
+ const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
361
+ } else if (range === "custom" && fromParam) {
362
+ fromDate = fromParam;
363
+ if (toParam) toDate = toParam;
364
+ } else {
365
+ fromDate = toDate;
366
+ }
367
+
368
+ const allRecords = pi.readClaudeUsage();
369
+ const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
370
+ res.setHeader("Content-Type", "application/json");
371
+ return res.end(JSON.stringify(usage));
372
+ }
373
+
374
+ // Handle GET /api/pi/codex-usage-range?range=today|7d|30d|custom&from=...&to=...
375
+ if (method === "GET" && pathOnly === "/api/pi/codex-usage-range") {
376
+ const parsedUrl = new URL(url, "http://localhost");
377
+ const range = parsedUrl.searchParams.get("range") || "today";
378
+ const fromParam = parsedUrl.searchParams.get("from") || "";
379
+ const toParam = parsedUrl.searchParams.get("to") || "";
380
+
381
+ const now = new Date();
382
+ const localDateStr = (dt: Date) =>
383
+ `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
384
+ let fromDate: string;
385
+ let toDate = localDateStr(now);
386
+
387
+ if (range === "today") {
388
+ fromDate = toDate;
389
+ } else if (range === "7d") {
390
+ const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
391
+ } else if (range === "30d") {
392
+ const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
393
+ } else if (range === "custom" && fromParam) {
394
+ fromDate = fromParam;
395
+ if (toParam) toDate = toParam;
396
+ } else {
397
+ fromDate = toDate;
398
+ }
399
+
400
+ const allRecords = pi.readCodexUsage();
401
+ const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
402
+ res.setHeader("Content-Type", "application/json");
403
+ return res.end(JSON.stringify(usage));
404
+ }
405
+
406
+ // Helper: resolve date range params
407
+ const resolveDateRange = (range: string, fromParam: string, toParam: string) => {
408
+ const now = new Date();
409
+ const localDateStr = (dt: Date) =>
410
+ `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
411
+ let fromDate: string;
412
+ let toDate = localDateStr(now);
413
+ if (range === "today") fromDate = toDate;
414
+ else if (range === "7d") { const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d); }
415
+ else if (range === "30d") { const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d); }
416
+ else if (range === "custom" && fromParam) { fromDate = fromParam; if (toParam) toDate = toParam; }
417
+ else fromDate = toDate;
418
+ return { fromDate, toDate };
419
+ };
420
+
421
+ // Handle GET /api/pi/all-usage-range
422
+ if (method === "GET" && pathOnly === "/api/pi/all-usage-range") {
423
+ const parsedUrl = new URL(url, "http://localhost");
424
+ const range = parsedUrl.searchParams.get("range") || "today";
425
+ const fromParam = parsedUrl.searchParams.get("from") || "";
426
+ const toParam = parsedUrl.searchParams.get("to") || "";
427
+ const { fromDate, toDate } = resolveDateRange(range, fromParam, toParam);
428
+ const allRecords = pi.readAllCombinedUsage();
429
+ const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
430
+ res.setHeader("Content-Type", "application/json");
431
+ return res.end(JSON.stringify(usage));
432
+ }
433
+
434
+ // Handle provider-filtered endpoints: /api/pi/{provider}-usage-range
435
+ const providerMatch = pathOnly.match(/^\/api\/pi\/(opencode|gemini|grok)-usage-range$/);
436
+ if (method === "GET" && providerMatch) {
437
+ const providerId = providerMatch[1]!;
438
+ const parsedUrl = new URL(url, "http://localhost");
439
+ const range = parsedUrl.searchParams.get("range") || "today";
440
+ const fromParam = parsedUrl.searchParams.get("from") || "";
441
+ const toParam = parsedUrl.searchParams.get("to") || "";
442
+ const { fromDate, toDate } = resolveDateRange(range, fromParam, toParam);
443
+ const allRecords = pi.filterByProvider(pi.readAllCombinedUsage(), providerId);
444
+ const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
445
+ res.setHeader("Content-Type", "application/json");
446
+ return res.end(JSON.stringify(usage));
447
+ }
448
+
309
449
  const key = `${method} ${pathOnly}`;
310
450
  const handler = routes[key];
311
451
  if (handler) {
@@ -327,6 +467,7 @@ export default defineConfig({
327
467
  react(),
328
468
  tailwindcss(),
329
469
  piApiPlugin(),
470
+ chatApiPlugin(),
330
471
  ],
331
472
  resolve: {
332
473
  alias: {