opencode-go-usage-tui 1.0.0 → 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 = {};
@@ -55,6 +159,22 @@ function saveConfig(patch) {
55
159
  } catch {
56
160
  }
57
161
  }
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
+ }
58
178
  async function fetchUsage(workspaceId, authCookie) {
59
179
  if (!workspaceId || !authCookie) return {
60
180
  error: "not_configured"
@@ -118,13 +238,21 @@ async function fetchUsage(workspaceId, authCookie) {
118
238
  }
119
239
  }
120
240
  function formatReset(sec) {
121
- if (sec <= 0) return "\u5DF2\u91CD\u7F6E";
241
+ if (sec <= 0) return t("resetDone");
122
242
  const d = Math.floor(sec / 86400);
123
243
  const h = Math.floor(sec % 86400 / 3600);
124
244
  const m = Math.floor(sec % 3600 / 60);
125
- if (d > 0) return `${d}\u5929${h}\u5C0F\u65F6`;
126
- if (h > 0) return `${h}\u5C0F\u65F6${m}\u5206\u949F`;
127
- 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
+ });
128
256
  }
129
257
  function progressBar(percent, width) {
130
258
  const clamped = Math.max(0, Math.min(100, percent));
@@ -176,11 +304,11 @@ function GoUsagePanel(props) {
176
304
  setConfigured(true);
177
305
  const u = await fetchUsage(cfg.workspaceId, cfg.authCookie);
178
306
  if (u === null) {
179
- setError("\u67E5\u8BE2\u5931\u8D25");
307
+ setError(t("queryFailed"));
180
308
  return;
181
309
  }
182
310
  if (u.error === "cookie_expired") {
183
- setError("cookie \u5DF2\u8FC7\u671F");
311
+ setError(t("cookieExpired"));
184
312
  return;
185
313
  }
186
314
  if (u.error === "not_configured") {
@@ -194,18 +322,25 @@ function GoUsagePanel(props) {
194
322
  onMount(() => {
195
323
  refresh();
196
324
  const timer = setInterval(refresh, CHECK_INTERVAL);
197
- onCleanup(() => clearInterval(timer));
325
+ const stopWatch = createEffect(() => {
326
+ void configTick();
327
+ refresh();
328
+ });
329
+ onCleanup(() => {
330
+ clearInterval(timer);
331
+ stopWatch();
332
+ });
198
333
  });
199
334
  const [pal, setPal] = createSignal({
200
335
  ...FALLBACK
201
336
  });
202
337
  createEffect(() => {
203
- const t = props.theme;
338
+ const th = props.theme;
204
339
  const p = {
205
340
  ...FALLBACK
206
341
  };
207
342
  for (const k of Object.keys(FALLBACK)) {
208
- const h = hex(t?.[k]);
343
+ const h = hex(th?.[k]);
209
344
  if (h) p[k] = h;
210
345
  }
211
346
  setPal(p);
@@ -263,7 +398,7 @@ function GoUsagePanel(props) {
263
398
  };
264
399
  const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - 4));
265
400
  return (() => {
266
- 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`);
267
402
  _$insertNode(_el$1, _el$10);
268
403
  var _ref$ = boxEl;
269
404
  typeof _ref$ === "function" ? _$use(_ref$, _el$1) : boxEl = _el$1;
@@ -278,23 +413,28 @@ function GoUsagePanel(props) {
278
413
  });
279
414
  _$insertNode(_el$10, _el$11);
280
415
  _$insertNode(_el$10, _el$12);
416
+ _$insertNode(_el$10, _el$14);
281
417
  _$setProp(_el$10, "onMouseUp", () => setOpen((o) => !o));
282
418
  _$insert(_el$11, () => open() ? "\u25BC " : "\u25B6 ");
283
419
  _$insertNode(_el$12, _el$13);
284
- _$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);
285
423
  _$insert(_el$10, _$createComponent(Show, {
286
424
  get when() {
287
425
  return _$memo(() => !!!open())() && usage();
288
426
  },
289
427
  get children() {
290
- var _el$15 = _$createElement("span"), _el$16 = _$createTextNode(` \u5468 `), _el$18 = _$createTextNode(`%`);
291
- _$insertNode(_el$15, _el$16);
292
- _$insertNode(_el$15, _el$18);
293
- _$insert(_el$15, () => usage().weeklyUsage.usagePercent, _el$18);
294
- _$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", {
295
435
  fg: colorFor(usage().weeklyUsage.usagePercent)
296
436
  }, _$p));
297
- return _el$15;
437
+ return _el$16;
298
438
  }
299
439
  }), null);
300
440
  _$insert(_el$1, _$createComponent(Show, {
@@ -303,23 +443,23 @@ function GoUsagePanel(props) {
303
443
  },
304
444
  get children() {
305
445
  return [(() => {
306
- var _el$19 = _$createElement("text");
307
- _$insert(_el$19, sep);
308
- _$effect((_$p) => _$setProp(_el$19, "fg", colors().muted, _$p));
309
- 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;
310
450
  })(), _$createComponent(Show, {
311
451
  get when() {
312
452
  return !configured();
313
453
  },
314
454
  get children() {
315
455
  return [(() => {
316
- var _el$20 = _$createElement("text");
317
- _$insertNode(_el$20, _$createTextNode(`\u672A\u914D\u7F6E\u8D26\u53F7`));
318
- _$effect((_$p) => _$setProp(_el$20, "fg", colors().warning, _$p));
319
- 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;
320
460
  })(), (() => {
321
461
  var _el$22 = _$createElement("text");
322
- _$insertNode(_el$22, _$createTextNode(`\u8F93\u5165 /go-config \u8BBE\u7F6E`));
462
+ _$insert(_el$22, () => t("notConfiguredHint"));
323
463
  _$effect((_$p) => _$setProp(_el$22, "fg", colors().muted, _$p));
324
464
  return _el$22;
325
465
  })()];
@@ -339,49 +479,50 @@ function GoUsagePanel(props) {
339
479
  return configured();
340
480
  },
341
481
  get children() {
342
- var _el$31 = _$createElement("text");
343
- _$insertNode(_el$31, _$createTextNode(`\u52A0\u8F7D\u4E2D...`));
344
- _$effect((_$p) => _$setProp(_el$31, "fg", colors().muted, _$p));
345
- 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;
346
486
  }
347
487
  });
348
488
  },
349
489
  get children() {
350
- 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)), (() => {
351
- var _el$27 = _$createElement("text"), _el$28 = _$createElement("span"), _el$30 = _$createElement("span");
352
- _$insertNode(_el$27, _el$28);
353
- _$insertNode(_el$27, _el$30);
354
- _$insertNode(_el$28, _$createTextNode(`\u6700\u8FD1\u66F4\u65B0 `));
355
- _$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);
356
497
  _$effect((_p$) => {
357
- var _v$8 = {
498
+ var _v$9 = {
358
499
  fg: colors().muted
359
- }, _v$9 = {
500
+ }, _v$0 = {
360
501
  fg: colors().muted
361
502
  };
362
- _v$8 !== _p$.e && (_p$.e = _$setProp(_el$28, "style", _v$8, _p$.e));
363
- _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));
364
505
  return _p$;
365
506
  }, {
366
507
  e: void 0,
367
508
  t: void 0
368
509
  });
369
- return _el$27;
510
+ return _el$25;
370
511
  })()];
371
512
  }
372
513
  });
373
514
  },
374
515
  get children() {
375
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
+ })(), (() => {
376
522
  var _el$24 = _$createElement("text");
377
- _$insert(_el$24, error);
378
- _$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));
379
525
  return _el$24;
380
- })(), (() => {
381
- var _el$25 = _$createElement("text");
382
- _$insertNode(_el$25, _$createTextNode(`\u8F93\u5165 /go-config \u91CD\u65B0\u914D\u7F6E`));
383
- _$effect((_$p) => _$setProp(_el$25, "fg", colors().muted, _$p));
384
- return _el$25;
385
526
  })()];
386
527
  }
387
528
  })];
@@ -392,15 +533,19 @@ function GoUsagePanel(props) {
392
533
  fg: colors().muted
393
534
  }, _v$7 = {
394
535
  fg: colors().primary
536
+ }, _v$8 = {
537
+ fg: colors().muted
395
538
  };
396
539
  _v$5 !== _p$.e && (_p$.e = _$setProp(_el$1, "borderColor", _v$5, _p$.e));
397
540
  _v$6 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$6, _p$.t));
398
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));
399
543
  return _p$;
400
544
  }, {
401
545
  e: void 0,
402
546
  t: void 0,
403
- a: void 0
547
+ a: void 0,
548
+ o: void 0
404
549
  });
405
550
  return _el$1;
406
551
  })();
@@ -420,62 +565,121 @@ function createSidebarSlot(api) {
420
565
  }
421
566
  };
422
567
  }
423
- var tui = async (api) => {
424
- api.slots.register(createSidebarSlot(api));
425
- api.command?.register(() => [{
426
- title: "Go Usage: Configure",
427
- value: "go-usage.config",
428
- description: "\u8BBE\u7F6E OpenCode Go workspace ID \u548C auth cookie",
429
- slash: {
430
- name: "go-config"
568
+ function runConfigDialog(api, dialog) {
569
+ dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
570
+ get title() {
571
+ return t("wsPromptTitle");
572
+ },
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");
431
580
  },
432
- onSelect: (dialog) => {
581
+ onConfirm: (value) => {
582
+ const wsId = value.trim();
583
+ if (!wsId) {
584
+ dialog?.clear();
585
+ return;
586
+ }
587
+ saveConfig({
588
+ workspace_id: wsId
589
+ });
433
590
  dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
434
- title: "\u8F93\u5165 workspace_id",
591
+ get title() {
592
+ return t("cookiePromptTitle");
593
+ },
435
594
  description: () => (() => {
436
- var _el$33 = _$createElement("text");
437
- _$insertNode(_el$33, _$createTextNode(`\u5728 opencode.ai \u63A7\u5236\u53F0 URL \u4E2D\u83B7\u53D6\uFF08wrk_ \u5F00\u5934\uFF09`));
438
- return _el$33;
595
+ var _el$31 = _$createElement("text");
596
+ _$insert(_el$31, () => t("cookiePromptDesc"));
597
+ return _el$31;
439
598
  })(),
440
- placeholder: "wrk_xxxxxxxxxxxx",
441
- onConfirm: (value) => {
442
- const wsId = value.trim();
443
- if (!wsId) {
599
+ get placeholder() {
600
+ return t("cookiePlaceholder");
601
+ },
602
+ onConfirm: (val) => {
603
+ const cookie = val.trim();
604
+ if (!cookie) {
444
605
  dialog?.clear();
445
606
  return;
446
607
  }
447
608
  saveConfig({
448
- workspace_id: wsId
609
+ cookie
449
610
  });
450
- dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
451
- title: "\u8F93\u5165 auth cookie",
452
- description: () => (() => {
453
- var _el$35 = _$createElement("text");
454
- _$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`));
455
- return _el$35;
456
- })(),
457
- placeholder: "Fe26.2*...",
458
- onConfirm: (val) => {
459
- const cookie = val.trim();
460
- if (!cookie) {
461
- dialog?.clear();
462
- return;
463
- }
464
- saveConfig({
465
- cookie
466
- });
467
- api.ui.toast({
468
- variant: "success",
469
- message: "Go \u7528\u91CF\u914D\u7F6E\u5DF2\u4FDD\u5B58"
470
- });
471
- dialog?.clear();
472
- },
473
- onCancel: () => dialog?.clear()
474
- }));
611
+ setConfigTick((v) => v + 1);
612
+ api.ui.toast({
613
+ variant: "success",
614
+ message: t("configSaved")
615
+ });
616
+ dialog?.clear();
475
617
  },
