char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somsubscription: add usage limits for opencode-goaa0b468

main
5.2 KiB167 linesraw
1import { defineRenderers, type HeaderContext, type HeaderView } from "@berry/api/web.ts";
2import * as j from "@char/justin";
3import {
4  isProvider,
5  type Provider,
6  providerNames,
7  type SubscriptionUsage,
8  usageSchema,
9} from "./types.ts";
10import "./web.css";
11
12const usageOf = j.validation.compile(usageSchema);
13
14export function subscriptionHeader(): HeaderView {
15  const button = <button type="button" aria-expanded="false">limits</button> as HTMLButtonElement;
16  const body = <div class="subscription-body" /> as HTMLElement;
17  const panel = (
18    <div class="subscription-panel" role="region" aria-label="Subscription usage" hidden>
19      {body}
20    </div>
21  ) as HTMLElement;
22  const element = <span class="subscription-usage" hidden>{button}{panel}</span> as HTMLElement;
23  let context: HeaderContext;
24  let provider: Provider | null = null;
25  let open = false;
26  let disposed = false;
27  let revision = 0;
28  let pending = false;
29  let timer: ReturnType<typeof setTimeout> | undefined;
30  let result: SubscriptionUsage | null = null;
31  let fetchedAt = 0;
32
33  function draw() {
34    if (result === null) {
35      body.textContent = pending
36        ? "Loading subscription usage…"
37        : "Click to view subscription usage";
38    } else if (result.status !== "ok") {
39      body.textContent = result.message;
40    } else {
41      const heading = <strong>{provider ? providerNames[provider] : ""} subscription</strong>;
42      const rows = result.windows.map((window) => {
43        const reset = window.resetsAt ? new Date(window.resetsAt) : null;
44        const remaining = reset ? Math.max(0, reset.getTime() - Date.now()) : null;
45        const duration = remaining === null
46          ? "Reset time unavailable"
47          : remaining === 0
48          ? "Reset due; refresh pending"
49          : remaining < 3600_000
50          ? `Resets in ${Math.ceil(remaining / 60_000)}m`
51          : remaining < 86400_000
52          ? `Resets in ${Math.ceil(remaining / 3600_000)}h`
53          : `Resets in ${Math.ceil(remaining / 86400_000)}d`;
54        const exact = reset?.toLocaleString(undefined, {
55          month: "short",
56          day: "numeric",
57          hour: "numeric",
58          minute: "2-digit",
59        });
60        const detail = <small>{duration}{exact ? ` · ${exact}` : ""}</small> as HTMLElement;
61        if (reset) detail.title = reset.toLocaleString();
62        return (
63          <div class="subscription-window">
64            <div>
65              {window.label}
66              <span>{window.usedPercent.toFixed(0)}% used</span>
67            </div>
68            <meter
69              min={0}
70              max={100}
71              _also={(meter) => meter.value = Math.min(100, window.usedPercent)}
72              aria-label={`${window.label} used`}
73            />
74            {detail}
75          </div>
76        );
77      });
78      const updated = new Date(result.fetchedAt);
79      body.replaceChildren(heading, ...rows, <small>Updated {updated.toLocaleTimeString()}</small>);
80    }
81  }
82
83  async function refresh() {
84    if (disposed || !open || !provider || pending || document.hidden) return;
85    if (result !== null && Date.now() - fetchedAt < 60_000) {
86      draw();
87      return;
88    }
89    const current = ++revision;
90    pending = true;
91    draw();
92    try {
93      const value = await context.request("usage", { provider });
94      if (disposed || current !== revision) return;
95      result = usageOf(value).value ??
96        { status: "error", message: "Invalid subscription usage response." };
97    } catch {
98      if (disposed || current !== revision) return;
99      result = {
100        status: "error",
101        message: "Could not load subscription usage. Try again shortly.",
102      };
103    }
104    if (disposed || current !== revision) return;
105    pending = false;
106    fetchedAt = Date.now();
107    draw();
108  }
109
110  function setOpen(value: boolean) {
111    open = value && provider !== null && !disposed;
112    panel.hidden = !open;
113    button.setAttribute("aria-expanded", String(open));
114    clearTimeout(timer);
115    if (open) {
116      void refresh();
117      const tick = () => {
118        void refresh();
119        timer = setTimeout(tick, 60_000);
120      };
121      timer = setTimeout(tick, 60_000);
122    }
123  }
124
125  element.addEventListener("focusout", (event) => {
126    if (!element.contains((event as FocusEvent).relatedTarget as Node | null)) {
127      setOpen(false);
128    }
129  });
130  button.addEventListener("click", () => {
131    setOpen(!open);
132  });
133  element.addEventListener("keydown", (event) => {
134    if ((event as KeyboardEvent).key === "Escape") {
135      setOpen(false);
136    }
137  });
138
139  return {
140    element,
141    update(next) {
142      context = next;
143      const chosen = next.model?.provider ?? "";
144      const nextProvider = isProvider(chosen) ? chosen : null;
145      if (nextProvider === provider) return;
146      provider = nextProvider;
147      element.hidden = provider === null;
148      button.setAttribute(
149        "aria-label",
150        `${provider ? providerNames[provider] : ""} subscription limits`,
151      );
152      revision++;
153      pending = false;
154      result = null;
155      fetchedAt = 0;
156      setOpen(false);
157      draw();
158    },
159    dispose() {
160      disposed = true;
161      revision++;
162      clearTimeout(timer);
163    },
164  };
165}
166
167export default defineRenderers({ header: subscriptionHeader });