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 +9 -0
- package/README_EN.md +103 -0
- package/build.tui.mjs +4 -0
- package/dist/tui.js +288 -93
- package/package.json +3 -2
- package/src/i18n.ts +108 -0
- package/src/index.tsx +120 -54
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 "
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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("
|
|
307
|
+
setError(t("queryFailed"));
|
|
181
308
|
return;
|
|
182
309
|
}
|
|
183
310
|
if (u.error === "cookie_expired") {
|
|
184
|
-
setError("
|
|
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
|
|
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(
|
|
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
|
-
_$
|
|
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$
|
|
299
|
-
_$insertNode(_el$
|
|
300
|
-
_$insertNode(_el$
|
|
301
|
-
_$
|
|
302
|
-
_$
|
|
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$
|
|
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$
|
|
315
|
-
_$insert(_el$
|
|
316
|
-
_$effect((_$p) => _$setProp(_el$
|
|
317
|
-
return _el$
|
|
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$
|
|
325
|
-
_$
|
|
326
|
-
_$effect((_$p) => _$setProp(_el$
|
|
327
|
-
return _el$
|
|
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
|
-
_$
|
|
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$
|
|
351
|
-
_$
|
|
352
|
-
_$effect((_$p) => _$setProp(_el$
|
|
353
|
-
return _el$
|
|
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("
|
|
359
|
-
var _el$
|
|
360
|
-
_$insertNode(_el$
|
|
361
|
-
_$insertNode(_el$
|
|
362
|
-
_$insertNode(_el$
|
|
363
|
-
_$insert(_el$
|
|
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$
|
|
498
|
+
var _v$9 = {
|
|
366
499
|
fg: colors().muted
|
|
367
|
-
}, _v$
|
|
500
|
+
}, _v$0 = {
|
|
368
501
|
fg: colors().muted
|
|
369
502
|
};
|
|
370
|
-
_v$
|
|
371
|
-
_v$
|
|
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$
|
|
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,
|
|
386
|
-
_$effect((_$p) => _$setProp(_el$24, "fg", colors().
|
|
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
|
-
|
|
432
|
-
api.
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
|
591
|
+
get title() {
|
|
592
|
+
return t("cookiePromptTitle");
|
|
593
|
+
},
|
|
443
594
|
description: () => (() => {
|
|
444
|
-
var _el$
|
|
445
|
-
_$
|
|
446
|
-
return _el$
|
|
595
|
+
var _el$31 = _$createElement("text");
|
|
596
|
+
_$insert(_el$31, () => t("cookiePromptDesc"));
|
|
597
|
+
return _el$31;
|
|
447
598
|
})(),
|
|
448
|
-
placeholder
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
609
|
+
cookie
|
|
457
610
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
|
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
|
|
115
|
-
if (h > 0) return
|
|
116
|
-
return
|
|
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("
|
|
179
|
-
if (u.error === "cookie_expired") { setError("
|
|
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
|
|
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(
|
|
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>
|
|
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)}
|
|
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}
|
|
266
|
-
<text fg={colors().muted}
|
|
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}
|
|
287
|
+
<Show when={configured()}><text fg={colors().muted}>{t("loading")}</text></Show>
|
|
272
288
|
}>
|
|
273
|
-
{renderRow("
|
|
274
|
-
{renderRow("
|
|
275
|
-
{renderRow("
|
|
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 }}
|
|
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}
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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="
|
|
331
|
+
title={t("cookiePromptTitle")}
|
|
314
332
|
description={() => (
|
|
315
|
-
<text
|
|
333
|
+
<text>{t("cookiePromptDesc")}</text>
|
|
316
334
|
)}
|
|
317
|
-
placeholder="
|
|
318
|
-
onConfirm={(
|
|
319
|
-
const
|
|
320
|
-
if (!
|
|
321
|
-
saveConfig({
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
}
|