@yinxe/opencode-tui-usage 0.0.6 → 0.0.8

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.
@@ -0,0 +1,9 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { JSX } from "solid-js";
3
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
4
+ export interface ContextUsageViewProps {
5
+ api: TuiPluginApi;
6
+ sessionId: string;
7
+ }
8
+ export declare function ContextUsageView(props: ContextUsageViewProps): JSX.Element;
9
+ //# sourceMappingURL=context-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-usage.d.ts","sourceRoot":"","sources":["../src/context-usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAKpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,GAAG,CAAC,OAAO,CAiE1E"}
@@ -0,0 +1,54 @@
1
+ import { createSignal, createEffect } from "solid-js";
2
+ import { Show } from "solid-js";
3
+ import { ProgressBar } from "./components.jsx";
4
+ import { formatNumber, formatPercent } from "./formatters.js";
5
+ export function ContextUsageView(props) {
6
+ const [contextData, setContextData] = createSignal(null);
7
+ createEffect(() => {
8
+ const sessionId = props.sessionId;
9
+ const messages = props.api.state.session.messages(sessionId);
10
+ if (!messages || messages.length === 0) {
11
+ setContextData(null);
12
+ return;
13
+ }
14
+ let latestTokens = 0;
15
+ let latestTime = -Infinity;
16
+ let limit = 0;
17
+ for (const msg of messages) {
18
+ if (msg.role !== "assistant" || !msg.tokens)
19
+ continue;
20
+ const tokens = msg.tokens.input +
21
+ msg.tokens.output +
22
+ msg.tokens.reasoning +
23
+ msg.tokens.cache.read +
24
+ msg.tokens.cache.write;
25
+ if (tokens <= 0)
26
+ continue;
27
+ const time = msg.time.completed ?? msg.time.created;
28
+ if (time > latestTime) {
29
+ latestTime = time;
30
+ latestTokens = tokens;
31
+ const provider = props.api.state.provider.find((p) => p.id === msg.providerID);
32
+ const model = provider?.models[msg.modelID];
33
+ limit = model?.limit?.context ?? 0;
34
+ }
35
+ }
36
+ if (latestTokens === 0 || limit === 0) {
37
+ setContextData(null);
38
+ return;
39
+ }
40
+ const percent = Math.min(100, (latestTokens / limit) * 100);
41
+ setContextData({ tokens: latestTokens, limit, percent });
42
+ });
43
+ return (<Show when={contextData()} fallback={<></>}>
44
+ <box flexDirection="column" gap={0}>
45
+ <box flexDirection="row" gap={2}>
46
+ <text fg="#a29bfe">Context:</text>
47
+ <text>
48
+ {formatNumber(contextData().tokens)} / {formatNumber(contextData().limit)} ({formatPercent(contextData().percent)})
49
+ </text>
50
+ </box>
51
+ <ProgressBar value={contextData().percent} color="#a29bfe" width={20}/>
52
+ </box>
53
+ </Show>);
54
+ }
@@ -0,0 +1,5 @@
1
+ export declare function formatNumber(n: number): string;
2
+ export declare function formatCost(cost: number): string;
3
+ export declare function formatDuration(totalSeconds: number): string;
4
+ export declare function formatPercent(value: number): string;
5
+ //# sourceMappingURL=formatters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,30 @@
1
+ export function formatNumber(n) {
2
+ if (n >= 1000000)
3
+ return (n / 1000000).toFixed(1) + "M";
4
+ if (n >= 1000)
5
+ return (n / 1000).toFixed(1) + "K";
6
+ return n.toString();
7
+ }
8
+ export function formatCost(cost) {
9
+ if (cost === 0)
10
+ return "$0";
11
+ const formatted = cost.toFixed(6).replace(/\.?0+$/, "");
12
+ return "$" + formatted;
13
+ }
14
+ export function formatDuration(totalSeconds) {
15
+ if (totalSeconds < 60) {
16
+ return `${totalSeconds}s`;
17
+ }
18
+ if (totalSeconds < 3600) {
19
+ const m = Math.floor(totalSeconds / 60);
20
+ const s = totalSeconds % 60;
21
+ return `${m}:${s.toString().padStart(2, "0")}`;
22
+ }
23
+ const h = Math.floor(totalSeconds / 3600);
24
+ const m = Math.floor((totalSeconds % 3600) / 60);
25
+ const s = totalSeconds % 60;
26
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
27
+ }
28
+ export function formatPercent(value) {
29
+ return `${Math.round(value)}%`;
30
+ }
@@ -1,13 +1,22 @@
1
1
  import type { QuotaData, ProviderConfig } from "../types.js";
