@iasiv5/dsh-quota-watch 0.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/src/client.mjs ADDED
@@ -0,0 +1,369 @@
1
+ import { CLIENT_POLL_INTERVAL_MS, CLIENT_ROUTES } from './shared.mjs'
2
+
3
+ export const name = 'quota-watch-client'
4
+ export const inject = []
5
+
6
+ const CARD_SELECTOR = '[data-dsh-quota-watch-card]'
7
+ const COLLAPSE_KEY = 'dsh-quota-watch:collapsed'
8
+ const FETCH_TIMEOUT_MS = 15_000
9
+
10
+ const COPY = {
11
+ zh: {
12
+ title: '套餐监控',
13
+ refresh: '刷新',
14
+ collapse: '收起',
15
+ expand: '展开',
16
+ loading: '读取中',
17
+ updated: '更新',
18
+ notConfigured: '未配置',
19
+ noPlan: '暂无套餐窗口',
20
+ failed: '查询失败',
21
+ stale: '数据可能已过期',
22
+ requestFailed: '刷新失败,显示上次成功数据',
23
+ available: '可用额度',
24
+ used: '本期已用',
25
+ balanceUnavailable: '余额暂不可用',
26
+ reset: '重置',
27
+ windows: { '5h': '5 小时', week: '每周', month: '每月' },
28
+ },
29
+ en: {
30
+ title: 'Quota Watch',
31
+ refresh: 'Refresh',
32
+ collapse: 'Collapse',
33
+ expand: 'Expand',
34
+ loading: 'Loading',
35
+ updated: 'Updated',
36
+ notConfigured: 'Not configured',
37
+ noPlan: 'No plan windows',
38
+ failed: 'Query failed',
39
+ stale: 'Data may be stale',
40
+ requestFailed: 'Refresh failed; showing last successful data',
41
+ available: 'Available credits',
42
+ used: 'Used this cycle',
43
+ balanceUnavailable: 'Balance unavailable',
44
+ reset: 'Resets',
45
+ windows: { '5h': '5 hours', week: 'Weekly', month: 'Monthly' },
46
+ },
47
+ }
48
+
49
+ const STYLE_TEXT = `
50
+ [data-dsh-quota-watch-card] { box-sizing: border-box; width: 100%; margin: 0 0 8px; color: var(--dsw-alias-label-primary, inherit); font: inherit; }
51
+ [data-dsh-quota-watch-card] *, [data-dsh-quota-watch-card] *::before, [data-dsh-quota-watch-card] *::after { box-sizing: border-box; }
52
+ .dqw-card { border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); padding: 8px; display: flex; flex-direction: column; gap: 7px; }
53
+ .dqw-head { display: flex; align-items: center; gap: 4px; min-width: 0; }
54
+ .dqw-title { flex: 1; min-width: 0; margin: 0; font-size: 12px; font-weight: 600; line-height: 18px; }
55
+ .dqw-button { border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; font: inherit; font-size: 11px; line-height: 18px; padding: 1px 4px; }
56
+ .dqw-button:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.16)); }
57
+ .dqw-button:disabled { cursor: wait; opacity: .6; }
58
+ .dqw-meta { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
59
+ .dqw-provider { border-top: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 4px; }
60
+ .dqw-provider-title { margin: 0; font-size: 11px; font-weight: 600; line-height: 16px; }
61
+ .dqw-row { display: flex; flex-direction: column; gap: 2px; }
62
+ .dqw-row-head { display: flex; justify-content: space-between; gap: 5px; font-size: 10px; line-height: 14px; }
63
+ .dqw-bar { display: block; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
64
+ .dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
65
+ .dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
66
+ .dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
67
+ .dqw-credit { margin: 0; font-size: 10px; line-height: 14px; font-variant-numeric: tabular-nums; }
68
+ `
69
+
70
+ function localeFor(win) {
71
+ return String(win?.navigator?.language ?? 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
72
+ }
73
+
74
+ function formatNumber(value, locale) {
75
+ if (typeof value !== 'number' || !Number.isFinite(value)) return ''
76
+ return new Intl.NumberFormat(locale === 'zh' ? 'zh-CN' : 'en-US', { maximumFractionDigits: 1 }).format(value)
77
+ }
78
+
79
+ function formatPercent(value, locale) {
80
+ if (typeof value !== 'number' || !Number.isFinite(value)) return ''
81
+ const clamped = Math.max(0, Math.min(100, value))
82
+ const rounded = clamped >= 10 ? Math.round(clamped) : Number(clamped.toFixed(1))
83
+ return `${formatNumber(rounded, locale)}%`
84
+ }
85
+
86
+ function formatTime(value, locale) {
87
+ if (typeof value !== 'string' || value === '') return ''
88
+ const date = new Date(value)
89
+ if (Number.isNaN(date.getTime())) return ''
90
+ return new Intl.DateTimeFormat(locale === 'zh' ? 'zh-CN' : 'en-US', {
91
+ dateStyle: 'short',
92
+ timeStyle: 'short',
93
+ }).format(date)
94
+ }
95
+
96
+ function text(doc, tag, className, value) {
97
+ const node = doc.createElement(tag)
98
+ if (className) node.className = className
99
+ node.textContent = String(value ?? '')
100
+ return node
101
+ }
102
+
103
+ function progress(doc, label, percent, locale) {
104
+ const row = doc.createElement('div')
105
+ row.className = 'dqw-row'
106
+ const head = doc.createElement('div')
107
+ head.className = 'dqw-row-head'
108
+ head.append(text(doc, 'span', '', label), text(doc, 'span', '', formatPercent(percent, locale)))
109
+ const bar = doc.createElement('span')
110
+ bar.className = 'dqw-bar'
111
+ bar.setAttribute('role', 'progressbar')
112
+ bar.setAttribute('aria-valuemin', '0')
113
+ bar.setAttribute('aria-valuemax', '100')
114
+ bar.setAttribute('aria-valuenow', String(Math.max(0, Math.min(100, percent))))
115
+ const fill = doc.createElement('span')
116
+ fill.className = 'dqw-bar-fill'
117
+ fill.style.width = `${Math.max(0, Math.min(100, percent))}%`
118
+ bar.append(fill)
119
+ row.append(head, bar)
120
+ return row
121
+ }
122
+
123
+ function providerSection(doc, key, heading) {
124
+ const section = doc.createElement('section')
125
+ section.className = 'dqw-provider'
126
+ section.dataset.provider = key
127
+ section.append(text(doc, 'h4', 'dqw-provider-title', heading))
128
+ const content = doc.createElement('div')
129
+ content.dataset.role = 'content'
130
+ content.className = 'dqw-content'
131
+ section.append(content)
132
+ return section
133
+ }
134
+
135
+ function appendStaleError(doc, content, provider, copy) {
136
+ if (provider.status === 'stale') content.append(text(doc, 'p', 'dqw-error', `${copy.stale}: ${provider.error ?? ''}`))
137
+ else if (provider.status === 'error') content.append(text(doc, 'p', 'dqw-error', provider.error ?? copy.failed))
138
+ }
139
+
140
+ function renderGlm(doc, section, provider, copy, locale) {
141
+ const content = section.querySelector('[data-role="content"]')
142
+ content.replaceChildren()
143
+ appendStaleError(doc, content, provider, copy)
144
+ if (provider.status === 'missing') {
145
+ content.append(text(doc, 'p', 'dqw-muted', copy.notConfigured))
146
+ return
147
+ }
148
+ if (provider.status === 'loading') {
149
+ content.append(text(doc, 'p', 'dqw-muted', copy.loading))
150
+ return
151
+ }
152
+ if (!provider.plan?.windows?.length) {
153
+ if (provider.status !== 'error') content.append(text(doc, 'p', 'dqw-muted', copy.noPlan))
154
+ return
155
+ }
156
+ const planName = provider.plan.planName
157
+ if (planName) content.append(text(doc, 'p', 'dqw-muted', planName))
158
+ for (const window of provider.plan.windows) {
159
+ const label = window.name || copy.windows[window.key] || window.key
160
+ if (typeof window.percent === 'number' && Number.isFinite(window.percent)) {
161
+ content.append(progress(doc, label, window.percent, locale))
162
+ } else {
163
+ content.append(text(doc, 'p', 'dqw-muted', label))
164
+ }
165
+ const reset = formatTime(window.resetsAt, locale)
166
+ if (reset) content.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
167
+ }
168
+ }
169
+
170
+ function renderCopilot(doc, section, provider, copy, locale) {
171
+ const content = section.querySelector('[data-role="content"]')
172
+ content.replaceChildren()
173
+ appendStaleError(doc, content, provider, copy)
174
+ if (provider.status === 'missing') {
175
+ content.append(text(doc, 'p', 'dqw-muted', copy.notConfigured))
176
+ return
177
+ }
178
+ if (provider.status === 'loading') {
179
+ content.append(text(doc, 'p', 'dqw-muted', copy.loading))
180
+ return
181
+ }
182
+ const quota = provider.quota
183
+ if (quota?.planName) content.append(text(doc, 'p', 'dqw-muted', quota.planName))
184
+ if (quota?.balanceAvailable === true) {
185
+ content.append(text(doc, 'p', 'dqw-credit', `${copy.available}: ${formatNumber(quota.remaining, locale)} / ${formatNumber(quota.entitlement, locale)}`))
186
+ if (typeof quota.percentRemaining === 'number') {
187
+ content.append(progress(doc, copy.available, quota.percentRemaining, locale))
188
+ }
189
+ } else {
190
+ content.append(text(doc, 'p', 'dqw-muted', copy.balanceUnavailable))
191
+ }
192
+ if (typeof quota?.creditsUsed === 'number') {
193
+ content.append(text(doc, 'p', 'dqw-muted', `${copy.used}: ${formatNumber(quota.creditsUsed, locale)}`))
194
+ }
195
+ const reset = formatTime(quota?.resetsAt, locale)
196
+ if (reset) content.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
197
+ }
198
+
199
+ function footArea(doc) {
200
+ const sidebar = doc.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]')
201
+ return sidebar?.querySelector('[class*="footArea"]') ?? undefined
202
+ }
203
+
204
+ function readCollapsed(win) {
205
+ try { return win.localStorage.getItem(COLLAPSE_KEY) === '1' } catch { return false }
206
+ }
207
+
208
+ function writeCollapsed(win, value) {
209
+ try { win.localStorage.setItem(COLLAPSE_KEY, value ? '1' : '0') } catch { /* Storage may be unavailable. */ }
210
+ }
211
+
212
+ async function fetchJson(fetchImpl, path, method) {
213
+ const response = await fetchImpl(path, {
214
+ ...(method === 'POST' ? { method: 'POST' } : {}),
215
+ cache: 'no-store',
216
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
217
+ })
218
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
219
+ return response.json()
220
+ }
221
+
222
+ /** Mount a self-healing sidebar card. Dependencies are injectable for DOM tests. */
223
+ export function mountQuotaCard({
224
+ doc = globalThis.document,
225
+ win = globalThis.window,
226
+ fetchImpl = globalThis.fetch,
227
+ pollIntervalMs = CLIENT_POLL_INTERVAL_MS,
228
+ } = {}) {
229
+ if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
230
+ if (doc.querySelector(CARD_SELECTOR)) return () => {}
231
+
232
+ const locale = localeFor(win)
233
+ const copy = COPY[locale]
234
+ const container = doc.createElement('div')
235
+ container.dataset.dshQuotaWatchCard = ''
236
+ container.dataset.dshPlugin = 'quota-watch'
237
+ container.dataset.dshPart = 'sidebar-card'
238
+ const style = text(doc, 'style', '', STYLE_TEXT)
239
+ const card = doc.createElement('section')
240
+ card.className = 'dqw-card'
241
+ card.setAttribute('aria-label', copy.title)
242
+ const header = doc.createElement('header')
243
+ header.className = 'dqw-head'
244
+ header.append(text(doc, 'h3', 'dqw-title', copy.title))
245
+ const refreshButton = text(doc, 'button', 'dqw-button', '↻')
246
+ refreshButton.type = 'button'
247
+ refreshButton.title = copy.refresh
248
+ refreshButton.setAttribute('aria-label', copy.refresh)
249
+ refreshButton.dataset.action = 'refresh'
250
+ const collapseButton = text(doc, 'button', 'dqw-button', '−')
251
+ collapseButton.type = 'button'
252
+ collapseButton.dataset.action = 'collapse'
253
+ const body = doc.createElement('div')
254
+ body.className = 'dqw-body'
255
+ const lastUpdated = text(doc, 'p', 'dqw-meta', copy.loading)
256
+ lastUpdated.dataset.role = 'updated'
257
+ const glmSection = providerSection(doc, 'glm', 'GLM Coding Plan')
258
+ const copilotSection = providerSection(doc, 'copilot', 'GitHub Copilot')
259
+ body.append(lastUpdated, glmSection, copilotSection)
260
+ header.append(refreshButton, collapseButton)
261
+ card.append(header, body)
262
+ container.append(style, card)
263
+ let collapsed = readCollapsed(win)
264
+ let snapshot
265
+ let requestSequence = 0
266
+ let timer
267
+ let disposed = false
268
+ let queuedPlace = false
269
+
270
+ const applyCollapsed = () => {
271
+ body.hidden = collapsed
272
+ collapseButton.textContent = collapsed ? '+' : '−'
273
+ collapseButton.title = collapsed ? copy.expand : copy.collapse
274
+ collapseButton.setAttribute('aria-label', collapseButton.title)
275
+ collapseButton.setAttribute('aria-expanded', String(!collapsed))
276
+ }
277
+
278
+ const render = (next) => {
279
+ snapshot = next
280
+ const glm = next?.providers?.find((provider) => provider.key === 'glm')
281
+ const copilot = next?.providers?.find((provider) => provider.key === 'copilot')
282
+ if (glm) renderGlm(doc, glmSection, glm, copy, locale)
283
+ if (copilot) renderCopilot(doc, copilotSection, copilot, copy, locale)
284
+ const updated = formatTime(next?.updatedAt ? new Date(next.updatedAt).toISOString() : '', locale)
285
+ lastUpdated.textContent = updated ? `${copy.updated}: ${updated}` : copy.loading
286
+ }
287
+
288
+ const showTransportError = () => {
289
+ if (snapshot) lastUpdated.textContent = copy.requestFailed
290
+ else lastUpdated.textContent = copy.failed
291
+ }
292
+
293
+ const poll = async (force) => {
294
+ if (disposed) return
295
+ const sequence = ++requestSequence
296
+ refreshButton.disabled = force === true
297
+ try {
298
+ const next = await fetchJson(fetchImpl, force ? CLIENT_ROUTES.refresh : CLIENT_ROUTES.overview, force ? 'POST' : 'GET')
299
+ if (sequence === requestSequence && !disposed) render(next)
300
+ } catch {
301
+ if (sequence === requestSequence && !disposed) showTransportError()
302
+ } finally {
303
+ if (!disposed && sequence === requestSequence) refreshButton.disabled = false
304
+ }
305
+ }
306
+
307
+ const place = () => {
308
+ const foot = footArea(doc)
309
+ if (!foot) return
310
+ const settings = foot.querySelector('[class*="settingsArea"]')
311
+ if (settings) {
312
+ if (container.parentElement !== foot || container.nextElementSibling !== settings) foot.insertBefore(container, settings)
313
+ } else if (container.parentElement !== foot || foot.lastElementChild !== container) {
314
+ foot.append(container)
315
+ }
316
+ }
317
+
318
+ const schedulePlace = () => {
319
+ if (queuedPlace || disposed) return
320
+ queuedPlace = true
321
+ queueMicrotask(() => {
322
+ queuedPlace = false
323
+ if (!disposed) place()
324
+ })
325
+ }
326
+
327
+ const observer = new win.MutationObserver(schedulePlace)
328
+ observer.observe(doc.body, { childList: true, subtree: true })
329
+ place()
330
+ applyCollapsed()
331
+
332
+ const startPolling = () => {
333
+ if (timer !== undefined || doc.visibilityState === 'hidden' || disposed) return
334
+ void poll(false)
335
+ timer = win.setInterval(() => { void poll(false) }, pollIntervalMs)
336
+ }
337
+ const stopPolling = () => {
338
+ if (timer !== undefined) win.clearInterval(timer)
339
+ timer = undefined
340
+ }
341
+ const onVisibilityChange = () => {
342
+ if (doc.visibilityState === 'hidden') stopPolling()
343
+ else startPolling()
344
+ }
345
+ const onRefresh = () => { void poll(true) }
346
+ const onCollapse = () => {
347
+ collapsed = !collapsed
348
+ writeCollapsed(win, collapsed)
349
+ applyCollapsed()
350
+ }
351
+ refreshButton.addEventListener('click', onRefresh)
352
+ collapseButton.addEventListener('click', onCollapse)
353
+ doc.addEventListener('visibilitychange', onVisibilityChange)
354
+ startPolling()
355
+
356
+ return () => {
357
+ disposed = true
358
+ stopPolling()
359
+ observer.disconnect()
360
+ doc.removeEventListener('visibilitychange', onVisibilityChange)
361
+ refreshButton.removeEventListener('click', onRefresh)
362
+ collapseButton.removeEventListener('click', onCollapse)
363
+ container.remove()
364
+ }
365
+ }
366
+
367
+ export function apply(ctx) {
368
+ ctx.effect(() => mountQuotaCard(), 'dsh-quota-watch: sidebar')
369
+ }
@@ -0,0 +1,163 @@
1
+ const GLM_HOSTS = Object.freeze({
2
+ 'zai-coding-cn': 'open.bigmodel.cn',
3
+ zai: 'api.z.ai',
4
+ 'zai-coding': 'api.z.ai',
5
+ })
6
+
7
+ const GLM_WINDOW_KEYS = Object.freeze({ 3: '5h', 5: 'month', 6: 'week' })
8
+ const COPILOT_USER_URL = 'https://api.github.com/copilot_internal/user'
9
+ const COPILOT_HEADERS = Object.freeze({
10
+ accept: 'application/json',
11
+ 'user-agent': 'GitHubCopilotChat/0.35.0',
12
+ 'editor-version': 'vscode/1.107.0',
13
+ 'editor-plugin-version': 'copilot-chat/0.35.0',
14
+ 'copilot-integration-id': 'vscode-chat',
15
+ })
16
+
17
+ function asRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined
19
+ }
20
+
21
+ function asString(value) {
22
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
23
+ }
24
+
25
+ function asNumber(value) {
26
+ if (typeof value === 'number' && Number.isFinite(value)) return value
27
+ if (typeof value === 'string' && value.trim() !== '') {
28
+ const parsed = Number(value)
29
+ return Number.isFinite(parsed) ? parsed : undefined
30
+ }
31
+ return undefined
32
+ }
33
+
34
+ function clampPercent(value) {
35
+ return Math.max(0, Math.min(100, value))
36
+ }
37
+
38
+ function toIso(value) {
39
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
40
+ const milliseconds = value < 1e12 ? value * 1000 : value
41
+ const date = new Date(milliseconds)
42
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
43
+ }
44
+ const text = asString(value)
45
+ if (text === undefined) return undefined
46
+ if (/^\d+$/.test(text)) return toIso(Number(text))
47
+ const date = new Date(text)
48
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
49
+ }
50
+
51
+ function glmPercent(row, kind) {
52
+ if (kind === 'CREDIT_LIMIT') {
53
+ const current = asNumber(row.currentValue)
54
+ const total = asNumber(row.usage)
55
+ if (current !== undefined && total !== undefined && total > 0) {
56
+ return clampPercent((current / total) * 100)
57
+ }
58
+ }
59
+ const reported = asNumber(row.percentage)
60
+ return reported === undefined ? undefined : clampPercent(reported)
61
+ }
62
+
63
+ /** Build a fixed-origin GLM Coding Plan quota request. */
64
+ export function buildGlmProbe({ provider, apiKey } = {}) {
65
+ const host = GLM_HOSTS[provider]
66
+ const key = asString(apiKey)
67
+ if (host === undefined || key === undefined) return undefined
68
+ return {
69
+ url: `https://${host}/api/monitor/usage/quota/limit`,
70
+ headers: {
71
+ authorization: key,
72
+ 'accept-language': 'en-US,en',
73
+ },
74
+ }
75
+ }
76
+
77
+ /** Normalize GLM's provider-defined quota windows without relying on row order. */
78
+ export function parseGlmPlan(status, body) {
79
+ if (status !== 200) return undefined
80
+ const root = asRecord(body)
81
+ if (root?.success !== true) return undefined
82
+ const data = asRecord(root.data)
83
+ if (!Array.isArray(data?.limits)) return undefined
84
+
85
+ const windows = []
86
+ for (const candidate of data.limits) {
87
+ const row = asRecord(candidate)
88
+ if (row === undefined) continue
89
+ const kind = asString(row.type)
90
+ if (kind === 'TIME_LIMIT') continue
91
+ if (kind !== 'TOKENS_LIMIT' && kind !== 'CREDIT_LIMIT') continue
92
+ const unit = asNumber(row.unit)
93
+ const key = GLM_WINDOW_KEYS[unit]
94
+ if (key === undefined) continue
95
+ const percent = glmPercent(row, kind)
96
+ const resetsAt = toIso(row.nextResetTime)
97
+ windows.push({
98
+ key,
99
+ ...(percent !== undefined ? { percent } : {}),
100
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
101
+ })
102
+ }
103
+
104
+ if (windows.length === 0) return undefined
105
+ const planName = asString(data.level)
106
+ return { ...(planName !== undefined ? { planName } : {}), windows }
107
+ }
108
+
109
+ /** Build the Copilot user-quota request using the raw GitHub OAuth grant. */
110
+ export function buildCopilotProbe({ githubOAuthToken } = {}) {
111
+ const token = asString(githubOAuthToken)
112
+ if (token === undefined) return undefined
113
+ return {
114
+ url: COPILOT_USER_URL,
115
+ headers: {
116
+ authorization: `Bearer ${token}`,
117
+ ...COPILOT_HEADERS,
118
+ },
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Normalize the Copilot user's premium-interaction snapshot.
124
+ * Remaining credits are never derived from entitlement minus credits_used.
125
+ */
126
+ export function parseCopilotUser(status, body) {
127
+ if (status !== 200) return undefined
128
+ const root = asRecord(body)
129
+ const snapshots = asRecord(root?.quota_snapshots)
130
+ const snapshot = asRecord(snapshots?.premium_interactions)
131
+ if (snapshot === undefined) return undefined
132
+
133
+ const tokenBasedBilling = typeof snapshot.token_based_billing === 'boolean'
134
+ ? snapshot.token_based_billing
135
+ : typeof root?.token_based_billing === 'boolean'
136
+ ? root.token_based_billing
137
+ : undefined
138
+ const entitlement = asNumber(snapshot.entitlement)
139
+ const reportedRemaining = asNumber(snapshot.remaining)
140
+ const balanceAvailable = tokenBasedBilling === true
141
+ && snapshot.unlimited === false
142
+ && entitlement !== undefined
143
+ && entitlement > 0
144
+ && reportedRemaining !== undefined
145
+ && reportedRemaining >= 0
146
+ const percentReported = asNumber(snapshot.percent_remaining)
147
+ const creditsUsed = tokenBasedBilling === true ? asNumber(snapshot.credits_used) : undefined
148
+ const resetsAt = toIso(root?.quota_reset_date_utc) ?? toIso(root?.quota_reset_date)
149
+ const planName = asString(root?.copilot_plan)
150
+
151
+ return {
152
+ ...(planName !== undefined ? { planName } : {}),
153
+ ...(tokenBasedBilling !== undefined ? { tokenBasedBilling } : {}),
154
+ balanceAvailable,
155
+ ...(balanceAvailable ? {
156
+ remaining: reportedRemaining,
157
+ entitlement,
158
+ ...(percentReported !== undefined ? { percentRemaining: clampPercent(percentReported) } : {}),
159
+ } : {}),
160
+ ...(creditsUsed !== undefined && creditsUsed >= 0 ? { creditsUsed } : {}),
161
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
162
+ }
163
+ }