@cortexkit/opencode-openai-auth 1.0.5 → 1.1.0

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.
Files changed (102) hide show
  1. package/dist/bin.js +9 -8
  2. package/dist/{pool-lifecycle-C_6YRmsh.d.ts → codex-request-BXDXToyt.d.ts} +158 -27
  3. package/dist/index-0yh85bwb.js +69 -0
  4. package/dist/{index-826s1yxa.js → index-4c8yw54k.js} +2 -2
  5. package/dist/{index-y4gxyxqd.js → index-7c6sxtd6.js} +2 -2
  6. package/dist/index-7eb3dw37.js +90 -0
  7. package/dist/index-8re8rprk.js +2 -0
  8. package/dist/{index-66asrje2.js → index-csky2xpg.js} +1 -1
  9. package/dist/index-edbdscy7.js +65 -0
  10. package/dist/{index-pg305s90.js → index-f2fmb596.js} +1 -1
  11. package/dist/{index-fsxbncpw.js → index-j8v2xwq6.js} +1 -1
  12. package/dist/{index-fsky1rsh.js → index-k6cydkgt.js} +2 -2
  13. package/dist/{index-116q849r.js → index-m5qmvr6c.js} +2 -2
  14. package/dist/index-msx2m78k.js +8 -0
  15. package/dist/{index-nx2qh047.js → index-zpm95mt5.js} +1 -1
  16. package/dist/index.d.ts +15 -7
  17. package/dist/index.js +1 -1
  18. package/dist/{map-m6f5a_IA.d.ts → map-Buq-GW7l.d.ts} +1 -1
  19. package/dist/{protocol-DctqemRI.d.ts → model-YRg7Uowo.d.ts} +67 -25
  20. package/dist/{port-file-ZPlxXB4G.d.ts → port-file-CxEpgSHY.d.ts} +1 -1
  21. package/dist/protocol-CDV1Pc6h.d.ts +29 -0
  22. package/dist/rpc/port-file.d.ts +1 -1
  23. package/dist/rpc/port-file.js +1 -1
  24. package/dist/rpc/protocol.d.ts +1 -1
  25. package/dist/rpc/protocol.js +1 -1
  26. package/dist/rpc/rpc-client.d.ts +3 -3
  27. package/dist/rpc/rpc-client.js +1 -1
  28. package/dist/rpc/rpc-dir.js +1 -1
  29. package/dist/{sidebar-state-z_M7Trnd.d.ts → sidebar-state-DjTWUbmf.d.ts} +15 -9
  30. package/dist/sidebar-state.d.ts +1 -1
  31. package/dist/sidebar-state.js +1 -1
  32. package/dist/{token-bzhkgc29.js → token-rz4t3ay6.js} +1 -1
  33. package/dist/token-util-tpy2c7h6.js +1 -0
  34. package/dist/tui-preferences.js +1 -1
  35. package/dist/tui.d.ts +20 -2
  36. package/dist/v2/server.d.ts +73 -1
  37. package/dist/v2/server.js +3 -3
  38. package/package.json +2 -2
  39. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +13 -8
  40. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +354 -0
  41. package/src/tui-compiled/raw/shared/134b008900698fe0-backoff.js +92 -0
  42. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +393 -0
  43. package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  44. package/src/tui-compiled/raw/shared/2dce6e9613993329-quota-manager.js +473 -0
  45. package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +566 -0
  46. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  47. package/src/tui-compiled/raw/shared/4593a4d71edf69fd-tombstone.js +25 -0
  48. package/src/tui-compiled/raw/shared/4d05c2d7a531aece-oauth.js +500 -0
  49. package/src/tui-compiled/raw/shared/6371b24a0573d79e-any-signal.js +42 -0
  50. package/src/tui-compiled/raw/shared/7c140680fc81be92-interlock.js +29 -0
  51. package/src/tui-compiled/raw/shared/85512cc5263ba259-provider.js +87 -0
  52. package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +192 -0
  53. package/src/tui-compiled/raw/shared/b9588ad4725c1d10-enrollment.js +806 -0
  54. package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  55. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +0 -1
  56. package/src/tui-compiled/raw/shared/ca0fccaba1d87af4-errors.js +8 -0
  57. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +152 -0
  58. package/src/tui-compiled/raw/shared/d9d2892a4c371301-host-slot.js +72 -0
  59. package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +1470 -0
  60. package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  61. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +24 -0
  62. package/src/tui-compiled/raw/sidebar-state.js +41 -14
  63. package/src/tui-compiled/raw/tui/command-dialogs.tsx +3 -34
  64. package/src/tui-compiled/raw/tui/section-text.js +21 -0
  65. package/src/tui-compiled/raw/tui/v2-dialog.js +137 -0
  66. package/src/tui-compiled/raw/tui/v2.tsx +133 -0
  67. package/src/tui-compiled/raw/tui.tsx +146 -40
  68. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +13 -8
  69. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +354 -0
  70. package/src/tui-compiled/runtime/shared/134b008900698fe0-backoff.js +92 -0
  71. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +393 -0
  72. package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  73. package/src/tui-compiled/runtime/shared/2dce6e9613993329-quota-manager.js +473 -0
  74. package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +566 -0
  75. package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  76. package/src/tui-compiled/runtime/shared/4593a4d71edf69fd-tombstone.js +25 -0
  77. package/src/tui-compiled/runtime/shared/4d05c2d7a531aece-oauth.js +500 -0
  78. package/src/tui-compiled/runtime/shared/6371b24a0573d79e-any-signal.js +42 -0
  79. package/src/tui-compiled/runtime/shared/7c140680fc81be92-interlock.js +29 -0
  80. package/src/tui-compiled/runtime/shared/85512cc5263ba259-provider.js +87 -0
  81. package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +192 -0
  82. package/src/tui-compiled/runtime/shared/b9588ad4725c1d10-enrollment.js +806 -0
  83. package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  84. package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +0 -1
  85. package/src/tui-compiled/runtime/shared/ca0fccaba1d87af4-errors.js +8 -0
  86. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +152 -0
  87. package/src/tui-compiled/runtime/shared/d9d2892a4c371301-host-slot.js +72 -0
  88. package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +1470 -0
  89. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  90. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +24 -0
  91. package/src/tui-compiled/runtime/sidebar-state.js +41 -14
  92. package/src/tui-compiled/runtime/tui/command-dialogs.js +3 -15
  93. package/src/tui-compiled/runtime/tui/section-text.js +21 -0
  94. package/src/tui-compiled/runtime/tui/v2-dialog.js +137 -0
  95. package/src/tui-compiled/runtime/tui/v2.js +95 -0
  96. package/src/tui-compiled/runtime/tui.js +255 -163
  97. package/dist/index-4fnghj9e.js +0 -65
  98. package/dist/index-a69gr76b.js +0 -2
  99. package/dist/index-h263aftt.js +0 -157
  100. package/dist/index-ssj7m2rd.js +0 -8
  101. package/dist/index-ydz3hbhc.js +0 -2
  102. package/dist/token-util-4a2xxnpg.js +0 -1
