Skip to content

Commit 6f69375

Browse files
ReekinDimillian
andauthored
feat: add automatic app update check setting (#557)
Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
1 parent 898bc1f commit 6f69375

13 files changed

Lines changed: 182 additions & 6 deletions

File tree

src-tauri/src/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,11 @@ pub(crate) struct AppSettings {
506506
pub(crate) chat_history_scrollback_items: Option<u32>,
507507
#[serde(default, rename = "threadTitleAutogenerationEnabled")]
508508
pub(crate) thread_title_autogeneration_enabled: bool,
509+
#[serde(
510+
default = "default_automatic_app_update_checks_enabled",
511+
rename = "automaticAppUpdateChecksEnabled"
512+
)]
513+
pub(crate) automatic_app_update_checks_enabled: bool,
509514
#[serde(default = "default_ui_font_family", rename = "uiFontFamily")]
510515
pub(crate) ui_font_family: String,
511516
#[serde(default = "default_code_font_family", rename = "codeFontFamily")]
@@ -714,6 +719,10 @@ fn default_chat_history_scrollback_items() -> Option<u32> {
714719
Some(200)
715720
}
716721

722+
fn default_automatic_app_update_checks_enabled() -> bool {
723+
true
724+
}
725+
717726
fn default_ui_font_family() -> String {
718727
"system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif".to_string()
719728
}
@@ -1150,6 +1159,7 @@ impl Default for AppSettings {
11501159
show_message_file_path: default_show_message_file_path(),
11511160
chat_history_scrollback_items: default_chat_history_scrollback_items(),
11521161
thread_title_autogeneration_enabled: false,
1162+
automatic_app_update_checks_enabled: true,
11531163
ui_font_family: default_ui_font_family(),
11541164
code_font_family: default_code_font_family(),
11551165
code_font_size: default_code_font_size(),
@@ -1315,6 +1325,7 @@ mod tests {
13151325
assert!(settings.show_message_file_path);
13161326
assert_eq!(settings.chat_history_scrollback_items, Some(200));
13171327
assert!(!settings.thread_title_autogeneration_enabled);
1328+
assert!(settings.automatic_app_update_checks_enabled);
13181329
assert!(settings.ui_font_family.contains("system-ui"));
13191330
assert!(settings.code_font_family.contains("ui-monospace"));
13201331
assert_eq!(settings.code_font_size, 11);

src/features/app/components/MainApp.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,8 @@ export default function MainApp() {
632632
handleTestSystemNotification,
633633
} = useUpdaterController({
634634
enabled: updaterEnabled,
635+
autoCheckOnMount:
636+
!appSettingsLoading && appSettings.automaticAppUpdateChecksEnabled,
635637
notificationSoundsEnabled: appSettings.notificationSoundsEnabled,
636638
systemNotificationsEnabled: appSettings.systemNotificationsEnabled,
637639
subagentSystemNotificationsEnabled:
@@ -1013,6 +1015,18 @@ export default function MainApp() {
10131015
[queueSaveSettings, setAppSettings],
10141016
);
10151017

1018+
const handleToggleAutomaticAppUpdateChecks = useCallback(() => {
1019+
setAppSettings((current) => {
1020+
const nextSettings = {
1021+
...current,
1022+
automaticAppUpdateChecksEnabled:
1023+
!current.automaticAppUpdateChecksEnabled,
1024+
};
1025+
void queueSaveSettings(nextSettings);
1026+
return nextSettings;
1027+
});
1028+
}, [queueSaveSettings, setAppSettings]);
1029+
10161030
const openAppIconById = useOpenAppIcons(appSettings.openAppTargets);
10171031

10181032
const persistProjectCopiesFolder = useCallback(
@@ -1118,6 +1132,7 @@ export default function MainApp() {
11181132
appSettings,
11191133
openAppIconById,
11201134
queueSaveSettings,
1135+
handleToggleAutomaticAppUpdateChecks,
11211136
doctor,
11221137
codexUpdate,
11231138
updateWorkspaceSettings,

src/features/app/hooks/useMainAppModals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ type UseMainAppModalsArgs = {
107107
appSettings: AppSettings;
108108
openAppIconById: Record<string, string>;
109109
queueSaveSettings: (next: AppSettings) => Promise<unknown>;
110+
handleToggleAutomaticAppUpdateChecks: () => void;
110111
doctor: (
111112
codexBin: string | null,
112113
codexArgs: string | null,
@@ -268,6 +269,8 @@ export function useMainAppModals({
268269
onUpdateAppSettings: async (next) => {
269270
await Promise.resolve(settings.queueSaveSettings(next));
270271
},
272+
onToggleAutomaticAppUpdateChecks:
273+
settings.handleToggleAutomaticAppUpdateChecks,
271274
onRunDoctor: settings.doctor,
272275
onRunCodexUpdate: settings.codexUpdate,
273276
onUpdateWorkspaceSettings: async (id, nextSettings) => {

src/features/app/hooks/useUpdaterController.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { DebugEntry } from "../../../types";
1111

1212
type Params = {
1313
enabled?: boolean;
14+
autoCheckOnMount?: boolean;
1415
notificationSoundsEnabled: boolean;
1516
systemNotificationsEnabled: boolean;
1617
subagentSystemNotificationsEnabled: boolean;
@@ -24,6 +25,7 @@ type Params = {
2425

2526
export function useUpdaterController({
2627
enabled = true,
28+
autoCheckOnMount = true,
2729
notificationSoundsEnabled,
2830
systemNotificationsEnabled,
2931
subagentSystemNotificationsEnabled,
@@ -43,6 +45,7 @@ export function useUpdaterController({
4345
dismissPostUpdateNotice,
4446
} = useUpdater({
4547
enabled,
48+
autoCheckOnMount,
4649
onDebug,
4750
});
4851
const isWindowFocused = useWindowFocusState();

src/features/settings/components/SettingsView.test.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { describe, expect, it, vi } from "vitest";
1313
import type { AppSettings, WorkspaceInfo } from "@/types";
1414
import {
1515
connectWorkspace,
16+
getAppBuildType,
1617
getAgentsSettings,
1718
getConfigModel,
1819
getExperimentalFeatureList,
20+
isMobileRuntime,
1921
getModelList,
2022
listWorkspaces,
2123
} from "@services/tauri";
@@ -34,22 +36,28 @@ vi.mock("@services/tauri", async () => {
3436
return {
3537
...actual,
3638
connectWorkspace: vi.fn(),
39+
getAppBuildType: vi.fn(),
3740
getModelList: vi.fn(),
3841
getConfigModel: vi.fn(),
3942
getExperimentalFeatureList: vi.fn(),
4043
getAgentsSettings: vi.fn(),
44+
isMobileRuntime: vi.fn(),
4145
listWorkspaces: vi.fn(),
4246
};
4347
});
4448

4549
const connectWorkspaceMock = vi.mocked(connectWorkspace);
50+
const getAppBuildTypeMock = vi.mocked(getAppBuildType);
4651
const getConfigModelMock = vi.mocked(getConfigModel);
4752
const getModelListMock = vi.mocked(getModelList);
4853
const getExperimentalFeatureListMock = vi.mocked(getExperimentalFeatureList);
4954
const getAgentsSettingsMock = vi.mocked(getAgentsSettings);
55+
const isMobileRuntimeMock = vi.mocked(isMobileRuntime);
5056
const listWorkspacesMock = vi.mocked(listWorkspaces);
5157
connectWorkspaceMock.mockResolvedValue(undefined);
58+
getAppBuildTypeMock.mockResolvedValue("release");
5259
getConfigModelMock.mockResolvedValue(null);
60+
isMobileRuntimeMock.mockResolvedValue(false);
5361
listWorkspacesMock.mockResolvedValue([]);
5462
getAgentsSettingsMock.mockResolvedValue({
5563
configPath: "/Users/me/.codex/config.toml",
@@ -105,6 +113,7 @@ const baseSettings: AppSettings = {
105113
showMessageFilePath: true,
106114
chatHistoryScrollbackItems: 200,
107115
threadTitleAutogenerationEnabled: false,
116+
automaticAppUpdateChecksEnabled: true,
108117
uiFontFamily:
109118
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
110119
codeFontFamily:
@@ -267,6 +276,56 @@ const renderComposerSection = (
267276
return { onUpdateAppSettings };
268277
};
269278

279+
const renderAboutSection = (
280+
options: {
281+
appSettings?: Partial<AppSettings>;
282+
onUpdateAppSettings?: ComponentProps<typeof SettingsView>["onUpdateAppSettings"];
283+
onToggleAutomaticAppUpdateChecks?: ComponentProps<
284+
typeof SettingsView
285+
>["onToggleAutomaticAppUpdateChecks"];
286+
} = {},
287+
) => {
288+
cleanup();
289+
const onUpdateAppSettings =
290+
options.onUpdateAppSettings ?? vi.fn().mockResolvedValue(undefined);
291+
const onToggleAutomaticAppUpdateChecks =
292+
options.onToggleAutomaticAppUpdateChecks ?? vi.fn();
293+
const props: ComponentProps<typeof SettingsView> = {
294+
reduceTransparency: false,
295+
onToggleTransparency: vi.fn(),
296+
appSettings: { ...baseSettings, ...options.appSettings },
297+
openAppIconById: {},
298+
onUpdateAppSettings,
299+
onToggleAutomaticAppUpdateChecks,
300+
workspaceGroups: [],
301+
groupedWorkspaces: [],
302+
ungroupedLabel: "Ungrouped",
303+
onClose: vi.fn(),
304+
onMoveWorkspace: vi.fn(),
305+
onDeleteWorkspace: vi.fn(),
306+
onCreateWorkspaceGroup: vi.fn().mockResolvedValue(null),
307+
onRenameWorkspaceGroup: vi.fn().mockResolvedValue(null),
308+
onMoveWorkspaceGroup: vi.fn().mockResolvedValue(null),
309+
onDeleteWorkspaceGroup: vi.fn().mockResolvedValue(null),
310+
onAssignWorkspaceGroup: vi.fn().mockResolvedValue(null),
311+
onRunDoctor: vi.fn().mockResolvedValue(createDoctorResult()),
312+
onUpdateWorkspaceSettings: vi.fn().mockResolvedValue(undefined),
313+
scaleShortcutTitle: "Scale shortcut",
314+
scaleShortcutText: "Use Command +/-",
315+
onTestNotificationSound: vi.fn(),
316+
onTestSystemNotification: vi.fn(),
317+
dictationModelStatus: null,
318+
onDownloadDictationModel: vi.fn(),
319+
onCancelDictationDownload: vi.fn(),
320+
onRemoveDictationModel: vi.fn(),
321+
};
322+
323+
render(<SettingsView {...props} />);
324+
fireEvent.click(screen.getByRole("button", { name: "About" }));
325+
326+
return { onUpdateAppSettings, onToggleAutomaticAppUpdateChecks };
327+
};
328+
270329
const renderFeaturesSection = (
271330
options: {
272331
appSettings?: Partial<AppSettings>;
@@ -673,6 +732,28 @@ describe("SettingsView Display", () => {
673732
});
674733
});
675734

735+
describe("SettingsView About", () => {
736+
it("toggles automatic app update checks", async () => {
737+
const onToggleAutomaticAppUpdateChecks = vi.fn();
738+
renderAboutSection({
739+
onToggleAutomaticAppUpdateChecks,
740+
appSettings: { automaticAppUpdateChecksEnabled: false },
741+
});
742+
743+
const row = screen
744+
.getByText("Automatically check for app updates")
745+
.closest(".settings-toggle-row") as HTMLElement | null;
746+
if (!row) {
747+
throw new Error("Expected automatic app update checks row");
748+
}
749+
fireEvent.click(within(row).getByRole("button"));
750+
751+
await waitFor(() => {
752+
expect(onToggleAutomaticAppUpdateChecks).toHaveBeenCalledTimes(1);
753+
});
754+
});
755+
});
756+
676757
describe("SettingsView Environments", () => {
677758
it("saves the setup script for the selected project", async () => {
678759
const onUpdateWorkspaceSettings = vi.fn().mockResolvedValue(undefined);

src/features/settings/components/SettingsView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type SettingsViewProps = {
4242
appSettings: AppSettings;
4343
openAppIconById: Record<string, string>;
4444
onUpdateAppSettings: (next: AppSettings) => Promise<void>;
45+
onToggleAutomaticAppUpdateChecks?: () => void;
4546
onRunDoctor: (
4647
codexBin: string | null,
4748
codexArgs: string | null,
@@ -83,6 +84,7 @@ export function SettingsView({
8384
appSettings,
8485
openAppIconById,
8586
onUpdateAppSettings,
87+
onToggleAutomaticAppUpdateChecks,
8688
onRunDoctor,
8789
onRunCodexUpdate,
8890
onUpdateWorkspaceSettings,
@@ -114,6 +116,7 @@ export function SettingsView({
114116
appSettings,
115117
openAppIconById,
116118
onUpdateAppSettings,
119+
onToggleAutomaticAppUpdateChecks,
117120
onRunDoctor,
118121
onRunCodexUpdate,
119122
onUpdateWorkspaceSettings,

src/features/settings/components/sections/SettingsAboutSection.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { useEffect, useState } from "react";
2+
import type { AppSettings } from "@/types";
23
import {
34
getAppBuildType,
45
isMobileRuntime,
56
type AppBuildType,
67
} from "@services/tauri";
78
import { useUpdater } from "@/features/update/hooks/useUpdater";
8-
import { SettingsSection } from "@/features/design-system/components/settings/SettingsPrimitives";
9+
import {
10+
SettingsSection,
11+
SettingsToggleRow,
12+
SettingsToggleSwitch,
13+
} from "@/features/design-system/components/settings/SettingsPrimitives";
14+
15+
type SettingsAboutSectionProps = {
16+
appSettings: AppSettings;
17+
onToggleAutomaticAppUpdateChecks?: () => void;
18+
};
919

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

24-
export function SettingsAboutSection() {
34+
export function SettingsAboutSection({
35+
appSettings,
36+
onToggleAutomaticAppUpdateChecks,
37+
}: SettingsAboutSectionProps) {
2538
const [appBuildType, setAppBuildType] = useState<AppBuildType | "unknown">("unknown");
2639
const [updaterEnabled, setUpdaterEnabled] = useState(false);
2740
const { state: updaterState, checkForUpdates, startUpdate } = useUpdater({
2841
enabled: updaterEnabled,
42+
autoCheckOnMount: false,
2943
});
3044

3145
useEffect(() => {
@@ -96,6 +110,17 @@ export function SettingsAboutSection() {
96110
</div>
97111
<div className="settings-field">
98112
<div className="settings-label">App Updates</div>
113+
<SettingsToggleRow
114+
title="Automatically check for app updates"
115+
subtitle="When enabled, CodexMonitor checks for new app versions on launch."
116+
>
117+
<SettingsToggleSwitch
118+
pressed={appSettings.automaticAppUpdateChecksEnabled}
119+
onClick={() => {
120+
onToggleAutomaticAppUpdateChecks?.();
121+
}}
122+
/>
123+
</SettingsToggleRow>
99124
<div className="settings-help">
100125
Currently running version <code>{__APP_VERSION__}</code>
101126
</div>

src/features/settings/components/sections/SettingsSectionContainers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function SettingsSectionContainers({
3333
return <SettingsDisplaySection {...orchestration.displaySectionProps} />;
3434
}
3535
if (activeSection === "about") {
36-
return <SettingsAboutSection />;
36+
return <SettingsAboutSection {...orchestration.aboutSectionProps} />;
3737
}
3838
if (activeSection === "composer") {
3939
return <SettingsComposerSection {...orchestration.composerSectionProps} />;

src/features/settings/hooks/useAppSettings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ function buildDefaultSettings(): AppSettings {
171171
showMessageFilePath: true,
172172
chatHistoryScrollbackItems: CHAT_SCROLLBACK_DEFAULT,
173173
threadTitleAutogenerationEnabled: false,
174+
automaticAppUpdateChecksEnabled: true,
174175
uiFontFamily: DEFAULT_UI_FONT_FAMILY,
175176
codeFontFamily: DEFAULT_CODE_FONT_FAMILY,
176177
codeFontSize: CODE_FONT_SIZE_DEFAULT,

src/features/settings/hooks/useSettingsViewOrchestration.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type UseSettingsViewOrchestrationArgs = {
3434
appSettings: AppSettings;
3535
openAppIconById: Record<string, string>;
3636
onUpdateAppSettings: (next: AppSettings) => Promise<void>;
37+
onToggleAutomaticAppUpdateChecks?: () => void;
3738
onRunDoctor: (
3839
codexBin: string | null,
3940
codexArgs: string | null,
@@ -76,6 +77,7 @@ export function useSettingsViewOrchestration({
7677
appSettings,
7778
openAppIconById,
7879
onUpdateAppSettings,
80+
onToggleAutomaticAppUpdateChecks,
7981
onRunDoctor,
8082
onRunCodexUpdate,
8183
onUpdateWorkspaceSettings,
@@ -211,6 +213,10 @@ export function useSettingsViewOrchestration({
211213
const agentsSectionProps = useSettingsAgentsSection({ projects });
212214

213215
return {
216+
aboutSectionProps: {
217+
appSettings,
218+
onToggleAutomaticAppUpdateChecks,
219+
},
214220
projectsSectionProps,
215221
environmentsSectionProps,
216222
displaySectionProps,

0 commit comments

Comments
 (0)