---
title: Usage Forecast (predicted rate-limit exhaustion) (part 2)
---

# Usage Forecast (predicted rate-limit exhaustion) (part 2)

## What it is

This is part 2 of the [Usage Forecast (predicted rate-limit exhaustion)](usage-forecast.md) page. It covers how to read each per-account forecast state, the threshold that fires an early warning and when it does and does not fire, the data-freshness line, the four-card Usage Stats page, the optional header widget, and the architecture behind the per-account slope, the pool rollup, the warning dispatcher and the renderer hosts.

## Where to find it

The same surface as the [full page](usage-forecast.md): the **Accounts** popover off the utilization chip in the top-right of the main window, the per-account row in **Settings → Accounts**, the two controls in **Settings → Notifications**, the **Usage** tab under the **Statistics** sidebar group, and the optional header **Usage widget** you add from the header overflow menu or **Settings → Widgets**.

## How it behaves

### How to use it

1. **Where the per-account forecast row appears.** The same projection row is rendered in **two places** so you can read it without leaving your current view:
   - **Top-bar Accounts popover** — click the utilization percentage chip in the top-right of the main window (e.g. `91% v`) to open the floating Accounts panel. The global banner sits at the very top; below it, each OAuth account row shows the `5h`/`7d`/`Extra` mini-bars, and the forecast row sits directly under those bars in the same row card.
   - **Settings → Accounts** — open Settings, scroll to the Accounts section. Same row of text appears just below the colored `5h` usage bar on each OAuth account card. (The global banner is popover-only — it does NOT appear in Settings → Accounts.)

   API-key accounts do not show a forecast row in either location — API keys don't have a 5-hour rate-limit window to project against.

2. **What each per-account forecast state means.**
   - **`Limit reached`** (red) — you are already at or above 100% utilization in this 5-hour bucket. Nothing left until the reset.
   - **`Calculating…`** (muted grey) — Omniscio has fewer than ~10 minutes of signal inside the current window. Slope estimates from short samples are noise, so the forecast holds off.
   - **`won't reach limit before reset`** (muted grey) — at the current burn rate, the bucket will reset (refill) before it ever reaches 100%. You're safe to keep working at the current pace.
   - **`~X.Xh until exhausted, resets in Yh`** (amber) — your standard projection. Burn-rate math says you'll hit 100% at the displayed ETA, and the bucket will reset at the displayed time. Both are rough — they assume your current rate continues. (When the ETA is between 30 minutes and 1 hour, the unit switches to minutes — e.g. `≈45 min until exhausted, resets in 4h`. The "Almost out" prefix only kicks in below 30 minutes.)
   - **`Almost out — ≈X min until exhausted, resets in Yh`** (red) — projected exhaustion is **30 minutes or less** away. The "Almost out" prefix is intentional so urgency isn't communicated by color alone (matters for color-blind users and screen-readers).
   - **`· data N min stale`** (suffix) — your last successful usage poll is more than 30 minutes old, so the projection is using older numbers than usual. Refresh the account or wait for the next poll cycle.
   - **`· unusual rate`** (suffix) — the burn-rate exceeds 80%/hour, which is suspiciously fast. Could be a runaway agent, another machine logged into the same account, or a real intense session. Worth a glance.

