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
41 changes: 41 additions & 0 deletions apps/desktop-tauri/src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
emit_build_date();
tauri_build::build()
}

/// Expose the UTC build date as `CODEXBAR_BUILD_DATE` (YYYY-MM-DD) for the
/// About pane. `SOURCE_DATE_EPOCH` pins it for reproducible builds.
///
/// `tauri_build` registers its own rerun triggers, so Cargo would otherwise
/// keep this script's first date across later incremental builds. Rerunning
/// when the shell or `codexbar` sources change keeps the date current; those
/// changes recompile this crate anyway.
fn emit_build_date() {
println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=../../../rust/src");
let seconds = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|value| value.trim().parse::<u64>().ok())
.unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_secs())
.unwrap_or(0)
});
let (year, month, day) = civil_from_days(seconds / 86_400);
println!("cargo:rustc-env=CODEXBAR_BUILD_DATE={year:04}-{month:02}-{day:02}");
}

/// Days since 1970-01-01 to a proleptic Gregorian date (Howard Hinnant's algorithm).
fn civil_from_days(days: u64) -> (u64, u64, u64) {
let z = days + 719_468;
let era = z / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + u64::from(month <= 2);
(year, month, day)
}
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/auto_refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ mod tests {
let mut settings = Settings {
enabled_providers: ["codex".to_string()].into_iter().collect(),
refresh_interval_secs: 300,
adaptive_refresh: false,
..Default::default()
};
settings.set_auto_resume_after_quota_reset(ProviderId::Codex, true);
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,7 @@ pub struct SettingsSnapshot {
std::collections::HashMap<String, codexbar::settings::UsageThresholdOverride>,
predictive_pace_warning_enabled: bool,
credential_expiry_notifications_enabled: bool,
session_quota_notifications_enabled: bool,
show_pace: bool,
tray_icon_mode: &'static str,
stacked_tray_top_provider: Option<String>,
Expand Down Expand Up @@ -966,6 +967,7 @@ impl From<Settings> for SettingsSnapshot {
predictive_pace_warning_enabled: settings.predictive_pace_warning_enabled,
credential_expiry_notifications_enabled: settings
.credential_expiry_notifications_enabled,
session_quota_notifications_enabled: settings.session_quota_notifications_enabled,
show_pace: settings.show_pace,
tray_icon_mode: tray_icon_mode_label(settings.tray_icon_mode),
stacked_tray_top_provider: settings.stacked_tray_top_provider,
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ pub struct AppInfoBridge {
pub name: String,
pub version: String,
pub build_number: String,
/// UTC date this binary was built (YYYY-MM-DD). `None` when the build
/// script output was reused from a target dir shared with another branch.
#[serde(skip_serializing_if = "Option::is_none")]
pub build_date: Option<String>,
pub update_channel: String,
pub tagline: String,
}
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct SettingsUpdate {
Option<std::collections::HashMap<String, codexbar::settings::UsageThresholdOverride>>,
pub predictive_pace_warning_enabled: Option<bool>,
pub credential_expiry_notifications_enabled: Option<bool>,
pub session_quota_notifications_enabled: Option<bool>,
pub show_pace: Option<bool>,
pub tray_icon_mode: Option<String>,
pub stacked_tray_top_provider: Option<String>,
Expand Down Expand Up @@ -340,6 +341,9 @@ impl SettingsUpdate {
if let Some(v) = self.credential_expiry_notifications_enabled {
settings.credential_expiry_notifications_enabled = v;
}
if let Some(v) = self.session_quota_notifications_enabled {
settings.session_quota_notifications_enabled = v;
}
if let Some(v) = self.show_pace {
settings.show_pace = v;
}
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/commands/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub fn get_app_info() -> AppInfoBridge {
name: "CodexBar".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
build_number: option_env!("BUILD_NUMBER").unwrap_or("dev").to_string(),
build_date: option_env!("CODEXBAR_BUILD_DATE").map(str::to_string),
update_channel: update_channel_label(settings.update_channel).to_string(),
tagline: "May your tokens never run out—keep agent limits in view.".to_string(),
}
Expand Down
39 changes: 37 additions & 2 deletions apps/desktop-tauri/src/components/FormControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ export function Toggle({
label,
ariaLabel,
disabled,
variant = "checkbox",
}: {
checked: boolean;
onChange: (v: boolean) => void;
label?: string;
ariaLabel?: string;
disabled?: boolean;
/** "switch" draws the macOS-style switch used by the grouped settings panes. */
variant?: "checkbox" | "switch";
}) {
const fieldAria = useFieldAria(ariaLabel);
const input = (
<input
type="checkbox"
className="toggle"
className={variant === "switch" ? "toggle toggle--switch" : "toggle"}
checked={checked}
{...(label ? { "aria-label": ariaLabel } : fieldAria)}
disabled={disabled}
Expand All @@ -56,18 +59,21 @@ export function Select({
disabled,
ariaLabel,
minWidth,
variant = "box",
}: {
value: string;
options: { value: string; label: string }[];
onChange: (v: string) => void;
disabled?: boolean;
ariaLabel?: string;
minWidth?: number;
/** "menu" draws a borderless "Value ⌄" pop-up button like the macOS panes. */
variant?: "box" | "menu";
}) {
const fieldAria = useFieldAria(ariaLabel);
return (
<select
className="select"
className={variant === "menu" ? "select select--menu" : "select"}
style={{ minWidth }}
value={value}
disabled={disabled}
Expand Down Expand Up @@ -154,3 +160,32 @@ export function Field({
</div>
);
}

// ── grouped section (macOS grouped Form look) ───────────────────────

/** A bold section title, a rounded box of rows with dividers, and an optional footer. */
export function SettingsGroup({
title,
footer,
children,
}: {
title?: string;
footer?: React.ReactNode;
children: React.ReactNode;
}) {
const id = useId();
return (
<section
className="settings-group"
aria-labelledby={title ? `${id}-title` : undefined}
>
{title && (
<h3 id={`${id}-title`} className="settings-group__title">
{title}
</h3>
)}
<div className="settings-group__box">{children}</div>
{footer && <div className="settings-group__footer">{footer}</div>}
</section>
);
}
19 changes: 19 additions & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const ALL_LOCALE_KEYS = [
"TabAbout",
"TabShortcuts",
"InterfaceLanguage",
"GeneralLanguageHelper",
"StartupSettings",
"StartAtLogin",
"StartMinimized",
Expand Down Expand Up @@ -227,6 +228,7 @@ export const ALL_LOCALE_KEYS = [
"MenuAbout",
"MenuAboutVersion",
"MenuQuit",
"QuitCodexBar",
"TrayMenuSwitchAccount",
"TrayMenuUsageDashboard",
"TrayMenuStatusPage",
Expand Down Expand Up @@ -579,6 +581,8 @@ export const ALL_LOCALE_KEYS = [
"AgentSessionsLoading",
"AgentSessionsEmpty",
"UpdatesTitle",
"AboutBuilt",
"SectionLinks",
"UpdateChannelChoice",
"UpdateChannelChoiceHelper",
"AutoDownloadUpdates",
Expand All @@ -595,6 +599,7 @@ export const ALL_LOCALE_KEYS = [
"SelectProvider",
"RefreshInterval30Sec",
"RefreshInterval1Min",
"RefreshInterval2Min",
"RefreshInterval5Min",
"RefreshInterval10Min",
"BrowserCookiesTitle",
Expand Down Expand Up @@ -633,7 +638,17 @@ export const ALL_LOCALE_KEYS = [
"UpdateDownloadingMessage",
"TabTokenAccounts",
"SectionRefresh",
"SectionRefreshing",
"RefreshManualFooter",
"SectionNotifications",
"SectionAlerts",
"SessionQuotaNotifications",
"SessionQuotaNotificationsHelper",
"WarningAtPercentUsed",
"CriticalAtPercentUsed",
"ThresholdsCaption",
"ThresholdOverridesTitle",
"ThresholdOverridesCaption",
"SectionUsageThresholds",
"SectionKeyboard",
"SectionUsageRendering",
Expand Down Expand Up @@ -667,6 +682,9 @@ export const ALL_LOCALE_KEYS = [
"PreferencesTransferCaption",
"PreferencesExportButton",
"PreferencesImportButton",
"PreferencesTransferFooter",
"SwitcherShortcutsOpenButton",
"DialogDone",
"PreferencesExportSuccess",
"PreferencesImportSuccess",
"ShortcutRecordButton",
Expand Down Expand Up @@ -735,6 +753,7 @@ export const ALL_LOCALE_KEYS = [
"AvoidKeychainPromptsHelper",
"DisableAllKeychainLabel",
"DisableAllKeychainHelper",
"CredentialAccessFooter",
"SectionTheme",
"ThemeLabel",
"ThemeHelper",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

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

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

function renderDialog(onClose = vi.fn(), set = vi.fn()) {
const opener = document.createElement("button");
document.body.appendChild(opener);
opener.focus();
const view = render(
<SwitcherShortcutsDialog
settings={{ switcherShortcuts: { next: "ctrl+alt+2" } } as unknown as SettingsSnapshot}
set={set as never}
saving={false}
onClose={onClose}
/>,
);
return { ...view, opener, onClose, set };
}

describe("SwitcherShortcutsDialog", () => {
it("is a labelled modal dialog that takes focus and returns it on close", () => {
const { opener, unmount } = renderDialog();
const dialog = screen.getByRole("dialog", { name: "SwitcherShortcutsTitle" });
expect(dialog).toHaveAttribute("aria-modal", "true");
expect(dialog).toHaveFocus();

unmount();
expect(opener).toHaveFocus();
opener.remove();
});

it("keeps Tab and Shift+Tab inside the sheet", () => {
const { onClose, opener } = renderDialog();
const dialog = screen.getByRole("dialog");
const done = screen.getByRole("button", { name: "DialogDone" });
const firstRecord = screen.getByRole("button", {
name: "SwitcherShortcutPrevious: ShortcutRecordButton",
});

// Tab from the last control wraps to the first one.
done.focus();
fireEvent.keyDown(done, { key: "Tab" });
expect(firstRecord).toHaveFocus();

// Shift+Tab from the first control wraps to Done.
fireEvent.keyDown(firstRecord, { key: "Tab", shiftKey: true });
expect(done).toHaveFocus();

// From the sheet itself (focus on open), Shift+Tab lands on Done and Tab on the first control.
dialog.focus();
fireEvent.keyDown(dialog, { key: "Tab", shiftKey: true });
expect(done).toHaveFocus();
dialog.focus();
fireEvent.keyDown(dialog, { key: "Tab" });
expect(firstRecord).toHaveFocus();

// A Tab between two inner controls is left to the browser.
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true });
firstRecord.dispatchEvent(event);
expect(event.defaultPrevented).toBe(false);
expect(onClose).not.toHaveBeenCalled();
opener.remove();
});

it("closes on Escape, on Done and on a backdrop click", () => {
const { onClose, container, opener } = renderDialog();
fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
fireEvent.click(screen.getByRole("button", { name: "DialogDone" }));
fireEvent.mouseDown(container.querySelector(".settings-dialog-backdrop") as Element);
expect(onClose).toHaveBeenCalledTimes(3);
opener.remove();
});

it("does not close when the press starts inside the sheet", () => {
const { onClose, opener } = renderDialog();
fireEvent.mouseDown(screen.getByRole("dialog"));
expect(onClose).not.toHaveBeenCalled();
opener.remove();
});

it("restores defaults through the section's Restore Defaults button", () => {
const { set, opener } = renderDialog();
fireEvent.click(screen.getByRole("button", { name: "SwitcherShortcutReset" }));
expect(set).toHaveBeenCalledWith({ switcherShortcuts: {} });
opener.remove();
});
});
Loading