@chenlongapps/opencode-token-usage 0.4.6 → 0.4.7

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
@@ -58,9 +58,11 @@ The dialog is split into five sections with separate statistics:
58
58
 
59
59
  One line under the title names the session and its active model, and the body never repeats the model name. Compact mode uses `K`/`M` abbreviations (`812`, `139.4K`, `3.70M`), hides empty rows and merges the two tool families in Context Breakdown into a single `Tools` row. Detailed mode shows exact numbers, empty rows, and the `System Tools` / `MCP Tools` split. The sidebar keeps exact numbers and its own layout.
60
60
 
61
- Costs follow the sidebar's pricing and `partial`/unavailable/free conventions. In detailed mode, By Model shows the applied Input, Output, Reasoning, Cache Read and Cache Write rates in USD per million text tokens. Each rate group names its source (OpenCode or the built-in snapshot), incoming-token tier and call count; a model can have multiple groups. `Incoming` means Input + Cache Read + Cache Write. Missing rates show `—`, confirmed free rates show `$0.00`, and unpriced calls are counted separately. These are the current rates used to estimate recorded calls, not historical provider bills. Context sections are unavailable until the viewed session has reported usage and a known context limit.
61
+ Costs follow the sidebar's pricing and `partial`/unavailable/free conventions. In detailed mode, By Model shows the applied Input, Output, Reasoning, Cache Read and Cache Write rates in USD per million text tokens. Each rate group names its source (OpenCode or the built-in snapshot), incoming-token tier and call count; a model can have multiple groups. `Incoming` means Input + Cache Read + Cache Write. Missing rates show `—`, confirmed free rates show `$0.00`, and unpriced calls are counted separately. These are the current rates used to estimate recorded calls, not historical provider bills.
62
62
 
63
- **Context Breakdown** separates Messages, System Tools, System Prompt, Skills, MCP Tools, and Other. It estimates the text and tool definitions in the viewed session's **latest assembled model request**; its percentages use the sum of those estimates. These are not provider-reported token counts and do not add up to the measured context window, which also includes output and reasoning. Media and provider-specific framing cannot be measured from the request. A server plugin records only aggregate estimates (not prompt text); sessions without a captured request or an available server RPC show “Source estimates unavailable”.
63
+ **Context Window** requires reported usage and a valid active-model context limit. **Last Request** shows the selected assistant call's reported tokens even when that limit is unknown or source estimates are unavailable. Its timestamp comes from that same message: stream end, completion, then creation time, using the first valid value and hiding the timestamp if none is available. Request selection follows the viewed session's revert cutoff and most recent completed compaction boundary.
64
+
65
+ **Context Breakdown** separates Messages, System Tools, System Prompt, Skills, MCP Tools, and Other. It estimates the text and tool definitions in the viewed session's **latest assembled model request**; its percentages use the sum of those estimates. Its timestamp is the estimate's own capture time, which may refer to a different request than Last Request. These are not provider-reported token counts and do not add up to the measured context window, which also includes output and reasoning. Media and provider-specific framing cannot be measured from the request. A server plugin records only aggregate estimates (not prompt text); sessions without a captured request or an available server RPC show “Source estimates unavailable”.
64
66
 
65
67
  ### Metrics
66
68
 
@@ -103,7 +105,7 @@ The OpenCode SDK packages are pinned to `2.0.24`, with OpenTUI `0.5.14`. Clean i
103
105
 
104
106
  The [price update workflow](.github/workflows/update-prices.yml) checks models.dev every day at 22:00 UTC (or on demand via **Run workflow** on `main`). No snapshot diff means no release. Changes within the reviewed first-party source allowlist pass typecheck, tests, build, and pack validation, then automatically bump the patch version (for example, `0.4.4` → `0.4.5`), commit only `src/prices.generated.ts`, `package.json`, and `package-lock.json`, and atomically push `main` and its version tag. The workflow creates a GitHub Release and explicitly dispatches [`publish.yml`](.github/workflows/publish.yml) on that tag to publish via npm Trusted Publishing/OIDC; no long-lived npm token is needed. GitHub Actions must be allowed to push to `main` and create tags, and npm must trust `publish.yml` with permission to run `npm publish`.
105
107
 
106
- Failed validation never bumps the version. An unfinished tagged price release is retried with the same version before checking new prices; registry outages are not treated as missing versions. Publishing skips versions already on npm, prevents moving `latest` backwards, and verifies registry visibility after publication. Manufacturer/source allowlist additions, aliases, and manually verified price exceptions still require human review. See the [release guide](docs/releasing.md) for setup and retries.
108
+ Failed validation never bumps the version. An unfinished tagged price release is retried with the same version before checking new prices; registry outages are not treated as missing versions. Publishing skips versions already on npm, prevents moving `latest` backwards, and polls registry visibility for up to 10 minutes after publication, logging progress. A verification timeout is not proof that `npm publish` failed: check the registry and retry the same tag without bumping the version; already-published versions are not republished. Manufacturer/source allowlist additions, aliases, and manually verified price exceptions still require human review. See the [release guide](docs/releasing.md) for setup, verification timing, and retries.
107
109
 
108
110
  For a manual refresh, run `npm run prices:update` in a networked environment, review the generated diff and exceptions, then run the checks above. Builds and plugin refreshes do not contact models.dev.
109
111
 
package/README.zh-CN.md CHANGED
@@ -58,9 +58,11 @@ opencode plugin add @chenlongapps/opencode-token-usage
58
58
 
59
59
  标题下方一行注明会话与当前活动模型,正文不再重复模型名。紧凑模式使用 `K`/`M` 缩写(`812`、`139.4K`、`3.70M`),隐藏零值行,并把 Context Breakdown 中的两类工具合并为一行 `Tools`;详细模式显示精确数字、零值行及 `System Tools` / `MCP Tools` 拆分。侧边栏仍使用精确数字和原有布局。
60
60
 
61
- 费用沿用侧边栏的价格及 `partial`、不可定价、免费口径。详细模式下,By Model 显示 Input、Output、Reasoning、Cache Read、Cache Write 的美元/百万文本 token 费率。每组费率注明来源(OpenCode 或内置快照)、传入 token 档位和调用数;同一模型可能有多组。传入 token 为 Input + Cache Read + Cache Write。缺失费率显示 `—`,确认免费显示 `$0.00`,无法定价的调用单独计数。这些是当前用于估算已记录调用的费率,不是历史提供商账单。当前会话尚未上报用量或上下文上限未知时,上下文相关区域显示不可用。
61
+ 费用沿用侧边栏的价格及 `partial`、不可定价、免费口径。详细模式下,By Model 显示 Input、Output、Reasoning、Cache Read、Cache Write 的美元/百万文本 token 费率。每组费率注明来源(OpenCode 或内置快照)、传入 token 档位和调用数;同一模型可能有多组。传入 token 为 Input + Cache Read + Cache Write。缺失费率显示 `—`,确认免费显示 `$0.00`,无法定价的调用单独计数。这些是当前用于估算已记录调用的费率,不是历史提供商账单。
62
62
 
63
- 弹窗中的 **Context Breakdown** 将消息、系统工具、系统提示词、技能、MCP 工具及其他分开展示。它根据被查看会话**最近一次已组装的模型请求**中的文本和工具定义估算,占比的分母是六项估算值之和;这些不是提供商上报的精确 token,也不会与上方含输出、推理的实测上下文窗口相加一致。媒体内容与提供商额外包装无法准确计量。服务端插件仅保存分类数值,不保存提示词正文;未捕获到请求或服务端 RPC 不可用时显示“来源估算不可用”。
63
+ **Context Window** 需要已上报用量和有效的活动模型上下文上限。**Last Request** 展示所选 assistant 调用的实测 token,上限未知或来源估算不可用时仍可显示。其时间取自同一条消息,依次选用有效的流式结束、完成、创建时间;均不可用时隐藏时间。请求选择遵循被查看会话的回退截断和最后一次已完成压缩边界。
64
+
65
+ 弹窗中的 **Context Breakdown** 将消息、系统工具、系统提示词、技能、MCP 工具及其他分开展示。它根据被查看会话**最近一次已组装的模型请求**中的文本和工具定义估算,占比的分母是六项估算值之和。其时间为该估算自身的采样时间,可能与 Last Request 对应不同请求。这些不是提供商上报的精确 token,也不会与上方含输出、推理的实测上下文窗口相加一致。媒体内容与提供商额外包装无法准确计量。服务端插件仅保存分类数值,不保存提示词正文;未捕获到请求或服务端 RPC 不可用时显示“来源估算不可用”。
64
66
 
65
67
  ### 指标
66
68
 
@@ -103,7 +105,7 @@ OpenCode SDK 包精确锁定为 `2.0.24`,OpenTUI 为 `0.5.14`。Node.js 22 下
103
105
 
