@leo000001/opencode-quota-sidebar 3.0.0 → 3.0.1
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/dist/format.d.ts +4 -0
- package/dist/format.js +14 -7
- package/dist/tui.tsx +131 -35
- package/dist/tui_helpers.d.ts +15 -0
- package/dist/tui_helpers.js +141 -0
- package/package.json +1 -1
package/dist/format.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export declare function renderSidebarUsageLines(usage: UsageSummary, config: Quo
|
|
|
27
27
|
showCost?: boolean;
|
|
28
28
|
}): string[];
|
|
29
29
|
export declare function renderSidebarQuotaLines(quotas: QuotaSnapshot[], config: QuotaSidebarConfig): string[];
|
|
30
|
+
export declare function renderSidebarQuotaLineGroups(quotas: QuotaSnapshot[], config: QuotaSidebarConfig): {
|
|
31
|
+
quota: QuotaSnapshot;
|
|
32
|
+
lines: string[];
|
|
33
|
+
}[];
|
|
30
34
|
export declare function renderMarkdownReport(period: string, usage: UsageSummary, quotas: QuotaSnapshot[], options?: {
|
|
31
35
|
showCost?: boolean;
|
|
32
36
|
}): string;
|
package/dist/format.js
CHANGED
|
@@ -599,6 +599,9 @@ export function renderSidebarUsageLines(usage, config, options) {
|
|
|
599
599
|
}).map((line) => fitLine(line, width));
|
|
600
600
|
}
|
|
601
601
|
export function renderSidebarQuotaLines(quotas, config) {
|
|
602
|
+
return renderSidebarQuotaLineGroups(quotas, config).flatMap((group) => group.lines);
|
|
603
|
+
}
|
|
604
|
+
export function renderSidebarQuotaLineGroups(quotas, config) {
|
|
602
605
|
const width = Math.max(8, Math.floor(config.sidebar.width || 36));
|
|
603
606
|
const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
|
|
604
607
|
const labelWidth = visibleQuotas.reduce((max, item) => {
|
|
@@ -606,14 +609,18 @@ export function renderSidebarQuotaLines(quotas, config) {
|
|
|
606
609
|
return Math.max(max, stringCellWidth(label));
|
|
607
610
|
}, 0);
|
|
608
611
|
return visibleQuotas
|
|
609
|
-
.
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
612
|
+
.map((item) => ({
|
|
613
|
+
quota: item,
|
|
614
|
+
lines: compactQuotaWide(item, labelWidth, {
|
|
615
|
+
width,
|
|
616
|
+
wrapLines: config.sidebar.wrapQuotaLines,
|
|
617
|
+
forceWrapped: false,
|
|
618
|
+
compactDetails: true,
|
|
619
|
+
})
|
|
620
|
+
.filter((line) => Boolean(line))
|
|
621
|
+
.map((line) => fitLine(line, width)),
|
|
614
622
|
}))
|
|
615
|
-
.filter((
|
|
616
|
-
.map((line) => fitLine(line, width));
|
|
623
|
+
.filter((group) => group.lines.length > 0);
|
|
617
624
|
}
|
|
618
625
|
/**
|
|
619
626
|
* Multi-window quota format for sidebar.
|
package/dist/tui.tsx
CHANGED
|
@@ -6,11 +6,15 @@ import type {
|
|
|
6
6
|
} from '@opencode-ai/plugin/tui'
|
|
7
7
|
import { createMemo, createSignal, For, onCleanup, Show } from 'solid-js'
|
|
8
8
|
|
|
9
|
+
import { fitLine, renderSidebarUsageLines } from './format.js'
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
fallbackQuotaGroupsFromTitle,
|
|
12
|
+
quotaGroupsAreCollapsible,
|
|
13
|
+
quotaGroupsSummary,
|
|
14
|
+
quotaGroupsUseBullets,
|
|
15
|
+
renderSidebarQuotaGroups,
|
|
16
|
+
type SidebarQuotaGroup,
|
|
17
|
+
} from './tui_helpers.js'
|
|
14
18
|
import {
|
|
15
19
|
loadConfig,
|
|
16
20
|
loadState,
|
|
@@ -31,7 +35,7 @@ type SidebarPanelData = {
|
|
|
31
35
|
enabled: boolean
|
|
32
36
|
width: number
|
|
33
37
|
usageLines: string[]
|
|
34
|
-
|
|
38
|
+
quotaGroups: SidebarQuotaGroup[]
|
|
35
39
|
compactTitle?: string
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -96,7 +100,7 @@ async function loadSidebarPanel(
|
|
|
96
100
|
enabled,
|
|
97
101
|
width,
|
|
98
102
|
usageLines: [],
|
|
99
|
-
|
|
103
|
+
quotaGroups: [],
|
|
100
104
|
compactTitle: session?.lastAppliedTitle,
|
|
101
105
|
}
|
|
102
106
|
}
|
|
@@ -104,7 +108,7 @@ async function loadSidebarPanel(
|
|
|
104
108
|
const usageLines = usage
|
|
105
109
|
? renderSidebarUsageLines(usage, panelConfig(config))
|
|
106
110
|
: []
|
|
107
|
-
const
|
|
111
|
+
const quotaGroups = renderSidebarQuotaGroups(
|
|
108
112
|
session?.sidebarPanel?.quotas || [],
|
|
109
113
|
panelConfig(config),
|
|
110
114
|
)
|
|
@@ -113,7 +117,7 @@ async function loadSidebarPanel(
|
|
|
113
117
|
enabled,
|
|
114
118
|
width,
|
|
115
119
|
usageLines,
|
|
116
|
-
|
|
120
|
+
quotaGroups,
|
|
117
121
|
compactTitle,
|
|
118
122
|
}
|
|
119
123
|
}
|
|
@@ -193,20 +197,83 @@ function useSidebarPanelData(api: TuiPluginApi, sessionID: () => string) {
|
|
|
193
197
|
return panel
|
|
194
198
|
}
|
|
195
199
|
|
|
196
|
-
function
|
|
197
|
-
|
|
200
|
+
function SectionHeading(props: {
|
|
201
|
+
api: TuiPluginApi
|
|
202
|
+
value: string
|
|
203
|
+
collapsible?: boolean
|
|
204
|
+
open?: boolean
|
|
205
|
+
summary?: string
|
|
206
|
+
onToggle?: () => void
|
|
207
|
+
}) {
|
|
208
|
+
const clickable = () => props.collapsible === true && props.onToggle
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<box
|
|
212
|
+
flexDirection="row"
|
|
213
|
+
gap={1}
|
|
214
|
+
onMouseDown={() => {
|
|
215
|
+
if (!clickable()) return
|
|
216
|
+
props.onToggle?.()
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<Show when={props.collapsible}>
|
|
220
|
+
<text fg={props.api.theme.current.text}>{props.open ? '▼' : '▶'}</text>
|
|
221
|
+
</Show>
|
|
222
|
+
<text fg={props.api.theme.current.text}>
|
|
223
|
+
<b>{props.value}</b>
|
|
224
|
+
<Show when={props.summary}>
|
|
225
|
+
<span style={{ fg: props.api.theme.current.textMuted }}>
|
|
226
|
+
{' '}
|
|
227
|
+
{props.summary}
|
|
228
|
+
</span>
|
|
229
|
+
</Show>
|
|
230
|
+
</text>
|
|
231
|
+
</box>
|
|
232
|
+
)
|
|
198
233
|
}
|
|
199
234
|
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
235
|
+
function quotaToneColor(api: TuiPluginApi, tone: SidebarQuotaGroup['tone']) {
|
|
236
|
+
const theme = api.theme.current
|
|
237
|
+
if (tone === 'success') return theme.success
|
|
238
|
+
if (tone === 'warning') return theme.warning
|
|
239
|
+
if (tone === 'error') return theme.error
|
|
240
|
+
return theme.textMuted
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function QuotaGroupBlock(props: {
|
|
244
|
+
api: TuiPluginApi
|
|
245
|
+
group: SidebarQuotaGroup
|
|
246
|
+
bullet: boolean
|
|
247
|
+
}) {
|
|
248
|
+
const content = (
|
|
249
|
+
<box gap={0}>
|
|
250
|
+
<text>
|
|
251
|
+
<span style={{ fg: props.api.theme.current.text }}>
|
|
252
|
+
{props.group.shortLabel}
|
|
253
|
+
</span>
|
|
254
|
+
<Show when={props.group.detail}>
|
|
255
|
+
<span style={{ fg: props.api.theme.current.textMuted }}>
|
|
256
|
+
{' '}
|
|
257
|
+
{props.group.detail}
|
|
258
|
+
</span>
|
|
259
|
+
</Show>
|
|
260
|
+
</text>
|
|
261
|
+
<For each={props.group.continuationLines}>
|
|
262
|
+
{(line) => <text fg={props.api.theme.current.textMuted}>{line}</text>}
|
|
263
|
+
</For>
|
|
264
|
+
</box>
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<Show when={props.bullet} fallback={content}>
|
|
269
|
+
<box flexDirection="row" gap={1}>
|
|
270
|
+
<text flexShrink={0} fg={quotaToneColor(props.api, props.group.tone)}>
|
|
271
|
+
•
|
|
272
|
+
</text>
|
|
273
|
+
{content}
|
|
274
|
+
</box>
|
|
275
|
+
</Show>
|
|
276
|
+
)
|
|
210
277
|
}
|
|
211
278
|
|
|
212
279
|
function fallbackUsageCostLineFromTitle(title: string, width: number) {
|
|
@@ -220,6 +287,7 @@ function fallbackUsageCostLineFromTitle(title: string, width: number) {
|
|
|
220
287
|
|
|
221
288
|
function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
|
|
222
289
|
const panel = useSidebarPanelData(props.api, () => props.sessionID)
|
|
290
|
+
const [quotaOpen, setQuotaOpen] = createSignal(true)
|
|
223
291
|
const width = createMemo(
|
|
224
292
|
() => panel()?.width || DEFAULT_WIDTH - SECTION_INDENT,
|
|
225
293
|
)
|
|
@@ -234,22 +302,32 @@ function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
234
302
|
const costLine = fallbackUsageCostLineFromTitle(compactTitle(), width())
|
|
235
303
|
return costLine ? [...liveLines, costLine] : liveLines
|
|
236
304
|
})
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
return
|
|
305
|
+
const quotaGroups = createMemo(() => {
|
|
306
|
+
const liveGroups = panel()?.quotaGroups || []
|
|
307
|
+
if (liveGroups.length > 0) return liveGroups
|
|
308
|
+
return fallbackQuotaGroupsFromTitle(compactTitle(), width())
|
|
241
309
|
})
|
|
242
310
|
const hasUsage = createMemo(() => usageLines().length > 0)
|
|
243
|
-
const hasQuota = createMemo(() =>
|
|
311
|
+
const hasQuota = createMemo(() => quotaGroups().length > 0)
|
|
312
|
+
const quotaBullets = createMemo(() => quotaGroupsUseBullets(quotaGroups()))
|
|
313
|
+
const quotaCollapsible = createMemo(() =>
|
|
314
|
+
quotaGroupsAreCollapsible(quotaGroups()),
|
|
315
|
+
)
|
|
316
|
+
const quotaSummary = createMemo(() => {
|
|
317
|
+
if (!quotaCollapsible() || quotaOpen()) return undefined
|
|
318
|
+
return quotaGroupsSummary(quotaGroups())
|
|
319
|
+
})
|
|
244
320
|
|
|
245
321
|
return (
|
|
246
322
|
<box gap={0}>
|
|
247
323
|
<Show when={hasUsage()}>
|
|
248
324
|
<box gap={0}>
|
|
249
|
-
{
|
|
325
|
+
<SectionHeading api={props.api} value="Usage" />
|
|
250
326
|
<box gap={0}>
|
|
251
327
|
<For each={usageLines()}>
|
|
252
|
-
{(line) =>
|
|
328
|
+
{(line) => (
|
|
329
|
+
<text fg={props.api.theme.current.textMuted}>{line}</text>
|
|
330
|
+
)}
|
|
253
331
|
</For>
|
|
254
332
|
</box>
|
|
255
333
|
</box>
|
|
@@ -257,12 +335,27 @@ function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
257
335
|
|
|
258
336
|
<Show when={hasQuota()}>
|
|
259
337
|
<box paddingTop={hasUsage() ? 1 : 0} gap={0}>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
338
|
+
<SectionHeading
|
|
339
|
+
api={props.api}
|
|
340
|
+
value="Quota"
|
|
341
|
+
collapsible={quotaCollapsible()}
|
|
342
|
+
open={quotaOpen()}
|
|
343
|
+
summary={quotaSummary()}
|
|
344
|
+
onToggle={() => setQuotaOpen((value) => !value)}
|
|
345
|
+
/>
|
|
346
|
+
<Show when={!quotaCollapsible() || quotaOpen()}>
|
|
347
|
+
<box gap={0}>
|
|
348
|
+
<For each={quotaGroups()}>
|
|
349
|
+
{(group) => (
|
|
350
|
+
<QuotaGroupBlock
|
|
351
|
+
api={props.api}
|
|
352
|
+
group={group}
|
|
353
|
+
bullet={quotaBullets()}
|
|
354
|
+
/>
|
|
355
|
+
)}
|
|
356
|
+
</For>
|
|
357
|
+
</box>
|
|
358
|
+
</Show>
|
|
266
359
|
</box>
|
|
267
360
|
</Show>
|
|
268
361
|
</box>
|
|
@@ -292,10 +385,13 @@ function SidebarTitleView(props: {
|
|
|
292
385
|
|
|
293
386
|
return (
|
|
294
387
|
<box gap={0} paddingRight={1}>
|
|
295
|
-
{sectionHeading(props.api, 'TITLE')}
|
|
296
388
|
<box gap={0}>
|
|
297
389
|
<For each={titleLines()}>
|
|
298
|
-
{(line) =>
|
|
390
|
+
{(line) => (
|
|
391
|
+
<text fg={props.api.theme.current.text}>
|
|
392
|
+
<b>{line}</b>
|
|
393
|
+
</text>
|
|
394
|
+
)}
|
|
299
395
|
</For>
|
|
300
396
|
<Show when={shareLine()}>
|
|
301
397
|
<text fg={props.api.theme.current.textMuted}>{shareLine()}</text>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { QuotaSidebarConfig, QuotaSnapshot } from './types.js';
|
|
2
|
+
export type SidebarQuotaTone = 'success' | 'warning' | 'error' | 'muted';
|
|
3
|
+
export type SidebarQuotaGroup = {
|
|
4
|
+
providerID: string;
|
|
5
|
+
status: QuotaSnapshot['status'];
|
|
6
|
+
tone: SidebarQuotaTone;
|
|
7
|
+
shortLabel: string;
|
|
8
|
+
detail: string;
|
|
9
|
+
continuationLines: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function renderSidebarQuotaGroups(quotas: QuotaSnapshot[], config: QuotaSidebarConfig): SidebarQuotaGroup[];
|
|
12
|
+
export declare function fallbackQuotaGroupsFromTitle(title: string, width: number): SidebarQuotaGroup[];
|
|
13
|
+
export declare function quotaGroupsUseBullets(groups: SidebarQuotaGroup[]): boolean;
|
|
14
|
+
export declare function quotaGroupsAreCollapsible(groups: SidebarQuotaGroup[]): boolean;
|
|
15
|
+
export declare function quotaGroupsSummary(groups: SidebarQuotaGroup[]): string | undefined;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { fitLine, renderSidebarQuotaLineGroups } from './format.js';
|
|
2
|
+
import { collapseQuotaSnapshots } from './quota_render.js';
|
|
3
|
+
const VISIBLE_QUOTA_STATUSES = new Set([
|
|
4
|
+
'ok',
|
|
5
|
+
'error',
|
|
6
|
+
'unsupported',
|
|
7
|
+
'unavailable',
|
|
8
|
+
]);
|
|
9
|
+
function parseQuotaLineParts(lines) {
|
|
10
|
+
const firstLine = lines[0]?.trimStart() || '';
|
|
11
|
+
const match = /^(\S+)(?:\s+(.*))?$/.exec(firstLine);
|
|
12
|
+
const shortLabel = match?.[1] || firstLine || 'Quota';
|
|
13
|
+
const detail = match?.[2] || '';
|
|
14
|
+
const continuationLines = lines
|
|
15
|
+
.slice(1)
|
|
16
|
+
.map((line) => line.trimEnd())
|
|
17
|
+
.filter((line) => Boolean(line.trim()));
|
|
18
|
+
return {
|
|
19
|
+
shortLabel,
|
|
20
|
+
detail,
|
|
21
|
+
continuationLines,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function quotaPercents(quota) {
|
|
25
|
+
const values = [];
|
|
26
|
+
if (quota.remainingPercent !== undefined &&
|
|
27
|
+
Number.isFinite(quota.remainingPercent)) {
|
|
28
|
+
values.push(quota.remainingPercent);
|
|
29
|
+
}
|
|
30
|
+
for (const window of quota.windows || []) {
|
|
31
|
+
if (window.remainingPercent !== undefined &&
|
|
32
|
+
Number.isFinite(window.remainingPercent)) {
|
|
33
|
+
values.push(window.remainingPercent);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
function quotaTone(quota) {
|
|
39
|
+
if (quota.status === 'error')
|
|
40
|
+
return 'error';
|
|
41
|
+
if (quota.status === 'unsupported' || quota.status === 'unavailable') {
|
|
42
|
+
return 'muted';
|
|
43
|
+
}
|
|
44
|
+
if (quota.status !== 'ok')
|
|
45
|
+
return 'muted';
|
|
46
|
+
const percents = quotaPercents(quota);
|
|
47
|
+
if (percents.length === 0) {
|
|
48
|
+
if (quota.balance && Number.isFinite(quota.balance.amount)) {
|
|
49
|
+
if (quota.balance.amount < 0)
|
|
50
|
+
return 'error';
|
|
51
|
+
return 'muted';
|
|
52
|
+
}
|
|
53
|
+
return 'muted';
|
|
54
|
+
}
|
|
55
|
+
const remaining = Math.min(...percents);
|
|
56
|
+
if (remaining <= 5)
|
|
57
|
+
return 'error';
|
|
58
|
+
if (remaining <= 20)
|
|
59
|
+
return 'warning';
|
|
60
|
+
return 'success';
|
|
61
|
+
}
|
|
62
|
+
function fallbackQuotaTone(detail) {
|
|
63
|
+
const safe = detail.trim();
|
|
64
|
+
if (!safe)
|
|
65
|
+
return 'muted';
|
|
66
|
+
if (/\b(?:unsupported|unavailable)\b/i.test(safe))
|
|
67
|
+
return 'muted';
|
|
68
|
+
if (/\berror\b/i.test(safe) || /^\?$/.test(safe))
|
|
69
|
+
return 'error';
|
|
70
|
+
if (/\bB-/.test(safe))
|
|
71
|
+
return 'error';
|
|
72
|
+
const percents = [...safe.matchAll(/\b(?:\d+[hdw]|[DWM]|S7d)(\d{1,3})\b/gi)]
|
|
73
|
+
.map((match) => Number(match[1]))
|
|
74
|
+
.filter((value) => Number.isFinite(value));
|
|
75
|
+
if (percents.length === 0)
|
|
76
|
+
return 'muted';
|
|
77
|
+
const remaining = Math.min(...percents);
|
|
78
|
+
if (remaining <= 5)
|
|
79
|
+
return 'error';
|
|
80
|
+
if (remaining <= 20)
|
|
81
|
+
return 'warning';
|
|
82
|
+
return 'success';
|
|
83
|
+
}
|
|
84
|
+
export function renderSidebarQuotaGroups(quotas, config) {
|
|
85
|
+
const visibleQuotaCount = collapseQuotaSnapshots(quotas).filter((quota) => VISIBLE_QUOTA_STATUSES.has(quota.status)).length;
|
|
86
|
+
const renderConfig = visibleQuotaCount > 1
|
|
87
|
+
? {
|
|
88
|
+
...config,
|
|
89
|
+
sidebar: {
|
|
90
|
+
...config.sidebar,
|
|
91
|
+
width: Math.max(8, config.sidebar.width - 2),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
: config;
|
|
95
|
+
return renderSidebarQuotaLineGroups(quotas, renderConfig).map((group) => {
|
|
96
|
+
const parsed = parseQuotaLineParts(group.lines);
|
|
97
|
+
return {
|
|
98
|
+
providerID: group.quota.providerID,
|
|
99
|
+
status: group.quota.status,
|
|
100
|
+
tone: quotaTone(group.quota),
|
|
101
|
+
shortLabel: parsed.shortLabel,
|
|
102
|
+
detail: parsed.detail,
|
|
103
|
+
continuationLines: parsed.continuationLines,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export function fallbackQuotaGroupsFromTitle(title, width) {
|
|
108
|
+
const parts = (title || '')
|
|
109
|
+
.split(' | ')
|
|
110
|
+
.map((part) => part.trim())
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
const quotaParts = parts
|
|
113
|
+
.slice(1)
|
|
114
|
+
.filter((part) => !/^Cd\d/.test(part) && !/^Est\b/.test(part));
|
|
115
|
+
if (quotaParts.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
const contentWidth = quotaParts.length > 1 ? Math.max(1, width - 2) : width;
|
|
118
|
+
return quotaParts.map((part, index) => {
|
|
119
|
+
const line = fitLine(part, contentWidth);
|
|
120
|
+
const parsed = parseQuotaLineParts([line]);
|
|
121
|
+
return {
|
|
122
|
+
providerID: `fallback:${index}`,
|
|
123
|
+
status: 'ok',
|
|
124
|
+
tone: fallbackQuotaTone(parsed.detail),
|
|
125
|
+
shortLabel: parsed.shortLabel,
|
|
126
|
+
detail: parsed.detail,
|
|
127
|
+
continuationLines: parsed.continuationLines,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function quotaGroupsUseBullets(groups) {
|
|
132
|
+
return groups.length > 1;
|
|
133
|
+
}
|
|
134
|
+
export function quotaGroupsAreCollapsible(groups) {
|
|
135
|
+
return groups.length > 2;
|
|
136
|
+
}
|
|
137
|
+
export function quotaGroupsSummary(groups) {
|
|
138
|
+
if (groups.length === 0)
|
|
139
|
+
return undefined;
|
|
140
|
+
return `(${groups.length})`;
|
|
141
|
+
}
|
package/package.json
CHANGED