@jeffreycao/copilot-api 2.7.39 → 2.7.40

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/pages/index.html CHANGED
@@ -114,6 +114,7 @@
114
114
 
115
115
  .page-tabs {
116
116
  display: inline-flex;
117
+ max-width: 100%;
117
118
  gap: 4px;
118
119
  margin-bottom: 14px;
119
120
  padding: 4px;
@@ -390,6 +391,10 @@
390
391
  grid-template-columns: repeat(6, minmax(0, 1fr));
391
392
  }
392
393
 
394
+ .subscription-grid {
395
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
396
+ }
397
+
393
398
  .metric-grid-tight .metric-label {
394
399
  white-space: nowrap;
395
400
  }
@@ -531,6 +536,8 @@
531
536
  }
532
537
 
533
538
  @media (max-width: 639px) {
539
+ .page-tabs { display: flex; width: 100%; gap: 2px; }
540
+ .page-tab { flex: 1 1 auto; min-width: 0; justify-content: center; gap: 5px; padding: 8px; font-size: 12px; white-space: nowrap; }
534
541
  .main-shell {
535
542
  padding: 12px 14px 18px;
536
543
  }
@@ -556,6 +563,82 @@
556
563
  grid-template-columns: repeat(2, minmax(0, 1fr));
557
564
  }
558
565
  }
