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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

### Fixed
- **Opening Settings from the floating bar no longer lands on General.** The bar asked for a `menuBar` tab the Settings window does not have, so the window fell back to General instead of Display. It now opens Display, the same tab the dashboard already uses for that action. The lists that name a Settings tab on each side of the bridge are compared in CI so a renamed tab cannot silently send you to General again.
- **Cursor no longer treats missing usage as zero.** An empty `individualUsage` object used to paint a 0% monthly bar and hide a real team pool sitting next to it. Monthly is now marked unavailable when Cursor reports no reading, and an empty individual object falls through to team usage. On-demand stays billed spend; plan and included dollars are labeled **Included** so they are not read as an invoice. A missing Composer tracking database is shown as unavailable, not as no activity.
- **Cursor no longer treats missing usage as zero.** An empty `individualUsage` object used to paint a 0% monthly bar and hide a real team pool sitting next to it. Monthly is now marked unavailable when Cursor reports no reading, and an empty individual object falls through to team usage. Glance surfaces no longer paint a 0% Plan bar when monthly is unavailable — Overview, flyout, detail, floating bar, and the native taskbar tile show the named state instead. On-demand stays billed spend; plan and included dollars are labeled **Included** so they are not read as an invoice. A missing Composer tracking database is shown as unavailable, not as no activity.
- **Leftover English on glance surfaces now goes through locale keys.** Floatbar settings, freshness chips, account-status labels, Charts tab names, and About update copy used hardcoded English. They now use `en-US.ftl` (and zh-CN) so chips no longer show raw `stale` / `error` tokens.
- **Refreshing model prices no longer blanks out the prices you already had.** The models.dev price cache was emptied before the new copy was written, so a second Ceiling process reading during that moment saw nothing, and a crash mid-write threw the cache away for good. Either way token costs quietly disappeared until a network refresh worked. The new copy is now written beside the old one and swapped in, so there is never a moment with no prices on disk.
- **A Claude token refresh no longer replaces a symlinked credentials file.** Claude Code owns `.credentials.json`, and people who manage it with chezmoi or stow, or who share one file between WSL and Windows, keep a symlink at that path. Writing a refreshed token used to drop a fresh private file over the link, so the real file kept the old tokens and the next `chezmoi apply` signed Claude Code out. The refresh now follows the link and keeps the file's own permissions. It also takes a per-file lock on the shared file rather than on each path that points at it. If the app and the command line refresh the same login at the same moment, Claude retires the token the slower one used, so that one now steps aside and picks up the tokens that actually work instead of writing back dead ones and signing Claude Code out. Gemini and Grok already worked this way.
Expand Down
323 changes: 298 additions & 25 deletions apps/desktop-tauri/src-tauri/src/taskbar_widget.rs

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions apps/desktop-tauri/src/components/PlanStatusCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,40 @@ describe("PlanStatusCard", () => {
expect(screen.queryByText("lifted")).toBeNull();
});

/**
* SBS-876: a missing Cursor plan used to paint Plan 0% used (or 100% left)
* and a quiet "Plan Unavailable" row at the same time.
*/
it("shows Unavailable for a missing Cursor plan and does not paint 0% or 100% left", () => {
render(
<PlanStatusCard
provider={provider({
primary: window(0),
primaryLabel: "Plan",
secondary: null,
secondaryLabel: undefined,
extraRateWindows: [],
inactiveRateWindows: [
{
id: "cursor-plan",
title: "Plan",
description: "No usage reported",
state: "unavailable",
},
],
})}
resetTimeRelative
showAsUsed
/>,
);

expect(screen.getByText("unavailable")).toBeInTheDocument();
expect(screen.getByText("Plan")).toBeInTheDocument();
expect(screen.queryByText(/0% used/)).toBeNull();
expect(screen.queryByText(/100% left/)).toBeNull();
expect(screen.queryByText(/0% left/)).toBeNull();
});