2
2
  import { QuotaProvider } from "../provider.js";
3
- export declare class MiniMaxCNQuotaProvider implements QuotaProvider {
4
- readonly name = "minimax-cn-coding-plan";
3
+ declare class MiniMaxQuotaProvider implements QuotaProvider {
4
+ readonly name: string;
5
5
  private apiKey;
6
6
  private baseUrl;
7
+ private logTag;
8
+ constructor(name: string, baseUrl: string);
7
9
  init(config: ProviderConfig, _credentials: Record<string, unknown>): void;
8
10
  fetchQuota(): Promise<QuotaData | null>;
9
11
  resolveEnvVar(value: string | undefined): string | undefined;
10
12
  private mapResponseToQuotaData;
11
13
  private formatDuration;
12
14
  }
15
+ export declare class MiniMaxCNQuotaProvider extends MiniMaxQuotaProvider {
16
+ constructor();
17
+ }
18
+ export declare class MiniMaxIOQuotaProvider extends MiniMaxQuotaProvider {
19
+ constructor();
20
+ }
21
+ export {};
13
22
  //# sourceMappingURL=minimax.d.ts.map
@@ -1 +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"}
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,cAAM,oBAAqB,YAAW,aAAa;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;gBAEX,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAMzC,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;AAED,qBAAa,sBAAuB,SAAQ,oBAAoB;;CAI/D;AAED,qBAAa,sBAAuB,SAAQ,oBAAoB;;CAI/D"}
@@ -1,15 +1,21 @@
1
1
  import { resolveEnvVar } from "../provider.js";
2
- export class MiniMaxCNQuotaProvider {
3
- name = "minimax-cn-coding-plan";
2
+ class MiniMaxQuotaProvider {
3
+ name;
4
4
  apiKey;
5
- baseUrl = "https://www.minimaxi.com";
5
+ baseUrl;
6
+ logTag;
7
+ constructor(name, baseUrl) {
8
+ this.name = name;
9
+ this.baseUrl = baseUrl;
10
+ this.logTag = `[${name}]`;
11
+ }
6
12
  init(config, _credentials) {
7
13
  const apiKeyRaw = config.apiKey;
8
14
  this.apiKey = resolveEnvVar(apiKeyRaw) || resolveEnvVar(config.apiKeyEnvVar);
9
15
  }
10
16
  async fetchQuota() {
11
17
  if (!this.apiKey) {
12
- console.warn("[MiniMaxCNQuotaProvider] Missing apiKey");
18
+ console.warn(`${this.logTag} Missing apiKey`);
13
19
  return null;
14
20
  }
15
21
  try {
@@ -21,23 +27,23 @@ export class MiniMaxCNQuotaProvider {
21
27
  },
22
28
  });
23
29
  if (!response.ok) {
24
- console.error(`[MiniMaxCNQuotaProvider] API error: ${response.status}`);
30
+ console.error(`${this.logTag} API error: ${response.status}`);
25
31
  return null;
26
32
  }
27
33
  const data = (await response.json());
28
34
  if (data.base_resp?.status_code !== 0) {
29
- console.error(`[MiniMaxCNQuotaProvider] API error: ${data.base_resp?.status_msg}`);
35
+ console.error(`${this.logTag} API error: ${data.base_resp?.status_msg}`);
30
36
  return null;
31
37
  }
32
38
  const codingPlanModels = data.model_remains.filter((m) => m.model_name.startsWith("MiniMax-M"));
33
39
  if (codingPlanModels.length === 0) {
34
- console.warn("[MiniMaxCNQuotaProvider] No coding plan models found");
40
+ console.warn(`${this.logTag} No coding plan models found`);
35
41
  return null;
36
42
  }
37
43
  return this.mapResponseToQuotaData(codingPlanModels);
38
44
  }
39
45
  catch (error) {
40
- console.error("[MiniMaxCNQuotaProvider] Fetch failed:", error);
46
+ console.error(`${this.logTag} Fetch failed:`, error);
41
47
  return null;
42
48
  }
43
49
  }
@@ -92,3 +98,13 @@ export class MiniMaxCNQuotaProvider {
92
98
  return `${Math.round(seconds / 86400)}d`;
93
99
  }
94
100
  }