@@ -3,7 +3,12 @@
3
3
  import { readFileSync } from "node:fs"
4
4
  import { dirname, join } from "node:path"
5
5
  import { fileURLToPath } from "node:url"
6
- import { formatQuota, quotaTextParts } from "./shared/d5a8f1061ee2f114-index.js"
6
+ import {
7
+ formatQuota,
8
+ quotaTextParts,
9
+ quotaWindowParts,
10
+ } from "./shared/d5a8f1061ee2f114-index.js"
11
+ import type { Plugin } from "@opencode/plugin/tui"
7
12
  import type {
8
13
  TuiPlugin,
9
14
  TuiPluginApi,
@@ -38,6 +43,7 @@ import {
38
43
  sidebarQuotaProjection,
39
44
  } from "./sidebar-state.js"
40
45
  import { openCommandDialog } from "./tui/command-dialogs.tsx"
46
+ import { setupTui, type TuiTransportOptions } from "./tui/v2.tsx"
41
47
  import {
42
48
  type AppearancePrefs,
43
49
  computeEffectiveOrder,
@@ -95,7 +101,17 @@ const PLUGIN_VERSION: string = (() => {
95
101
  const SINGLE_BORDER = { type: 'single' } as any
96
102
 
97
103
  type Tone = 'ok' | 'warn' | 'err' | 'muted' | 'accent' | 'text'
98
- type ThemeCurrent = TuiPluginApi['theme']['current']
104
+ type ThemeCurrent = Pick<
105
+ TuiPluginApi['theme']['current'],
106
+ | 'text'
107
+ | 'textMuted'
108
+ | 'accent'
109
+ | 'background'
110
+ | 'borderActive'
111
+ | 'success'
112
+ | 'warning'
113
+ | 'error'
114
+ >
99
115
  type ThemeColor = ThemeCurrent['text']
100
116
 
101
117
  // Tone -> theme token. Theme tokens only — never hardcoded colors. Fallbacks
@@ -254,6 +270,7 @@ export interface QuotaDisplayRow {
254
270
  label: string
255
271
  labelWidth: number
256
272
  text: string
273
+ resetText?: string
257
274
  window: QuotaWindow
258
275
  pacing: QuotaPacing | null
259
276
  }
@@ -292,9 +309,7 @@ export function renderedQuotas(state: {
292
309
  }): ReadonlyArray<AccountQuota | null | undefined> {
293
310
  // In vault mode only the vault accounts are rendered.
294
311
  if (state.vaultAccounts !== undefined)
295
- return state.vaultAccounts
296
- .filter((account) => account.enabled)
297
- .map((account) => account.quota)
312
+ return state.vaultAccounts.map((account) => account.quota)
298
313
  return [
299
314
  state.main?.quota ?? null,
300
315
  ...(state.fallbacks ?? [])
@@ -327,33 +342,50 @@ export function buildQuotaRowsForDisplay(
327
342
  ): QuotaDisplayRow[] {
328
343
  const projection = sidebarQuotaProjection(quota, now)
329
344
  const rows: Array<Omit<QuotaDisplayRow, 'labelWidth'>> =
330
- getPresentQuotaWindows(quota).map((row) => ({
331
- key: row.key,
332
- label: row.label,
333
- window: row.window,
334
- text: formatQuota(
335
- projection && {
336
- ...projection,
337
- budget: undefined,
338
- limits: projection.limits.filter((limit) => limit.label === row.key),
339
- },
340
- { now, form: 'compact', staleAfterMs: Number.POSITIVE_INFINITY },
341
- ),
342
- pacing:
343
- pacingEnabled && row.windowMs !== null
344
- ? computeQuotaPacing(row.window, row.windowMs, now)
345
- : null,
346
- }))
345
+ getPresentQuotaWindows(quota).map((row) => {
346
+ const rowProjection = projection && {
347
+ ...projection,
348
+ budget: undefined,
349
+ limits: projection.limits.filter((limit) => limit.label === row.key),
350
+ }
351
+ const reset = quotaWindowParts(rowProjection, {
352
+ now,
353
+ staleAfterMs: Number.POSITIVE_INFINITY,
354
+ }).windows[0]
355
+ return {
356
+ key: row.key,
357
+ label: row.label,
358
+ window: row.window,
359
+ text: formatQuota(rowProjection, {
360
+ now,
361
+ form: 'compact',
362
+ staleAfterMs: Number.POSITIVE_INFINITY,
363
+ }),
364
+ resetText:
365
+ reset?.resetIn ?? (reset?.resetSinceReading ? 'reset' : undefined),
366
+ pacing:
367
+ pacingEnabled && row.windowMs !== null
368
+ ? computeQuotaPacing(row.window, row.windowMs, now)
369
+ : null,
370
+ }
371
+ })
347
372
  const spendControl = quota?.spendControl
348
373
  if (spendControl) {
374
+ const rowProjection = projection && { ...projection, limits: [] }
375
+ const reset = quotaWindowParts(rowProjection, {
376
+ now,
377
+ staleAfterMs: Number.POSITIVE_INFINITY,
378
+ }).windows[0]
349
379
  rows.push({
350
380
  key: 'spendControl',
351
381
  label: SPEND_CONTROL_LABEL,
352
- text: formatQuota(projection && { ...projection, limits: [] }, {
382
+ text: formatQuota(rowProjection, {
353
383
  now,
354
384
  form: 'compact',
355
385
  staleAfterMs: Number.POSITIVE_INFINITY,
356
386
  }),
387
+ resetText:
388
+ reset?.resetIn ?? (reset?.resetSinceReading ? 'reset' : undefined),
357
389
  window: {
358
390
  usedPercent: spendControl.usedPercent,
359
391
  remainingPercent: spendControl.remainingPercent,
@@ -435,6 +467,7 @@ function QuotaRow(props: {
435
467
  label: string
436
468
  labelWidth: number
437
469
  text: string
470
+ resetText?: string
438
471
  window: { usedPercent: number; resetsAt?: string } | undefined
439
472
  pacing: QuotaPacing | null
440
473
  }) {
@@ -479,6 +512,9 @@ function QuotaRow(props: {
479
512
  {` ${props.text.slice(props.label.length + 1)}`}
480
513
  </text>
481
514
  </box>
515
+ <Show when={props.resetText}>
516
+ <text fg={props.theme.textMuted}>{props.resetText}</text>
517
+ </Show>
482
518
  </box>
483
519
  <Show when={paceLine()}>
484
520
  <box width='100%' flexDirection='row'>
@@ -506,6 +542,8 @@ function AccountBlock(props: {
506
542
  name: string
507
543
  quota: AccountQuota | null
508
544
  killed: boolean
545
+ vaultState?: string
546
+ hostDisabled?: boolean
509
547
  active: boolean
510
548
  pacingEnabled: boolean
511
549
  labelWidth: number
@@ -533,6 +571,12 @@ function AccountBlock(props: {
533
571
  <b>{statusWord()}</b>
534
572
  </text>
535
573
  </box>
574
+ <Show when={props.vaultState}>
575
+ <text fg={props.theme.warning}>{props.vaultState}</text>
576
+ </Show>
577
+ <Show when={props.hostDisabled}>
578
+ <text fg={props.theme.textMuted}>off in /openai</text>
579
+ </Show>
536
580
  <Show
537
581
  when={isQuotaLoaded(props.quota)}
538
582
  fallback={<text fg={props.theme.textMuted}>{'checking\u2026'}</text>}
@@ -549,6 +593,7 @@ function AccountBlock(props: {
549
593
  label={row.label}
550
594
  labelWidth={row.labelWidth}
551
595
  text={row.text}
596
+ resetText={row.resetText}
552
597
  window={row.window}
553
598
  pacing={row.pacing}
554
599
  />
@@ -629,6 +674,7 @@ interface SidebarController {
629
674
  prefs: () => OpenaiAuthTuiPrefs
630
675
  collapsed: () => boolean
631
676
  toggleCollapsed: () => void
677
+ dispose: () => void
632
678
  }
633
679
 
634
680
  // The TUI may unmount and remount sidebar_content when the user switches
@@ -651,14 +697,13 @@ function createSidebarController(
651
697
  let lastPersistedCollapsed: boolean | null = initialPrefs.collapsed
652
698
  let lastApplied = JSON.stringify(initialPrefs)
653
699
 
654
- // The watcher lives for the plugin/process lifetime — it is intentionally
655
- // never disposed. Collapse guard mirrors the race-fix in toggleCollapsed:
656
- // lastPersistedCollapsed is advanced only once our own write lands, so
657
- // watcher echoes of the previous persisted value are rejected by the
658
- // `!==` check and cannot revert a user click.
659
- watchTuiPreferences(() => {
700
+ let disposed = false
701
+ // Remember the last value written to disk, not an unpersisted click. A watcher
702
+ // echo of the previous disk value must not undo a click whose write is pending.
703
+ const unwatch = watchTuiPreferences(() => {
660
704
  void (async () => {
661
705
  const next = resolveOpenaiAuthPrefs(await readTuiPreferencesFile())
706
+ if (disposed) return
662
707
  const serialized = JSON.stringify(next)
663
708
  if (serialized === lastApplied) return
664
709
  lastApplied = serialized
@@ -686,11 +731,27 @@ function createSidebarController(
686
731
  }
687
732
  }
688
733
 
689
- return { prefs, collapsed, toggleCollapsed }
734
+ return {
735
+ prefs,
736
+ collapsed,
737
+ toggleCollapsed,
738
+ dispose() {
739
+ disposed = true
740
+ unwatch()
741
+ },
742
+ }
690
743
  }
691
744
 
692
745
  function QuotaSidebar(props: {
693
- api: TuiPluginApi
746
+ api: {
747
+ theme: { readonly current: ThemeCurrent }
748
+ event: {
749
+ on(
750
+ type: 'session.updated' | 'message.updated',
751
+ listener: () => void,
752
+ ): () => void
753
+ }
754
+ }
694
755
  controller: SidebarController
695
756
  sessionId: string
696
757
  }) {
@@ -699,10 +760,12 @@ function QuotaSidebar(props: {
699
760
  const [state, setState] = createSignal<SidebarState>(DEFAULT_SIDEBAR_STATE)
700
761
  let lastUpdated = 0
701
762
  let debounce: ReturnType<typeof setTimeout> | null = null
763
+ let disposed = false
702
764
 
703
765
  async function refresh() {
766
+ if (disposed) return
704
767
  const next = await readStateFromFile()
705
- if (next.lastUpdated !== lastUpdated) {
768
+ if (!disposed && next.lastUpdated !== lastUpdated) {
706
769
  lastUpdated = next.lastUpdated
707
770
  setState(next)
708
771
  }
@@ -727,9 +790,11 @@ function QuotaSidebar(props: {
727
790
  props.api.event.on('session.updated', scheduleRefresh),
728
791
  props.api.event.on('message.updated', scheduleRefresh),
729
792
  ]
730
- setTimeout(refresh, 300)
793
+ const initialRefresh = setTimeout(refresh, 300)
731
794
 
732
795
  onCleanup(() => {
796
+ disposed = true
797
+ clearTimeout(initialRefresh)
733
798
  if (debounce) clearTimeout(debounce)
734
799
  for (const u of unsubs) u()
735
800
  })
@@ -740,11 +805,10 @@ function QuotaSidebar(props: {
740
805
  // Vault mode: the host uses only the vault's accounts, which the file lists
741
806
  // in `vaultAccounts`; main and the local fallbacks are not shown.
742
807
  const vaultMode = () => state().vaultAccounts !== undefined
743
- const enabledVaultAccounts = () =>
744
- (state().vaultAccounts ?? []).filter((account) => account.enabled)
808
+ const vaultAccounts = () => state().vaultAccounts ?? []
745
809
  const hasData = () =>
746
810
  vaultMode()
747
- ? enabledVaultAccounts().length > 0
811
+ ? vaultAccounts().length > 0
748
812
  : state().main?.quota != null || enabledFallbacks().length > 0
749
813
 
750
814
  const headerLabel = () => {
@@ -788,8 +852,8 @@ function QuotaSidebar(props: {
788
852
  }
789
853
  const killedNames = () =>
790
854
  [
791
- state().main?.killed ? 'main' : '',
792
- ...enabledFallbacks()
855
+ !vaultMode() && state().main?.killed ? 'main' : '',
856
+ ...(vaultMode() ? vaultAccounts() : enabledFallbacks())
793
857
  .filter((f) => f.killed)
794
858
  .map((f) => f.label ?? f.id),
795
859
  ].filter(Boolean)
@@ -887,7 +951,7 @@ function QuotaSidebar(props: {
887
951
  <Show
888
952
  when={!vaultMode()}
889
953
  fallback={
890
- <For each={enabledVaultAccounts()}>
954
+ <For each={vaultAccounts()}>
891
955
  {(account, index) => (
892
956
  <AccountBlock
893
957
  theme={theme()}
@@ -895,6 +959,8 @@ function QuotaSidebar(props: {
895
959
  name={account.label ?? account.id}
896
960
  quota={account.quota}
897
961
  killed={account.killed}
962
+ vaultState={account.vaultState}
963
+ hostDisabled={!account.enabled}
898
964
  active={sessionRouting().activeId === account.id}
899
965
  pacingEnabled={prefs().sections.pacing}
900
966
  labelWidth={quotaLabelWidth()}
@@ -1064,8 +1130,48 @@ const tui: TuiPlugin = async (api) => {
1064
1130
  }
1065
1131
  }
1066
1132
 
1067
- const plugin: TuiPluginModule & { id: string } = {
1133
+ async function setup(
1134
+ context: Plugin.Context,
1135
+ options: TuiTransportOptions = {},
1136
+ ) {
1137
+ if (options.sidebarText !== undefined || options.sidebar)
1138
+ return setupTui(context, options)
1139
+ const root = await readTuiPreferencesFile()
1140
+ const controller = createSidebarController(resolveOpenaiAuthPrefs(root))
1141
+ const api = {
1142
+ theme: {
1143
+ get current() {
1144
+ return context.theme
1145
+ },
1146
+ },
1147
+ event: {
1148
+ on: (type: 'session.updated' | 'message.updated', listener: () => void) =>
1149
+ context.data.on(
1150
+ type === 'session.updated'
1151
+ ? 'session.metadata.updated'
1152
+ : 'session.usage.updated',
1153
+ listener,
1154
+ ),
1155
+ },
1156
+ }
1157
+ return setupTui(context, {
1158
+ ...options,
1159
+ sidebar: {
1160
+ render: (input) => (
1161
+ <QuotaSidebar
1162
+ api={api}
1163
+ controller={controller}
1164
+ sessionId={input.sessionID}
1165
+ />
1166
+ ),
1167
+ dispose: controller.dispose,
1168
+ },
1169
+ })
1170
+ }
1171
+
1172
+ const plugin: TuiPluginModule & { id: string; setup: typeof setup } = {
1068
1173
  id: ID,
1174
+ setup,
1069
1175
  tui,
1070
1176
  }
1071
1177
 
@@ -22,18 +22,23 @@ export const NO_QUOTA_READING = 'no quota reading yet';
22
22
  */
23
23
  export const NO_LIMITS_REPORTED = 'no limits reported';
24
24
  /**
25
- * A length or a time span in its largest whole-ish unit: `45m`, `5h`, `3d`.
26
- * Spans under an hour are minutes, under a day hours, otherwise days, each
27
- * rounded to the nearest unit and never shown as zero.
25
+ * A time span in at most its two largest units: `45m`, `2h29m`, `5h`, `3d23h`,
26
+ * `7d`. Every unit is rounded down, so a reset is never shown later than it is;
27
+ * the one exception is a span under a minute, which reads `1m` rather than
28
+ * zero. A zero second unit is left out.
28
29
  */
29
30
  function shortSpan(ms) {
30
- const minutes = Math.max(1, Math.round(ms / 60_000));
31
+ const minutes = Math.max(1, Math.floor(ms / 60_000));
31
32
  if (minutes < 60)
32
33
  return `${minutes}m`;
33
- const hours = Math.round(minutes / 60);
34
- if (hours < 24)
35
- return `${hours}h`;
36
- return `${Math.max(1, Math.round(minutes / 1440))}d`;
34
+ if (minutes < 1440) {
35
+ const hours = Math.floor(minutes / 60);
36
+ const rest = minutes % 60;
37
+ return rest === 0 ? `${hours}h` : `${hours}h${rest}m`;
38
+ }
39
+ const days = Math.floor(minutes / 1440);
40
+ const hours = Math.floor((minutes % 1440) / 60);
41
+ return hours === 0 ? `${days}d` : `${days}d${hours}h`;
37
42
  }
38
43
  /** A stored window length in minutes as its exact name: `5h`, `7d`, `90m`. */
39
44
  function lengthName(minutes) {