3. **Early-warning setting (threshold only; no UI alert today).** Open **Settings → Notifications** and scroll to the bottom:
   - The **Usage forecast** master toggle (default ON) controls whether the inline row AND the global banner appear at all and whether the warning dispatcher runs. Turn it off and you go back to plain percentage bars with no projection text and no global banner.
   - Under the toggle, the **5-hour exhaustion warning** number input (range 0–5 hours, step 0.5, default 0) sets your alert threshold. **0 disables the dispatcher entirely** but the inline forecast row still displays. Set it to (for example) `1` and the dispatcher activates when the projected exhaustion drops below 1 hour inside any 5-hour window.
   - The threshold input is nested under the master toggle and is hidden when the toggle is off (because the dispatcher can't run if the feature is off).

4. **When the warning dispatcher fires (and when it doesn't).** Omniscio's main process emits a `usage-forecast:warning` push event (channel `IPC.USAGE_FORECAST_WARNING`) when **all** of the following are true at once:
   - The forecast state is `projected` (we have enough signal to compute slope).
   - The forecast says `willExhaustBeforeReset = true` (you're on track to hit 100% before the window resets).
   - The projected `hoursUntilExhaustion` is **less than your configured threshold**.
   - The forecast is NOT flagged as `unusualRate` (we suppress alerts when burn looks anomalous to avoid noisy alarms — the inline row still shows the "unusual rate" suffix though).
   - You haven't already been warned for this exact 5-hour window. Dedup is per-account, per-bucket, keyed on the window's `resetsAt` timestamp — a new window unlocks a fresh warning automatically.
   - The Usage forecast master toggle is on AND the threshold is above 0.

   **Important — no renderer consumer today.** The main process fires the push event but the renderer currently has no listener for `usage-forecast:warning`. The event is dispatched into the void — no toast, no sound, no badge is shown when the threshold is crossed. The inline row and global banner are the only user-visible early-warning surfaces for now. A renderer consumer (OS notification or toast) is the planned next step for this feature.

   (Warnings fire from per-account state only. The global banner is informational; it does NOT have its own warning dispatcher.)

5. **How to disable.** Two levels:
   - **Hide everything (rows + banner):** Settings → Notifications → **Usage forecast** toggle → off. The global banner, the inline rows under each account, and the warning dispatcher all go silent.
   - **Silence the dispatcher (rows and banner stay):** Settings → Notifications → **5-hour exhaustion warning** → set to `0`. Rows and banner still display so you can read projections; the dispatcher just doesn't fire.

6. **The "What this banner means" explainer.** A small **"i"** sits beside the banner's verdict line and reveals a plain-English legend that decodes the whole banner: the health check + color, the rolling **5-hour** limit (a window that refills every 5 hours, so hitting it is a brief pause on one account — not running out of Claude), the **weekly** limit (the one that can pause you for days), what **coverage %** means (over 100% is comfortable, under 100% runs tight before reset), and what **"sessions left"** estimates. It exists because the green "healthy" glyph next to a `~2.5h until your 5-hour limit` headline read as a contradiction. It adapts to the device: on a **hover-capable** device the "i" reveals the legend as a tooltip (hover or keyboard-focus); on a **touch** device it is a tap-toggle that expands the legend inline (a hover bubble can't open on a tap). It appears in every state where a verdict renders, and the legend copy is deliberately conceptual (no exact numbers) so it stays accurate even if the tier thresholds change.

### Data freshness ("Updated Xm ago")

The bars and the forecast are only as current as the last successful poll, so the Accounts surface tells you when it was last refreshed — you can trust (or distrust) the numbers at a glance instead of guessing whether a static-looking bar is fresh or frozen:

- The **top-bar popover footer** and **Settings → Accounts** (on the "Login Accounts" header) both render a small **`Updated Xm ago`** line. It's driven by the **oldest** login account's fetch time — the honest worst case for the whole pool, not just the active account — and **re-renders every ~30 seconds** so it visibly ticks up while the panel sits open (a label that never moves itself reads as "frozen").
- It stays calm grey at normal cache age and turns **amber only once that oldest account is more than 30 minutes stale** — the same 30-minute gate that drives the `· data N min stale` banner suffix. Before the first successful fetch it reads `Not checked`.
- The **Refresh** button beside it forces a fresh fetch of every account and spins while the fetch is in flight.

Expect a healthy pool to usually read **`Updated ~10–14m ago`** and tick upward: Omniscio samples each account every ~15 minutes and the renderer re-polls every ~5 minutes behind a ~10-minute cache, so a 10–14-minute age is the _normal_ cadence, not staleness. The amber threshold (30 min) sits deliberately well past that, so amber means something genuinely stalled.

### Usage Stats page

The Accounts popover gives you a one-line forecast, but for the full picture there's a dedicated **Usage Stats** sub-page reached from the Statistics virtual project in the Omniscio sidebar group — once open, the project's middle column shows two tabs at the top (`Overview` and `Usage`); click **Usage** to land on the page. The page is a 2×2 grid of four cards, each a self-contained chart with its own data fetch. Together they answer four different questions about your pool: where is the burn heading, when does it usually peak, which account is closest to capping, and how does the next week look at a glance.

1. **Pool Trajectory (`Last 7 days → Next 7 days`)** — stacked line chart with one row for the 5-hour bucket and one for the weekly bucket. A **solid line** traces the past 7 days of actual pool burn, a **dashed line** continues into the next 7 days as the HOD-walk projection, and a **translucent band** around the dashed segment shows the high/low standard-deviation envelope. A single vertical "now" marker spans both rows. Per-row green-dashed verticals mark account resets; a red-dashed horizontal sits at 100% on each row. A corner toggle highlights one bucket at a time, but both rows always render.
2. **Burn Rate Heat-Map (HOD × DOW)** — two 24×7 grids stacked top-to-bottom (5h on top, weekly below). Each cell's opacity maps to that hour-of-day × day-of-week's mean burn delta. **Outlined-amber cells** flag low-confidence cells with only one sample; **dashed-empty cells** mean no samples at all. Hover any cell for a tooltip with `mean ± std` and `n` samples.
3. **Per-Account Pool Contribution** — one horizontal bar per OAuth login account. Each bar shows the account's **current utilization** as a solid fill, the **projected addition** through the bucket horizon as a striped overlay, and the **100% headroom** as a thin outline. Color tiers: green below 70%, amber 70–95%, red above 95%. Reset times sit as text beside each account label. A toggle picks 5h or 7d — one bucket at a time.
4. **Daily Pool Projection (Next 7 days)** — one vertical bar per day, height = projected weekly-bucket utilization for that day, color-tiered identically to the per-account bars. A green dot sits below any day where one or more accounts reset.

All four cards share a single pool walk (`walkPoolNativeWithTrace`) so the chart you read against the heat-map matches the trajectory line above it — no source-of-truth drift between views. Each card fetches independently, so a single failed fetch leaves the other three intact.

**Audit callouts.** Each card renders an amber inline notice below its chart when its detector fires. **Trajectory** fires when the projected slope would breach 100% inside the bucket's planning horizon AND the recent-window mean burn is more than 20% above the historical rate — signal: "you're burning hotter than usual right now". **Heat-Map** fires when the cell containing "now" shows a ≥30% mismatch between projected and historical burn — signal: "this hour is busier than your usual pattern". **Per-Account** fires when any one account is at ≥90% utilization in either bucket — signal: "one account is about to cap out". **Daily** fires when more than half of the projected 7-day bars exceed 90% AND the headline coverage is below 100% — signal: "the next week looks tight". (Daily currently receives `null` for headline coverage in v1, so it stays dormant pending a v2 follow-up.)

**Cold-start gating.** When fewer than 7 distinct calendar days of usage samples exist inside the 14-day window (`distinctDays < 7`), all four cards collapse simultaneously to a `Learning your patterns — N more days needed` hint. The gate is centralized in the tab component off the heat-map payload's `distinctDays`, so the four cards never go out of sync — either they all show their chart or they all show the cold-start hint.

**Where to find it.** Two entry points open the page: the **AccountIndicator popover** has a `View usage stats →` link row in its footer, directly above the Updated-timestamp + Manage Accounts row; and **Settings → Accounts** has an `Open Usage Stats →` link button inside the Accounts heading block, after the description text. Both navigate to the Statistics virtual project and switch the Statistics tab to `Usage`.

**Where the code lives.**

- The tab host: [/src/renderer/src/features/statistics/usage/UsageTab.tsx](/src/renderer/src/features/statistics/usage/UsageTab.tsx).
- The four chart components: [/src/renderer/src/features/statistics/usage/UsageTrajectoryChart.tsx](/src/renderer/src/features/statistics/usage/UsageTrajectoryChart.tsx), [/src/renderer/src/features/statistics/usage/UsageHeatMap.tsx](/src/renderer/src/features/statistics/usage/UsageHeatMap.tsx), [/src/renderer/src/features/statistics/usage/UsagePerAccountBars.tsx](/src/renderer/src/features/statistics/usage/UsagePerAccountBars.tsx), [/src/renderer/src/features/statistics/usage/UsageDailyProjectionChart.tsx](/src/renderer/src/features/statistics/usage/UsageDailyProjectionChart.tsx).
- The pure audit detectors: [/src/renderer/src/features/statistics/usage/usage-audit.ts](/src/renderer/src/features/statistics/usage/usage-audit.ts).
- The four IPC channels — `IPC.USAGE_FORECAST_GET_TRAJECTORY`, `IPC.USAGE_FORECAST_GET_HOD_CELLS`, `IPC.USAGE_FORECAST_GET_PER_ACCOUNT`, `IPC.USAGE_FORECAST_GET_DAILY_PROJECTION` — and their handlers in [/src/main/ipc/usage-forecast-handlers.ts](/src/main/ipc/usage-forecast-handlers.ts).

### Usage widget (optional header chip)

A third place usage can appear — a compact, always-visible chip in the top header row, for people who want a running read on capacity without opening the Accounts popover or the Stats page. It is **opt-in** and **off by default**: you add it from the header's **⋯ overflow** menu or **Settings → Widgets** (it is a normal header widget, so it shows / hides / reorders exactly like the account pill and the Hardcore indicator — see the header-widget contract for the mechanics).

- **What it shows.** For the pool's **most-constrained account** per window: the **5-hour** usage % and the **weekly** usage %, each with a reset countdown, colored by the same threshold gradient used everywhere else (green < 60 · yellow 60–69 · orange 70–89 · red 90+). "Most-constrained" = the account nearest its limit for that window, so the chip answers "do I have room to start another big task?" honestly for a multi-account pool.
- **Click / hover.** Click opens the full **Stats → Usage** page; the chip's popover shows the full 5-hour + weekly bars, reusing the same `MiniUsageBar` as the Accounts popover.
- **Field options (which fields it shows).** Three toggles in **Settings → Widgets → Usage widget** — `Show 5-hour usage`, `Show weekly usage`, `Show reset countdowns` (all on by default; settings `usageWidgetShowFiveHour` / `usageWidgetShowWeekly` / `usageWidgetShowResets`). Turn any off to slim the chip down to just the fields you care about.
- **No new data.** It reads the same per-account `allUsage` the app already polls (the `ACCOUNT_USAGE_UPDATED` push) — no new IPC, DB, or background work. It is hidden on-camera under the same presentation-mode setting as the account widget (usage numbers are sensitive on a screen share).
- **Discovery (one-time tip).** Because the widget is opt-in and off by default, a **one-time, dismissible tip** surfaces on a later launch to point new users to it: a single informational toast ("add a Usage widget to your header…") with a one-click **Add it** that pins it. It fires at most once per install (localStorage `amc-usage-widget-discovery-tip-shown`), **desktop only**, waits ~30s after the dashboard mounts, never fires over onboarding / a sign-in gate / an open modal (it retries a few minutes later), and never fires if you already pinned the widget yourself. It is the direct sibling of the "press ? to see every shortcut" tip (`useShortcutDiscoveryTip`).
- **Where the code lives.** [/src/renderer/src/components/ui/UsageHeaderWidget.tsx](/src/renderer/src/components/ui/UsageHeaderWidget.tsx) (the widget), [/src/renderer/src/lib/pool-usage-summary.ts](/src/renderer/src/lib/pool-usage-summary.ts) (`selectPoolUsageSummary` — the most-constrained-per-window reducer), the `usage` catalog entry + `HEADER_WIDGET_BLOCK_IDS` in [/src/renderer/src/features/toolbar/toolbar-catalog.ts](/src/renderer/src/features/toolbar/toolbar-catalog.ts), the three toggles in [/src/renderer/src/features/toolbar/ToolbarSettings.tsx](/src/renderer/src/features/toolbar/ToolbarSettings.tsx), and the discovery tip in [/src/renderer/src/hooks/useUsageWidgetDiscoveryTip.ts](/src/renderer/src/hooks/useUsageWidgetDiscoveryTip.ts) (wired in [/src/renderer/src/App.tsx](/src/renderer/src/App.tsx)).

## For agents

### How it works

The per-account forecast math is a pure function in [/src/main/services/usage/usage-forecast.ts](/src/main/services/usage/usage-forecast.ts) — `forecastFiveHour(util, resetsAt, now, lastPolledAt, recentSamples?)` returns a discriminated union with one of five states: `'no-data' | 'no-usage-yet' | 'calculating' | 'projected' | 'exhausted'`. The function uses a **recency-biased slope**: it computes both the whole-window slope (`utilization% / hoursElapsed` from the start of the current 5-hour window to `now`) AND a trailing-window slope from the last 90 minutes of samples (via `recentSlopePctPerHour` in [/src/shared/usage-forecast.ts](/src/shared/usage-forecast.ts)), then takes the **maximum** of the two. This catches the idle-then-burst pattern that flat-extrapolation misses — if you spent the first 3 hours of the window doing almost nothing then ramped to 50%/hour for the last 30 minutes, the whole-window slope underestimates and the headline says "you have 4 hours" when you actually have 30 minutes. Max-of-two means the forecast is never less safe than the legacy whole-window math but is more responsive when burn accelerates. Projected total at reset is `slope × 5h`; exhaustion ETA is `(100 − util) / slope`. The function tags the result with `unusualRate: true` when the slope exceeds 80%/hour and with `staleMinutes` when the most recent poll is older than 30 minutes. Sample windows shorter than 10 minutes return `'calculating'` to avoid noisy projections from too few data points.

**The trailing-window slope helper** (`recentSlopePctPerHour`) takes the last 90 minutes of samples and applies three gates before reporting a slope: (1) **window clamp** — samples older than the current 5-hour window's start are excluded, so a sample from the previous (already-reset) cycle never poisons the slope; (2) **span gate** — the oldest and newest samples in the window must be at least 30 minutes apart, otherwise the slope is too noisy to use; (3) **freshness gate** — the newest sample must be no older than 30 minutes, otherwise we're computing on stale data; (4) **reset detection** — if `fiveHourResetsAt` advanced between the oldest and newest sample, the bucket reset mid-window and the delta is meaningless. When any gate trips, the helper returns `null` and `forecastFiveHour` falls back to whole-window math alone (which is the pre-2026-05 behavior).

The global rollup is a separate pure function in [/src/shared/usage-forecast.ts](/src/shared/usage-forecast.ts) — `forecastGlobalExhaustion(accounts, now, samplesByAccountId?)` returns a `GlobalBucketForecast` discriminated union with five states: `'no-data' | 'no-usage-yet' | 'calculating' | 'all-exhausted' | 'projected'`. It filters to OAuth-only accounts, bails to `'no-data'` if any are missing usage, runs `forecastFiveHour` for each one (passing the per-account samples from `samplesByAccountId.get(accountId)` so each account gets its own recency-biased slope — mixing accounts would smear the slope and is intentionally avoided), then walks the per-account forecasts: any `'calculating'` propagates to a global `'calculating'`; all `'exhausted'` propagates to `'all-exhausted'` with the soonest reset; otherwise it computes `T = max(perAccount.tExhaust)` (the moment the slowest account hits 100%) and verifies every earlier-exhausted account's `resetsAt` is at or after `T`. If yes, `willAllExhaustBeforeAnyReset = false` (the muted-green "Won't run out before reset" branch). If no, the result includes `hoursUntilAllExhausted`, the soonest-reset hour count, and propagated `staleMinutes`/`unusualRate` flags.

The forecast for the active 5-hour bucket is computed **once per cache write** inside `auth-service.cacheAndReturn` (see [/src/main/services/auth/auth-service.ts](/src/main/services/auth/auth-service.ts) — search for `forecastFiveHour`) and is included in the `ACCOUNT_USAGE_UPDATED` push payload. The cache-write site loads the last **6 hours** of samples per-account (covers the 90-minute trailing window with comfortable margin) via `getUsageSamplesForAccountSince(accountId, cutoff)` from [/src/main/db/queries-usage-samples.ts](/src/main/db/queries-usage-samples.ts) and passes them as the 5th argument to `forecastFiveHour`. The DB query is account-scoped on purpose — pulling all-account samples on every cache write would scale linearly with N accounts; the scoped query stays flat. The DB load is wrapped in a try/catch so the boot-race case (auth service initializes before the SQLite connection is ready) degrades gracefully to the legacy whole-window slope rather than throwing. This means the renderer never has to make a separate IPC call — it gets the projection alongside the raw utilization on every poll. There is also a pull channel `IPC.USAGE_FORECAST_GET` (`'usage-forecast:get'` — see [/src/shared/ipc-channels/](/src/shared/ipc-channels/)) that returns `{ fiveHour: BucketForecast }` per account if the renderer needs to fetch on demand. The pull-channel handler at [/src/main/ipc/usage-forecast-handlers.ts](/src/main/ipc/usage-forecast-handlers.ts) loads all-account samples once per call (the 14-day cutoff is shared with the HOD coverage line below the headline so we don't pay two SQLite scans per forecast tick) and groups them via a `Map<accountId, RawSample[]>` before dispatching `forecastFiveHour` per account.

The warning dispatcher lives in [/src/main/services/usage/usage-forecast-notifier.ts](/src/main/services/usage/usage-forecast-notifier.ts) — `createUsageForecastNotifier({ config, emitPush })` returns an object with an `evaluate(accountId, bucket, forecast, resetsAt)` method. It bails out for any of the suppression conditions in the bullet list above, then writes `{accountId}:{bucket} -> resetsAt` into `settings.lastWarnedForecast` after firing. Because the dedup key is the window's `resetsAt`, the next 5-hour window will have a different `resetsAt` value and naturally re-arm without any timer logic. AppSettings fields `usageForecastEnabled` (default `true`) and `usageForecastWarnHoursFiveHour` (default `0`, max `5`, step `0.5`) live in [/src/shared/types.ts](/src/shared/types.ts) and are validated in [/src/shared/ipc-schemas.ts](/src/shared/ipc-schemas.ts).

On the renderer, the standalone per-account inline row was removed — its rendering folded into the global banner [/src/renderer/src/features/settings/GlobalUsageForecastRow.tsx](/src/renderer/src/features/settings/GlobalUsageForecastRow.tsx). That component renders `null` for the `'no-data'` and `'no-usage-yet'` states (no signal worth showing), a muted neutral row for `'exhausted'` and `'calculating'`, and the headline-plus-reset-clause format for `'projected'`. Color is determined by `pickColorClass()` — red when `hoursUntilExhaustion <= 0.5`, amber otherwise (and only when `willExhaustBeforeReset` is true). The settings UI lives in [/src/renderer/src/features/settings/sections/notifications/NotificationSettings.tsx](/src/renderer/src/features/settings/sections/notifications/NotificationSettings.tsx) (master toggle plus nested threshold input), and both controls have entries in [/src/renderer/src/features/settings/settings-search-index.ts](/src/renderer/src/features/settings/settings-search-index.ts) (`usage-forecast-enabled` and `usage-forecast-warn-hours-five-hour`) so they're discoverable via the global Ctrl+K settings search.

The global banner component is [/src/renderer/src/features/settings/GlobalUsageForecastRow.tsx](/src/renderer/src/features/settings/GlobalUsageForecastRow.tsx). It accepts a `GlobalBucketForecast` prop, returns `null` for `'no-data'` and `'no-usage-yet'`, renders a muted-grey "Calculating…" for `'calculating'`, a red "All accounts exhausted — resets in …" for `'all-exhausted'`, a muted-green "Won't run out before reset" when `willAllExhaustBeforeAnyReset` is `false`, and the standard `<duration> until you run out` line otherwise. The 3-tier color rule (`URGENT_HOURS = 0.5`, `WARN_HOURS = 2`) plus the `Almost out — ` / `Running low — ` text prefixes mirror the per-account row's spec so stacked global+account rows feel consistent. The component also fires a one-shot `feature_events` row keyed on the displayed state (telemetry only re-fires on state transitions, not on every numeric tick).

The banner's verdict line carries an on-demand **explainer affordance** (the "What this banner means" legend). `ForecastVerdict` is the shared verdict-row shell all four states render through — it wraps the changing verdict text in the `role="status"` live region and appends `<UsageExplainer>` (the "i") beside it. `UsageExplainer` branches on [`isTouchOnlyDevice()`](/src/renderer/src/lib/platform.ts): a hover-capable device renders `<UsageExplainerHoverTip>` (the shared [`<InfoTooltip>`](/src/renderer/src/components/ui/InfoTooltip.tsx), hover / keyboard-focus), a touch device renders `<UsageExplainerTapPanel>` (a shared [`<IconButton>`](/src/renderer/src/components/ui/IconButton.tsx) toggling a `basis-full` inline panel). The split keeps each variant's hooks unconditional (Rules of Hooks), and both render one shared `<UsageExplainerContent>` legend whose copy lives under the `settings.usageForecast.explainer.*` keys in [en/](/resources/locales/en/) (the catalog is sharded: common.00.json, common.01.json, …). The touch toggle carries `data-ui-anchor="usage-forecast-explainer-toggle"` (co-located in `GlobalUsageForecastRow.ui-anchors.ts`). Locked by the "Explainer affordance" cases in [/tests/unit/components/global-usage-forecast-row.test.tsx](/tests/unit/components/global-usage-forecast-row.test.tsx).

Three host components mount the forecast UI, all gated on `forecastEnabled` (the AppSettings flag):

- **Settings → Accounts** — [/src/renderer/src/features/settings/sections/accounts/AccountSettings.tsx](/src/renderer/src/features/settings/sections/accounts/AccountSettings.tsx) renders the per-account row directly under each OAuth account's mini-bars block. (Settings does NOT show the global banner.)
- **Top-bar Accounts popover** — [/src/renderer/src/components/ui/AccountIndicator.tsx](/src/renderer/src/components/ui/AccountIndicator.tsx) is the host for both the global banner and the per-account rows. It computes the `GlobalBucketForecast` via `useMemo` keyed on `allUsage` (so it doesn't re-aggregate on unrelated re-renders) and renders `<GlobalUsageForecastRow>` above the per-account list. Each per-account row is rendered by [/src/renderer/src/components/ui/account/LoginAccountRow.tsx](/src/renderer/src/components/ui/account/LoginAccountRow.tsx), which reuses the same `<UsageForecastRow>` as Settings.

## Related

This is part 2 of the [Usage Forecast (predicted rate-limit exhaustion)](usage-forecast.md) page — what the feature is, where to read it, and the verdicts and algorithms behind the headline live there.
