opencode-go-usage-tui 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1";
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 {
@@ -314,7 +692,7 @@ async function fetchUsage(workspaceId, authCookie) {
314
692
  };
315
693
  }
316
694
  const extract = (key) => {
317
- const m = text.match(new RegExp(`${key}:[^}]*?\\{status:"ok",resetInSec:(\\d+),usagePercent:(\\d+)\\}`));
695
+ const m = text.match(new RegExp(`${key}:[^}]*?\\{status:"[^"]*",resetInSec:(\\d+),usagePercent:(\\d+)\\}`));
318
696
  if (!m) return null;
319
697
  return {
320
698
  resetInSec: Number(m[1]),
@@ -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,259 @@ 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
+ createEffect(() => {
1084
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
1085
+ setPanelWidth(Math.max(20, boxEl.width));
1086
+ }
1087
+ });
1088
+ async function refreshPricing() {
1089
+ if (priceLoading()) return;
1090
+ setPriceLoading(true);
1091
+ setPriceError("");
1092
+ priceController?.abort();
1093
+ priceController = new AbortController();
1094
+ try {
1095
+ const data = await fetchPricing();
1096
+ const store = updatePricingStore(data);
1097
+ setSharedPricing(data);
1098
+ setPricingStore(store);
1099
+ const prev = store.previous;
1100
+ if (prev && prev.fetchTime !== data.fetchTime) {
1101
+ const diff = comparePricing(prev, data);
1102
+ if (diff.hasChanges) {
1103
+ props.api.toast({
1104
+ variant: "info",
1105
+ title: t("priceChanged"),
1106
+ message: t("priceChangesSummary", {
1107
+ t: diff.summary.total,
1108
+ a: diff.summary.added,
1109
+ r: diff.summary.removed,
1110
+ p: diff.summary.priceChanges,
1111
+ l: diff.summary.limitChanges
1112
+ })
1113
+ });
1114
+ }
1115
+ }
1116
+ } catch {
1117
+ setPriceError(t("priceError"));
1118
+ } finally {
1119
+ setPriceLoading(false);
1120
+ priceController = null;
1121
+ }
1122
+ }
1123
+ function formatCount(n) {
1124
+ if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
1125
+ return String(n);
1126
+ }
1127
+ function visualPadEnd(s, w) {
1128
+ const cur = visualWidth(s);
1129
+ if (cur >= w) return s;
1130
+ return s + " ".repeat(w - cur);
1131
+ }
1132
+ function abbreviateName(name, maxLen) {
1133
+ if (visualWidth(name) <= maxLen) return name;
1134
+ let out = "";
1135
+ let w = 0;
1136
+ for (const c of name) {
1137
+ const cw = visualWidth(c);
1138
+ if (w + cw > maxLen - 1) break;
1139
+ out += c;
1140
+ w += cw;
1141
+ }
1142
+ return out + "\u2026";
1143
+ }
1144
+ function justify(label, value) {
1145
+ const outer = boxEl && typeof boxEl.width === "number" && boxEl.width > 0 ? boxEl.width : panelWidth();
1146
+ const gauge = Math.max(10, outer - 4);
1147
+ const used = visualWidth(label) + visualWidth(value);
1148
+ const gap = Math.max(1, gauge - used);
1149
+ return label + " ".repeat(gap) + value;
1150
+ }
1151
+ function renderLimitRow(m) {
1152
+ const name = abbreviateName(m.name, 18);
1153
+ const req = `${formatCount(m.limits.fiveHour)}/${formatCount(m.limits.weekly)}/${formatCount(m.limits.monthly)}`;
1154
+ return (() => {
1155
+ var _el$30 = _$createElement("text"), _el$31 = _$createElement("span");
1156
+ _$insertNode(_el$30, _el$31);
1157
+ _$insert(_el$31, () => justify(name, req));
1158
+ _$effect((_$p) => _$setProp(_el$31, "style", {
1159
+ fg: colors().text
1160
+ }, _$p));
1161
+ return _el$30;
1162
+ })();
1163
+ }
1164
+ const [pal, setPal] = createSignal({
1165
+ ...FALLBACK
1166
+ });
1167
+ createEffect(() => {
1168
+ const th = props.theme;
1169
+ const p = {
1170
+ ...FALLBACK
1171
+ };
1172
+ for (const k of Object.keys(FALLBACK)) {
1173
+ const h = hex(th?.[k]);
1174
+ if (h) p[k] = h;
1175
+ }
1176
+ setPal(p);
1177
+ });
1178
+ const colors = () => pal();
1179
+ const sep = () => {
1180
+ const outer = boxEl && typeof boxEl.width === "number" && boxEl.width > 0 ? boxEl.width : panelWidth();
1181
+ return "\u2500".repeat(Math.max(1, outer - 4));
1182
+ };
1183
+ setTimeout(() => {
1184
+ const store = loadPricingStore();
1185
+ if (store.current.models.length > 0) {
1186
+ setSharedPricing(store.current);
1187
+ setPricingStore(store);
1188
+ }
1189
+ refreshPricing();
1190
+ setInterval(() => refreshPricing(), loadConfig().pricingIntervalMs);
1191
+ }, 2e3);
1192
+ return (() => {
1193
+ 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");
1194
+ _$insertNode(_el$32, _el$33);
1195
+ var _ref$2 = boxEl;
1196
+ typeof _ref$2 === "function" ? _$use(_ref$2, _el$32) : boxEl = _el$32;
1197
+ _$setProp(_el$32, "border", true);
1198
+ _$setProp(_el$32, "paddingLeft", 1);
1199
+ _$setProp(_el$32, "paddingRight", 1);
1200
+ _$setProp(_el$32, "flexDirection", "column");
1201
+ _$setProp(_el$32, "gap", 0);
1202
+ _$setProp(_el$32, "onSizeChange", () => {
1203
+ if (resizeTimeout) clearTimeout(resizeTimeout);
1204
+ resizeTimeout = setTimeout(() => {
1205
+ const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
1206
+ setPanelWidth(w);
1207
+ }, 100);
1208
+ });
1209
+ _$insertNode(_el$33, _el$34);
1210
+ _$insertNode(_el$33, _el$35);
1211
+ _$insertNode(_el$33, _el$40);
1212
+ _$setProp(_el$33, "onMouseUp", () => setOpen((o) => !o));
1213
+ _$insert(_el$34, () => open() ? "\u25BC " : "\u25B6 ");
1214
+ _$insertNode(_el$35, _el$36);
1215
+ _$insert(_el$36, () => t("priceTitle"));
1216
+ _$insert(_el$33, _$createComponent(Show, {
1217
+ get when() {
1218
+ return sharedPricing();
1219
+ },
1220
+ get children() {
1221
+ var _el$37 = _$createElement("span"), _el$38 = _$createTextNode(` (`), _el$39 = _$createTextNode(`)`);
1222
+ _$insertNode(_el$37, _el$38);
1223
+ _$insertNode(_el$37, _el$39);
1224
+ _$insert(_el$37, () => sharedPricing().models.length, _el$39);
1225
+ _$effect((_$p) => _$setProp(_el$37, "style", {
1226
+ fg: colors().muted
1227
+ }, _$p));
1228
+ return _el$37;
1229
+ }
1230
+ }), _el$40);
1231
+ _$insert(_el$40, () => sep().slice(visualWidth((open() ? "\u25BC " : "\u25B6 ") + t("priceTitle") + (sharedPricing() ? ` (${sharedPricing().models.length})` : ""))));
1232
+ _$insert(_el$32, _$createComponent(Show, {
1233
+ get when() {
1234
+ return open();
1235
+ },
1236
+ get children() {
1237
+ return [(() => {
1238
+ var _el$41 = _$createElement("text");
1239
+ _$insert(_el$41, () => justify(t("priceModelCol"), t("priceReqs")));
1240
+ _$effect((_$p) => _$setProp(_el$41, "fg", colors().muted, _$p));
1241
+ return _el$41;
1242
+ })(), (() => {
1243
+ var _el$42 = _$createElement("text");
1244
+ _$insert(_el$42, sep);
1245
+ _$effect((_$p) => _$setProp(_el$42, "fg", colors().muted, _$p));
1246
+ return _el$42;
1247
+ })(), _$createComponent(Show, {
1248
+ get when() {
1249
+ return priceLoading() || !sharedPricing();
1250
+ },
1251
+ get fallback() {
1252
+ return _$createComponent(Show, {
1253
+ get when() {
1254
+ return sharedPricing();
1255
+ },
1256
+ get fallback() {
1257
+ return (() => {
1258
+ var _el$46 = _$createElement("text");
1259
+ _$insert(_el$46, () => t("priceLoading"));
1260
+ _$effect((_$p) => _$setProp(_el$46, "fg", colors().muted, _$p));
1261
+ return _el$46;
1262
+ })();
1263
+ },
1264
+ get children() {
1265
+ return [_$memo(() => sharedPricing().models.filter((m) => {
1266
+ return cfg().focusModels.length === 0 || cfg().focusModels.includes(m.id);
1267
+ }).map((m) => renderLimitRow(m))), (() => {
1268
+ var _el$44 = _$createElement("text");
1269
+ _$insert(_el$44, () => justify(t("priceUpdated"), new Date(sharedPricing().fetchTime).toLocaleTimeString()));
1270
+ _$effect((_$p) => _$setProp(_el$44, "fg", colors().muted, _$p));
1271
+ return _el$44;
1272
+ })(), _$createComponent(Show, {
1273
+ get when() {
1274
+ return _$memo(() => !!pricingStore())() && pricingStore().history.length > 0;
1275
+ },
1276
+ get children() {
1277
+ var _el$45 = _$createElement("text");
1278
+ _$insert(_el$45, () => t("priceChangesSummary", {
1279
+ t: pricingStore().history[0].summary.total,
1280
+ a: pricingStore().history[0].summary.added,
1281
+ r: pricingStore().history[0].summary.removed,
1282
+ p: pricingStore().history[0].summary.priceChanges,
1283
+ l: pricingStore().history[0].summary.limitChanges
1284
+ }));
1285
+ _$effect((_$p) => _$setProp(_el$45, "fg", colors().warning, _$p));
1286
+ return _el$45;
1287
+ }
1288
+ })];
1289
+ }
1290
+ });
1291
+ },
1292
+ get children() {
1293
+ var _el$43 = _$createElement("text");
1294
+ _$insert(_el$43, () => t("priceLoading"));
1295
+ _$effect((_$p) => _$setProp(_el$43, "fg", colors().muted, _$p));
1296
+ return _el$43;
1297
+ }
1298
+ })];
1299
+ }
1300
+ }), null);
1301
+ _$effect((_p$) => {
1302
+ var _v$1 = colors().border, _v$10 = {
1303
+ fg: colors().muted
1304
+ }, _v$11 = {
1305
+ fg: colors().primary
1306
+ }, _v$12 = {
1307
+ fg: colors().muted
1308
+ };
1309
+ _v$1 !== _p$.e && (_p$.e = _$setProp(_el$32, "borderColor", _v$1, _p$.e));
1310
+ _v$10 !== _p$.t && (_p$.t = _$setProp(_el$34, "style", _v$10, _p$.t));
1311
+ _v$11 !== _p$.a && (_p$.a = _$setProp(_el$35, "style", _v$11, _p$.a));
1312
+ _v$12 !== _p$.o && (_p$.o = _$setProp(_el$40, "style", _v$12, _p$.o));
1313
+ return _p$;
1314
+ }, {
1315
+ e: void 0,
1316
+ t: void 0,
1317
+ a: void 0,
1318
+ o: void 0
1319
+ });
1320
+ return _el$32;
1321
+ })();
1322
+ }
650
1323
  function createSidebarSlot(api) {
651
1324
  return {
652
1325
  order: 60,
@@ -662,15 +1335,30 @@ function createSidebarSlot(api) {
662
1335
  }
663
1336
  };
664
1337
  }
