opencode-go-usage-tui 1.1.0 → 1.3.0

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/tui.js CHANGED
@@ -9,8 +9,9 @@ import { insertNode as _$insertNode } from "@opentui/solid";
9
9
  import { insert as _$insert } from "@opentui/solid";
10
10
  import { createElement as _$createElement } from "@opentui/solid";
11
11
  import { createSignal, createEffect, onMount, onCleanup, Show } from "solid-js";
12
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
- import { join, dirname } from "node:path";
12
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, renameSync, existsSync as existsSync2 } from "node:fs";
13
+ import { join as join2, dirname as dirname2 } from "node:path";
14
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
14
15
 
15
16
  // src/i18n.ts
16
17
  var ZH_T = {
@@ -48,7 +49,51 @@ var ZH_T = {
48
49
  onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
49
50
  onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
50
51
  onboardConfigDesc: "First-time setup / \u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\u8D26\u53F7",
51
- setupCanceled: "\u5DF2\u53D6\u6D88\u914D\u7F6E\uFF0C\u8F93\u5165 /go-config \u53EF\u91CD\u65B0\u8BBE\u7F6E"
52
+ setupCanceled: "\u5DF2\u53D6\u6D88\u914D\u7F6E\uFF0C\u8F93\u5165 /go-config \u53EF\u91CD\u65B0\u8BBE\u7F6E",
53
+ // ── 模型价格表 ──
54
+ priceTitle: "\u6A21\u578B\u4EF7\u683C",
55
+ priceLoading: "\u6293\u53D6\u4EF7\u683C\u4E2D...",
56
+ priceError: "\u4EF7\u683C\u6293\u53D6\u5931\u8D25",
57
+ priceUpdated: "\u66F4\u65B0\u65F6\u95F4",
58
+ priceChanged: "\u4EF7\u683C\u6709\u53D8\u5316",
59
+ priceNoChange: "\u65E0\u53D8\u5316",
60
+ priceChangesSummary: "{t} \u9879\u53D8\u5316\uFF1A{a} \u65B0\u589E / {r} \u4E0B\u67B6 / {p} \u4EF7\u683C / {l} \u9650\u989D",
61
+ priceVariantTag: "{n}\u4E2A\u6863\u4F4D",
62
+ priceInput: "\u8F93\u5165",
63
+ priceOutput: "\u8F93\u51FA",
64
+ priceCacheRead: "\u7F13\u5B58\u8BFB",
65
+ priceCacheWrite: "\u7F13\u5B58\u5199",
66
+ priceUsage: "\u6708\u9650\u989D",
67
+ priceReq5h: "5h\u8BF7\u6C42",
68
+ priceReqWeek: "\u5468\u8BF7\u6C42",
69
+ priceReqMonth: "\u6708\u8BF7\u6C42",
70
+ priceModelCol: "\u6A21\u578B",
71
+ priceReqs: "\u8BF7\u6C42\u6570(5h/\u5468/\u6708)",
72
+ priceSdk: "SDK",
73
+ priceDiffUp: "\u2191{v}",
74
+ priceDiffDown: "\u2193{v}",
75
+ priceDiffSame: "=",
76
+ priceChartTitle: "\u4EF7\u683C\u53D8\u5316\u8BB0\u5F55",
77
+ priceChartEmpty: "\u6682\u65E0\u53D8\u5316\u8BB0\u5F55",
78
+ priceChartAdded: "\u65B0\u589E: {n}",
79
+ priceChartChanged: "\u53D8\u5316: {n}",
80
+ priceChartRemoved: "\u79FB\u9664: {n}",
81
+ priceCollapse: "\u4EF7\u683C\u8868",
82
+ priceExpand: "\u4EF7\u683C\u8868",
83
+ settingsTitle: "Go \u7528\u91CF\u8BBE\u7F6E",
84
+ settingsDesc: "\u5173\u6CE8\u6A21\u578B\u3001\u5237\u65B0\u95F4\u9694\u8BBE\u7F6E",
85
+ settingsFocus: "\u5173\u6CE8\u7684\u6A21\u578B",
86
+ settingsRefresh: "\u5237\u65B0\u95F4\u9694 (\u79D2)",
87
+ settingsRefreshUsage: "\u7528\u91CF\u5237\u65B0\u95F4\u9694 (\u79D2)",
88
+ settingsRefreshPricing: "\u4EF7\u683C\u5237\u65B0\u95F4\u9694 (\u79D2)",
89
+ settingsSaved: "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
90
+ settingsCanceled: "\u5DF2\u53D6\u6D88\u8BBE\u7F6E",
91
+ focusAll: "\u5168\u90E8\u9009\u62E9",
92
+ focusNone: "\u6E05\u7A7A\u9009\u62E9",
93
+ focusModelsHint: "\u9009\u62E9\u8981\u5173\u6CE8\u7684\u6A21\u578B\uFF08\u7559\u7A7A\u663E\u793A\u5168\u90E8\uFF09",
94
+ focusDone: "\u5B8C\u6210",
95
+ focusToggle: "\u5207\u6362\u9009\u4E2D",
96
+ focusSelected: "\u5DF2\u9009 {n}"
52
97
  };
53
98
  var EN_T = {
54
99
  panelTitle: "Go Usage",
@@ -85,7 +130,51 @@ var EN_T = {
85
130
  onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
86
131
  onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
87
132
  onboardConfigDesc: "First-time setup / \u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\u8D26\u53F7",
88
- setupCanceled: "Setup canceled, run /go-config to reconfigure"
133
+ setupCanceled: "Setup canceled, run /go-config to reconfigure",
134
+ // ── 模型价格表 ──
135
+ priceTitle: "Model Pricing",
136
+ priceLoading: "Fetching pricing...",
137
+ priceError: "Pricing fetch failed",
138
+ priceUpdated: "Pricing updated",
139
+ priceChanged: "Pricing changed",
140
+ priceNoChange: "No changes",
141
+ priceChangesSummary: "{t} change(s): {a} added / {r} removed / {p} price / {l} limit",
142
+ priceVariantTag: "{n} tiers",
143
+ priceInput: "In",
144
+ priceOutput: "Out",
145
+ priceCacheRead: "Cache In",
146
+ priceCacheWrite: "Cache Wr",
147
+ priceUsage: "Usage",
148
+ priceReq5h: "5h req",
149
+ priceReqWeek: "Wk req",
150
+ priceReqMonth: "Mo req",
151
+ priceModelCol: "Model",
152
+ priceReqs: "Requests (5h/wk/mo)",
153
+ priceSdk: "SDK",
154
+ priceDiffUp: "\u2191{v}",
155
+ priceDiffDown: "\u2193{v}",
156
+ priceDiffSame: "=",
157
+ priceChartTitle: "Price change history",
158
+ priceChartEmpty: "No change history yet",
159
+ priceChartAdded: "Added: {n}",
160
+ priceChartChanged: "Changed: {n}",
161
+ priceChartRemoved: "Removed: {n}",
162
+ priceCollapse: "Pricing",
163
+ priceExpand: "Pricing",
164
+ settingsTitle: "Go Usage Settings",
165
+ settingsDesc: "Focused models, refresh interval settings",
166
+ settingsFocus: "Focused models",
167
+ settingsRefresh: "Refresh interval (sec)",
168
+ settingsRefreshUsage: "Usage refresh interval (sec)",
169
+ settingsRefreshPricing: "Pricing refresh interval (sec)",
170
+ settingsSaved: "Settings saved",
171
+ settingsCanceled: "Settings canceled",
172
+ focusAll: "Select all",
173
+ focusNone: "Clear selection",
174
+ focusModelsHint: "Select models to focus (empty shows all)",
175
+ focusDone: "Done",
176
+ focusToggle: "Toggle select",
177
+ focusSelected: "{n} selected"
89
178
  };
90
179
  var LANGS = { zh: ZH_T, en: EN_T };
91
180
  var LANG_META = [
@@ -112,56 +201,440 @@ function detectLang() {
112
201
  }
113
202
  }
114
203
 
204
+ // src/pricing.ts
205
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
206
+ import { join, dirname } from "node:path";
207
+ var DOCS_URL = "https://opencode.ai/docs/go";
208
+ var PRICING_DIR = process?.env?.OPENCODE_CONFIG_DIR || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "") || process?.env?.HOME + "/.config/opencode";
209
+ var PRICING_FILE = join(PRICING_DIR, "go-pricing-data.json");
210
+ function cleanText(html) {
211
+ return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&nbsp;/g, " ").replace(/&#x27;|&#39;/g, "'").trim();
212
+ }
213
+ function parsePrice(str) {
214
+ const s = (str || "").replace(/[$\s]/g, "");
215
+ if (!s || s === "-" || s === "null" || s === "\u2014" || s === "\u2013") return null;
216
+ const num = parseFloat(s.replace(/,/g, ""));
217
+ return isNaN(num) ? null : num;
218
+ }
219
+ function parseCount(str) {
220
+ const s = (str || "").replace(/[,\s]/g, "");
221
+ if (!s || s === "-" || s === "\u2014" || s === "\u2013") return 0;
222
+ const m = s.match(/^([\d.]+)k$/i);
223
+ if (m) return Math.round(parseFloat(m[1]) * 1e3);
224
+ const num = parseInt(s, 10);
225
+ return isNaN(num) ? 0 : num;
226
+ }
227
+ function normalizeName(name) {
228
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
229
+ }
230
+ function generateId(name) {
231
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
232
+ }
233
+ function extractTables(html) {
234
+ const tables = [];
235
+ const re = /<table[^>]*>([\s\S]*?)<\/table>/gi;
236
+ let m;
237
+ while ((m = re.exec(html)) !== null) tables.push(m[1]);
238
+ return tables;
239
+ }
240
+ function extractRows(tableHtml) {
241
+ const rows = [];
242
+ const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
243
+ let rm;
244
+ while ((rm = rowRe.exec(tableHtml)) !== null) {
245
+ const cells = [];
246
+ const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
247
+ let cm;
248
+ while ((cm = cellRe.exec(rm[1])) !== null) cells.push(cleanText(cm[1]));
249
+ if (cells.length > 0) rows.push(cells);
250
+ }
251
+ return rows;
252
+ }
253
+ function extractLinks(html) {
254
+ const links = [];
255
+ const re = /<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
256
+ let m;
257
+ while ((m = re.exec(html)) !== null) {
258
+ const href = m[1];
259
+ if (href.startsWith("http") || href.startsWith("/")) links.push(href);
260
+ }
261
+ return links;
262
+ }
263
+ async function fetchPricing() {
264
+ const res = await fetch(DOCS_URL, {
265
+ headers: {
266
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
267
+ "accept": "text/html"
268
+ },
269
+ signal: AbortSignal.timeout(15e3)
270
+ });
271
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
272
+ const html = await res.text();
273
+ const tables = extractTables(html);
274
+ if (tables.length < 4) throw new Error("Required tables not found");
275
+ const priceTable = tables[1];
276
+ const limitTable = tables[0];
277
+ const endpointTable = tables[2];
278
+ const retentionTable = tables[3];
279
+ const priceRows = extractRows(priceTable);
280
+ const limitRows = extractRows(limitTable);
281
+ const endpointRows = extractRows(endpointTable);
282
+ const retentionRows = extractRows(retentionTable);
283
+ const limitMap = /* @__PURE__ */ new Map();
284
+ for (const row of limitRows.slice(1)) {
285
+ if (row.length >= 4) limitMap.set(normalizeName(row[0]), row);
286
+ }
287
+ const endpointMap = /* @__PURE__ */ new Map();
288
+ for (const row of endpointRows.slice(1)) {
289
+ if (row.length >= 4) endpointMap.set(normalizeName(row[0]), row);
290
+ }
291
+ const retentionMap = /* @__PURE__ */ new Map();
292
+ for (const row of retentionRows.slice(1)) {
293
+ if (row.length >= 3) retentionMap.set(normalizeName(row[0]), row);
294
+ }
295
+ const priceGroupMap = /* @__PURE__ */ new Map();
296
+ for (const row of priceRows.slice(1)) {
297
+ if (row.length < 5) continue;
298
+ const rawName = row[0];
299
+ const m = rawName.match(/^(.*?)\s*[((]\s*([^(()]*?)\s*[))]\s*$/);
300
+ const baseName = m ? m[1].trim() : rawName.trim();
301
+ const variant = m ? m[2].trim() : "";
302
+ const key = normalizeName(baseName);
303
+ if (!priceGroupMap.has(key)) priceGroupMap.set(key, []);
304
+ priceGroupMap.get(key).push({ baseName, variant, row });
305
+ }
306
+ const models = [];
307
+ for (const row of limitRows.slice(1)) {
308
+ if (row.length < 4) continue;
309
+ const name = row[0];
310
+ const key = normalizeName(name);
311
+ const id = generateId(name);
312
+ const group = priceGroupMap.get(key);
313
+ const endpointRow = endpointMap.get(key);
314
+ const retentionRow = retentionMap.get(key);
315
+ const variants = (group ?? []).map((g) => ({
316
+ label: g.variant || "default",
317
+ pricing: {
318
+ input: parsePrice(g.row[1]),
319
+ output: parsePrice(g.row[2]),
320
+ cachedRead: parsePrice(g.row[3]),
321
+ cachedWrite: g.row.length > 4 ? parsePrice(g.row[4]) : null
322
+ }
323
+ }));
324
+ const allInputs = variants.map((v) => v.pricing.input).filter((v) => v !== null);
325
+ const allOutputs = variants.map((v) => v.pricing.output).filter((v) => v !== null);
326
+ const allReads = variants.map((v) => v.pricing.cachedRead).filter((v) => v !== null);
327
+ const allWrites = variants.map((v) => v.pricing.cachedWrite).filter((v) => v !== null);
328
+ models.push({
329
+ id,
330
+ name,
331
+ variants,
332
+ pricing: {
333
+ input: variants.length === 1 ? variants[0].pricing.input : allInputs.length ? Math.min(...allInputs) : null,
334
+ output: variants.length === 1 ? variants[0].pricing.output : allOutputs.length ? Math.min(...allOutputs) : null,
335
+ cachedRead: variants.length === 1 ? variants[0].pricing.cachedRead : allReads.length ? Math.min(...allReads) : null,
336
+ cachedWrite: variants.length === 1 ? variants[0].pricing.cachedWrite : allWrites.length ? Math.min(...allWrites) : null
337
+ },
338
+ limits: {
339
+ fiveHour: parseCount(row[1]),
340
+ weekly: parseCount(row[2]),
341
+ monthly: parseCount(row[3])
342
+ },
343
+ usageLimit: group && group[0].row.length > 5 ? parsePrice(group[0].row[5]) : null,
344
+ endpoint: endpointRow ? extractLinks(endpointRow.join(" "))[0] || "" : "",
345
+ sdk: endpointRow ? endpointRow[3] || "" : "",
346
+ training: retentionRow ? retentionRow[1] || "" : "",
347
+ retention: retentionRow ? retentionRow[2] || "" : ""
348
+ });
349
+ }
350
+ return { fetchTime: (/* @__PURE__ */ new Date()).toISOString(), models };
351
+ }
352
+ function comparePricing(oldData, newData) {
353
+ const changes = [];
354
+ if (!oldData || oldData.models.length === 0) {
355
+ return {
356
+ hasChanges: true,
357
+ changes: newData.models.map((m) => ({ type: "added", modelId: m.id, modelName: m.name, newValue: m })),
358
+ summary: { total: newData.models.length, added: newData.models.length, removed: 0, priceChanges: 0, limitChanges: 0 }
359
+ };
360
+ }
361
+ const oldIndex = new Map(oldData.models.map((m) => [m.id, m]));
362
+ for (const newModel of newData.models) {
363
+ const oldModel = oldIndex.get(newModel.id);
364
+ if (!oldModel) {
365
+ changes.push({ type: "added", modelId: newModel.id, modelName: newModel.name, newValue: newModel });
366
+ continue;
367
+ }
368
+ const priceChanges = [];
369
+ const limitChanges = [];
370
+ for (const field of ["input", "output", "cachedRead", "cachedWrite"]) {
371
+ if (oldModel.pricing[field] !== newModel.pricing[field]) {
372
+ priceChanges.push({
373
+ field: `pricing.${field}`,
374
+ oldValue: oldModel.pricing[field],
375
+ newValue: newModel.pricing[field],
376
+ changeType: changeType(oldModel.pricing[field], newModel.pricing[field])
377
+ });
378
+ }
379
+ }
380
+ if (priceChanges.length > 0) {
381
+ changes.push({ type: "pricing", modelId: newModel.id, modelName: newModel.name, changes: priceChanges });
382
+ }
383
+ for (const field of ["fiveHour", "weekly", "monthly"]) {
384
+ if (oldModel.limits[field] !== newModel.limits[field]) {
385
+ limitChanges.push({
386
+ field: `limits.${field}`,
387
+ oldValue: oldModel.limits[field],
388
+ newValue: newModel.limits[field],
389
+ changeType: changeType(oldModel.limits[field], newModel.limits[field])
390
+ });
391
+ }
392
+ }
393
+ if (limitChanges.length > 0) {
394
+ changes.push({ type: "limits", modelId: newModel.id, modelName: newModel.name, changes: limitChanges });
395
+ }
396
+ if (oldModel.usageLimit !== newModel.usageLimit) {
397
+ changes.push({
398
+ type: "usageLimit",
399
+ modelId: newModel.id,
400
+ modelName: newModel.name,
401
+ changes: [{
402
+ field: "usageLimit",
403
+ oldValue: oldModel.usageLimit,
404
+ newValue: newModel.usageLimit,
405
+ changeType: changeType(oldModel.usageLimit, newModel.usageLimit)
406
+ }]
407
+ });
408
+ }
409
+ oldIndex.delete(newModel.id);
410
+ }
411
+ for (const [id, oldModel] of oldIndex) {
412
+ changes.push({ type: "removed", modelId: id, modelName: oldModel.name, oldValue: oldModel });
413
+ }
414
+ const summary = { total: changes.length, added: 0, removed: 0, priceChanges: 0, limitChanges: 0 };
415
+ for (const c of changes) {
416
+ if (c.type === "added") summary.added++;
417
+ else if (c.type === "removed") summary.removed++;
418
+ else if (c.type === "pricing") summary.priceChanges++;
419
+ else if (c.type === "limits" || c.type === "usageLimit") summary.limitChanges++;
420
+ }
421
+ return { hasChanges: changes.length > 0, changes, summary };
422
+ }
423
+ function changeType(oldVal, newVal) {
424
+ if (oldVal === null && newVal !== null) return "added";
425
+ if (oldVal !== null && newVal === null) return "removed";
426
+ if (newVal !== null && oldVal !== null && newVal > oldVal) return "increase";
427
+ if (newVal !== null && oldVal !== null && newVal < oldVal) return "decrease";
428
+ return "unchanged";
429
+ }
430
+ function loadPricingStore() {
431
+ try {
432
+ const data = JSON.parse(readFileSync(PRICING_FILE, "utf8"));
433
+ if (data && Array.isArray(data.current?.models)) return data;
434
+ } catch {
435
+ }
436
+ return { lastFetch: "", current: { fetchTime: "", models: [] }, previous: null, history: [] };
437
+ }
438
+ function savePricingStore(store) {
439
+ try {
440
+ mkdirSync(dirname(PRICING_FILE), { recursive: true });
441
+ writeFileSync(PRICING_FILE, JSON.stringify(store, null, 2), "utf8");
442
+ } catch {
443
+ }
444
+ }
445
+ function updatePricingStore(newData) {
446
+ const store = loadPricingStore();
447
+ const compare = comparePricing(store.current.models.length > 0 ? store.current : null, newData);
448
+ const prev = store.current;
449
+ const newStore = {
450
+ lastFetch: newData.fetchTime,
451
+ current: newData,
452
+ previous: prev.models.length > 0 ? prev : null,
453
+ history: [
454
+ { fetchTime: newData.fetchTime, changes: compare.changes, summary: compare.summary },
455
+ ...store.history
456
+ ].slice(0, 10)
457
+ };
458
+ savePricingStore(newStore);
459
+ return newStore;
460
+ }
461
+
115
462
  // src/index.tsx
116
- var VERSION = "1.1.0";
463
+ var VERSION = "1.3.0";
117
464
  var CHECK_INTERVAL = Number(process?.env?.OPENCODE_GO_CHECK_INTERVAL ?? 6e4);
118
465
  var WARN_THRESHOLD = Number(process?.env?.OPENCODE_GO_WARN_THRESHOLD ?? 0.8);
119
466
  var CONFIG_DIR = process?.env?.OPENCODE_CONFIG_DIR || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "") || process?.env?.HOME + "/.config/opencode";
120
- var CONFIG_FILE = join(CONFIG_DIR, "go-usage-config.json");
467
+ var CONFIG_FILE = join2(CONFIG_DIR, "go-usage-config.json");
468
+ var COOKIE_ENC_FILE = join2(CONFIG_DIR, "go-auth-cookie.enc");
469
+ var KEY_FILE = join2(CONFIG_DIR, ".encryption-key");
470
+ var COOKIE_PLAIN_FILE = `${CONFIG_DIR}\\go-auth-cookie.txt`;
121
471
  var KV_PREFIX = "go-usage";
122
- function loadConfig() {
123
- const env = process?.env ?? {};
124
- let fileCfg = {};
472
+ function loadEncryptionKey() {
125
473
  try {
126
- fileCfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
474
+ const hex2 = readFileSync2(KEY_FILE, "utf8").trim();
475
+ if (hex2) return Buffer.from(hex2, "hex");
127
476
  } catch {
128
- fileCfg = {};
129
477
  }
130
- const cookieFile = env.OPENCODE_GO_COOKIE_FILE || fileCfg.cookie_file || `${CONFIG_DIR}\\go-auth-cookie.txt`;
131
- let fileCookie = "";
478
+ const key = randomBytes(32);
479
+ mkdirSync2(dirname2(KEY_FILE), {
480
+ recursive: true
481
+ });
482
+ writeFileSync2(KEY_FILE, key.toString("hex"), {
483
+ encoding: "utf8",
484
+ mode: 384
485
+ });
486
+ return key;
487
+ }
488
+ function encryptText(text) {
489
+ const key = loadEncryptionKey();
490
+ const iv = randomBytes(16);
491
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
492
+ const data = cipher.update(text, "utf8", "hex") + cipher.final("hex");
493
+ return JSON.stringify({
494
+ v: 1,
495
+ iv: iv.toString("hex"),
496
+ data,
497
+ tag: cipher.getAuthTag().toString("hex")
498
+ });
499
+ }
500
+ function decryptText(encJson) {
501
+ const {
502
+ iv,
503
+ data,
504
+ tag
505
+ } = JSON.parse(encJson);
506
+ const key = loadEncryptionKey();
507
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
508
+ decipher.setAuthTag(Buffer.from(tag, "hex"));
509
+ return decipher.update(data, "hex", "utf8") + decipher.final("utf8");
510
+ }
511
+ function readJsonFile(file) {
132
512
  try {
133
- let p = cookieFile;
134
- if (p === "~") p = (env.HOME || env.USERPROFILE) + "";
135
- else if (p.startsWith("~/")) p = (env.HOME || env.USERPROFILE || "") + p.slice(1);
136
- fileCookie = readFileSync(p, "utf8").trim();
513
+ return JSON.parse(readFileSync2(file, "utf8"));
514
+ } catch {
515
+ return null;
516
+ }
517
+ }
518
+ function loadConfig() {
519
+ const env = process?.env ?? {};
520
+ const fileCfg = readJsonFile(CONFIG_FILE) ?? {};
521
+ if (env.OPENCODE_GO_AUTH_COOKIE) {
522
+ return {
523
+ workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
524
+ authCookie: env.OPENCODE_GO_AUTH_COOKIE,
525
+ usagePanel: fileCfg.ui?.usage_panel ?? true,
526
+ pricePanel: fileCfg.ui?.price_panel ?? true,
527
+ focusModels: fileCfg.focus_models ?? [],
528
+ refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
529
+ pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
530
+ };
531
+ }
532
+ try {
533
+ if (existsSync2(COOKIE_ENC_FILE)) {
534
+ const cookie = decryptText(readFileSync2(COOKIE_ENC_FILE, "utf8"));
535
+ if (cookie) {
536
+ return {
537
+ workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
538
+ authCookie: cookie,
539
+ usagePanel: fileCfg.ui?.usage_panel ?? true,
540
+ pricePanel: fileCfg.ui?.price_panel ?? true,
541
+ focusModels: fileCfg.focus_models ?? [],
542
+ refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
543
+ pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
544
+ };
545
+ }
546
+ }
137
547
  } catch {
138
- fileCookie = "";
548
+ }
549
+ const legacyCookie = fileCfg.auth_cookie || fileCfg.cookie || (() => {
550
+ try {
551
+ return readFileSync2(COOKIE_PLAIN_FILE, "utf8").trim();
552
+ } catch {
553
+ return "";
554
+ }
555
+ })();
556
+ if (legacyCookie) {
557
+ const enc = encryptText(legacyCookie);
558
+ mkdirSync2(CONFIG_DIR, {
559
+ recursive: true
560
+ });
561
+ writeFileSync2(COOKIE_ENC_FILE, enc, "utf8");
562
+ const cleanCfg = {
563
+ ...fileCfg
564
+ };
565
+ delete cleanCfg.cookie;
566
+ delete cleanCfg.auth_cookie;
567
+ try {
568
+ writeFileSync2(CONFIG_FILE, JSON.stringify(cleanCfg, null, 2), "utf8");
569
+ } catch {
570
+ }
571
+ try {
572
+ if (existsSync2(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
573
+ } catch {
574
+ }
575
+ return {
576
+ workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
577
+ authCookie: legacyCookie,
578
+ usagePanel: fileCfg.ui?.usage_panel ?? true,
579
+ pricePanel: fileCfg.ui?.price_panel ?? true,
580
+ focusModels: fileCfg.focus_models ?? [],
581
+ refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
582
+ pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
583
+ };
139
584
  }
140
585
  return {
141
586
  workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
142
- authCookie: env.OPENCODE_GO_AUTH_COOKIE || fileCfg.auth_cookie || fileCfg.cookie || fileCookie
587
+ authCookie: "",
588
+ usagePanel: fileCfg.ui?.usage_panel ?? true,
589
+ pricePanel: fileCfg.ui?.price_panel ?? true,
590
+ focusModels: fileCfg.focus_models ?? [],
591
+ refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
592
+ pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
143
593
  };
144
594
  }
145
595
  function saveConfig(patch) {
146
- let fileCfg = {};
147
- try {
148
- fileCfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
149
- } catch {
150
- fileCfg = {};
151
- }
596
+ const fileCfg = readJsonFile(CONFIG_FILE) ?? {};
152
597
  if (patch.workspace_id !== void 0) fileCfg.workspace_id = patch.workspace_id;
153
- if (patch.cookie !== void 0) fileCfg.cookie = patch.cookie;
598
+ if (patch.cookie !== void 0) {
599
+ if (patch.cookie) {
600
+ mkdirSync2(CONFIG_DIR, {
601
+ recursive: true
602
+ });
603
+ writeFileSync2(COOKIE_ENC_FILE, encryptText(patch.cookie), "utf8");
604
+ delete fileCfg.cookie;
605
+ delete fileCfg.auth_cookie;
606
+ try {
607
+ if (existsSync2(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
608
+ } catch {
609
+ }
610
+ } else {
611
+ try {
612
+ if (existsSync2(COOKIE_ENC_FILE)) renameSync(COOKIE_ENC_FILE, COOKIE_ENC_FILE + ".bak");
613
+ } catch {
614
+ }
615
+ }
616
+ }
617
+ if (patch.ui !== void 0) {
618
+ fileCfg.ui = {
619
+ ...fileCfg.ui ?? {},
620
+ ...patch.ui
621
+ };
622
+ }
623
+ if (patch.focus_models !== void 0) fileCfg.focus_models = patch.focus_models;
624
+ if (patch.refresh_interval_sec !== void 0) fileCfg.refresh_interval_sec = patch.refresh_interval_sec;
625
+ if (patch.pricing_interval_sec !== void 0) fileCfg.pricing_interval_sec = patch.pricing_interval_sec;
154
626
  try {
155
- mkdirSync(dirname(CONFIG_FILE), {
627
+ mkdirSync2(dirname2(CONFIG_FILE), {
156
628
  recursive: true
157
629
  });
158
- writeFileSync(CONFIG_FILE, JSON.stringify(fileCfg, null, 2), "utf8");
630
+ writeFileSync2(CONFIG_FILE, JSON.stringify(fileCfg, null, 2), "utf8");
159
631
  } catch {
160
632
  }
161
633
  }
162
634
  var [configTick, setConfigTick] = createSignal(0);
163
635
  var [langCode, setLangCode] = createSignal(detectLang());
164
636
  var t = createT(() => langCode());
637
+ var [sharedPricing, setSharedPricing] = createSignal(null);
165
638
  function kvTryGet(api, key) {
166
639
  try {
167
640
  return api.kv.get(key);
@@ -175,7 +648,8 @@ function kvTrySet(api, key, val) {
175
648
  } catch {
176
649
  }
177
650
  }
178
- async function fetchUsage(workspaceId, authCookie) {
651
+ var REQUEST_TIMEOUT = 1e4;
652
+ async function fetchUsage(workspaceId, authCookie, signal) {
179
653
  if (!workspaceId || !authCookie) return {
180
654
  error: "not_configured"
181
655
  };
@@ -202,7 +676,8 @@ async function fetchUsage(workspaceId, authCookie) {
202
676
  },
203
677
  f: 31,
204
678
  m: []
205
- })
679
+ }),
680
+ signal
206
681
  });
207
682
  if (!res.ok) {
208
683
  if (res.status === 302) return {
@@ -292,36 +767,70 @@ function GoUsagePanel(props) {
292
767
  const [configured, setConfigured] = createSignal(true);
293
768
  const [lastUpdated, setLastUpdated] = createSignal("");
294
769
  const [panelWidth, setPanelWidth] = createSignal(24);
770
+ const [cfg, setCfg] = createSignal(loadConfig());
771
+ createEffect(() => {
772
+ void configTick();
773
+ setCfg(loadConfig());
774
+ });
295
775
  let boxEl;
776
+ let abortController = null;
777
+ let timeoutId = null;
778
+ let isRefreshing = false;
779
+ let resizeTimeout2 = null;
780
+ function cleanupPendingRequest() {
781
+ if (timeoutId) {
782
+ clearTimeout(timeoutId);
783
+ timeoutId = null;
784
+ }
785
+ if (abortController) {
786
+ abortController.abort();
787
+ abortController = null;
788
+ }
789
+ }
296
790
  async function refresh() {
297
- const cfg = loadConfig();
298
- if (!cfg.workspaceId || !cfg.authCookie) {
791
+ if (isRefreshing) return;
792
+ const cfg2 = loadConfig();
793
+ if (!cfg2.workspaceId || !cfg2.authCookie) {
299
794
  setConfigured(false);
300
795
  setUsage(null);
301
796
  setError("");
302
797
  return;
303
798
  }
304
799
  setConfigured(true);
305
- const u = await fetchUsage(cfg.workspaceId, cfg.authCookie);
306
- if (u === null) {
307
- setError(t("queryFailed"));
308
- return;
309
- }
310
- if (u.error === "cookie_expired") {
311
- setError(t("cookieExpired"));
312
- return;
313
- }
314
- if (u.error === "not_configured") {
315
- setConfigured(false);
316
- return;
800
+ cleanupPendingRequest();
801
+ abortController = new AbortController();
802
+ const signal = abortController.signal;
803
+ timeoutId = setTimeout(() => abortController?.abort(), REQUEST_TIMEOUT);
804
+ isRefreshing = true;
805
+ try {
806
+ const u = await fetchUsage(cfg2.workspaceId, cfg2.authCookie, signal);
807
+ if (signal.aborted) return;
808
+ if (u === null) {
809
+ setError(t("queryFailed"));
810
+ return;
811
+ }
812
+ if (u.error === "cookie_expired") {
813
+ setError(t("cookieExpired"));
814
+ return;
815
+ }
816
+ if (u.error === "not_configured") {
817
+ setConfigured(false);
818
+ return;
819
+ }
820
+ setUsage(u);
821
+ setLastUpdated((/* @__PURE__ */ new Date()).toLocaleTimeString());
822
+ setError("");
823
+ } finally {
824
+ if (timeoutId) {
825
+ clearTimeout(timeoutId);
826
+ timeoutId = null;
827
+ }
828
+ isRefreshing = false;
317
829
  }
318
- setUsage(u);
319
- setLastUpdated((/* @__PURE__ */ new Date()).toLocaleTimeString());
320
- setError("");
321
830
  }
322
831
  onMount(() => {
323
832
  refresh();
324
- const timer = setInterval(refresh, CHECK_INTERVAL);
833
+ const timer = setInterval(refresh, loadConfig().refreshIntervalMs);
325
834
  const stopWatch = createEffect(() => {
326
835
  void configTick();
327
836
  refresh();
@@ -329,6 +838,11 @@ function GoUsagePanel(props) {
329
838
  onCleanup(() => {
330
839
  clearInterval(timer);
331
840
  stopWatch();
841
+ cleanupPendingRequest();
842
+ if (resizeTimeout2) {
843
+ clearTimeout(resizeTimeout2);
844
+ resizeTimeout2 = null;
845
+ }
332
846
  });
333
847
  });
334
848
  const [pal, setPal] = createSignal({
@@ -408,8 +922,11 @@ function GoUsagePanel(props) {
408
922
  _$setProp(_el$1, "flexDirection", "column");
409
923
  _$setProp(_el$1, "gap", 0);
410
924
  _$setProp(_el$1, "onSizeChange", () => {
411
- const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
412
- setPanelWidth(w);
925
+ if (resizeTimeout2) clearTimeout(resizeTimeout2);
926
+ resizeTimeout2 = setTimeout(() => {
927
+ const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
928
+ setPanelWidth(w);
929
+ }, 100);
413
930
  });
414
931
  _$insertNode(_el$10, _el$11);
415
932
  _$insertNode(_el$10, _el$12);
@@ -550,6 +1067,250 @@ function GoUsagePanel(props) {
550
1067
  return _el$1;
551
1068
  })();
552
1069
  }
1070
+ function PricePanel(props) {
1071
+ const [open, setOpen] = createSignal(true);
1072
+ const [pricingStore, setPricingStore] = createSignal(null);
1073
+ const [priceError, setPriceError] = createSignal("");
1074
+ const [priceLoading, setPriceLoading] = createSignal(false);
1075
+ const [panelWidth, setPanelWidth] = createSignal(24);
1076
+ const [cfg, setCfg] = createSignal(loadConfig());
1077
+ createEffect(() => {
1078
+ void configTick();
1079
+ setCfg(loadConfig());
1080
+ });
1081
+ let boxEl;
1082
+ let priceController = null;
1083
+ async function refreshPricing() {
1084
+ if (priceLoading()) return;
1085
+ setPriceLoading(true);
1086
+ setPriceError("");
1087
+ priceController?.abort();
1088
+ priceController = new AbortController();
1089
+ try {
1090
+ const data = await fetchPricing();
1091
+ const store = updatePricingStore(data);
1092
+ setSharedPricing(data);
1093
+ setPricingStore(store);
1094
+ const prev = store.previous;
1095
+ if (prev && prev.fetchTime !== data.fetchTime) {
1096
+ const diff = comparePricing(prev, data);
1097
+ if (diff.hasChanges) {
1098
+ props.api.toast({
1099
+ variant: "info",
1100
+ title: t("priceChanged"),
1101
+ message: t("priceChangesSummary", {
1102
+ t: diff.summary.total,
1103
+ a: diff.summary.added,
1104
+ r: diff.summary.removed,
1105
+ p: diff.summary.priceChanges,
1106
+ l: diff.summary.limitChanges
1107
+ })
1108
+ });
1109
+ }
1110
+ }
1111
+ } catch {
1112
+ setPriceError(t("priceError"));
1113
+ } finally {
1114
+ setPriceLoading(false);
1115
+ priceController = null;
1116
+ }
1117
+ }
1118
+ function formatCount(n) {
1119
+ if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
1120
+ return String(n);
1121
+ }
1122
+ function visualPadEnd(s, w) {
1123
+ const cur = visualWidth(s);
1124
+ if (cur >= w) return s;
1125
+ return s + " ".repeat(w - cur);
1126
+ }
1127
+ function abbreviateName(name, maxLen) {
1128
+ if (visualWidth(name) <= maxLen) return name;
1129
+ let out = "";
1130
+ let w = 0;
1131
+ for (const c of name) {
1132
+ const cw = visualWidth(c);
1133
+ if (w + cw > maxLen - 1) break;
1134
+ out += c;
1135
+ w += cw;
1136
+ }
1137
+ return out + "\u2026";
1138
+ }
1139
+ function justify(label, value) {
1140
+ const gauge = panelWidth() - 4;
1141
+ const used = visualWidth(label) + visualWidth(value);
1142
+ const gap = Math.max(1, gauge - used);
1143
+ return label + " ".repeat(gap) + value;
1144
+ }
1145
+ function renderLimitRow(m) {
1146
+ const name = abbreviateName(m.name, 18);
1147
+ const req = `${formatCount(m.limits.fiveHour)}/${formatCount(m.limits.weekly)}/${formatCount(m.limits.monthly)}`;
1148
+ return (() => {
1149
+ var _el$30 = _$createElement("text"), _el$31 = _$createElement("span");
1150
+ _$insertNode(_el$30, _el$31);
1151
+ _$insert(_el$31, () => justify(name, req));
1152
+ _$effect((_$p) => _$setProp(_el$31, "style", {
1153
+ fg: colors().text
1154
+ }, _$p));
1155
+ return _el$30;
1156
+ })();
1157
+ }
1158
+ const [pal, setPal] = createSignal({
1159
+ ...FALLBACK
1160
+ });
1161
+ createEffect(() => {
1162
+ const th = props.theme;
1163
+ const p = {
1164
+ ...FALLBACK
1165
+ };
1166
+ for (const k of Object.keys(FALLBACK)) {
1167
+ const h = hex(th?.[k]);
1168
+ if (h) p[k] = h;
1169
+ }
1170
+ setPal(p);
1171
+ });
1172
+ const colors = () => pal();
1173
+ const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - 4));
1174
+ setTimeout(() => {
1175
+ const store = loadPricingStore();
1176
+ if (store.current.models.length > 0) {
1177
+ setSharedPricing(store.current);
1178
+ setPricingStore(store);
1179
+ }
1180
+ refreshPricing();
1181
+ setInterval(() => refreshPricing(), loadConfig().pricingIntervalMs);
1182
+ }, 2e3);
1183
+ return (() => {
1184
+ var _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$34 = _$createElement("span"), _el$35 = _$createElement("span"), _el$36 = _$createElement("b"), _el$40 = _$createElement("span");
1185
+ _$insertNode(_el$32, _el$33);
1186
+ var _ref$2 = boxEl;
1187
+ typeof _ref$2 === "function" ? _$use(_ref$2, _el$32) : boxEl = _el$32;
1188
+ _$setProp(_el$32, "border", true);
1189
+ _$setProp(_el$32, "paddingLeft", 1);
1190
+ _$setProp(_el$32, "paddingRight", 1);
1191
+ _$setProp(_el$32, "flexDirection", "column");
1192
+ _$setProp(_el$32, "gap", 0);
1193
+ _$setProp(_el$32, "onSizeChange", () => {
1194
+ if (resizeTimeout) clearTimeout(resizeTimeout);
1195
+ resizeTimeout = setTimeout(() => {
1196
+ const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
1197
+ setPanelWidth(w);
1198
+ }, 100);
1199
+ });
1200
+ _$insertNode(_el$33, _el$34);
1201
+ _$insertNode(_el$33, _el$35);
1202
+ _$insertNode(_el$33, _el$40);
1203
+ _$setProp(_el$33, "onMouseUp", () => setOpen((o) => !o));
1204
+ _$insert(_el$34, () => open() ? "\u25BC " : "\u25B6 ");
1205
+ _$insertNode(_el$35, _el$36);
1206
+ _$insert(_el$36, () => t("priceTitle"));
1207
+ _$insert(_el$33, _$createComponent(Show, {
1208
+ get when() {
1209
+ return sharedPricing();
1210
+ },
1211
+ get children() {
1212
+ var _el$37 = _$createElement("span"), _el$38 = _$createTextNode(` (`), _el$39 = _$createTextNode(`)`);
1213
+ _$insertNode(_el$37, _el$38);
1214
+ _$insertNode(_el$37, _el$39);
1215
+ _$insert(_el$37, () => sharedPricing().models.length, _el$39);
1216
+ _$effect((_$p) => _$setProp(_el$37, "style", {
1217
+ fg: colors().muted
1218
+ }, _$p));
1219
+ return _el$37;
1220
+ }
1221
+ }), _el$40);
1222
+ _$insert(_el$40, () => sep().slice(visualWidth((open() ? "\u25BC " : "\u25B6 ") + t("priceTitle") + (sharedPricing() ? ` (${sharedPricing().models.length})` : ""))));
1223
+ _$insert(_el$32, _$createComponent(Show, {
1224
+ get when() {
1225
+ return open();
1226
+ },
1227
+ get children() {
1228
+ return [(() => {
1229
+ var _el$41 = _$createElement("text");
1230
+ _$insert(_el$41, () => justify(t("priceModelCol"), t("priceReqs")));
1231
+ _$effect((_$p) => _$setProp(_el$41, "fg", colors().muted, _$p));
1232
+ return _el$41;
1233
+ })(), (() => {
1234
+ var _el$42 = _$createElement("text");
1235
+ _$insert(_el$42, sep);
1236
+ _$effect((_$p) => _$setProp(_el$42, "fg", colors().muted, _$p));
1237
+ return _el$42;
1238
+ })(), _$createComponent(Show, {
1239
+ get when() {
1240
+ return priceLoading() || !sharedPricing();
1241
+ },
1242
+ get fallback() {
1243
+ return _$createComponent(Show, {
1244
+ get when() {
1245
+ return sharedPricing();
1246
+ },
1247
+ get fallback() {
1248
+ return (() => {
1249
+ var _el$46 = _$createElement("text");
1250
+ _$insert(_el$46, () => t("priceLoading"));
1251
+ _$effect((_$p) => _$setProp(_el$46, "fg", colors().muted, _$p));
1252
+ return _el$46;
1253
+ })();
1254
+ },
1255
+ get children() {
1256
+ return [_$memo(() => sharedPricing().models.filter((m) => {
1257
+ return cfg().focusModels.length === 0 || cfg().focusModels.includes(m.id);
1258
+ }).map((m) => renderLimitRow(m))), (() => {
1259
+ var _el$44 = _$createElement("text");
1260
+ _$insert(_el$44, () => justify(t("priceUpdated"), new Date(sharedPricing().fetchTime).toLocaleTimeString()));
1261
+ _$effect((_$p) => _$setProp(_el$44, "fg", colors().muted, _$p));
1262
+ return _el$44;
1263
+ })(), _$createComponent(Show, {
1264
+ get when() {
1265
+ return _$memo(() => !!pricingStore())() && pricingStore().history.length > 0;
1266
+ },
1267
+ get children() {
1268
+ var _el$45 = _$createElement("text");
1269
+ _$insert(_el$45, () => t("priceChangesSummary", {
1270
+ t: pricingStore().history[0].summary.total,
1271
+ a: pricingStore().history[0].summary.added,
1272
+ r: pricingStore().history[0].summary.removed,
1273
+ p: pricingStore().history[0].summary.priceChanges,
1274
+ l: pricingStore().history[0].summary.limitChanges
1275
+ }));
1276
+ _$effect((_$p) => _$setProp(_el$45, "fg", colors().warning, _$p));
1277
+ return _el$45;
1278
+ }
1279
+ })];
1280
+ }
1281
+ });
1282
+ },
1283
+ get children() {
1284
+ var _el$43 = _$createElement("text");
1285
+ _$insert(_el$43, () => t("priceLoading"));
1286
+ _$effect((_$p) => _$setProp(_el$43, "fg", colors().muted, _$p));
1287
+ return _el$43;
1288
+ }
1289
+ })];
1290
+ }
1291
+ }), null);
1292
+ _$effect((_p$) => {
1293
+ var _v$1 = colors().border, _v$10 = {
1294
+ fg: colors().muted
1295
+ }, _v$11 = {
1296
+ fg: colors().primary
1297
+ }, _v$12 = {
1298
+ fg: colors().muted
1299
+ };
1300
+ _v$1 !== _p$.e && (_p$.e = _$setProp(_el$32, "borderColor", _v$1, _p$.e));
1301
+ _v$10 !== _p$.t && (_p$.t = _$setProp(_el$34, "style", _v$10, _p$.t));
1302
+ _v$11 !== _p$.a && (_p$.a = _$setProp(_el$35, "style", _v$11, _p$.a));
1303
+ _v$12 !== _p$.o && (_p$.o = _$setProp(_el$40, "style", _v$12, _p$.o));
1304
+ return _p$;
1305
+ }, {
1306
+ e: void 0,
1307
+ t: void 0,
1308
+ a: void 0,
1309
+ o: void 0
1310
+ });
1311
+ return _el$32;
1312
+ })();
1313
+ }
553
1314
  function createSidebarSlot(api) {
554
1315
  return {
555
1316
  order: 60,
@@ -565,15 +1326,30 @@ function createSidebarSlot(api) {
565
1326
  }
566
1327
  };
567
1328
  }
1329
+ function createPricingSlot(api) {
1330
+ return {
1331
+ order: 61,
1332
+ slots: {
1333
+ sidebar_content(ctx, _input) {
1334
+ return _$createComponent(PricePanel, {
1335
+ get theme() {
1336
+ return ctx.theme.current;
1337
+ },
1338
+ api
1339
+ });
1340
+ }
1341
+ }
1342
+ };
1343
+ }
568
1344
  function runConfigDialog(api, dialog) {
569
1345
  dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
570
1346
  get title() {
571
1347
  return t("wsPromptTitle");
572
1348
  },
573
1349
  description: () => (() => {
574
- var _el$30 = _$createElement("text");
575
- _$insert(_el$30, () => t("wsPromptDesc"));
576
- return _el$30;
1350
+ var _el$47 = _$createElement("text");
1351
+ _$insert(_el$47, () => t("wsPromptDesc"));
1352
+ return _el$47;
577
1353
  })(),
578
1354
  get placeholder() {
579
1355
  return t("wsPlaceholder");
@@ -592,9 +1368,9 @@ function runConfigDialog(api, dialog) {
592
1368
  return t("cookiePromptTitle");
593
1369
  },
594
1370
  description: () => (() => {
595
- var _el$31 = _$createElement("text");
596
- _$insert(_el$31, () => t("cookiePromptDesc"));
597
- return _el$31;
1371
+ var _el$48 = _$createElement("text");
1372
+ _$insert(_el$48, () => t("cookiePromptDesc"));
1373
+ return _el$48;
598
1374
  })(),
599
1375
  get placeholder() {
600
1376
  return t("cookiePlaceholder");
@@ -649,8 +1425,161 @@ function runLangDialog(api, dialog) {
649
1425
  onCancel: () => dialog?.clear()
650
1426
  }));
651
1427
  }
1428
+ function runSettingsDialog(api, dialog) {
1429
+ let lastSelected;
1430
+ const renderSettingsDialog = (dialog2) => {
1431
+ const cfg = loadConfig();
1432
+ const options = [{
1433
+ title: `${t("settingsFocus")} (${cfg.focusModels.length} \u4E2A)`,
1434
+ value: "focus-models"
1435
+ }, {
1436
+ title: `${t("settingsRefreshUsage")} (${Math.round(cfg.refreshIntervalMs / 1e3)}s)`,
1437
+ value: "refresh-usage"
1438
+ }, {
1439
+ title: `${t("settingsRefreshPricing")} (${Math.round(cfg.pricingIntervalMs / 1e3)}s)`,
1440
+ value: "refresh-pricing"
1441
+ }];
1442
+ dialog2?.replace(() => _$createComponent(api.ui.DialogSelect, {
1443
+ get title() {
1444
+ return t("settingsTitle");
1445
+ },
1446
+ options,
1447
+ onSelect: (opt) => {
1448
+ const value = opt.value;
1449
+ if (value === "focus-models") {
1450
+ const renderFocusDialog = (dialog3) => {
1451
+ const cur = loadConfig().focusModels;
1452
+ const allModels = sharedPricing()?.models ?? [];
1453
+ const modelOptions = allModels.map((m) => ({
1454
+ title: `${cur.includes(m.id) ? "\u2611" : "\u2610"} ${m.name} (${m.pricing.input ?? "-"}/${m.pricing.output ?? "-"})`,
1455
+ value: m.id
1456
+ }));
1457
+ const focusOptions = [...modelOptions, {
1458
+ title: `${t("focusAll")} (${cur.length} \u4E2A\u5DF2\u9009)`,
1459
+ value: "select-all"
1460
+ }, {
1461
+ title: t("focusNone"),
1462
+ value: "clear-all"
1463
+ }, {
1464
+ title: `${t("focusDone")} (${t("focusSelected", {
1465
+ n: cur.length
1466
+ })})`,
1467
+ value: "done"
1468
+ }];
1469
+ dialog3?.replace(() => _$createComponent(api.ui.DialogSelect, {
1470
+ get title() {
1471
+ return t("settingsFocus");
1472
+ },
1473
+ options: focusOptions,
1474
+ current: lastSelected,
1475
+ onSelect: (opt2) => {
1476
+ const v2 = opt2.value;
1477
+ if (v2 === "done") {
1478
+ dialog3?.clear();
1479
+ renderSettingsDialog(dialog2);
1480
+ } else if (v2 === "select-all") {
1481
+ const all = allModels.map((m) => m.id);
1482
+ saveConfig({
1483
+ focus_models: all
1484
+ });
1485
+ setConfigTick((v) => v + 1);
1486
+ api.ui.toast({
1487
+ variant: "success",
1488
+ message: t("settingsSaved")
1489
+ });
1490
+ renderFocusDialog(dialog3);
1491
+ } else if (v2 === "clear-all") {
1492
+ saveConfig({
1493
+ focus_models: []
1494
+ });
1495
+ setConfigTick((v) => v + 1);
1496
+ api.ui.toast({
1497
+ variant: "success",
1498
+ message: t("settingsSaved")
1499
+ });
1500
+ renderFocusDialog(dialog3);
1501
+ } else {
1502
+ const next = cur.includes(v2) ? cur.filter((id) => id !== v2) : [...cur, v2];
1503
+ saveConfig({
1504
+ focus_models: next
1505
+ });
1506
+ setConfigTick((v) => v + 1);
1507
+ lastSelected = v2;
1508
+ renderFocusDialog(dialog3);
1509
+ }
1510
+ },
1511
+ onCancel: () => dialog3?.clear()
1512
+ }));
1513
+ };
1514
+ renderFocusDialog(dialog2);
1515
+ } else if (value === "refresh-usage") {
1516
+ dialog2?.replace(() => _$createComponent(api.ui.DialogPrompt, {
1517
+ get title() {
1518
+ return t("settingsRefreshUsage");
1519
+ },
1520
+ description: () => (() => {
1521
+ var _el$49 = _$createElement("text");
1522
+ _$insertNode(_el$49, _$createTextNode(`\u8F93\u5165\u79D2\u6570\uFF08\u9ED8\u8BA4 60\uFF09`));
1523
+ return _el$49;
1524
+ })(),
1525
+ placeholder: "60",
1526
+ onConfirm: (val) => {
1527
+ const sec = Number(val.trim());
1528
+ if (!sec || sec <= 0) {
1529
+ dialog2?.clear();
1530
+ return;
1531
+ }
1532
+ saveConfig({
1533
+ refresh_interval_sec: sec
1534
+ });
1535
+ setConfigTick((v) => v + 1);
1536
+ api.ui.toast({
1537
+ variant: "success",
1538
+ message: t("settingsSaved")
1539
+ });
1540
+ renderSettingsDialog(dialog2);
1541
+ },
1542
+ onCancel: () => dialog2?.clear()
1543
+ }));
1544
+ } else if (value === "refresh-pricing") {
1545
+ dialog2?.replace(() => _$createComponent(api.ui.DialogPrompt, {
1546
+ get title() {
1547
+ return t("settingsRefreshPricing");
1548
+ },
1549
+ description: () => (() => {
1550
+ var _el$51 = _$createElement("text");
1551
+ _$insertNode(_el$51, _$createTextNode(`\u8F93\u5165\u79D2\u6570\uFF08\u9ED8\u8BA4 1800\uFF09`));
1552
+ return _el$51;
1553
+ })(),
1554
+ placeholder: "1800",
1555
+ onConfirm: (val) => {
1556
+ const sec = Number(val.trim());
1557
+ if (!sec || sec <= 0) {
1558
+ dialog2?.clear();
1559
+ return;
1560
+ }
1561
+ saveConfig({
1562
+ pricing_interval_sec: sec
1563
+ });
1564
+ setConfigTick((v) => v + 1);
1565
+ api.ui.toast({
1566
+ variant: "success",
1567
+ message: t("settingsSaved")
1568
+ });
1569
+ renderSettingsDialog(dialog2);
1570
+ },
1571
+ onCancel: () => dialog2?.clear()
1572
+ }));
1573
+ }
1574
+ },
1575
+ onCancel: () => dialog2?.clear()
1576
+ }));
1577
+ };
1578
+ renderSettingsDialog(dialog);
1579
+ }
652
1580
  var tui = async (api) => {
653
1581
  api.slots.register(createSidebarSlot(api));
1582
+ api.slots.register(createPricingSlot(api));
654
1583
  const savedLang = kvTryGet(api, `${KV_PREFIX}.lang`);
655
1584
  if (savedLang === "zh" || savedLang === "en") setLangCode(savedLang);
656
1585
  else setLangCode(detectLang());
@@ -680,6 +1609,14 @@ var tui = async (api) => {
680
1609
  name: "go-lang"
681
1610
  },
682
1611
  onSelect: (dialog) => runLangDialog(api, dialog)
1612
+ }, {
1613
+ title: t("settingsTitle"),
1614
+ value: "go-usage.settings",
1615
+ description: t("settingsDesc"),
1616
+ slash: {
1617
+ name: "go-settings"
1618
+ },
1619
+ onSelect: (dialog) => runSettingsDialog(api, dialog)
683
1620
  }]);
684
1621
  };
685
1622
  var mod = {