Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/permissions/commands.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ commands.allow = [
"get_launch_block_reason",
"get_work_area_rect",
"play_notification_sound",
"send_test_notification",
"open_external_url",
"reanchor_tray_panel",
"quit_app",
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,14 @@ pub fn play_notification_sound() -> Result<(), String> {
Ok(())
}

/// Show a real Windows toast on demand so the user can confirm notifications
/// are delivered on their machine. Runs the full toast pipeline synchronously
/// and surfaces any failure back to the Settings button.
#[tauri::command]
pub fn send_test_notification() -> Result<(), String> {
codexbar::notifications::send_test_notification()
}

/// Reposition the flyout window so its bottom-right corner stays anchored to
/// the system-tray area. Called from the frontend after dynamic resize.
///
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ fn main() {
commands::get_launch_block_reason,
commands::get_work_area_rect,
commands::play_notification_sound,
commands::send_test_notification,
commands::open_external_url,
commands::reanchor_tray_panel,
commands::quit_app,
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop-tauri/src/components/PlanStatusCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useFormattedResetTime } from "../hooks/useFormattedResetTime";
import { useLocale } from "../hooks/useLocale";
import {
capacityFreshness,
codexResetCredits,
glanceMeters,
resetCreditsAvailable,
type ConstrainingWindow,
} from "../lib/capacityPresentation";

Expand Down Expand Up @@ -166,7 +166,7 @@ export default function PlanStatusCard({
const freshness = capacityFreshness(provider);
const planName = displayPlanName(provider.planName, provider.displayName);
const inactiveSummary = inactiveWindowSummary(provider);
const resetCredits = resetCreditsAvailable(provider);
const resetCredits = codexResetCredits(provider);

const className = [
"plan-status-card",
Expand Down Expand Up @@ -205,7 +205,9 @@ export default function PlanStatusCard({
</span>
)}
{resetCredits != null && (
<span className="plan-status-card__reset-credit">
<span
className={`plan-status-card__reset-credit${resetCredits === 0 ? " plan-status-card__reset-credit--empty" : ""}`}
>
↻ {resetCredits} {resetCredits === 1 ? "reset available" : "resets available"}
</span>
)}
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,12 @@ export const ALL_LOCALE_KEYS = [
"ShortcutEmptyPlaceholder",
"NotificationTestSound",
"NotificationTestSoundPlaying",
"NotificationTest",
"NotificationTestHelper",
"NotificationTestButton",
"NotificationTestSending",
"NotificationTestSent",
"NotificationTestFailed",
"TrayIconModeLabel",
"TrayIconModeHelper",
"TrayIconModeSingle",
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop-tauri/src/lib/capacityPresentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
activePromoInclusions,
providerGlanceStatus,
resetCreditsAvailable,
codexResetCredits,
} from "./capacityPresentation";
import type { ProviderUsageSnapshot, RateWindowSnapshot } from "../types/bridge";

Expand Down Expand Up @@ -211,4 +212,23 @@ describe("capacityPresentation", () => {
expect(resetCreditsAvailable(snap)).toBe(1);
expect(glanceMeters(snap).companions).toEqual([]);
});

it("reports a known zero banked-reset count instead of hiding it", () => {
expect(resetCreditsAvailable(provider({ resetCreditsAvailable: 0 }))).toBe(0);
});

it("exposes Codex banked resets only for Codex, including the zero state", () => {
// Codex is the only provider with banked resets, so the persistent
// indicator stays Codex-scoped and shows even when the count is zero.
expect(
codexResetCredits(provider({ providerId: "codex", resetCreditsAvailable: 0 })),
).toBe(0);
expect(
codexResetCredits(provider({ providerId: "codex", resetCreditsAvailable: 3 })),
).toBe(3);
// Another provider reporting the field must never light up the Codex chip.
expect(
codexResetCredits(provider({ providerId: "cursor", resetCreditsAvailable: 2 })),
).toBeNull();
});
});
21 changes: 19 additions & 2 deletions apps/desktop-tauri/src/lib/capacityPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,17 @@ function nonPrimaryWindows(
return out;
}

/** Available provider-reported resets, including legacy cached snapshots. */
/**
* Provider-reported banked resets, including legacy cached snapshots. Returns
* the observed count — `0` is a real reading and stays distinct from `null`,
* which means the provider does not report resets (or the fetch is unknown).
*/
export function resetCreditsAvailable(
provider: ProviderUsageSnapshot,
): number | null {
if (
typeof provider.resetCreditsAvailable === "number" &&
provider.resetCreditsAvailable > 0
provider.resetCreditsAvailable >= 0
) {
return Math.floor(provider.resetCreditsAvailable);
}
Expand All @@ -147,6 +151,19 @@ export function resetCreditsAvailable(
return match ? Number(match[0]) : null;
}

/**
* Codex banked resets for the persistent indicator. Codex is the only provider
* with this concept, so it is shown Codex-only and always (including the `0`
* state) whenever we have a trustworthy reading, keeping the feature visible
* and building trust that Ceiling is tracking it.
*/
export function codexResetCredits(
provider: ProviderUsageSnapshot,
): number | null {
if (provider.providerId !== "codex") return null;
return resetCreditsAvailable(provider);
}

/**
* Every measured rate window for a provider (plan pool + session/weekly/model/
* extra lanes), each carrying its display label. Powers the Activity timeline,
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop-tauri/src/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,11 @@ export function playNotificationSound(): Promise<void> {
return invoke<void>("play_notification_sound");
}

/** Show a real toast so the user can confirm notifications are delivered. */
export function sendTestNotification(): Promise<void> {
return invoke<void>("send_test_notification");
}

export function reanchorTrayPanel(): Promise<void> {
return invoke<void>("reanchor_tray_panel");
}
Expand Down
21 changes: 21 additions & 0 deletions apps/desktop-tauri/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,13 @@ body:has(.taskbar-flyout-frame) #root {
line-height: 14px;
}

/* No banked resets: keep the indicator visible but quiet — nothing to act on. */
.taskbar-flyout__reset-credit--empty {
border-color: rgba(224, 247, 255, 0.18);
background: transparent;
color: rgba(224, 247, 255, 0.6);
}

.taskbar-flyout__provider-status {
margin-top: 4px;
overflow: hidden;
Expand Down Expand Up @@ -4375,6 +4382,13 @@ html:has(.menu-surface--tray) {
line-height: 16px;
}

/* No banked resets: keep the indicator visible but quiet — nothing to act on. */
.plan-status-card__reset-credit--empty {
border-color: color-mix(in srgb, var(--text-secondary) 22%, transparent);
background: transparent;
color: var(--text-tertiary, var(--text-secondary));
}

.plan-status-card__error {
margin: 0;
font-size: 11px;
Expand Down Expand Up @@ -6644,6 +6658,13 @@ body:has(.dashboard-shell) #root {
line-height: 1;
white-space: nowrap;
}

/* No banked resets: keep the indicator visible but quiet — nothing to act on. */
.provider-focus__reset-credit--empty {
border-color: color-mix(in srgb, var(--text-secondary) 22%, transparent);
background: transparent;
color: var(--text-tertiary, var(--text-secondary));
}
.provider-focus__primary {
padding: 0 0 20px;
}
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop-tauri/src/surfaces/ProviderDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useLocale } from "../hooks/useLocale";
import { formatRelativeUpdated } from "../lib/relativeTime";
import { getProviderChartData } from "../lib/tauri";
import { providerSupportsChartData } from "../lib/providerCharts";
import { allMeasuredWindows, resetCreditsAvailable } from "../lib/capacityPresentation";
import { allMeasuredWindows, codexResetCredits } from "../lib/capacityPresentation";

type DetailWindow = {
id: string;
Expand Down Expand Up @@ -229,7 +229,7 @@ export default function ProviderDetailView({
const primaryPercent = Math.round(percentFor(provider.primary, showAsUsed));
const primaryLabel = provider.primaryLabel?.trim() || "Primary";
const planName = displayPlanName(provider.planName);
const resetCredits = resetCreditsAvailable(provider);
const resetCredits = codexResetCredits(provider);
const updated = Number.isNaN(Date.parse(provider.updatedAt))
? provider.updatedAt
: formatRelativeUpdated(Date.parse(provider.updatedAt), t);
Expand All @@ -251,7 +251,9 @@ export default function ProviderDetailView({
<div className="provider-focus__badges">
{planName && <span className="provider-focus__plan">{planName}</span>}
{resetCredits != null && (
<span className="provider-focus__reset-credit">
<span
className={`provider-focus__reset-credit${resetCredits === 0 ? " provider-focus__reset-credit--empty" : ""}`}
>
↻ {resetCredits} {resetCredits === 1 ? "reset available" : "resets available"}
</span>
)}
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop-tauri/src/surfaces/TaskbarFlyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useProviders } from "../hooks/useProviders";
import { useSettings } from "../hooks/useSettings";
import { getProviderIcon } from "../components/providers/providerIcons";
import { orderProviderSnapshots } from "../lib/providerOrder";
import { allMeasuredWindows, resetCreditsAvailable, type ConstrainingWindow } from "../lib/capacityPresentation";
import { allMeasuredWindows, codexResetCredits, type ConstrainingWindow } from "../lib/capacityPresentation";
import {
dismissTrayPanel,
getTaskbarSurfaceColor,
Expand Down Expand Up @@ -112,7 +112,7 @@ function ProviderRow({ provider, showAsUsed, now }: {
);
}
const windows = flyoutWindows(provider);
const resetCredits = resetCreditsAvailable(provider);
const resetCredits = codexResetCredits(provider);
const hiddenWindowCount = Math.max(
0,
allMeasuredWindows(provider).filter(isUtilityWindow).length - windows.length,
Expand All @@ -124,7 +124,9 @@ function ProviderRow({ provider, showAsUsed, now }: {
<div className="taskbar-flyout__provider-topline">
<span className="taskbar-flyout__provider-name">{provider.displayName}</span>
{resetCredits != null && (
<span className="taskbar-flyout__reset-credit">
<span
className={`taskbar-flyout__reset-credit${resetCredits === 0 ? " taskbar-flyout__reset-credit--empty" : ""}`}
>
↻ {resetCredits} {resetCredits === 1 ? "reset ready" : "resets ready"}
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

vi.mock("../../../hooks/useLocale", () => ({
useLocale: () => ({ t: (key: string) => key }),
}));

const { sendTestNotificationMock } = vi.hoisted(() => ({
sendTestNotificationMock: vi.fn(() => Promise.resolve()),
}));

vi.mock("../../../lib/tauri", () => ({
playNotificationSound: vi.fn(() => Promise.resolve()),
sendTestNotification: sendTestNotificationMock,
}));

import GeneralTab from "./GeneralTab";
import type { SettingsSnapshot } from "../../../types/bridge";

Expand Down Expand Up @@ -106,6 +115,42 @@ describe("GeneralTab", () => {
expect(set).toHaveBeenCalledWith({ capacityEventNotificationsEnabled: false });
});

it("sends a real test notification so delivery can be verified", async () => {
sendTestNotificationMock.mockClear();
render(
<GeneralTab
mode="notifications"
settings={settings}
set={vi.fn()}
saving={false}
/>,
);

fireEvent.click(screen.getByRole("button", { name: "NotificationTestButton" }));

expect(sendTestNotificationMock).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(
screen.getByRole("button", { name: "NotificationTestSent" }),
).toBeInTheDocument(),
);
});

it("disables the test notification button when notifications are off", () => {
render(
<GeneralTab
mode="notifications"
settings={{ ...settings, showNotifications: false }}
set={vi.fn()}
saving={false}
/>,
);

expect(
screen.getByRole("button", { name: "NotificationTestButton" }),
).toBeDisabled();
});

it("uses one global usage warning threshold", () => {
const set = vi.fn();
render(
Expand Down
41 changes: 40 additions & 1 deletion apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useCallback, useState } from "react";
import { useLocale } from "../../../hooks/useLocale";
import { playNotificationSound } from "../../../lib/tauri";
import { playNotificationSound, sendTestNotification } from "../../../lib/tauri";
import { Field, NumberInput, Toggle } from "../../../components/FormControls";
import type { TabProps } from "../../Settings";

type TestNotificationStatus = "idle" | "sending" | "sent" | "failed";

export default function GeneralTab({
mode = "general",
settings,
Expand All @@ -12,13 +14,24 @@ export default function GeneralTab({
}: TabProps & { mode?: "general" | "notifications" }) {
const { t } = useLocale();
const [playingSound, setPlayingSound] = useState(false);
const [testStatus, setTestStatus] = useState<TestNotificationStatus>("idle");

const handleTestSound = useCallback(() => {
setPlayingSound(true);
void playNotificationSound().catch(() => {});
window.setTimeout(() => setPlayingSound(false), 1500);
}, []);

const handleTestNotification = useCallback(() => {
setTestStatus("sending");
void sendTestNotification()
.then(() => setTestStatus("sent"))
.catch(() => setTestStatus("failed"))
.finally(() => {
window.setTimeout(() => setTestStatus("idle"), 4000);
});
}, []);

return (
<>
{mode === "general" && <section className="settings-section">
Expand Down Expand Up @@ -73,6 +86,32 @@ export default function GeneralTab({
onChange={(v) => set({ capacityEventNotificationsEnabled: v })}
/>
</Field>
<Field
label={t("NotificationTest")}
description={t("NotificationTestHelper")}
leading
>
<div className="sound-enabled-row">
<button
type="button"
className="shortcut-capture__button shortcut-capture__button--ghost"
disabled={
saving ||
!settings.showNotifications ||
testStatus === "sending"
}
onClick={handleTestNotification}
>
{testStatus === "sending"
? t("NotificationTestSending")
: testStatus === "sent"
? t("NotificationTestSent")
: testStatus === "failed"
? t("NotificationTestFailed")
: t("NotificationTestButton")}
</button>
</div>
</Field>
<Field label={t("SoundEnabled")} description={t("SoundEnabledHelper")} leading>
<div className="sound-enabled-row">
<Toggle
Expand Down
Loading