diff --git a/apps/desktop-tauri/scripts/check-locale-drift.mjs b/apps/desktop-tauri/scripts/check-locale-drift.mjs index f7dc04ede9..cf5964f606 100644 --- a/apps/desktop-tauri/scripts/check-locale-drift.mjs +++ b/apps/desktop-tauri/scripts/check-locale-drift.mjs @@ -56,10 +56,39 @@ const tsBlockMatch = tsSrc.match( if (!tsBlockMatch) { die(2, "could not locate `export const ALL_LOCALE_KEYS` in keys.ts"); } +// The block holds `"Key"` literals and `...TOPIC_KEYS` spreads of arrays +// imported from i18n/keyGroups/*.ts; resolve both in order. const tsKeys = []; const tsKeyRe = /"(\w+)"/g; -while ((m = tsKeyRe.exec(tsBlockMatch[1])) !== null) { - tsKeys.push(m[1]); +const entryRe = /"(\w+)"|\.\.\.([A-Z_]+)/g; +while ((m = entryRe.exec(tsBlockMatch[1])) !== null) { + if (m[1]) { + tsKeys.push(m[1]); + continue; + } + const name = m[2]; + const importMatch = tsSrc.match( + new RegExp(`import\\s*\\{\\s*${name}\\s*\\}\\s*from\\s*"(\\./[^"]+)"`), + ); + if (!importMatch) { + die(2, `could not resolve the import of ${name} in keys.ts`); + } + let groupSrc; + try { + groupSrc = readFileSync(resolve(dirname(tsPath), `${importMatch[1]}.ts`), "utf8"); + } catch (err) { + die(2, `failed to read ${importMatch[1]}.ts: ${err.message}`); + } + const groupMatch = groupSrc.match( + new RegExp(`export const ${name}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*as const;`), + ); + if (!groupMatch) { + die(2, `could not locate \`export const ${name}\` in ${importMatch[1]}.ts`); + } + let k; + while ((k = tsKeyRe.exec(groupMatch[1])) !== null) { + tsKeys.push(k[1]); + } } if (tsKeys.length === 0) { die(2, "parsed zero entries from ALL_LOCALE_KEYS"); diff --git a/apps/desktop-tauri/src/App.test.tsx b/apps/desktop-tauri/src/App.test.tsx index 05c3355627..53047f7e65 100644 --- a/apps/desktop-tauri/src/App.test.tsx +++ b/apps/desktop-tauri/src/App.test.tsx @@ -59,89 +59,15 @@ vi.mock("./hooks/useSurfaceSnapshot", () => ({ import App from "./App"; import { buildBundle } from "./test/localeHarness"; -import type { BootstrapState, SettingsSnapshot } from "./types/bridge"; +import type { BootstrapState } from "./types/bridge"; +import { makeSettings } from "./test/fixtures"; -function settings(overrides: Partial = {}): SettingsSnapshot { - return { - enabledProviders: ["codex", "claude"], - refreshIntervalSecs: 300, - adaptiveRefresh: false, - refreshAllProvidersOnMenuOpen: false, - lowPowerMode: false, - startAtLogin: false, - startMinimized: false, - showNotifications: true, - soundEnabled: true, - notificationSoundTheme: "windows", - notificationSoundPaths: { - predictiveWarning: null, - highUsage: null, - criticalUsage: null, - exhausted: null, - statusIssue: null, - sessionDepleted: null, - sessionRestored: null, - }, - highUsageThreshold: 70, - criticalUsageThreshold: 90, - predictivePaceWarningEnabled: false, - credentialExpiryNotificationsEnabled: false, - trayIconMode: "single", - switcherShowsIcons: true, - menuBarShowsHighestUsage: false, - menuBarShowsPercent: false, - menuBarColorPace: false, - showAsUsed: true, - showAllTokenAccountsInMenu: false, - enableAnimations: true, - resetTimeRelative: true, - showResetWhenExhausted: false, - menuBarDisplayMode: "detailed", - overviewLayout: "detailed", - hidePersonalInfo: false, - updateChannel: "stable", - autoDownloadUpdates: false, - installUpdatesOnQuit: false, - globalShortcut: "Ctrl+Shift+U", - switcherShortcuts: {}, - codexCustomSessionsDirs: [], - uiLanguage: "english", - // "dark" (not "auto") so useTheme's effect short-circuits before ever - // touching window.matchMedia, which jsdom doesn't implement here. - theme: "dark", - windowScalePercent: 125, - trayScalePercent: 100, - trayPanelAlwaysOnTop: false, - powertoysStatusPipeEnabled: false, - claudeAvoidKeychainPrompts: false, - codexSparkUsageVisible: true, - disableKeychainAccess: false, - providerMetrics: {}, - floatBarEnabled: false, - floatBarOpacity: 80, - floatBarScale: 100, - floatBarOrientation: "horizontal", - floatBarStyle: "floating", - floatBarClickThrough: false, - floatBarProviderIds: [], - floatBarDarkText: false, - floatBarShowResetInline: false, - floatBarShowCost: false, - claudeDailyRoutinesUsageVisible: true, - claudeAllowReadingClaudeCodeCredentials: false, - alibabaTokenPlanRegion: "cn", - weeklyProgressWorkDays: null, - costSummaryDisplayStyle: "compact", - providerAccentColors: {}, - ...overrides, - }; -} function bootstrap(): BootstrapState { return { contractVersion: "v1", providers: [], - settings: settings(), + settings: makeSettings(), }; } @@ -151,7 +77,7 @@ describe("App window-label routing", () => { webviewWindowMocks.label = "main"; surfaceMocks.snapshot = { mode: "hidden", target: { kind: "summary" } }; tauriMocks.getBootstrapState.mockResolvedValue(bootstrap()); - tauriMocks.getSettingsSnapshot.mockResolvedValue(settings()); + tauriMocks.getSettingsSnapshot.mockResolvedValue(makeSettings()); tauriMocks.checkForUpdates.mockResolvedValue({ status: "idle", version: null, diff --git a/apps/desktop-tauri/src/components/AgentSessions.test.tsx b/apps/desktop-tauri/src/components/AgentSessions.test.tsx index 8d22f7391d..9d4940bc5e 100644 --- a/apps/desktop-tauri/src/components/AgentSessions.test.tsx +++ b/apps/desktop-tauri/src/components/AgentSessions.test.tsx @@ -11,9 +11,7 @@ vi.mock("../lib/tauri", () => ({ focusAgentSession: api.focusAgentSession, })); // t(key) returns the key, so provider labels assert against locale-key names. -vi.mock("../hooks/useLocale", () => ({ - useLocale: () => ({ t: (key: string) => key, language: "english" }), -})); +vi.mock("../hooks/useLocale", () => import("../test/mocks/locale")); import AgentSessions from "./AgentSessions"; import type { AgentSession } from "../types/bridge"; diff --git a/apps/desktop-tauri/src/components/ClaudeAccountUsage.test.tsx b/apps/desktop-tauri/src/components/ClaudeAccountUsage.test.tsx index b9ba43486d..f312d24745 100644 --- a/apps/desktop-tauri/src/components/ClaudeAccountUsage.test.tsx +++ b/apps/desktop-tauri/src/components/ClaudeAccountUsage.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import type { ClaudeAccount } from "../types/bridge"; import ClaudeAccountUsage from "./ClaudeAccountUsage"; -vi.mock("../hooks/useLocale", () => ({ useLocale: () => ({ t: (key: string) => key }) })); +vi.mock("../hooks/useLocale", () => import("../test/mocks/locale")); const t = (key: string) => key; const account: ClaudeAccount = { id: "one:org", email: "one@example.test", organization: null, plan: "max", isActive: false, isSaved: true, diff --git a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx index 4906e1524a..5c224e8c07 100644 --- a/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx +++ b/apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx @@ -21,7 +21,7 @@ const mocks = vi.hoisted(() => { }); vi.mock("../lib/tauri", () => mocks); vi.mock("@tauri-apps/api/event", () => ({ listen: mocks.listen })); -vi.mock("../hooks/useLocale", () => ({ useLocale: () => ({ t: (key: string) => key }) })); +vi.mock("../hooks/useLocale", () => import("../test/mocks/locale")); import ClaudeAccountsMenu from "./ClaudeAccountsMenu"; const first: ClaudeAccount = { id: "first:org", email: "first@example.com", organization: "Personal", plan: "max", isActive: true, isSaved: true }; diff --git a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx index 84ab623a3b..b3d22010c7 100644 --- a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx +++ b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx @@ -1,10 +1,6 @@ -import { useCallback, useEffect, useState } from "react"; -import { listen } from "@tauri-apps/api/event"; -import type { - CodexAccount, - CodexAccountsStateBridge, - CodexAccountUsageSnapshot, -} from "../types/bridge"; +import { useEffect, useState } from "react"; +import type { CodexAccount, CodexAccountUsageSnapshot } from "../types/bridge"; +import { useCodexAccountsState } from "../hooks/useCodexAccountsState"; import { useLocale } from "../hooks/useLocale"; import { useFormattedResetTime } from "../hooks/useFormattedResetTime"; import ProviderAccountsMenu from "./ProviderAccountsMenu"; @@ -13,7 +9,6 @@ import { codexAccountAdd, codexAccountReauthenticate, codexAccountSwitch, - getCodexAccountsState, refreshProviders, } from "../lib/tauri"; @@ -32,56 +27,26 @@ export default function CodexAccountsMenu({ onLayoutChange?: () => void; }) { const { t } = useLocale(); - const [accounts, setAccounts] = useState([]); - const [snapshots, setSnapshots] = useState< - Record - >({}); - const [displayNames, setDisplayNames] = useState>({}); - const [accountOrdinals, setAccountOrdinals] = useState>({}); - const [accountNeedsAuthentication, setAccountNeedsAuthentication] = useState>({}); - const [loading, setLoading] = useState(true); + const { + accounts, + snapshots, + displayNames, + accountOrdinals, + needsAuthentication: accountNeedsAuthentication, + loading, + error, + setError, + load, + } = useCodexAccountsState(); const [pending, setPending] = useState(false); const [signingIn, setSigningIn] = useState(false); const busy = loading || pending; - const [error, setError] = useState(null); const [switched, setSwitched] = useState(false); - const load = useCallback(async () => { - setLoading(true); - setError(null); - try { - const next: CodexAccountsStateBridge = await getCodexAccountsState(); - setAccounts(next.accounts); - setDisplayNames(next.displayNames ?? {}); - setAccountOrdinals(next.accountOrdinals); - setSnapshots(next.snapshots); - setAccountNeedsAuthentication(next.needsAuthentication ?? {}); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - void load(); - }, [load]); - useEffect(() => { onLayoutChange?.(); }, [accounts, snapshots, error, pending, switched, onLayoutChange]); - useEffect(() => { - let cancelled = false; - const unlistenPromise = listen("codex-accounts-updated", () => { - if (!cancelled) void load(); - }); - return () => { - cancelled = true; - void unlistenPromise.then((fn) => fn()); - }; - }, [load]); - const run = async (action: () => Promise, login = false) => { setSwitched(false); setSigningIn(login); diff --git a/apps/desktop-tauri/src/components/FormControls.tsx b/apps/desktop-tauri/src/components/FormControls.tsx index 61be029a71..f694ae25b4 100644 --- a/apps/desktop-tauri/src/components/FormControls.tsx +++ b/apps/desktop-tauri/src/components/FormControls.tsx @@ -1,5 +1,6 @@ import type React from "react"; import { createContext, useContext, useId } from "react"; +import type { SettingsSnapshot, SettingsUpdate } from "../types/bridge"; // A Field's label and description ids, so the control inside it gets an // accessible name without every call site passing `ariaLabel`. @@ -154,3 +155,80 @@ export function Field({ ); } + +// ── settings-bound rows ────────────────────────────────────────────── + +export interface SettingsControl { + settings: SettingsSnapshot; + set: (patch: SettingsUpdate) => void; + saving: boolean; +} + +type SettingKey = { + [K in keyof SettingsUpdate & keyof SettingsSnapshot]-?: NonNullable extends T + ? K + : never; +}[keyof SettingsUpdate & keyof SettingsSnapshot]; + +/** A leading Field + Toggle that writes one boolean setting; an unset value reads as off. */ +export function SettingToggle({ + ctl, + field, + label, + description, + checked, + disabled, + ariaLabel, +}: { + ctl: SettingsControl; + field: SettingKey; + label: string; + description?: string; + checked?: boolean; + disabled?: boolean; + ariaLabel?: string; +}) { + return ( + + ctl.set({ [field]: v })} + /> + + ); +} + +/** A Field + Select that writes one string setting; an unset value selects "". */ +export function SettingSelect({ + ctl, + field, + label, + description, + options, + value, + disabled, + ariaLabel, +}: { + ctl: SettingsControl; + field: SettingKey; + label: string; + description?: string; + options: { value: string; label: string }[]; + value?: string; + disabled?: boolean; + ariaLabel?: string; +}) { + return ( + + set({ floatBarOrientation: v as FloatBarOrientation })} - /> - - + - set({ updateChannel: v as UpdateChannel })} - /> - +

{t("UpdateChannelChoiceHelper")}

diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx index 2bf33797de..74907e6e8f 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx @@ -8,83 +8,23 @@ const tauriMocks = vi.hoisted(() => ({ })); vi.mock("../../../lib/tauri", () => tauriMocks); -vi.mock("../../../hooks/useLocale", () => ({ - useLocale: () => ({ t: (key: string) => key }), -})); +vi.mock("../../../hooks/useLocale", () => import("../../../test/mocks/locale")); import AdvancedTab from "./AdvancedTab"; import type { SettingsSnapshot } from "../../../types/bridge"; +import { makeSettings } from "../../../test/fixtures"; -const settings: SettingsSnapshot = { +const settings: SettingsSnapshot = makeSettings({ enabledProviders: [], - refreshIntervalSecs: 300, - adaptiveRefresh: false, - refreshAllProvidersOnMenuOpen: false, - lowPowerMode: false, - startAtLogin: false, - startMinimized: false, - showNotifications: true, - soundEnabled: true, - notificationSoundTheme: "windows", - highUsageThreshold: 70, - criticalUsageThreshold: 90, - predictivePaceWarningEnabled: false, - credentialExpiryNotificationsEnabled: false, - trayIconMode: "single", - switcherShowsIcons: true, menuBarShowsHighestUsage: true, menuBarShowsPercent: true, - menuBarColorPace: false, showAsUsed: false, showAllTokenAccountsInMenu: true, - enableAnimations: true, - resetTimeRelative: true, - showResetWhenExhausted: false, menuBarDisplayMode: "compact", - overviewLayout: "detailed", - notificationSoundPaths: { - predictiveWarning: null, - highUsage: null, - criticalUsage: null, - exhausted: null, - statusIssue: null, - sessionDepleted: null, - sessionRestored: null, - }, - hidePersonalInfo: false, - autoDownloadUpdates: false, - installUpdatesOnQuit: false, globalShortcut: "", - switcherShortcuts: {}, - codexCustomSessionsDirs: [], - updateChannel: "stable", - uiLanguage: "english", - theme: "dark", - windowScalePercent: 125, - trayScalePercent: 100, - trayPanelAlwaysOnTop: false, - powertoysStatusPipeEnabled: false, claudeAvoidKeychainPrompts: true, - codexSparkUsageVisible: true, - disableKeychainAccess: false, - providerMetrics: {}, - floatBarEnabled: false, floatBarOpacity: 0.9, - floatBarScale: 100, - floatBarOrientation: "horizontal", - floatBarStyle: "floating", - floatBarClickThrough: false, - floatBarProviderIds: [], - floatBarDarkText: false, - floatBarShowResetInline: false, - floatBarShowCost: false, - claudeDailyRoutinesUsageVisible: true, - claudeAllowReadingClaudeCodeCredentials: false, - alibabaTokenPlanRegion: "cn", - weeklyProgressWorkDays: null, - costSummaryDisplayStyle: "compact", - providerAccentColors: {}, -}; +}); describe("AdvancedTab", () => { beforeEach(() => { diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx index 88b540e8de..e7c052ce7c 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx @@ -6,7 +6,7 @@ import { unregisterGlobalShortcut, } from "../../../lib/tauri"; import { ShortcutCapture } from "../../../components/ShortcutCapture"; -import { Field, Toggle } from "../../../components/FormControls"; +import { Field, SettingToggle } from "../../../components/FormControls"; import type { TabProps } from "../settingsTabs"; import PreferencesTransferSection from "./PreferencesTransferSection"; @@ -27,6 +27,7 @@ function parseSshHosts(value: string): string[] { export default function AdvancedTab({ settings, set, saving }: TabProps) { const { t } = useLocale(); + const ctl = { settings, set, saving }; const [shortcutError, setShortcutError] = useState(null); const [diagnosticsStatus, setDiagnosticsStatus] = useState( null, @@ -183,28 +184,18 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) {

{t("AgentSessionsTitle")}

- - set({ agentSessionsEnabled: v })} - /> - - + - set({ stayAwakeEnabled: v })} - /> - + />

{t("PrivacyTitle")}

- - set({ hidePersonalInfo: v })} - /> - + />
@@ -249,17 +235,12 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) { {t("SectionLocalIntegrations")}
- - set({ powertoysStatusPipeEnabled: v })} - /> - + />
@@ -268,17 +249,12 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) {

{t("NetworkProxyTitle")}

{t("NetworkProxyCaption")}

- - set({ httpProxyEnabled: v })} - /> - + /> {t("HooksTitle")}

{t("HooksCaption")}

- - set({ hooksEnabled: v })} - /> - + />

{t("HooksConfigPathHint")}

@@ -363,28 +334,19 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) { unavailable; paste Cookie headers manually in Providers.

- - set({ disableKeychainAccess: v })} - /> - - + - set({ claudeAvoidKeychainPrompts: v })} - /> - + disabled={saving || settings.disableKeychainAccess} + />
diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx index 8c57e3a31a..0db49beda8 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx @@ -1,9 +1,7 @@ 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" }), -})); +vi.mock("../../../hooks/useLocale", () => import("../../../test/mocks/locale")); // The FloatBar section pulls in its own bridge dependencies; it is irrelevant // to the display controls under test. vi.mock("../../../floatbar/SettingsSection", () => ({ diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx index 51f2607e42..1e6078f4fe 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx @@ -1,13 +1,7 @@ import { useEffect, useState } from "react"; import { useLocale } from "../../../hooks/useLocale"; -import { Field, Select, Toggle } from "../../../components/FormControls"; -import type { - MenuBarDisplayMode, - OverviewLayout, - ProviderCatalogEntry, - TrayIconMode, - TrayVisibilityStatusDto, -} from "../../../types/bridge"; +import { SettingSelect, SettingToggle } from "../../../components/FormControls"; +import type { ProviderCatalogEntry, TrayVisibilityStatusDto } from "../../../types/bridge"; import type { TabProps } from "../settingsTabs"; import FloatBarSettingsSection from "../../../floatbar/SettingsSection"; import SwitcherShortcutsSection from "../SwitcherShortcutsSection"; @@ -24,6 +18,7 @@ export default function DisplayTab({ providers?: ProviderCatalogEntry[]; }) { const { t } = useLocale(); + const ctl = { settings, set, saving }; const [trayVisibility, setTrayVisibility] = useState(null); useEffect(() => { @@ -45,134 +40,92 @@ export default function DisplayTab({ {mode === "menuBar" &&