104
106
  [价格更新工作流](.github/workflows/update-prices.yml) 每天 UTC 22:00 检查 models.dev,也可在 `main` 上通过 **Run workflow** 手动触发。快照无变化就不发布。经审核的原厂来源白名单内有变化时,先通过类型检查、测试、构建和打包检查,再自动升级 patch 版本(例如 `0.4.4` → `0.4.5`),只提交 `src/prices.generated.ts`、`package.json` 和 `package-lock.json`,并原子推送 `main` 与版本标签。工作流创建 GitHub Release,显式触发该标签上的 [`publish.yml`](.github/workflows/publish.yml),通过 npm Trusted Publishing/OIDC 发布,无需长期 npm token。仓库必须允许 GitHub Actions 推送 `main` 和创建标签;npm 必须信任 `publish.yml` 并允许执行 `npm publish`。
105
107
 
106
- 验证失败不会升版本。已打标签但未完成的自动价格发布会优先重试同一版本,再检查新价格;registry 故障不会被当作版本不存在。发布时跳过 npm 已有版本、防止 `latest` 倒退,并在发布后核验 registry 中的版本。新增原厂/来源白名单、别名及人工核实的价格例外仍需人工审核。配置和重试方法见[发布说明](docs/releasing.md)。
108
+ 验证失败不会升版本。已打标签但未完成的自动价格发布会优先重试同一版本,再检查新价格;registry 故障不会被当作版本不存在。发布时跳过 npm 已有版本、防止 `latest` 倒退,并在发布后最多轮询 10 分钟核验 registry 可见性,输出进度。校验超时不代表 `npm publish` 失败:先检查 registry,再重试原标签,不另升版本;已发布版本不会重复发布。新增原厂/来源白名单、别名及人工核实的价格例外仍需人工审核。配置、校验时限和重试方法见[发布说明](docs/releasing.md)。
107
109
 
108
110
  手动更新时,在联网的维护环境运行 `npm run prices:update`,审阅生成文件及例外差异,再执行上述检查。构建和插件刷新不会请求 models.dev。
109
111
 
@@ -3,20 +3,21 @@ import type { Plugin } from "@opencode/plugin";
3
3
  import type { ContextSources } from "./context-sources.js";
4
4
  type Request = Pick<SessionContext, "sessionID" | "model" | "system" | "messages" | "tools">;
