opencode-go-usage-tui 1.0.1 → 1.1.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  在 OpenCode 侧边栏实时显示 OpenCode Go 额度用量(5小时/每周/每月)。
4
4
 
5
+ 支持中英双语界面(`/go-lang` 切换,首次启动自动引导选择语言)。
6
+
5
7
  ## 安装
6
8
 
7
9
  ### 方式一:OpenCode 命令安装(推荐)
@@ -79,6 +81,13 @@ Cookie 有效期约 1 年,过期后重新获取。
79
81
  - 用量超过阈值(默认 80%,可用 `OPENCODE_GO_WARN_THRESHOLD` 调整)显示为红色
80
82
  - 点击面板标题可折叠/展开
81
83
  - 配置错误时面板会提示"未配置账号",输入 `/go-config` 即可设置
84
+ - 面板标题显示当前插件版本号(如 `v1.1.0`)
85
+
86
+ ## 语言
87
+
88
+ - 首次安装启动时自动弹出语言选择,选择后若未配置账号会继续引导 `/go-config`
89
+ - 随时执行 `/go-lang` 可切换 中文 / English,语言偏好会记住
90
+ - 自动检测系统语言(中文系统默认中文,其余默认英文)
82
91
 
83
92
  ## 开发
84
93
 
package/README_EN.md ADDED
@@ -0,0 +1,103 @@
1
+ # opencode-go-usage-tui
2
+
3
+ Display your OpenCode Go quota usage (5h / weekly / monthly) in the OpenCode sidebar in real time.
4
+
5
+ Bilingual UI (Chinese / English) — switch with `/go-lang`, or pick a language during the first-run onboarding.
6
+
7
+ ## Install
8
+
9
+ ### Option 1: Install via OpenCode command (recommended)
10
+
11
+ In OpenCode, press **`Ctrl+P`** to open the command palette, search for **`install plugin`**, and enter:
12
+
13
+ ```
14
+ opencode-go-usage-tui@latest
15
+ ```
16
+
17
+ Press Enter, restart OpenCode, and the "Go Usage" panel will appear in the sidebar.
18
+
19
+ ### Option 2: npm global install
20
+
21
+ ```bash
22
+ npm install -g opencode-go-usage-tui
23
+ ```
24
+
25
+ Then add it to the `plugin` array in `~/.config/opencode/tui.json`:
26
+
27
+ ```json
28
+ {
29
+ "plugin": ["opencode-go-usage-tui@latest"]
30
+ }
31
+ ```
32
+
33
+ Restart OpenCode.
34
+
35
+ ## Configuration
36
+
37
+ ### Option 1: Slash command (recommended)
38
+
39
+ Run in the TUI:
40
+
41
+ ```
42
+ /go-config
43
+ ```
44
+
45
+ Enter two values (unique per user — get them by logging into opencode.ai):
46
+
47
+ 1. **workspace_id**: starts with `wrk_`, found in the opencode.ai console URL
48
+ 2. **auth cookie**: in your browser, F12 → Application → Cookies → opencode.ai → copy the `auth` value (starts with `Fe26.2*`)
49
+
50
+ Config is saved to `~/.config/opencode/go-usage-config.json`.
51
+
52
+ ### Option 2: Environment variables
53
+
54
+ | Variable | Description |
55
+ | -------- | ----------- |
56
+ | `OPENCODE_GO_WORKSPACE_ID` | Workspace ID |
57
+ | `OPENCODE_GO_AUTH_COOKIE` | Auth cookie value |
58
+
59
+ ### Option 3: Config file
60
+
61
+ Create `~/.config/opencode/go-usage-config.json` manually:
62
+
63
+ ```json
64
+ {
65
+ "workspace_id": "wrk_xxxxxxxxxxxx",
66
+ "cookie": "Fe26.2*..."
67
+ }
68
+ ```
69
+
70
+ ## Getting the auth cookie
71
+
72
+ 1. Log into opencode.ai in Chrome
73
+ 2. Press F12 to open DevTools → Application → Cookies → select opencode.ai
74
+ 3. Find `auth`, copy its Value
75
+
76
+ The cookie is valid for about 1 year; get a new one after it expires.
77
+
78
+ ## Usage
79
+
80
+ - The sidebar panel auto-refreshes every 60 seconds (adjustable via `OPENCODE_GO_CHECK_INTERVAL`, in milliseconds)
81
+ - Usage above the threshold (default 80%, adjustable via `OPENCODE_GO_WARN_THRESHOLD`) is shown in red
82
+ - Click the panel title to collapse/expand
83
+ - If misconfigured, the panel shows "Not configured" — run `/go-config` to set up
84
+ - The panel title shows the current plugin version (e.g. `v1.1.0`)
85
+
86
+ ## Language
87
+
88
+ - On first install, a language picker pops up automatically; if the account isn't configured yet, it continues into the `/go-config` onboarding
89
+ - Run `/go-lang` anytime to switch between 中文 / English; your preference is remembered
90
+ - Auto-detects system language (Chinese systems default to 中文, everything else defaults to English)
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ # Build
96
+ bun run build.tui.mjs
97
+ # or
98
+ npm run build
99
+ ```
100
+
101
+ ## License
102
+
103
+ MIT
package/build.tui.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  import * as esbuild from "esbuild"
2
2
  import { solidPlugin } from "esbuild-plugin-solid"
3
+ import { readFileSync } from "node:fs"
4
+
5
+ const pkg = JSON.parse(readFileSync("package.json", "utf8"))
3
6
 
4
7
  await esbuild.build({
5
8
  entryPoints: ["src/index.tsx"],
@@ -9,5 +12,6 @@ await esbuild.build({
9
12
  bundle: true,
10
13
  external: ["@opencode-ai/*", "@opentui/*", "solid-js", "node:fs", "node:path"],
11
14
  plugins: [solidPlugin({ solid: { moduleName: "@opentui/solid", generate: "universal" } })],
15
+ define: { __PLUGIN_VERSION__: JSON.stringify(pkg.version) },
12
16
  logLevel: "info",
13
17
  })
package/dist/tui.js CHANGED
@@ -11,10 +11,114 @@ import { createElement as _$createElement } from "@opentui/solid";
11
11
  import { createSignal, createEffect, onMount, onCleanup, Show } from "solid-js";
12
12
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
13
  import { join, dirname } from "node:path";
14
+
15
+ // src/i18n.ts
16
+ var ZH_T = {
17
+ panelTitle: "Go \u7528\u91CF",
18
+ rowRolling: "5\u5C0F\u65F6\u7528\u91CF",
19
+ rowWeekly: "\u6BCF\u5468\u7528\u91CF",
20
+ rowMonthly: "\u6BCF\u6708\u7528\u91CF",
21
+ weeklyShort: "\u5468",
22
+ updatedAt: "\u6700\u8FD1\u66F4\u65B0",
23
+ notConfigured: "\u672A\u914D\u7F6E\u8D26\u53F7",
24
+ notConfiguredHint: "\u8F93\u5165 /go-config \u8BBE\u7F6E",
25
+ loading: "\u52A0\u8F7D\u4E2D...",
26
+ queryFailed: "\u67E5\u8BE2\u5931\u8D25",
27
+ cookieExpired: "cookie \u5DF2\u8FC7\u671F",
28
+ reconfigureHint: "\u8F93\u5165 /go-config \u91CD\u65B0\u914D\u7F6E",
29
+ resetDone: "\u5DF2\u91CD\u7F6E",
30
+ dayHour: "{d}\u5929{h}\u5C0F\u65F6",
31
+ hourMin: "{h}\u5C0F\u65F6{m}\u5206\u949F",
32
+ minute: "{m}\u5206\u949F",
33
+ cmdTitle: "Go Usage: Configure",
34
+ cmdDesc: "\u8BBE\u7F6E OpenCode Go workspace ID \u548C auth cookie",
35
+ wsPromptTitle: "\u8F93\u5165 workspace_id",
36
+ wsPromptDesc: "\u5728 opencode.ai \u63A7\u5236\u53F0 URL \u4E2D\u83B7\u53D6\uFF08wrk_ \u5F00\u5934\uFF09",
37
+ wsPlaceholder: "wrk_xxxxxxxxxxxx",
38
+ cookiePromptTitle: "\u8F93\u5165 auth cookie",
39
+ cookiePromptDesc: "\u767B\u5F55 opencode.ai \u540E\uFF0C\u6D4F\u89C8\u5668 F12 \u2192 Application \u2192 Cookies \u2192 opencode.ai \u2192 \u590D\u5236 auth \u7684\u503C",
40
+ cookiePlaceholder: "Fe26.2*...",
41
+ configSaved: "Go \u7528\u91CF\u914D\u7F6E\u5DF2\u4FDD\u5B58",
42
+ langTitle: "\u663E\u793A\u8BED\u8A00",
43
+ langCmdTitle: "Go Usage: Language",
44
+ langCmdDesc: "\u5207\u6362\u663E\u793A\u8BED\u8A00 | Switch display language",
45
+ langSwitchedZh: "\u5DF2\u5207\u6362\u4E3A\u4E2D\u6587",
46
+ langSwitchedEn: "Switched to English",
47
+ onboardLangTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
48
+ onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
49
+ onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
50
+ 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
+ };
53
+ var EN_T = {
54
+ panelTitle: "Go Usage",
55
+ rowRolling: "5h usage",
56
+ rowWeekly: "Weekly",
57
+ rowMonthly: "Monthly",
58
+ weeklyShort: "Wk",
59
+ updatedAt: "Updated",
60
+ notConfigured: "Not configured",
61
+ notConfiguredHint: "Run /go-config to set up",
62
+ loading: "Loading...",
63
+ queryFailed: "Query failed",
64
+ cookieExpired: "Cookie expired",
65
+ reconfigureHint: "Run /go-config to reconfigure",
66
+ resetDone: "Reset",
67
+ dayHour: "{d}d {h}h",
68
+ hourMin: "{h}h {m}m",
69
+ minute: "{m}m",
70
+ cmdTitle: "Go Usage: Configure",
71
+ cmdDesc: "Set OpenCode Go workspace ID and auth cookie",
72
+ wsPromptTitle: "Enter workspace_id",
73
+ wsPromptDesc: "Get it from the opencode.ai console URL (starts with wrk_)",
74
+ wsPlaceholder: "wrk_xxxxxxxxxxxx",
75
+ cookiePromptTitle: "Enter auth cookie",
76
+ cookiePromptDesc: "After logging into opencode.ai, press F12 \u2192 Application \u2192 Cookies \u2192 opencode.ai \u2192 copy the auth value",
77
+ cookiePlaceholder: "Fe26.2*...",
78
+ configSaved: "Go usage config saved",
79
+ langTitle: "Display language",
80
+ langCmdTitle: "Go Usage: Language",
81
+ langCmdDesc: "\u5207\u6362\u663E\u793A\u8BED\u8A00 | Switch display language",
82
+ langSwitchedZh: "\u5DF2\u5207\u6362\u4E3A\u4E2D\u6587",
83
+ langSwitchedEn: "Switched to English",
84
+ onboardLangTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
85
+ onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
86
+ onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
87
+ onboardConfigDesc: "First-time setup / \u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\u8D26\u53F7",
88
+ setupCanceled: "Setup canceled, run /go-config to reconfigure"
89
+ };
90
+ var LANGS = { zh: ZH_T, en: EN_T };
91
+ var LANG_META = [
92
+ { code: "zh", label: "\u4E2D\u6587" },
93
+ { code: "en", label: "English" }
94
+ ];
95
+ function applyParams(tpl, params) {
96
+ if (!params) return tpl;
97
+ return tpl.replace(
98
+ /\{(\w+)\}/g,
99
+ (m, k) => k in params ? String(params[k]) : m
100
+ );
101
+ }
102
+ function createT(getCode) {
103
+ return (key, params) => applyParams(LANGS[getCode()][key], params);
104
+ }
105
+ function detectLang() {
106
+ try {
107
+ const loc = Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase();
108
+ if (loc.startsWith("zh")) return "zh";
109
+ return "en";
110
+ } catch {
111
+ return "en";
112
+ }
113
+ }
114
+
115
+ // src/index.tsx
116
+ var VERSION = "1.1.0";
14
117
  var CHECK_INTERVAL = Number(process?.env?.OPENCODE_GO_CHECK_INTERVAL ?? 6e4);
15
118
  var WARN_THRESHOLD = Number(process?.env?.OPENCODE_GO_WARN_THRESHOLD ?? 0.8);
16
119
  var CONFIG_DIR = process?.env?.OPENCODE_CONFIG_DIR || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "") || process?.env?.HOME + "/.config/opencode";
17
120
  var CONFIG_FILE = join(CONFIG_DIR, "go-usage-config.json");
121
+ var KV_PREFIX = "go-usage";
18
122
  function loadConfig() {
19
123
  const env = process?.env ?? {};
20
124
  let fileCfg = {};
@@ -56,6 +160,21 @@ function saveConfig(patch) {
56
160
  }
57
161
  }
58
162
  var [configTick, setConfigTick] = createSignal(0);
163
+ var [langCode, setLangCode] = createSignal(detectLang());
164
+ var t = createT(() => langCode());
165
+ function kvTryGet(api, key) {
166
+ try {
167
+ return api.kv.get(key);
168
+ } catch {
169
+ return void 0;
170
+ }
171
+ }
172
+ function kvTrySet(api, key, val) {
173
+ try {
174
+ api.kv.set(key, val);
175
+ } catch {
176
+ }
177
+ }
59
178
  async function fetchUsage(workspaceId, authCookie) {
60
179
  if (!workspaceId || !authCookie) return {
61
180
  error: "not_configured"
@@ -119,13 +238,21 @@ async function fetchUsage(workspaceId, authCookie) {
119
238
  }
120
239
  }
121
240
  function formatReset(sec) {
122
- if (sec <= 0) return "\u5DF2\u91CD\u7F6E";
241
+ if (sec <= 0) return t("resetDone");
123
242
  const d = Math.floor(sec / 86400);
124
243
  const h = Math.floor(sec % 86400 / 3600);
125
244
  const m = Math.floor(sec % 3600 / 60);
126
- if (d > 0) return `${d}\u5929${h}\u5C0F\u65F6`;
127
- if (h > 0) return `${h}\u5C0F\u65F6${m}\u5206\u949F`;
128
- return `${m}\u5206\u949F`;
245
+ if (d > 0) return t("dayHour", {
246
+ d,
247
+ h
248
+ });
249
+ if (h > 0) return t("hourMin", {
250
+ h,
251
+ m
252
+ });
253
+ return t("minute", {
254
+ m
255
+ });
129
256
  }
130
257
  function progressBar(percent, width) {
131
258
  const clamped = Math.max(0, Math.min(100, percent));
@@ -177,11 +304,11 @@ function GoUsagePanel(props) {
177
304
  setConfigured(true);
178
305
  const u = await fetchUsage(cfg.workspaceId, cfg.authCookie);
179
306
  if (u === null) {
180
- setError("\u67E5\u8BE2\u5931\u8D25");
307
+ setError(t("queryFailed"));
181
308
  return;
182
309
  }
183
310
  if (u.error === "cookie_expired") {
184
- setError("cookie \u5DF2\u8FC7\u671F");
311
+ setError(t("cookieExpired"));
185
312
  return;
186
313
  }
187
314
  if (u.error === "not_configured") {
@@ -208,12 +335,12 @@ function GoUsagePanel(props) {
208
335
  ...FALLBACK
209
336
  });
210
337
  createEffect(() => {
211
- const t = props.theme;
338
+ const th = props.theme;
212
339
  const p = {
213
340
  ...FALLBACK
214
341
  };
215
342
  for (const k of Object.keys(FALLBACK)) {
216
- const h = hex(t?.[k]);
343
+ const h = hex(th?.[k]);
217
344
  if (h) p[k] = h;
218
345
  }
219
346
  setPal(p);
@@ -271,7 +398,7 @@ function GoUsagePanel(props) {
271
398
  };
272
399
  const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - 4));
273
400
  return (() => {
274
- var _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("span"), _el$12 = _$createElement("span"), _el$13 = _$createElement("b");
401
+ var _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("span"), _el$12 = _$createElement("span"), _el$13 = _$createElement("b"), _el$14 = _$createElement("span"), _el$15 = _$createTextNode(` v`);
275
402
  _$insertNode(_el$1, _el$10);
276
403
  var _ref$ = boxEl;
277
404
  typeof _ref$ === "function" ? _$use(_ref$, _el$1) : boxEl = _el$1;
@@ -286,23 +413,28 @@ function GoUsagePanel(props) {
286
413
  });
287
414
  _$insertNode(_el$10, _el$11);
288
415
  _$insertNode(_el$10, _el$12);
416
+ _$insertNode(_el$10, _el$14);
289
417
  _$setProp(_el$10, "onMouseUp", () => setOpen((o) => !o));
290
418
  _$insert(_el$11, () => open() ? "\u25BC " : "\u25B6 ");
291
419
  _$insertNode(_el$12, _el$13);
292
- _$insertNode(_el$13, _$createTextNode(`Go \u7528\u91CF`));
420
+ _$insert(_el$13, () => t("panelTitle"));
421
+ _$insertNode(_el$14, _el$15);
422
+ _$insert(_el$14, VERSION, null);
293
423
  _$insert(_el$10, _$createComponent(Show, {
294
424
  get when() {
295
425
  return _$memo(() => !!!open())() && usage();
296
426
  },
297
427
  get children() {
298
- var _el$15 = _$createElement("span"), _el$16 = _$createTextNode(` \u5468 `), _el$18 = _$createTextNode(`%`);
299
- _$insertNode(_el$15, _el$16);
300
- _$insertNode(_el$15, _el$18);
301
- _$insert(_el$15, () => usage().weeklyUsage.usagePercent, _el$18);
302
- _$effect((_$p) => _$setProp(_el$15, "style", {
428
+ var _el$16 = _$createElement("span"), _el$17 = _$createTextNode(` `), _el$18 = _$createTextNode(` `), _el$19 = _$createTextNode(`%`);
429
+ _$insertNode(_el$16, _el$17);
430
+ _$insertNode(_el$16, _el$18);
431
+ _$insertNode(_el$16, _el$19);
432
+ _$insert(_el$16, () => t("weeklyShort"), _el$18);
433
+ _$insert(_el$16, () => usage().weeklyUsage.usagePercent, _el$19);
434
+ _$effect((_$p) => _$setProp(_el$16, "style", {
303
435
  fg: colorFor(usage().weeklyUsage.usagePercent)
304
436
  }, _$p));
305
- return _el$15;
437
+ return _el$16;
306
438
  }
307
439
  }), null);
308
440
  _$insert(_el$1, _$createComponent(Show, {
@@ -311,23 +443,23 @@ function GoUsagePanel(props) {
311
443
  },
312
444
  get children() {
313
445
  return [(() => {
314
- var _el$19 = _$createElement("text");
315
- _$insert(_el$19, sep);
316
- _$effect((_$p) => _$setProp(_el$19, "fg", colors().muted, _$p));
317
- return _el$19;
446
+ var _el$20 = _$createElement("text");
447
+ _$insert(_el$20, sep);
448
+ _$effect((_$p) => _$setProp(_el$20, "fg", colors().muted, _$p));
449
+ return _el$20;
318
450
  })(), _$createComponent(Show, {
319
451
  get when() {
320
452
  return !configured();
321
453
  },
322
454
  get children() {
323
455
  return [(() => {
324
- var _el$20 = _$createElement("text");
325
- _$insertNode(_el$20, _$createTextNode(`\u672A\u914D\u7F6E\u8D26\u53F7`));
326
- _$effect((_$p) => _$setProp(_el$20, "fg", colors().warning, _$p));
327
- return _el$20;
456
+ var _el$21 = _$createElement("text");
457
+ _$insert(_el$21, () => t("notConfigured"));
458
+ _$effect((_$p) => _$setProp(_el$21, "fg", colors().warning, _$p));
459
+ return _el$21;
328
460
  })(), (() => {
329
461
  var _el$22 = _$createElement("text");
330
- _$insertNode(_el$22, _$createTextNode(`\u8F93\u5165 /go-config \u8BBE\u7F6E`));
462
+ _$insert(_el$22, () => t("notConfiguredHint"));
331
463
  _$effect((_$p) => _$setProp(_el$22, "fg", colors().muted, _$p));
332
464
  return _el$22;
333
465
  })()];
@@ -347,49 +479,50 @@ function GoUsagePanel(props) {
347
479
  return configured();
348
480
  },
349
481
  get children() {
350
- var _el$31 = _$createElement("text");
351
- _$insertNode(_el$31, _$createTextNode(`\u52A0\u8F7D\u4E2D...`));
352
- _$effect((_$p) => _$setProp(_el$31, "fg", colors().muted, _$p));
353
- return _el$31;
482
+ var _el$29 = _$createElement("text");
483
+ _$insert(_el$29, () => t("loading"));
484
+ _$effect((_$p) => _$setProp(_el$29, "fg", colors().muted, _$p));
485
+ return _el$29;
354
486
  }
355
487
  });
356
488
  },
357
489
  get children() {
358
- return [_$memo(() => renderRow("5\u5C0F\u65F6\u7528\u91CF", usage().rollingUsage)), _$memo(() => renderRow("\u6BCF\u5468\u7528\u91CF", usage().weeklyUsage)), _$memo(() => renderRow("\u6BCF\u6708\u7528\u91CF", usage().monthlyUsage)), (() => {
359
- var _el$27 = _$createElement("text"), _el$28 = _$createElement("span"), _el$30 = _$createElement("span");
360
- _$insertNode(_el$27, _el$28);
361
- _$insertNode(_el$27, _el$30);
362
- _$insertNode(_el$28, _$createTextNode(`\u6700\u8FD1\u66F4\u65B0 `));
363
- _$insert(_el$30, lastUpdated);
490
+ return [_$memo(() => renderRow(t("rowRolling"), usage().rollingUsage)), _$memo(() => renderRow(t("rowWeekly"), usage().weeklyUsage)), _$memo(() => renderRow(t("rowMonthly"), usage().monthlyUsage)), (() => {
491
+ var _el$25 = _$createElement("text"), _el$26 = _$createElement("span"), _el$27 = _$createTextNode(` `), _el$28 = _$createElement("span");
492
+ _$insertNode(_el$25, _el$26);
493
+ _$insertNode(_el$25, _el$28);
494
+ _$insertNode(_el$26, _el$27);
495
+ _$insert(_el$26, () => t("updatedAt"), _el$27);
496
+ _$insert(_el$28, lastUpdated);
364
497
  _$effect((_p$) => {
365
- var _v$8 = {
498
+ var _v$9 = {
366
499
  fg: colors().muted
367
- }, _v$9 = {
500
+ }, _v$0 = {
368
501
  fg: colors().muted
369
502
  };
370
- _v$8 !== _p$.e && (_p$.e = _$setProp(_el$28, "style", _v$8, _p$.e));
371
- _v$9 !== _p$.t && (_p$.t = _$setProp(_el$30, "style", _v$9, _p$.t));
503
+ _v$9 !== _p$.e && (_p$.e = _$setProp(_el$26, "style", _v$9, _p$.e));
504
+ _v$0 !== _p$.t && (_p$.t = _$setProp(_el$28, "style", _v$0, _p$.t));
372
505
  return _p$;
373
506
  }, {
374
507
  e: void 0,
375
508
  t: void 0
376
509
  });
377
- return _el$27;
510
+ return _el$25;
378
511
  })()];
379
512
  }
380
513
  });
381
514
  },
382
515
  get children() {
383
516
  return [(() => {
517
+ var _el$23 = _$createElement("text");
518
+ _$insert(_el$23, error);
519
+ _$effect((_$p) => _$setProp(_el$23, "fg", colors().error, _$p));
520
+ return _el$23;
521
+ })(), (() => {
384
522
  var _el$24 = _$createElement("text");
385
- _$insert(_el$24, error);
386
- _$effect((_$p) => _$setProp(_el$24, "fg", colors().error, _$p));
523
+ _$insert(_el$24, () => t("reconfigureHint"));
524
+ _$effect((_$p) => _$setProp(_el$24, "fg", colors().muted, _$p));
387
525
  return _el$24;
388
- })(), (() => {
389
- var _el$25 = _$createElement("text");
390
- _$insertNode(_el$25, _$createTextNode(`\u8F93\u5165 /go-config \u91CD\u65B0\u914D\u7F6E`));
391
- _$effect((_$p) => _$setProp(_el$25, "fg", colors().muted, _$p));
392
- return _el$25;
393
526
  })()];
394
527
  }
395
528
  })];
@@ -400,15 +533,19 @@ function GoUsagePanel(props) {
400
533
  fg: colors().muted
401
534
  }, _v$7 = {
402
535
  fg: colors().primary
536
+ }, _v$8 = {
537
+ fg: colors().muted
403
538
  };
404
539
  _v$5 !== _p$.e && (_p$.e = _$setProp(_el$1, "borderColor", _v$5, _p$.e));
405
540
  _v$6 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$6, _p$.t));
406
541
  _v$7 !== _p$.a && (_p$.a = _$setProp(_el$12, "style", _v$7, _p$.a));
542
+ _v$8 !== _p$.o && (_p$.o = _$setProp(_el$14, "style", _v$8, _p$.o));
407
543
  return _p$;
408
544
  }, {
409
545
  e: void 0,
410
546
  t: void 0,
411
- a: void 0
547
+ a: void 0,
548
+ o: void 0
412
549
  });
413
550
  return _el$1;
414
551
  })();
@@ -428,63 +565,121 @@ function createSidebarSlot(api) {
428
565
  }
429
566
  };
430
567
  }
431
- var tui = async (api) => {
432
- api.slots.register(createSidebarSlot(api));
433
- api.command?.register(() => [{
434
- title: "Go Usage: Configure",
435
- value: "go-usage.config",
436
- description: "\u8BBE\u7F6E OpenCode Go workspace ID \u548C auth cookie",
437
- slash: {
438
- name: "go-config"
568
+ function runConfigDialog(api, dialog) {
569
+ dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
570
+ get title() {
571
+ return t("wsPromptTitle");
439
572
  },
440
- onSelect: (dialog) => {
573
+ description: () => (() => {
574
+ var _el$30 = _$createElement("text");
575
+ _$insert(_el$30, () => t("wsPromptDesc"));
576
+ return _el$30;
577
+ })(),
578
+ get placeholder() {
579
+ return t("wsPlaceholder");
580
+ },
581
+ onConfirm: (value) => {
582
+ const wsId = value.trim();
583
+ if (!wsId) {
584
+ dialog?.clear();
585
+ return;
586
+ }
587
+ saveConfig({
588
+ workspace_id: wsId
589
+ });
441
590
  dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
442
- title: "\u8F93\u5165 workspace_id",
591
+ get title() {
592
+ return t("cookiePromptTitle");
593
+ },
443
594
  description: () => (() => {
444
- var _el$33 = _$createElement("text");
445
- _$insertNode(_el$33, _$createTextNode(`\u5728 opencode.ai \u63A7\u5236\u53F0 URL \u4E2D\u83B7\u53D6\uFF08wrk_ \u5F00\u5934\uFF09`));
446
- return _el$33;
595
+ var _el$31 = _$createElement("text");
596
+ _$insert(_el$31, () => t("cookiePromptDesc"));
597
+ return _el$31;
447
598
  })(),
448
- placeholder: "wrk_xxxxxxxxxxxx",
449
- onConfirm: (value) => {
450
- const wsId = value.trim();
451
- if (!wsId) {
599
+ get placeholder() {
600
+ return t("cookiePlaceholder");
601
+ },
602
+ onConfirm: (val) => {
603
+ const cookie = val.trim();
604
+ if (!cookie) {
452
605
  dialog?.clear();
453
606
  return;
454
607
  }
455
608
  saveConfig({
456
- workspace_id: wsId
609
+ cookie
457
610
  });
458
- dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
459
- title: "\u8F93\u5165 auth cookie",
460
- description: () => (() => {
461
- var _el$35 = _$createElement("text");
462
- _$insertNode(_el$35, _$createTextNode(`\u767B\u5F55 opencode.ai \u540E\uFF0C\u6D4F\u89C8\u5668 F12 \u2192 Application \u2192 Cookies \u2192 opencode.ai \u2192 \u590D\u5236 auth \u7684\u503C`));
463
- return _el$35;
464
- })(),
465
- placeholder: "Fe26.2*...",
466
- onConfirm: (val) => {
467
- const cookie = val.trim();
468
- if (!cookie) {
469
- dialog?.clear();
470
- return;
471
- }
472
- saveConfig({
473
- cookie
474
- });
475
- setConfigTick((v) => v + 1);
476
- api.ui.toast({
477
- variant: "success",
478
- message: "Go \u7528\u91CF\u914D\u7F6E\u5DF2\u4FDD\u5B58"
479
- });
480
- dialog?.clear();
481
- },
482
- onCancel: () => dialog?.clear()
483
- }));
611
+ setConfigTick((v) => v + 1);
612
+ api.ui.toast({
613
+ variant: "success",
614
+ message: t("configSaved")
615
+ });
616
+ dialog?.clear();
484
617
  },
485
618
  onCancel: () => dialog?.clear()
486
619
  }));
487
- }
620
+ },
621
+ onCancel: () => dialog?.clear()
622
+ }));
623
+ }
624
+ function runLangDialog(api, dialog) {
625
+ dialog?.replace(() => _$createComponent(api.ui.DialogSelect, {
626
+ get title() {
627
+ return t("langTitle");
628
+ },
629
+ get options() {
630
+ return LANG_META.map((m) => ({
631
+ title: `${m.label}${langCode() === m.code ? " \u2713" : ""}`,
632
+ value: m.code
633
+ }));
634
+ },
635
+ onSelect: (opt) => {
636
+ const code = opt.value;
637
+ setLangCode(code);
638
+ kvTrySet(api, `${KV_PREFIX}.lang`, code);
639
+ api.ui.toast({
640
+ message: code === "zh" ? t("langSwitchedZh") : t("langSwitchedEn")
641
+ });
642
+ const cfg = loadConfig();
643
+ if (!cfg.workspaceId || !cfg.authCookie) {
644
+ runConfigDialog(api, dialog);
645
+ } else {
646
+ dialog?.clear();
647
+ }
648
+ },
649
+ onCancel: () => dialog?.clear()
650
+ }));
651
+ }
652
+ var tui = async (api) => {
653
+ api.slots.register(createSidebarSlot(api));
654
+ const savedLang = kvTryGet(api, `${KV_PREFIX}.lang`);
655
+ if (savedLang === "zh" || savedLang === "en") setLangCode(savedLang);
656
+ else setLangCode(detectLang());
657
+ const onboarded = kvTryGet(api, `${KV_PREFIX}.onboarded`);
658
+ if (!onboarded) {
659
+ kvTrySet(api, `${KV_PREFIX}.onboarded`, "1");
660
+ setTimeout(() => {
661
+ try {
662
+ api.command.trigger("go-usage.lang");
663
+ } catch {
664
+ }
665
+ }, 1500);
666
+ }
667
+ api.command?.register(() => [{
668
+ title: t("cmdTitle"),
669
+ value: "go-usage.config",
670
+ description: t("cmdDesc"),
671
+ slash: {
672
+ name: "go-config"
673
+ },
674
+ onSelect: (dialog) => runConfigDialog(api, dialog)
675
+ }, {
676
+ title: t("langCmdTitle"),
677
+ value: "go-usage.lang",
678
+ description: t("langCmdDesc"),
679
+ slash: {
680
+ name: "go-lang"
681
+ },
682
+ onSelect: (dialog) => runLangDialog(api, dialog)
488
683
  }]);
489
684
  };
490
685
  var mod = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-go-usage-tui",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "OpenCode TUI plugin displaying OpenCode Go usage in the sidebar",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,7 +15,8 @@
15
15
  "dist",
16
16
  "src",
17
17
  "build.tui.mjs",
18
- "README.md"
18
+ "README.md",
19
+ "README_EN.md"
19
20
  ],
20
21
  "scripts": {
21
22
  "build": "bun run build.tui.mjs",
package/src/i18n.ts ADDED
@@ -0,0 +1,108 @@
1
+ export type LangCode = "zh" | "en"
2
+
3
+ const ZH_T = {
4
+ panelTitle: "Go 用量",
5
+ rowRolling: "5小时用量",
6
+ rowWeekly: "每周用量",
7
+ rowMonthly: "每月用量",
8
+ weeklyShort: "周",
9
+ updatedAt: "最近更新",
10
+ notConfigured: "未配置账号",
11
+ notConfiguredHint: "输入 /go-config 设置",
12
+ loading: "加载中...",
13
+ queryFailed: "查询失败",
14
+ cookieExpired: "cookie 已过期",
15
+ reconfigureHint: "输入 /go-config 重新配置",
16
+ resetDone: "已重置",
17
+ dayHour: "{d}天{h}小时",
18
+ hourMin: "{h}小时{m}分钟",
19
+ minute: "{m}分钟",
20
+ cmdTitle: "Go Usage: Configure",
21
+ cmdDesc: "设置 OpenCode Go workspace ID 和 auth cookie",
22
+ wsPromptTitle: "输入 workspace_id",
23
+ wsPromptDesc: "在 opencode.ai 控制台 URL 中获取(wrk_ 开头)",
24
+ wsPlaceholder: "wrk_xxxxxxxxxxxx",
25
+ cookiePromptTitle: "输入 auth cookie",
26
+ cookiePromptDesc: "登录 opencode.ai 后,浏览器 F12 → Application → Cookies → opencode.ai → 复制 auth 的值",
27
+ cookiePlaceholder: "Fe26.2*...",
28
+ configSaved: "Go 用量配置已保存",
29
+ langTitle: "显示语言",
30
+ langCmdTitle: "Go Usage: Language",
31
+ langCmdDesc: "切换显示语言 | Switch display language",
32
+ langSwitchedZh: "已切换为中文",
33
+ langSwitchedEn: "Switched to English",
34
+ onboardLangTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
35
+ onboardLangDesc: "Choose display language / 选择显示语言",
36
+ onboardConfigTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
37
+ onboardConfigDesc: "First-time setup / 首次使用需配置账号",
38
+ setupCanceled: "已取消配置,输入 /go-config 可重新设置",
39
+ } as const
40
+
41
+ export type Translation = { [K in keyof typeof ZH_T]: string }
42
+
43
+ const EN_T: Translation = {
44
+ panelTitle: "Go Usage",
45
+ rowRolling: "5h usage",
46
+ rowWeekly: "Weekly",
47
+ rowMonthly: "Monthly",
48
+ weeklyShort: "Wk",
49
+ updatedAt: "Updated",
50
+ notConfigured: "Not configured",
51
+ notConfiguredHint: "Run /go-config to set up",
52
+ loading: "Loading...",
53
+ queryFailed: "Query failed",
54
+ cookieExpired: "Cookie expired",
55
+ reconfigureHint: "Run /go-config to reconfigure",
56
+ resetDone: "Reset",
57
+ dayHour: "{d}d {h}h",
58
+ hourMin: "{h}h {m}m",
59
+ minute: "{m}m",
60
+ cmdTitle: "Go Usage: Configure",
61
+ cmdDesc: "Set OpenCode Go workspace ID and auth cookie",
62
+ wsPromptTitle: "Enter workspace_id",
63
+ wsPromptDesc: "Get it from the opencode.ai console URL (starts with wrk_)",
64
+ wsPlaceholder: "wrk_xxxxxxxxxxxx",
65
+ cookiePromptTitle: "Enter auth cookie",
66
+ cookiePromptDesc: "After logging into opencode.ai, press F12 → Application → Cookies → opencode.ai → copy the auth value",
67
+ cookiePlaceholder: "Fe26.2*...",
68
+ configSaved: "Go usage config saved",
69
+ langTitle: "Display language",
70
+ langCmdTitle: "Go Usage: Language",
71
+ langCmdDesc: "切换显示语言 | Switch display language",
72
+ langSwitchedZh: "已切换为中文",
73
+ langSwitchedEn: "Switched to English",
74
+ onboardLangTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
75
+ onboardLangDesc: "Choose display language / 选择显示语言",
76
+ onboardConfigTitle: "Welcome to Go Usage / 欢迎使用 Go 用量",
77
+ onboardConfigDesc: "First-time setup / 首次使用需配置账号",
78
+ setupCanceled: "Setup canceled, run /go-config to reconfigure",
79
+ }
80
+
81
+ export const LANGS: Record<LangCode, Translation> = { zh: ZH_T, en: EN_T }
82
+
83
+ export const LANG_META: { code: LangCode; label: string }[] = [
84
+ { code: "zh", label: "中文" },
85
+ { code: "en", label: "English" },
86
+ ]
87
+
88
+ export function applyParams(tpl: string, params?: Record<string, string | number>): string {
89
+ if (!params) return tpl
90
+ return tpl.replace(/\{(\w+)\}/g, (m, k: string) =>
91
+ k in params ? String(params[k]) : m,
92
+ )
93
+ }
94
+
95
+ export function createT(getCode: () => LangCode) {
96
+ return (key: keyof Translation, params?: Record<string, string | number>): string =>
97
+ applyParams(LANGS[getCode()][key], params)
98
+ }
99
+
100
+ export function detectLang(): LangCode {
101
+ try {
102
+ const loc = Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase()
103
+ if (loc.startsWith("zh")) return "zh"
104
+ return "en"
105
+ } catch {
106
+ return "en"
107
+ }
108
+ }
package/src/index.tsx CHANGED
@@ -12,15 +12,19 @@ import { createSignal, createEffect, onMount, onCleanup, Show } from "solid-js"
12
12
  import type { JSX } from "@opentui/solid"
13
13
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs"
14
14
  import { join, dirname } from "node:path"
15
+ import { createT, detectLang, LANG_META } from "./i18n"
16
+ import type { LangCode } from "./i18n"
15
17
 
16
18
  declare const process: { env: Record<string, string | undefined> } | undefined
17
19
 
20
+ const VERSION: string = __PLUGIN_VERSION__
18
21
  const CHECK_INTERVAL = Number(process?.env?.OPENCODE_GO_CHECK_INTERVAL ?? 60000)
19
22
  const WARN_THRESHOLD = Number(process?.env?.OPENCODE_GO_WARN_THRESHOLD ?? 0.8)
20
23
  const CONFIG_DIR = process?.env?.OPENCODE_CONFIG_DIR
21
24
  || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "")
22
25
  || process?.env?.HOME + "/.config/opencode"
23
26
  const CONFIG_FILE = join(CONFIG_DIR, "go-usage-config.json")
27
+ const KV_PREFIX = "go-usage"
24
28
 
25
29
  function loadConfig(): { workspaceId: string; authCookie: string } {
26
30
  const env = process?.env ?? {}
@@ -58,6 +62,17 @@ function saveConfig(patch: { workspace_id?: string; cookie?: string }): void {
58
62
  // 模块级共享信号:/go-config 保存配置后递增,面板监听它立即刷新
59
63
  const [configTick, setConfigTick] = createSignal(0)
60
64
 
65
+ // 模块级共享语言信号:/go-lang 或首次引导切换后,面板和命令实时响应
66
+ const [langCode, setLangCode] = createSignal<LangCode>(detectLang())
67
+ const t = createT(() => langCode())
68
+
69
+ function kvTryGet(api: TuiPluginApi, key: string): string | undefined {
70
+ try { return api.kv.get<string>(key) } catch { return undefined }
71
+ }
72
+ function kvTrySet(api: TuiPluginApi, key: string, val: unknown): void {
73
+ try { api.kv.set(key, val) } catch { /* 忽略 */ }
74
+ }
75
+
61
76
  interface Usage {
62
77
  rollingUsage: { usagePercent: number; resetInSec: number }
63
78
  weeklyUsage: { usagePercent: number; resetInSec: number }
@@ -107,13 +122,13 @@ async function fetchUsage(workspaceId: string, authCookie: string): Promise<Usag
107
122
  }
108
123
 
109
124
  function formatReset(sec: number): string {
110
- if (sec <= 0) return "已重置"
125
+ if (sec <= 0) return t("resetDone")
111
126
  const d = Math.floor(sec / 86400)
112
127
  const h = Math.floor((sec % 86400) / 3600)
113
128
  const m = Math.floor((sec % 3600) / 60)
114
- if (d > 0) return `${d}天${h}小时`
115
- if (h > 0) return `${h}小时${m}分钟`
116
- return `${m}分钟`
129
+ if (d > 0) return t("dayHour", { d, h })
130
+ if (h > 0) return t("hourMin", { h, m })
131
+ return t("minute", { m })
117
132
  }
118
133
 
119
134
  function progressBar(percent: number, width: number): string {
@@ -175,8 +190,8 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
175
190
  }
176
191
  setConfigured(true)
177
192
  const u = await fetchUsage(cfg.workspaceId, cfg.authCookie)
178
- if (u === null) { setError("查询失败"); return }
179
- if (u.error === "cookie_expired") { setError("cookie 已过期"); return }
193
+ if (u === null) { setError(t("queryFailed")); return }
194
+ if (u.error === "cookie_expired") { setError(t("cookieExpired")); return }
180
195
  if (u.error === "not_configured") { setConfigured(false); return }
181
196
  setUsage(u)
182
197
  setLastUpdated(new Date().toLocaleTimeString())
@@ -196,10 +211,10 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
196
211
 
197
212
  const [pal, setPal] = createSignal<Record<string, string>>({ ...FALLBACK })
198
213
  createEffect(() => {
199
- const t = props.theme as any
214
+ const th = props.theme as any
200
215
  const p: Record<string, string> = { ...FALLBACK }
201
216
  for (const k of Object.keys(FALLBACK)) {
202
- const h = hex(t?.[k])
217
+ const h = hex(th?.[k])
203
218
  if (h) p[k] = h
204
219
  }
205
220
  setPal(p)
@@ -250,10 +265,11 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
250
265
  >
251
266
  <text onMouseUp={() => setOpen(o => !o)}>
252
267
  <span style={{ fg: colors().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
253
- <span style={{ fg: colors().primary }}><b>Go 用量</b></span>
268
+ <span style={{ fg: colors().primary }}><b>{t("panelTitle")}</b></span>
269
+ <span style={{ fg: colors().muted }}> v{VERSION}</span>
254
270
  <Show when={!open() && usage()}>
255
271
  <span style={{ fg: colorFor(usage()!.weeklyUsage.usagePercent) }}>
256
- {" ".repeat(2)} {usage()!.weeklyUsage.usagePercent}%
272
+ {" ".repeat(2)}{t("weeklyShort")} {usage()!.weeklyUsage.usagePercent}%
257
273
  </span>
258
274
  </Show>
259
275
  </text>
@@ -262,25 +278,25 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
262
278
  <text fg={colors().muted}>{sep()}</text>
263
279
 
264
280
  <Show when={!configured()}>
265
- <text fg={colors().warning}>未配置账号</text>
266
- <text fg={colors().muted}>输入 /go-config 设置</text>
281
+ <text fg={colors().warning}>{t("notConfigured")}</text>
282
+ <text fg={colors().muted}>{t("notConfiguredHint")}</text>
267
283
  </Show>
268
284
 
269
285
  <Show when={configured() && error()} fallback={
270
286
  <Show when={configured() && usage()} fallback={
271
- <Show when={configured()}><text fg={colors().muted}>加载中...</text></Show>
287
+ <Show when={configured()}><text fg={colors().muted}>{t("loading")}</text></Show>
272
288
  }>
273
- {renderRow("5小时用量", usage()!.rollingUsage)}
274
- {renderRow("每周用量", usage()!.weeklyUsage)}
275
- {renderRow("每月用量", usage()!.monthlyUsage)}
289
+ {renderRow(t("rowRolling"), usage()!.rollingUsage)}
290
+ {renderRow(t("rowWeekly"), usage()!.weeklyUsage)}
291
+ {renderRow(t("rowMonthly"), usage()!.monthlyUsage)}
276
292
  <text>
277
- <span style={{ fg: colors().muted }}>最近更新 </span>
293
+ <span style={{ fg: colors().muted }}>{t("updatedAt")} </span>
278
294
  <span style={{ fg: colors().muted }}>{lastUpdated()}</span>
279
295
  </text>
280
296
  </Show>
281
297
  }>
282
298
  <text fg={colors().error}>{error()}</text>
283
- <text fg={colors().muted}>输入 /go-config 重新配置</text>
299
+ <text fg={colors().muted}>{t("reconfigureHint")}</text>
284
300
  </Show>
285
301
  </Show>
286
302
  </box>
@@ -298,50 +314,100 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
298
314
  }
299
315
  }
300
316
 
301
- const tui: TuiPlugin = async (api: TuiPluginApi) => {
302
- api.slots.register(createSidebarSlot(api))
303
-
304
- api.command?.register(() => [
305
- {
306
- title: "Go Usage: Configure",
307
- value: "go-usage.config",
308
- description: "设置 OpenCode Go workspace ID 和 auth cookie",
309
- slash: { name: "go-config" },
310
- onSelect: (dialog) => {
317
+ function runConfigDialog(api: TuiPluginApi, dialog: any): void {
318
+ dialog?.replace(() => (
319
+ <api.ui.DialogPrompt
320
+ title={t("wsPromptTitle")}
321
+ description={() => (
322
+ <text>{t("wsPromptDesc")}</text>
323
+ )}
324
+ placeholder={t("wsPlaceholder")}
325
+ onConfirm={(value) => {
326
+ const wsId = value.trim()
327
+ if (!wsId) { dialog?.clear(); return }
328
+ saveConfig({ workspace_id: wsId })
311
329
  dialog?.replace(() => (
312
330
  <api.ui.DialogPrompt
313
- title="输入 workspace_id"
331
+ title={t("cookiePromptTitle")}
314
332
  description={() => (
315
- <text>在 opencode.ai 控制台 URL 中获取(wrk_ 开头)</text>
333
+ <text>{t("cookiePromptDesc")}</text>
316
334
  )}
317
- placeholder="wrk_xxxxxxxxxxxx"
318
- onConfirm={(value) => {
319
- const wsId = value.trim()
320
- if (!wsId) { dialog?.clear(); return }
321
- saveConfig({ workspace_id: wsId })
322
- dialog?.replace(() => (
323
- <api.ui.DialogPrompt
324
- title="输入 auth cookie"
325
- description={() => (
326
- <text>登录 opencode.ai 后,浏览器 F12 → Application → Cookies → opencode.ai → 复制 auth 的值</text>
327
- )}
328
- placeholder="Fe26.2*..."
329
- onConfirm={(val) => {
330
- const cookie = val.trim()
331
- if (!cookie) { dialog?.clear(); return }
332
- saveConfig({ cookie })
333
- setConfigTick((v) => v + 1)
334
- api.ui.toast({ variant: "success", message: "Go 用量配置已保存" })
335
- dialog?.clear()
336
- }}
337
- onCancel={() => dialog?.clear()}
338
- />
339
- ))
335
+ placeholder={t("cookiePlaceholder")}
336
+ onConfirm={(val) => {
337
+ const cookie = val.trim()
338
+ if (!cookie) { dialog?.clear(); return }
339
+ saveConfig({ cookie })
340
+ setConfigTick((v) => v + 1)
341
+ api.ui.toast({ variant: "success", message: t("configSaved") })
342
+ dialog?.clear()
340
343
  }}
341
344
  onCancel={() => dialog?.clear()}
342
345
  />
343
346
  ))
344
- },
347
+ }}
348
+ onCancel={() => dialog?.clear()}
349
+ />
350
+ ))
351
+ }
352
+
353
+ function runLangDialog(api: TuiPluginApi, dialog: any): void {
354
+ dialog?.replace(() => (
355
+ <api.ui.DialogSelect
356
+ title={t("langTitle")}
357
+ options={LANG_META.map((m) => ({
358
+ title: `${m.label}${langCode() === m.code ? " \u2713" : ""}`,
359
+ value: m.code,
360
+ }))}
361
+ onSelect={(opt) => {
362
+ const code = opt.value as LangCode
363
+ setLangCode(code)
364
+ kvTrySet(api, `${KV_PREFIX}.lang`, code)
365
+ api.ui.toast({ message: code === "zh" ? t("langSwitchedZh") : t("langSwitchedEn") })
366
+ // 语言选择后:若无配置则继续引导配置账号
367
+ const cfg = loadConfig()
368
+ if (!cfg.workspaceId || !cfg.authCookie) {
369
+ runConfigDialog(api, dialog)
370
+ } else {
371
+ dialog?.clear()
372
+ }
373
+ }}
374
+ onCancel={() => dialog?.clear()}
375
+ />
376
+ ))
377
+ }
378
+
379
+ const tui: TuiPlugin = async (api: TuiPluginApi) => {
380
+ api.slots.register(createSidebarSlot(api))
381
+
382
+ // 恢复已保存的语言;若无则按系统检测
383
+ const savedLang = kvTryGet(api, `${KV_PREFIX}.lang`) as LangCode | undefined
384
+ if (savedLang === "zh" || savedLang === "en") setLangCode(savedLang)
385
+ else setLangCode(detectLang())
386
+
387
+ // 首次启动引导:已引导过则跳过(用 kv 标记)
388
+ const onboarded = kvTryGet(api, `${KV_PREFIX}.onboarded`)
389
+ if (!onboarded) {
390
+ kvTrySet(api, `${KV_PREFIX}.onboarded`, "1")
391
+ setTimeout(() => {
392
+ // 通过 trigger 触发语言选择命令,命令 onSelect 会拿到 dialog 栈并弹窗
393
+ try { api.command.trigger("go-usage.lang") } catch { /* 命令未就绪时忽略 */ }
394
+ }, 1500)
395
+ }
396
+
397
+ api.command?.register(() => [
398
+ {
399
+ title: t("cmdTitle"),
400
+ value: "go-usage.config",
401
+ description: t("cmdDesc"),
402
+ slash: { name: "go-config" },
403
+ onSelect: (dialog) => runConfigDialog(api, dialog),
404
+ },
405
+ {
406
+ title: t("langCmdTitle"),
407
+ value: "go-usage.lang",
408
+ description: t("langCmdDesc"),
409
+ slash: { name: "go-lang" },
410
+ onSelect: (dialog) => runLangDialog(api, dialog),
345
411
  },
346
412
  ])
347
413
  }