476
618
  onCancel: () => dialog?.clear()
477
619
  }));
478
- }
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)
479
683
  }]);
480
684
  };
481
685
  var mod = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-go-usage-tui",
3
- "version": "1.0.0",
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",
@@ -38,4 +39,4 @@
38
39
  "esbuild": "^0.25.0",
39
40
  "esbuild-plugin-solid": "^0.5.0"
40
41
  }
41
- }
42
+ }
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 ?? {}
@@ -55,6 +59,20 @@ function saveConfig(patch: { workspace_id?: string; cookie?: string }): void {
55
59
  } catch { /* 写入失败忽略 */ }
56
60
  }
57
61
 
62
+ // 模块级共享信号:/go-config 保存配置后递增,面板监听它立即刷新
63
+ const [configTick, setConfigTick] = createSignal(0)
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
+
58
76
  interface Usage {
59
77
  rollingUsage: { usagePercent: number; resetInSec: number }
60
78
  weeklyUsage: { usagePercent: number; resetInSec: number }
@@ -104,13 +122,13 @@ async function fetchUsage(workspaceId: string, authCookie: string): Promise<Usag
104
122
  }
105
123
 
106
124
  function formatReset(sec: number): string {
107
- if (sec <= 0) return "已重置"
125
+ if (sec <= 0) return t("resetDone")
108
126
  const d = Math.floor(sec / 86400)
109
127
  const h = Math.floor((sec % 86400) / 3600)
110
128
  const m = Math.floor((sec % 3600) / 60)
111
- if (d > 0) return `${d}天${h}小时`
112
- if (h > 0) return `${h}小时${m}分钟`
113
- 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 })
114
132
  }