1338
+ function createPricingSlot(api) {
1339
+ return {
1340
+ order: 61,
1341
+ slots: {
1342
+ sidebar_content(ctx, _input) {
1343
+ return _$createComponent(PricePanel, {
1344
+ get theme() {
1345
+ return ctx.theme.current;
1346
+ },
1347
+ api
1348
+ });
1349
+ }
1350
+ }
1351
+ };
1352
+ }
665
1353
  function runConfigDialog(api, dialog) {
666
1354
  dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
667
1355
  get title() {
668
1356
  return t("wsPromptTitle");
669
1357
  },
670
1358
  description: () => (() => {
671
- var _el$30 = _$createElement("text");
672
- _$insert(_el$30, () => t("wsPromptDesc"));
673
- return _el$30;
1359
+ var _el$47 = _$createElement("text");
1360
+ _$insert(_el$47, () => t("wsPromptDesc"));
1361
+ return _el$47;
674
1362
  })(),
675
1363
  get placeholder() {
676
1364
  return t("wsPlaceholder");
@@ -689,9 +1377,9 @@ function runConfigDialog(api, dialog) {
689
1377
  return t("cookiePromptTitle");
690
1378
  },
691
1379
  description: () => (() => {
692
- var _el$31 = _$createElement("text");
693
- _$insert(_el$31, () => t("cookiePromptDesc"));
694
- return _el$31;
1380
+ var _el$48 = _$createElement("text");
1381
+ _$insert(_el$48, () => t("cookiePromptDesc"));
1382
+ return _el$48;
695
1383
  })(),
696
1384
  get placeholder() {
697
1385
  return t("cookiePlaceholder");
@@ -746,8 +1434,161 @@ function runLangDialog(api, dialog) {
746
1434
  onCancel: () => dialog?.clear()
747
1435
  }));
748
1436
  }
