@leo000001/opencode-quota-sidebar 3.0.3 → 3.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 CHANGED
@@ -108,17 +108,17 @@ USAGE
108
108
  CR31.4k CW3.2k Cd66%
109
109
  Est $12.8
110
110
  QUOTA
111
- OAI 5h80 R16:20
112
- W70 R04-03
113
- Cop M78 R04-01
114
- RC D$88.9/$60 E02-27
111
+ OAI 5h80 R3h20m
112
+ W70 R2D04h
113
+ Cop M78 R12D00h
114
+ RC D$88.9/$60 E6D00h
115
115
  B260
116
116
  ```
117
117
 
118
118
  Compact shared title example:
119
119
 
120
120
  ```text
121
- Fix quota adapter matching | OAI 5h80 R16:20 W70 R04-03 | RC D$88.9/$60 B260 | Cd66% | Est$12.8
121
+ Fix quota adapter matching | OAI 5h80 R3h20m W70 R2D04h | RC D$88.9/$60 B260 | Cd66% | Est$12.8
122
122
  ```
123
123
 
124
124
  Another compact title example with multiple providers:
@@ -143,9 +143,9 @@ Example `quota_summary` markdown output shape:
143
143
 
144
144
  ## Quota
145
145
 
146
- - OpenAI: 5h 80% (reset 16:20), Weekly 70% (reset 04-03)
147
- - Copilot: Monthly 78% (reset 04-01)
148
- - RightCode: Daily $88.9/$60 (exp 02-27), Balance $260
146
+ - OpenAI: 5h 80% (reset 3h20m), Weekly 70% (reset 2D04h)
147
+ - Copilot: Monthly 78% (reset 12D00h)
148
+ - RightCode: Daily $88.9/$60 (exp 6D00h), Balance $260
149
149
  ```
150
150
 
151
151
  The tool already returns full markdown. Clients should display that report directly instead of replacing it with a short summary.
@@ -185,15 +185,15 @@ Quota tokens:
185
185
  - `D`: daily window
186
186
  - `W`: weekly window
187
187
  - `M`: monthly window
188
- - `R16:20`: reset at `16:20`
189
- - `R04-03`: reset on `04-03`
190
- - `E02-27`: expiry on `02-27`
188
+ - `R3h20m`: resets in `3h20m`
189
+ - `R2D04h`: resets in `2D04h`
190
+ - `E6D00h`: expires in `6D00h`
191
191
 
192
192
  Example compact quota fragments:
193
193
 
194
- - `OAI 5h80 R16:20`: OpenAI short window, 80% remaining, resets at `16:20`
195
- - `Cop M78 R04-01`: Copilot monthly quota, 78% remaining, resets on `04-01`
196
- - `RC D$88.9/$60 E02-27 B260`: RightCode daily quota plus balance
194
+ - `OAI 5h80 R3h20m`: OpenAI short window, 80% remaining, resets in `3h20m`
195
+ - `Cop M78 R12D00h`: Copilot monthly quota, 78% remaining, resets in `12D00h`
196
+ - `RC D$88.9/$60 E6D00h B260`: RightCode daily quota plus balance
197
197
 
198
198
  ## Config
199
199
 
package/README.zh-CN.md CHANGED
@@ -89,17 +89,17 @@ USAGE
89
89
  CR31.4k CW3.2k Cd66%
90
90
  Est $12.8
91
91
  QUOTA
92
- OAI 5h80 R16:20
93
- W70 R04-03
94
- Cop M78 R04-01
95
- RC D$88.9/$60 E02-27
92
+ OAI 5h80 R3h20m
93
+ W70 R2D04h
94
+ Cop M78 R12D00h
95
+ RC D$88.9/$60 E6D00h
96
96
  B260
97
97
  ```
98
98
 
99
99
  compact shared title 示例:
100
100
 
101
101
  ```text
102
- Fix quota adapter matching | OAI 5h80 R16:20 W70 R04-03 | RC D$88.9/$60 B260 | Cd66% | Est$12.8
102
+ Fix quota adapter matching | OAI 5h80 R3h20m W70 R2D04h | RC D$88.9/$60 B260 | Cd66% | Est$12.8
103
103
  ```
104
104
 
105
105
  另一个多 provider 示例:
@@ -124,9 +124,9 @@ Add XYAI quota adapter | Ant 5h100 W77 O7d60 | Cop M78 R04-01 | Cd52% | Est$2.34
124
124
 
125
125
  ## Quota
126
126
 
127
- - OpenAI: 5h 80% (reset 16:20), Weekly 70% (reset 04-03)
128
- - Copilot: Monthly 78% (reset 04-01)
129
- - RightCode: Daily $88.9/$60 (exp 02-27), Balance $260
127
+ - OpenAI: 5h 80% (reset 3h20m), Weekly 70% (reset 2D04h)
128
+ - Copilot: Monthly 78% (reset 12D00h)
129
+ - RightCode: Daily $88.9/$60 (exp 6D00h), Balance $260
130
130
  ```
131
131
 
132
132
  这个工具本身就返回完整 markdown,调用方应该直接展示,而不是再压缩成一句话摘要。
@@ -170,15 +170,15 @@ Quota token:
170
170
  - `D`:daily window
171
171
  - `W`:weekly window
172
172
  - `M`:monthly window
173
- - `R16:20`:重置时间为 `16:20`
174
- - `R04-03`:重置日期为 `04-03`
175
- - `E02-27`:到期日期为 `02-27`
173
+ - `R3h20m`:还剩 `3h20m` 重置
174
+ - `R2D04h`:还剩 `2D04h` 重置
175
+ - `E6D00h`:还剩 `6D00h` 到期
176
176
 
177
177
  compact quota 片段示例:
178
178
 
179
- - `OAI 5h80 R16:20`:OpenAI 短窗口剩余 80%,`16:20` 重置
180
- - `Cop M78 R04-01`:Copilot 月额度剩余 78%,`04-01` 重置
181
- - `RC D$88.9/$60 E02-27 B260`:RightCode 日额度 + 余额
179
+ - `OAI 5h80 R3h20m`:OpenAI 短窗口剩余 80%,还剩 `3h20m` 重置
180
+ - `Cop M78 R12D00h`:Copilot 月额度剩余 78%,还剩 `12D00h` 重置
181
+ - `RC D$88.9/$60 E6D00h B260`:RightCode 日额度 + 余额
182
182
 
183
183
  ## 安装
184
184
 
package/dist/cost.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { AssistantMessage } from "@opencode-ai/sdk";
2
- import type { CacheCoverageMode } from "./types.js";
1
+ import type { AssistantMessage } from '@opencode-ai/sdk';
2
+ import type { CacheCoverageMode } from './types.js';
3
3
  export declare const API_COST_ENABLED_PROVIDERS: Set<string>;
4
- export type CanonicalPriceSource = "official-doc" | "runtime";
4
+ export type CanonicalPriceSource = 'official-doc' | 'runtime';
5
5
  export declare function canonicalPricingProviderID(providerID: string): string;
6
6
  export declare function canonicalApiCostProviderID(providerID: string): string;