it("separates unavailable windows from not-enforced ones", () => {
render(
<PlanStatusCard
Expand Down
16 changes: 9 additions & 7 deletions apps/desktop-tauri/src/components/PlanStatusCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,15 @@ export default function PlanStatusCard({
<p className="plan-status-card__error">{provider.error}</p>
) : (
<div className="plan-status-card__meters">
<MeterRow
meter={meters.primary}
showAsUsed={showAsUsed}
resetTimeRelative={resetTimeRelative}
showResetWhenExhausted={showResetWhenExhausted}
hero
/>
{meters.primary && (
<MeterRow
meter={meters.primary}
showAsUsed={showAsUsed}
resetTimeRelative={resetTimeRelative}
showResetWhenExhausted={showResetWhenExhausted}
hero
/>
)}
{meters.companions.map((meter) => (
<MeterRow
key={meter.id}
Expand Down
15 changes: 9 additions & 6 deletions apps/desktop-tauri/src/components/ProviderGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ export default function ProviderGrid({
if (expanded === undefined) setUncontrolledExpanded(next);
onExpandedChange?.(next);
};
const gridPercent = (provider: ProviderUsageSnapshot) => {
const constraining = constrainingWindow(provider).window;
const gridPercent = (provider: ProviderUsageSnapshot): number | null => {
const constraining = constrainingWindow(provider);
// Named-state windows are not a 0/100 bar (SBS-876 / CEILING_UI.md).
if (constraining.namedState) return null;
const pct = showAsUsed
? constraining.usedPercent
: constraining.remainingPercent;
? constraining.window.usedPercent
: constraining.window.remainingPercent;
return Math.max(0, Math.min(100, pct));
};
const totalItems = providers.length + 1;
Expand Down Expand Up @@ -106,6 +108,7 @@ export default function ProviderGrid({
{visibleProviders.map((p) => {
const status = providerGlanceStatus(p);
const brand = getProviderIcon(p.providerId).brandColor;
const percent = gridPercent(p);
return (
<button
key={p.providerId}
Expand Down Expand Up @@ -164,12 +167,12 @@ export default function ProviderGrid({
</span>
)}
<span className="provider-grid__label">{labelFor(p.displayName)}</span>
{!p.error && (
{!p.error && percent != null && (
<span
className="provider-grid__weekly-track"
style={
{
"--weekly-pct": `${gridPercent(p)}%`,
"--weekly-pct": `${percent}%`,
"--weekly-color": brand,
} as CSSProperties
}
Expand Down
50 changes: 50 additions & 0 deletions apps/desktop-tauri/src/floatbar/FloatBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,56 @@ describe("FloatBar", () => {
expect(container.querySelector(".floatbar__pct--calm")).toHaveTextContent("60%");
});

/**
* SBS-876: exact mode already dropped the pace and reset for a placeholder
* window. Calm still built both from the required 0% primary, so the pill
* read "On pace · resets in 5d" for a plan with no reading at all.
*/
it("calm mode names the state instead of pacing an unavailable plan", async () => {
const unavailable: ProviderUsageSnapshot = {
...snapshot("cursor", "Cursor", 0, {
resetsAt: new Date(Date.now() + 5 * 24 * 3600_000).toISOString(),
}),
updatedAt: new Date().toISOString(),
inactiveRateWindows: [
{
id: "cursor-plan",
title: "Plan",
description: "No usage reported",
state: "unavailable",
},
],
pace: {
windowLabel: "Plan",
stage: "far_ahead",
deltaPercent: -38.6,
willLastToReset: true,
etaSeconds: null,
expectedUsedPercent: 38.6,
actualUsedPercent: 0,
},
};
tauriMocks.getCachedProviders.mockResolvedValue([unavailable]);
tauriMocks.getSettingsSnapshot.mockResolvedValue(
settings({ floatBarInformationMode: "calm", enabledProviders: ["cursor"] }),
);

const { container } = renderFloatBar(
bootstrap({ floatBarInformationMode: "calm", enabledProviders: ["cursor"] }),
);
await waitFor(() => {
expect(container.querySelector(".floatbar__pill--calm")).toBeInTheDocument();
});

expect(container.querySelector(".floatbar__pct--calm")).toHaveTextContent(
"Unavailable",
);
expect(container.querySelector(".floatbar__pace")).toBeNull();
expect(container.querySelector(".floatbar__calm-reset")).toBeNull();
expect(container.querySelector(".floatbar__pill--calm")?.getAttribute("title"))
.not.toContain("0%");
});

it("does not render hypothetical local costs from the legacy setting", async () => {
tauriMocks.getCachedProviders.mockResolvedValue([
snapshot("codex", "Codex", 75),
Expand Down
70 changes: 50 additions & 20 deletions apps/desktop-tauri/src/floatbar/FloatBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ function floatBarWindow(provider: ProviderUsageSnapshot): ConstrainingWindow {
return constrainingWindow(provider);
}

/** Named-state placeholders are not heat (SBS-876). */
function floatBarHeat(provider: ProviderUsageSnapshot): number {
const hero = floatBarWindow(provider);
return hero.namedState ? -1 : hero.window.usedPercent;
}

/**
* The capacity pill shown for a single provider.
*
Expand Down Expand Up @@ -172,37 +178,55 @@ function ProviderPill({
Math.min(100, hero.window.remainingPercent),
);
const exhausted =
hero.window.isExhausted || !!provider.error;
!hero.namedState && (hero.window.isExhausted || !!provider.error);
let tone: "ok" | "warn" | "crit" = "ok";
if (exhausted || pressureRemaining <= critRemaining) tone = "crit";
else if (pressureRemaining <= highRemaining) tone = "warn";
if (hero.namedState === "unavailable") {
// Caution/amber: the window was tracked but dropped out (CEILING_UI.md).
// Do not treat remainingPercent 100 as healthy (SBS-876).
tone = "warn";
} else if (exhausted || pressureRemaining <= critRemaining) {
tone = "crit";
} else if (pressureRemaining <= highRemaining) {
tone = "warn";
}

const brand = getProviderIcon(provider.providerId).brandColor;
// A lane billed in currency leads with the money. "62%" of a spend cap is not
// the number you act on — the amount owed is (SBS-191).
const spend = hero.amount ? stripAmountLabel(hero.amount, showAsUsed) : null;
const label = provider.error
? "—"
: (spend ?? `${Math.round(displayPercent)}%`);
const resetText = useFormattedResetTime(
hero.window.resetsAt,
hero.window.resetDescription,
resetRelative,
);
const inlineReset = resetText ? inlineResetTime(resetText) : null;
const { t } = useLocale();
const namedLabel =
hero.namedState === "unavailable"
? t("WindowUnavailable")
: hero.namedState === "notEnforced"
? t("NotCurrentlyEnforced")
: null;
const label = provider.error
? "—"
: (namedLabel ?? spend ?? `${Math.round(displayPercent)}%`);
const iconSize = Math.round(14 * scale);
const resetIconSize = Math.round(10 * scale);
const stateChip = freshnessChipLabel(freshness, t);
const boostTitle = boosts[0]?.title ?? null;
// Strip always shows reset when depleted; otherwise honor the setting.
const showReset = !!inlineReset && (showResetInline || exhausted);
// Named-state windows are not a quota, so do not promote a billing-cycle
// reset as if the placeholder 0% had just run out (SBS-876).
const showReset =
!hero.namedState && !!inlineReset && (showResetInline || exhausted);
const titleBits = [
`${provider.displayName}: ${label} ${displaySuffix}`,
hero.namedState
? `${provider.displayName}: ${label}`
: `${provider.displayName}: ${label} ${displaySuffix}`,
hero.label,
boostTitle ? `promo ${boostTitle}` : null,
stateChip,
resetText,
hero.namedState ? null : resetText,
]
.filter(Boolean)
.join("\n");
Expand All @@ -221,13 +245,20 @@ function ProviderPill({
// stays one keyboard/click away. The pill becomes an expandable button, so it
// opts out of the native drag region (drag the bar by its handle instead).
if (informationMode === "calm") {
const calm = calmPresentation(provider, hero);
// A named-state window is not a quota. Its pace and billing reset are both
// derived from the placeholder 0%, so calm drops them and shows the same
// named label exact mode does (SBS-876).
const calm = hero.namedState
? { pace: null, hasReset: false }
: calmPresentation(provider, hero);
// Compact hides the window·reset row, so calm degrades to pace-or-exact.
// Otherwise show exact only when there is no pace and no reset time to show.
// Either way the pill is never blank.
const showResetRow = !isCompact && calm.hasReset;
const showExact =
expanded || (isCompact ? !calm.pace : !calm.pace && !inlineReset);
!!hero.namedState ||
expanded ||
(isCompact ? !calm.pace : !calm.pace && !inlineReset);
const onKeyToggle = (event: ReactKeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
Expand All @@ -242,7 +273,11 @@ function ProviderPill({
showResetRow
? `${hero.label}${resetText ? ` ${resetText}` : ""}`
: null,
showExact ? `${label} ${displaySuffix}` : null,
showExact
? hero.namedState
? label
: `${label} ${displaySuffix}`
: null,
stateChip,
]
.filter(Boolean)
Expand Down Expand Up @@ -589,8 +624,7 @@ export default function FloatBar({ state }: { state: BootstrapState }) {
}
return [...group].sort((a, b) => {
const delta =
floatBarWindow(b).window.usedPercent -
floatBarWindow(a).window.usedPercent;
floatBarHeat(b) - floatBarHeat(a);
if (delta !== 0) return delta;
// Lowest account id, matching the native strip and the flyout so all
// three name the same seat when two accounts read the same pressure.
Expand All @@ -605,19 +639,15 @@ export default function FloatBar({ state }: { state: BootstrapState }) {
: [...byProvider.keys()]
.map((id) => pick(id))
.filter((p): p is (typeof eligible)[number] => p !== undefined)
.sort(
(a, b) =>
floatBarWindow(b).window.usedPercent -
floatBarWindow(a).window.usedPercent,
);
.sort((a, b) => floatBarHeat(b) - floatBarHeat(a));
const allEligible = [...byProvider.keys()]
.map((id) => pick(id))
.filter((p): p is (typeof eligible)[number] => p !== undefined);
return selectVisibleFloatBarProviders(pinned, allEligible, {
mode: selectionMode,
detectionEnabled: settings.floatBarForegroundDetection,
lastActiveProviderId,
usedPercent: (row) => floatBarWindow(row).window.usedPercent,
usedPercent: (row) => floatBarHeat(row),
highUsageThreshold: settings.highUsageThreshold,
});
}, [
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,8 @@ export const ALL_LOCALE_KEYS = [
"FreshnessError",
"NotCurrentlyEnforced",
"WindowUnavailable",
"StripStateUnavailable",
"StripStateNotEnforced",
"TaskbarUsageTitle",
"ShowTaskbarUsage",
"ShowTaskbarUsageHelp",
Expand Down
Loading
Loading