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 +9 -0
- package/README_EN.md +103 -0
- package/build.tui.mjs +4 -0
- package/dist/tui.js +297 -93
- package/package.json +4 -3
- package/src/i18n.ts +108 -0
- package/src/index.tsx +129 -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 = {};
|
|
@@ -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 "
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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("
|
|
307
|
+
setError(t("queryFailed"));
|
|
180
308
|
return;
|
|
181
309
|
}
|
|
182
310
|
if (u.error === "cookie_expired") {
|
|
183
|
-
setError("
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
_$
|
|
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$
|
|
291
|
-
_$insertNode(_el$
|
|
292
|
-
_$insertNode(_el$
|
|
293
|
-
_$
|
|
294
|
-
_$
|
|
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$
|
|
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$
|
|
307
|
-
_$insert(_el$
|
|
308
|
-
_$effect((_$p) => _$setProp(_el$
|
|
309
|
-
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;
|
|
310
450
|
})(), _$createComponent(Show, {
|
|
311
451
|
get when() {
|
|
312
452
|
return !configured();
|
|
313
453
|
},
|
|
314
454
|
get children() {
|
|
315
455
|
return [(() => {
|
|
316
|
-
var _el$
|
|
317
|
-
_$
|
|
318
|
-
_$effect((_$p) => _$setProp(_el$
|
|
319
|
-
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;
|
|
320
460
|
})(), (() => {
|
|
321
461
|
var _el$22 = _$createElement("text");
|
|
322
|
-
_$
|
|
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$
|
|
343
|
-
_$
|
|
344
|
-
_$effect((_$p) => _$setProp(_el$
|
|
345
|
-
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;
|
|
346
486
|
}
|
|
347
487
|
});
|
|
348
488
|
},
|
|
349
489
|
get children() {
|
|
350
|
-
return [_$memo(() => renderRow("
|
|
351
|
-
var _el$
|
|
352
|
-
_$insertNode(_el$
|
|
353
|
-
_$insertNode(_el$
|
|
354
|
-
_$insertNode(_el$
|
|
355
|
-
_$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);
|
|
356
497
|
_$effect((_p$) => {
|
|
357
|
-
var _v$
|
|
498
|
+
var _v$9 = {
|
|
358
499
|
fg: colors().muted
|
|
359
|
-
}, _v$
|
|
500
|
+
}, _v$0 = {
|
|
360
501
|
fg: colors().muted
|
|
361
502
|
};
|
|
362
|
-
_v$
|
|
363
|
-
_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));
|
|
364
505
|
return _p$;
|
|
365
506
|
}, {
|
|
366
507
|
e: void 0,
|
|
367
508
|
t: void 0
|
|
368
509
|
});
|
|
369
|
-
return _el$
|
|
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,
|
|
378
|
-
_$effect((_$p) => _$setProp(_el$24, "fg", colors().
|
|
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
|
-
|
|
424
|
-
api.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
description:
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
591
|
+
get title() {
|
|
592
|
+
return t("cookiePromptTitle");
|
|
593
|
+
},
|
|
435
594
|
description: () => (() => {
|
|
436
|
-
var _el$
|
|
437
|
-
_$
|
|
438
|
-
return _el$
|
|
595
|
+
var _el$31 = _$createElement("text");
|
|
596
|
+
_$insert(_el$31, () => t("cookiePromptDesc"));
|
|
597
|
+
return _el$31;
|
|
439
598
|
})(),
|
|
440
|
-
placeholder
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
609
|
+
cookie
|
|
449
610
|
});
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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.
|
|
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
|
|
112
|
-
if (h > 0) return
|
|
113
|
-
return
|
|
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("
|
|
176
|
-
if (u.error === "cookie_expired") { setError("
|
|
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
|
-
|
|
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
|
|
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(
|
|
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>
|
|
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)}
|
|
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}
|
|
258
|
-
<text fg={colors().muted}
|
|
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}
|
|
287
|
+
<Show when={configured()}><text fg={colors().muted}>{t("loading")}</text></Show>
|
|
264
288
|
}>
|
|
265
|
-
{renderRow("
|
|
266
|
-
{renderRow("
|
|
267
|
-
{renderRow("
|
|
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 }}
|
|
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}
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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="
|
|
331
|
+
title={t("cookiePromptTitle")}
|
|
306
332
|
description={() => (
|
|
307
|
-
<text
|
|
333
|
+
<text>{t("cookiePromptDesc")}</text>
|
|
308
334
|
)}
|
|
309
|
-
placeholder="
|
|
310
|
-
onConfirm={(
|
|
311
|
-
const
|
|
312
|
-
if (!
|
|
313
|
-
saveConfig({
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
}
|