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/README.md +53 -7
- package/README_EN.md +53 -7
- package/build.tui.mjs +1 -1
- package/dist/tui.js +993 -56
- package/package.json +1 -1
- package/src/i18n.ts +88 -0
- package/src/index.tsx +504 -58
- package/src/pricing.ts +348 -0
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ").replace(/'|'/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.
|
|
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 =
|
|
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
|
|
123
|
-
const env = process?.env ?? {};
|
|
124
|
-
let fileCfg = {};
|
|
472
|
+
function loadEncryptionKey() {
|
|
125
473
|
try {
|
|
126
|
-
|
|
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
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
627
|
+
mkdirSync2(dirname2(CONFIG_FILE), {
|
|
156
628
|
recursive: true
|
|
157
629
|
});
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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,
|
|
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
|
-
|
|
412
|
-
|
|
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$
|
|
575
|
-
_$insert(_el$
|
|
576
|
-
return _el$
|
|
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$
|
|
596
|
-
_$insert(_el$
|
|
597
|
-
return _el$
|
|
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 = {
|