1437
+ function runSettingsDialog(api, dialog) {
1438
+ let lastSelected;
1439
+ const renderSettingsDialog = (dialog2) => {
1440
+ const cfg = loadConfig();
1441
+ const options = [{
1442
+ title: `${t("settingsFocus")} (${cfg.focusModels.length} \u4E2A)`,
1443
+ value: "focus-models"
1444
+ }, {
1445
+ title: `${t("settingsRefreshUsage")} (${Math.round(cfg.refreshIntervalMs / 1e3)}s)`,
1446
+ value: "refresh-usage"
1447
+ }, {
1448
+ title: `${t("settingsRefreshPricing")} (${Math.round(cfg.pricingIntervalMs / 1e3)}s)`,
1449
+ value: "refresh-pricing"
1450
+ }];
1451
+ dialog2?.replace(() => _$createComponent(api.ui.DialogSelect, {
1452
+ get title() {
1453
+ return t("settingsTitle");
1454
+ },
1455
+ options,
1456
+ onSelect: (opt) => {
1457
+ const value = opt.value;
1458
+ if (value === "focus-models") {
1459
+ const renderFocusDialog = (dialog3) => {
1460
+ const cur = loadConfig().focusModels;
1461
+ const allModels = sharedPricing()?.models ?? [];
1462
+ const modelOptions = allModels.map((m) => ({
1463
+ title: `${cur.includes(m.id) ? "\u2611" : "\u2610"} ${m.name} (${m.pricing.input ?? "-"}/${m.pricing.output ?? "-"})`,
1464
+ value: m.id
1465
+ }));
1466
+ const focusOptions = [...modelOptions, {
1467
+ title: `${t("focusAll")} (${cur.length} \u4E2A\u5DF2\u9009)`,
1468
+ value: "select-all"
1469
+ }, {
1470
+ title: t("focusNone"),
1471
+ value: "clear-all"
1472
+ }, {
1473
+ title: `${t("focusDone")} (${t("focusSelected", {
1474
+ n: cur.length
1475
+ })})`,
1476
+ value: "done"
1477
+ }];
1478
+ dialog3?.replace(() => _$createComponent(api.ui.DialogSelect, {
1479
+ get title() {
1480
+ return t("settingsFocus");
1481
+ },
1482
+ options: focusOptions,
1483
+ current: lastSelected,
1484
+ onSelect: (opt2) => {
1485
+ const v2 = opt2.value;
1486
+ if (v2 === "done") {
1487
+ dialog3?.clear();
1488
+ renderSettingsDialog(dialog2);
1489
+ } else if (v2 === "select-all") {
1490
+ const all = allModels.map((m) => m.id);
1491
+ saveConfig({
1492
+ focus_models: all
1493
+ });
1494
+ setConfigTick((v) => v + 1);
1495
+ api.ui.toast({
1496
+ variant: "success",
1497
+ message: t("settingsSaved")
1498
+ });
1499
+ renderFocusDialog(dialog3);
1500
+ } else if (v2 === "clear-all") {
1501
+ saveConfig({
1502
+ focus_models: []
1503
+ });
1504
+ setConfigTick((v) => v + 1);
1505
+ api.ui.toast({
1506
+ variant: "success",
1507
+ message: t("settingsSaved")
1508
+ });
1509
+ renderFocusDialog(dialog3);
1510
+ } else {
1511
+ const next = cur.includes(v2) ? cur.filter((id) => id !== v2) : [...cur, v2];
1512
+ saveConfig({
1513
+ focus_models: next
1514
+ });
1515
+ setConfigTick((v) => v + 1);
1516
+ lastSelected = v2;
1517
+ renderFocusDialog(dialog3);
1518
+ }
1519
+ },
1520
+ onCancel: () => dialog3?.clear()
1521
+ }));
1522
+ };
1523
+ renderFocusDialog(dialog2);
1524
+ } else if (value === "refresh-usage") {
1525
+ dialog2?.replace(() => _$createComponent(api.ui.DialogPrompt, {
1526
+ get title() {
1527
+ return t("settingsRefreshUsage");
1528
+ },
1529
+ description: () => (() => {
1530
+ var _el$49 = _$createElement("text");
1531
+ _$insertNode(_el$49, _$createTextNode(`\u8F93\u5165\u79D2\u6570\uFF08\u9ED8\u8BA4 60\uFF09`));
1532
+ return _el$49;
1533
+ })(),
1534
+ placeholder: "60",
1535
+ onConfirm: (val) => {
1536
+ const sec = Number(val.trim());
1537
+ if (!sec || sec <= 0) {
1538
+ dialog2?.clear();
1539
+ return;
1540
+ }
1541
+ saveConfig({
1542
+ refresh_interval_sec: sec
1543
+ });
1544
+ setConfigTick((v) => v + 1);
1545
+ api.ui.toast({
1546
+ variant: "success",
1547
+ message: t("settingsSaved")
1548
+ });
1549
+ renderSettingsDialog(dialog2);
1550
+ },
1551
+ onCancel: () => dialog2?.clear()
1552
+ }));
1553
+ } else if (value === "refresh-pricing") {
1554
+ dialog2?.replace(() => _$createComponent(api.ui.DialogPrompt, {
1555
+ get title() {
1556
+ return t("settingsRefreshPricing");
1557
+ },
1558
+ description: () => (() => {
1559
+ var _el$51 = _$createElement("text");
1560
+ _$insertNode(_el$51, _$createTextNode(`\u8F93\u5165\u79D2\u6570\uFF08\u9ED8\u8BA4 1800\uFF09`));
1561
+ return _el$51;
1562
+ })(),
1563
+ placeholder: "1800",
1564
+ onConfirm: (val) => {
1565
+ const sec = Number(val.trim());
1566
+ if (!sec || sec <= 0) {
1567
+ dialog2?.clear();
1568
+ return;
1569
+ }
1570
+ saveConfig({
1571
+ pricing_interval_sec: sec
1572
+ });
1573
+ setConfigTick((v) => v + 1);
1574
+ api.ui.toast({
1575
+ variant: "success",
1576
+ message: t("settingsSaved")
1577
+ });
1578
+ renderSettingsDialog(dialog2);
1579
+ },
1580
+ onCancel: () => dialog2?.clear()
1581
+ }));
1582
+ }
1583
+ },
1584
+ onCancel: () => dialog2?.clear()
1585
+ }));
1586
+ };
1587
+ renderSettingsDialog(dialog);
1588
+ }
749
1589
  var tui = async (api) => {
750
1590
  api.slots.register(createSidebarSlot(api));
1591
+ api.slots.register(createPricingSlot(api));
751
1592
  const savedLang = kvTryGet(api, `${KV_PREFIX}.lang`);
752
1593
  if (savedLang === "zh" || savedLang === "en") setLangCode(savedLang);
753
1594
  else setLangCode(detectLang());
@@ -777,6 +1618,14 @@ var tui = async (api) => {
777
1618
  name: "go-lang"
778
1619
  },
779
1620
  onSelect: (dialog) => runLangDialog(api, dialog)
1621
+ }, {
1622
+ title: t("settingsTitle"),
1623
+ value: "go-usage.settings",
1624
+ description: t("settingsDesc"),
1625
+ slash: {
1626
+ name: "go-settings"
1627
+ },
1628
+ onSelect: (dialog) => runSettingsDialog(api, dialog)
780
1629
  }]);
781
1630
  };
782
1631
  var mod = {