@leo000001/opencode-quota-sidebar 4.0.5 → 4.0.11

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.
@@ -1,8 +1,9 @@
1
- import type { QuotaSidebarConfig, QuotaSnapshot, SidebarPanelState } from './types.js';
2
- export type SidebarQuotaTone = 'success' | 'warning' | 'error' | 'muted';
1
+ import type { QuotaSidebarConfig, QuotaSnapshot, SidebarPanelState } from "./types.js";
2
+ import type { UsageSummary } from "./usage.js";
3
+ export type SidebarQuotaTone = "success" | "warning" | "error" | "muted";
3
4
  export type SidebarQuotaGroup = {
4
5
  providerID: string;
5
- status: QuotaSnapshot['status'];
6
+ status: QuotaSnapshot["status"];
6
7
  tone: SidebarQuotaTone;
7
8
  shortLabel: string;
8
9
  detail: string;
@@ -10,6 +11,7 @@ export type SidebarQuotaGroup = {
10
11
  };
11
12
  export declare function renderSidebarQuotaGroups(quotas: QuotaSnapshot[], config: QuotaSidebarConfig): SidebarQuotaGroup[];
12
13
  export declare function sidebarPanelQuotaSnapshots(panel?: SidebarPanelState): QuotaSnapshot[];
14
+ export declare function mergeLiveAndPersistedPanelUsage(liveUsage: UsageSummary | undefined, persistedUsage: UsageSummary | undefined): UsageSummary | undefined;
13
15
  export declare function fallbackQuotaGroupsFromTitle(title: string, width: number): SidebarQuotaGroup[];
14
16
  export declare function quotaGroupsUseBullets(groups: SidebarQuotaGroup[]): boolean;
15
17
  export declare function quotaGroupsAreCollapsible(groups: SidebarQuotaGroup[]): boolean;
@@ -1,17 +1,17 @@
1
- import { fitLine, renderSidebarQuotaLineGroups } from './format.js';
2
- import { collapseQuotaSnapshots } from './quota_render.js';
3
- import { isSupportedQuotaSnapshot, isSupportedQuotaTitleLabel, } from './supported_quota.js';
1
+ import { fitLine, renderSidebarQuotaLineGroups } from "./format.js";
2
+ import { collapseQuotaSnapshots } from "./quota_render.js";
3
+ import { isSupportedQuotaSnapshot, isSupportedQuotaTitleLabel, } from "./supported_quota.js";
4
4
  const VISIBLE_QUOTA_STATUSES = new Set([
5
- 'ok',
6
- 'error',
7
- 'unsupported',
8
- 'unavailable',
5
+ "ok",
6
+ "error",
7
+ "unsupported",
8
+ "unavailable",
9
9
  ]);
10
10
  function parseQuotaLineParts(lines) {
11
- const firstLine = lines[0]?.trimStart() || '';
11
+ const firstLine = lines[0]?.trimStart() || "";
12
12
  const match = /^(\S+)(?:\s+(.*))?$/.exec(firstLine);
13
- const shortLabel = match?.[1] || firstLine || 'Quota';
14
- const detail = match?.[2] || '';
13
+ const shortLabel = match?.[1] || firstLine || "Quota";
14
+ const detail = match?.[2] || "";
15
15
  const continuationLines = lines
16
16
  .slice(1)
17
17
  .map((line) => line.trimEnd())
@@ -37,52 +37,52 @@ function quotaPercents(quota) {
37
37
  return values;
38
38
  }
39
39
  function quotaTone(quota) {
40
- if (quota.status === 'error')
41
- return 'error';
42
- if (quota.status === 'unsupported' || quota.status === 'unavailable') {
43
- return 'muted';
40
+ if (quota.status === "error")
41
+ return "error";
42
+ if (quota.status === "unsupported" || quota.status === "unavailable") {
43
+ return "muted";
44
44
  }
45
- if (quota.status !== 'ok')
46
- return 'muted';
45
+ if (quota.status !== "ok")
46
+ return "muted";
47
47
  const percents = quotaPercents(quota);
48
48
  if (percents.length === 0) {
49
49
  if (quota.balance && Number.isFinite(quota.balance.amount)) {
50
50
  if (quota.balance.amount < 0)
51
- return 'error';
52
- return 'muted';
51
+ return "error";
52
+ return "muted";
53
53
  }
54
- return 'muted';
54
+ return "muted";
55
55
  }
56
56
  const remaining = Math.min(...percents);
57
57
  if (remaining <= 5)
58
- return 'error';
58
+ return "error";
59
59
  if (remaining <= 20)
60
- return 'warning';
61
- return 'success';
60
+ return "warning";
61
+ return "success";
62
62
  }
63
63
  function fallbackQuotaTone(detail) {
64
64
  const safe = detail.trim();
65
65
  if (!safe)
66
- return 'muted';
66
+ return "muted";
67
67
  if (/\b(?:unsupported|unavailable)\b/i.test(safe))
68
- return 'muted';
68
+ return "muted";
69
69
  if (/\berror\b/i.test(safe) || /^\?$/.test(safe))
70
- return 'error';
70
+ return "error";
71
71
  if (/\bB-/.test(safe))
72
- return 'error';
72
+ return "error";
73
73
  const percents = [
74
74
  ...safe.matchAll(/\b(?:\d+[hdw]|[DWM]|S7d|O7d|OA7d|Co7d|Sk5h|SkW)(\d{1,3})\b/gi),
75
75
  ]
76
76
  .map((match) => Number(match[1]))
77
77
  .filter((value) => Number.isFinite(value));
78
78
  if (percents.length === 0)
79
- return 'muted';
79
+ return "muted";
80
80
  const remaining = Math.min(...percents);
81
81
  if (remaining <= 5)
82
- return 'error';
82
+ return "error";
83
83
  if (remaining <= 20)
84
- return 'warning';
85
- return 'success';
84
+ return "warning";
85
+ return "success";
86
86
  }
87
87
  export function renderSidebarQuotaGroups(quotas, config) {
88
88
  const visibleQuotaCount = collapseQuotaSnapshots(quotas).filter((quota) => VISIBLE_QUOTA_STATUSES.has(quota.status)).length;
@@ -110,14 +110,42 @@ export function renderSidebarQuotaGroups(quotas, config) {
110
110
  export function sidebarPanelQuotaSnapshots(panel) {
111
111
  return (panel?.panelQuotas || panel?.quotas || []).filter((quota) => isSupportedQuotaSnapshot(quota));
112
112
  }
113
+ export function mergeLiveAndPersistedPanelUsage(liveUsage, persistedUsage) {
114
+ if (!liveUsage)
115
+ return persistedUsage;
116
+ if (!persistedUsage)
117
+ return liveUsage;
118
+ const preferLive = liveUsage.assistantMessages > 0 &&
119
+ (liveUsage.assistantMessages > persistedUsage.assistantMessages ||
120
+ (liveUsage.assistantMessages === persistedUsage.assistantMessages &&
121
+ liveUsage.total >= persistedUsage.total));
122
+ if (!preferLive)
123
+ return persistedUsage;
124
+ const sameAggregateSurface = liveUsage.assistantMessages === persistedUsage.assistantMessages &&
125
+ liveUsage.input === persistedUsage.input &&
126
+ liveUsage.output === persistedUsage.output &&
127
+ liveUsage.cacheRead === persistedUsage.cacheRead &&
128
+ liveUsage.cacheWrite === persistedUsage.cacheWrite &&
129
+ liveUsage.total === persistedUsage.total;
130
+ if (!sameAggregateSurface ||
131
+ liveUsage.apiCost > 0 ||
132
+ persistedUsage.apiCost <= 0) {
133
+ return liveUsage;
134
+ }
135
+ return {
136
+ ...liveUsage,
137
+ apiCost: persistedUsage.apiCost,
138
+ };
139
+ }
113
140
  export function fallbackQuotaGroupsFromTitle(title, width) {
114
- const parts = (title || '')
115
- .split(' | ')
141
+ // Legacy compatibility: old sessions may only have compact title fragments.
142
+ const parts = (title || "")
143
+ .split(" | ")
116
144
  .map((part) => part.trim())
117
145
  .filter(Boolean);
118
146
  const quotaParts = parts
119
147
  .slice(1)
120
- .filter((part) => !/^Cd\d/.test(part) && !/^Est\b/.test(part));
148
+ .filter((part) => !/^Cd\d/.test(part) && !/^API\b/.test(part) && !/^Est\b/.test(part));
121
149
  if (quotaParts.length === 0)
122
150
  return [];
123
151
  const contentWidth = quotaParts.length > 1 ? Math.max(1, width - 2) : width;
@@ -129,7 +157,7 @@ export function fallbackQuotaGroupsFromTitle(title, width) {
129
157
  continue;
130
158
  groups.push({
131
159
  providerID: `fallback:${index}`,
132
- status: 'ok',
160
+ status: "ok",
133
161
  tone: fallbackQuotaTone(parsed.detail),
134
162
  shortLabel: parsed.shortLabel,
135
163
  detail: parsed.detail,
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type QuotaStatus = 'ok' | 'unavailable' | 'unsupported' | 'error';
2
- export type SidebarTitleMode = 'auto' | 'multiline' | 'compact';
1
+ export type QuotaStatus = "ok" | "unavailable" | "unsupported" | "error";
2
+ export type SidebarTitleMode = "auto" | "multiline" | "compact";
3
3
  export type QuotaWindow = {
4
4
  label: string;
5
5
  /** Set false when this window line should not render a trailing percentage. */
@@ -12,7 +12,7 @@ export type QuotaWindow = {
12
12
  usedPercent?: number;
13
13
  resetAt?: string;
14
14
  };
15
- export type QuotaStaleReasonKind = 'timeout' | 'network' | 'http_5xx' | 'http_transient' | 'invalid_response' | 'unknown';
15
+ export type QuotaStaleReasonKind = "timeout" | "network" | "http_5xx" | "http_transient" | "invalid_response" | "unknown";
16
16
  export type QuotaStaleMeta = {
17
17
  staleAt: number;
18
18
  staleReason: string;
@@ -52,7 +52,7 @@ export type SessionTitleState = {
52
52
  baseTitle: string;
53
53
  lastAppliedTitle?: string;
54
54
  };
55
- export type CacheCoverageMode = 'none' | 'read-only' | 'read-write';
55
+ export type CacheCoverageMode = "none" | "read-only" | "read-write";
56
56
  export type CacheUsageBucket = {
57
57
  input: number;
58
58
  cacheRead: number;
@@ -95,6 +95,10 @@ export type CachedProviderUsage = {
95
95
  export type CachedSessionUsage = {
96
96
  /** Billing aggregation cache version for cost/apiCost refresh migrations. */
97
97
  billingVersion?: number;
98
+ /** Pricing fingerprint for the exact model/rate set used by this session. */
99
+ pricingFingerprint?: string;
100
+ /** Unique assistant-message provider/model pairs used to build the fingerprint. */
101
+ pricingKeys?: string[];
98
102
  input: number;
99
103
  output: number;
100
104
  reasoning: number;
@@ -172,12 +176,6 @@ export type QuotaSidebarConfig = {
172
176
  * sidebar plugin render the rich panel layout.
173
177
  */
174
178
  titleMode?: SidebarTitleMode;
175
- /**
176
- * Legacy switch retained for compatibility.
177
- * TUI keeps a compact multiline sidebar layout; Desktop keeps a compact
178
- * single-line layout.
179
- */
180
- multilineTitle?: boolean;
181
179
  showCost: boolean;
182
180
  showQuota: boolean;
183
181
  /** When true, wrap long quota lines and indent continuations. */
package/dist/usage.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import type { AssistantMessage, Message } from '@opencode-ai/sdk';
2
- import type { CacheCoverageMetrics, CacheCoverageMode, CacheUsageBuckets, CachedSessionUsage, IncrementalCursor, RecentProviderEvent } from './types.js';
1
+ import type { AssistantMessage, Message } from "@opencode-ai/sdk";
2
+ import type { CacheCoverageMetrics, CacheCoverageMode, CacheUsageBuckets, CachedSessionUsage, IncrementalCursor, RecentProviderEvent } from "./types.js";
3
3
  /**
4
4
  * Billing cache version — bump this whenever the persisted `CachedSessionUsage`
5
5
  * shape changes in a way that requires recomputation (e.g. new aggregate
6
6
  * fields). This is distinct from the plugin *state* version managed by the
7
7
  * persistence layer; billing version only governs usage-cache staleness.
8
8
  */
9
- export declare const USAGE_BILLING_CACHE_VERSION = 9;
9
+ export declare const USAGE_BILLING_CACHE_VERSION = 10;
10
10
  export type ProviderUsage = {
11
11
  providerID: string;
12
12
  input: number;
@@ -43,8 +43,8 @@ export type UsageOptions = {
43
43
  /** Cache-behavior classifier for the message model/provider. */
44
44
  classifyCacheMode?: (message: AssistantMessage) => CacheCoverageMode;
45
45
  };
46
- export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
47
- export declare function getProviderCacheCoverageMetrics(usage: Pick<ProviderUsage, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
46
+ export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, "input" | "cacheRead" | "cacheWrite" | "assistantMessages" | "cacheBuckets">): CacheCoverageMetrics;
47
+ export declare function getProviderCacheCoverageMetrics(usage: Pick<ProviderUsage, "input" | "cacheRead" | "cacheWrite" | "assistantMessages" | "cacheBuckets">): CacheCoverageMetrics;
48
48
  export declare function emptyUsageSummary(): UsageSummary;
49
49
  export declare function accumulateMessagesInCompletedRange(target: UsageSummary, entries: Array<{
50
50
  info: Message;
@@ -84,5 +84,8 @@ export declare function summarizeMessagesIncremental(entries: Array<{
84
84
  export declare function mergeUsage(target: UsageSummary, source: UsageSummary, options?: {
85
85
  includeCost?: boolean;
86
86
  }): UsageSummary;
87
- export declare function toCachedSessionUsage(summary: UsageSummary): CachedSessionUsage;
87
+ export declare function toCachedSessionUsage(summary: UsageSummary, options?: {
88
+ pricingFingerprint?: string;
89
+ pricingKeys?: string[];
90
+ }): CachedSessionUsage;
88
91
  export declare function fromCachedSessionUsage(cached: CachedSessionUsage, sessionCount?: number): UsageSummary;
package/dist/usage.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * fields). This is distinct from the plugin *state* version managed by the
5
5
  * persistence layer; billing version only governs usage-cache staleness.
6
6
  */
7
- export const USAGE_BILLING_CACHE_VERSION = 9;
7
+ export const USAGE_BILLING_CACHE_VERSION = 10;
8
8
  const MAX_RECENT_PROVIDER_EVENTS = 100;
9
9
  function emptyCacheUsageBucket() {
10
10
  return {
@@ -150,7 +150,7 @@ function emptyProviderUsage(providerID) {
150
150
  };
151
151
  }
152
152
  function isAssistant(message) {
153
- return message.role === 'assistant';
153
+ return message.role === "assistant";
154
154
  }
155
155
  function tokenTotal(message) {
156
156
  return (message.tokens.input +
@@ -166,8 +166,8 @@ function mergedOutput(message) {
166
166
  function mergeRecentProviderEvents(target, source) {
167
167
  const merged = [...(target || []), ...(source || [])]
168
168
  .filter((item) => !!item &&
169
- typeof item.providerID === 'string' &&
170
- typeof item.completedAt === 'number' &&
169
+ typeof item.providerID === "string" &&
170
+ typeof item.completedAt === "number" &&
171
171
  Number.isFinite(item.completedAt))
172
172
  .sort((left, right) => right.completedAt - left.completedAt);
173
173
  return merged.length > MAX_RECENT_PROVIDER_EVENTS
@@ -177,7 +177,7 @@ function mergeRecentProviderEvents(target, source) {
177
177
  function addMessageUsage(target, message, options) {
178
178
  const total = tokenTotal(message);
179
179
  const output = mergedOutput(message);
180
- const cost = typeof message.cost === 'number' && Number.isFinite(message.cost)
180
+ const cost = typeof message.cost === "number" && Number.isFinite(message.cost)
181
181
  ? message.cost
182
182
  : 0;
183
183
  const apiCostRaw = options?.calcApiCost ? options.calcApiCost(message) : 0;
@@ -207,23 +207,25 @@ function addMessageUsage(target, message, options) {
207
207
  provider.apiCost += apiCost;
208
208
  provider.assistantMessages += 1;
209
209
  target.providers[message.providerID] = provider;
210
- const cacheMode = options?.classifyCacheMode?.(message) || 'none';
211
- if (cacheMode === 'read-only') {
210
+ const cacheMode = options?.classifyCacheMode?.(message) || "none";
211
+ if (cacheMode === "read-only") {
212
212
  const buckets = (target.cacheBuckets ||= emptyCacheUsageBuckets());
213
213
  addMessageCacheUsage(buckets.readOnly, message);
214
- const providerBuckets = (provider.cacheBuckets ||= emptyCacheUsageBuckets());
214
+ const providerBuckets = (provider.cacheBuckets ||=
215
+ emptyCacheUsageBuckets());
215
216
  addMessageCacheUsage(providerBuckets.readOnly, message);
216
217
  }
217
- else if (cacheMode === 'read-write') {
218
+ else if (cacheMode === "read-write") {
218
219
  const buckets = (target.cacheBuckets ||= emptyCacheUsageBuckets());
219
220
  addMessageCacheUsage(buckets.readWrite, message);
220
- const providerBuckets = (provider.cacheBuckets ||= emptyCacheUsageBuckets());
221
+ const providerBuckets = (provider.cacheBuckets ||=
222
+ emptyCacheUsageBuckets());
221
223
  addMessageCacheUsage(providerBuckets.readWrite, message);
222
224
  }
223
225
  }
224
226
  function completedTimeOf(message) {
225
227
  const completed = message.time.completed;
226
- if (typeof completed !== 'number')
228
+ if (typeof completed !== "number")
227
229
  return undefined;
228
230
  if (!Number.isFinite(completed))
229
231
  return undefined;
@@ -302,11 +304,11 @@ export function accumulateMessagesAcrossCompletedRanges(summaries, entries, rang
302
304
  return touched;
303
305
  }
304
306
  export function mergeCursorFromEntries(cursor, entries) {
305
- let bestTime = typeof cursor?.lastMessageTime === 'number' &&
307
+ let bestTime = typeof cursor?.lastMessageTime === "number" &&
306
308
  Number.isFinite(cursor.lastMessageTime)
307
309
  ? cursor.lastMessageTime
308
310
  : Number.NEGATIVE_INFINITY;
309
- let bestID = cursor?.lastMessageId || '';
311
+ let bestID = cursor?.lastMessageId || "";
310
312
  const idsAtBestTime = new Set(Array.isArray(cursor?.lastMessageIdsAtTime)
311
313
  ? cursor.lastMessageIdsAtTime
312
314
  : cursor?.lastMessageId && Number.isFinite(bestTime)
@@ -350,7 +352,7 @@ export function summarizeMessagesIncremental(entries, existingUsage, cursor, for
350
352
  // If no cursor or force rescan, do full scan
351
353
  if (forceRescan ||
352
354
  !cursor?.lastMessageId ||
353
- typeof cursor.lastMessageTime !== 'number' ||
355
+ typeof cursor.lastMessageTime !== "number" ||
354
356
  !Number.isFinite(cursor.lastMessageTime) ||
355
357
  !existingUsage) {
356
358
  const usage = summarizeMessages(entries, 0, 1, options);
@@ -382,7 +384,7 @@ export function summarizeMessagesIncremental(entries, existingUsage, cursor, for
382
384
  const msg = entry.info;
383
385
  if (!isAssistant(msg))
384
386
  continue;
385
- if (typeof msg.time.completed !== 'number')
387
+ if (typeof msg.time.completed !== "number")
386
388
  continue;
387
389
  if (!Number.isFinite(msg.time.completed))
388
390
  continue;
@@ -407,7 +409,7 @@ export function summarizeMessagesIncremental(entries, existingUsage, cursor, for
407
409
  }
408
410
  const isAfterCursor = (message) => {
409
411
  const completed = message.time.completed;
410
- if (typeof completed !== 'number' || !Number.isFinite(completed))
412
+ if (typeof completed !== "number" || !Number.isFinite(completed))
411
413
  return false;
412
414
  if (completed > cursorTime)
413
415
  return true;
@@ -430,7 +432,7 @@ export function summarizeMessagesIncremental(entries, existingUsage, cursor, for
430
432
  const msg = entry.info;
431
433
  if (!isAssistant(msg))
432
434
  continue;
433
- if (typeof msg.time.completed !== 'number')
435
+ if (typeof msg.time.completed !== "number")
434
436
  continue;
435
437
  if (!Number.isFinite(msg.time.completed))
436
438
  continue;
@@ -487,7 +489,7 @@ function collectCompletedAssistantIdsAt(entries, completedTime) {
487
489
  const msg = entry.info;
488
490
  if (!isAssistant(msg))
489
491
  continue;
490
- if (typeof msg.time.completed !== 'number')
492
+ if (typeof msg.time.completed !== "number")
491
493
  continue;
492
494
  if (!Number.isFinite(msg.time.completed))
493
495
  continue;
@@ -500,12 +502,12 @@ function collectCompletedAssistantIdsAt(entries, completedTime) {
500
502
  function findLastCompletedAssistant(entries) {
501
503
  let best;
502
504
  let bestTime = -Infinity;
503
- let bestID = '';
505
+ let bestID = "";
504
506
  for (const entry of entries) {
505
507
  const msg = entry.info;
506
508
  if (!isAssistant(msg))
507
509
  continue;
508
- if (typeof msg.time.completed !== 'number')
510
+ if (typeof msg.time.completed !== "number")
509
511
  continue;
510
512
  if (!Number.isFinite(msg.time.completed))
511
513
  continue;
@@ -561,7 +563,7 @@ export function mergeUsage(target, source, options) {
561
563
  }
562
564
  return target;
563
565
  }
564
- export function toCachedSessionUsage(summary) {
566
+ export function toCachedSessionUsage(summary, options) {
565
567
  const providers = Object.entries(summary.providers).reduce((acc, [providerID, provider]) => {
566
568
  acc[providerID] = {
567
569
  input: provider.input,
@@ -580,6 +582,10 @@ export function toCachedSessionUsage(summary) {
580
582
  }, {});
581
583
  return {
582
584
  billingVersion: USAGE_BILLING_CACHE_VERSION,
585
+ pricingFingerprint: options?.pricingFingerprint,
586
+ pricingKeys: options?.pricingKeys
587
+ ? Array.from(new Set(options.pricingKeys)).sort()
588
+ : undefined,
583
589
  input: summary.input,
584
590
  output: summary.output,
585
591
  // Always 0 after merge into output; kept for serialization shape.
@@ -1,9 +1,9 @@
1
- import type { PluginInput } from '@opencode-ai/plugin';
2
- import { type HistoryPeriod } from './period.js';
3
- import { type UsageSummary } from './usage.js';
4
- export type { HistoryUsageRow, HistoryUsageResult } from './history_usage.js';
5
- import type { HistoryUsageResult } from './history_usage.js';
6
- import type { QuotaSidebarConfig, QuotaSidebarState } from './types.js';
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ import { type HistoryPeriod } from "./period.js";
3
+ import { type UsageSummary } from "./usage.js";
4
+ export type { HistoryUsageRow, HistoryUsageResult } from "./history_usage.js";
5
+ import type { HistoryUsageResult } from "./history_usage.js";
6
+ import type { QuotaSidebarConfig, QuotaSidebarState } from "./types.js";
7
7
  type DescendantsResolver = {
8
8
  listDescendantSessionIDs: (sessionID: string, opts: {
9
9
  maxDepth: number;
@@ -20,8 +20,9 @@ export declare function createUsageService(deps: {
20
20
  state: QuotaSidebarState;
21
21
  config: QuotaSidebarConfig;
22
22
  statePath: string;
23
- client: PluginInput['client'];
23
+ client: PluginInput["client"];
24
24
  directory: string;
25
+ worktree?: string;
25
26
  persistence: Persistence;
26
27
  descendantsResolver: DescendantsResolver;
27
28
  }): {