7
7
  export type ModelCostRates = {
package/dist/cost.js CHANGED
@@ -1,23 +1,23 @@
1
- import { asNumber, isRecord } from "./helpers.js";
1
+ import { asNumber, isRecord } from './helpers.js';
2
2
  export const API_COST_ENABLED_PROVIDERS = new Set([
3
- "openai",
4
- "anthropic",
5
- "kimi-for-coding",
6
- "zhipu",
7
- "minimax-cn-coding-plan",
3
+ 'openai',
4
+ 'anthropic',
5
+ 'kimi-for-coding',
6
+ 'zhipu',
7
+ 'minimax-cn-coding-plan',
8
8
  ]);
9
9
  const MODEL_COST_RATE_ALIASES = {
10
- "zhipuai-coding-plan:glm-5.1": ["zhipu:glm-5"],
11
- "zhipuai-coding-plan:glm-5.1-thinking": ["zhipu:glm-5"],
12
- "zhipu:glm-5.1": ["zhipu:glm-5"],
13
- "zhipu:glm-5.1-thinking": ["zhipu:glm-5"],
10
+ 'zhipuai-coding-plan:glm-5.1': ['zhipu:glm-5'],
11
+ 'zhipuai-coding-plan:glm-5.1-thinking': ['zhipu:glm-5'],
12
+ 'zhipu:glm-5.1': ['zhipu:glm-5'],
13
+ 'zhipu:glm-5.1-thinking': ['zhipu:glm-5'],
14
14
  };
15
15
  function moonshotCanonicalModelID(modelID) {
16
- const stripped = modelID.replace(/^moonshotai[/:]/i, "");
16
+ const stripped = modelID.replace(/^moonshotai[/:]/i, '');
17
17
  switch (stripped) {
18
- case "k2p5":
19
- case "kimi-k2-5":
20
- return "kimi-k2.5";
18
+ case 'k2p5':
19
+ case 'kimi-k2-5':
20
+ return 'kimi-k2.5';
21
21
  default:
22
22
  return stripped;
23
23
  }
@@ -30,7 +30,7 @@ function moonshotModelAliases(modelID, options) {
30
30
  if (!aliases.includes(value))
31
31
  aliases.push(value);
32
32
  };
33
- const stripped = modelID.replace(/^moonshotai[/:]/i, "");
33
+ const stripped = modelID.replace(/^moonshotai[/:]/i, '');
34
34
  const canonical = moonshotCanonicalModelID(modelID);
35
35
  if (!options?.canonicalProviderKeys)
36
36
  push(modelID);
@@ -49,11 +49,11 @@ function minimaxModelAliases(modelID) {
49
49
  };
50
50
  push(modelID);
51
51
  push(modelID.toLowerCase());
52
- const stripped = modelID.replace(/^(?:minimax|minimax-cn-coding-plan)[/:]/i, "");
52
+ const stripped = modelID.replace(/^(?:minimax|minimax-cn-coding-plan)[/:]/i, '');
53
53
  push(stripped);
54
54
  push(stripped.toLowerCase());
55
55
  if (/^minimax-/i.test(stripped)) {
56
- const suffix = stripped.slice("minimax-".length);
56
+ const suffix = stripped.slice('minimax-'.length);
57
57
  push(`MiniMax-${suffix}`);
58
58
  push(`minimax-${suffix.toLowerCase()}`);
59
59
  }
@@ -77,19 +77,19 @@ function zhipuModelAliases(modelID) {
77
77
  push(modelID);
78
78
  for (let index = 0; index < queue.length; index++) {
79
79
  const stem = queue[index];
80
- const withoutProviderPrefix = stem.replace(/^(?:zhipu|z-ai|bigmodel|zhipuai-coding-plan)[/:]/, "");
80
+ const withoutProviderPrefix = stem.replace(/^(?:zhipu|z-ai|bigmodel|zhipuai-coding-plan)[/:]/, '');
81
81
  push(withoutProviderPrefix);
82
82
  push(`zhipu/${withoutProviderPrefix}`);
83
- const withoutBillingSuffix = withoutProviderPrefix.replace(/-billing$/, "");
83
+ const withoutBillingSuffix = withoutProviderPrefix.replace(/-billing$/, '');
84
84
  push(withoutBillingSuffix);
85
85
  push(`zhipu/${withoutBillingSuffix}`);
86
- const withoutThinkingSuffix = withoutBillingSuffix.replace(/-thinking$/, "");
86
+ const withoutThinkingSuffix = withoutBillingSuffix.replace(/-thinking$/, '');
87
87
  push(withoutThinkingSuffix);
88
88
  push(`zhipu/${withoutThinkingSuffix}`);
89
- const dotted = withoutThinkingSuffix.replace(/(\d)-(\d)(?=-|$)/g, "$1.$2");
89
+ const dotted = withoutThinkingSuffix.replace(/(\d)-(\d)(?=-|$)/g, '$1.$2');
90
90
  push(dotted);
91
91
  push(`zhipu/${dotted}`);
92
- const hyphenated = withoutThinkingSuffix.replace(/(\d)\.(\d)(?=-|$)/g, "$1-$2");
92
+ const hyphenated = withoutThinkingSuffix.replace(/(\d)\.(\d)(?=-|$)/g, '$1-$2');
93
93
  push(hyphenated);
94
94
  push(`zhipu/${hyphenated}`);
95
95
  }
@@ -110,71 +110,71 @@ function anthropicModelAliases(modelID) {
110
110
  for (let index = 0; index < queue.length; index++) {
111
111
  const stem = queue[index];
112
112
  const withoutProviderPrefix = stem
113
- .replace(/^(?:[a-z]+\.)*anthropic\./, "")
114
- .replace(/^anthropic[/.]/, "");
113
+ .replace(/^(?:[a-z]+\.)*anthropic\./, '')
114
+ .replace(/^anthropic[/.]/, '');
115
115
  push(withoutProviderPrefix);
116
116
  push(`anthropic/${withoutProviderPrefix}`);
117
- const withoutVersionSuffix = withoutProviderPrefix.replace(/-v\d+(?::\d+)?$/, "");
117
+ const withoutVersionSuffix = withoutProviderPrefix.replace(/-v\d+(?::\d+)?$/, '');
118
118
  push(withoutVersionSuffix);
119
119
  push(`anthropic/${withoutVersionSuffix}`);
120
- const atDate = withoutVersionSuffix.replace(/@(\d{8})$/, "-$1");
120
+ const atDate = withoutVersionSuffix.replace(/@(\d{8})$/, '-$1');
121
121
  push(atDate);
122
122
  push(`anthropic/${atDate}`);
123
- const withAtDate = withoutVersionSuffix.replace(/-(\d{8})$/, "@$1");
123
+ const withAtDate = withoutVersionSuffix.replace(/-(\d{8})$/, '@$1');
124
124
  push(withAtDate);
125
125
  push(`anthropic/${withAtDate}`);
126
- const withoutThinkingSuffix = withoutVersionSuffix.replace(/-thinking$/, "");
126
+ const withoutThinkingSuffix = withoutVersionSuffix.replace(/-thinking$/, '');
127
127
  push(withoutThinkingSuffix);
128
128
  push(`anthropic/${withoutThinkingSuffix}`);
129
- const withoutLatestSuffix = withoutThinkingSuffix.replace(/-latest$/, "");
129
+ const withoutLatestSuffix = withoutThinkingSuffix.replace(/-latest$/, '');
130
130
  push(withoutLatestSuffix);
131
131
  push(`anthropic/${withoutLatestSuffix}`);
132
132
  const withoutDateSuffix = withoutLatestSuffix
133
- .replace(/-\d{8}$/, "")
134
- .replace(/@\d{8}$/, "");
133
+ .replace(/-\d{8}$/, '')
134
+ .replace(/@\d{8}$/, '');
135
135
  push(withoutDateSuffix);
136
136
  push(`anthropic/${withoutDateSuffix}`);
137
- const dotted = withoutDateSuffix.replace(/(\d)-(\d)(?=-|$)/g, "$1.$2");
137
+ const dotted = withoutDateSuffix.replace(/(\d)-(\d)(?=-|$)/g, '$1.$2');
138
138
  push(dotted);
139
139
  push(`anthropic/${dotted}`);
140
- const hyphenated = withoutDateSuffix.replace(/(\d)\.(\d)(?=-|$)/g, "$1-$2");
140
+ const hyphenated = withoutDateSuffix.replace(/(\d)\.(\d)(?=-|$)/g, '$1-$2');
141
141
  push(hyphenated);
142
142
  push(`anthropic/${hyphenated}`);
143
143
  }
144
144
  return aliases;
145
145
  }
146
146
  function normalizeKnownProviderID(providerID) {
147
- if (providerID.startsWith("github-copilot"))
148
- return "github-copilot";
147
+ if (providerID.startsWith('github-copilot'))
148
+ return 'github-copilot';
149
149
  return providerID;
150
150
  }
151
151
  function isCanonicalZhipuProviderID(providerID) {
152
- return (providerID === "zhipu" ||
153
- providerID === "bigmodel" ||
154
- providerID === "z-ai" ||
155
- providerID === "zhipuai-coding-plan");
152
+ return (providerID === 'zhipu' ||
153
+ providerID === 'bigmodel' ||
154
+ providerID === 'z-ai' ||
155
+ providerID === 'zhipuai-coding-plan');
156
156
  }
157
157
  function isCanonicalMiniMaxProviderID(providerID) {
158
- return providerID === "minimax" || providerID === "minimax-cn-coding-plan";
158
+ return providerID === 'minimax' || providerID === 'minimax-cn-coding-plan';
159
159
  }
160
160
  export function canonicalPricingProviderID(providerID) {
161
161
  const normalized = normalizeKnownProviderID(providerID);
162
162
  const lowered = normalized.toLowerCase();
163
163
  if (isCanonicalMiniMaxProviderID(lowered)) {
164
- return "minimax";
164
+ return 'minimax';
165
165
  }
166
166
  if (isCanonicalZhipuProviderID(lowered)) {
167
- return "zhipu";
167
+ return 'zhipu';
168
168
  }
169
- if (lowered === "kimi-for-coding")
170
- return "moonshotai";
171
- if (lowered.includes("anthropic") || lowered.includes("claude")) {
172
- return "anthropic";
169
+ if (lowered === 'kimi-for-coding')
170
+ return 'moonshotai';
171
+ if (lowered.includes('anthropic') || lowered.includes('claude')) {
172
+ return 'anthropic';
173
173
  }
174
- if (lowered.includes("openai") || lowered.endsWith("-oai"))
175
- return "openai";
176
- if (lowered.includes("copilot"))
177
- return "github-copilot";
174
+ if (lowered.includes('openai') || lowered.endsWith('-oai'))
175
+ return 'openai';
176
+ if (lowered.includes('copilot'))
177
+ return 'github-copilot';
178
178
  return normalized;
179
179
  }
180
180
  export function canonicalApiCostProviderID(providerID) {
@@ -182,17 +182,17 @@ export function canonicalApiCostProviderID(providerID) {
182
182
  const lowered = normalized.toLowerCase();
183
183
  if (API_COST_ENABLED_PROVIDERS.has(lowered))
184
184
  return lowered;
185
- if (lowered === "minimax")
186
- return "minimax-cn-coding-plan";
187
- if (lowered.includes("copilot"))
188
- return "github-copilot";
189
- if (lowered.includes("openai") || lowered.endsWith("-oai"))
190
- return "openai";
191
- if (lowered.includes("anthropic") || lowered.includes("claude")) {
192
- return "anthropic";
185
+ if (lowered === 'minimax')
186
+ return 'minimax-cn-coding-plan';
187
+ if (lowered.includes('copilot'))
188
+ return 'github-copilot';
189
+ if (lowered.includes('openai') || lowered.endsWith('-oai'))
190
+ return 'openai';
191
+ if (lowered.includes('anthropic') || lowered.includes('claude')) {
192
+ return 'anthropic';
193
193
  }
194
194
  if (isCanonicalZhipuProviderID(lowered)) {
195
- return "zhipu";
195
+ return 'zhipu';
196
196
  }
197
197
  return normalized;
198
198
  }
@@ -233,6 +233,14 @@ function moonshotPricing(input, output, cacheRead) {
233
233
  cacheWrite: 0,
234
234
  };
235
235
  }
236
+ function openaiPricing(input, output, cacheRead) {
237
+ return {
238
+ input,
239
+ output,
240
+ cacheRead,
241
+ cacheWrite: 0,
242
+ };
243
+ }
236
244
  function minimaxPricing(input, output, cacheRead, cacheWrite) {
237
245
  return {
238
246
  input,
@@ -243,238 +251,325 @@ function minimaxPricing(input, output, cacheRead, cacheWrite) {
243
251
  }
244
252
  const BUNDLED_CANONICAL_PRICE_ENTRIES = [
245
253
  {
246
- provider: "anthropic",
247
- model: "claude-opus-4-6",
254
+ provider: 'openai',
255
+ // OpenCode commonly reports the flagship alias as `gpt-5`; keep a bundled
256
+ // fallback so API-equivalent cost still renders when runtime metadata omits
257
+ // OpenAI pricing on a given client or subscription path.
258
+ model: 'gpt-5',
259
+ rates: openaiPricing(2.5, 15, 0.25),
260
+ source: 'official-doc',
261
+ sourceURL: 'https://openai.com/api/pricing/',
262
+ },
263
+ {
264
+ provider: 'openai',
265
+ model: 'gpt-5.3',
266
+ rates: openaiPricing(1.75, 14, 0.175),
267
+ source: 'official-doc',
268
+ sourceURL: 'https://developers.openai.com/api/docs/models/gpt-5.3-chat-latest',
269
+ },
270
+ {
271
+ provider: 'openai',
272
+ model: 'gpt-5.3-chat-latest',
273
+ rates: openaiPricing(1.75, 14, 0.175),
274
+ source: 'official-doc',
275
+ sourceURL: 'https://developers.openai.com/api/docs/models/gpt-5.3-chat-latest',
276
+ },
277
+ {
278
+ provider: 'openai',
279
+ model: 'gpt-5.3-codex',
280
+ rates: openaiPricing(1.75, 14, 0.175),
281
+ source: 'official-doc',
282
+ sourceURL: 'https://developers.openai.com/api/docs/models/gpt-5.3-codex',
283
+ },
284
+ {
285
+ provider: 'openai',
286
+ model: 'gpt-5.2',
287
+ rates: openaiPricing(1.75, 14, 0.175),
288
+ source: 'official-doc',
289
+ sourceURL: 'https://developers.openai.com/api/docs/models/gpt-5.2',
290
+ },
291
+ {
292
+ provider: 'openai',
293
+ model: 'gpt-5.2-chat-latest',
294
+ rates: openaiPricing(1.75, 14, 0.175),
295
+ source: 'official-doc',
296
+ sourceURL: 'https://developers.openai.com/api/docs/models/gpt-5.2-chat-latest',
297
+ },
298
+ {
299
+ provider: 'openai',
300
+ model: 'gpt-5.2-pro',
301
+ rates: openaiPricing(21, 168, 0),
302
+ source: 'official-doc',
303
+ sourceURL: 'https://openai.com/index/introducing-gpt-5-2/',
304
+ },
305
+ {
306
+ provider: 'openai',
307
+ model: 'gpt-5.4',
308
+ rates: openaiPricing(2.5, 15, 0.25),
309
+ source: 'official-doc',
310
+ sourceURL: 'https://openai.com/api/pricing/',
311
+ },
312
+ {
313
+ provider: 'openai',
314
+ model: 'gpt-5-mini',
315
+ rates: openaiPricing(0.75, 4.5, 0.075),
316
+ source: 'official-doc',
317
+ sourceURL: 'https://openai.com/api/pricing/',
318
+ },
319
+ {
320
+ provider: 'openai',
321
+ model: 'gpt-5.4-mini',
322
+ rates: openaiPricing(0.75, 4.5, 0.075),
323
+ source: 'official-doc',
324
+ sourceURL: 'https://openai.com/api/pricing/',
325
+ },
326
+ {
327
+ provider: 'openai',
328
+ model: 'gpt-5-nano',
329
+ rates: openaiPricing(0.2, 1.25, 0.02),
330
+ source: 'official-doc',
331
+ sourceURL: 'https://openai.com/api/pricing/',
332
+ },
333
+ {
334
+ provider: 'openai',
335
+ model: 'gpt-5.4-nano',
336
+ rates: openaiPricing(0.2, 1.25, 0.02),
337
+ source: 'official-doc',
338
+ sourceURL: 'https://openai.com/api/pricing/',
339
+ },
340
+ {
341
+ provider: 'anthropic',
342
+ model: 'claude-opus-4-6',
248
343
  rates: anthropicPricing(5, 25),
249
- source: "official-doc",
250
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
344
+ source: 'official-doc',
345
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
251
346
  },
252
347
  {
253
- provider: "anthropic",
254
- model: "claude-opus-4-5",
348
+ provider: 'anthropic',
349
+ model: 'claude-opus-4-5',
255
350
  rates: anthropicPricing(5, 25),
256
- source: "official-doc",
257
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
351
+ source: 'official-doc',
352
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
258
353
  },
259
354
  {
260
- provider: "anthropic",
261
- model: "claude-opus-4-1",
355
+ provider: 'anthropic',
356
+ model: 'claude-opus-4-1',
262
357
  rates: anthropicPricing(15, 75),
263
- source: "official-doc",
264
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
358
+ source: 'official-doc',
359
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
265
360
  },
266
361
  {
267
- provider: "anthropic",
268
- model: "claude-opus-4",
362
+ provider: 'anthropic',
363
+ model: 'claude-opus-4',
269
364
  rates: anthropicPricing(15, 75),
270
- source: "official-doc",
271
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
365
+ source: 'official-doc',
366
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
272
367
  },
273
368
  {
274
- provider: "anthropic",
275
- model: "claude-sonnet-4-6",
369
+ provider: 'anthropic',
370
+ model: 'claude-sonnet-4-6',
276
371
  rates: anthropicPricing(3, 15),
277
- source: "official-doc",
278
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
372
+ source: 'official-doc',
373
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
279
374
  },
280
375
  {
281
- provider: "anthropic",
282
- model: "claude-sonnet-4-5",
376
+ provider: 'anthropic',
377
+ model: 'claude-sonnet-4-5',
283
378
  rates: anthropicPricing(3, 15, {
284
379
  longContextInput: 6,
285
380
  longContextOutput: 22.5,
286
381
  }),
287
- source: "official-doc",
288
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
382
+ source: 'official-doc',
383
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
289
384
  },
290
385
  {
291
- provider: "anthropic",
292
- model: "claude-sonnet-4",
386
+ provider: 'anthropic',
387
+ model: 'claude-sonnet-4',
293
388
  rates: anthropicPricing(3, 15, {
294
389
  longContextInput: 6,
295
390
  longContextOutput: 22.5,
296
391
  }),
297
- source: "official-doc",
298
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
392
+ source: 'official-doc',
393
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
299
394
  },
300
395
  {
301
- provider: "anthropic",
302
- model: "claude-3-7-sonnet",
396
+ provider: 'anthropic',
397
+ model: 'claude-3-7-sonnet',
303
398
  rates: anthropicPricing(3, 15),
304
- source: "official-doc",
305
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
399
+ source: 'official-doc',
400
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
306
401
  },
307
402
  {
308
- provider: "anthropic",
309
- model: "claude-3-5-sonnet",
403
+ provider: 'anthropic',
404
+ model: 'claude-3-5-sonnet',
310
405
  rates: anthropicPricing(3, 15),
311
- source: "official-doc",
312
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
406
+ source: 'official-doc',
407
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
313
408
  },
314
409
  {
315
- provider: "anthropic",
316
- model: "claude-haiku-4-5",
410
+ provider: 'anthropic',
411
+ model: 'claude-haiku-4-5',
317
412
  rates: anthropicPricing(1, 5),
318
- source: "official-doc",
319
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
413
+ source: 'official-doc',
414
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
320
415
  },
321
416
  {
322
- provider: "anthropic",
323
- model: "claude-3-5-haiku",
417
+ provider: 'anthropic',
418
+ model: 'claude-3-5-haiku',
324
419
  rates: anthropicPricing(0.8, 4),
325
- source: "official-doc",
326
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
420
+ source: 'official-doc',
421
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
327
422
  },
328
423
  {
329
- provider: "anthropic",
330
- model: "claude-3-opus",
424
+ provider: 'anthropic',
425
+ model: 'claude-3-opus',
331
426
  rates: anthropicPricing(15, 75),
332
- source: "official-doc",
333
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
427
+ source: 'official-doc',
428
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
334
429
  },
335
430
  {
336
- provider: "anthropic",
337
- model: "claude-3-haiku",
431
+ provider: 'anthropic',
432
+ model: 'claude-3-haiku',
338
433
  rates: anthropicPricing(0.25, 1.25),
339
- source: "official-doc",
340
- sourceURL: "https://docs.anthropic.com/en/docs/about-claude/pricing",
434
+ source: 'official-doc',
435
+ sourceURL: 'https://docs.anthropic.com/en/docs/about-claude/pricing',
341
436
  },
342
437
  {
343
- provider: "zhipu",
344
- model: "glm-5",
438
+ provider: 'zhipu',
439
+ model: 'glm-5',
345
440
  rates: zhipuPricing(1, 3.2, 0.2),
346
- source: "official-doc",
347
- sourceURL: "https://docs.z.ai/guides/overview/pricing",
441
+ source: 'official-doc',
442
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
348
443
  },
349
444
  {
350
- provider: "zhipu",
351
- model: "glm-4.7",
445
+ provider: 'zhipu',
446
+ model: 'glm-4.7',
352
447
  rates: zhipuPricing(0.6, 2.2, 0.11),
353
- source: "official-doc",
354
- sourceURL: "https://docs.z.ai/guides/overview/pricing",
448
+ source: 'official-doc',
449
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
355
450
  },
356
451
  {
357
- provider: "zhipu",
358
- model: "glm-4.6",
452
+ provider: 'zhipu',
453
+ model: 'glm-4.6',
359
454
  rates: zhipuPricing(0.6, 2.2, 0.11),
360
- source: "official-doc",
361
- sourceURL: "https://docs.z.ai/guides/overview/pricing",
455
+ source: 'official-doc',
456
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
362
457
  },
363
458
  {
364
- provider: "zhipu",
365
- model: "glm-4.6v",
459
+ provider: 'zhipu',
460
+ model: 'glm-4.6v',
366
461
  rates: zhipuPricing(0.3, 0.9, 0.05),
367
- source: "official-doc",
368
- sourceURL: "https://docs.z.ai/guides/overview/pricing",
462
+ source: 'official-doc',
463
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
369
464
  },
370
465
  {
371
- provider: "zhipu",
372
- model: "glm-4.5",
466
+ provider: 'zhipu',
467
+ model: 'glm-4.5',
373
468
  rates: zhipuPricing(0.6, 2.2, 0.11),
374
- source: "official-doc",
375
- sourceURL: "https://docs.z.ai/guides/overview/pricing",
469
+ source: 'official-doc',
470
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
376
471
  },
377
472
  {
378
- provider: "zhipu",
379
- model: "glm-4.5-air",
473
+ provider: 'zhipu',
474
+ model: 'glm-4.5-air',
380
475
  rates: zhipuPricing(0.2, 1.1, 0.03),
381
- source: "official-doc",
382
- sourceURL: "https://docs.z.ai/guides/overview/pricing",
476
+ source: 'official-doc',
477
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
383
478
  },
384
479
  {
385
- provider: "zhipu",
386
- model: "glm-4.5v",
480
+ provider: 'zhipu',
481
+ model: 'glm-4.5v',
387
482
  rates: zhipuPricing(0.6, 1.8, 0.11),
388
- source: "official-doc",
389
- sourceURL: "https://docs.z.ai/guides/overview/pricing",
483
+ source: 'official-doc',
484
+ sourceURL: 'https://docs.z.ai/guides/overview/pricing',
390
485
  },
391
486
  {
392
- provider: "moonshotai",
393
- model: "kimi-k2.5",
487
+ provider: 'moonshotai',
488
+ model: 'kimi-k2.5',
394
489
  rates: moonshotPricing(0.6, 3, 0.1),
395
- source: "official-doc",
396
- sourceURL: "https://platform.moonshot.ai/docs/pricing/chat",
490
+ source: 'official-doc',
491
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
397
492
  },
398
493
  {
399
- provider: "moonshotai",
400
- model: "kimi-k2-thinking",
494
+ provider: 'moonshotai',
495
+ model: 'kimi-k2-thinking',
401
496
  rates: moonshotPricing(0.6, 2.5, 0.15),
402
- source: "official-doc",
403
- sourceURL: "https://platform.moonshot.ai/docs/pricing/chat",
497
+ source: 'official-doc',
498
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
404
499
  },
405
500
  {
406
- provider: "moonshotai",
407
- model: "kimi-k2-0711-preview",
501
+ provider: 'moonshotai',
502
+ model: 'kimi-k2-0711-preview',
408
503
  rates: moonshotPricing(0.6, 2.5, 0.15),
409
- source: "official-doc",
410
- sourceURL: "https://platform.moonshot.ai/docs/pricing/chat",
504
+ source: 'official-doc',
505
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
411
506
  },
412
507
  {
413
- provider: "moonshotai",
414
- model: "kimi-k2-0905-preview",
508
+ provider: 'moonshotai',
509
+ model: 'kimi-k2-0905-preview',
415
510
  rates: moonshotPricing(0.6, 2.5, 0.15),
416
- source: "official-doc",
417
- sourceURL: "https://platform.moonshot.ai/docs/pricing/chat",
511
+ source: 'official-doc',
512
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
418
513
  },
419
514
  {
420
- provider: "moonshotai",
421
- model: "kimi-k2-turbo-preview",
515
+ provider: 'moonshotai',
516
+ model: 'kimi-k2-turbo-preview',
422
517
  rates: moonshotPricing(2.4, 10, 0.6),
423
- source: "official-doc",
424
- sourceURL: "https://platform.moonshot.ai/docs/pricing/chat",
518
+ source: 'official-doc',
519
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
425
520
  },
426
521
  {
427
- provider: "moonshotai",
428
- model: "kimi-k2-thinking-turbo",
522
+ provider: 'moonshotai',
523
+ model: 'kimi-k2-thinking-turbo',
429
524
  rates: moonshotPricing(1.15, 8, 0.15),
430
- source: "official-doc",
431
- sourceURL: "https://platform.moonshot.ai/docs/pricing/chat",
525
+ source: 'official-doc',
526
+ sourceURL: 'https://platform.moonshot.ai/docs/pricing/chat',
432
527
  },
433
528
  {
434
- provider: "minimax",
435
- model: "MiniMax-M2.7",
529
+ provider: 'minimax',
530
+ model: 'MiniMax-M2.7',
436
531
  // OpenCode sources provider pricing from models.dev. The bundled MiniMax
437
532
  // fallback mirrors those USD-denominated entries rather than the CN RMB
438
533
  // docs so API-equivalent cost stays on the same currency basis as the rest
439
534
  // of the sidebar/report output.
440
535
  rates: minimaxPricing(0.3, 1.2, 0.06, 0.375),
441
- source: "runtime",
442
- sourceURL: "https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.7.toml",
536
+ source: 'runtime',
537
+ sourceURL: 'https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.7.toml',
443
538
  },
444
539
  {
445
- provider: "minimax",
446
- model: "MiniMax-M2.7-highspeed",
540
+ provider: 'minimax',
541
+ model: 'MiniMax-M2.7-highspeed',
447
542
  rates: minimaxPricing(0.6, 2.4, 0.06, 0.375),
448
- source: "runtime",
449
- sourceURL: "https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.7-highspeed.toml",
543
+ source: 'runtime',
544
+ sourceURL: 'https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.7-highspeed.toml',
450
545
  },
451
546
  {
452
- provider: "minimax",
453
- model: "MiniMax-M2.5",
547
+ provider: 'minimax',
548
+ model: 'MiniMax-M2.5',
454
549
  rates: minimaxPricing(0.3, 1.2, 0.03, 0.375),
455
- source: "runtime",
456
- sourceURL: "https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.5.toml",
550
+ source: 'runtime',
551
+ sourceURL: 'https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.5.toml',
457
552
  },
458
553
  {
459
- provider: "minimax",
460
- model: "MiniMax-M2.5-highspeed",
554
+ provider: 'minimax',
555
+ model: 'MiniMax-M2.5-highspeed',
461
556
  rates: minimaxPricing(0.6, 2.4, 0.06, 0.375),
462
- source: "runtime",
463
- sourceURL: "https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.5-highspeed.toml",
557
+ source: 'runtime',
558
+ sourceURL: 'https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.5-highspeed.toml',
464
559
  },
465
560
  {
466
- provider: "minimax",
467
- model: "MiniMax-M2.1",
561
+ provider: 'minimax',
562
+ model: 'MiniMax-M2.1',
468
563
  rates: minimaxPricing(0.3, 1.2, 0.03, 0.375),
469
- source: "runtime",
470
- sourceURL: "https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.1.toml",
564
+ source: 'runtime',
565
+ sourceURL: 'https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.1.toml',
471
566
  },
472
567
  {
473
- provider: "minimax",
474
- model: "MiniMax-M2",
568
+ provider: 'minimax',
569
+ model: 'MiniMax-M2',
475
570
  rates: minimaxPricing(0.3, 1.2, 0, 0),
476
- source: "runtime",
477
- sourceURL: "https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.toml",
571
+ source: 'runtime',
572
+ sourceURL: 'https://github.com/anomalyco/models.dev/blob/dev/providers/minimax/models/MiniMax-M2.toml',
478
573
  },
479
574
  ];
480
575
  export function modelCostKey(providerID, modelID) {
@@ -487,13 +582,13 @@ export function modelCostLookupKeys(providerID, modelID) {
487
582
  if (!keys.includes(key))
488
583
  keys.push(key);
489
584
  };
490
- const modelIDsFor = (options) => canonicalProviderID === "anthropic"
585
+ const modelIDsFor = (options) => canonicalProviderID === 'anthropic'
491
586
  ? anthropicModelAliases(modelID)
492
- : canonicalProviderID === "zhipu"
587
+ : canonicalProviderID === 'zhipu'
493
588
  ? zhipuModelAliases(modelID)
494
- : canonicalProviderID === "moonshotai"
589
+ : canonicalProviderID === 'moonshotai'
495
590
  ? moonshotModelAliases(modelID, options)
496
- : canonicalProviderID === "minimax"
591
+ : canonicalProviderID === 'minimax'
497
592
  ? minimaxModelAliases(modelID)
498
593
  : [modelID];
499
594
  for (const candidateModelID of modelIDsFor()) {
@@ -541,9 +636,9 @@ export function parseModelCostRates(value) {
541
636
  if (!isRecord(value))
542
637
  return undefined;
543
638
  const readRate = (input) => {
544
- if (typeof input === "number")
639
+ if (typeof input === 'number')
545
640
  return input;
546
- if (typeof input === "string") {
641
+ if (typeof input === 'string') {
547
642
  const parsed = Number(input);
548
643
  return Number.isFinite(parsed) ? parsed : 0;
549
644
  }
@@ -595,12 +690,12 @@ export function guessModelCostDivisor(rates) {
595
690
  }
596
691
  export function cacheCoverageModeFromRates(rates) {
597
692
  if (!rates)
598
- return "none";
693
+ return 'none';
599
694
  if (rates.cacheWrite > 0)
600
- return "read-write";
695
+ return 'read-write';
601
696
  if (rates.cacheRead > 0)
602
- return "read-only";
603
- return "none";
697
+ return 'read-only';
698
+ return 'none';
604
699
  }
605
700
  export function calcEquivalentApiCostForMessage(message, rates) {
606
701
  const effectiveRates = message.tokens.input + message.tokens.cache.read > 200_000 &&
package/dist/events.d.ts CHANGED
@@ -9,5 +9,5 @@ export declare function createEventDispatcher(handlers: {
9
9
  sessionID: string;
10
10
  messageID?: string;
11
11
  }) => Promise<void>;
12
- onAssistantMessageCompleted: (message: AssistantMessage) => Promise<void>;
12
+ onAssistantMessageUpdated: (message: AssistantMessage) => Promise<void>;
13
13
  }): (event: Event) => Promise<void>;
package/dist/events.js CHANGED
@@ -16,7 +16,8 @@ export function createEventDispatcher(handlers) {
16
16
  await handlers.onSessionDeleted(event.properties.info);
17
17
  return;
18
18
  }
19
- if (tui.type === 'tui.prompt.append' || tui.type === 'tui.command.execute') {
19
+ if (tui.type === 'tui.prompt.append' ||
20
+ tui.type === 'tui.command.execute') {
20
21
  await handlers.onTuiActivity();
21
22
  return;
22
23
  }
@@ -39,9 +40,6 @@ export function createEventDispatcher(handlers) {
39
40
  return;
40
41
  if (!isAssistantMessage(event.properties.info))
41
42
  return;
42
- const completed = event.properties.info.time.completed;
43
- if (typeof completed !== 'number' || !Number.isFinite(completed))
44
- return;
45
- await handlers.onAssistantMessageCompleted(event.properties.info);
43
+ await handlers.onAssistantMessageUpdated(event.properties.info);
46
44
  };
47
45
  }
package/dist/format.js CHANGED
@@ -736,36 +736,36 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
736
736
  : `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`;
737
737
  return maybeBreak(fallbackText, [fallbackText]);
738
738
  }
739
- function isShortResetWindow(label) {
740
- if (typeof label !== 'string')
741
- return false;
742
- return /^\s*(?:\d+\s*[hd]|daily)\b/i.test(label);
739
+ function compactCountdown(remainingMs) {
740
+ if (!Number.isFinite(remainingMs))
741
+ return undefined;
742
+ if (remainingMs <= 0)
743
+ return '0m';
744
+ const minuteMs = 60_000;
745
+ const hourMinutes = 60;
746
+ const dayMinutes = 24 * hourMinutes;
747
+ const totalMinutes = Math.max(1, Math.floor(remainingMs / minuteMs));
748
+ if (totalMinutes < hourMinutes) {
749
+ return `${totalMinutes}m`;
750
+ }
751
+ if (totalMinutes < dayMinutes) {
752
+ const hours = Math.floor(totalMinutes / hourMinutes);
753
+ const minutes = totalMinutes % hourMinutes;
754
+ return `${hours}h${`${minutes}`.padStart(2, '0')}m`;
755
+ }
756
+ const days = Math.floor(totalMinutes / dayMinutes);
757
+ const hours = Math.floor((totalMinutes % dayMinutes) / hourMinutes);
758
+ return `${days}D${`${hours}`.padStart(2, '0')}h`;
743
759
  }
744
760
  function compactReset(iso, resetLabel, windowLabel) {
761
+ void resetLabel;
762
+ void windowLabel;
745
763
  if (!iso)
746
764
  return undefined;
747
765
  const timestamp = Date.parse(iso);
748
766
  if (Number.isNaN(timestamp))
749
767
  return undefined;
750
- const value = new Date(timestamp);
751
- // RightCode subscriptions are displayed as an expiry date (MM-DD), not a time.
752
- // Using UTC here makes the output stable across time zones for ISO `...Z` input.
753
- if (typeof resetLabel === 'string' && resetLabel.startsWith('Exp')) {
754
- const two = (num) => `${num}`.padStart(2, '0');
755
- return `${two(value.getUTCMonth() + 1)}-${two(value.getUTCDate())}`;
756
- }
757
- const now = new Date();
758
- const sameDay = value.getFullYear() === now.getFullYear() &&
759
- value.getMonth() === now.getMonth() &&
760
- value.getDate() === now.getDate();
761
- const two = (num) => `${num}`.padStart(2, '0');
762
- if (isShortResetWindow(windowLabel)) {
763
- const hhmm = `${two(value.getHours())}:${two(value.getMinutes())}`;
764
- if (sameDay)
765
- return hhmm;
766
- return `${two(value.getMonth() + 1)}-${two(value.getDate())} ${hhmm}`;
767
- }
768
- return `${two(value.getMonth() + 1)}-${two(value.getDate())}`;
768
+ return compactCountdown(timestamp - Date.now());
769
769
  }
770
770
  function dateLine(iso) {
771
771
  if (!iso)
@@ -785,16 +785,10 @@ function expiryAlertLine(iso, nowMs = Date.now()) {
785
785
  const thresholdMs = 3 * 24 * 60 * 60 * 1000;
786
786
  if (remainingMs > thresholdMs)
787
787
  return undefined;
788
- const value = new Date(timestamp);
789
- const now = new Date(nowMs);
790
- const sameDay = value.getFullYear() === now.getFullYear() &&
791
- value.getMonth() === now.getMonth() &&
792
- value.getDate() === now.getDate();
793
- const two = (num) => `${num}`.padStart(2, '0');
794
- const hhmm = `${two(value.getHours())}:${two(value.getMinutes())}`;
795
- if (sameDay)
796
- return `Exp today ${hhmm}`;
797
- return `Exp ${two(value.getMonth() + 1)}-${two(value.getDate())} ${hhmm}`;
788
+ const countdown = compactCountdown(remainingMs);
789
+ if (!countdown)
790
+ return undefined;
791
+ return `Exp ${countdown}`;
798
792
  }
799
793
  function quotaExpiryPairs(quotas, nowMs = Date.now()) {
800
794
  return collapseQuotaSnapshots(quotas)
@@ -805,6 +799,9 @@ function quotaExpiryPairs(quotas, nowMs = Date.now()) {
805
799
  }))
806
800
  .filter((item) => Boolean(item.value));
807
801
  }
802
+ function toolVisibleQuotaSnapshots(quotas) {
803
+ return collapseQuotaSnapshots(quotas).filter((item) => item.status === 'ok' || item.status === 'error');
804
+ }
808
805
  function reportResetLine(iso, resetLabel, windowLabel) {
809
806
  const compact = compactReset(iso, resetLabel, windowLabel);
810
807
  if (compact)
@@ -927,7 +924,7 @@ export function renderMarkdownReport(period, usage, quotas, options) {
927
924
  const providerDivider = showCost
928
925
  ? '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |'
929
926
  : '| --- | ---: | ---: | ---: | ---: | ---: |';
930
- const quotaLines = collapseQuotaSnapshots(quotas).flatMap((quota) => {
927
+ const quotaLines = toolVisibleQuotaSnapshots(quotas).flatMap((quota) => {
931
928
  const displayLabel = quotaDisplayLabel(quota);
932
929
  // Multi-window detail
933
930
  if (quota.windows && quota.windows.length > 0 && quota.status === 'ok') {
@@ -953,7 +950,7 @@ export function renderMarkdownReport(period, usage, quotas, options) {
953
950
  mdCell(`- ${displayLabel}: ${quota.status} | balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`),
954
951
  ];
955
952
  }
956
- if (quota.status !== 'ok') {
953
+ if (quota.status === 'error') {
957
954
  return [
958
955
  mdCell(`- ${displayLabel}: ${quota.status}${quota.note ? ` | ${quota.note}` : ''}`),
959
956
  ];
@@ -1074,7 +1071,7 @@ export function renderToastMessage(period, usage, quotas, options) {
1074
1071
  lines.push(fitLine('Provider Cache', width));
1075
1072
  lines.push(...alignPairs(providerCachePairs).map((line) => fitLine(line, width)));
1076
1073
  }
1077
- const quotaPairs = collapseQuotaSnapshots(quotas).flatMap((item) => {
1074
+ const quotaPairs = toolVisibleQuotaSnapshots(quotas).flatMap((item) => {
1078
1075
  if (item.status === 'ok') {
1079
1076
  if (item.windows && item.windows.length > 0) {
1080
1077
  const pairs = item.windows.map((win, idx) => {
@@ -1118,12 +1115,6 @@ export function renderToastMessage(period, usage, quotas, options) {
1118
1115
  },
1119
1116
  ];
1120
1117
  }
1121
- if (item.status === 'unsupported') {
1122
- return [{ label: quotaDisplayLabel(item), value: 'unsupported' }];
1123
- }
1124
- if (item.status === 'unavailable') {
1125
- return [{ label: quotaDisplayLabel(item), value: 'unavailable' }];
1126
- }
1127
1118
  return [{ label: quotaDisplayLabel(item), value: 'Remaining ?' }];
1128
1119
  });
1129
1120
  if (quotaPairs.length > 0) {
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { createUsageService } from './usage_service.js';
13
13
  import { createTitleApplicator } from './title_apply.js';
14
14
  const SHUTDOWN_HOOK_KEY = Symbol.for('opencode-quota-sidebar.shutdown-hook');
15
15
  const SHUTDOWN_CALLBACKS_KEY = Symbol.for('opencode-quota-sidebar.shutdown-callbacks');
16
+ const SESSION_ACTIVE_GRACE_MS = 15_000;
16
17
  export async function QuotaSidebarPlugin(input) {
17
18
  const quotaRuntime = createQuotaRuntime();
18
19
  const config = await loadConfig(quotaConfigPaths(input.worktree, input.directory));
@@ -116,11 +117,33 @@ export async function QuotaSidebarPlugin(input) {
116
117
  });
117
118
  const summarizeSessionUsageForDisplay = usageService.summarizeSessionUsageForDisplay;
118
119
  const summarizeForTool = usageService.summarizeForTool;
120
+ const activeSessionUntil = new Map();
121
+ const markSessionActive = (sessionID, now = Date.now()) => {
122
+ activeSessionUntil.set(sessionID, now + SESSION_ACTIVE_GRACE_MS);
123
+ };
124
+ const clearSessionActivity = (sessionID) => {
125
+ activeSessionUntil.delete(sessionID);
126
+ };
127
+ const isSessionActive = (sessionID, now = Date.now()) => {
128
+ const expiresAt = activeSessionUntil.get(sessionID);
129
+ if (expiresAt === undefined)
130
+ return false;
131
+ if (expiresAt > now)
132
+ return true;
133
+ activeSessionUntil.delete(sessionID);
134
+ return false;
135
+ };
119
136
  // title apply / refresh lifecycle
120
137
  let scheduleTitleRefresh = (sessionID, delay = 250) => {
121
138
  void sessionID;
122
139
  void delay;
123
140
  };
141
+ const scheduleActiveTitleRefresh = (sessionID, delay = 250) => {
142
+ if (!isSessionActive(sessionID))
143
+ return false;
144
+ scheduleTitleRefresh(sessionID, delay);
145
+ return true;
146
+ };
124
147
  const scheduleParentRefreshIfSafe = (sessionID, parentID) => {
125
148
  if (!config.sidebar.includeChildren)
126
149
  return;
@@ -140,7 +163,7 @@ export async function QuotaSidebarPlugin(input) {
140
163
  seen.add(current);
141
164
  current = state.sessions[current]?.parentID;
142
165
  }
143
- scheduleTitleRefresh(parentID, 0);
166
+ scheduleActiveTitleRefresh(parentID, 0);
144
167
  };
145
168
  const titleApplicator = createTitleApplicator({
146
169
  state,
@@ -159,6 +182,8 @@ export async function QuotaSidebarPlugin(input) {
159
182
  });
160
183
  const titleRefresh = createTitleRefreshScheduler({
161
184
  apply: async (sessionID) => {
185
+ if (!isSessionActive(sessionID))
186
+ return;
162
187
  await titleApplicator.applyTitle(sessionID);
163
188
  },
164
189
  onError: swallow('titleRefresh'),
@@ -186,10 +211,7 @@ export async function QuotaSidebarPlugin(input) {
186
211
  startupTitleWork = runStartupRestore().catch(swallow('startup:restoreAllVisibleTitles'));
187
212
  }
188
213
  else {
189
- startupTitleWork = Promise.allSettled([
190
- refreshAllVisibleTitles().catch(swallow('startup:refreshAllVisibleTitles')),
191
- refreshAllTouchedTitles().catch(swallow('startup:refreshAllTouchedTitles')),
192
- ]).then(() => undefined);
214
+ startupTitleWork = Promise.resolve();
193
215
  }
194
216
  const shutdown = async () => {
195
217
  await Promise.race([
@@ -318,7 +340,7 @@ export async function QuotaSidebarPlugin(input) {
318
340
  sessionID: session.id,
319
341
  incomingTitle: session.title,
320
342
  sessionState,
321
- scheduleRefresh: titleRefresh.schedule,
343
+ scheduleRefresh: scheduleActiveTitleRefresh,
322
344
  });
323
345
  },
324
346
  onSessionDeleted: async (session) => {
@@ -328,6 +350,7 @@ export async function QuotaSidebarPlugin(input) {
328
350
  usageService.forgetSession(session.id);
329
351
  titleApplicator.forgetSession(session.id);
330
352
  titleRefresh.cancel(session.id);
353
+ clearSessionActivity(session.id);
331
354
  const dateKey = state.sessionDateMap[session.id] ||
332
355
  dateKeyFromTimestamp(session.time.created);
333
356
  state.deletedSessionDateMap[session.id] = dateKey;
@@ -341,23 +364,28 @@ export async function QuotaSidebarPlugin(input) {
341
364
  scheduleSave();
342
365
  }
343
366
  if (config.sidebar.includeChildren && session.parentID) {
344
- titleRefresh.schedule(session.parentID, 0);
367
+ scheduleActiveTitleRefresh(session.parentID, 0);
345
368
  }
346
369
  },
347
370
  onTuiActivity: async () => {
348
371
  return;
349
372
  },
350
373
  onTuiSessionSelect: async (sessionID) => {
351
- titleRefresh.schedule(sessionID, 0);
374
+ scheduleActiveTitleRefresh(sessionID, 0);
352
375
  },
353
376
  onMessageRemoved: async (info) => {
354
377
  usageService.markForceRescan(info.sessionID);
355
- titleRefresh.schedule(info.sessionID, 0);
378
+ scheduleActiveTitleRefresh(info.sessionID, 0);
356
379
  scheduleParentRefreshIfSafe(info.sessionID, state.sessions[info.sessionID]?.parentID);
357
380
  },
358
- onAssistantMessageCompleted: async (message) => {
381
+ onAssistantMessageUpdated: async (message) => {
382
+ markSessionActive(message.sessionID);
383
+ const completed = message.time.completed;
384
+ if (typeof completed !== 'number' || !Number.isFinite(completed)) {
385
+ return;
386
+ }
359
387
  usageService.markSessionDirty(message.sessionID);
360
- titleRefresh.schedule(message.sessionID);
388
+ scheduleActiveTitleRefresh(message.sessionID);
361
389
  void maybeShowExpiryToast(message.sessionID);
362
390
  },
363
391
  });
@@ -378,7 +406,7 @@ export async function QuotaSidebarPlugin(input) {
378
406
  scheduleSave,
379
407
  flushSave,
380
408
  waitForStartupTitleWork: () => startupTitleWork,
381
- refreshSessionTitle: (sessionID, delay) => titleRefresh.schedule(sessionID, delay ?? 250),
409
+ refreshSessionTitle: (sessionID, delay) => scheduleActiveTitleRefresh(sessionID, delay ?? 250),
382
410
  cancelAllTitleRefreshes: () => titleRefresh.cancelAll(),
383
411
  flushScheduledTitleRefreshes: () => titleRefresh.flushScheduled(),
384
412
  waitForTitleRefreshIdle: () => titleRefresh.waitForIdle(),
package/dist/title.js CHANGED
@@ -57,28 +57,30 @@ function isCoreDecoratedDetail(line) {
57
57
  function isQuotaDecoratedDetail(line) {
58
58
  if (!line)
59
59
  return false;
60
+ const resetValue = '[-:\\dhmD]+';
61
+ const compactResetValue = '\\d[\\d:.\\-hmD]*';
60
62
  if (/^(OAI|Cop|Ant|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|(?:\d+h|D|W|M)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
61
63
  return true;
62
64
  }
63
65
  if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)\s*$/.test(line)) {
64
66
  return true;
65
67
  }
66
- if (/^(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|Balance\s+\$[\d.,]+|Remaining\s+\?|(?:error|unsupported|unavailable))$/.test(line)) {
68
+ if (new RegExp(`^(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|Balance\\s+\\$[\\d.,]+|Remaining\\s+\\?|(?:error|unsupported|unavailable))$`).test(line)) {
67
69
  return true;
68
70
  }
69
- if (/^(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?$/.test(line)) {
71
+ if (new RegExp(`^(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3})(?:\\s+(?:R|E\\+?)${compactResetValue})?$`).test(line)) {
70
72
  return true;
71
73
  }
72
- if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|(?:error|unsupported|unavailable)))$/.test(line)) {
74
+ if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|(?:error|unsupported|unavailable)))$`).test(line)) {
73
75
  return true;
74
76
  }
75
- if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?)$/.test(line)) {
77
+ if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\\s]+)?)(?:\\s+(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3})(?:\\s+(?:R|E\\+?)${compactResetValue})?)$`).test(line)) {
76
78
  return true;
77
79
  }
78
- if (/^(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*))(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
80
+ if (new RegExp(`^(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|(?:R|E\\+?)${compactResetValue}))(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
79
81
  return true;
80
82
  }
81
- if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
83
+ if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
82
84
  return true;
83
85
  }
84
86
  return false;
package/dist/tools.js CHANGED
@@ -63,25 +63,16 @@ export function createQuotaSidebarTools(deps) {
63
63
  deps.setTitleEnabled(true);
64
64
  deps.scheduleSave();
65
65
  await deps.flushSave();
66
- const visible = await deps.refreshAllVisibleTitles();
67
- const touched = await deps.refreshAllTouchedTitles();
68
66
  deps.refreshSessionTitle(context.sessionID, 0);
69
67
  if (startupTimedOut) {
70
68
  void deps.waitForStartupTitleWork().then(() => {
71
69
  if (!deps.getTitleEnabled())
72
70
  return;
73
- void deps.refreshAllVisibleTitles();
74
- void deps.refreshAllTouchedTitles();
75
71
  deps.refreshSessionTitle(context.sessionID, 0);
76
72
  });
77
73
  }
78
74
  await deps.showToast('toggle', 'Sidebar usage display: ON');
79
- if (visible.listFailed ||
80
- visible.refreshed < visible.attempted ||
81
- touched.refreshed < touched.attempted) {
82
- return 'Sidebar usage display is now ON. Visible-session refresh failed, so only touched/current session titles are guaranteed to refresh immediately.';
83
- }
84
- return 'Sidebar usage display is now ON. Visible session titles are refreshing to show token usage and quota.';
75
+ return 'Sidebar usage display is now ON. Only assistant-active sessions will refresh shared titles.';
85
76
  }
86
77
  deps.setTitleEnabled(false);
87
78
  deps.scheduleSave();
@@ -96,8 +87,6 @@ export function createQuotaSidebarTools(deps) {
96
87
  deps.setTitleEnabled(true);
97
88
  deps.scheduleSave();
98
89
  await deps.flushSave();
99
- await deps.refreshAllVisibleTitles();
100
- await deps.refreshAllTouchedTitles();
101
90
  deps.refreshSessionTitle(context.sessionID, 0);
102
91
  await deps.showToast('toggle', 'Sidebar usage display: OFF failed');
103
92
  return 'Sidebar usage display remains ON because some touched session titles could not be restored. Try again after the session service recovers.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "3.0.3",
3
+ "version": "3.0.6",
4
4
  "description": "OpenCode plugin that shows quota and token usage in TUI sidebar panels and compact session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",