101
+ export class MiniMaxCNQuotaProvider extends MiniMaxQuotaProvider {
102
+ constructor() {
103
+ super("minimax-cn-coding-plan", "https://www.minimaxi.com");
104
+ }
105
+ }
106
+ export class MiniMaxIOQuotaProvider extends MiniMaxQuotaProvider {
107
+ constructor() {
108
+ super("minimax-coding-plan", "https://api.minimax.io");
109
+ }
110
+ }
@@ -10,6 +10,9 @@ export declare class QuotaService {
10
10
  registerProvider(provider: QuotaProvider): void;
11
11
  setActiveProvider(providerName: string): boolean;
12
12
  getActiveProviderName(): string | null;
13
+ isProviderSupported(providerName: string): boolean;
14
+ getRegisteredProviderNames(): string[];
15
+ getConfiguredProviderNames(): string[];
13
16
  fetchQuota(): Promise<QuotaResult | null>;
14
17
  }
15
18
  //# sourceMappingURL=service.d.ts.map
@@ -1 +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"}
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;;IAQzB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI/C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAmBhD,qBAAqB,IAAI,MAAM,GAAG,IAAI;IAItC,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAIlD,0BAA0B,IAAI,MAAM,EAAE;IAItC,0BAA0B,IAAI,MAAM,EAAE;IAIhC,UAAU,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;CAahD"}
@@ -1,5 +1,5 @@
1
1
  import { readProviderConfig, getProviderConfig } from "./config.js";
2
- import { MiniMaxCNQuotaProvider } from "./providers/minimax.js";
2
+ import { MiniMaxCNQuotaProvider, MiniMaxIOQuotaProvider } from "./providers/minimax.js";
3
3
  import { OpenCodeGoQuotaProvider } from "./providers/opencode-go.js";