566
+
567
+ /* Subscription allowance workspace. Provider SVGs are bundled from models.dev. */
568
+ .subscriptions-page {
569
+ --quota-text: var(--color-ink, var(--color-fg-lightest));
570
+ --quota-muted: var(--color-ink-soft, var(--color-gray));
571
+ --quota-faint: var(--color-ink-faint, var(--color-fg-darker));
572
+ --quota-line: var(--color-line, var(--color-bg-light-2));
573
+ --quota-soft: var(--color-sunken, var(--color-bg-light-1));
574
+ --quota-surface: var(--color-surface, var(--color-bg));
575
+ padding: 0;
576
+ color: var(--quota-text);
577
+ }
578
+ .subscriptions-page-heading { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-bottom: 26px; }
579
+ .subscriptions-eyebrow { margin: 0 0 7px; font-size: 10px; font-weight: 650; letter-spacing: .16em; color: var(--quota-faint); }
580
+ .subscriptions-page-heading h2 { margin: 0; font-size: 25px; font-weight: 650; letter-spacing: -.04em; line-height: 1.25; }
581
+ .subscriptions-description { margin: 9px 0 0; font-size: 13px; line-height: 1.6; color: var(--quota-muted); }
582
+ .subscriptions-count { display: inline-flex; align-items: center; gap: 7px; flex-shrink: 0; padding: 7px 11px; border: 1px solid var(--quota-line); border-radius: 99px; font-size: 11px; color: var(--quota-muted); background: var(--quota-surface); }
583
+ .subscriptions-count > span { width: 5px; height: 5px; border-radius: 50%; background: #22a06b; }
584
+ .subscriptions-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; align-items: start; }
585
+ .quota-provider-card { --quota-brand: #7c5ac7; position: relative; min-width: 0; padding: 22px 20px 14px; overflow: hidden; border: 1px solid var(--quota-line); border-radius: 18px; background: var(--quota-surface); box-shadow: 0 3px 12px rgb(0 0 0 / 2%); }
586
+ .quota-provider-card[data-provider="codex"] { --quota-brand: #179876; }
587
+ .quota-provider-card[data-provider="xai"] { --quota-brand: #386fce; }
588
+ .dark .quota-provider-card { --quota-brand: #b095ef; }
589
+ .dark .quota-provider-card[data-provider="codex"] { --quota-brand: #50bd9b; }
590
+ .dark .quota-provider-card[data-provider="xai"] { --quota-brand: #7aa8f1; }
591
+ .quota-card-header { display: flex; align-items: center; gap: 11px; }
592
+ .quota-provider-logo { display: grid; place-items: center; width: 44px; height: 44px; flex-shrink: 0; border: 1px solid var(--quota-line); border-radius: 13px; background: var(--quota-soft); }
593
+ .quota-provider-logo img { width: 32px; height: 32px; object-fit: contain; }
594
+ .quota-card-header h3 { margin: 0; font-size: 16px; font-weight: 650; letter-spacing: -.025em; line-height: 1.3; }
595
+ .quota-provider-product { margin: 3px 0 0; font-size: 11px; color: var(--quota-faint); }
596
+ .quota-refresh { display: grid; place-items: center; flex-shrink: 0; width: 30px; height: 30px; padding: 0; margin-left: auto; border: 1px solid var(--quota-line); border-radius: 9px; color: var(--quota-muted); background: var(--quota-surface); cursor: pointer; }
597
+ .quota-refresh:hover { background: var(--quota-soft); color: var(--quota-text); }
598
+ .quota-refresh:disabled { opacity: .45; cursor: default; }
599
+ .quota-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 17px 0 0; }
600
+ .quota-plan { min-width: 0; max-width: 65%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 4px 8px; border-radius: 6px; background: var(--quota-soft); color: var(--quota-muted); font-size: 11px; font-weight: 500; }
601
+ .quota-status { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; font-size: 10px; color: var(--quota-faint); }
602
+ .quota-status > span { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
603
+ .quota-status[data-state="ready"] { color: var(--quota-muted); }
604
+ .quota-status[data-state="warning"] { color: #d58a25; }
605
+ .quota-summary { margin: 18px 0 22px; padding: 18px; border-radius: 13px; background: color-mix(in srgb, var(--quota-brand) 4%, var(--quota-soft)); }
606
+ .quota-summary-label { margin: 0 0 9px; font-size: 11px; font-weight: 500; color: var(--quota-muted); }
607
+ .quota-summary-value { font-size: 40px; line-height: 1.05; font-weight: 600; letter-spacing: -.055em; font-variant-numeric: tabular-nums; }
608
+ .quota-summary-context { margin: 9px 0 0; font-size: 11px; color: var(--quota-faint); }
609
+ .quota-details { font-size: 12px; }
610
+ .quota-window + .quota-window { margin-top: 18px; }
611
+ .quota-window-heading { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 7px; font-size: 12px; }
612
+ .quota-window-heading > :first-child { color: var(--quota-muted); }
613
+ .quota-window-heading > :last-child { font-weight: 550; color: var(--quota-text); }
614
+ .quota-progress { height: 6px; overflow: hidden; border-radius: 99px; background: var(--quota-soft); }
615
+ .quota-progress-fill { height: 100%; border-radius: inherit; background: var(--quota-brand); transition: width .25s ease; }
616
+ .quota-window[data-warning="true"] .quota-progress-fill { background: #d58a25; }
617
+ .quota-window-note { margin: 6px 0 0; font-size: 11px; color: var(--quota-faint); line-height: 1.5; }
618
+ .quota-balance { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--quota-line); font-size: 12px; color: var(--quota-muted); }
619
+ .quota-balance strong { color: var(--quota-text); font-weight: 600; }
620
+ .quota-notice { margin: 0 0 15px; padding: 10px 12px; border-radius: 9px; background: rgb(213 138 37 / 8%); color: #b67822; font-size: 12px; line-height: 1.6; }
621
+ .quota-card-footer { margin-top: 22px; padding-top: 12px; border-top: 1px solid var(--quota-line); color: var(--quota-faint); font-size: 10px; }
622
+ .quota-card-message { display: flex; align-items: center; min-height: 180px; margin: 0; font-size: 12px; line-height: 1.8; color: var(--quota-muted); }
623
+ .quota-card-message[role="alert"] { color: #b67822; }
624
+ .quota-skeleton { display: flex; flex-direction: column; justify-content: center; gap: 16px; min-height: 230px; animation: quota-pulse 1.5s ease-in-out infinite; }
625
+ .quota-skeleton > div { height: 9px; border-radius: 6px; background: var(--quota-soft); }
626
+ .quota-skeleton > div:first-child { width: 55%; height: 42px; margin-bottom: 22px; }
627
+ .quota-skeleton > div:last-child { width: 75%; }
628
+ @keyframes quota-pulse { 50% { opacity: .45; } }
629
+ .subscriptions-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; min-height: 260px; padding: 30px; text-align: center; border: 1px dashed var(--quota-line); border-radius: 18px; color: var(--quota-muted); font-size: 13px; }
630
+ .subscriptions-empty h3 { margin: 0; font-size: 17px; font-weight: 600; color: var(--quota-text); }
631
+ .subscriptions-empty p { max-width: 360px; margin: 0; line-height: 1.7; }
632
+ .subscriptions-action { margin-top: 5px; border: 0; border-radius: 9px; padding: 8px 14px; background: var(--quota-text); color: var(--quota-surface); cursor: pointer; font-size: 12px; }
633
+ @media (max-width: 899px) {
634
+ .subscriptions-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
635
+ }
636
+ @media (max-width: 639px) {
637
+ .subscriptions-page-heading { align-items: flex-start; gap: 12px; margin-bottom: 20px; flex-direction: column; }
638
+ .subscriptions-page-heading h2 { font-size: 23px; }
639
+ .subscriptions-grid { grid-template-columns: minmax(0, 1fr); }
640
+ .quota-provider-card { padding: 20px 18px 14px; }
641
+ }
559
642
  </style>
560
643
  </head>
561
644
  <body class="antialiased">
@@ -587,6 +670,10 @@
587
670
  <i data-lucide="bar-chart-3" class="h-4 w-4" aria-hidden="true"></i>
588
671
  Token Usage
589
672
  </button>
673
+ <button id="subscriptions-tab" type="button" class="page-tab" role="tab" data-tab="subscriptions" aria-selected="false" aria-controls="content-area" tabindex="-1">
674
+ <i data-lucide="gauge" class="h-4 w-4" aria-hidden="true"></i>
675
+ Subscriptions
676
+ </button>
590
677
  <button
591
678
  id="models-tab"
592
679
  type="button"
@@ -668,6 +755,7 @@
668
755
  const pageTabs = document.getElementById("page-tabs");
669
756
  const tabButtons = [
670
757
  document.getElementById("usage-tab"),
758
+ document.getElementById("subscriptions-tab"),
671
759
  document.getElementById("models-tab"),
672
760
  ];
673
761
  const periodField = document.getElementById("token-usage-period-field");
@@ -682,6 +770,29 @@
682
770
  fetchButton.style.backgroundColor = "var(--color-blue)";
683
771
  });
684
772
 
773
+ // models.dev provider SVGs are bundled to work without external requests.
774
+ const SUBSCRIPTION_PROVIDERS = {
775
+ "github-copilot": {
776
+ "name": "GitHub Copilot",
777
+ "product": "GitHub",
778
+ "logo": "data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2040%2040%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M35.3993%2026.4544C34.2871%2028.3855%2027.8314%2032.9425%2020%2032.9425C12.1686%2032.9425%205.71288%2028.3855%204.60075%2026.4544C4.5427%2026.3532%204.50826%2026.2401%204.5%2026.1237V22.418C4.51199%2022.3209%204.53496%2022.2256%204.56846%2022.1338C5.04896%2020.9261%206.30833%2019.1733%207.93325%2018.7031C8.14896%2018.149%208.468%2017.3404%208.76508%2016.7437C8.71727%2016.2776%208.69485%2015.8094%208.69792%2015.3409C8.69792%2013.6217%209.06217%2012.113%2010.1601%2010.9906C10.6729%2010.4662%2011.3097%2010.0644%2012.064%209.76091C13.871%208.29357%2016.4453%207.05745%2019.9716%207.05745C23.4978%207.05745%2026.129%208.29357%2027.936%209.76091C28.6903%2010.0644%2029.3271%2010.4662%2029.8399%2010.9906C30.9378%2012.113%2031.3021%2013.6217%2031.3021%2015.3409C31.3021%2015.8162%2031.284%2016.2877%2031.2349%2016.7437C31.532%2017.3404%2031.851%2018.149%2032.0667%2018.7031C33.6917%2019.1733%2034.951%2020.9261%2035.4315%2022.1338C35.4677%2022.2248%2035.4908%2022.3205%2035.5%2022.418V26.1237C35.4917%2026.2401%2035.4573%2026.3532%2035.3993%2026.4544ZM20.2222%2018.7147H19.7778C19.6422%2018.945%2019.4889%2019.1644%2019.3193%2019.3709C18.3247%2020.5941%2016.8419%2021.2981%2014.7881%2021.2981C12.56%2021.2981%2010.9273%2020.8344%209.90304%2019.6719C9.86495%2019.6283%209.82833%2019.5835%209.79325%2019.5375L9.66667%2019.6719V28.1775C11.5202%2029.1837%2015.4972%2030.992%2020%2030.992C24.5028%2030.992%2028.4798%2029.1837%2030.3333%2028.1775V19.6719L30.2067%2019.5375C30.2067%2019.5375%2030.1641%2019.5957%2030.097%2019.6719C29.0727%2020.8344%2027.44%2021.2981%2025.2119%2021.2981C23.1581%2021.2981%2021.6753%2020.5941%2020.6807%2019.3709C20.5111%2019.1644%2020.3578%2018.945%2020.2222%2018.7147ZM23.2292%2022.9127C23.9383%2022.9127%2024.5208%2023.4952%2024.5208%2024.2043V26.7877C24.5208%2027.4968%2023.9383%2028.0793%2023.2292%2028.0793C22.52%2028.0793%2021.9375%2027.4968%2021.9375%2026.7877V24.2043C21.9375%2023.4952%2022.52%2022.9127%2023.2292%2022.9127ZM16.7708%2022.9127C17.48%2022.9127%2018.0625%2023.4952%2018.0625%2024.2043V26.7877C18.0625%2027.4968%2017.48%2028.0793%2016.7708%2028.0793C16.0617%2028.0793%2015.4792%2027.4968%2015.4792%2026.7877V24.2043C15.4792%2023.4952%2016.0617%2022.9127%2016.7708%2022.9127ZM21.0501%2014.9237C21.2258%2016.289%2021.5707%2017.3947%2022.1842%2018.149C22.7551%2018.8517%2023.649%2019.3606%2025.2119%2019.3606C27.2437%2019.3606%2028.1724%2018.9253%2028.6438%2018.3905C29.1398%2017.8287%2029.3646%2016.9051%2029.3646%2015.3409C29.3646%2013.8684%2029.0507%2012.9552%2028.454%2012.3455C27.8378%2011.7152%2026.7502%2011.2321%2024.8063%2011.0216C22.8856%2010.8136%2021.975%2011.1998%2021.5345%2011.7049C21.187%2012.1014%2020.97%2012.7485%2020.9688%2013.7431V13.7702C20.9688%2014.1121%2020.9959%2014.4966%2021.0501%2014.9237ZM18.9499%2014.9237C19.0041%2014.4966%2019.0312%2014.1117%2019.0312%2013.7689V13.7431C19.03%2012.7485%2018.813%2012.1014%2018.4655%2011.7049C18.025%2011.1998%2017.1144%2010.8136%2015.1937%2011.0216C13.2498%2011.2321%2012.1622%2011.7152%2011.546%2012.3455C10.9493%2012.9552%2010.6354%2013.8684%2010.6354%2015.3409C10.6354%2016.9051%2010.8615%2017.8287%2011.3562%2018.3905C11.8276%2018.9253%2012.7563%2019.3606%2014.7881%2019.3606C16.351%2019.3606%2017.2449%2018.8517%2017.8158%2018.149C18.4293%2017.3947%2018.7742%2016.289%2018.9499%2014.9237Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
779
+ },
780
+ "codex": {
781
+ "name": "Codex",
782
+ "product": "OpenAI",
783
+ "logo": "data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2040%2040%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M32.8377%2017.282C33.2127%2016.25%2033.3072%2015.218%2033.2127%2014.1875C33.1197%2013.1571%2032.7447%2012.1251%2032.2752%2011.1876C31.4322%209.78209%2030.2127%208.6571%2028.8072%208.0001C27.3072%207.34461%2025.7127%207.15711%2024.1197%207.53211C23.3698%206.78212%2022.5253%206.12512%2021.5878%205.65713C20.6503%205.18913%2019.5253%205.00013%2018.4948%205.00013C16.8851%204.99074%2015.3125%205.48246%2013.9948%206.40712C12.6824%207.34311%2011.7449%208.6571%2011.2754%2010.1571C10.1504%2010.4376%209.21289%2010.9071%208.27539%2011.4696C7.4324%2012.1251%206.77541%2012.9696%206.21291%2013.8126C5.36992%2015.2195%205.08792%2016.8125%205.27542%2018.407C5.46399%2019.9968%206.11605%2021.496%207.1504%2022.718C6.79608%2023.7086%206.66795%2024.7659%206.77541%2025.8124C6.86991%2026.8444%207.2449%2027.8749%207.7129%2028.8124C8.55739%2030.2194%209.77538%2031.3444%2011.1824%2031.9999C12.6824%2032.6569%2014.2753%2032.8444%2015.8698%2032.4694C16.6198%2033.2194%2017.4628%2033.8749%2018.4003%2034.3444C19.3378%2034.8139%2020.4628%2034.9999%2021.4948%2034.9999C23.1043%2035.0097%2024.6769%2034.5185%2025.9947%2033.5944C27.3072%2032.6569%2028.2447%2031.3444%2028.7127%2029.8444C29.7719%2029.6432%2030.7682%2029.1934%2031.6197%2028.5319C32.4627%2027.8749%2033.2127%2027.1249%2033.6822%2026.1874C34.5251%2024.7819%2034.8071%2023.1875%2034.6196%2021.5945C34.4322%2020%2033.8697%2018.5015%2032.8377%2017.282ZM21.5878%2033.0304C20.0878%2033.0304%2018.9628%2032.5609%2017.9323%2031.7179C17.9323%2031.7179%2018.0253%2031.6234%2018.1198%2031.6234L24.1197%2028.1554C24.2862%2028.0803%2024.4196%2027.9469%2024.4947%2027.7804C24.5698%2027.636%2024.6021%2027.4731%2024.5877%2027.3109V18.875L27.1197%2020.375V27.3124C27.1455%2028.0547%2027.0215%2028.7945%2026.755%2029.4878C26.4885%2030.181%2026.085%2030.8134%2025.5687%2031.3473C25.0523%2031.8811%2024.4337%2032.3054%2023.7497%2032.5949C23.0658%2032.8843%2022.3305%2033.0314%2021.5878%2033.0304ZM9.49488%2027.8749C8.83789%2026.7499%208.55739%2025.4374%208.83789%2024.125C8.83789%2024.125%208.93239%2024.2195%209.02539%2024.2195L15.0253%2027.6874C15.1693%2027.7638%2015.3325%2027.7966%2015.4948%2027.7819C15.6823%2027.7819%2015.8698%2027.7819%2015.9628%2027.6874L23.2753%2023.4695V26.3749L17.1823%2029.9374C16.5506%2030.3042%2015.8527%2030.5427%2015.1287%2030.6393C14.4046%2030.7358%2013.6686%2030.6884%2012.9629%2030.4999C11.4629%2030.1249%2010.2449%2029.1874%209.49488%2027.8749ZM7.9004%2014.8445C8.56239%2013.7234%209.58826%2012.8627%2010.8074%2012.4056V19.532C10.8074%2019.718%2010.8074%2019.907%2010.9004%2020C10.9755%2020.1665%2011.1089%2020.2998%2011.2754%2020.375L18.5878%2024.5944L16.0573%2026.0944L10.0574%2022.625C9.41842%2022.2639%208.85742%2021.7797%208.40684%2021.2004C7.95627%2020.6211%207.62506%2019.9582%207.4324%2019.25C7.05741%2017.8445%207.1504%2016.157%207.9004%2014.8445ZM28.6197%2019.625L21.3073%2015.407L23.8377%2013.9071L29.8377%2017.375C30.7752%2017.9375%2031.5252%2018.6875%2031.9947%2019.625C32.4642%2020.5625%2032.7447%2021.5945%2032.6502%2022.7195C32.5603%2023.7755%2032.1699%2024.7837%2031.5252%2025.6249C30.8697%2026.4694%2030.0252%2027.1249%2028.9947%2027.4999V20.375C28.9947%2020.1875%2028.9947%2020%2028.9002%2019.907C28.9002%2019.907%2028.8072%2019.718%2028.6197%2019.625ZM31.1502%2015.875C31.1502%2015.875%2031.0572%2015.782%2030.9627%2015.782L24.9627%2012.3126C24.7752%2012.2196%2024.6822%2012.2196%2024.4947%2012.2196C24.3072%2012.2196%2024.1197%2012.2196%2024.0252%2012.3126L16.7128%2016.532V13.6251L22.8073%2010.0626C23.7448%209.50009%2024.7752%209.31259%2025.9002%209.31259C26.9322%209.31259%2027.9627%209.68759%2028.9002%2010.3446C29.7447%2011.0001%2030.4947%2011.8446%2030.8697%2012.7821C31.2447%2013.7196%2031.3377%2014.8445%2031.1502%2015.875ZM15.4003%2021.125L12.8699%2019.625V12.5946C12.8699%2011.5626%2013.1503%2010.4376%2013.7128%209.59459C14.2753%208.6571%2015.1198%208.0001%2016.0573%207.53211C17.0127%207.05249%2018.0956%206.88812%2019.1503%207.06261C20.1823%207.15711%2021.2128%207.62511%2022.0573%208.2821C22.0573%208.2821%2021.9628%208.3751%2021.8698%208.3751L15.8698%2011.8446C15.7033%2011.9197%2015.57%2012.0531%2015.4948%2012.2196C15.4003%2012.4071%2015.4003%2012.5001%2015.4003%2012.6876V21.125ZM16.7128%2018.125L19.9948%2016.25L23.2753%2018.125V21.875L19.9948%2023.75L16.7128%2021.875V18.125Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
784
+ },
785
+ "xai": {
786
+ "name": "xAI",
787
+ "product": "Grok Build",
788
+ "logo": "data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2040%2040%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M12.4579%2015.6036L26.1529%2035H20.0656L6.37059%2015.6036H12.4579ZM12.4524%2026.3764L15.4974%2030.6909L12.4551%2035H6.36377L12.4524%2026.3764ZM33.6365%207.15727V35H28.647V14.2236L33.6365%207.15727ZM33.6365%205L20.0656%2024.2205L17.0206%2019.9073L27.5451%205H33.6365Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
789
+ },
790
+ "opencode-go": {
791
+ "name": "OpenCode Go",
792
+ "product": "OpenCode",
793
+ "logo": "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M15.6%2017.4H8.4V10.2H15.6V17.4Z%22%20fill%3D%22currentColor%22%20fill-opacity%3D%220.4%22%2F%3E%0A%3Cpath%20d%3D%22M15.6%206.6H8.4V17.4H15.6V6.6ZM19.2%2021H4.8V3H19.2V21Z%22%20fill%3D%22currentColor%22%2F%3E%0A%3C%2Fsvg%3E%0A"
794
+ }
795
+ };
685
796
  const DEFAULT_ENDPOINT = "http://localhost:4141/usage";
686
797
  const DEFAULT_TOKEN_USAGE_PERIOD = "today";
687
798
  const DEFAULT_TOKEN_USAGE_EVENTS_PAGE_SIZE = 20;
@@ -722,8 +833,15 @@
722
833
  isLoading: false,
723
834
  isTokenUsageLoading: false,
724
835
  isEventsLoading: false,
725
- error: null,
726
- data: null,
836
+ subscriptionUsage: {},
837
+ subscriptionUsageErrors: {},
838
+ isSubscriptionsLoading: false,
839
+ subscriptionRequestId: 0,
840
+ subscriptionRequests: {},
841
+ subscriptionLoadingProviders: {},
842
+ subscriptionsLoaded: false,
843
+ subscriptionsConnectionKey: null,
844
+ subscriptionPageError: null,
727
845
  tokenUsageDailySummary: null,
728
846
  tokenUsageSummary: null,
729
847
  tokenUsageSessionsPage: null,
@@ -1049,6 +1167,8 @@
1049
1167
  const currentUrl = new URL(window.location.href);
1050
1168
  const endpoint = endpointUrlInput.value.trim();
1051
1169
  const period = getSelectedPeriod();
1170
+ if (state.activeTab === "usage") currentUrl.searchParams.delete("tab");
1171
+ else currentUrl.searchParams.set("tab", state.activeTab);
1052
1172
 
1053
1173
  if (endpoint) {
1054
1174
  currentUrl.searchParams.set("endpoint", endpoint);
@@ -1067,17 +1187,17 @@
1067
1187
  const isLoading =
1068
1188
  state.activeTab === "models"
1069
1189
  ? state.isModelsLoading
1070
- : state.isLoading;
1190
+ : state.activeTab === "subscriptions" ? state.isSubscriptionsLoading : state.isLoading;
1071
1191
  fetchButton.disabled = isLoading;
1072
1192
  fetchButtonLabel.textContent = isLoading
1073
1193
  ? "Refreshing..."
1074
- : "Refresh";
1194
+ : state.activeTab === "subscriptions" ? "Refresh all" : "Refresh";
1075
1195
  tokenUsagePeriodSelect.disabled =
1076
1196
  state.isLoading || state.isTokenUsageLoading;
1077
- periodField.hidden = state.activeTab === "models";
1197
+ periodField.hidden = state.activeTab !== "usage";
1078
1198
  endpointForm.classList.toggle(
1079
1199
  "models-view",
1080
- state.activeTab === "models"
1200
+ state.activeTab !== "usage"
1081
1201
  );
1082
1202
  contentArea.setAttribute("aria-labelledby", `${state.activeTab}-tab`);
1083
1203
  contentArea.setAttribute("aria-busy", String(isLoading));
@@ -1239,41 +1359,22 @@
1239
1359
  return;
1240
1360
  }
1241
1361
 
1362
+ if (state.activeTab === "subscriptions") {
1363
+ contentArea.innerHTML = renderSubscriptionUsageSection();
1364
+ createIcons();
1365
+ return;
1366
+ }
1367
+
1242
1368
  if (state.isLoading) {
1243
1369
  contentArea.innerHTML = renderSpinner();
1244
1370
  createIcons();
1245
1371
  return;
1246
1372
  }
1247
1373
 
1248
- const hasContent = Boolean(
1249
- state.error
1250
- || state.data
1251
- || state.isTokenUsageLoading
1252
- || state.isEventsLoading
1253
- || state.tokenUsageDailySummary
1254
- || state.tokenUsageSummary
1255
- || state.tokenUsageEventsPage
1256
- || state.tokenUsageDailyError
1257
- || state.tokenUsageSummaryError
1258
- || state.tokenUsageEventsError
1259
- );
1260
-
1261
- if (!hasContent) {
1262
- contentArea.innerHTML = renderWelcomeMessage();
1263
- } else if (state.error && !state.data) {
1264
- contentArea.innerHTML = `
1265
- ${renderTokenUsageSection()}
1266
- ${renderUsageErrorSection()}
1267
- `;
1268
- } else if (state.data) {
1269
- contentArea.innerHTML = `
1270
- ${renderTokenUsageSection()}
1271
- ${renderUsageQuotas(state.data.quota_snapshots)}
1272
- ${renderDetailedData(state.data)}
1273
- `;
1274
- } else {
1275
- contentArea.innerHTML = renderTokenUsageSection();
1276
- }
1374
+ const hasContent = Boolean(state.isTokenUsageLoading || state.isEventsLoading
1375
+ || state.tokenUsageSummary || state.tokenUsageDailySummary || state.tokenUsageEventsPage
1376
+ || state.tokenUsageSummaryError || state.tokenUsageDailyError || state.tokenUsageEventsError);
1377
+ contentArea.innerHTML = hasContent ? renderTokenUsageSection() : renderWelcomeMessage();
1277
1378
 
1278
1379
  // Replace placeholder icons with actual Lucide icons
1279
1380
  createIcons();
@@ -1287,8 +1388,6 @@
1287
1388
  "modelBreakdown",
1288
1389
  "sessionBreakdown",
1289
1390
  "requestEvents",
1290
- "copilotQuotas",
1291
- "usageApiResponse",
1292
1391
  ]);
1293
1392
 
1294
1393
  function readCollapsedSections() {
@@ -1488,7 +1587,7 @@
1488
1587
  `;
1489
1588
  }
1490
1589
 
1491
- function getModelsConnectionKey() {
1590
+ function getConnectionKey() {
1492
1591
  return JSON.stringify([
1493
1592
  endpointUrlInput.value.trim(),
1494
1593
  apiKeyInput.value.trim(),
@@ -1496,7 +1595,7 @@
1496
1595
  }
1497
1596
 
1498
1597
  function syncModelsConnection() {
1499
- const key = getModelsConnectionKey();
1598
+ const key = getConnectionKey();
1500
1599
  if (state.modelsConnectionKey !== key) {
1501
1600
  state.modelsConnectionKey = key;
1502
1601
  state.modelsRequestId += 1;
@@ -1506,9 +1605,24 @@
1506
1605
  }
1507
1606
  }
1508
1607
 
1608
+ function syncSubscriptionsConnection() {
1609
+ const key = getConnectionKey();
1610
+ if (state.subscriptionsConnectionKey !== key) {
1611
+ state.subscriptionsConnectionKey = key;
1612
+ state.subscriptionRequestId += 1;
1613
+ state.subscriptionUsage = {};
1614
+ state.subscriptionUsageErrors = {};
1615
+ state.subscriptionLoadingProviders = {};
1616
+ state.subscriptionsLoaded = false;
1617
+ state.isSubscriptionsLoading = false;
1618
+ state.subscriptionPageError = null;
1619
+ }
1620
+ }
1621
+
1509
1622
  function handleModelsConnectionChange() {
1510
1623
  syncModelsConnection();
1511
- if (state.activeTab === "models") {
1624
+ syncSubscriptionsConnection();
1625
+ if (state.activeTab === "models" || state.activeTab === "subscriptions") {
1512
1626
  render();
1513
1627
  }
1514
1628
  }
@@ -1520,7 +1634,7 @@
1520
1634
  const requestId = ++state.modelsRequestId;
1521
1635
  const isCurrentRequest = () =>
1522
1636
  requestId === state.modelsRequestId
1523
- && connectionKey === getModelsConnectionKey();
1637
+ && connectionKey === getConnectionKey();
1524
1638
  state.models = null;
1525
1639
  state.modelsError = null;
1526
1640
  state.isModelsLoading = true;
@@ -1550,16 +1664,21 @@
1550
1664
  }
1551
1665
 
1552
1666
  function activateTab(tab) {
1553
- if (tab !== "usage" && tab !== "models") {
1667
+ if (!["usage", "subscriptions", "models"].includes(tab)) {
1554
1668
  return;
1555
1669
  }
1556
1670
  syncEndpointCredential();
1557
1671
  syncModelsConnection();
1672
+ syncSubscriptionsConnection();
1558
1673
  state.activeTab = tab;
1674
+ syncUrlState();
1559
1675
  render();
1560
1676
  if (tab === "models" && !state.models && !state.isModelsLoading) {
1561
1677
  void fetchModels();
1562
1678
  }
1679
+ if (tab === "subscriptions" && !state.subscriptionsLoaded && !state.isSubscriptionsLoading) {
1680
+ void refreshSubscriptionQuotas();
1681
+ }
1563
1682
  }
1564
1683
 
1565
1684
  function handleTabsKeydown(event) {
@@ -1568,10 +1687,10 @@
1568
1687
  return;
1569
1688
  }
1570
1689
  event.preventDefault();
1571
- const index = state.activeTab === "usage" ? 0 : 1;
1690
+ const index = tabButtons.findIndex((button) => button.dataset.tab === state.activeTab);
1572
1691
  const nextIndex = event.key === "Home" ? 0
1573
- : event.key === "End" ? 1
1574
- : 1 - index;
1692
+ : event.key === "End" ? tabButtons.length - 1
1693
+ : (index + (event.key === "ArrowRight" ? 1 : -1) + tabButtons.length) % tabButtons.length;
1575
1694
  activateTab(tabButtons[nextIndex].dataset.tab);
1576
1695
  tabButtons[nextIndex].focus();
1577
1696
  }
@@ -1600,197 +1719,85 @@
1600
1719
  feedback.classList.add("py-3");
1601
1720
  }
1602
1721
 
1603
- /**
1604
- * Renders the "Usage Quotas" section with progress bars.
1605
- * @param {object} snapshots - The quota_snapshots object from the API response.
1606
- * @returns {string} HTML string for the usage quotas section.
1607
- */
1608
- function renderUsageQuotas(snapshots) {
1609
- if (!snapshots) return "";
1610
-
1611
- const quotaCards = Object.entries(snapshots)
1612
- .map(([key, value]) => {
1613
- return renderQuotaCard(key, value);
1614
- })
1615
- .join("");
1616
-
1722
+ // Subscription quotas use the same normalized server response as desktop.
1723
+ function subscriptionQuotaLabel(window) {
1724
+ let label = "Included allowance";
1725
+ if (window.id.startsWith("copilot:")) return window.name || "Premium";
1726
+ if (window.kind === "spend") return "Monthly credit limit";
1727
+ if (window.kind === "on-demand") return "Pay as you go · Monthly";
1728
+ const seconds = window.windowSeconds;
1729
+ if (window.period === "weekly" || seconds === 604800) label = "Weekly limit";
1730
+ else if (window.period === "monthly") label = "Monthly limit";
1731
+ else if (Number.isFinite(seconds) && seconds >= 86400) label = `${seconds / 86400}-day limit`;
1732
+ else if (Number.isFinite(seconds) && seconds >= 3600) label = `${seconds / 3600}-hour limit`;
1733
+ else if (Number.isFinite(seconds) && seconds > 0) label = `${seconds / 60}-minute limit`;
1734
+ return window.name ? `${window.name} · ${label}` : label;
1735
+ }
1736
+
1737
+ function formatSubscriptionAmount(value, unit) {
1738
+ if (!Number.isFinite(value)) return "—";
1739
+ return unit === "USD"
1740
+ ? value.toLocaleString(undefined, { style: "currency", currency: "USD" })
1741
+ : value.toLocaleString(undefined, { maximumFractionDigits: 2 });
1742
+ }
1743
+
1744
+ function renderSubscriptionQuotaWindow(window) {
1745
+ const percent = window.usedPercent;
1746
+ const hasPercent = Number.isFinite(percent) && !window.unlimited;
1747
+ const width = hasPercent ? Math.min(100, Math.max(0, percent)) : 0;
1748
+ const label = escapeHtml(subscriptionQuotaLabel(window));
1749
+ const reset = Number.isFinite(window.resetAt) ? new Date(window.resetAt * 1000) : null;
1750
+ const resetDate = window.resetDate || (reset && Number.isFinite(reset.getTime()) ? reset.toLocaleString() : null);
1617
1751
  return `
1618
- <section id="usage-quotas" class="dashboard-section">
1619
- <h2 class="section-heading">
1620
- ${renderCollapseToggle(
1621
- "copilotQuotas",
1622
- `<i data-lucide="gauge" class="h-5 w-5" style="color: var(--color-blue);"></i><span>Copilot Quotas</span>`
1623
- )}
1624
- </h2>
1625
- ${renderCollapseBody("copilotQuotas", quotaCards, "metric-grid")}
1626
- </section>
1627
- `;
1628
- }
1629
-
1630
- function renderUsageErrorSection() {
1631
- if (!state.error || state.data) {
1632
- return "";
1633
- }
1634
-
1635
- return `
1636
- <section id="usage-quotas" class="dashboard-section">
1637
- <h2 class="section-heading">
1638
- ${renderCollapseToggle(
1639
- "copilotQuotas",
1640
- `<i data-lucide="gauge" class="h-5 w-5" style="color: var(--color-blue);"></i><span>Copilot Quotas</span>`
1641
- )}
1642
- </h2>
1643
- ${renderCollapseBody(
1644
- "copilotQuotas",
1645
- renderError(state.error, "Usage request failed")
1646
- )}
1647
- </section>
1648
- `;
1649
- }
1650
-
1651
- /**
1652
- * Renders a single quota card.
1653
- * @param {string} title - The name of the quota (e.g., 'chat').
1654
- * @param {object} details - The details object for the quota.
1655
- * @returns {string} HTML string for a single card.
1656
- */
1657
- function getCopilotQuotaRemaining(details) {
1658
- if (Number.isFinite(details.remaining)) return details.remaining;
1659
- if (Number.isFinite(details.quota_remaining)) {
1660
- return details.quota_remaining;
1661
- }
1662
- return null;
1663
- }
1664
-
1665
- function getCopilotQuotaPercentRemaining(details) {
1666
- if (details.unlimited) return 100;
1667
- const percent = Number.isFinite(details.percent_remaining)
1668
- ? details.percent_remaining
1669
- : Number.isFinite(details.entitlement) && details.entitlement > 0
1670
- ? ((getCopilotQuotaRemaining(details) ?? details.entitlement) /
1671
- details.entitlement) * 100
1672
- : 100;
1673
- return Math.max(0, Math.min(100, percent));
1674
- }
1675
-
1676
- function renderQuotaCard(title, details) {
1677
- const { entitlement, unlimited } = details;
1678
- const remaining = getCopilotQuotaRemaining(details) ?? 0;
1679
-
1680
- const percentUsed = 100 - getCopilotQuotaPercentRemaining(details);
1681
- const used = unlimited
1682
- ? "N/A"
1683
- : (entitlement - remaining).toLocaleString();
1684
-
1685
- let progressBarColor = "var(--color-green)";
1686
- if (percentUsed > 75) progressBarColor = "var(--color-yellow)";
1687
- if (percentUsed > 90) progressBarColor = "var(--color-red)";
1688
- if (unlimited) progressBarColor = "var(--color-blue)";
1689
-
1690
- return `
1691
- <div class="metric-card">
1692
- <div class="flex items-center justify-between gap-3">
1693
- <div class="metric-label capitalize">
1694
- <span class="metric-dot" style="background-color: ${progressBarColor};"></span>
1695
- <span>${escapeHtml(title.replace(/_/g, " "))}</span>
1696
- </div>
1697
- ${
1698
- unlimited
1699
- ? `<span class="rounded-md px-2 py-1 text-xs font-semibold" style="background-color: rgba(37, 99, 235, 0.1); color: var(--color-blue);">Unlimited</span>`
1700
- : `<span class="text-xs font-semibold" style="color: var(--color-gray);">${percentUsed.toFixed(1)}% used</span>`
1701
- }
1702
- </div>
1703
- <div class="mt-5">
1704
- <div class="w-full progress-bar-bg h-2">
1705
- <div class="progress-bar-fg h-2" style="width: ${unlimited ? 100 : percentUsed}%; background-color: ${progressBarColor};"></div>
1706
- </div>
1707
- </div>
1708
- <div class="mt-2 flex items-center justify-between gap-4 text-xs font-medium" style="color: var(--color-gray);">
1709
- <span>${used} / ${unlimited ? "∞" : entitlement.toLocaleString()}</span>
1710
- <span>${unlimited ? "∞" : remaining.toLocaleString()} remaining</span>
1711
- </div>
1752
+ <div class="quota-window" data-warning="${Boolean(window.limitReached || percent >= 90)}">
1753
+ <div class="quota-window-heading"><span>${label}</span><span>${window.unlimited ? "Unlimited" : hasPercent ? `${Math.round(percent * 10) / 10}% used` : "—"}</span></div>
1754
+ ${hasPercent ? `<div role="progressbar" aria-label="${label}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${width}" class="quota-progress"><div class="quota-progress-fill" style="width: ${width}%;"></div></div>` : ""}
1755
+ ${!window.unlimited && (window.used !== undefined || window.limit !== undefined) ? `<p class="quota-window-note">${formatSubscriptionAmount(window.used, window.unit)} / ${formatSubscriptionAmount(window.limit, window.unit)}${window.unit === "credits" ? " Credits" : window.unit === "requests" ? " requests" : ""}</p>` : ""}
1756
+ ${resetDate ? `<p class="quota-window-note">Resets ${escapeHtml(resetDate)}</p>` : ""}
1712
1757
  </div>
1713
1758
  `;
1714
1759
  }
1715
1760
 
1716
- /**
1717
- * Recursively builds a formatted HTML list from a JSON object.
1718
- * @param {object} obj - The object to format.
1719
- * @returns {string} HTML string for the formatted list.
1720
- */
1721
- function formatObject(obj) {
1722
- if (Array.isArray(obj)) {
1723
- if (obj.length === 0) {
1724
- return `<span style="color: var(--color-gray);">[]</span>`;
1725
- }
1726
-
1727
- return (
1728
- '<div class="pl-4">' +
1729
- obj
1730
- .map(
1731
- (value, index) =>
1732
- `<div class="mt-1"><span class="capitalize font-semibold" style="color: var(--color-fg-lightest);">[${index}]:</span> ${formatObject(value)}</div>`,
1733
- )
1734
- .join("") +
1735
- "</div>"
1736
- );
1737
- }
1738
-
1739
- if (obj === null || typeof obj !== "object") {
1740
- if (typeof obj === "boolean") {
1741
- return `<span class="font-semibold" style="color: ${obj ? "var(--color-green)" : "var(--color-red)"}">${escapeHtml(String(obj))}</span>`;
1742
- }
1743
-
1744
- return `<span style="color: var(--color-blue);">${escapeHtml(JSON.stringify(obj))}</span>`;
1745
- }
1746
-
1747
- const entries = Object.entries(obj);
1748
- if (entries.length === 0) {
1749
- return `<span style="color: var(--color-gray);">{}</span>`;
1750
- }
1751
-
1752
- return (
1753
- '<div class="pl-4">' +
1754
- entries
1755
- .map(([key, value]) => {
1756
- const formattedKey = escapeHtml(key.replace(/_/g, " "));
1757
- return `<div class="mt-1">
1758
- <span class="capitalize font-semibold" style="color: var(--color-fg-lightest);">${formattedKey}:</span>
1759
- ${formatObject(value)}
1760
- </div>`;
1761
- })
1762
- .join("") +
1763
- "</div>"
1764
- );
1765
- }
1766
-
1767
- /**
1768
- * Renders the section with the full, formatted API response.
1769
- * @param {object} data - The full API response data.
1770
- * @returns {string} HTML string for the full data section.
1771
- */
1772
- function renderDetailedData(data) {
1773
- if (data === null || typeof data === "undefined") {
1774
- return "";
1775
- }
1776
-
1777
- const formattedDetails = formatObject(data);
1778
- return `
1779
- <section id="detailed-data" class="dashboard-section">
1780
- <h2 class="section-heading">
1781
- ${renderCollapseToggle(
1782
- "usageApiResponse",
1783
- `<i data-lucide="file-text" class="h-5 w-5" style="color: var(--color-blue);"></i><span>Copilot Usage API Response</span>`
1784
- )}
1785
- </h2>
1786
- ${renderCollapseBody(
1787
- "usageApiResponse",
1788
- `<div class="panel p-4 relative font-mono text-xs code-block overflow-auto" style="max-height: 420px;">
1789
- ${formattedDetails}
1790
- </div>`
1791
- )}
1792
- </section>
1793
- `;
1761
+ function renderSubscriptionSummary(usage) {
1762
+ const main = usage.windows.find((window) => window.kind === "usage") || usage.windows[0];
1763
+ const credits = usage.credits;
1764
+ const showCredits = credits && (credits.hasCredits || credits.unlimited || !main);
1765
+ const value = showCredits ? credits.unlimited ? "∞"
1766
+ : formatSubscriptionAmount(credits.balance, credits.unit)
1767
+ : main?.unlimited ? "∞"
1768
+ : Number.isFinite(main?.usedPercent) ? `${formatSubscriptionAmount(Math.max(0, Math.min(100, 100 - main.usedPercent)))}%` : "—";
1769
+ const label = showCredits ? credits.unit === "USD" ? "Prepaid balance" : "Credits" : "Remaining allowance";
1770
+ return `<div class="quota-summary"><p class="quota-summary-label">${label}</p><div class="quota-summary-value">${value}</div><p class="quota-summary-context">${showCredits ? credits.unlimited ? "Unlimited" : "Available balance" : main ? escapeHtml(subscriptionQuotaLabel(main)) : "Included allowance"}</p></div>`;
1771
+ }
1772
+
1773
+ function renderSubscriptionUsageSection() {
1774
+ const ids = Object.keys(SUBSCRIPTION_PROVIDERS).filter((provider) => state.subscriptionUsage?.[provider] || state.subscriptionUsageErrors?.[provider] || state.subscriptionLoadingProviders?.[provider]);
1775
+ const cards = ids.map((provider) => {
1776
+ const meta = SUBSCRIPTION_PROVIDERS[provider];
1777
+ const usage = state.subscriptionUsage?.[provider];
1778
+ const failed = state.subscriptionUsageErrors?.[provider];
1779
+ const loading = state.subscriptionLoadingProviders?.[provider];
1780
+ const credits = usage?.credits;
1781
+ let balance = "—";
1782
+ if (credits?.unlimited) balance = "Unlimited";
1783
+ else if (Number.isFinite(credits?.balance)) balance = formatSubscriptionAmount(credits.balance, credits.unit);
1784
+ else if (credits?.hasCredits) balance = "Available";
1785
+ let limitMessage = "Usage limit reached";
1786
+ if (usage?.limitReason === "workspace_owner_credits_depleted" || usage?.limitReason === "workspace_member_credits_depleted") limitMessage = "Workspace credits are depleted";
1787
+ if (usage?.limitReason === "workspace_owner_usage_limit_reached" || usage?.limitReason === "workspace_member_usage_limit_reached") limitMessage = "Workspace spending limit reached";
1788
+ const stateName = loading ? "loading" : failed || usage?.limitReached ? "warning" : "ready";
1789
+ const status = loading ? "Loading…" : failed ? "Could not load" : usage?.limitReached ? "Limit reached" : "Available";
1790
+ return `
1791
+ <section class="quota-provider-card" data-provider="${provider}" aria-label="${meta.name} subscription quota">
1792
+ <div class="quota-card-header"><span class="quota-provider-logo"><img src="${meta.logo}" alt="" /></span><div style="min-width: 0; flex: 1;"><h3>${meta.name}</h3><p class="quota-provider-product">${meta.product}</p></div><button class="quota-refresh" type="button" data-quota-refresh="${provider}" aria-label="Refresh ${meta.name} quota" title="Refresh" ${loading ? "disabled" : ""}><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 7v5h-5M4 17v-5h5" /><path d="M6.1 7a7 7 0 0 1 11.6-2L20 8M4 16l2.3 3A7 7 0 0 0 18 17" /></svg></button></div>
1793
+ <div class="quota-card-meta"><span class="quota-plan">${escapeHtml(usage?.planType || meta.product)}</span><span class="quota-status" data-state="${stateName}"><span></span>${status}</span></div>
1794
+ ${loading ? `<div class="quota-skeleton" aria-label="Loading"><div></div><div></div><div></div></div>` : failed ? `<p role="alert" class="quota-card-message">Could not load quota. Refresh to retry.</p>` : `${renderSubscriptionSummary(usage)}<div class="quota-details">${usage.limitReached ? `<p class="quota-notice">${limitMessage}</p>` : ""}${usage.windows.map(renderSubscriptionQuotaWindow).join("")}${credits && usage.windows.length ? `<div class="quota-balance"><span>${credits.unit === "USD" ? "Prepaid balance" : "Credits"}</span><strong>${balance}</strong></div>` : ""}${!usage.windows.length && !credits && !usage.limitReached ? `<p class="quota-window-note">No quota details reported.</p>` : ""}</div>`}
1795
+ <div class="quota-card-footer">Subscription allowance · Refresh anytime</div>
1796
+ </section>
1797
+ `;
1798
+ }).join("");
1799
+ const empty = state.subscriptionPageError || (state.subscriptionsLoaded ? "No subscriptions available. Enable a subscription provider and refresh." : "Refresh to load your subscription allowances.");
1800
+ return `<section id="subscription-quotas" class="subscriptions-page"><div class="subscriptions-page-heading"><div><p class="subscriptions-eyebrow">ACCOUNT USAGE</p><h2>Subscriptions</h2><p class="subscriptions-description">Your subscriptions, balances and reset times in one place.</p></div>${ids.length ? `<span class="subscriptions-count"><span></span>${ids.length} providers</span>` : ""}</div>${cards ? `<div class="subscriptions-grid">${cards}</div>` : `<div class="subscriptions-empty"><h3>Your subscriptions will appear here</h3><p>${escapeHtml(empty)}</p></div>`}</section>`;
1794
1801
  }
1795
1802
 
1796
1803
  function renderTokenUsageSection() {
@@ -2685,111 +2692,72 @@
2685
2692
  <div class="panel text-center py-16 px-4">
2686
2693
  <i data-lucide="info" class="mx-auto h-10 w-10" style="color: var(--color-blue);"></i>
2687
2694
  <h3 class="mt-3 text-lg font-bold" style="color: var(--color-fg-lightest);">Usage data is ready to load</h3>
2688
- <p class="mt-2 text-sm" style="color: var(--color-gray);">Refresh the configured endpoint to view Copilot quotas, token totals, trends, and request events.</p>
2695
+ <p class="mt-2 text-sm" style="color: var(--color-gray);">Refresh the configured endpoint to view token totals, trends, and request events. Open Subscriptions for account allowances.</p>
2689
2696
  </div>
2690
2697
  `;
2691
2698
  }
2692
2699
 
2693
2700
  // --- Data Fetching ---
2694
2701
 
2702
+ async function refreshSubscriptionQuotas(provider) {
2703
+ syncSubscriptionsConnection();
2704
+ try {
2705
+ if (!endpointUrlInput.value.trim()) throw new Error("Endpoint URL cannot be empty.");
2706
+ await fetchSubscriptionQuotas(resolveUsageUrl(endpointUrlInput.value.trim()), provider);
2707
+ } catch (error) {
2708
+ state.subscriptionPageError = getErrorMessage(error);
2709
+ render("subscriptions");
2710
+ }
2711
+ }
2712
+
2713
+ async function fetchSubscriptionQuotas(usageUrl, selectedProvider) {
2714
+ if (selectedProvider && !Object.hasOwn(SUBSCRIPTION_PROVIDERS, selectedProvider)) return;
2715
+ if (!selectedProvider) {
2716
+ state.subscriptionRequestId += 1;
2717
+ state.subscriptionUsage = {};
2718
+ state.subscriptionUsageErrors = {};
2719
+ state.subscriptionLoadingProviders = {};
2720
+ }
2721
+ const requestId = state.subscriptionRequestId;
2722
+ const connectionKey = getConnectionKey();
2723
+ state.subscriptionPageError = null;
2724
+ const providers = selectedProvider ? [selectedProvider] : Object.keys(SUBSCRIPTION_PROVIDERS);
2725
+ await Promise.all(providers.map(async (provider) => {
2726
+ const revision = (state.subscriptionRequests[provider] || 0) + 1;
2727
+ state.subscriptionRequests[provider] = revision;
2728
+ delete state.subscriptionUsage[provider];
2729
+ delete state.subscriptionUsageErrors[provider];
2730
+ state.subscriptionLoadingProviders[provider] = true;
2731
+ state.isSubscriptionsLoading = true;
2732
+ render("subscriptions");
2733
+ const isCurrent = () => requestId === state.subscriptionRequestId
2734
+ && connectionKey === getConnectionKey() && revision === state.subscriptionRequests[provider];
2735
+ try {
2736
+ const usage = await fetchJson(deriveEndpointUrl(usageUrl, `usage/${provider}`));
2737
+ if (!isCurrent()) return;
2738
+ if (usage?.provider !== provider || !Array.isArray(usage.windows)) throw new Error("Invalid subscription quota response");
2739
+ state.subscriptionUsage[provider] = usage;
2740
+ } catch (error) {
2741
+ if (!isCurrent()) return;
2742
+ if (error?.status !== 404) state.subscriptionUsageErrors[provider] = true;
2743
+ } finally {
2744
+ if (isCurrent()) {
2745
+ delete state.subscriptionLoadingProviders[provider];
2746
+ state.isSubscriptionsLoading = Object.keys(state.subscriptionLoadingProviders).length > 0;
2747
+ state.subscriptionsLoaded = true;
2748
+ render("subscriptions");
2749
+ }
2750
+ }
2751
+ }));
2752
+ }
2753
+
2695
2754
  /**
2696
2755
  * Fetches usage data and token usage data from the specified API endpoint.
2697
2756
  */
2698
2757
  async function fetchData(page = 1) {
2699
- invalidateSessionsRequest();
2700
- const endpoint = endpointUrlInput.value.trim();
2701
- if (!endpoint) {
2702
- state.data = null;
2703
- state.tokenUsageDailySummary = null;
2704
- state.tokenUsageSummary = null;
2705
- state.tokenUsageEventsPage = null;
2706
- state.error = "Endpoint URL cannot be empty.";
2707
- state.tokenUsageDailyError =
2708
- "Token usage requires a valid endpoint URL.";
2709
- state.tokenUsageSummaryError =
2710
- "Token usage requires a valid endpoint URL.";
2711
- state.tokenUsageEventsError = null;
2712
- render();
2713
- return;
2714
- }
2715
-
2716
- const period = getSelectedPeriod();
2717
2758
  state.isLoading = true;
2718
- state.error = null;
2719
- state.tokenUsageDailyError = null;
2720
- state.tokenUsageSummaryError = null;
2721
- state.tokenUsageEventsError = null;
2722
- render();
2723
-
2724
2759
  try {
2725
- const usageUrl = resolveUsageUrl(endpoint);
2726
- const [usageResult, summaryResult, dailyResult, eventsResult] =
2727
- await Promise.allSettled([
2728
- fetchJson(usageUrl),
2729
- fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
2730
- fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
2731
- fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
2732
- ]);
2733
-
2734
- if (usageResult.status === "fulfilled") {
2735
- state.data = usageResult.value;
2736
- } else {
2737
- state.data = null;
2738
- state.error = getErrorMessage(usageResult.reason);
2739
- }
2740
-
2741
- if (summaryResult.status === "fulfilled") {
2742
- state.tokenUsageSummary = summaryResult.value;
2743
- void fetchTokenUsageSessionsPage(1);
2744
- } else if (
2745
- isMissingTokenUsageEndpointError(summaryResult.reason)
2746
- ) {
2747
- state.tokenUsageSummary = null;
2748
- state.tokenUsageSummaryError = null;
2749
- } else {
2750
- state.tokenUsageSummary = null;
2751
- state.tokenUsageSummaryError = getErrorMessage(
2752
- summaryResult.reason
2753
- );
2754
- }
2755
-
2756
- if (dailyResult.status === "fulfilled") {
2757
- state.tokenUsageDailySummary = dailyResult.value;
2758
- } else if (
2759
- isMissingTokenUsageEndpointError(dailyResult.reason)
2760
- ) {
2761
- state.tokenUsageDailySummary = null;
2762
- state.tokenUsageDailyError = null;
2763
- } else {
2764
- state.tokenUsageDailySummary = null;
2765
- state.tokenUsageDailyError = getErrorMessage(
2766
- dailyResult.reason
2767
- );
2768
- }
2769
-
2770
- if (eventsResult.status === "fulfilled") {
2771
- state.tokenUsageEventsPage = eventsResult.value;
2772
- } else if (
2773
- isMissingTokenUsageEndpointError(eventsResult.reason)
2774
- ) {
2775
- state.tokenUsageEventsPage = null;
2776
- state.tokenUsageEventsError = null;
2777
- } else {
2778
- state.tokenUsageEventsPage = null;
2779
- state.tokenUsageEventsError = getErrorMessage(
2780
- eventsResult.reason
2781
- );
2782
- }
2783
- } catch (error) {
2784
- console.error("Fetch error:", error);
2785
- state.data = null;
2786
- state.tokenUsageDailySummary = null;
2787
- state.tokenUsageSummary = null;
2788
- state.tokenUsageEventsPage = null;
2789
- state.error = getErrorMessage(error);
2790
- state.tokenUsageDailyError = getErrorMessage(error);
2791
- state.tokenUsageSummaryError = getErrorMessage(error);
2792
- state.tokenUsageEventsError = getErrorMessage(error);
2760
+ await fetchTokenUsageSummaryAndEvents(page);
2793
2761
  } finally {
2794
2762
  state.isLoading = false;
2795
2763
  render("usage");
@@ -2982,6 +2950,8 @@
2982
2950
  syncUrlState();
2983
2951
  if (state.activeTab === "models") {
2984
2952
  void fetchModels();
2953
+ } else if (state.activeTab === "subscriptions") {
2954
+ void refreshSubscriptionQuotas();
2985
2955
  } else {
2986
2956
  void fetchData();
2987
2957
  }
@@ -2996,6 +2966,11 @@
2996
2966
  if (!(event.target instanceof Element)) {
2997
2967
  return;
2998
2968
  }
2969
+ const quotaRefresh = event.target.closest("[data-quota-refresh]");
2970
+ if (quotaRefresh) {
2971
+ void refreshSubscriptionQuotas(quotaRefresh.dataset.quotaRefresh);
2972
+ return;
2973
+ }
2999
2974
  const copyButton = event.target.closest("[data-model-copy]");
3000
2975
  if (copyButton) {
3001
2976
  void copyModelId(copyButton.dataset.modelCopy);
@@ -3129,6 +3104,8 @@
3129
3104
  const endpointFromUrl = urlParams.get("endpoint");
3130
3105
  const initialEndpoint = endpointFromUrl || DEFAULT_ENDPOINT;
3131
3106
 
3107
+ const initialTab = urlParams.get("tab");
3108
+ if (["usage", "subscriptions", "models"].includes(initialTab)) state.activeTab = initialTab;
3132
3109
  state.tokenUsagePeriod = periodFromUrl;
3133
3110
  tokenUsagePeriodSelect.value = periodFromUrl;
3134
3111
  endpointUrlInput.value = initialEndpoint;
@@ -3137,7 +3114,9 @@
3137
3114
  state.collapsedSections = readCollapsedSections();
3138
3115
 
3139
3116
  if (endpointFromUrl) {
3140
- void fetchData();
3117
+ if (state.activeTab === "subscriptions") void refreshSubscriptionQuotas();
3118
+ else if (state.activeTab === "models") void fetchModels();
3119
+ else void fetchData();
3141
3120
  } else {
3142
3121
  render();
3143
3122
  }