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
11 changes: 11 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,11 @@ pub(crate) struct AppSettings {
pub(crate) chat_history_scrollback_items: Option<u32>,
#[serde(default, rename = "threadTitleAutogenerationEnabled")]
pub(crate) thread_title_autogeneration_enabled: bool,
#[serde(
default = "default_automatic_app_update_checks_enabled",
rename = "automaticAppUpdateChecksEnabled"
)]
pub(crate) automatic_app_update_checks_enabled: bool,
#[serde(default = "default_ui_font_family", rename = "uiFontFamily")]
pub(crate) ui_font_family: String,
#[serde(default = "default_code_font_family", rename = "codeFontFamily")]
Expand Down Expand Up @@ -710,6 +715,10 @@ fn default_chat_history_scrollback_items() -> Option<u32> {
Some(200)
}

fn default_automatic_app_update_checks_enabled() -> bool {
true
}

fn default_ui_font_family() -> String {
"system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif".to_string()
}
Expand Down Expand Up @@ -1146,6 +1155,7 @@ impl Default for AppSettings {
show_message_file_path: default_show_message_file_path(),
chat_history_scrollback_items: default_chat_history_scrollback_items(),
thread_title_autogeneration_enabled: false,
automatic_app_update_checks_enabled: true,
ui_font_family: default_ui_font_family(),
code_font_family: default_code_font_family(),
code_font_size: default_code_font_size(),
Expand Down Expand Up @@ -1310,6 +1320,7 @@ mod tests {
assert!(settings.show_message_file_path);
assert_eq!(settings.chat_history_scrollback_items, Some(200));
assert!(!settings.thread_title_autogeneration_enabled);
assert!(settings.automatic_app_update_checks_enabled);
assert!(settings.ui_font_family.contains("system-ui"));
assert!(settings.code_font_family.contains("ui-monospace"));
assert_eq!(settings.code_font_size, 11);
Expand Down
15 changes: 15 additions & 0 deletions src/features/app/components/MainApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,8 @@ export default function MainApp() {
handleTestSystemNotification,
} = useUpdaterController({
enabled: updaterEnabled,
autoCheckOnMount:
!appSettingsLoading && appSettings.automaticAppUpdateChecksEnabled,
notificationSoundsEnabled: appSettings.notificationSoundsEnabled,
systemNotificationsEnabled: appSettings.systemNotificationsEnabled,
subagentSystemNotificationsEnabled:
Expand Down Expand Up @@ -1013,6 +1015,18 @@ export default function MainApp() {
[queueSaveSettings, setAppSettings],
);

const handleToggleAutomaticAppUpdateChecks = useCallback(() => {
setAppSettings((current) => {
const nextSettings = {
...current,
automaticAppUpdateChecksEnabled:
!current.automaticAppUpdateChecksEnabled,
};
void queueSaveSettings(nextSettings);
return nextSettings;
});
}, [queueSaveSettings, setAppSettings]);

const openAppIconById = useOpenAppIcons(appSettings.openAppTargets);

const persistProjectCopiesFolder = useCallback(
Expand Down Expand Up @@ -1118,6 +1132,7 @@ export default function MainApp() {
appSettings,
openAppIconById,
queueSaveSettings,
handleToggleAutomaticAppUpdateChecks,
doctor,
codexUpdate,
updateWorkspaceSettings,
Expand Down
3 changes: 3 additions & 0 deletions src/features/app/hooks/useMainAppModals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ type UseMainAppModalsArgs = {
appSettings: AppSettings;
openAppIconById: Record<string, string>;
queueSaveSettings: (next: AppSettings) => Promise<unknown>;
handleToggleAutomaticAppUpdateChecks: () => void;
doctor: (
codexBin: string | null,
codexArgs: string | null,
Expand Down Expand Up @@ -268,6 +269,8 @@ export function useMainAppModals({
onUpdateAppSettings: async (next) => {
await Promise.resolve(settings.queueSaveSettings(next));
},
onToggleAutomaticAppUpdateChecks:
settings.handleToggleAutomaticAppUpdateChecks,
onRunDoctor: settings.doctor,
onRunCodexUpdate: settings.codexUpdate,
onUpdateWorkspaceSettings: async (id, nextSettings) => {
Expand Down
3 changes: 3 additions & 0 deletions src/features/app/hooks/useUpdaterController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { DebugEntry } from "../../../types";

type Params = {
enabled?: boolean;
autoCheckOnMount?: boolean;
notificationSoundsEnabled: boolean;
systemNotificationsEnabled: boolean;
subagentSystemNotificationsEnabled: boolean;
Expand All @@ -24,6 +25,7 @@ type Params = {

export function useUpdaterController({
enabled = true,
autoCheckOnMount = true,
notificationSoundsEnabled,
systemNotificationsEnabled,
subagentSystemNotificationsEnabled,
Expand All @@ -43,6 +45,7 @@ export function useUpdaterController({
dismissPostUpdateNotice,
} = useUpdater({
enabled,
autoCheckOnMount,
onDebug,
});
const isWindowFocused = useWindowFocusState();
Expand Down
81 changes: 81 additions & 0 deletions src/features/settings/components/SettingsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { describe, expect, it, vi } from "vitest";
import type { AppSettings, WorkspaceInfo } from "@/types";
import {
connectWorkspace,
getAppBuildType,
getAgentsSettings,
getConfigModel,
getExperimentalFeatureList,
isMobileRuntime,
getModelList,
listWorkspaces,
} from "@services/tauri";
Expand All @@ -34,22 +36,28 @@ vi.mock("@services/tauri", async () => {
return {
...actual,
connectWorkspace: vi.fn(),
getAppBuildType: vi.fn(),
getModelList: vi.fn(),
getConfigModel: vi.fn(),
getExperimentalFeatureList: vi.fn(),
getAgentsSettings: vi.fn(),
isMobileRuntime: vi.fn(),
listWorkspaces: vi.fn(),
};
});

const connectWorkspaceMock = vi.mocked(connectWorkspace);
const getAppBuildTypeMock = vi.mocked(getAppBuildType);
const getConfigModelMock = vi.mocked(getConfigModel);
const getModelListMock = vi.mocked(getModelList);
const getExperimentalFeatureListMock = vi.mocked(getExperimentalFeatureList);
const getAgentsSettingsMock = vi.mocked(getAgentsSettings);
const isMobileRuntimeMock = vi.mocked(isMobileRuntime);
const listWorkspacesMock = vi.mocked(listWorkspaces);
connectWorkspaceMock.mockResolvedValue(undefined);
getAppBuildTypeMock.mockResolvedValue("release");
getConfigModelMock.mockResolvedValue(null);
isMobileRuntimeMock.mockResolvedValue(false);
listWorkspacesMock.mockResolvedValue([]);
getAgentsSettingsMock.mockResolvedValue({
configPath: "/Users/me/.codex/config.toml",
Expand Down Expand Up @@ -105,6 +113,7 @@ const baseSettings: AppSettings = {
showMessageFilePath: true,
chatHistoryScrollbackItems: 200,
threadTitleAutogenerationEnabled: false,
automaticAppUpdateChecksEnabled: true,
uiFontFamily:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
codeFontFamily:
Expand Down Expand Up @@ -266,6 +275,56 @@ const renderComposerSection = (
return { onUpdateAppSettings };
};

const renderAboutSection = (
options: {
appSettings?: Partial<AppSettings>;
onUpdateAppSettings?: ComponentProps<typeof SettingsView>["onUpdateAppSettings"];
onToggleAutomaticAppUpdateChecks?: ComponentProps<
typeof SettingsView
>["onToggleAutomaticAppUpdateChecks"];
} = {},
) => {
cleanup();
const onUpdateAppSettings =
options.onUpdateAppSettings ?? vi.fn().mockResolvedValue(undefined);
const onToggleAutomaticAppUpdateChecks =
options.onToggleAutomaticAppUpdateChecks ?? vi.fn();
const props: ComponentProps<typeof SettingsView> = {
reduceTransparency: false,
onToggleTransparency: vi.fn(),
appSettings: { ...baseSettings, ...options.appSettings },
openAppIconById: {},
onUpdateAppSettings,
onToggleAutomaticAppUpdateChecks,
workspaceGroups: [],
groupedWorkspaces: [],
ungroupedLabel: "Ungrouped",
onClose: vi.fn(),
onMoveWorkspace: vi.fn(),
onDeleteWorkspace: vi.fn(),
onCreateWorkspaceGroup: vi.fn().mockResolvedValue(null),
onRenameWorkspaceGroup: vi.fn().mockResolvedValue(null),
onMoveWorkspaceGroup: vi.fn().mockResolvedValue(null),
onDeleteWorkspaceGroup: vi.fn().mockResolvedValue(null),
onAssignWorkspaceGroup: vi.fn().mockResolvedValue(null),
onRunDoctor: vi.fn().mockResolvedValue(createDoctorResult()),
onUpdateWorkspaceSettings: vi.fn().mockResolvedValue(undefined),
scaleShortcutTitle: "Scale shortcut",
scaleShortcutText: "Use Command +/-",
onTestNotificationSound: vi.fn(),
onTestSystemNotification: vi.fn(),
dictationModelStatus: null,
onDownloadDictationModel: vi.fn(),
onCancelDictationDownload: vi.fn(),
onRemoveDictationModel: vi.fn(),
};

render(<SettingsView {...props} />);
fireEvent.click(screen.getByRole("button", { name: "About" }));

return { onUpdateAppSettings, onToggleAutomaticAppUpdateChecks };
};

const renderFeaturesSection = (
options: {
appSettings?: Partial<AppSettings>;
Expand Down Expand Up @@ -672,6 +731,28 @@ describe("SettingsView Display", () => {
});
});

describe("SettingsView About", () => {
it("toggles automatic app update checks", async () => {
const onToggleAutomaticAppUpdateChecks = vi.fn();
renderAboutSection({
onToggleAutomaticAppUpdateChecks,
appSettings: { automaticAppUpdateChecksEnabled: false },
});

const row = screen
.getByText("Automatically check for app updates")
.closest(".settings-toggle-row") as HTMLElement | null;
if (!row) {
throw new Error("Expected automatic app update checks row");
}
fireEvent.click(within(row).getByRole("button"));

await waitFor(() => {
expect(onToggleAutomaticAppUpdateChecks).toHaveBeenCalledTimes(1);
});
});
});

describe("SettingsView Environments", () => {
it("saves the setup script for the selected project", async () => {
const onUpdateWorkspaceSettings = vi.fn().mockResolvedValue(undefined);
Expand Down
3 changes: 3 additions & 0 deletions src/features/settings/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type SettingsViewProps = {
appSettings: AppSettings;
openAppIconById: Record<string, string>;
onUpdateAppSettings: (next: AppSettings) => Promise<void>;
onToggleAutomaticAppUpdateChecks?: () => void;
onRunDoctor: (
codexBin: string | null,
codexArgs: string | null,
Expand Down Expand Up @@ -83,6 +84,7 @@ export function SettingsView({
appSettings,
openAppIconById,
onUpdateAppSettings,
onToggleAutomaticAppUpdateChecks,
onRunDoctor,
onRunCodexUpdate,
onUpdateWorkspaceSettings,
Expand Down Expand Up @@ -114,6 +116,7 @@ export function SettingsView({
appSettings,
openAppIconById,
onUpdateAppSettings,
onToggleAutomaticAppUpdateChecks,
onRunDoctor,
onRunCodexUpdate,
onUpdateWorkspaceSettings,
Expand Down
29 changes: 27 additions & 2 deletions src/features/settings/components/sections/SettingsAboutSection.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { useEffect, useState } from "react";
import type { AppSettings } from "@/types";
import {
getAppBuildType,
isMobileRuntime,
type AppBuildType,
} from "@services/tauri";
import { useUpdater } from "@/features/update/hooks/useUpdater";
import { SettingsSection } from "@/features/design-system/components/settings/SettingsPrimitives";
import {
SettingsSection,
SettingsToggleRow,
SettingsToggleSwitch,
} from "@/features/design-system/components/settings/SettingsPrimitives";

type SettingsAboutSectionProps = {
appSettings: AppSettings;
onToggleAutomaticAppUpdateChecks?: () => void;
};

function formatBytes(value: number) {
if (!Number.isFinite(value) || value <= 0) {
Expand All @@ -21,11 +31,15 @@ function formatBytes(value: number) {
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`;
}

export function SettingsAboutSection() {
export function SettingsAboutSection({
appSettings,
onToggleAutomaticAppUpdateChecks,
}: SettingsAboutSectionProps) {
const [appBuildType, setAppBuildType] = useState<AppBuildType | "unknown">("unknown");
const [updaterEnabled, setUpdaterEnabled] = useState(false);
const { state: updaterState, checkForUpdates, startUpdate } = useUpdater({
enabled: updaterEnabled,
autoCheckOnMount: false,
});

useEffect(() => {
Expand Down Expand Up @@ -96,6 +110,17 @@ export function SettingsAboutSection() {
</div>
<div className="settings-field">
<div className="settings-label">App Updates</div>
<SettingsToggleRow
title="Automatically check for app updates"
subtitle="When enabled, CodexMonitor checks for new app versions on launch."
>
<SettingsToggleSwitch
pressed={appSettings.automaticAppUpdateChecksEnabled}
onClick={() => {
onToggleAutomaticAppUpdateChecks?.();
}}
/>
</SettingsToggleRow>
<div className="settings-help">
Currently running version <code>{__APP_VERSION__}</code>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function SettingsSectionContainers({
return <SettingsDisplaySection {...orchestration.displaySectionProps} />;
}
if (activeSection === "about") {
return <SettingsAboutSection />;
return <SettingsAboutSection {...orchestration.aboutSectionProps} />;
}
if (activeSection === "composer") {
return <SettingsComposerSection {...orchestration.composerSectionProps} />;
Expand Down
1 change: 1 addition & 0 deletions src/features/settings/hooks/useAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ function buildDefaultSettings(): AppSettings {
showMessageFilePath: true,
chatHistoryScrollbackItems: CHAT_SCROLLBACK_DEFAULT,
threadTitleAutogenerationEnabled: false,
automaticAppUpdateChecksEnabled: true,
uiFontFamily: DEFAULT_UI_FONT_FAMILY,
codeFontFamily: DEFAULT_CODE_FONT_FAMILY,
codeFontSize: CODE_FONT_SIZE_DEFAULT,
Expand Down
6 changes: 6 additions & 0 deletions src/features/settings/hooks/useSettingsViewOrchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type UseSettingsViewOrchestrationArgs = {
appSettings: AppSettings;
openAppIconById: Record<string, string>;
onUpdateAppSettings: (next: AppSettings) => Promise<void>;
onToggleAutomaticAppUpdateChecks?: () => void;
onRunDoctor: (
codexBin: string | null,
codexArgs: string | null,
Expand Down Expand Up @@ -76,6 +77,7 @@ export function useSettingsViewOrchestration({
appSettings,
openAppIconById,
onUpdateAppSettings,
onToggleAutomaticAppUpdateChecks,
onRunDoctor,
onRunCodexUpdate,
onUpdateWorkspaceSettings,
Expand Down Expand Up @@ -211,6 +213,10 @@ export function useSettingsViewOrchestration({
const agentsSectionProps = useSettingsAgentsSection({ projects });

return {
aboutSectionProps: {
appSettings,
onToggleAutomaticAppUpdateChecks,
},
projectsSectionProps,
environmentsSectionProps,
displaySectionProps,
Expand Down
Loading
Loading