Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions src/features/app/components/MainApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ export default function MainApp() {
handleTestSystemNotification,
} = useUpdaterController({
enabled: updaterEnabled,
autoCheckOnMount: appSettings.automaticAppUpdateChecksEnabled,
Comment thread
Reekin marked this conversation as resolved.
Outdated
notificationSoundsEnabled: appSettings.notificationSoundsEnabled,
systemNotificationsEnabled: appSettings.systemNotificationsEnabled,
subagentSystemNotificationsEnabled:
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
77 changes: 77 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,50 @@ const renderComposerSection = (
return { onUpdateAppSettings };
};

const renderAboutSection = (
options: {
appSettings?: Partial<AppSettings>;
onUpdateAppSettings?: ComponentProps<typeof SettingsView>["onUpdateAppSettings"];
} = {},
) => {
cleanup();
const onUpdateAppSettings =
options.onUpdateAppSettings ?? vi.fn().mockResolvedValue(undefined);
const props: ComponentProps<typeof SettingsView> = {
reduceTransparency: false,
onToggleTransparency: vi.fn(),
appSettings: { ...baseSettings, ...options.appSettings },
openAppIconById: {},
onUpdateAppSettings,
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 };
};

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

describe("SettingsView About", () => {
it("toggles automatic app update checks", async () => {
const onUpdateAppSettings = vi.fn().mockResolvedValue(undefined);
renderAboutSection({
onUpdateAppSettings,
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(onUpdateAppSettings).toHaveBeenCalledWith(
expect.objectContaining({ automaticAppUpdateChecksEnabled: true }),
);
});
});
});

describe("SettingsView Environments", () => {
it("saves the setup script for the selected project", async () => {
const onUpdateWorkspaceSettings = vi.fn().mockResolvedValue(undefined);
Expand Down
33 changes: 31 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;
onUpdateAppSettings: (next: AppSettings) => Promise<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,
onUpdateAppSettings,
}: 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,21 @@ 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={() =>
void onUpdateAppSettings({
...appSettings,
automaticAppUpdateChecksEnabled:
!appSettings.automaticAppUpdateChecksEnabled,
})
}
/>
</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
4 changes: 4 additions & 0 deletions src/features/settings/hooks/useSettingsViewOrchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ export function useSettingsViewOrchestration({
const agentsSectionProps = useSettingsAgentsSection({ projects });

return {
aboutSectionProps: {
appSettings,
onUpdateAppSettings,
},
projectsSectionProps,
environmentsSectionProps,
displaySectionProps,
Expand Down
17 changes: 17 additions & 0 deletions src/features/update/hooks/useUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,23 @@ describe("useUpdater", () => {
expect(result.current.state.stage).toBe("idle");
});

it("skips automatic startup checks when auto-check is disabled but still allows manual checks", async () => {
checkMock.mockResolvedValue(null);

const { result } = renderHook(() =>
useUpdater({ autoCheckOnMount: false }),
);

expect(checkMock).not.toHaveBeenCalled();

await act(async () => {
await result.current.checkForUpdates({ announceNoUpdate: true });
});

expect(checkMock).toHaveBeenCalledTimes(1);
expect(result.current.state.stage).toBe("latest");
});

it("loads post-update release notes after restart when marker matches current version", async () => {
window.localStorage.setItem(
STORAGE_KEY_PENDING_POST_UPDATE_VERSION,
Expand Down
11 changes: 8 additions & 3 deletions src/features/update/hooks/useUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,15 @@ export type PostUpdateNoticeState = PostUpdateNotice | null;

type UseUpdaterOptions = {
enabled?: boolean;
autoCheckOnMount?: boolean;
onDebug?: (entry: DebugEntry) => void;
};

export function useUpdater({ enabled = true, onDebug }: UseUpdaterOptions) {
export function useUpdater({
enabled = true,
autoCheckOnMount = true,
onDebug,
}: UseUpdaterOptions) {
const [state, setState] = useState<UpdateState>({ stage: "idle" });
const [postUpdateNotice, setPostUpdateNotice] = useState<PostUpdateNoticeState>(
null,
Expand Down Expand Up @@ -205,11 +210,11 @@ export function useUpdater({ enabled = true, onDebug }: UseUpdaterOptions) {
}, [checkForUpdates, enabled, onDebug]);

useEffect(() => {
if (!enabled || import.meta.env.DEV || !isTauri()) {
if (!enabled || !autoCheckOnMount || import.meta.env.DEV || !isTauri()) {
return;
}
void checkForUpdates();
}, [checkForUpdates, enabled]);
}, [autoCheckOnMount, checkForUpdates, enabled]);

useEffect(() => {
if (!enabled || !isTauri()) {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export type AppSettings = {
showMessageFilePath: boolean;
chatHistoryScrollbackItems: number | null;
threadTitleAutogenerationEnabled: boolean;
automaticAppUpdateChecksEnabled: boolean;
uiFontFamily: string;
codeFontFamily: string;
codeFontSize: number;
Expand Down