Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/release-notes-1.5.24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Highlights

### Your usage bars now show where you *should* be

Ceiling could already tell whether you were on course to run out early. It just wasn't on the thing you actually look at.

Every weekly and monthly bar — in the Overview and in a provider's detail view — now carries a marker for where usage should be at this point in the window. One rule, everywhere: **the marker is where the bar's edge should be right now.**

- Edge sitting at the marker → you're on pace.
- Edge past it → you're ahead of budget, and the overspend fills in as a striped band so you can see *how far*, not just *which side*.
- Bars set to show remaining capacity mirror the marker, so it means the same thing either way.

It's worked out from elapsed time against the window's own length, which means it needs nothing from the provider and shows up on every long window at once, instead of only the single window a pace prediction was calculated for.

Five-hour session bars are deliberately left plain. Nobody spends a session evenly, so a marker there would drift across the bar all afternoon and tell you nothing.

### Cursor on-demand spend is visible where you look

1.5.23 added the dollars behind Cursor's on-demand lane. The main window was filtering that lane out by name, so the number never made it to the screen most people use.

The **On-demand** row now appears in a provider's detail view with its spend beside it — `$0.00 of $1.00`. On-demand is the only Cursor lane that bills real money, which makes it the row worth showing.

## Installers

- **Ceiling-1.5.24-Setup.exe** - standard installer
- **Ceiling-1.5.24-portable.exe** - portable
- **Ceiling-1.5.24-Store-Setup.exe** - Microsoft Store package (WebView2 bundled)

Portable builds show alerts as banners but do not keep them in the notification center. That requires the Start Menu shortcut installed by the standard or Store build.

---

**Full Changelog**: https://github.com/tsouth89/ceiling/compare/v1.5.23...v1.5.24
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [Ceiling] 1.5.24 - 2026-08-05

Puts the pace answer on the usage bars themselves, in the main window.

### Added
- Every weekly and monthly bar in the overview and in a provider's detail view now carries a marker showing where usage *should* be at this point in the window. The rule is the same wherever it appears: the marker is where the bar's edge should be right now. Run past it and the overspend shows as a striped band, so "how far ahead am I" reads as a distance rather than as a line to decode. Bars shown as remaining mirror the marker so it keeps the same meaning either way.
- The marker is derived from elapsed time against the window's own duration, so it needs no provider support and appears on every long window at once, rather than on the single window pace prediction is calculated for.
- Short windows are left alone. A five-hour session is not spent evenly, so a marker there would sweep across the bar and mean nothing; anything under twelve hours is skipped.

### Fixed
- Cursor's on-demand lane now appears in the app window, with the money beside it. It is the only Cursor lane that bills real money, and the detail view was filtering it out by name, so 1.5.23's work to surface overdraft spend could not reach the surface most people actually look at.

## [Ceiling] 1.5.23 - 2026-08-05

