diff --git a/apps/desktop-tauri/scripts/check-locale-drift.mjs b/apps/desktop-tauri/scripts/check-locale-drift.mjs index f7dc04ede96..cf5964f606e 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-tauri/src/shell/flyout_window.rs b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs index 1ca79f05c25..2fec84cae39 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs @@ -425,7 +425,7 @@ mod tests { #[test] fn panel_fill_matches_the_css_panel_background() { - let css = include_str!("../../../src/styles.css"); + let css = include_str!("../../../src/styles/tray-panel.css"); let tauri::utils::config::Color(r, g, b, a) = PANEL_FILL; assert_eq!(a, 0xFF); assert!(css.contains(&format!("--mac-panel-bg: #{r:02x}{g:02x}{b:02x};"))); diff --git a/apps/desktop-tauri/src/App.test.tsx b/apps/desktop-tauri/src/App.test.tsx index 48bb48268af..f3dfe563a32 100644 --- a/apps/desktop-tauri/src/App.test.tsx +++ b/apps/desktop-tauri/src/App.test.tsx @@ -1,3 +1,4 @@ +import type { SettingsSnapshot } from "./types/bridge"; import { render, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -59,7 +60,8 @@ 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 { @@ -151,7 +153,7 @@ function bootstrap(): BootstrapState { return { contractVersion: "v1", providers: [], - settings: settings(), + settings: makeSettings(), }; } @@ -161,7 +163,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 8d22f7391d1..9d4940bc5ed 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 b9ba43486d7..f312d24745f 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/FormControls.tsx b/apps/desktop-tauri/src/components/FormControls.tsx index 8bb4f1e4ca6..c90d49efc6b 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`. @@ -161,6 +162,83 @@ 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 })} - /> - - + -