{t("MenuBar")}

- - - provider.value !== settings.stackedTrayBottomProvider, - ), - ]} - onChange={(provider) => - set({ stackedTrayTopProvider: provider }) - } - /> - - - - set({ menuBarDisplayMode: v as MenuBarDisplayMode }) - } - /> - - + - set({ promoteTrayIcon: v })} - /> - + : t("PromoteTrayIconUnsupportedHint")} + disabled={saving || trayVisibility?.support !== "supported"} + />
} @@ -180,85 +133,56 @@ export default function DisplayTab({ {mode === "menu" &&

{t("TabMenu")}

- - set({ trayPanelAlwaysOnTop: v })} - /> - - + - set({ showAsUsed: v })} - /> - - + - ({ - value: opt.value, - label: opt.display, - }))} - onChange={(v) => set({ uiLanguage: v as Language })} - /> - - - ({ - value: option.value, - label: t(option.labelKey), - }))} - onChange={(value) => set({ theme: value as ThemePreference })} - /> - + ({ + value: option.value, + label: t(option.labelKey), + }))} + />
} {mode === "general" &&

{t("StartupSettings")}

- - set({ startAtLogin: v })} - /> - - + - set({ startMinimized: v })} - /> - + />
} @@ -339,48 +330,32 @@ export default function GeneralTab({ {t("SectionNotifications")}
- - set({ showNotifications: v })} - /> - - + - set({ predictivePaceWarningEnabled: v })} - /> - - + - set({ credentialExpiryNotificationsEnabled: v })} - /> - - - set({ soundEnabled: v })} - /> - + ariaLabel={t("CredentialExpiryNotifications")} + /> + {settings.soundEnabled && ( <> - - set({ refreshAllProvidersOnMenuOpen: v })} - /> - - + -