opencode-go-usage-tui 1.2.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,8 @@ 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, renameSync, existsSync } 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
14
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
15
15
 
16
16
  // src/i18n.ts
@@ -49,7 +49,51 @@ var ZH_T = {
49
49
  onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
50
50
  onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
51
51
  onboardConfigDesc: "First-time setup / \u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\u8D26\u53F7",
52
- 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}"
53
97
  };
54
98
  var EN_T = {
55
99
  panelTitle: "Go Usage",
@@ -86,7 +130,51 @@ var EN_T = {
86
130
  onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
87
131
  onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
88
132
  onboardConfigDesc: "First-time setup / \u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\u8D26\u53F7",
89
- 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"
90
178
  };
91
179
  var LANGS = { zh: ZH_T, en: EN_T };
92
180
  var LANG_META = [
@@ -113,27 +201,285 @@ function detectLang() {
113
201
  }
114
202
  }
115
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
+
116
462
  // src/index.tsx
117
- var VERSION = "1.2.0";
463
+ var VERSION = "1.3.0";
118
464
  var CHECK_INTERVAL = Number(process?.env?.OPENCODE_GO_CHECK_INTERVAL ?? 6e4);
119
465
  var WARN_THRESHOLD = Number(process?.env?.OPENCODE_GO_WARN_THRESHOLD ?? 0.8);
120
466
  var CONFIG_DIR = process?.env?.OPENCODE_CONFIG_DIR || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "") || process?.env?.HOME + "/.config/opencode";
121
- var CONFIG_FILE = join(CONFIG_DIR, "go-usage-config.json");
122
- var COOKIE_ENC_FILE = join(CONFIG_DIR, "go-auth-cookie.enc");
123
- var KEY_FILE = join(CONFIG_DIR, ".encryption-key");
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");
124
470
  var COOKIE_PLAIN_FILE = `${CONFIG_DIR}\\go-auth-cookie.txt`;
125
471
  var KV_PREFIX = "go-usage";
