@yinxe/opencode-tui-usage 0.0.6

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 ADDED
@@ -0,0 +1,285 @@
1
+ # OpenCode TUI Usage Plugin
2
+
3
+ OpenCode TUI 插件,在侧边栏显示用量和额度信息,支持多额度 provider。
4
+
5
+ ![preview](./preview.png)
6
+
7
+ ## 功能特性
8
+
9
+ - 📊 实时显示 Rolling / Weekly / Monthly 三种维度的额度使用情况
10
+ - 🎨 彩色标签:Rolling(绿) / Weekly(黄) / Monthly(蓝)
11
+ - 📈 进度条可视化展示使用比例
12
+ - 🔄 自动根据当前会话的 provider 切换数据源
13
+ - 🛠️ 支持扩展新的 provider 适配器
14
+
15
+ ## 安装
16
+
17
+ 包发布在 npm 和 GitHub Packages,推荐从 npm 安装。
18
+
19
+ ### 从 npm 安装(推荐)
20
+
21
+ ```bash
22
+ npm install @yinx-in/opencode-tui-usage
23
+ ```
24
+
25
+ ### 从 GitHub Packages 安装
26
+
27
+ ```bash
28
+ # 设置 registry
29
+ npm config set @yinx-in:registry https://npm.pkg.github.com
30
+
31
+ # 登录(需要 GitHub Personal Access Token)
32
+ echo "YOUR_GITHUB_TOKEN" | npm login --registry=https://npm.pkg.github.com --username=YOUR_GITHUB_USERNAME --email=you@example.com
33
+
34
+ # 安装
35
+ npm install @yinx-in/opencode-tui-usage
36
+ ```
37
+
38
+ ### 配置插件
39
+
40
+ 安装后,在 `~/.config/opencode/tui.json` 中添加:
41
+
42
+ ```json
43
+ {
44
+ "$schema": "https://opencode.ai/tui.json",
45
+ "plugin": ["@yinx-in/opencode-tui-usage"]
46
+ }
47
+ ```
48
+
49
+ ### 本地安装(开发)
50
+
51
+ 1. 克隆或下载此项目到本地
52
+ 2. 在 `~/.config/opencode/tui.json` 中添加插件路径:
53
+
54
+ ```json
55
+ {
56
+ "$schema": "https://opencode.ai/tui.json",
57
+ "plugin": ["/absolute/path/to/opencode-tui-usage-plugin"]
58
+ }
59
+ ```
60
+
61
+ 3. 重启 OpenCode 使插件生效
62
+
63
+ ## 配置额度 Provider
64
+
65
+ 插件支持多个额度 provider,会根据当前会话的 `providerID` 自动选择对应的适配器。
66
+
67
+ ### MiniMax-CN
68
+
69
+ 适用于 `providerID` 为 `minimax-cn-coding-plan` 的会话。
70
+
71
+ 创建 `~/.config/opencode/usage.provider.json`:
72
+
73
+ ```json
74
+ {
75
+ "providers": {
76
+ "minimax-cn-coding-plan": {
77
+ "apiKey": "${MINIMAX_API_KEY}"
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ 设置环境变量:
84
+
85
+ ```bash
86
+ export MINIMAX_API_KEY="your-api-key-here"
87
+ ```
88
+
89
+ ### OpenCode-Go
90
+
91
+ 适用于 `providerID` 为 `opencode-go` 的会话。
92
+
93
+ 在 `~/.config/opencode/usage.provider.json` 中添加:
94
+
95
+ ```json
96
+ {
97
+ "providers": {
98
+ "opencode-go": {
99
+ "cookie": "${OPENCODE_GO_AUTH_COOKIE}",
100
+ "workspaceId": "${OPENCODE_GO_WORKSPACE_ID}"
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ 设置环境变量:
107
+
108
+ ```bash
109
+ export OPENCODE_GO_AUTH_COOKIE="your-cookie"
110
+ export OPENCODE_GO_WORKSPACE_ID="wrk_xxxxxxxxxxxx"
111
+ ```
112
+
113
+ ### 获取 OpenCode-Go 配置
114
+
115
+ 1. 登录 https://opencode.ai
116
+ 2. 打开浏览器开发者工具 → Network
117
+ 3. 访问 `/workspace/{workspaceId}/usage` 页面
118
+ 4. 找到 `_server` 请求,从 Request Headers 复制完整的 `cookie`
119
+ 5. workspaceId 从 URL 中获取(格式:`wrk_` 开头)
120
+
121
+ ## 开发
122
+
123
+ ```bash
124
+ # 安装依赖
125
+ npm install
126
+
127
+ # 类型检查
128
+ npm run lint
129
+
130
+ # 构建输出到 dist/
131
+ npm run build
132
+
133
+ # 监听模式(开发时使用)
134
+ npm run dev
135
+ ```
136
+
137
+ ## 目录结构
138
+
139
+ ```
140
+ src/
141
+ ├── tui.tsx # 插件入口,注册 sidebar_content slot
142
+ ├── usage.tsx # Usage 组件(彩色标签 + 进度条)
143
+ ├── session-info.tsx # Session Info 组件
144
+ ├── components.tsx # 可复用组件
145
+ └── quota/ # 额度服务
146
+ ├── types.ts # QuotaData, QuotaResult 类型定义
147
+ ├── provider.ts # QuotaProvider 接口
148
+ ├── service.ts # QuotaService 管理多 provider
149
+ ├── config.ts # 读取 usage.provider.json
150
+ └── providers/ # provider 适配器
151
+ ├── minimax.ts
152
+ └── opencode-go.ts
153
+ ```
154
+
155
+ ## 添加新的 Provider 适配器
156
+
157
+ 如果需要支持新的额度来源(如其他 AI provider),按以下步骤添加:
158
+
159
+ ### 1. 创建适配器文件
160
+
161
+ 在 `src/quota/providers/` 下创建 `{provider-name}.ts`:
162
+
163
+ ```typescript
164
+ import type { QuotaData, ProviderConfig } from "../types.js";
165
+ import { QuotaProvider, resolveEnvVar } from "../provider.js";
166
+
167
+ export class MyQuotaProvider implements QuotaProvider {
168
+ readonly name = "my-provider"; // 与 usage.provider.json 中的 key 对应
169
+
170
+ init(config: ProviderConfig, _credentials: Record<string, unknown>): void {
171
+ // 读取 config 并解析环境变量
172
+ }
173
+
174
+ async fetchQuota(): Promise<QuotaData | null> {
175
+ // 调用 API 并返回 QuotaData 格式
176
+ }
177
+ }
178
+ ```
179
+
180
+ ### 2. 注册到 QuotaService
181
+
182
+ 在 `src/quota/service.ts` 构造函数中添加:
183
+
184
+ ```typescript
185
+ this.registerProvider(new MyQuotaProvider());
186
+ ```
187
+
188
+ ### 3. 添加配置
189
+
190
+ 在 `~/.config/opencode/usage.provider.json` 中添加 provider 配置:
191
+
192
+ ```json
193
+ {
194
+ "providers": {
195
+ "my-provider": {
196
+ "apiKey": "${MY_API_KEY}"
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### 4. 测试
203
+
204
+ 切换到对应 provider 的会话,检查侧边栏是否正常显示额度数据。
205
+
206
+ ## 调试
207
+
208
+ 查看插件日志:
209
+
210
+ ```bash
211
+ cat ~/.local/share/opencode/log/$(ls -t ~/.local/share/opencode/log/ | head -1) | grep -i "tui.plugin\|QuotaService\|MiniMaxCN\|OpenCodeGo"
212
+ ```
213
+
214
+ 常见问题:
215
+
216
+ | 问题 | 原因 | 解决方案 |
217
+ |------|------|----------|
218
+ | 侧边栏无显示 | 缺少 `"oc-plugin": ["tui"]` | 检查 package.json |
219
+ | JSX 报错 | 缺少 pragma | 每个 .tsx 顶部加 `/** @jsxImportSource @opentui/solid */` |
220
+ | 显示 "No data" | provider 未注册或配置缺失 | 检查 usage.provider.json 和环境变量 |
221
+ | opencode-go 500 错误 | cookie 过期或 headers 不对 | 重新抓包获取最新 cookie |
222
+
223
+ ## 技术栈
224
+
225
+ - **Solid.js** - 响应式 UI 框架
226
+ - **@opentui/solid** - TUI 组件库(`<box>`, `<text>` 等)
227
+ - **TypeScript** - 类型安全
228
+
229
+ ## CI/CD 自动化发版
230
+
231
+ 本项目使用 GitHub Actions 实现自动化发版。推送 `v*` 格式的 tag 后,**同时发布到 npm 和 GitHub Packages**。
232
+
233
+ ### 发布流程
234
+
235
+ 使用 `npm version` 管理版本号(会自动创建 tag):
236
+
237
+ ```bash
238
+ # 更新版本并创建 tag
239
+ npm version patch # 0.0.1 → 0.0.2
240
+ npm version minor # 0.0.1 → 0.1.0
241
+ npm version major # 0.0.1 → 1.0.0
242
+
243
+ # 推送 tag 触发 CI/CD
244
+ git push origin v0.0.3
245
+ ```
246
+
247
+ ### 自动触发的工作流
248
+
249
+ 推送 tag 后,以下 job 会自动执行:
250
+
251
+ ```
252
+ build → publish-npm + publish-github
253
+ ```
254
+
255
+ | Job | 目标 | 依赖 |
256
+ |-----|------|------|
257
+ | `build` | 安装依赖、构建项目、运行测试 | - |
258
+ | `publish-npm` | 发布到 [npm registry](https://www.npmjs.com/) | build |
259
+ | `publish-github` | 发布到 [GitHub Packages](https://github.com/Yinxe/opencode-tui-usage/packages) | build |
260
+
261
+ ### 首次发版配置
262
+
263
+ 1. **GitHub Packages 认证**
264
+ - 无需额外配置,使用内置 `GITHUB_TOKEN`
265
+
266
+ 2. **npm 认证**(如需发布到 npm)
267
+ - 在 [npm.npmjs.com](https://www.npmjs.com/) 创建 Access Token
268
+ - 在 GitHub 仓库 Settings → Secrets and variables → Actions 添加 secret:
269
+ - Name: `npm_token`
270
+ - Secret: 你的 npm access token
271
+
272
+ 3. **scoped 包配置**
273
+ - 包名 `@yinx-in/opencode-tui-usage` 已在 package.json 中配置
274
+ - `publishConfig.registry` 指定发布到哪个 registry
275
+
276
+ ### 发布地址
277
+
278
+ | 平台 | 包名 | 地址 |
279
+ |------|------|------|
280
+ | npm | `@yinx-in/opencode-tui-usage` | https://www.npmjs.com/package/@yinx-in/opencode-tui-usage |
281
+ | GitHub Packages | `@yinx-in/opencode-tui-usage` | https://github.com/Yinxe/opencode-tui-usage/packages |
282
+
283
+ ## License
284
+
285
+ MIT
@@ -0,0 +1,20 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { JSX } from "solid-js";
3
+ export interface LabelValueProps {
4
+ label: string;
5
+ value: string | number;
6
+ labelColor?: string;
7
+ }
8
+ export declare function LabelValue(props: LabelValueProps): JSX.Element;
9
+ export interface TitleProps {
10
+ text: string;
11
+ color?: string;
12
+ }
13
+ export declare function Title(props: TitleProps): JSX.Element;
14
+ export interface ProgressBarProps {
15
+ value: number;
16
+ color?: string;
17
+ width?: number;
18
+ }
19
+ export declare function ProgressBar(props: ProgressBarProps): JSX.Element;
20
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CAQ9D;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,CAAC,OAAO,CAEpD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,GAAG,CAAC,OAAO,CAchE"}
@@ -0,0 +1,22 @@
1
+ export function LabelValue(props) {
2
+ return (<box flexDirection="row" gap={1}>
3
+ <text fg={props.labelColor}>{props.label}</text>
4
+ <text>:</text>
5
+ <text>{props.value}</text>
6
+ </box>);
7
+ }
8
+ export function Title(props) {
9
+ return <text fg={props.color}>{props.text}</text>;
10
+ }
11
+ export function ProgressBar(props) {
12
+ const width = props.width ?? 20;
13
+ const filled = Math.round((props.value / 100) * width);
14
+ const empty = filled === 0 ? width - 1 : width - filled;
15
+ const barColor = props.color ?? '#6bcf7f';
16
+ return (<box flexDirection="row" gap={0}>
17
+ <text>[</text>
18
+ <text fg={barColor}>{'■'.repeat(filled)}</text>
19
+ <text>{' '.repeat(empty)}</text>
20
+ <text>]</text>
21
+ </box>);
22
+ }
@@ -0,0 +1,4 @@
1
+ import tui from "./tui.jsx";
2
+ export { tui };
3
+ export default tui;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,WAAW,CAAC;AAE5B,OAAO,EAAE,GAAG,EAAE,CAAC;AACf,eAAe,GAAG,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import tui from "./tui.jsx";
2
+ export { tui };
3
+ export default tui;
@@ -0,0 +1,4 @@
1
+ import type { ProviderRegistry, ProviderConfig } from "./types.js";
2
+ export declare function readProviderConfig(): ProviderRegistry;
3
+ export declare function getProviderConfig(registry: ProviderRegistry, providerName: string): ProviderConfig | undefined;
4
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/quota/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASnE,wBAAgB,kBAAkB,IAAI,gBAAgB,CAWrD;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAI5B"}
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from "fs";
2
+ import { resolve, join } from "path";
3
+ const HOME_DIR = process.env.HOME || process.env.USERPROFILE || "";
4
+ const OPENCODE_CONFIG_DIR = join(HOME_DIR, ".config", "opencode");
5
+ export function readProviderConfig() {
6
+ try {
7
+ const configPath = resolve(OPENCODE_CONFIG_DIR, "usage.provider.json");
8
+ const content = readFileSync(configPath, "utf-8");
9
+ const data = JSON.parse(content);
10
+ console.log("[QuotaService] Config content:", JSON.stringify(data));
11
+ return data.providers || {};
12
+ }
13
+ catch (error) {
14
+ console.warn("[QuotaService] Failed to read usage.provider.json:", error);
15
+ return {};
16
+ }
17
+ }
18
+ export function getProviderConfig(registry, providerName) {
19
+ console.log("[QuotaService] Looking up provider:", providerName);
20
+ console.log("[QuotaService] Registry keys:", Object.keys(registry));
21
+ return registry[providerName];
22
+ }
@@ -0,0 +1,9 @@
1
+ import type { QuotaData, ProviderConfig } from "./types.js";
2
+ export interface QuotaProvider {
3
+ readonly name: string;
4
+ init(config: ProviderConfig, credentials: Record<string, unknown>): void;
5
+ fetchQuota(): Promise<QuotaData | null>;
6
+ resolveEnvVar(value: string | undefined): string | undefined;
7
+ }
8
+ export declare const resolveEnvVar: (value: string | undefined) => string | undefined;
9
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/quota/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAEzE,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAExC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;CAC9D;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAOlE,CAAC"}
@@ -0,0 +1,9 @@
1
+ export const resolveEnvVar = (value) => {
2
+ if (!value)
3
+ return undefined;
4
+ const match = value.match(/^\$\{(\w+)\}$/);
5
+ if (match) {
6
+ return process.env[match[1]];
7
+ }
8
+ return value;
9
+ };
@@ -0,0 +1,13 @@
1
+ import type { QuotaData, ProviderConfig } from "../types.js";
2
+ import { QuotaProvider } from "../provider.js";
3
+ export declare class MiniMaxCNQuotaProvider implements QuotaProvider {
4
+ readonly name = "minimax-cn-coding-plan";
5
+ private apiKey;
6
+ private baseUrl;
7
+ init(config: ProviderConfig, _credentials: Record<string, unknown>): void;
8
+ fetchQuota(): Promise<QuotaData | null>;
9
+ resolveEnvVar(value: string | undefined): string | undefined;
10
+ private mapResponseToQuotaData;
11
+ private formatDuration;
12
+ }
13
+ //# sourceMappingURL=minimax.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minimax.d.ts","sourceRoot":"","sources":["../../../src/quota/providers/minimax.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAwB9D,qBAAa,sBAAuB,YAAW,aAAa;IAC1D,QAAQ,CAAC,IAAI,4BAA4B;IAEzC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAA8B;IAE7C,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAKnE,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA8C7C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;IAI5D,OAAO,CAAC,sBAAsB;IA8C9B,OAAO,CAAC,cAAc;CAMvB"}
@@ -0,0 +1,94 @@
1
+ import { resolveEnvVar } from "../provider.js";
2
+ export class MiniMaxCNQuotaProvider {
3
+ name = "minimax-cn-coding-plan";
4
+ apiKey;
5
+ baseUrl = "https://www.minimaxi.com";
6
+ init(config, _credentials) {
7
+ const apiKeyRaw = config.apiKey;
8
+ this.apiKey = resolveEnvVar(apiKeyRaw) || resolveEnvVar(config.apiKeyEnvVar);
9
+ }
10
+ async fetchQuota() {
11
+ if (!this.apiKey) {
12
+ console.warn("[MiniMaxCNQuotaProvider] Missing apiKey");
13
+ return null;
14
+ }
15
+ try {
16
+ const response = await fetch(`${this.baseUrl}/v1/api/openplatform/coding_plan/remains`, {
17
+ method: "GET",
18
+ headers: {
19
+ Authorization: `Bearer ${this.apiKey}`,
20
+ "Content-Type": "application/json",
21
+ },
22
+ });
23
+ if (!response.ok) {
24
+ console.error(`[MiniMaxCNQuotaProvider] API error: ${response.status}`);
25
+ return null;
26
+ }
27
+ const data = (await response.json());
28
+ if (data.base_resp?.status_code !== 0) {
29
+ console.error(`[MiniMaxCNQuotaProvider] API error: ${data.base_resp?.status_msg}`);
30
+ return null;
31
+ }
32
+ const codingPlanModels = data.model_remains.filter((m) => m.model_name.startsWith("MiniMax-M"));
33
+ if (codingPlanModels.length === 0) {
34
+ console.warn("[MiniMaxCNQuotaProvider] No coding plan models found");
35
+ return null;
36
+ }
37
+ return this.mapResponseToQuotaData(codingPlanModels);
38
+ }
39
+ catch (error) {
40
+ console.error("[MiniMaxCNQuotaProvider] Fetch failed:", error);
41
+ return null;
42
+ }
43
+ }
44
+ resolveEnvVar(value) {
45
+ return resolveEnvVar(value);
46
+ }
47
+ mapResponseToQuotaData(models) {
48
+ let totalRollingAvailable = 0;
49
+ let totalRollingLimit = 0;
50
+ let rollingResetMs = 0;
51
+ let totalWeeklyAvailable = 0;
52
+ let totalWeeklyLimit = 0;
53
+ let weeklyResetMs = 0;
54
+ for (const m of models) {
55
+ totalRollingAvailable += m.current_interval_usage_count;
56
+ totalRollingLimit += m.current_interval_total_count;
57
+ totalWeeklyAvailable += m.current_weekly_usage_count;
58
+ totalWeeklyLimit += m.current_weekly_total_count;
59
+ rollingResetMs = Math.max(rollingResetMs, m.end_time);
60
+ weeklyResetMs = Math.max(weeklyResetMs, m.weekly_end_time);
61
+ }
62
+ const rollingUsed = Math.max(0, totalRollingLimit - totalRollingAvailable);
63
+ const weeklyUsed = Math.max(0, totalWeeklyLimit - totalWeeklyAvailable);
64
+ const rollingPercent = totalRollingLimit > 0
65
+ ? Math.round((rollingUsed / totalRollingLimit) * 100)
66
+ : 0;
67
+ const weeklyPercent = totalWeeklyLimit > 0
68
+ ? Math.round((weeklyUsed / totalWeeklyLimit) * 100)
69
+ : 0;
70
+ const now = Date.now();
71
+ const rollingResetSec = Math.max(0, Math.floor((rollingResetMs - now) / 1000));
72
+ const weeklyResetSec = Math.max(0, Math.floor((weeklyResetMs - now) / 1000));
73
+ return {
74
+ rolling: {
75
+ usage: rollingPercent,
76
+ reset: this.formatDuration(rollingResetSec),
77
+ },
78
+ weekly: {
79
+ usage: weeklyPercent,
80
+ reset: this.formatDuration(weeklyResetSec),
81
+ },
82
+ monthly: undefined,
83
+ };
84
+ }
85
+ formatDuration(seconds) {
86
+ if (seconds < 60)
87
+ return `${seconds}s`;
88
+ if (seconds < 3600)
89
+ return `${Math.round(seconds / 60)}m`;
90
+ if (seconds < 86400)
91
+ return `${Math.round(seconds / 3600)}h`;
92
+ return `${Math.round(seconds / 86400)}d`;
93
+ }
94
+ }
@@ -0,0 +1,14 @@
1
+ import type { QuotaData, ProviderConfig } from "../types.js";
2
+ import { QuotaProvider } from "../provider.js";
3
+ export declare class OpenCodeGoQuotaProvider implements QuotaProvider {
4
+ readonly name = "opencode-go";
5
+ private cookie;
6
+ private workspaceId;
7
+ private serviceId;
8
+ private baseUrl;
9
+ init(config: ProviderConfig, _credentials: Record<string, unknown>): void;
10
+ fetchQuota(): Promise<QuotaData | null>;
11
+ resolveEnvVar(value: string | undefined): string | undefined;
12
+ private formatDuration;
13
+ }
14
+ //# sourceMappingURL=opencode-go.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-go.d.ts","sourceRoot":"","sources":["../../../src/quota/providers/opencode-go.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAiB9D,qBAAa,uBAAwB,YAAW,aAAa;IAC3D,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,SAAS,CAAsE;IACvF,OAAO,CAAC,OAAO,CAAyB;IAExC,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAKnE,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAsE7C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;IAI5D,OAAO,CAAC,cAAc;CAMvB"}
@@ -0,0 +1,85 @@
1
+ import { resolveEnvVar } from "../provider.js";
2
+ export class OpenCodeGoQuotaProvider {
3
+ name = "opencode-go";
4
+ cookie;
5
+ workspaceId;
6
+ serviceId = "c7389bd0e731f80f49593e5ee53835475f4e28594dd6bd83eb229bab753498cd";
7
+ baseUrl = "https://opencode.ai";
8
+ init(config, _credentials) {
9
+ this.cookie = resolveEnvVar(config.cookie);
10
+ this.workspaceId = resolveEnvVar(config.workspaceId);
11
+ }
12
+ async fetchQuota() {
13
+ if (!this.cookie || !this.workspaceId) {
14
+ console.warn("[OpenCodeGoQuotaProvider] Missing cookie or workspaceId");
15
+ return null;
16
+ }
17
+ const args = JSON.stringify({
18
+ t: { t: 9, i: 0, l: 1, a: [{ t: 1, s: this.workspaceId }], o: 0 },
19
+ f: 31,
20
+ m: [],
21
+ });
22
+ const url = `${this.baseUrl}/_server?id=${this.serviceId}&args=${encodeURIComponent(args)}`;
23
+ try {
24
+ const response = await fetch(url, {
25
+ method: "GET",
26
+ headers: {
27
+ accept: "*/*",
28
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,pt;q=0.5,pl;q=0.4",
29
+ cookie: this.cookie,
30
+ referer: `${this.baseUrl}/workspace/${this.workspaceId}/usage`,
31
+ "x-server-id": this.serviceId,
32
+ "x-server-instance": "server-fn:3",
33
+ priority: "u=1, i",
34
+ "sec-fetch-mode": "cors",
35
+ "sec-fetch-site": "same-origin",
36
+ "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0",
37
+ },
38
+ });
39
+ if (!response.ok) {
40
+ console.error(`[OpenCodeGoQuotaProvider] API error: ${response.status}`);
41
+ return null;
42
+ }
43
+ const text = await response.text();
44
+ const rollingMatch = text.match(/rollingUsage:\$R\[1\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
45
+ const weeklyMatch = text.match(/weeklyUsage:\$R\[2\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
46
+ const monthlyMatch = text.match(/monthlyUsage:\$R\[3\]=\{status:"([^"]+)",resetInSec:(\d+),usagePercent:(\d+)\}/);
47
+ if (!rollingMatch || !weeklyMatch || !monthlyMatch) {
48
+ console.error("[OpenCodeGoQuotaProvider] Failed to parse response:", text.substring(0, 200));
49
+ return null;
50
+ }
51
+ const rollingUsage = { status: rollingMatch[1], resetInSec: parseInt(rollingMatch[2], 10), usagePercent: parseInt(rollingMatch[3], 10) };
52
+ const weeklyUsage = { status: weeklyMatch[1], resetInSec: parseInt(weeklyMatch[2], 10), usagePercent: parseInt(weeklyMatch[3], 10) };
53
+ const monthlyUsage = { status: monthlyMatch[1], resetInSec: parseInt(monthlyMatch[2], 10), usagePercent: parseInt(monthlyMatch[3], 10) };
54
+ return {
55
+ rolling: {
56
+ usage: rollingUsage.usagePercent,
57
+ reset: this.formatDuration(rollingUsage.resetInSec),
58
+ },
59
+ weekly: {
60
+ usage: weeklyUsage.usagePercent,
61
+ reset: this.formatDuration(weeklyUsage.resetInSec),
62
+ },
63
+ monthly: monthlyUsage.status !== "unlimited"
64
+ ? { usage: monthlyUsage.usagePercent, reset: this.formatDuration(monthlyUsage.resetInSec) }
65
+ : undefined,
66
+ };
67
+ }
68
+ catch (error) {
69
+ console.error("[OpenCodeGoQuotaProvider] Fetch failed:", error);
70
+ return null;
71
+ }
72
+ }
73
+ resolveEnvVar(value) {
74
+ return resolveEnvVar(value);
75
+ }
76
+ formatDuration(seconds) {
77
+ if (seconds < 60)
78
+ return `${seconds}s`;
79
+ if (seconds < 3600)
80
+ return `${Math.round(seconds / 60)}m`;
81
+ if (seconds < 86400)
82
+ return `${Math.round(seconds / 3600)}h`;
83
+ return `${Math.round(seconds / 86400)}d`;
84
+ }
85
+ }
@@ -0,0 +1,15 @@
1
+ import type { QuotaResult } from "./types.js";
2
+ import type { QuotaProvider } from "./provider.js";
3
+ export declare class QuotaService {
4
+ private providers;
5
+ private providerRegistry;
6
+ private activeProviderName;
7
+ private activeProvider;
8
+ private refreshCount;
9
+ constructor();
10
+ registerProvider(provider: QuotaProvider): void;
11
+ setActiveProvider(providerName: string): boolean;
12
+ getActiveProviderName(): string | null;
13
+ fetchQuota(): Promise<QuotaResult | null>;
14
+ }
15
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/quota/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAKnD,qBAAa,YAAY;IACvB,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,gBAAgB,CAAwB;IAChD,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,YAAY,CAAK;;IAOzB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI/C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAmBhD,qBAAqB,IAAI,MAAM,GAAG,IAAI;IAIhC,UAAU,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;CAahD"}
@@ -0,0 +1,47 @@
1
+ import { readProviderConfig, getProviderConfig } from "./config.js";
2
+ import { MiniMaxCNQuotaProvider } from "./providers/minimax.js";
3
+ import { OpenCodeGoQuotaProvider } from "./providers/opencode-go.js";
4
+ export class QuotaService {
5
+ providers = new Map();
6
+ providerRegistry = readProviderConfig();
7
+ activeProviderName = null;
8
+ activeProvider = null;
9
+ refreshCount = 0;
10
+ constructor() {
11
+ this.registerProvider(new MiniMaxCNQuotaProvider());
12
+ this.registerProvider(new OpenCodeGoQuotaProvider());
13
+ }
14
+ registerProvider(provider) {
15
+ this.providers.set(provider.name, provider);
16
+ }
17
+ setActiveProvider(providerName) {
18
+ const provider = this.providers.get(providerName);
19
+ if (!provider) {
20
+ console.warn(`[QuotaService] Unknown provider: ${providerName}`);
21
+ return false;
22
+ }
23
+ const config = getProviderConfig(this.providerRegistry, providerName);
24
+ if (!config) {
25
+ console.warn(`[QuotaService] No config for provider: ${providerName}`);
26
+ }
27
+ provider.init(config || {}, {});
28
+ this.activeProviderName = providerName;
29
+ this.activeProvider = provider;
30
+ return true;
31
+ }
32
+ getActiveProviderName() {
33
+ return this.activeProviderName;
34
+ }
35
+ async fetchQuota() {
36
+ if (!this.activeProvider) {
37
+ return null;
38
+ }
39
+ this.refreshCount++;
40
+ const quota = await this.activeProvider.fetchQuota();
41
+ return {
42
+ provider: this.activeProviderName,
43
+ quota: quota,
44
+ refreshCount: this.refreshCount,
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,21 @@
1
+ export interface QuotaUsage {
2
+ usage: number;
3
+ reset: string;
4
+ }
5
+ export interface QuotaData {
6
+ rolling: QuotaUsage | undefined;
7
+ weekly: QuotaUsage | undefined;
8
+ monthly: QuotaUsage | undefined;
9
+ }
10
+ export interface QuotaResult {
11
+ provider: string;
12
+ quota: QuotaData;
13
+ refreshCount: number;
14
+ }
15
+ export interface ProviderConfig {
16
+ [key: string]: unknown;
17
+ }
18
+ export interface ProviderRegistry {
19
+ [providerName: string]: ProviderConfig;
20
+ }
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/quota/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAAC;CACxC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
3
+ import type { JSX } from "solid-js";
4
+ export declare function SessionInfoView(props: {
5
+ api: TuiPluginApi;
6
+ sessionId: string;
7
+ }): JSX.Element;
8
+ //# sourceMappingURL=session-info.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-info.d.ts","sourceRoot":"","sources":["../src/session-info.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAcpC,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,GAAG,CAAC,OAAO,CA2Ed"}
@@ -0,0 +1,44 @@
1
+ import { createSignal, createEffect, Show } from "solid-js";
2
+ import { LabelValue } from "./components.jsx";
3
+ export function SessionInfoView(props) {
4
+ const [data, setData] = createSignal(null);
5
+ createEffect(() => {
6
+ const sessionId = props.sessionId;
7
+ const messages = props.api.state.session.messages(sessionId);
8
+ const todos = props.api.state.session.todo(sessionId);
9
+ const diff = props.api.state.session.diff(sessionId);
10
+ const vcs = props.api.state.vcs;
11
+ const lastAssistantMsg = [...messages]
12
+ .reverse()
13
+ .find((m) => m.role === "assistant");
14
+ const lastModelInfo = lastAssistantMsg && "modelID" in lastAssistantMsg
15
+ ? {
16
+ providerID: lastAssistantMsg.providerID,
17
+ modelID: lastAssistantMsg.modelID,
18
+ }
19
+ : null;
20
+ setData({
21
+ sessionId,
22
+ branch: vcs?.branch,
23
+ provider: lastModelInfo?.providerID ?? "None",
24
+ model: lastModelInfo?.modelID ?? "None",
25
+ messageCount: messages.length,
26
+ todoCount: todos.length,
27
+ diffCount: diff.length,
28
+ });
29
+ });
30
+ return (<Show when={data()} fallback={<text>Loading...</text>}>
31
+ {() => {
32
+ const d = data();
33
+ return (<>
34
+ <LabelValue label="Session" value={d.sessionId.slice(0, 8) + "..."} labelColor="#6bcf7f"/>
35
+ <LabelValue label="Branch" value={d.branch ?? "N/A"} labelColor="#ffd93d"/>
36
+ <LabelValue label="Provider" value={d.provider} labelColor="#ff6b6b"/>
37
+ <LabelValue label="Model" value={d.model} labelColor="#74b9ff"/>
38
+ <LabelValue label="Messages" value={d.messageCount} labelColor="#a29bfe"/>
39
+ <LabelValue label="TODOs" value={d.todoCount} labelColor="#fd79a8"/>
40
+ <LabelValue label="Changes" value={d.diffCount} labelColor="#00cec9"/>
41
+ </>);
42
+ }}
43
+ </Show>);
44
+ }
package/dist/tui.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
3
+ declare const plugin: TuiPluginModule & {
4
+ id: string;
5
+ };
6
+ export default plugin;
7
+ //# sourceMappingURL=tui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAa,eAAe,EAAE,MAAM,yBAAyB,CAAC;AA+B1E,QAAA,MAAM,MAAM,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAG3C,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/tui.jsx ADDED
@@ -0,0 +1,24 @@
1
+ import { UsageView } from "./usage.jsx";
2
+ import { SessionInfoView } from "./session-info.jsx";
3
+ import { QuotaService } from "./quota/service.js";
4
+ const id = "opencode-tui-usage-plugin";
5
+ const tui = async (api) => {
6
+ const quotaService = new QuotaService();
7
+ api.slots.register({
8
+ order: 150,
9
+ slots: {
10
+ sidebar_content(_ctx, _props) {
11
+ return (<box gap={0}>
12
+ <UsageView quotaService={quotaService} api={api} sessionId={_props.session_id}/>
13
+ <SessionInfoView api={api} sessionId={_props.session_id}/>
14
+ </box>);
15
+ },
16
+ },
17
+ });
18
+ console.log(`[${id}] Plugin registered`);
19
+ };
20
+ const plugin = {
21
+ id,
22
+ tui,
23
+ };
24
+ export default plugin;
@@ -0,0 +1,14 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { JSX } from "solid-js";
3
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
4
+ import type { QuotaResult } from "./quota/types.js";
5
+ export interface UsageViewProps {
6
+ quotaService: {
7
+ fetchQuota(): Promise<QuotaResult | null>;
8
+ setActiveProvider(providerName: string): boolean;
9
+ };
10
+ api: TuiPluginApi;
11
+ sessionId: string;
12
+ }
13
+ export declare function UsageView(props: UsageViewProps): JSX.Element;
14
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAGpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAmBpD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE;QACZ,UAAU,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;QAC1C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;KAClD,CAAC;IACF,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,GAAG,CAAC,OAAO,CA0L5D"}
package/dist/usage.jsx ADDED
@@ -0,0 +1,162 @@
1
+ import { createSignal, createEffect, onCleanup } from "solid-js";
2
+ import { Title, ProgressBar } from "./components.jsx";
3
+ const REFRESH_INTERVAL = 60;
4
+ function formatDuration(totalSeconds) {
5
+ if (totalSeconds < 60) {
6
+ return `${totalSeconds}s`;
7
+ }
8
+ if (totalSeconds < 3600) {
9
+ const m = Math.floor(totalSeconds / 60);
10
+ const s = totalSeconds % 60;
11
+ return `${m}:${s.toString().padStart(2, "0")}`;
12
+ }
13
+ const h = Math.floor(totalSeconds / 3600);
14
+ const m = Math.floor((totalSeconds % 3600) / 60);
15
+ const s = totalSeconds % 60;
16
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
17
+ }
18
+ export function UsageView(props) {
19
+ const [result, setResult] = createSignal(null);
20
+ const [loading, setLoading] = createSignal(true);
21
+ const [currentProvider, setCurrentProvider] = createSignal(null);
22
+ const [currentModel, setCurrentModel] = createSignal(null);
23
+ const [refreshCountdown, setRefreshCountdown] = createSignal(REFRESH_INTERVAL);
24
+ const doRefresh = () => {
25
+ const providerID = currentProvider();
26
+ if (!providerID)
27
+ return;
28
+ setLoading(true);
29
+ props.quotaService.setActiveProvider(providerID);
30
+ props.quotaService.fetchQuota().then((data) => {
31
+ if (data && data.quota) {
32
+ setResult(data);
33
+ }
34
+ setLoading(false);
35
+ }).catch(() => {
36
+ setLoading(false);
37
+ });
38
+ };
39
+ createEffect(() => {
40
+ const sessionId = props.sessionId;
41
+ const messages = props.api.state.session.messages(sessionId);
42
+ if (!messages || messages.length === 0) {
43
+ setCurrentProvider(null);
44
+ setCurrentModel(null);
45
+ return;
46
+ }
47
+ const lastAssistantMsg = [...messages]
48
+ .reverse()
49
+ .find((m) => m.role === "assistant");
50
+ if (!lastAssistantMsg) {
51
+ setCurrentProvider(null);
52
+ setCurrentModel(null);
53
+ return;
54
+ }
55
+ if (!("providerID" in lastAssistantMsg)) {
56
+ setCurrentProvider(null);
57
+ setCurrentModel(null);
58
+ return;
59
+ }
60
+ const providerID = lastAssistantMsg.providerID;
61
+ const modelID = "modelID" in lastAssistantMsg
62
+ ? lastAssistantMsg.modelID
63
+ : "";
64
+ setCurrentProvider(providerID);
65
+ setCurrentModel(modelID);
66
+ });
67
+ createEffect(() => {
68
+ const providerID = currentProvider();
69
+ const modelID = currentModel();
70
+ if (!providerID) {
71
+ setResult(null);
72
+ setLoading(false);
73
+ return;
74
+ }
75
+ setLoading(true);
76
+ props.quotaService.setActiveProvider(providerID);
77
+ props.quotaService.fetchQuota().then((data) => {
78
+ if (data && data.quota) {
79
+ setResult(data);
80
+ }
81
+ else {
82
+ setResult(null);
83
+ }
84
+ setLoading(false);
85
+ }).catch((error) => {
86
+ console.error("[UsageView] Failed to fetch quota:", error);
87
+ setResult(null);
88
+ setLoading(false);
89
+ });
90
+ });
91
+ createEffect(() => {
92
+ setRefreshCountdown(REFRESH_INTERVAL);
93
+ const id = setInterval(() => {
94
+ setRefreshCountdown((r) => {
95
+ if (r <= 1) {
96
+ doRefresh();
97
+ return REFRESH_INTERVAL;
98
+ }
99
+ return r - 1;
100
+ });
101
+ }, 1000);
102
+ onCleanup(() => clearInterval(id));
103
+ });
104
+ return (<box flexDirection="column" gap={0}>
105
+ <Title text="Usage" color="#6bcf7f"/>
106
+ <text fg="#888">Provider: {currentProvider() ?? "Unknown"}</text>
107
+ {loading() ? (<>
108
+ <box flexDirection="column" gap={0}>
109
+ <box flexDirection="row" gap={1}>
110
+ <text fg="#6bcf7f">Rolling:</text>
111
+ <text fg="#888">Loading...</text>
112
+ </box>
113
+ <ProgressBar value={0} color="#6bcf7f"/>
114
+ </box>
115
+ <box flexDirection="column" gap={0}>
116
+ <box flexDirection="row" gap={1}>
117
+ <text fg="#ffd93d">Weekly:</text>
118
+ <text fg="#888">Loading...</text>
119
+ </box>
120
+ <ProgressBar value={0} color="#ffd93d"/>
121
+ </box>
122
+ <box flexDirection="column" gap={0}>
123
+ <box flexDirection="row" gap={1}>
124
+ <text fg="#4da6ff">Monthly:</text>
125
+ <text fg="#888">Loading...</text>
126
+ </box>
127
+ <ProgressBar value={0} color="#4da6ff"/>
128
+ </box>
129
+ </>) : result() && result().quota ? (<>
130
+ {result().quota.rolling ? (<box flexDirection="column" gap={0}>
131
+ <box flexDirection="row" gap={1}>
132
+ <text fg="#6bcf7f">Rolling:</text>
133
+ <text>{result().quota.rolling.usage}%</text>
134
+ <text fg="#888">reset {result().quota.rolling.reset}</text>
135
+ </box>
136
+ <ProgressBar value={result().quota.rolling.usage} color="#6bcf7f"/>
137
+ </box>) : (<text fg="#888">Rolling: N/A</text>)}
138
+ {result().quota.weekly ? (<box flexDirection="column" gap={0}>
139
+ <box flexDirection="row" gap={1}>
140
+ <text fg="#ffd93d">Weekly:</text>
141
+ <text>{result().quota.weekly.usage}%</text>
142
+ <text fg="#888">reset {result().quota.weekly.reset}</text>
143
+ </box>
144
+ <ProgressBar value={result().quota.weekly.usage} color="#ffd93d"/>
145
+ </box>) : (<text fg="#888">Weekly: N/A</text>)}
146
+ <box flexDirection="column" gap={0}>
147
+ <box flexDirection="row" gap={1}>
148
+ <text fg="#4da6ff">Monthly:</text>
149
+ {result().quota.monthly ? (<>
150
+ <text>{result().quota.monthly.usage}%</text>
151
+ <text fg="#888">reset {result().quota.monthly.reset}</text>
152
+ </>) : (<>
153
+ <text fg="#888">0%</text>
154
+ <text fg="#888">reset ∞</text>
155
+ </>)}
156
+ </box>
157
+ <ProgressBar value={result().quota.monthly?.usage ?? 0} color="#4da6ff"/>
158
+ </box>
159
+ <text fg="#888">{formatDuration(refreshCountdown())} Refresh #{result().refreshCount}</text>
160
+ </>) : (<text>No data</text>)}
161
+ </box>);
162
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@yinxe/opencode-tui-usage",
3
+ "version": "0.0.6",
4
+ "description": "OpenCode TUI 额度显示插件 - 在侧边栏显示用量和额度信息",
5
+ "repository": "github:Yinxe/opencode-tui-usage",
6
+ "type": "module",
7
+ "oc-plugin": [
8
+ "tui"
9
+ ],
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org/",
12
+ "access": "public"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./tui": {
20
+ "types": "./dist/tui.d.ts",
21
+ "default": "./dist/tui.jsx"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "dev": "tsc --watch",
31
+ "lint": "tsc --noEmit",
32
+ "test": "echo test"
33
+ },
34
+ "keywords": [
35
+ "opencode",
36
+ "opencode-plugin",
37
+ "opencode-tui",
38
+ "quota",
39
+ "usage"
40
+ ],
41
+ "author": "Yinxe",
42
+ "license": "MIT",
43
+ "peerDependencies": {
44
+ "@opencode-ai/plugin": "^1.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@opentui/core": "^0.2.2",
48
+ "@opentui/solid": "^0.2.2",
49
+ "solid-js": "^1.8.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.0.0",
53
+ "typescript": "^5.3.0",
54
+ "vitest": "^1.0.0"
55
+ }
56
+ }