5
5
  type Mcp = {
6
- list(): Promise<{
6
+ list(...args: Parameters<Plugin.Context["mcp"]["list"]>): Promise<{
7
7
  data: readonly {
8
8
  name: string;
9
9
  }[];
10
10
  }>;
11
11
  };
12
12
  type Storage = Pick<Plugin.Context["storage"], "get" | "set" | "remove">;
13
- /** The hook awaits MCP metadata only on a cache miss; persistence follows the request. */
13
+ /** MCP metadata waits are bounded; persistence never delays the model request. */
14
14
  export declare class ContextCapture {
15
15
  private readonly mcp;
16
16
  private readonly storage;
17
17
  private readonly ttl;
18
18
  private readonly now;
19
19
  private readonly elapsed;
20
+ private readonly mcpTimeoutMs;
20
21
  private names;
21
22
  private expiresAt;
22
23
  private listing;
@@ -31,8 +32,9 @@ export declare class ContextCapture {
31
32
  storageWrites: number;
32
33
  storageWriteMs: number;
33
34
  };
34
- constructor(mcp: Mcp, storage: Storage, ttl?: number, now?: () => number, elapsed?: () => number);
35
+ constructor(mcp: Mcp, storage: Storage, ttl?: number, now?: () => number, elapsed?: () => number, mcpTimeoutMs?: number);
35
36
  private servers;
37
+ private readServers;
36
38
  invalidateMcp(): void;
37
39
  capture(request: Request): Promise<void>;
38
40
  private record;
@@ -1,11 +1,12 @@
1
1
  import { estimateContextSources, parseContextSources } from "./context-sources.js";
2
- /** The hook awaits MCP metadata only on a cache miss; persistence follows the request. */
2
+ /** MCP metadata waits are bounded; persistence never delays the model request. */
3
3
  export class ContextCapture {
4
4
  mcp;
5
5
  storage;
6
6
  ttl;
7
7
  now;
8
8
  elapsed;
9
+ mcpTimeoutMs;
9
10
  names;
10
11
  expiresAt = 0;
11
12
  listing;
@@ -14,32 +15,48 @@ export class ContextCapture {
14
15
  latestEstimate = new Map();
15
16
  deleted = new Set();
16
17
  timings = { requests: 0, mcpReads: 0, mcpWaitMs: 0, storageWrites: 0, storageWriteMs: 0 };
17
- constructor(mcp, storage, ttl = 30_000, now = Date.now, elapsed = () => performance.now()) {
18
+ constructor(mcp, storage, ttl = 30_000, now = Date.now, elapsed = () => performance.now(), mcpTimeoutMs = 100) {
18
19
  this.mcp = mcp;
19
20
  this.storage = storage;
20
21
  this.ttl = ttl;
21
22
  this.now = now;
22
23
  this.elapsed = elapsed;
24
+ this.mcpTimeoutMs = mcpTimeoutMs;
23
25
  }
24
26
  async servers() {
25
27
  if (this.names && this.now() < this.expiresAt)
26
28
  return this.names;
27
29
  if (!this.listing) {
28
30
  this.timings.mcpReads++;
29
- this.listing = this.mcp.list().then(result => {
30
- this.names = result.data.map(server => server.name);
31
- this.expiresAt = this.now() + this.ttl;
32
- return this.names;
33
- }).catch(error => {
34
- if (this.names) {
35
- this.expiresAt = this.now() + this.ttl;
36
- return this.names;
37
- }
38
- throw error;
39
- }).finally(() => { this.listing = undefined; });
31
+ this.listing = this.readServers().finally(() => { this.listing = undefined; });
40
32
  }
41
33
  return this.listing;
42
34
  }
35
+ async readServers() {
36
+ const controller = new AbortController();
37
+ let timer;
38
+ try {
39
+ const timeout = new Promise((_, reject) => {
40
+ timer = setTimeout(() => {
41
+ const error = new DOMException("MCP metadata read timed out", "TimeoutError");
42
+ reject(error);
43
+ controller.abort(error);
44
+ }, this.mcpTimeoutMs);
45
+ });
46
+ const result = await Promise.race([this.mcp.list(undefined, { signal: controller.signal }), timeout]);
47
+ // Only the bounded read may update the cache, even if cancellation is ignored.
48
+ this.names = result.data.map(server => server.name);
49
+ this.expiresAt = this.now() + this.ttl;
50
+ return this.names;
51
+ }
52
+ catch {
53
+ // Reuse stale metadata without renewing it, so the next request can retry.
54
+ return this.names;
55
+ }
56
+ finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
43
60
  invalidateMcp() { this.expiresAt = 0; }
44
61
  capture(request) {
45
62
  const tasks = this.inflight.get(request.sessionID) ?? new Set();
@@ -64,7 +81,7 @@ export class ContextCapture {
64
81
  this.timings.requests++;
65
82
  this.timings.mcpWaitMs += this.elapsed() - start;
66
83
  }
67
- if (this.deleted.has(request.sessionID))
84
+ if (!names || this.deleted.has(request.sessionID))
68
85
  return;
69
86
  const value = estimateContextSources(request, names);
70
87
  this.latestEstimate.set(request.sessionID, value);
@@ -2,13 +2,13 @@ import type { UsageSource } from "./source.js";
2
2
  import { PerformanceMonitor } from "./performance.js";
3
3
  import type { PerformanceSummary } from "./performance.js";
4
4
  import type { ContextSources } from "./context-sources.js";
5
- import type { ContextDetails, ContextUsage, ModelCost, Summary } from "./usage.js";
5
+ import type { ContextUsage, ModelCost, RequestDetails, Summary } from "./usage.js";
6
6
  export interface UsageState {
7
7
  status: "loading" | "ready" | "stale" | "unavailable";
8
8
  summary?: Summary;
9
9
  context?: ContextUsage | undefined;
10
10
  details?: {
11
- context?: ContextDetails | undefined;
11
+ request?: RequestDetails | undefined;
12
12
  models: readonly ModelCost[];
13
13
  sources?: ContextSources | undefined;
14
14
  sessionTitle?: string | undefined;
@@ -1,6 +1,6 @@
1
1
  import { loadSnapshot, uniqueMessages, viewedMessages } from "./source.js";
2
2
  import { PerformanceMonitor, preparePerformance } from "./performance.js";
3
- import { contextDetails, summarize, summarizeModels } from "./usage.js";
3
+ import { contextUsage, requestDetails, summarize, summarizeModels } from "./usage.js";
4
4
  const sessionEvents = new Set([
5
5
  "session.created", "session.deleted", "session.forked", "session.moved",
6
6
  "session.model.selected", "session.agent.selected", "session.step.ended", "session.step.failed",
@@ -164,18 +164,20 @@ export class UsageController {
164
164
  this.prepared = preparePerformance(messages);
165
165
  clearTimeout(this.performanceTimer);
166
166
  this.performanceTimer = undefined;
167
- const context = contextDetails(viewedMessages(snapshot), snapshot.model.context, snapshot.sessions.get(snapshot.viewedID)?.revert?.messageID);
167
+ const history = viewedMessages(snapshot);
168
+ const session = snapshot.sessions.get(snapshot.viewedID);
169
+ const revertMessageID = session.revert?.messageID;
168
170
  this.update({
169
171
  status: "ready",
170
172
  summary: summarize(messages, snapshot.model.catalog),
171
- context: context?.usage,
172
- ...(this.detailed ? { details: { context, models: summarizeModels(messages, snapshot.model.catalog), sessionTitle: snapshot.sessions.get(snapshot.viewedID)?.title } } : {}),
173
+ context: contextUsage(history, snapshot.model.context, revertMessageID),
174
+ ...(this.detailed ? { details: { request: requestDetails(history, revertMessageID), models: summarizeModels(messages, snapshot.model.catalog), sessionTitle: session.title } } : {}),
173
175
  performance: this.performance.summaryPrepared(this.prepared, this.sessions),
174
176
  model: snapshot.model.label,
175
177
  });
176
178
  // The server plugin may be absent. An optional RPC must not delay measured usage.
177
179
  if (this.detailed && this.source.composition) {
178
- void this.source.composition(snapshot.sessions.get(snapshot.viewedID), AbortSignal.any([request.signal, AbortSignal.timeout(5_000)]))
180
+ void this.source.composition(session, AbortSignal.any([request.signal, AbortSignal.timeout(5_000)]))
179
181
  .then(sources => {
180
182
  if (this.disposed || generation !== this.generation || this.snapshot !== snapshot || this.state.status !== "ready" || !this.state.details)
181
183
  return;
@@ -262,6 +262,10 @@ export class PerformanceMonitor {
262
262
  const cutoff = event.created - LIVE_WINDOW_MS;
263
263
  while (step.samples.length > 0 && step.samples[0].time < cutoff)
264
264
  step.samples.shift();
265
+ // Advance smoothing once per accepted delta, independently of summary readers.
266
+ const raw = estimatedLiveTps(step.samples);
267
+ if (raw !== undefined)
268
+ step.liveTps = smoothLiveTps(step.liveTps, raw);
265
269
  step.last = Math.max(step.last ?? event.created, event.created);
266
270
  if (step.first === undefined) {
267
271
  step.first = event.created;
@@ -309,15 +313,13 @@ export class PerformanceMonitor {
309
313
  for (const step of this.active.values()) {
310
314
  if (!sessions.has(step.sessionID) || step.reconciledAt !== undefined)
311
315
  continue;
312
- const raw = estimatedLiveTps(step.samples);
313
- if (raw === undefined)
316
+ if (step.liveTps === undefined || step.samples.length < 2)
314
317
  continue;
315
318
  const first = step.samples[0];
316
319
  const last = step.samples[step.samples.length - 1];
317
320
  const duration = last.time - first.time;
318
321
  if (!finite(duration) || duration <= 0)
319
322
  continue;
320
- step.liveTps = smoothLiveTps(step.liveTps, raw);
321
323
  weighted += step.liveTps * duration;
322
324
  durationTotal += duration;
323
325
  }
package/dist/tui.js CHANGED
@@ -297,7 +297,12 @@ function UsageDialog(props) {
297
297
  }]
298
298
  }));
299
299
  const theme = () => props.context.theme.text;
300
- const context = createMemo(() => state().details?.context);
300
+ const context = createMemo(() => state().context);
301
+ const request = createMemo(() => state().details?.request);
302
+ const requestTime = createMemo(() => {
303
+ const time = request()?.time;
304
+ return time === undefined ? undefined : new Date(time).toLocaleTimeString();
305
+ });
301
306
  const models = createMemo(() => state().details?.models ?? []);
302
307
  const sources = createMemo(() => state().details?.sources);
303
308
  // The host gives this dialog a fixed width; keep every row inside that content box.
@@ -400,12 +405,12 @@ function UsageDialog(props) {
400
405
  _el$30 = _$createElement("text"),
401
406
  _el$31 = _$createElement("b"),
402
407
  _el$33 = _$createElement("text"),
403
- _el$62 = _$createElement("box"),
404
- _el$63 = _$createElement("text"),
405
- _el$65 = _$createElement("text"),
406
- _el$67 = _$createElement("text");
408
+ _el$63 = _$createElement("box"),
409
+ _el$64 = _$createElement("text"),
410
+ _el$66 = _$createElement("text"),
411
+ _el$68 = _$createElement("text");
407
412
  _$insertNode(_el$28, _el$29);
408
- _$insertNode(_el$28, _el$62);
413
+ _$insertNode(_el$28, _el$63);
409
414
  _$setProp(_el$28, "paddingLeft", 2);
410
415
  _$setProp(_el$28, "paddingRight", 2);
411
416
  _$setProp(_el$28, "paddingBottom", 1);
@@ -430,7 +435,7 @@ function UsageDialog(props) {
430
435
  _$effect(_$p => _$setProp(_el$35, "fg", theme().muted, _$p));
431
436
  return _el$35;
432
437
  }
433
- }), _el$62);
438
+ }), _el$63);
434
439
  _$insert(_el$28, _$createComponent(Show, {
435
440
  get when() {
436
441
  return state().status !== "ready";
@@ -441,7 +446,7 @@ function UsageDialog(props) {
441
446
  _$effect(_$p => _$setProp(_el$36, "fg", theme().muted, _$p));
442
447
  return _el$36;
443
448
  }
444
- }), _el$62);
449
+ }), _el$63);
445
450
  _$insert(_el$28, _$createComponent(Show, {
446
451
  get when() {
447
452
  return state().summary;
@@ -457,14 +462,15 @@ function UsageDialog(props) {
457
462
  _el$45 = _$createElement("text"),
458
463
  _el$46 = _$createElement("b"),
459
464
  _el$48 = _$createElement("box"),
460
- _el$49 = _$createElement("text"),
461
- _el$50 = _$createElement("b"),
462
- _el$52 = _$createElement("box"),
463
- _el$53 = _$createElement("text"),
464
- _el$54 = _$createElement("b"),
465
- _el$56 = _$createElement("box"),
466
- _el$57 = _$createElement("text"),
467
- _el$58 = _$createElement("b");
465
+ _el$49 = _$createElement("box"),
466
+ _el$50 = _$createElement("text"),
467
+ _el$51 = _$createElement("b"),
468
+ _el$53 = _$createElement("box"),
469
+ _el$54 = _$createElement("text"),
470
+ _el$55 = _$createElement("b"),
471
+ _el$57 = _$createElement("box"),
472
+ _el$58 = _$createElement("text"),
473
+ _el$59 = _$createElement("b");
468
474
  _$insertNode(_el$37, _el$38);
469
475
  _$use(value => {
470
476
  scroll = value;
@@ -478,8 +484,8 @@ function UsageDialog(props) {
478
484
  _$insertNode(_el$38, _el$39);
479
485
  _$insertNode(_el$38, _el$43);
480
486
  _$insertNode(_el$38, _el$48);
481
- _$insertNode(_el$38, _el$52);
482
- _$insertNode(_el$38, _el$56);
487
+ _$insertNode(_el$38, _el$53);
488
+ _$insertNode(_el$38, _el$57);
483
489
  _$setProp(_el$38, "gap", 1);
484
490
  _$insertNode(_el$39, _el$40);
485
491
  _$insertNode(_el$40, _el$41);
@@ -490,48 +496,48 @@ function UsageDialog(props) {
490
496
  },
491
497
  get fallback() {
492
498
  return (() => {
493
- var _el$69 = _$createElement("text");
494
- _$insertNode(_el$69, _$createTextNode(`Context usage unavailable`));
495
- _$effect(_$p => _$setProp(_el$69, "fg", theme().muted, _$p));
496
- return _el$69;
499
+ var _el$70 = _$createElement("text");
500
+ _$insertNode(_el$70, _$createTextNode(`Context usage unavailable`));
501
+ _$effect(_$p => _$setProp(_el$70, "fg", theme().muted, _$p));
502
+ return _el$70;
497
503
  })();
498
504
  },
499
505
  children: details => (() => {
500
- var _el$71 = _$createElement("box"),
501
- _el$72 = _$createElement("box"),
502
- _el$73 = _$createElement("text"),
503
- _el$75 = _$createElement("text"),
504
- _el$76 = _$createTextNode(` / `),
505
- _el$77 = _$createTextNode(` (`),
506
- _el$78 = _$createTextNode(`%)`);
507
- _$insertNode(_el$71, _el$72);
506
+ var _el$72 = _$createElement("box"),
507
+ _el$73 = _$createElement("box"),
508
+ _el$74 = _$createElement("text"),
509
+ _el$76 = _$createElement("text"),
510
+ _el$77 = _$createTextNode(` / `),
511
+ _el$78 = _$createTextNode(` (`),
512
+ _el$79 = _$createTextNode(`%)`);
508
513
  _$insertNode(_el$72, _el$73);
509
- _$insertNode(_el$72, _el$75);
510
- _$setProp(_el$72, "flexDirection", "row");
511
- _$setProp(_el$72, "justifyContent", "space-between");
512
- _$setProp(_el$72, "gap", 2);
513
- _$insertNode(_el$73, _$createTextNode(`Used / Limit`));
514
- _$setProp(_el$73, "flexShrink", 0);
515
- _$insertNode(_el$75, _el$76);
516
- _$insertNode(_el$75, _el$77);
517
- _$insertNode(_el$75, _el$78);
518
- _$setProp(_el$75, "wrapMode", "word");
519
- _$setProp(_el$75, "minWidth", 0);
520
- _$insert(_el$75, () => number(details().usage.used), _el$76);
521
- _$insert(_el$75, () => number(details().usage.limit), _el$77);
522
- _$insert(_el$75, () => details().usage.percent.toFixed(1), _el$78);
523
- _$insert(_el$71, () => barRow(details().usage.percent, windowBar()), null);
514
+ _$insertNode(_el$73, _el$74);
515
+ _$insertNode(_el$73, _el$76);
516
+ _$setProp(_el$73, "flexDirection", "row");
517
+ _$setProp(_el$73, "justifyContent", "space-between");
518
+ _$setProp(_el$73, "gap", 2);
519
+ _$insertNode(_el$74, _$createTextNode(`Used / Limit`));
520
+ _$setProp(_el$74, "flexShrink", 0);
521
+ _$insertNode(_el$76, _el$77);
522
+ _$insertNode(_el$76, _el$78);
523
+ _$insertNode(_el$76, _el$79);
524
+ _$setProp(_el$76, "wrapMode", "word");
525
+ _$setProp(_el$76, "minWidth", 0);
526
+ _$insert(_el$76, () => number(details().used), _el$77);
527
+ _$insert(_el$76, () => number(details().limit), _el$78);
528
+ _$insert(_el$76, () => details().percent.toFixed(1), _el$79);
529
+ _$insert(_el$72, () => barRow(details().percent, windowBar()), null);
524
530
  _$effect(_p$ => {
525
531
  var _v$21 = theme().muted,
526
532
  _v$22 = theme().base;
527
- _v$21 !== _p$.e && (_p$.e = _$setProp(_el$73, "fg", _v$21, _p$.e));
528
- _v$22 !== _p$.t && (_p$.t = _$setProp(_el$75, "fg", _v$22, _p$.t));
533
+ _v$21 !== _p$.e && (_p$.e = _$setProp(_el$74, "fg", _v$21, _p$.e));
534
+ _v$22 !== _p$.t && (_p$.t = _$setProp(_el$76, "fg", _v$22, _p$.t));
529
535
  return _p$;
530
536
  }, {
531
537
  e: undefined,
532
538
  t: undefined
533
539
  });
534
- return _el$71;
540
+ return _el$72;
535
541
  })()
536
542
  }), null);
537
543
  _$insertNode(_el$43, _el$44);
@@ -543,46 +549,61 @@ function UsageDialog(props) {
543
549
  _$insertNode(_el$46, _$createTextNode(`Last Request`));
544
550
  _$insert(_el$44, _$createComponent(Show, {
545
551
  get when() {
546
- return sources();
552
+ return requestTime();
547
553
  },
548
- children: captured => (() => {
549
- var _el$79 = _$createElement("text");
550
- _$insert(_el$79, () => new Date(captured().capturedAt).toLocaleTimeString());
551
- _$effect(_$p => _$setProp(_el$79, "fg", theme().muted, _$p));
552
- return _el$79;
554
+ children: time => (() => {
555
+ var _el$80 = _$createElement("text");
556
+ _$insert(_el$80, time);
557
+ _$effect(_$p => _$setProp(_el$80, "fg", theme().muted, _$p));
558
+ return _el$80;
553
559
  })()
554
560
  }), null);
555
561
  _$insert(_el$43, _$createComponent(Show, {
556
562
  get when() {
557
- return context();
563
+ return request();
558
564
  },
559
565
  get fallback() {
560
566
  return (() => {
561
- var _el$80 = _$createElement("text");
562
- _$insertNode(_el$80, _$createTextNode(`Request composition unavailable`));
563
- _$effect(_$p => _$setProp(_el$80, "fg", theme().muted, _$p));
564
- return _el$80;
567
+ var _el$81 = _$createElement("text");
568
+ _$insertNode(_el$81, _$createTextNode(`Request usage unavailable`));
569
+ _$effect(_$p => _$setProp(_el$81, "fg", theme().muted, _$p));
570
+ return _el$81;
565
571
  })();
566
572
  },
567
573
  children: details => (() => {
568
- var _el$82 = _$createElement("box");
569
- _$insert(_el$82, () => requestRowsList(requestRows(details(), compact())));
570
- return _el$82;
574
+ var _el$83 = _$createElement("box");
575
+ _$insert(_el$83, () => requestRowsList(requestRows(details(), compact())));
576
+ return _el$83;
571
577
  })()
572
578
  }), null);
573
579
  _$insertNode(_el$48, _el$49);
574
580
  _$insertNode(_el$49, _el$50);
575
- _$insertNode(_el$50, _$createTextNode(`Context Breakdown`));
581
+ _$setProp(_el$49, "flexDirection", "row");
582
+ _$setProp(_el$49, "justifyContent", "space-between");
583
+ _$setProp(_el$49, "gap", 2);
584
+ _$insertNode(_el$50, _el$51);
585
+ _$insertNode(_el$51, _$createTextNode(`Context Breakdown`));
586
+ _$insert(_el$49, _$createComponent(Show, {
587
+ get when() {
588
+ return sources();
589
+ },
590
+ children: captured => (() => {
591
+ var _el$84 = _$createElement("text");
592
+ _$insert(_el$84, () => new Date(captured().capturedAt).toLocaleTimeString());
593
+ _$effect(_$p => _$setProp(_el$84, "fg", theme().muted, _$p));
594
+ return _el$84;
595
+ })()
596
+ }), null);
576
597
  _$insert(_el$48, _$createComponent(Show, {
577
598
  get when() {
578
599
  return sources();
579
600
  },
580
601
  get fallback() {
581
602
  return (() => {
582
- var _el$83 = _$createElement("text");
583
- _$insertNode(_el$83, _$createTextNode(`Source estimates unavailable (no request snapshot or RPC)`));
584
- _$effect(_$p => _$setProp(_el$83, "fg", theme().muted, _$p));
585
- return _el$83;
603
+ var _el$85 = _$createElement("text");
604
+ _$insertNode(_el$85, _$createTextNode(`Source estimates unavailable (no request snapshot or RPC)`));
605
+ _$effect(_$p => _$setProp(_el$85, "fg", theme().muted, _$p));
606
+ return _el$85;
586
607
  })();
587
608
  },
588
609
  children: captured => _$createComponent(For, {
@@ -590,39 +611,39 @@ function UsageDialog(props) {
590
611
  return breakdownRows(captured(), detailed());
591
612
  },
592
613
  children: row => (() => {
593
- var _el$85 = _$createElement("box"),
594
- _el$86 = _$createElement("box"),
595
- _el$87 = _$createElement("text"),
614
+ var _el$87 = _$createElement("box"),
596
615
  _el$88 = _$createElement("box"),
597
616
  _el$89 = _$createElement("text"),
598
- _el$90 = _$createElement("text"),
599
- _el$91 = _$createTextNode(`%`);
600
- _$insertNode(_el$85, _el$86);
601
- _$insertNode(_el$85, _el$88);
602
- _$insertNode(_el$85, _el$89);
603
- _$insertNode(_el$85, _el$90);
604
- _$setProp(_el$85, "flexDirection", "row");
605
- _$setProp(_el$85, "gap", 1);
606
- _$insertNode(_el$86, _el$87);
607
- _$setProp(_el$86, "width", 14);
608
- _$setProp(_el$86, "flexShrink", 0);
609
- _$insert(_el$87, () => row.label);
617
+ _el$90 = _$createElement("box"),
618
+ _el$91 = _$createElement("text"),
619
+ _el$92 = _$createElement("text"),
620
+ _el$93 = _$createTextNode(`%`);
621
+ _$insertNode(_el$87, _el$88);
622
+ _$insertNode(_el$87, _el$90);
623
+ _$insertNode(_el$87, _el$91);
624
+ _$insertNode(_el$87, _el$92);
625
+ _$setProp(_el$87, "flexDirection", "row");
626
+ _$setProp(_el$87, "gap", 1);
627
+ _$insertNode(_el$88, _el$89);
628
+ _$setProp(_el$88, "width", 14);
610
629
  _$setProp(_el$88, "flexShrink", 0);
611
- _$insert(_el$88, () => barRow(row.percent, breakdownBar()));
612
- _$setProp(_el$89, "flexShrink", 0);
613
- _$insert(_el$89, () => number(row.tokens));
614
- _$insertNode(_el$90, _el$91);
630
+ _$insert(_el$89, () => row.label);
615
631
  _$setProp(_el$90, "flexShrink", 0);
616
- _$insert(_el$90, () => row.percent.toFixed(1), _el$91);
632
+ _$insert(_el$90, () => barRow(row.percent, breakdownBar()));
633
+ _$setProp(_el$91, "flexShrink", 0);
634
+ _$insert(_el$91, () => number(row.tokens));
635
+ _$insertNode(_el$92, _el$93);
636
+ _$setProp(_el$92, "flexShrink", 0);
637
+ _$insert(_el$92, () => row.percent.toFixed(1), _el$93);
617
638
  _$effect(_p$ => {
618
639
  var _v$23 = theme().muted,
619
640
  _v$24 = breakdownBar(),
620
641
  _v$25 = theme().base,
621
642
  _v$26 = theme().muted;
622
- _v$23 !== _p$.e && (_p$.e = _$setProp(_el$87, "fg", _v$23, _p$.e));
623
- _v$24 !== _p$.t && (_p$.t = _$setProp(_el$88, "width", _v$24, _p$.t));
624
- _v$25 !== _p$.a && (_p$.a = _$setProp(_el$89, "fg", _v$25, _p$.a));
625
- _v$26 !== _p$.o && (_p$.o = _$setProp(_el$90, "fg", _v$26, _p$.o));
643
+ _v$23 !== _p$.e && (_p$.e = _$setProp(_el$89, "fg", _v$23, _p$.e));
644
+ _v$24 !== _p$.t && (_p$.t = _$setProp(_el$90, "width", _v$24, _p$.t));
645
+ _v$25 !== _p$.a && (_p$.a = _$setProp(_el$91, "fg", _v$25, _p$.a));
646
+ _v$26 !== _p$.o && (_p$.o = _$setProp(_el$92, "fg", _v$26, _p$.o));
626
647
  return _p$;
627
648
  }, {
628
649
  e: undefined,
@@ -630,23 +651,23 @@ function UsageDialog(props) {
630
651
  a: undefined,
631
652
  o: undefined
632
653
  });
633
- return _el$85;
654
+ return _el$87;
634
655
  })()
635
656
  })
636
657
  }), null);
637
- _$insertNode(_el$52, _el$53);
638
658
  _$insertNode(_el$53, _el$54);
639
- _$insertNode(_el$54, _$createTextNode(`Session`));
640
- _$insert(_el$52, _$createComponent(Show, {
659
+ _$insertNode(_el$54, _el$55);
660
+ _$insertNode(_el$55, _$createTextNode(`Session`));
661
+ _$insert(_el$53, _$createComponent(Show, {
641
662
  get when() {
642
663
  return state().summary;
643
664
  },
644
665
  get fallback() {
645
666
  return (() => {
646
- var _el$92 = _$createElement("text");
647
- _$insertNode(_el$92, _$createTextNode(`Session usage unavailable`));
648
- _$effect(_$p => _$setProp(_el$92, "fg", theme().muted, _$p));
649
- return _el$92;
667
+ var _el$94 = _$createElement("text");
668
+ _$insertNode(_el$94, _$createTextNode(`Session usage unavailable`));
669
+ _$effect(_$p => _$setProp(_el$94, "fg", theme().muted, _$p));
670
+ return _el$94;
650
671
  })();
651
672
  },
652
673
  children: summary => _$createComponent(Show, {
@@ -655,26 +676,26 @@ function UsageDialog(props) {
655
676
  },
656
677
  get fallback() {
657
678
  return (() => {
658
- var _el$94 = _$createElement("text"),
659
- _el$95 = _$createTextNode(` · `),
660
- _el$96 = _$createTextNode(` · `),
661
- _el$97 = _$createTextNode(` tokens · `),
662
- _el$98 = _$createTextNode(`% cached`);
663
- _$insertNode(_el$94, _el$95);
664
- _$insertNode(_el$94, _el$96);
665
- _$insertNode(_el$94, _el$97);
666
- _$insertNode(_el$94, _el$98);
667
- _$setProp(_el$94, "wrapMode", "word");
668
- _$insert(_el$94, () => countLabel(summary().steps, "step"), _el$95);
669
- _$insert(_el$94, () => countLabel(summary().calls, "call"), _el$96);
670
- _$insert(_el$94, () => formatCompact(summary().total), _el$97);
671
- _$insert(_el$94, () => (summary().cacheRate * 100).toFixed(1), _el$98);
672
- _$insert(_el$94, (() => {
679
+ var _el$96 = _$createElement("text"),
680
+ _el$97 = _$createTextNode(` · `),
681
+ _el$98 = _$createTextNode(` · `),
682
+ _el$99 = _$createTextNode(` tokens · `),
683
+ _el$100 = _$createTextNode(`% cached`);
684
+ _$insertNode(_el$96, _el$97);
685
+ _$insertNode(_el$96, _el$98);
686
+ _$insertNode(_el$96, _el$99);
687
+ _$insertNode(_el$96, _el$100);
688
+ _$setProp(_el$96, "wrapMode", "word");
689
+ _$insert(_el$96, () => countLabel(summary().steps, "step"), _el$97);
690
+ _$insert(_el$96, () => countLabel(summary().calls, "call"), _el$98);
691
+ _$insert(_el$96, () => formatCompact(summary().total), _el$99);
692
+ _$insert(_el$96, () => (summary().cacheRate * 100).toFixed(1), _el$100);
693
+ _$insert(_el$96, (() => {
673
694
  var _c$ = _$memo(() => !!formatEstimatedCost(summary().cost, summary().costStatus));
674
695
  return () => _c$() ? ` · ${formatEstimatedCost(summary().cost, summary().costStatus)}` : "";
675
696
  })(), null);
676
- _$effect(_$p => _$setProp(_el$94, "fg", theme().base, _$p));
677
- return _el$94;
697
+ _$effect(_$p => _$setProp(_el$96, "fg", theme().base, _$p));
698
+ return _el$96;
678
699
  })();
679
700
  },
680
701
  get children() {
@@ -683,58 +704,58 @@ function UsageDialog(props) {
683
704
  return summaryRows(summary());
684
705
  },
685
706
  children: ([label, value]) => (() => {
686
- var _el$99 = _$createElement("box"),
687
- _el$100 = _$createElement("text"),
688
- _el$101 = _$createElement("text");
689
- _$insertNode(_el$99, _el$100);
690
- _$insertNode(_el$99, _el$101);
691
- _$setProp(_el$99, "flexDirection", "row");
692
- _$setProp(_el$99, "justifyContent", "space-between");
693
- _$setProp(_el$99, "gap", 2);
694
- _$setProp(_el$100, "flexShrink", 0);
695
- _$insert(_el$100, label);
696
- _$insert(_el$101, value);
707
+ var _el$101 = _$createElement("box"),
708
+ _el$102 = _$createElement("text"),
709
+ _el$103 = _$createElement("text");
710
+ _$insertNode(_el$101, _el$102);
711
+ _$insertNode(_el$101, _el$103);
712
+ _$setProp(_el$101, "flexDirection", "row");
713
+ _$setProp(_el$101, "justifyContent", "space-between");
714
+ _$setProp(_el$101, "gap", 2);
715
+ _$setProp(_el$102, "flexShrink", 0);
716
+ _$insert(_el$102, label);
717
+ _$insert(_el$103, value);
697
718
  _$effect(_p$ => {
698
719
  var _v$27 = theme().muted,
699
720
  _v$28 = theme().base;
700
- _v$27 !== _p$.e && (_p$.e = _$setProp(_el$100, "fg", _v$27, _p$.e));
701
- _v$28 !== _p$.t && (_p$.t = _$setProp(_el$101, "fg", _v$28, _p$.t));
721
+ _v$27 !== _p$.e && (_p$.e = _$setProp(_el$102, "fg", _v$27, _p$.e));
722
+ _v$28 !== _p$.t && (_p$.t = _$setProp(_el$103, "fg", _v$28, _p$.t));
702
723
  return _p$;
703
724
  }, {
704
725
  e: undefined,
705
726
  t: undefined
706
727
  });
707
- return _el$99;
728
+ return _el$101;
708
729
  })()
709
730
  });
710
731
  }
711
732
  })
712
733
  }), null);
713
- _$insertNode(_el$56, _el$57);
714
734
  _$insertNode(_el$57, _el$58);
715
- _$insertNode(_el$58, _$createTextNode(`By Model`));
716
- _$insert(_el$56, _$createComponent(Show, {
735
+ _$insertNode(_el$58, _el$59);
736
+ _$insertNode(_el$59, _$createTextNode(`By Model`));
737
+ _$insert(_el$57, _$createComponent(Show, {
717
738
  get when() {
718
739
  return _$memo(() => !!detailed())() && models().length;
719
740
  },
720
741
  get children() {
721
- var _el$60 = _$createElement("text");
722
- _$insertNode(_el$60, _$createTextNode(`Rates used for Est. Cost · USD / 1M text tokens · incoming = Input + Cache Read + Cache Write`));
723
- _$setProp(_el$60, "wrapMode", "word");
724
- _$effect(_$p => _$setProp(_el$60, "fg", theme().muted, _$p));
725
- return _el$60;
742
+ var _el$61 = _$createElement("text");
743
+ _$insertNode(_el$61, _$createTextNode(`Rates used for Est. Cost · USD / 1M text tokens · incoming = Input + Cache Read + Cache Write`));
744
+ _$setProp(_el$61, "wrapMode", "word");
745
+ _$effect(_$p => _$setProp(_el$61, "fg", theme().muted, _$p));
746
+ return _el$61;
726
747
  }
727
748
  }), null);
728
- _$insert(_el$56, _$createComponent(Show, {
749
+ _$insert(_el$57, _$createComponent(Show, {
729
750
  get when() {
730
751
  return models().length;
731
752
  },
732
753
  get fallback() {
733
754
  return (() => {
734
- var _el$102 = _$createElement("text");
735
- _$insertNode(_el$102, _$createTextNode(`No model usage yet`));
736
- _$effect(_$p => _$setProp(_el$102, "fg", theme().muted, _$p));
737
- return _el$102;
755
+ var _el$104 = _$createElement("text");
756
+ _$insertNode(_el$104, _$createTextNode(`No model usage yet`));
757
+ _$effect(_$p => _$setProp(_el$104, "fg", theme().muted, _$p));
758
+ return _el$104;
738
759
  })();
739
760
  },
740
761
  get children() {
@@ -743,29 +764,29 @@ function UsageDialog(props) {
743
764
  return models();
744
765
  },
745
766
  children: model => (() => {
746
- var _el$104 = _$createElement("box"),
747
- _el$105 = _$createElement("box"),
748
- _el$106 = _$createElement("text"),
749
- _el$107 = _$createElement("text"),
767
+ var _el$106 = _$createElement("box"),
768
+ _el$107 = _$createElement("box"),
750
769
  _el$108 = _$createElement("text"),
751
- _el$109 = _$createTextNode(` tokens · `);
752
- _$insertNode(_el$104, _el$105);
753
- _$insertNode(_el$104, _el$108);
754
- _$insertNode(_el$105, _el$106);
755
- _$insertNode(_el$105, _el$107);
756
- _$setProp(_el$105, "flexDirection", "row");
757
- _$setProp(_el$105, "justifyContent", "space-between");
758
- _$setProp(_el$105, "gap", 2);
759
- _$setProp(_el$106, "wrapMode", "word");
760
- _$setProp(_el$106, "minWidth", 0);
761
- _$setProp(_el$106, "flexGrow", 1);
762
- _$insert(_el$106, () => model.model);
763
- _$setProp(_el$107, "flexShrink", 0);
764
- _$insert(_el$107, () => formatEstimatedCost(model.cost, model.costStatus) ?? "");
765
- _$insertNode(_el$108, _el$109);
766
- _$insert(_el$108, () => number(model.tokens), _el$109);
767
- _$insert(_el$108, () => countLabel(model.calls, "call"), null);
768
- _$insert(_el$104, _$createComponent(Show, {
770
+ _el$109 = _$createElement("text"),
771
+ _el$110 = _$createElement("text"),
772
+ _el$111 = _$createTextNode(` tokens · `);
773
+ _$insertNode(_el$106, _el$107);
774
+ _$insertNode(_el$106, _el$110);
775
+ _$insertNode(_el$107, _el$108);
776
+ _$insertNode(_el$107, _el$109);
777
+ _$setProp(_el$107, "flexDirection", "row");
778
+ _$setProp(_el$107, "justifyContent", "space-between");
779
+ _$setProp(_el$107, "gap", 2);
780
+ _$setProp(_el$108, "wrapMode", "word");
781
+ _$setProp(_el$108, "minWidth", 0);
782
+ _$setProp(_el$108, "flexGrow", 1);
783
+ _$insert(_el$108, () => model.model);
784
+ _$setProp(_el$109, "flexShrink", 0);
785
+ _$insert(_el$109, () => formatEstimatedCost(model.cost, model.costStatus) ?? "");
786
+ _$insertNode(_el$110, _el$111);
787
+ _$insert(_el$110, () => number(model.tokens), _el$111);
788
+ _$insert(_el$110, () => countLabel(model.calls, "call"), null);
789
+ _$insert(_el$106, _$createComponent(Show, {
769
790
  get when() {
770
791
  return detailed();
771
792
  },
@@ -775,34 +796,34 @@ function UsageDialog(props) {
775
796
  return model.appliedRates;
776
797
  },
777
798
  children: rates => (() => {
778
- var _el$112 = _$createElement("box"),
779
- _el$113 = _$createElement("text"),
780
- _el$114 = _$createTextNode(` · `),
781
- _el$115 = _$createTextNode(` · `);
782
- _$insertNode(_el$112, _el$113);
783
- _$setProp(_el$112, "marginTop", 1);
784
- _$insertNode(_el$113, _el$114);
785
- _$insertNode(_el$113, _el$115);
786
- _$setProp(_el$113, "wrapMode", "word");
787
- _$insert(_el$113, () => rates.source, _el$114);
788
- _$insert(_el$113, () => formatRateTier(rates), _el$115);
789
- _$insert(_el$113, () => countLabel(rates.calls, "call"), null);
790
- _$insert(_el$112, () => requestRowsList(rateRows(rates)), null);
791
- _$effect(_$p => _$setProp(_el$113, "fg", theme().muted, _$p));
792
- return _el$112;
799
+ var _el$114 = _$createElement("box"),
800
+ _el$115 = _$createElement("text"),
801
+ _el$116 = _$createTextNode(` · `),
802
+ _el$117 = _$createTextNode(` · `);
803
+ _$insertNode(_el$114, _el$115);
804
+ _$setProp(_el$114, "marginTop", 1);
805
+ _$insertNode(_el$115, _el$116);
806
+ _$insertNode(_el$115, _el$117);
807
+ _$setProp(_el$115, "wrapMode", "word");
808
+ _$insert(_el$115, () => rates.source, _el$116);
809
+ _$insert(_el$115, () => formatRateTier(rates), _el$117);
810
+ _$insert(_el$115, () => countLabel(rates.calls, "call"), null);
811
+ _$insert(_el$114, () => requestRowsList(rateRows(rates)), null);
812
+ _$effect(_$p => _$setProp(_el$115, "fg", theme().muted, _$p));
813
+ return _el$114;
793
814
  })()
794
815
  }), _$createComponent(Show, {
795
816
  get when() {
796
817
  return model.unpricedCalls > 0;
797
818
  },
798
819
  get children() {
799
- var _el$110 = _$createElement("text"),
800
- _el$111 = _$createTextNode(`Pricing unavailable for `);
801
- _$insertNode(_el$110, _el$111);
802
- _$setProp(_el$110, "wrapMode", "word");
803
- _$insert(_el$110, () => countLabel(model.unpricedCalls, "call"), null);
804
- _$effect(_$p => _$setProp(_el$110, "fg", theme().muted, _$p));
805
- return _el$110;
820
+ var _el$112 = _$createElement("text"),
821
+ _el$113 = _$createTextNode(`Pricing unavailable for `);
822
+ _$insertNode(_el$112, _el$113);
823
+ _$setProp(_el$112, "wrapMode", "word");
824
+ _$insert(_el$112, () => countLabel(model.unpricedCalls, "call"), null);
825
+ _$effect(_$p => _$setProp(_el$112, "fg", theme().muted, _$p));
826
+ return _el$112;
806
827
  }
807
828
  })];
808
829
  }
@@ -811,16 +832,16 @@ function UsageDialog(props) {
811
832
  var _v$29 = theme().base,
812
833
  _v$30 = theme().base,
813
834
  _v$31 = theme().muted;
814
- _v$29 !== _p$.e && (_p$.e = _$setProp(_el$106, "fg", _v$29, _p$.e));
815
- _v$30 !== _p$.t && (_p$.t = _$setProp(_el$107, "fg", _v$30, _p$.t));
816
- _v$31 !== _p$.a && (_p$.a = _$setProp(_el$108, "fg", _v$31, _p$.a));
835
+ _v$29 !== _p$.e && (_p$.e = _$setProp(_el$108, "fg", _v$29, _p$.e));
836
+ _v$30 !== _p$.t && (_p$.t = _$setProp(_el$109, "fg", _v$30, _p$.t));
837
+ _v$31 !== _p$.a && (_p$.a = _$setProp(_el$110, "fg", _v$31, _p$.a));
817
838
  return _p$;
818
839
  }, {
819
840
  e: undefined,
820
841
  t: undefined,
821
842
  a: undefined
822
843
  });
823
- return _el$104;
844
+ return _el$106;
824
845
  })()
825
846
  });
826
847
  }
@@ -835,9 +856,9 @@ function UsageDialog(props) {
835
856
  _v$10 !== _p$.e && (_p$.e = _$setProp(_el$37, "maxHeight", _v$10, _p$.e));
836
857
  _v$11 !== _p$.t && (_p$.t = _$setProp(_el$40, "fg", _v$11, _p$.t));
837
858
  _v$12 !== _p$.a && (_p$.a = _$setProp(_el$45, "fg", _v$12, _p$.a));
838
- _v$13 !== _p$.o && (_p$.o = _$setProp(_el$49, "fg", _v$13, _p$.o));
839
- _v$14 !== _p$.i && (_p$.i = _$setProp(_el$53, "fg", _v$14, _p$.i));
840
- _v$15 !== _p$.n && (_p$.n = _$setProp(_el$57, "fg", _v$15, _p$.n));
859
+ _v$13 !== _p$.o && (_p$.o = _$setProp(_el$50, "fg", _v$13, _p$.o));
860
+ _v$14 !== _p$.i && (_p$.i = _$setProp(_el$54, "fg", _v$14, _p$.i));
861
+ _v$15 !== _p$.n && (_p$.n = _$setProp(_el$58, "fg", _v$15, _p$.n));
841
862
  return _p$;
842
863
  }, {
843
864
  e: undefined,
@@ -849,17 +870,17 @@ function UsageDialog(props) {
849
870
  });
850
871
  return _el$37;
851
872
  }
852
- }), _el$62);
853
- _$insertNode(_el$62, _el$63);
854
- _$insertNode(_el$62, _el$65);
855
- _$insertNode(_el$62, _el$67);
856
- _$setProp(_el$62, "flexDirection", "row");
857
- _$insertNode(_el$63, _$createTextNode(`↑/↓ scroll · `));
858
- _$insertNode(_el$65, _$createTextNode(`d details`));
859
- _$setProp(_el$65, "onMouseUp", event => {
873
+ }), _el$63);
874
+ _$insertNode(_el$63, _el$64);
875
+ _$insertNode(_el$63, _el$66);
876
+ _$insertNode(_el$63, _el$68);
877
+ _$setProp(_el$63, "flexDirection", "row");
878
+ _$insertNode(_el$64, _$createTextNode(`↑/↓ scroll · `));
879
+ _$insertNode(_el$66, _$createTextNode(`d details`));
880
+ _$setProp(_el$66, "onMouseUp", event => {
860
881
  if (event.button === 0) toggleDetails();
861
882
  });
862
- _$insertNode(_el$67, _$createTextNode(` · esc close`));
883
+ _$insertNode(_el$68, _$createTextNode(` · esc close`));
863
884
  _$effect(_p$ => {
864
885
  var _v$16 = theme().base,
865
886
  _v$17 = theme().muted,
@@ -868,9 +889,9 @@ function UsageDialog(props) {
868
889
  _v$20 = theme().muted;
869
890
  _v$16 !== _p$.e && (_p$.e = _$setProp(_el$30, "fg", _v$16, _p$.e));
870
891
  _v$17 !== _p$.t && (_p$.t = _$setProp(_el$33, "fg", _v$17, _p$.t));
871
- _v$18 !== _p$.a && (_p$.a = _$setProp(_el$63, "fg", _v$18, _p$.a));
872
- _v$19 !== _p$.o && (_p$.o = _$setProp(_el$65, "fg", _v$19, _p$.o));
873
- _v$20 !== _p$.i && (_p$.i = _$setProp(_el$67, "fg", _v$20, _p$.i));
892
+ _v$18 !== _p$.a && (_p$.a = _$setProp(_el$64, "fg", _v$18, _p$.a));
893
+ _v$19 !== _p$.o && (_p$.o = _$setProp(_el$66, "fg", _v$19, _p$.o));
894
+ _v$20 !== _p$.i && (_p$.i = _$setProp(_el$68, "fg", _v$20, _p$.i));
874
895
  return _p$;
875
896
  }, {
876
897
  e: undefined,
package/dist/usage.d.ts CHANGED
@@ -128,11 +128,13 @@ export interface ContextUsage {
128
128
  limit: number;
129
129
  percent: number;
130
130
  }
131
- export interface ContextDetails {
132
- usage: ContextUsage;
131
+ export interface RequestDetails {
133
132
  tokens: Tokens;
133
+ total: number;
134
+ /** The selected assistant's stream end, completion, or creation time, in that order. */
135
+ time?: number | undefined;
134
136
  }
135
- export declare function contextDetails(messages: readonly UsageMessage[], limit?: number, revertMessageID?: string): ContextDetails | undefined;
137
+ export declare function requestDetails(messages: readonly UsageMessage[], revertMessageID?: string): RequestDetails | undefined;
136
138
  export declare function contextUsage(messages: readonly UsageMessage[], limit?: number, revertMessageID?: string): ContextUsage | undefined;
137
139
  export declare function formatTokens(value: number): string;
138
140
  export declare const formatCost: (value: number) => string;
@@ -152,7 +154,7 @@ export declare function bar(percent: number, width: number): {
152
154
  * One model request: the five categories of the last assistant call plus its cache rate.
153
155
  * Compact mode hides zero rows and drops percentages; detailed mode keeps both.
154
156
  */
155
- export declare function requestRows(details: ContextDetails, compact: boolean): readonly (readonly [string, string])[];
157
+ export declare function requestRows(details: RequestDetails, compact: boolean): readonly (readonly [string, string])[];
156
158
  /** Tree summary rows for the dialog's detailed Session view, calls included. */
157
159
  export declare function summaryRows(summary: Summary): readonly (readonly [string, string])[];
158
160
  /** `1 step` reads better than `1 steps` in the dialog's single-line summaries. */
package/dist/usage.js CHANGED
@@ -150,13 +150,12 @@ export function summarizeModels(messages, catalog = new Map()) {
150
150
  })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
151
151
  }
152
152
  const usableLimit = (value) => value !== undefined && Number.isFinite(value) && value > 0;
153
+ const usableTime = (value) => value !== undefined && value >= 0 && Number.isFinite(new Date(value).getTime());
153
154
  // Matches the OpenCode 2.0.10 sidebar: the last assistant message with tokens that follows
154
155
  // the last completed compaction carries the current context size. Running or failed
155
- // compactions do not reset it, and a last message without usage hides the row entirely
156
- // instead of falling back to pre-compaction history.
157
- export function contextDetails(messages, limit, revertMessageID) {
158
- if (!usableLimit(limit))
159
- return undefined;
156
+ // compactions do not reset it. Missing token reports are skipped, while an explicit
157
+ // zero report is not replaced by older usage. Request details need no model limit.
158
+ export function requestDetails(messages, revertMessageID) {
160
159
  // The staged revert excludes its boundary message and every later message.
161
160
  // Session.message.list is ascending; an absent marker can occur during a
162
161
  // concurrent commit, in which case the ordered ID still gives the cutoff.
@@ -178,12 +177,16 @@ export function contextDetails(messages, limit, revertMessageID) {
178
177
  continue;
179
178
  const tokens = normalize(message.tokens);
180
179
  const used = total(tokens);
181
- return used > 0 ? { usage: { used, limit, percent: used / limit * 100 }, tokens } : undefined;
180
+ const time = [message.time?.streamed, message.time?.completed, message.time?.created].find(usableTime);
181
+ return used > 0 ? { tokens, total: used, time } : undefined;
182
182
  }
183
183
  return undefined;
184
184
  }
185
185
  export function contextUsage(messages, limit, revertMessageID) {
186
- return contextDetails(messages, limit, revertMessageID)?.usage;
186
+ if (!usableLimit(limit))
187
+ return undefined;
188
+ const request = requestDetails(messages, revertMessageID);
189
+ return request ? { used: request.total, limit, percent: request.total / limit * 100 } : undefined;
187
190
  }
188
191
  export function formatTokens(value) {
189
192
  return Math.round(safe(value)).toLocaleString("en-US");
@@ -251,7 +254,7 @@ export function requestRows(details, compact) {
251
254
  return [
252
255
  ...rows.map(([label, count]) => [
253
256
  label,
254
- compact ? formatCompact(count) : `${formatTokens(count)} (${(count / details.usage.used * 100).toFixed(1)}%)`,
257
+ compact ? formatCompact(count) : `${formatTokens(count)} (${(count / details.total * 100).toFixed(1)}%)`,
255
258
  ]),
256
259
  ["Cache Rate", `${(incomingTokens ? t.cache.read / incomingTokens * 100 : 0).toFixed(1)}%`],
257
260
  ];
@@ -1,5 +1,69 @@
1
1
  # 验证记录
2
2
 
3
+ ## 2026-10-09 · 统计钩子的 MCP 等待上限
4
+
5
+ 环境:macOS、Node.js v22.23.2、npm 10.9.8,插件包仍为 0.4.6、SDK 2.0.24,真实宿主为 PATH 中的 OpenCode 2.0.25。未升级版本、提交、推送或发布。
6
+
7
+ MCP 元数据缓存缺失或失效时,共享读取默认最多等待 100 ms,构造参数可注入测试预算。独立定时器结束等待,同时通过 SDK 的 `AbortSignal` 请求中止读取;忽略取消的迟到成功/失败不会更新缓存或清除后续读取。超时或错误时复用旧元数据,不延长缓存 TTL,后续请求可重试;无缓存则跳过本次估算,保留已有估算与时间戳,不写入伪造零值。成功读取的空列表仍是有效缓存。持久化继续按会话顺序异步执行。
8
+
9
+ | 检查 | 结果 |
10
+ | --- | --- |
11
+ | `npm run typecheck` | 通过 |
12
+ | `node --import tsx --test test/context-capture.test.ts` | 专项 14 项通过,新增 11 项回归测试 |
13
+ | `npm test` | 129 项通过 |
14
+ | `npm run build` | 通过 |
15
+ | `npm run test:smoke` | 完整通过,打包/隔离安装 + 真实 OpenCode 2.0.25 + 本地模拟提供商 |
16
+
17
+ 新增测试覆盖永久挂起、100 ms 默认预算与可注入预算、并发共享读取、TTL/事件失效后的旧缓存回退、空缓存、超时后重试、迟到成功/失败与新读取的竞争、同步/异步错误、定时器清理,以及会话删除、flush 和服务端钩子的有界 MCP 等待。挂起与迟到竞态使用可控 Promise 和模拟时钟验证;真实 smoke 验证正常宿主链路,未在真实宿主中注入 MCP 挂起。Node 22 的 MockTimers 实验性警告和 OpenTUI 安装引擎警告未隐藏。
18
+
19
+ 验证产物:`/private/var/folders/rw/bmx6c8hd737brl55m0_ff_b80000gn/T/opencode/token-usage-smoke-Syw70N`。
20
+
21
+ ## 2026-10-09 · Last Request 与上下文上限解耦
22
+
23
+ 环境:macOS、Node.js v22.23.2、npm 10.9.8,插件包 0.4.6、SDK 2.0.24,真实宿主为 PATH 中的 OpenCode 2.0.25。
24
+
25
+ `requestDetails` 独立提取所选 assistant 的五类 token、总量和消息时间,`contextUsage` 单独校验模型上限。Last Request 的分类占比以请求总量为分母;上限缺失、为零、负值或非有限值时,实测请求仍可显示。时间依次取同一条消息有效的 `streamed`、`completed`、`created`,缺失时隐藏;来源估算的 `capturedAt` 仅显示在 Context Breakdown。
26
+
27
+ | 检查 | 结果 |
28
+ | --- | --- |
29
+ | `npm run typecheck` | 通过 |
30
+ | `npm test` | 116 项通过,新增 3 项请求明细、时间与边界回归测试,并扩展控制器生命周期断言 |
31
+ | `npm run build` | 通过 |
32
+ | `npm run test:smoke` | 完整通过,打包/隔离安装 + 真实 OpenCode 2.0.25 + 本地模拟提供商 |
33
+
34
+ 自动化测试覆盖有效/无效上限切换、请求分类与缓存率、时间优先级及无效日期、压缩与回退边界、子会话本地请求、刷新失败保留快照、跨会话清理,以及来源 RPC 缓慢/失败/返回另一轮请求的估算时间。
35
+
36
+ 真实终端验证新增上限为 0 的活动模型:Context Window 显示不可用,Last Request 在紧凑/详细模式下仍显示 `Cache Read 1.0K`/`1,000 (78.7%)` 与 `Cache Rate 83.3%`,切回有效上限后恢复 Context。另核对两个标题各自显示实测 assistant 的流式结束时间和来源估算采样时间,并验证空请求不显示时间。完整 smoke 同时通过 100/48 列布局、子代理全树汇总、TPS/TTFT、定价、真实分叉及已完成压缩与消息排序检查。启动时按既有版本策略提示 2.0.25 未验证,随后实际完成全部检查。
37
+
38
+ 验证产物:`/private/var/folders/rw/bmx6c8hd737brl55m0_ff_b80000gn/T/opencode/token-usage-smoke-12xAKR`,其中 `08-unknown-context-limit.txt` 保存未知上限下的详细弹窗。
39
+
40
+ ## 2026-10-07 · npm 发布可见性校验“假失败”修复
41
+
42
+ 环境:macOS、Node.js v22.23.2、npm 10.9.8,本地包版本为 0.4.5、SDK 为 2.0.24。本轮仅修改发布校验、工作流、测试与文档,未修改价格快照或插件运行时代码,未升级版本、移动标签、提交、推送、重跑 GitHub 工作流或实际发布 npm。
43
+
44
+ 只读诊断确认已有真实自动发布链路:
45
+
46
+ - [价格更新运行 37555311596](https://github.com/chenlongapps/opencode-token-usage/actions/runs/37555311596) 由 `schedule` 触发,验证、patch 提交/原子推送、GitHub Release 和 dispatch 均成功。
47
+ - [发布运行 37555363814](https://github.com/chenlongapps/opencode-token-usage/actions/runs/37555363814) 固定在 `v0.4.5`(`66e0584e1f125f2e5012b081014e42d7d8d57d19`)。`npm publish` 成功,生成 provenance,并在 UTC 01:06:28 提示包仍在处理、可能需几分钟才能可用;失败的是 UTC 01:08:21 的 registry 可见性校验。旧实现只查询 12 次、间隔 10 秒,约 2 分钟即超时。
48
+ - 当前 registry 查询已返回目标版本与 `latest` 均为 `0.4.5`,`gitHead` 匹配上述提交,`dist.shasum` 为 `75f8490aa51a974a5d76942a4e1ffa2d9d13fd3c`,与真实发布日志一致。因此不能把该工作流的失败结论当作 npm 未收到版本,更不能为此另升 patch。
49
+
50
+ 修复将按次数重试改为默认 10 分钟总时限,使用单调时钟计算剩余预算;每次版本/`latest` 请求最多 30 秒,且不得超过剩余预算,等待同样按剩余时间截短。`NPM_VERIFY_TIMEOUT_MS` 与 `NPM_VERIFY_INTERVAL_MS` 可配置并校验合法性。日志显示轮询进度、当前 `latest` 与具体查询错误;最终超时仍返回非零退出码,在 Actions 摘要中保留最后原因及原标签重试提示,不用 `continue-on-error` 掩盖故障。发布校验步骤上限为 11 分钟,整个任务仍为 20 分钟;已发布版本跳过发布与 `latest` 核验,原有禁止倒退 `latest`、registry 故障不等于版本不存在等保护保持不变。
51
+
52
+ | 检查 | 结果 |
53
+ | --- | --- |
54
+ | `npm run typecheck` | 通过 |
55
+ | `node --test test/release.test.mjs` | 发布专项 23 项通过 |
56
+ | `npm test` | 113 项通过,比上轮净增 10 项发布校验回归测试 |
57
+ | `npm run build` | 通过 |
58
+ | `npm pack --dry-run` | 通过,执行 `prepack` 构建,包含 37 个发布文件 |
59
+ | `npm publish --dry-run` | 通过,执行类型检查、113 项测试与构建;未写入 registry |
60
+ | GitHub 工作流 | 三个工作流均通过官方 actionlint 1.7.12 检查,下载产物已校验 SHA-256 |
61
+ | npm 只读查询 | `publish-state` 返回 `published=true`;新版 `verify-published` 确认 `0.4.5` 与 `latest` |
62
+
63
+ 回归测试用注入时钟模拟版本在 3 分钟后可见、`latest` 在 4 分钟后更新;同时覆盖 404、HTTP 401/403/429/500/503、网络及无效响应、最后原因保留、单次请求 30 秒上限、请求与等待共用预算、到期后不再发起 `latest` 查询或判定成功,以及真实 AbortSignal 中止挂起请求。CLI 测试预载本地 registry 模拟,不访问真实 npm,验证环境配置、超时非零退出码和失败/成功摘要。
64
+
65
+ 以上真实 GitHub 运行属于修复前的已有标签;新校验逻辑尚未在新的真实发布工作流中验证。本次没有重跑 TUI smoke,之前的宿主验证记录保留。修复提交进入 `main` 后仅供后续新标签使用;旧标签不移动,重跑已可见的 `v0.4.5` 会由原实现跳过发布并核验现有版本。
66
+
3
67
  ## 2026-10-07 · SDK 2.0.24 升级
4
68
 
5
69
  环境:macOS、Node.js v22.23.2、npm 10.9.8,插件包仍为 0.4.4。`@opencode/plugin`、`@opencode/client`、`@opencode/schema` 和 `@opencode/theme` 从 2.0.11 同步精确升级到 2.0.24;锁文件中的全部七个 OpenCode 包均为 2.0.24。OpenTUI 从 0.5.10 同步到 SDK 与 theme 配套的 0.5.14,没有追随独立发布的 0.5.15。未修改价格快照、未升插件包版本、未提交或推送、未实际发布 npm。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenlongapps/opencode-token-usage",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Session-tree token usage, live performance, context window and estimated cost for OpenCode 2",
5
5
  "keywords": [
6
6
  "opencode",