115
133
 
116
134
  function progressBar(percent: number, width: number): string {
@@ -172,8 +190,8 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
172
190
  }
173
191
  setConfigured(true)
174
192
  const u = await fetchUsage(cfg.workspaceId, cfg.authCookie)
175
- if (u === null) { setError("查询失败"); return }
176
- 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 }
177
195
  if (u.error === "not_configured") { setConfigured(false); return }
178
196
  setUsage(u)
179
197
  setLastUpdated(new Date().toLocaleTimeString())
@@ -183,15 +201,20 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
183
201
  onMount(() => {
184
202
  refresh()
185
203
  const timer = setInterval(refresh, CHECK_INTERVAL)
186
- onCleanup(() => clearInterval(timer))
204
+ // 监听配置变更(/go-config 保存后)立即刷新
205
+ const stopWatch = createEffect(() => {
206
+ void configTick()
207
+ refresh()
208
+ })
209
+ onCleanup(() => { clearInterval(timer); stopWatch() })
187
210
  })
188
211
 
189
212
  const [pal, setPal] = createSignal<Record<string, string>>({ ...FALLBACK })
190
213
  createEffect(() => {
191
- const t = props.theme as any
214
+ const th = props.theme as any
192
215
  const p: Record<string, string> = { ...FALLBACK }
193
216
  for (const k of Object.keys(FALLBACK)) {
194
- const h = hex(t?.[k])
217
+ const h = hex(th?.[k])
195
218
  if (h) p[k] = h
196
219
  }
197
220
  setPal(p)
@@ -242,10 +265,11 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
242
265
  >
243
266
  <text onMouseUp={() => setOpen(o => !o)}>
244
267
  <span style={{ fg: colors().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
245
- <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>
246
270
  <Show when={!open() && usage()}>
247
271
  <span style={{ fg: colorFor(usage()!.weeklyUsage.usagePercent) }}>
248
- {" ".repeat(2)} {usage()!.weeklyUsage.usagePercent}%
272
+ {" ".repeat(2)}{t("weeklyShort")} {usage()!.weeklyUsage.usagePercent}%
249
273
  </span>
250
274
  </Show>
251
275
  </text>
@@ -254,25 +278,25 @@ function GoUsagePanel(props: { theme: TuiThemeCurrent; api: TuiPluginApi }): JSX
254
278
  <text fg={colors().muted}>{sep()}</text>
255
279
 
256
280
  <Show when={!configured()}>
257
- <text fg={colors().warning}>未配置账号</text>
258
- <text fg={colors().muted}>输入 /go-config 设置</text>
281
+ <text fg={colors().warning}>{t("notConfigured")}</text>
282
+ <text fg={colors().muted}>{t("notConfiguredHint")}</text>
259
283
  </Show>
260
284
 
261
285
  <Show when={configured() && error()} fallback={
262
286
  <Show when={configured() && usage()} fallback={
263
- <Show when={configured()}><text fg={colors().muted}>加载中...</text></Show>
287
+ <Show when={configured()}><text fg={colors().muted}>{t("loading")}</text></Show>
264
288
  }>
265
- {renderRow("5小时用量", usage()!.rollingUsage)}
266
- {renderRow("每周用量", usage()!.weeklyUsage)}
267
- {renderRow("每月用量", usage()!.monthlyUsage)}
289
+ {renderRow(t("rowRolling"), usage()!.rollingUsage)}
290
+ {renderRow(t("rowWeekly"), usage()!.weeklyUsage)}
291
+ {renderRow(t("rowMonthly"), usage()!.monthlyUsage)}
268
292
  <text>
269
- <span style={{ fg: colors().muted }}>最近更新 </span>
293
+ <span style={{ fg: colors().muted }}>{t("updatedAt")} </span>
270
294
  <span style={{ fg: colors().muted }}>{lastUpdated()}</span>
271
295
  </text>
272
296
  </Show>
273
297
  }>
274
298
  <text fg={colors().error}>{error()}</text>
275
- <text fg={colors().muted}>输入 /go-config 重新配置</text>
299
+ <text fg={colors().muted}>{t("reconfigureHint")}</text>
276
300
  </Show>
277
301
  </Show>
278
302
  </box>
@@ -290,49 +314,100 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
290
314
  }
291
315
  }
292
316
 
293
- const tui: TuiPlugin = async (api: TuiPluginApi) => {
294
- api.slots.register(createSidebarSlot(api))
295
-
296
- api.command?.register(() => [
297
- {
298
- title: "Go Usage: Configure",
299
- value: "go-usage.config",
300
- description: "设置 OpenCode Go workspace ID 和 auth cookie",
301
- slash: { name: "go-config" },
302
- 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 })
303
329
  dialog?.replace(() => (
304
330
  <api.ui.DialogPrompt
305
- title="输入 workspace_id"
331
+ title={t("cookiePromptTitle")}
306
332
  description={() => (
307
- <text>在 opencode.ai 控制台 URL 中获取(wrk_ 开头)</text>
333
+ <text>{t("cookiePromptDesc")}</text>
308
334
  )}
309
- placeholder="wrk_xxxxxxxxxxxx"
310
- onConfirm={(value) => {
311
- const wsId = value.trim()
312
- if (!wsId) { dialog?.clear(); return }
313
- saveConfig({ workspace_id: wsId })
314
- dialog?.replace(() => (
315
- <api.ui.DialogPrompt
316
- title="输入 auth cookie"
317
- description={() => (
318
- <text>登录 opencode.ai 后,浏览器 F12 → Application → Cookies → opencode.ai → 复制 auth 的值</text>
319
- )}
320
- placeholder="Fe26.2*..."
321
- onConfirm={(val) => {
322
- const cookie = val.trim()
323
- if (!cookie) { dialog?.clear(); return }
324
- saveConfig({ cookie })
325
- api.ui.toast({ variant: "success", message: "Go 用量配置已保存" })
326
- dialog?.clear()
327
- }}
328
- onCancel={() => dialog?.clear()}
329
- />
330
- ))
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()
331
343
  }}
332
344
  onCancel={() => dialog?.clear()}
333
345
  />
334
346
  ))
335
- },
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),
336
411
  },
337
412
  ])
338
413
  }