126
472
  function loadEncryptionKey() {
127
473
  try {
128
- const hex2 = readFileSync(KEY_FILE, "utf8").trim();
474
+ const hex2 = readFileSync2(KEY_FILE, "utf8").trim();
129
475
  if (hex2) return Buffer.from(hex2, "hex");
130
476
  } catch {
131
477
  }
132
478
  const key = randomBytes(32);
133
- mkdirSync(dirname(KEY_FILE), {
479
+ mkdirSync2(dirname2(KEY_FILE), {
134
480
  recursive: true
135
481
  });
136
- writeFileSync(KEY_FILE, key.toString("hex"), {
482
+ writeFileSync2(KEY_FILE, key.toString("hex"), {
137
483
  encoding: "utf8",
138
484
  mode: 384
139
485
  });
@@ -164,7 +510,7 @@ function decryptText(encJson) {
164
510
  }
165
511
  function readJsonFile(file) {
166
512
  try {
167
- return JSON.parse(readFileSync(file, "utf8"));
513
+ return JSON.parse(readFileSync2(file, "utf8"));
168
514
  } catch {
169
515
  return null;
170
516
  }
@@ -175,16 +521,26 @@ function loadConfig() {
175
521
  if (env.OPENCODE_GO_AUTH_COOKIE) {
176
522
  return {
177
523
  workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
178
- authCookie: env.OPENCODE_GO_AUTH_COOKIE
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
179
530
  };
180
531
  }
181
532
  try {
182
- if (existsSync(COOKIE_ENC_FILE)) {
183
- const cookie = decryptText(readFileSync(COOKIE_ENC_FILE, "utf8"));
533
+ if (existsSync2(COOKIE_ENC_FILE)) {
534
+ const cookie = decryptText(readFileSync2(COOKIE_ENC_FILE, "utf8"));
184
535
  if (cookie) {
185
536
  return {
186
537
  workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
187
- authCookie: cookie
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
188
544
  };
189
545
  }
190
546
  }
@@ -192,38 +548,48 @@ function loadConfig() {
192
548
  }
193
549
  const legacyCookie = fileCfg.auth_cookie || fileCfg.cookie || (() => {
194
550
  try {
195
- return readFileSync(COOKIE_PLAIN_FILE, "utf8").trim();
551
+ return readFileSync2(COOKIE_PLAIN_FILE, "utf8").trim();
196
552
  } catch {
197
553
  return "";
198
554
  }
199
555
  })();
200
556
  if (legacyCookie) {
201
557
  const enc = encryptText(legacyCookie);
202
- mkdirSync(CONFIG_DIR, {
558
+ mkdirSync2(CONFIG_DIR, {
203
559
  recursive: true
204
560
  });
205
- writeFileSync(COOKIE_ENC_FILE, enc, "utf8");
561
+ writeFileSync2(COOKIE_ENC_FILE, enc, "utf8");
206
562
  const cleanCfg = {
207
563
  ...fileCfg
208
564
  };
209
565
  delete cleanCfg.cookie;
210
566
  delete cleanCfg.auth_cookie;
211
567
  try {
212
- writeFileSync(CONFIG_FILE, JSON.stringify(cleanCfg, null, 2), "utf8");
568
+ writeFileSync2(CONFIG_FILE, JSON.stringify(cleanCfg, null, 2), "utf8");
213
569
  } catch {
214
570
  }
215
571
  try {
216
- if (existsSync(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
572
+ if (existsSync2(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
217
573
  } catch {
218
574
  }
219
575
  return {
220
576
  workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
221
- authCookie: legacyCookie
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
222
583
  };
223
584
  }
224
585
  return {
225
586
  workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
226
- authCookie: ""
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
227
593
  };
228
594
  }
229
595
  function saveConfig(patch) {
@@ -231,34 +597,44 @@ function saveConfig(patch) {
231
597
  if (patch.workspace_id !== void 0) fileCfg.workspace_id = patch.workspace_id;
232
598
  if (patch.cookie !== void 0) {
233
599
  if (patch.cookie) {
234
- mkdirSync(CONFIG_DIR, {
600
+ mkdirSync2(CONFIG_DIR, {
235
601
  recursive: true
236
602
  });
237
- writeFileSync(COOKIE_ENC_FILE, encryptText(patch.cookie), "utf8");
603
+ writeFileSync2(COOKIE_ENC_FILE, encryptText(patch.cookie), "utf8");
238
604
  delete fileCfg.cookie;
239
605
  delete fileCfg.auth_cookie;
240
606
  try {
241
- if (existsSync(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
607
+ if (existsSync2(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
242
608
  } catch {
243
609
  }
244
610
  } else {
245
611
  try {
246
- if (existsSync(COOKIE_ENC_FILE)) renameSync(COOKIE_ENC_FILE, COOKIE_ENC_FILE + ".bak");
612
+ if (existsSync2(COOKIE_ENC_FILE)) renameSync(COOKIE_ENC_FILE, COOKIE_ENC_FILE + ".bak");
247
613
  } catch {
248
614
  }
249
615
  }
250
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;
251
626
  try {
252
- mkdirSync(dirname(CONFIG_FILE), {
627
+ mkdirSync2(dirname2(CONFIG_FILE), {
253
628
  recursive: true
254
629
  });
255
- writeFileSync(CONFIG_FILE, JSON.stringify(fileCfg, null, 2), "utf8");
630
+ writeFileSync2(CONFIG_FILE, JSON.stringify(fileCfg, null, 2), "utf8");
256
631
  } catch {
257
632
  }
258
633
  }
259
634
  var [configTick, setConfigTick] = createSignal(0);
260
635
  var [langCode, setLangCode] = createSignal(detectLang());
261
636
  var t = createT(() => langCode());
637
+ var [sharedPricing, setSharedPricing] = createSignal(null);
262
638
  function kvTryGet(api, key) {
263
639
  try {
264
640
  return api.kv.get(key);
@@ -272,7 +648,8 @@ function kvTrySet(api, key, val) {
272
648
  } catch {
273
649
  }
274
650
  }
275
- async function fetchUsage(workspaceId, authCookie) {
651
+ var REQUEST_TIMEOUT = 1e4;
652
+ async function fetchUsage(workspaceId, authCookie, signal) {
276
653
  if (!workspaceId || !authCookie) return {
277
654
  error: "not_configured"
278
655
  };
@@ -299,7 +676,8 @@ async function fetchUsage(workspaceId, authCookie) {
299
676
  },
300
677
  f: 31,
301
678
  m: []
302
- })
679
+ }),
680
+ signal
303
681
  });
304
682
  if (!res.ok) {
305
683
  if (res.status === 302) return {
@@ -389,36 +767,70 @@ function GoUsagePanel(props) {
389
767
  const [configured, setConfigured] = createSignal(true);
390
768
  const [lastUpdated, setLastUpdated] = createSignal("");
391
769
  const [panelWidth, setPanelWidth] = createSignal(24);
770
+ const [cfg, setCfg] = createSignal(loadConfig());
771
+ createEffect(() => {
772
+ void configTick();
773
+ setCfg(loadConfig());
774
+ });
392
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
+ }
393
790
  async function refresh() {
394
- const cfg = loadConfig();
395
- if (!cfg.workspaceId || !cfg.authCookie) {
791
+ if (isRefreshing) return;
792
+ const cfg2 = loadConfig();
793
+ if (!cfg2.workspaceId || !cfg2.authCookie) {
396
794
  setConfigured(false);
397
795
  setUsage(null);
398
796
  setError("");
399
797
  return;
400
798
  }
401
799
  setConfigured(true);
402
- const u = await fetchUsage(cfg.workspaceId, cfg.authCookie);
403
- if (u === null) {
404
- setError(t("queryFailed"));
405
- return;
406
- }
407
- if (u.error === "cookie_expired") {
408
- setError(t("cookieExpired"));
409
- return;
410
- }
411
- if (u.error === "not_configured") {
412
- setConfigured(false);
413
- 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;
414
829
  }
415
- setUsage(u);
416
- setLastUpdated((/* @__PURE__ */ new Date()).toLocaleTimeString());
417
- setError("");
418
830
  }
419
831
  onMount(() => {
420
832
  refresh();
421
- const timer = setInterval(refresh, CHECK_INTERVAL);
833
+ const timer = setInterval(refresh, loadConfig().refreshIntervalMs);
422
834
  const stopWatch = createEffect(() => {
423
835
  void configTick();
424
836
  refresh();
@@ -426,6 +838,11 @@ function GoUsagePanel(props) {
426
838
  onCleanup(() => {
427
839
  clearInterval(timer);
428
840
  stopWatch();
841
+ cleanupPendingRequest();
842
+ if (resizeTimeout2) {
843
+ clearTimeout(resizeTimeout2);
844
+ resizeTimeout2 = null;
845
+ }
429
846
  });
430
847
  });
431
848
  const [pal, setPal] = createSignal({
@@ -505,8 +922,11 @@ function GoUsagePanel(props) {
505
922
  _$setProp(_el$1, "flexDirection", "column");
506
923
  _$setProp(_el$1, "gap", 0);
507
924
  _$setProp(_el$1, "onSizeChange", () => {
508
- const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
509
- 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);
510
930
  });
511
931
  _$insertNode(_el$10, _el$11);
512
932
  _$insertNode(_el$10, _el$12);
@@ -647,6 +1067,250 @@ function GoUsagePanel(props) {
647
1067
  return _el$1;
648
1068
  })();
649
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
+ }
650
1314
  function createSidebarSlot(api) {
651
1315
  return {
652
1316
  order: 60,
@@ -662,15 +1326,30 @@ function createSidebarSlot(api) {
662
1326
  }
663
1327
  };
664
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
+ }
665
1344
  function runConfigDialog(api, dialog) {
666
1345
  dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
667
1346
  get title() {
668
1347
  return t("wsPromptTitle");
669
1348
  },
670
1349
  description: () => (() => {
671
- var _el$30 = _$createElement("text");
672
- _$insert(_el$30, () => t("wsPromptDesc"));
673
- return _el$30;
1350
+ var _el$47 = _$createElement("text");
1351
+ _$insert(_el$47, () => t("wsPromptDesc"));
1352
+ return _el$47;
674
1353
  })(),
675
1354
  get placeholder() {
676
1355
  return t("wsPlaceholder");
@@ -689,9 +1368,9 @@ function runConfigDialog(api, dialog) {
689
1368
  return t("cookiePromptTitle");
690
1369
  },
691
1370
  description: () => (() => {
692
- var _el$31 = _$createElement("text");
693
- _$insert(_el$31, () => t("cookiePromptDesc"));
694
- return _el$31;
1371
+ var _el$48 = _$createElement("text");
1372
+ _$insert(_el$48, () => t("cookiePromptDesc"));
1373
+ return _el$48;
695
1374
  })(),
696
1375
  get placeholder() {
697
1376
  return t("cookiePlaceholder");
@@ -746,8 +1425,161 @@ function runLangDialog(api, dialog) {
746
1425
  onCancel: () => dialog?.clear()
747
1426
  }));
748
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
+ }
749
1580
  var tui = async (api) => {
750
1581
  api.slots.register(createSidebarSlot(api));
1582
+ api.slots.register(createPricingSlot(api));
751
1583
  const savedLang = kvTryGet(api, `${KV_PREFIX}.lang`);
752
1584
  if (savedLang === "zh" || savedLang === "en") setLangCode(savedLang);
753
1585
  else setLangCode(detectLang());
@@ -777,6 +1609,14 @@ var tui = async (api) => {
777
1609
  name: "go-lang"
778
1610
  },
779
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)
780
1620
  }]);
781
1621
  };
782
1622
  var mod = {