4
4
  export class QuotaService {
5
5
  providers = new Map();
@@ -9,6 +9,7 @@ export class QuotaService {
9
9
  refreshCount = 0;
10
10
  constructor() {
11
11
  this.registerProvider(new MiniMaxCNQuotaProvider());
12
+ this.registerProvider(new MiniMaxIOQuotaProvider());
12
13
  this.registerProvider(new OpenCodeGoQuotaProvider());
13
14
  }
14
15
  registerProvider(provider) {
@@ -32,6 +33,15 @@ export class QuotaService {
32
33
  getActiveProviderName() {
33
34
  return this.activeProviderName;
34
35
  }
36
+ isProviderSupported(providerName) {
37
+ return this.providers.has(providerName);
38
+ }
39
+ getRegisteredProviderNames() {
40
+ return Array.from(this.providers.keys());
41
+ }
42
+ getConfiguredProviderNames() {
43
+ return Object.keys(this.providerRegistry);
44
+ }
35
45
  async fetchQuota() {
36
46
  if (!this.activeProvider) {
37
47
  return null;
@@ -0,0 +1,20 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { JSX } from "solid-js";
3
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
4
+ export interface TokenStats {
5
+ providerID: string;
6
+ modelID: string;
7
+ totalCost: number;
8
+ input: number;
9
+ output: number;
10
+ reasoning: number;
11
+ cacheRead: number;
12
+ cacheWrite: number;
13
+ messageCount: number;
14
+ }
15
+ export interface TokensUsageViewProps {
16
+ api: TuiPluginApi;
17
+ sessionId: string;
18
+ }
19
+ export declare function TokensUsageView(props: TokensUsageViewProps): JSX.Element;
20
+ //# sourceMappingURL=tokens-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens-usage.d.ts","sourceRoot":"","sources":["../src/tokens-usage.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAIpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAYD,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,GAAG,CAAC,OAAO,CAsIxE"}
@@ -0,0 +1,119 @@
1
+ import { createSignal, createEffect, Show, For } from "solid-js";
2
+ import { Title } from "./components.jsx";
3
+ import { formatNumber, formatCost } from "./formatters.js";
4
+ function InlineMetric(props) {
5
+ return (<box flexDirection="row" gap={0}>
6
+ <text fg={props.color}>{props.label}</text>
7
+ <text fg="#888">:</text>
8
+ <text>{props.value}</text>
9
+ </box>);
10
+ }
11
+ export function TokensUsageView(props) {
12
+ const [stats, setStats] = createSignal([]);
13
+ const [totals, setTotals] = createSignal(null);
14
+ const [isLoading, setIsLoading] = createSignal(true);
15
+ createEffect(() => {
16
+ const sessionId = props.sessionId;
17
+ const messages = props.api.state.session.messages(sessionId);
18
+ if (!messages || messages.length === 0) {
19
+ setStats([]);
20
+ setTotals(null);
21
+ setIsLoading(false);
22
+ return;
23
+ }
24
+ const grouped = new Map();
25
+ let lastInput = 0;
26
+ messages.forEach((msg) => {
27
+ if (msg.role !== "assistant")
28
+ return;
29
+ const assistantMsg = msg;
30
+ if (!assistantMsg.tokens)
31
+ return;
32
+ const key = `${assistantMsg.providerID || "unknown"}::${assistantMsg.modelID || "unknown"}`;
33
+ if (!grouped.has(key)) {
34
+ grouped.set(key, {
35
+ providerID: assistantMsg.providerID || "unknown",
36
+ modelID: assistantMsg.modelID || "unknown",
37
+ totalCost: 0,
38
+ input: 0,
39
+ output: 0,
40
+ reasoning: 0,
41
+ cacheRead: 0,
42
+ cacheWrite: 0,
43
+ messageCount: 0,
44
+ });
45
+ }
46
+ const stat = grouped.get(key);
47
+ stat.totalCost += assistantMsg.cost || 0;
48
+ stat.input += assistantMsg.tokens.input || 0;
49
+ stat.output += assistantMsg.tokens.output || 0;
50
+ stat.reasoning += assistantMsg.tokens.reasoning || 0;
51
+ stat.cacheRead += assistantMsg.tokens.cache?.read || 0;
52
+ stat.cacheWrite += assistantMsg.tokens.cache?.write || 0;
53
+ stat.messageCount += 1;
54
+ lastInput = assistantMsg.tokens.input || 0;
55
+ });
56
+ let totalInput = 0, totalOutput = 0, totalReasoning = 0;
57
+ let totalCacheRead = 0, totalCacheWrite = 0, totalCost = 0;
58
+ grouped.forEach((stat) => {
59
+ totalInput += stat.input;
60
+ totalOutput += stat.output;
61
+ totalReasoning += stat.reasoning;
62
+ totalCacheRead += stat.cacheRead;
63
+ totalCacheWrite += stat.cacheWrite;
64
+ totalCost += stat.totalCost;
65
+ });
66
+ setStats(Array.from(grouped.values()));
67
+ setTotals({
68
+ input: totalInput,
69
+ output: totalOutput,
70
+ reasoning: totalReasoning,
71
+ cacheRead: totalCacheRead,
72
+ cacheWrite: totalCacheWrite,
73
+ cost: totalCost,
74
+ currentInput: lastInput,
75
+ });
76
+ setIsLoading(false);
77
+ });
78
+ return (<box flexDirection="column" gap={0}>
79
+ <Title text="Usage Tokens" color="#a29bfe"/>
80
+
81
+ <Show when={isLoading()}>
82
+ <text fg="#888">...</text>
83
+ </Show>
84
+
85
+ <Show when={!isLoading() && stats().length === 0}>
86
+ <text fg="#888">-</text>
87
+ </Show>
88
+
89
+ <Show when={!isLoading() && stats().length > 0}>
90
+ <box flexDirection="column" gap={0}>
91
+ <box flexDirection="row" gap={2}>
92
+ <InlineMetric label="In" value={formatNumber(totals()?.input ?? 0)} color="#6bcf7f"/>
93
+ <InlineMetric label="Out" value={formatNumber(totals()?.output ?? 0)} color="#fd79a8"/>
94
+ <InlineMetric label="Rea" value={formatNumber(totals()?.reasoning ?? 0)} color="#fdcb6e"/>
95
+ </box>
96
+ <box flexDirection="row" gap={2}>
97
+ <InlineMetric label="Cache" value={`R:${formatNumber(totals()?.cacheRead ?? 0)} W:${formatNumber(totals()?.cacheWrite ?? 0)}`} color="#00cec9"/>
98
+ </box>
99
+ <box flexDirection="row" gap={2}>
100
+ <InlineMetric label="Cost" value={formatCost(totals()?.cost ?? 0)} color="#ffd93d"/>
101
+ </box>
102
+ </box>
103
+ </Show>
104
+
105
+ <Show when={!isLoading() && stats().length > 0}>
106
+ <For each={stats()}>
107
+ {(stat) => (<box flexDirection="column" gap={0}>
108
+ <box flexDirection="row" gap={1}>
109
+ <text fg="#74b9ff">{stat.modelID || "unknown"}</text>
110
+ <text fg="#ffd93d">· {formatCost(stat.totalCost)}</text>
111
+ </box>
112
+ <text fg="#888">
113
+ I:{formatNumber(stat.input)} O:{formatNumber(stat.output)} R:{formatNumber(stat.reasoning)} C:{formatNumber(stat.cacheRead + stat.cacheWrite)}({stat.messageCount})
114
+ </text>
115
+ </box>)}
116
+ </For>
117
+ </Show>
118
+ </box>);
119
+ }
package/dist/tui.d.ts.map CHANGED
@@ -1 +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"}
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;AAyC1E,QAAA,MAAM,MAAM,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAG3C,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/tui.jsx CHANGED
@@ -1,5 +1,7 @@
1
1
  import { UsageView } from "./usage.jsx";
2
2
  import { SessionInfoView } from "./session-info.jsx";
3
+ import { TokensUsageView } from "./tokens-usage.jsx";
4
+ import { ContextUsageView } from "./context-usage.jsx";
3
5
  import { QuotaService } from "./quota/service.js";
4
6
  const id = "opencode-tui-usage-plugin";
5
7
  const tui = async (api) => {
@@ -11,6 +13,8 @@ const tui = async (api) => {
11
13
  return (<box gap={0}>
12
14
  <UsageView quotaService={quotaService} api={api} sessionId={_props.session_id}/>
13
15
  <SessionInfoView api={api} sessionId={_props.session_id}/>
16
+ <ContextUsageView api={api} sessionId={_props.session_id}/>
17
+ <TokensUsageView api={api} sessionId={_props.session_id}/>
14
18
  </box>);
15
19
  },
16
20
  },
package/dist/usage.d.ts CHANGED
@@ -6,6 +6,9 @@ export interface UsageViewProps {
6
6
  quotaService: {
7
7
  fetchQuota(): Promise<QuotaResult | null>;
8
8
  setActiveProvider(providerName: string): boolean;
9
+ isProviderSupported(providerName: string): boolean;
10
+ getRegisteredProviderNames(): string[];
11
+ getConfiguredProviderNames(): string[];
9
12
  };
10
13
  api: TuiPluginApi;
11
14
  sessionId: string;
@@ -1 +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"}
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;AAIpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIpD,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;QACjD,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QACnD,0BAA0B,IAAI,MAAM,EAAE,CAAC;QACvC,0BAA0B,IAAI,MAAM,EAAE,CAAC;KACxC,CAAC;IACF,GAAG,EAAE,YAAY,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAoCD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,GAAG,CAAC,OAAO,CA2N5D"}
package/dist/usage.jsx CHANGED
@@ -1,19 +1,24 @@
1
1
  import { createSignal, createEffect, onCleanup } from "solid-js";
2
2
  import { Title, ProgressBar } from "./components.jsx";
3
+ import { formatDuration } from "./formatters.js";
3
4
  const REFRESH_INTERVAL = 60;
4
- function formatDuration(totalSeconds) {
5
- if (totalSeconds < 60) {
6
- return `${totalSeconds}s`;
5
+ function EmptyState(props) {
6
+ if (!props.provider) {
7
+ return <text fg="#888">No LLM activity detected</text>;
7
8
  }
8
- if (totalSeconds < 3600) {
9
- const m = Math.floor(totalSeconds / 60);
10
- const s = totalSeconds % 60;
11
- return `${m}:${s.toString().padStart(2, "0")}`;
9
+ if (!props.supported) {
10
+ return (<box flexDirection="column" gap={0}>
11
+ <text fg="#ff6b6b">not support provider: {props.provider}</text>
12
+ <text fg="#74b9ff">github.com/Yinxe/opencode-tui-usage</text>
13
+ </box>);
12
14
  }
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")}`;
15
+ if (props.error) {
16
+ return (<box flexDirection="column" gap={0}>
17
+ <text fg="#ff6666">Error fetching quota</text>
18
+ <text fg="#888">{props.error}</text>
19
+ </box>);
20
+ }
21
+ return <text fg="#888">No quota data available</text>;
17
22
  }
18
23
  export function UsageView(props) {
19
24
  const [result, setResult] = createSignal(null);
@@ -21,18 +26,31 @@ export function UsageView(props) {
21
26
  const [currentProvider, setCurrentProvider] = createSignal(null);
22
27
  const [currentModel, setCurrentModel] = createSignal(null);
23
28
  const [refreshCountdown, setRefreshCountdown] = createSignal(REFRESH_INTERVAL);
29
+ const [providerSupported, setProviderSupported] = createSignal(true);
30
+ const [fetchError, setFetchError] = createSignal(null);
24
31
  const doRefresh = () => {
25
32
  const providerID = currentProvider();
26
33
  if (!providerID)
27
34
  return;
28
35
  setLoading(true);
29
- props.quotaService.setActiveProvider(providerID);
36
+ setFetchError(null);
37
+ const supported = props.quotaService.setActiveProvider(providerID);
38
+ setProviderSupported(supported);
39
+ if (!supported) {
40
+ setLoading(false);
41
+ setResult(null);
42
+ return;
43
+ }
30
44
  props.quotaService.fetchQuota().then((data) => {
31
45
  if (data && data.quota) {
32
46
  setResult(data);
33
47
  }
48
+ else {
49
+ setResult(null);
50
+ }
34
51
  setLoading(false);
35
- }).catch(() => {
52
+ }).catch((error) => {
53
+ setFetchError(String(error));
36
54
  setLoading(false);
37
55
  });
38
56
  };
@@ -42,6 +60,8 @@ export function UsageView(props) {
42
60
  if (!messages || messages.length === 0) {
43
61
  setCurrentProvider(null);
44
62
  setCurrentModel(null);
63
+ setFetchError(null);
64
+ setProviderSupported(false);
45
65
  return;
46
66
  }
47
67
  const lastAssistantMsg = [...messages]
@@ -50,11 +70,15 @@ export function UsageView(props) {
50
70
  if (!lastAssistantMsg) {
51
71
  setCurrentProvider(null);
52
72
  setCurrentModel(null);
73
+ setFetchError(null);
74
+ setProviderSupported(false);
53
75
  return;
54
76
  }
55
77
  if (!("providerID" in lastAssistantMsg)) {
56
78
  setCurrentProvider(null);
57
79
  setCurrentModel(null);
80
+ setFetchError(null);
81
+ setProviderSupported(false);
58
82
  return;
59
83
  }
60
84
  const providerID = lastAssistantMsg.providerID;
@@ -66,14 +90,21 @@ export function UsageView(props) {
66
90
  });
67
91
  createEffect(() => {
68
92
  const providerID = currentProvider();
69
- const modelID = currentModel();
70
93
  if (!providerID) {
71
94
  setResult(null);
72
95
  setLoading(false);
96
+ setProviderSupported(false);
73
97
  return;
74
98
  }
75
99
  setLoading(true);
76
- props.quotaService.setActiveProvider(providerID);
100
+ setFetchError(null);
101
+ const supported = props.quotaService.setActiveProvider(providerID);
102
+ setProviderSupported(supported);
103
+ if (!supported) {
104
+ setResult(null);
105
+ setLoading(false);
106
+ return;
107
+ }
77
108
  props.quotaService.fetchQuota().then((data) => {
78
109
  if (data && data.quota) {
79
110
  setResult(data);
@@ -84,6 +115,7 @@ export function UsageView(props) {
84
115
  setLoading(false);
85
116
  }).catch((error) => {
86
117
  console.error("[UsageView] Failed to fetch quota:", error);
118
+ setFetchError(String(error));
87
119
  setResult(null);
88
120
  setLoading(false);
89
121
  });
@@ -102,7 +134,7 @@ export function UsageView(props) {
102
134
  onCleanup(() => clearInterval(id));
103
135
  });
104
136
  return (<box flexDirection="column" gap={0}>
105
- <Title text="Usage" color="#6bcf7f"/>
137
+ <Title text="Usage Quota" color="#6bcf7f"/>
106
138
  <text fg="#888">Provider: {currentProvider() ?? "Unknown"}</text>
107
139
  {loading() ? (<>
108
140
  <box flexDirection="column" gap={0}>
@@ -157,6 +189,6 @@ export function UsageView(props) {
157
189
  <ProgressBar value={result().quota.monthly?.usage ?? 0} color="#4da6ff"/>
158
190
  </box>
159
191
  <text fg="#888">{formatDuration(refreshCountdown())} Refresh #{result().refreshCount}</text>
160
- </>) : (<text>No data</text>)}
192
+ </>) : (<EmptyState provider={currentProvider()} supported={providerSupported()} error={fetchError()} registeredProviders={props.quotaService.getRegisteredProviderNames()} configuredProviders={props.quotaService.getConfiguredProviderNames()}/>)}
161
193
  </box>);
162
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yinxe/opencode-tui-usage",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "OpenCode TUI 额度显示插件 - 在侧边栏显示用量和额度信息",
5
5
  "repository": "github:Yinxe/opencode-tui-usage",
6
6
  "type": "module",