Adds a pace verdict and Cursor on-demand spend, both from user feature requests.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop-tauri/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "desktop-tauri",
"private": true,
"version": "1.5.23",
"version": "1.5.24",
"packageManager": "pnpm@10.18.1",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-tauri/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "codexbar-desktop-tauri"
version = "1.5.23"
version = "1.5.24"
edition = "2024"
publish = false

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-tauri/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Ceiling",
"version": "1.5.23",
"version": "1.5.24",
"identifier": "io.github.tsouth89.ceiling",
"build": {
"beforeDevCommand": "pnpm run dev",
Expand Down
44 changes: 38 additions & 6 deletions apps/desktop-tauri/src/components/PlanStatusCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ProviderUsageSnapshot, RateWindowSnapshot } from "../types/bridge"
import { ProviderIcon } from "./providers/ProviderIcon";
import { accountIdentityLabel } from "../lib/providerRow";
import { getProviderIcon } from "./providers/providerIcons";
import { expectedOverlay } from "../lib/expectedPace";
import { useFormattedResetTime } from "../hooks/useFormattedResetTime";
import { useLocale } from "../hooks/useLocale";
import {
Expand Down Expand Up @@ -89,6 +90,8 @@ function MeterRow({
const awaitingReset = snap.isExhausted && showReset;
// Optional setting: promote reset into the hero slot when depleted.
const resetAsHero = awaitingReset && showResetWhenExhausted;
// Where usage should be by now, on long windows only.
const expected = expectedOverlay(snap, showAsUsed);

return (
<div
Expand Down Expand Up @@ -140,12 +143,41 @@ function MeterRow({
</>
)}
</div>
<div className="plan-status-card__bar" aria-hidden>
<div
className="plan-status-card__bar-fill"
data-level={level}
style={{ width: `${barPct}%` }}
/>
<div
className="pace-overlay"
aria-hidden
title={
expected
? t("UsageExpectedByNow").replace(
"{}",
String(Math.round(expected.expectedUsedPercent)),
)
: undefined
}
>
<div className="plan-status-card__bar">
<div
className="plan-status-card__bar-fill"
data-level={level}
style={{ width: `${barPct}%` }}
/>
{expected?.gap && (
<div
className="pace-overlay__gap"
style={{
left: `${expected.gap.left.toFixed(1)}%`,
width: `${expected.gap.width.toFixed(1)}%`,
}}
/>
)}
</div>
{expected && (
<div
className="pace-overlay__tick"
data-ahead={expected.ahead ? "true" : "false"}
style={{ left: `${expected.tickPercent.toFixed(1)}%` }}
/>
)}
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ export const ALL_LOCALE_KEYS = [
"PanelLeftSuffix",
"PanelOnPaceBudget",
"PanelAmountOf",
"UsageExpectedByNow",
"PaceVerdictOnTrack",
"PaceVerdictAhead",
"PaceVerdictPlenty",
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop-tauri/src/lib/capacityPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
PaceSnapshot,
ProviderUsageSnapshot,
RateWindowSnapshot,
WindowAmountBridge,
} from "../types/bridge";

export type CapacityFreshness = "live" | "stale" | "error";
Expand All @@ -10,6 +11,8 @@ export type ConstrainingWindow = {
id: string;
label: string;
window: RateWindowSnapshot;
/** Money behind this lane, when the provider meters it in currency. */
amount?: WindowAmountBridge | null;
};

export type GlanceMeters = {
Expand Down Expand Up @@ -252,12 +255,14 @@ function nonPrimaryWindows(
label: string | null | undefined,
window: RateWindowSnapshot | null | undefined,
fallback: string,
amount?: WindowAmountBridge | null,
) => {
if (!window) return;
out.push({
id,
label: label?.trim() || fallback,
window,
amount,
});
};

Expand All @@ -266,7 +271,7 @@ function nonPrimaryWindows(
push("tertiary", provider.tertiaryLabel, provider.tertiary, "Extra");
for (const extra of provider.extraRateWindows ?? []) {
if (extra.id === "reset-credits") continue;
push(`extra-${extra.id}`, extra.title, extra.window, extra.title);
push(`extra-${extra.id}`, extra.title, extra.window, extra.title, extra.amount);
}
return out;
}
Expand Down
119 changes: 119 additions & 0 deletions apps/desktop-tauri/src/lib/expectedPace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, expect, it } from "vitest";

import type { RateWindowSnapshot } from "../types/bridge";
import { expectedOverlay, expectedUsedPercent } from "./expectedPace";

const NOW = Date.parse("2026-08-05T12:00:00.000Z");
const MINUTE = 60 * 1000;

function win(overrides: Partial<RateWindowSnapshot> = {}): RateWindowSnapshot {
return {
usedPercent: 50,
remainingPercent: 50,
windowMinutes: 10080,
resetsAt: new Date(NOW + 3.5 * 24 * 60 * MINUTE).toISOString(),
resetDescription: null,
isExhausted: false,
reservePercent: null,
reserveDescription: null,
reserveWillLastToReset: false,
reserveEtaSeconds: null,
...overrides,
};
}

describe("expectedUsedPercent", () => {
it("is the elapsed fraction of the window", () => {
// Weekly window resetting in 3.5 days => exactly half elapsed.
expect(expectedUsedPercent(win(), NOW)).toBeCloseTo(50, 5);
});

it("tracks a monthly window", () => {
const monthly = win({
windowMinutes: 30 * 24 * 60,
resetsAt: new Date(NOW + 6 * 24 * 60 * MINUTE).toISOString(),
});
// 24 of 30 days elapsed.
expect(expectedUsedPercent(monthly, NOW)).toBeCloseTo(80, 5);
});

it("stays silent on short windows, where an even burn means nothing", () => {
// A 5-hour session window: nobody paces one linearly.
const session = win({
windowMinutes: 300,
resetsAt: new Date(NOW + 150 * MINUTE).toISOString(),
});
expect(expectedUsedPercent(session, NOW)).toBeNull();
});

it("needs both a duration and a reset to place the marker", () => {
expect(expectedUsedPercent(win({ windowMinutes: null }), NOW)).toBeNull();
expect(expectedUsedPercent(win({ resetsAt: null }), NOW)).toBeNull();
expect(expectedUsedPercent(win({ resetsAt: "not-a-date" }), NOW)).toBeNull();
expect(expectedUsedPercent(win({ windowMinutes: 0 }), NOW)).toBeNull();
});

it("stays silent outside the window rather than pinning to an edge", () => {
// Already reset.
const past = win({ resetsAt: new Date(NOW - MINUTE).toISOString() });
expect(expectedUsedPercent(past, NOW)).toBeNull();
// Reset further out than the window is long: the window has not started.
const future = win({
resetsAt: new Date(NOW + 30 * 24 * 60 * MINUTE).toISOString(),
});
expect(expectedUsedPercent(future, NOW)).toBeNull();
});
});

describe("expectedOverlay", () => {
// 80% of a monthly window elapsed.
const monthly = (usedPercent: number) =>
win({
usedPercent,
remainingPercent: 100 - usedPercent,
windowMinutes: 30 * 24 * 60,
resetsAt: new Date(NOW + 6 * 24 * 60 * MINUTE).toISOString(),
});

it("mirrors the marker when the bar shows remaining capacity", () => {
expect(expectedOverlay(monthly(80), true, NOW)?.tickPercent).toBeCloseTo(80, 5);
expect(expectedOverlay(monthly(80), false, NOW)?.tickPercent).toBeCloseTo(20, 5);
});

it("reads 'ahead' from usage, not from bar geometry", () => {
// Same overspending window in both display modes.
expect(expectedOverlay(monthly(92), true, NOW)?.ahead).toBe(true);
expect(expectedOverlay(monthly(92), false, NOW)?.ahead).toBe(true);
expect(expectedOverlay(monthly(60), true, NOW)?.ahead).toBe(false);
expect(expectedOverlay(monthly(60), false, NOW)?.ahead).toBe(false);
});

it("shades the overspend band in both modes, spanning edge to marker", () => {
// Used 92% against an expected 80%.
const used = expectedOverlay(monthly(92), true, NOW);
expect(used?.gap?.left).toBeCloseTo(80, 5);
expect(used?.gap?.width).toBeCloseTo(12, 5);

// Same window shown as remaining: edge at 8%, marker at 20%.
const left = expectedOverlay(monthly(92), false, NOW);
expect(left?.gap?.left).toBeCloseTo(8, 5);
expect(left?.gap?.width).toBeCloseTo(12, 5);
});

it("draws no band when under budget", () => {
expect(expectedOverlay(monthly(60), true, NOW)?.gap).toBeNull();
});

it("draws no band for a hairline difference", () => {
// Half a percent off pace is not worth a stripe.
expect(expectedOverlay(monthly(80.5), true, NOW)?.gap).toBeNull();
});

it("passes the silence through", () => {
const session = win({
windowMinutes: 300,
resetsAt: new Date(NOW + 150 * MINUTE).toISOString(),
});
expect(expectedOverlay(session, true, NOW)).toBeNull();
});
});
Loading
Loading