From eceecb82352ff1f5db2ef3b306088a404a3a24f8 Mon Sep 17 00:00:00 2001 From: Paul Liu <20290410+Paulkm2006@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:50:14 +0800 Subject: [PATCH 1/6] feat: wire launch-at-login setting to Wails --- cmd/bootagent-desktop/main_wails.go | 24 +++++++++++ .../BootAgent/internal/app/models.ts | 5 +++ internal/app/settings.go | 9 ++++- internal/app/settings_test.go | 22 ++++++++++ internal/binding/services.go | 40 ++++++++++++++++--- internal/binding/services_test.go | 24 +++++++++++ 6 files changed, 118 insertions(+), 6 deletions(-) diff --git a/cmd/bootagent-desktop/main_wails.go b/cmd/bootagent-desktop/main_wails.go index 1d90778d..4d66fdff 100644 --- a/cmd/bootagent-desktop/main_wails.go +++ b/cmd/bootagent-desktop/main_wails.go @@ -143,6 +143,23 @@ func main() { } return current.Browser.OpenURL(url) }, binding.ServicesOptions{ + Autostart: binding.AutostartCallbacks{ + IsEnabled: func() (bool, error) { + if appInstance == nil || appInstance.Autostart == nil { + return false, oneerrors.New(oneerrors.InternalError, "Autostart manager is not ready") + } + return appInstance.Autostart.IsEnabled() + }, + SetEnabled: func(enabled bool) error { + if appInstance == nil || appInstance.Autostart == nil { + return oneerrors.New(oneerrors.InternalError, "Autostart manager is not ready") + } + if enabled { + return appInstance.Autostart.Enable() + } + return appInstance.Autostart.Disable() + }, + }, AfterGetStatus: func(status app.StatusResponse) { if status.FirstRun { return @@ -210,6 +227,13 @@ func main() { } }) appInstance.RegisterService(application.NewServiceWithOptions(binding.NewUpdateService(updateBackend), application.ServiceOptions{MarshalError: oneerrors.Marshal})) + if settings, err := core.Settings(context.Background()); err != nil { + slog.Warn("autostart setting unavailable", "error", err) + } else if settings.Autostart { + if err := appInstance.Autostart.Enable(); err != nil { + slog.Warn("autostart registration failed", "error", err) + } + } if !application.System.IsServer() { // A floor, not a second breakpoint: the sidebar deliberately collapses to // a 72px icon rail under 900px, and the layout is verified down to 560px. diff --git a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts index 039df0d4..a663d5bd 100644 --- a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts +++ b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts @@ -255,6 +255,11 @@ export interface SaveProviderResult { export interface Settings { "schema_version": number; + /** + * Autostart launches BootAgent when the user logs in. + */ + "autostart": boolean; + /** * PreferMirror routes runtime archives through the mirror in * runtimes.lock.json and npm-managed Agents through the npmmirror registry. diff --git a/internal/app/settings.go b/internal/app/settings.go index cc26260c..e0a130d9 100644 --- a/internal/app/settings.go +++ b/internal/app/settings.go @@ -29,6 +29,8 @@ const regionProbeTimeout = 3 * time.Second // request and do not belong to a profile. type Settings struct { SchemaVersion int `json:"schema_version"` + // Autostart launches BootAgent when the user logs in. + Autostart bool `json:"autostart"` // PreferMirror routes runtime archives through the mirror in // runtimes.lock.json and npm-managed Agents through the npmmirror registry. // Runtime archives keep their locked checksum verification; npm verifies @@ -51,6 +53,7 @@ type Settings struct { // launch. type storedSettings struct { SchemaVersion int `json:"schema_version"` + Autostart *bool `json:"autostart"` PreferMirror *bool `json:"prefer_mirror"` BackupRetention *int `json:"backup_retention"` } @@ -69,6 +72,9 @@ func (u *UseCases) Settings(ctx context.Context) (Settings, error) { return settings, nil } if stored, ok := u.readStoredSettings(); ok { + if stored.Autostart != nil { + settings.Autostart = *stored.Autostart + } if stored.PreferMirror != nil { settings.PreferMirror = *stored.PreferMirror } @@ -152,9 +158,10 @@ func (u *UseCases) SaveSettings(ctx context.Context, settings Settings) (Setting // That is what stops the regional default from re-ticking the box for a user // who turned it off. chosen := settings.PreferMirror + autostart := settings.Autostart retention := settings.BackupRetention data, err := json.MarshalIndent(storedSettings{ - SchemaVersion: settingsSchemaVersion, PreferMirror: &chosen, BackupRetention: &retention, + SchemaVersion: settingsSchemaVersion, Autostart: &autostart, PreferMirror: &chosen, BackupRetention: &retention, }, "", " ") if err != nil { return Settings{}, err diff --git a/internal/app/settings_test.go b/internal/app/settings_test.go index 3955efb3..4fdd8d39 100644 --- a/internal/app/settings_test.go +++ b/internal/app/settings_test.go @@ -76,6 +76,28 @@ func TestSettingsDefaultToTheOfficialSourceAndSurviveARestart(t *testing.T) { } } +func TestAutostartSettingDefaultsOffAndPersists(t *testing.T) { + home := t.TempDir() + core := settingsCore(t, home) + settings, err := core.Settings(context.Background()) + if err != nil { + t.Fatal(err) + } + if settings.Autostart { + t.Fatal("a fresh install enabled autostart") + } + if _, err := core.SaveSettings(context.Background(), Settings{Autostart: true}); err != nil { + t.Fatal(err) + } + reloaded, err := settingsCore(t, home).Settings(context.Background()) + if err != nil { + t.Fatal(err) + } + if !reloaded.Autostart { + t.Fatal("autostart did not survive a restart") + } +} + func TestBackupRetentionPersistsAndBoundsValues(t *testing.T) { home := t.TempDir() core := settingsCore(t, home) diff --git a/internal/binding/services.go b/internal/binding/services.go index 84700567..1f3f3977 100644 --- a/internal/binding/services.go +++ b/internal/binding/services.go @@ -37,6 +37,12 @@ type Services struct { type ServicesOptions struct { AfterGetStatus func(app.StatusResponse) InstallOutput process.OutputListener + Autostart AutostartCallbacks +} + +type AutostartCallbacks struct { + IsEnabled func() (bool, error) + SetEnabled func(bool) error } func NewServicesWithOptions(core *app.UseCases, opener BrowserOpener, options ServicesOptions) *Services { @@ -45,7 +51,7 @@ func NewServicesWithOptions(core *app.UseCases, opener BrowserOpener, options Se Provider: NewProviderService(core, opener), Agent: &AgentService{core: core, onOutput: options.InstallOutput}, Profile: NewProfileService(core), - Runtime: &RuntimeService{core: core, onOutput: options.InstallOutput}, + Runtime: &RuntimeService{core: core, onOutput: options.InstallOutput, autostart: options.Autostart}, DesktopAgent: NewDesktopAgentService(core, options.InstallOutput), Transfer: &TransferService{}, MCP: NewMCPService(core), @@ -135,8 +141,9 @@ func (s *DesktopAgentService) Configure(ctx context.Context, request DesktopAgen // output listener so the UI can render runtime byte progress alongside Agent // install output; runtime bootstrap does not emit a fake command line. type RuntimeService struct { - core *app.UseCases - onOutput process.OutputListener + core *app.UseCases + onOutput process.OutputListener + autostart AutostartCallbacks } func (s *RuntimeService) ListRuntimes(ctx context.Context) ([]app.RuntimeStatus, error) { @@ -173,7 +180,12 @@ func (s *RuntimeService) GetSettings(ctx context.Context) (app.Settings, error) if s == nil || s.core == nil { return app.Settings{}, notReady("Runtime service is not configured") } - return s.core.Settings(ctx) + settings, err := s.core.Settings(ctx) + if err != nil || s.autostart.IsEnabled == nil { + return settings, err + } + settings.Autostart, err = s.autostart.IsEnabled() + return settings, err } // SaveSettings persists the download preferences and returns what was stored. @@ -184,7 +196,25 @@ func (s *RuntimeService) SaveSettings(ctx context.Context, request app.Settings) if s == nil || s.core == nil { return app.Settings{}, notReady("Runtime service is not configured") } - return s.core.SaveSettings(ctx, request) + previous, changed := false, false + if s.autostart.SetEnabled != nil { + if s.autostart.IsEnabled != nil { + var err error + previous, err = s.autostart.IsEnabled() + if err != nil { + return app.Settings{}, err + } + changed = previous != request.Autostart + } + if err := s.autostart.SetEnabled(request.Autostart); err != nil { + return app.Settings{}, err + } + } + saved, err := s.core.SaveSettings(ctx, request) + if err != nil && changed { + _ = s.autostart.SetEnabled(previous) + } + return saved, err } // HelpURL is the published help site. It lives here rather than in the frontend diff --git a/internal/binding/services_test.go b/internal/binding/services_test.go index f4850b24..41c43bbb 100644 --- a/internal/binding/services_test.go +++ b/internal/binding/services_test.go @@ -95,6 +95,30 @@ func TestStatusServiceRunsNativeSmokeHookAfterSuccess(t *testing.T) { } } +func TestRuntimeServiceAppliesAutostartCallbacks(t *testing.T) { + core := providerCore(t, nil) + enabled := false + service := &RuntimeService{ + core: core, + autostart: AutostartCallbacks{ + IsEnabled: func() (bool, error) { return enabled, nil }, + SetEnabled: func(next bool) error { + enabled = next + return nil + }, + }, + } + if got, err := service.GetSettings(context.Background()); err != nil || got.Autostart { + t.Fatalf("initial autostart = %#v, err=%v", got, err) + } + if _, err := service.SaveSettings(context.Background(), app.Settings{Autostart: true}); err != nil { + t.Fatal(err) + } + if !enabled { + t.Fatal("autostart callback was not enabled") + } +} + func TestOpenRegistrationUsesConfiguredProviderURL(t *testing.T) { var opened string service := NewProviderService(providerCore(t, nil), func(value string) error { From 4364d33dbd90576a4a1018b4e4a083a8521212c7 Mon Sep 17 00:00:00 2001 From: Paul Liu <20290410+Paulkm2006@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:50:25 +0800 Subject: [PATCH 2/6] feat: prompt for launch at login with API conversion --- README.md | 1 + README_ZH.md | 1 + .../src/components/MirrorSetting.test.tsx | 4 +- frontend/src/components/MirrorSetting.tsx | 4 +- frontend/src/i18n.tsx | 8 +++- frontend/src/pages/ConversionPage.tsx | 37 +++++++++++++++++-- frontend/src/pages/SettingsPage.test.tsx | 25 +++++++++++++ frontend/src/pages/SettingsPage.tsx | 37 ++++++++++++++++++- frontend/src/state/confirmDelete.ts | 11 +++++- 9 files changed, 119 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e6ca0d7c..8aaf4aa3 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ BootAgent is a local desktop workspace for AI coding Agents. It turns a fresh ma - Carries a Profile's reasoning effort (`off`, `low`, `medium`, `high`, `max`) into every Agent whose own config format documents a place for it, translating the scale to what each one accepts. Agents with no documented depth setting are left alone rather than given invented keys. - Bootstraps required runtimes such as Node.js, uv, and Aider's managed Python when needed. - Keeps long-running installs visible and cancellable in the Task Center. +- Provides local API format conversion and an optional launch-at-login setting; both are off by default, and enabling conversion offers to enable launch at login. - Imports and exports Providers, Profiles, and selected MCP servers. API keys and MCP secrets are excluded by default; password-encrypted or explicitly confirmed plaintext export is also available. - Discovers MCP servers from initialized Claude Code, Codex, OpenCode, Kilo CLI, and Hermes installations, and applies selected servers across them from the MCP Registry. Scanning runs in the background; edits are explicit and local. - Creates backups, writes atomically, and keeps credentials in private local storage. The latest three historical versions are kept per Profile, Provider, MCP, Agent configuration target, and Skill; backups live under `~/.bootagent/backup` and the per-target count can be changed in Settings. diff --git a/README_ZH.md b/README_ZH.md index ce5eb6b1..fcba043d 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -22,6 +22,7 @@ BootAgent 是一个本地桌面工作台,用来统一管理 AI 编程 Agent。 - 把 Profile 上的思考深度(`off`、`low`、`medium`、`high`、`max`)写入每个自身配置格式有对应位置的 Agent,并按各自接受的取值做换算。没有文档化深度设置的 Agent 保持原样,不会被塞进自造的字段。 - 按需准备 Node.js、uv,以及 Aider 所需的托管 Python 运行时。 - 长时间安装任务在任务中心持续可见,并且可以取消。 +- 提供本地 API 格式转换和可选的开机自启动;两者默认关闭,启用格式转换时会询问是否同时开机自启动。 - 导入和导出 Provider、Profile 以及选中的 MCP 服务器;默认不导出 API Key 和 MCP 秘密,也支持密码加密或明确确认后的明文导出。 - 从已初始化的 Claude Code、Codex、OpenCode、Kilo CLI 和 Hermes 中发现 MCP 服务器,并在 MCP Registry 页面选择同步目标。扫描在后台进行,编辑必须显式应用并只写入本机。 - 创建备份、原子写入,并将凭据保存在本机私有存储中。Profile、Provider、MCP、Agent 配置目标和 Skill 分别保留最近 3 个历史版本;备份统一放在 `~/.bootagent/backup`,可在“设置”中修改每个目标的保留数量。 diff --git a/frontend/src/components/MirrorSetting.test.tsx b/frontend/src/components/MirrorSetting.test.tsx index 45711a0d..187b80c7 100644 --- a/frontend/src/components/MirrorSetting.test.tsx +++ b/frontend/src/components/MirrorSetting.test.tsx @@ -45,7 +45,7 @@ describe("MirrorSetting", () => { await userEvent.click(screen.getByRole("switch")); await waitFor(() => - expect(saveSettings).toHaveBeenCalledWith({ schema_version: 1, prefer_mirror: true, mirror_from_region: false, backup_retention: 3 }), + expect(saveSettings).toHaveBeenCalledWith({ schema_version: 1, autostart: false, prefer_mirror: true, mirror_from_region: false, backup_retention: 3 }), ); expect((screen.getByRole("switch") as HTMLInputElement).checked).toBe(true); }); @@ -103,7 +103,7 @@ describe("MirrorSetting", () => { await userEvent.click(screen.getByRole("switch")); await waitFor(() => - expect(saveSettings).toHaveBeenCalledWith({ schema_version: 1, prefer_mirror: false, mirror_from_region: false, backup_retention: 3 }), + expect(saveSettings).toHaveBeenCalledWith({ schema_version: 1, autostart: false, prefer_mirror: false, mirror_from_region: false, backup_retention: 3 }), ); expect((screen.getByRole("switch") as HTMLInputElement).checked).toBe(false); // The regional explanation is gone: it is now a stored choice. diff --git a/frontend/src/components/MirrorSetting.tsx b/frontend/src/components/MirrorSetting.tsx index 095d5398..6b5107c6 100644 --- a/frontend/src/components/MirrorSetting.tsx +++ b/frontend/src/components/MirrorSetting.tsx @@ -15,6 +15,7 @@ export function MirrorSetting() { const { t } = useI18n(); const [preferMirror, setPreferMirror] = useState(null); const [backupRetention, setBackupRetention] = useState(3); + const [autostart, setAutostart] = useState(false); // Whether the tick came from the machine's region rather than from a choice. // Shown so an already-ticked box does not look like something the user did and // forgot, and so it is clear it can be turned off. @@ -30,6 +31,7 @@ export function MirrorSetting() { setPreferMirror(settings.prefer_mirror); setFromRegion(settings.mirror_from_region); setBackupRetention(settings.backup_retention ?? 3); + setAutostart(settings.autostart === true); }) .catch(() => { // An unreadable preference is not worth a visible error: the backend @@ -54,7 +56,7 @@ export function MirrorSetting() { setFromRegion(false); setFailure(""); try { - const saved = await api.saveSettings({ schema_version: 1, prefer_mirror: next, mirror_from_region: false, backup_retention: backupRetention }); + const saved = await api.saveSettings({ schema_version: 1, autostart, prefer_mirror: next, mirror_from_region: false, backup_retention: backupRetention }); setPreferMirror(saved.prefer_mirror); setFromRegion(saved.mirror_from_region); setBackupRetention(saved.backup_retention ?? backupRetention); diff --git a/frontend/src/i18n.tsx b/frontend/src/i18n.tsx index e4b8c2c0..33f93cc3 100644 --- a/frontend/src/i18n.tsx +++ b/frontend/src/i18n.tsx @@ -73,6 +73,13 @@ const english = { "管理界面偏好与配置迁移": "Manage interface preferences and configuration transfer", "界面": "Interface", "下载": "Downloads", + "启动设置": "Startup", + "启动": "Launch", + "开机自启动": "Launch at login", + "登录系统后自动启动 BootAgent": "Start BootAgent automatically when you log in", + "暂不启用": "Not now", + "API 转换已启用,是否让 BootAgent 随系统启动?": "API conversion is enabled. Start BootAgent automatically when you log in?", + "无法保存开机自启动设置": "Could not save the launch-at-login setting", "数据": "Data", "备份历史版本数": "Backup history versions", "每个目标分别保留历史版本,默认保留 3 个": "Keep history per target; the default is 3 versions", @@ -453,7 +460,6 @@ const english = { "已安装 Agent": "Installed agents", "共 {count} 个": "Total: {count}", "尚未安装任何命令行 Agent": "No command-line agents installed", - "启动": "Launch", "在新终端窗口中启动,并载入 BootAgent 写入的配置": "Open a new terminal window running this agent with the configuration BootAgent wrote", "无法启动 Agent": "Could not launch the agent", "选择启动目录": "Choose launch directory", diff --git a/frontend/src/pages/ConversionPage.tsx b/frontend/src/pages/ConversionPage.tsx index 49b8acd6..0a973474 100644 --- a/frontend/src/pages/ConversionPage.tsx +++ b/frontend/src/pages/ConversionPage.tsx @@ -1,11 +1,12 @@ import { Power } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { api, describeFailure } from "../backend/api"; import { PageScaffold } from "../components/PageScaffold"; import { SelectField } from "../components/SelectField"; import { useI18n } from "../i18n"; import { useWizard } from "../state/WizardContext"; +import { confirmAction } from "../state/confirmDelete"; import type { ConversionConfig, StatusResponse } from "../types/api"; export function ConversionPage() { @@ -15,11 +16,31 @@ export function ConversionPage() { const [status, setStatus] = useState(null); const [failure, setFailure] = useState(""); const [saving, setSaving] = useState(false); + const autostartPrompted = useRef(false); + + const maybeAskAutostart = async (enabled: boolean) => { + if (!enabled || autostartPrompted.current) return; + autostartPrompted.current = true; + try { + const settings = await api.getSettings(); + if (settings.autostart) return; + if (!await confirmAction({ + title: t("开机自启动"), + message: t("API 转换已启用,是否让 BootAgent 随系统启动?"), + confirmLabel: t("开机自启动"), + cancelLabel: t("暂不启用"), + })) return; + await api.saveSettings({ ...settings, autostart: true }); + } catch (error) { + setFailure(describeFailure(error, t("无法保存开机自启动设置"), t).message); + } + }; useEffect(() => { void Promise.all([api.getConversion(), api.status()]).then(([loaded, nextStatus]) => { setConfig(loaded); setStatus(nextStatus); + void maybeAskAutostart(loaded.enabled); }).catch((error) => setFailure(describeFailure(error, t("无法读取格式转换设置"), t).message)); }, [t]); @@ -27,7 +48,12 @@ export function ConversionPage() { if (!config) return; setSaving(true); setFailure(""); - try { setConfig(await api.saveConversion(config)); void refreshStatus(); } + try { + const saved = await api.saveConversion(config); + setConfig(saved); + await maybeAskAutostart(saved.enabled); + void refreshStatus(); + } catch (error) { setFailure(describeFailure(error, t("无法保存格式转换设置"), t).message); } finally { setSaving(false); } }; @@ -36,7 +62,12 @@ export function ConversionPage() { if (!config) return; setSaving(true); setFailure(""); - try { setConfig(await api.saveConversion({ ...config, enabled: !config.enabled })); void refreshStatus(); } + try { + const saved = await api.saveConversion({ ...config, enabled: !config.enabled }); + setConfig(saved); + await maybeAskAutostart(saved.enabled); + void refreshStatus(); + } catch (error) { setFailure(describeFailure(error, t("无法切换格式转换"), t).message); } finally { setSaving(false); } }; diff --git a/frontend/src/pages/SettingsPage.test.tsx b/frontend/src/pages/SettingsPage.test.tsx index 57f1e0ce..b322b700 100644 --- a/frontend/src/pages/SettingsPage.test.tsx +++ b/frontend/src/pages/SettingsPage.test.tsx @@ -17,6 +17,7 @@ describe("SettingsPage", () => { it("contains appearance and download settings and opens the transfer child page", async () => { vi.spyOn(api, "getSettings").mockResolvedValue({ schema_version: 1, + autostart: false, prefer_mirror: false, mirror_from_region: false, backup_retention: 3, @@ -52,12 +53,14 @@ describe("SettingsPage", () => { it("loads and saves per-target backup retention", async () => { const getSettings = vi.spyOn(api, "getSettings").mockResolvedValue({ schema_version: 1, + autostart: false, prefer_mirror: false, mirror_from_region: false, backup_retention: 3, }); const saveSettings = vi.spyOn(api, "saveSettings").mockResolvedValue({ schema_version: 1, + autostart: false, prefer_mirror: false, mirror_from_region: false, backup_retention: 7, @@ -71,12 +74,34 @@ describe("SettingsPage", () => { await userEvent.tab(); await waitFor(() => expect(saveSettings).toHaveBeenCalledWith({ schema_version: 1, + autostart: false, prefer_mirror: false, mirror_from_region: false, backup_retention: 7, })); }); + it("saves the launch-at-login checkbox", async () => { + vi.spyOn(api, "getSettings").mockResolvedValue({ + schema_version: 1, + autostart: false, + prefer_mirror: false, + mirror_from_region: false, + backup_retention: 3, + }); + const saveSettings = vi.spyOn(api, "saveSettings").mockResolvedValue({ + schema_version: 1, + autostart: true, + prefer_mirror: false, + mirror_from_region: false, + backup_retention: 3, + }); + show(); + const checkbox = await screen.findByRole("checkbox", { name: "开机自启动" }); + await userEvent.click(checkbox); + await waitFor(() => expect(saveSettings).toHaveBeenCalledWith(expect.objectContaining({ autostart: true }))); + }); + // An app running from a mounted dmg sits on a read-only volume, so the update // helper cannot write its backup and abandons the swap without restarting the // app. The backend withholds the version and reports the location instead; this diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index fab22f60..5d76262f 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { Archive, BookOpen, ChevronRight, ExternalLink, Import, Languages, RefreshCw } from "lucide-react"; +import { Archive, BookOpen, ChevronRight, ExternalLink, Import, Languages, Power, RefreshCw } from "lucide-react"; import { useState } from "react"; import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; @@ -32,6 +32,7 @@ export function SettingsPage() { const [settings, setSettings] = useState(null); const [backupRetention, setBackupRetention] = useState(""); const [backupFailure, setBackupFailure] = useState(""); + const [autostartFailure, setAutostartFailure] = useState(""); useEffect(() => { void api.version().then(setVersion).catch(() => {}); }, []); useEffect(() => { @@ -68,6 +69,20 @@ export function SettingsPage() { } }; + const toggleAutostart = async (enabled: boolean) => { + if (!settings) return; + const previous = settings.autostart; + setSettings({ ...settings, autostart: enabled }); + setAutostartFailure(""); + try { + const saved = await api.saveSettings({ ...settings, autostart: enabled }); + setSettings(saved); + } catch (error) { + setSettings({ ...settings, autostart: previous }); + setAutostartFailure(describeFailure(error, t("无法保存开机自启动设置"), t).message); + } + }; + const checkUpdate = async () => { setChecking(true); setUpdateMessage(""); @@ -157,6 +172,26 @@ export function SettingsPage() {

{t("下载")}

+
+

{t("启动设置")}

+
+ +
+ {autostartFailure ?

{autostartFailure}

: null} +

{t("数据")}

diff --git a/frontend/src/state/confirmDelete.ts b/frontend/src/state/confirmDelete.ts index 07d52477..5dbf2052 100644 --- a/frontend/src/state/confirmDelete.ts +++ b/frontend/src/state/confirmDelete.ts @@ -32,7 +32,7 @@ function hasNativeWebView(): boolean { return Boolean(host.chrome?.webview?.postMessage || host.webkit?.messageHandlers?.external?.postMessage || host.wails?.invoke); } -export async function confirmDelete(options: { +export async function confirmAction(options: { title: string; message: string; confirmLabel: string; @@ -62,3 +62,12 @@ export async function confirmDelete(options: { if (typeof window.confirm !== "function") return false; return window.confirm(options.message); } + +export function confirmDelete(options: { + title: string; + message: string; + confirmLabel: string; + cancelLabel: string; +}): Promise { + return confirmAction(options); +} From e8e7064eb9f7dc42ac49de142b51650cc7b1ad4f Mon Sep 17 00:00:00 2001 From: Paul Liu <20290410+Paulkm2006@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:52:35 +0800 Subject: [PATCH 3/6] chore: modernize code --- internal/config/write.go | 19 +++++++------------ internal/convertproxy/convert.go | 12 ++++++------ internal/convertproxy/server.go | 9 +++------ 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/internal/config/write.go b/internal/config/write.go index 8fe45c01..eef3d30d 100644 --- a/internal/config/write.go +++ b/internal/config/write.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strconv" "strings" @@ -72,10 +73,8 @@ var dshOfficialReasoningEfforts = []string{"off", "high", "max"} // surface as an UNSUPPORTED_REASONING_EFFORT error on the user's first request // -- far from the Profile edit that caused it. func ValidateDSHOfficialReasoningEffort(effort string) error { - for _, allowed := range dshOfficialReasoningEfforts { - if effort == allowed { - return nil - } + if slices.Contains(dshOfficialReasoningEfforts, effort) { + return nil } return oneerrors.New(oneerrors.InvalidRequest, fmt.Sprintf( "DeepSeek reasoning effort must be one of %s, got %q", @@ -100,10 +99,8 @@ var profileReasoningEfforts = []string{"off", "minimal", "low", "medium", "high" // vocabulary. This is the save-time gate; the adapter-specific gates below run // at activation, when the Agent the value must fit is finally known. func ValidateProfileReasoningEffort(effort string) error { - for _, allowed := range profileReasoningEfforts { - if effort == allowed { - return nil - } + if slices.Contains(profileReasoningEfforts, effort) { + return nil } return oneerrors.New(oneerrors.InvalidRequest, fmt.Sprintf( "Reasoning effort must be one of %s, got %q", @@ -151,10 +148,8 @@ var openAIReasoningEfforts = []string{"low", "medium", "high"} // cannot forward. The message names no Agent because three adapters share the // gate; the activation that surfaces it already says which Agent refused. func validateOpenAIReasoningEffort(effort string) error { - for _, allowed := range openAIReasoningEfforts { - if effort == allowed { - return nil - } + if slices.Contains(openAIReasoningEfforts, effort) { + return nil } return oneerrors.New(oneerrors.InvalidRequest, fmt.Sprintf( "This Agent accepts a reasoning effort of %s, got %q", diff --git a/internal/convertproxy/convert.go b/internal/convertproxy/convert.go index cd17d55d..044bdea2 100644 --- a/internal/convertproxy/convert.go +++ b/internal/convertproxy/convert.go @@ -658,10 +658,10 @@ func chatReasoning(message map[string]any) string { } } text := stringValue(message["content"]) - if strings.HasPrefix(strings.TrimSpace(text), "") { - text = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(text), "")) - if end := strings.Index(text, ""); end >= 0 { - return strings.TrimSpace(text[:end]) + if after, ok := strings.CutPrefix(strings.TrimSpace(text), ""); ok { + text = strings.TrimSpace(after) + if before, _, ok := strings.Cut(text, ""); ok { + return strings.TrimSpace(before) } } return "" @@ -671,8 +671,8 @@ func chatMessageText(message map[string]any) string { text := stringValue(message["content"]) if strings.HasPrefix(strings.TrimSpace(text), "") { trimmed := strings.TrimSpace(text) - if end := strings.Index(trimmed, ""); end >= 0 { - return strings.TrimSpace(trimmed[end+len(""):]) + if _, after, ok := strings.Cut(trimmed, ""); ok { + return strings.TrimSpace(after) } } return text diff --git a/internal/convertproxy/server.go b/internal/convertproxy/server.go index 4aed451d..e51dd39c 100644 --- a/internal/convertproxy/server.go +++ b/internal/convertproxy/server.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "io" + "maps" "net" "net/http" "strconv" @@ -197,9 +198,7 @@ func (s *Server) forwardConverted(w http.ResponseWriter, r *http.Request, body [ if format != "chat" && resp.StatusCode < 300 { data, _ = FromChat(format, data) } - for k, v := range resp.Header { - w.Header()[k] = v - } + maps.Copy(w.Header(), resp.Header) // The body is read and written again (and may be transformed or transparently // decompressed), so upstream framing/encoding metadata is no longer valid. w.Header().Del("Content-Length") @@ -389,9 +388,7 @@ func modelFromRequest(body []byte) string { } func (s *Server) forwardResponsesStream(w http.ResponseWriter, resp *http.Response, model string) { - for k, v := range resp.Header { - w.Header()[k] = v - } + maps.Copy(w.Header(), resp.Header) w.Header().Set("Content-Type", "text/event-stream") w.Header().Del("Content-Length") w.WriteHeader(resp.StatusCode) From 81a2342361f128957941504c9890aa0a2152d564 Mon Sep 17 00:00:00 2001 From: Paul Liu <20290410+Paulkm2006@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:59:19 +0800 Subject: [PATCH 4/6] refactor: support partial settings updates --- .../BootAgent/internal/app/index.ts | 1 + .../BootAgent/internal/app/models.ts | 10 +++ .../internal/binding/runtimeservice.ts | 2 +- frontend/src/backend/wails.ts | 3 +- .../src/components/MirrorSetting.test.tsx | 4 +- frontend/src/components/MirrorSetting.tsx | 4 +- frontend/src/pages/ConversionPage.tsx | 2 +- frontend/src/pages/SettingsPage.test.tsx | 8 +- frontend/src/pages/SettingsPage.tsx | 9 +-- frontend/src/types/api.ts | 1 + internal/app/settings.go | 76 +++++++++++++++---- internal/app/settings_test.go | 28 +++++++ internal/binding/services.go | 10 +-- internal/binding/services_test.go | 3 +- 14 files changed, 117 insertions(+), 44 deletions(-) diff --git a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/index.ts b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/index.ts index a7cb5e49..6f9c564d 100644 --- a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/index.ts +++ b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/index.ts @@ -24,6 +24,7 @@ export type { SaveProfileResult, SaveProviderResult, Settings, + SettingsPatch, SkillAgentApplyResult, SkillApplyRequest, SkillApplyResult, diff --git a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts index a663d5bd..6f0727c1 100644 --- a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts +++ b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/app/models.ts @@ -283,6 +283,16 @@ export interface Settings { "backup_retention": number; } +/** + * SettingsPatch updates only the machine-level preferences named by the caller. + * A nil field means "leave the stored value unchanged". + */ +export interface SettingsPatch { + "autostart"?: boolean | null; + "prefer_mirror"?: boolean | null; + "backup_retention"?: number | null; +} + export interface SkillAgentApplyResult { "agent": string; "target_updated": boolean; diff --git a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/binding/runtimeservice.ts b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/binding/runtimeservice.ts index 47cec2c5..d4fc0189 100644 --- a/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/binding/runtimeservice.ts +++ b/frontend/bindings/github.com/MaimoryLab/BootAgent/internal/binding/runtimeservice.ts @@ -38,6 +38,6 @@ export function ListRuntimes(): $CancellablePromise { +export function SaveSettings(request: app$0.SettingsPatch): $CancellablePromise { return $Call.ByID(3381352702, request); } diff --git a/frontend/src/backend/wails.ts b/frontend/src/backend/wails.ts index 075f58f8..ddfa4405 100644 --- a/frontend/src/backend/wails.ts +++ b/frontend/src/backend/wails.ts @@ -40,6 +40,7 @@ import type { SaveProviderInput, SaveProviderResult, Settings, + SettingsPatch, SkillApplyRequest, SkillApplyResult, SkillBackupSummary, @@ -213,7 +214,7 @@ export const wailsApi = { installRuntime: (runtime: string): CancellableRequest => call(() => RuntimeService.InstallRuntime({ runtime })) as CancellableRequest, getSettings: (): Promise => call(() => RuntimeService.GetSettings()) as Promise, - saveSettings: (settings: Settings): Promise => + saveSettings: (settings: SettingsPatch): Promise => call(() => RuntimeService.SaveSettings(settings)) as Promise, getConversion: (): Promise => call(() => ConversionService.Get()) as Promise, saveConversion: (config: ConversionConfig): Promise => call(() => ConversionService.Save(config)) as Promise, diff --git a/frontend/src/components/MirrorSetting.test.tsx b/frontend/src/components/MirrorSetting.test.tsx index 187b80c7..2f3d118c 100644 --- a/frontend/src/components/MirrorSetting.test.tsx +++ b/frontend/src/components/MirrorSetting.test.tsx @@ -45,7 +45,7 @@ describe("MirrorSetting", () => { await userEvent.click(screen.getByRole("switch")); await waitFor(() => - expect(saveSettings).toHaveBeenCalledWith({ schema_version: 1, autostart: false, prefer_mirror: true, mirror_from_region: false, backup_retention: 3 }), + expect(saveSettings).toHaveBeenCalledWith({ prefer_mirror: true }), ); expect((screen.getByRole("switch") as HTMLInputElement).checked).toBe(true); }); @@ -103,7 +103,7 @@ describe("MirrorSetting", () => { await userEvent.click(screen.getByRole("switch")); await waitFor(() => - expect(saveSettings).toHaveBeenCalledWith({ schema_version: 1, autostart: false, prefer_mirror: false, mirror_from_region: false, backup_retention: 3 }), + expect(saveSettings).toHaveBeenCalledWith({ prefer_mirror: false }), ); expect((screen.getByRole("switch") as HTMLInputElement).checked).toBe(false); // The regional explanation is gone: it is now a stored choice. diff --git a/frontend/src/components/MirrorSetting.tsx b/frontend/src/components/MirrorSetting.tsx index 6b5107c6..3273e050 100644 --- a/frontend/src/components/MirrorSetting.tsx +++ b/frontend/src/components/MirrorSetting.tsx @@ -15,7 +15,6 @@ export function MirrorSetting() { const { t } = useI18n(); const [preferMirror, setPreferMirror] = useState(null); const [backupRetention, setBackupRetention] = useState(3); - const [autostart, setAutostart] = useState(false); // Whether the tick came from the machine's region rather than from a choice. // Shown so an already-ticked box does not look like something the user did and // forgot, and so it is clear it can be turned off. @@ -31,7 +30,6 @@ export function MirrorSetting() { setPreferMirror(settings.prefer_mirror); setFromRegion(settings.mirror_from_region); setBackupRetention(settings.backup_retention ?? 3); - setAutostart(settings.autostart === true); }) .catch(() => { // An unreadable preference is not worth a visible error: the backend @@ -56,7 +54,7 @@ export function MirrorSetting() { setFromRegion(false); setFailure(""); try { - const saved = await api.saveSettings({ schema_version: 1, autostart, prefer_mirror: next, mirror_from_region: false, backup_retention: backupRetention }); + const saved = await api.saveSettings({ prefer_mirror: next }); setPreferMirror(saved.prefer_mirror); setFromRegion(saved.mirror_from_region); setBackupRetention(saved.backup_retention ?? backupRetention); diff --git a/frontend/src/pages/ConversionPage.tsx b/frontend/src/pages/ConversionPage.tsx index 0a973474..aba8f585 100644 --- a/frontend/src/pages/ConversionPage.tsx +++ b/frontend/src/pages/ConversionPage.tsx @@ -30,7 +30,7 @@ export function ConversionPage() { confirmLabel: t("开机自启动"), cancelLabel: t("暂不启用"), })) return; - await api.saveSettings({ ...settings, autostart: true }); + await api.saveSettings({ autostart: true }); } catch (error) { setFailure(describeFailure(error, t("无法保存开机自启动设置"), t).message); } diff --git a/frontend/src/pages/SettingsPage.test.tsx b/frontend/src/pages/SettingsPage.test.tsx index b322b700..110b7960 100644 --- a/frontend/src/pages/SettingsPage.test.tsx +++ b/frontend/src/pages/SettingsPage.test.tsx @@ -72,13 +72,7 @@ describe("SettingsPage", () => { await userEvent.clear(input); await userEvent.type(input, "7"); await userEvent.tab(); - await waitFor(() => expect(saveSettings).toHaveBeenCalledWith({ - schema_version: 1, - autostart: false, - prefer_mirror: false, - mirror_from_region: false, - backup_retention: 7, - })); + await waitFor(() => expect(saveSettings).toHaveBeenCalledWith({ backup_retention: 7 })); }); it("saves the launch-at-login checkbox", async () => { diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 5d76262f..f4e80b48 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -56,12 +56,7 @@ export function SettingsPage() { setBackupRetention(nextRetention); setBackupFailure(""); try { - const saved = await api.saveSettings({ - ...settings, - schema_version: 1, - mirror_from_region: false, - backup_retention: nextRetention, - }); + const saved = await api.saveSettings({ backup_retention: nextRetention }); setSettings(saved); setBackupRetention(saved.backup_retention ?? nextRetention); } catch (error) { @@ -75,7 +70,7 @@ export function SettingsPage() { setSettings({ ...settings, autostart: enabled }); setAutostartFailure(""); try { - const saved = await api.saveSettings({ ...settings, autostart: enabled }); + const saved = await api.saveSettings({ autostart: enabled }); setSettings(saved); } catch (error) { setSettings({ ...settings, autostart: previous }); diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index b1c1700d..4f343a40 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -48,6 +48,7 @@ export type DesktopAgentActionResult = Omit & { runtimes: RuntimeStatus[] }; export type Settings = AppModels.Settings; +export type SettingsPatch = AppModels.SettingsPatch; export type ConversionConfig = AppModels.ConversionConfig; export type ActivateAgentResponse = BindingModels.ActivateResponse; export type LaunchAgentResponse = BindingModels.LaunchResponse; diff --git a/internal/app/settings.go b/internal/app/settings.go index e0a130d9..30d085d7 100644 --- a/internal/app/settings.go +++ b/internal/app/settings.go @@ -46,6 +46,14 @@ type Settings struct { BackupRetention int `json:"backup_retention"` } +// SettingsPatch updates only the machine-level preferences named by the caller. +// A nil field means "leave the stored value unchanged". +type SettingsPatch struct { + Autostart *bool `json:"autostart,omitempty"` + PreferMirror *bool `json:"prefer_mirror,omitempty"` + BackupRetention *int `json:"backup_retention,omitempty"` +} + // storedSettings is the on-disk shape. PreferMirror is a pointer so an absent // field ("never chosen", which may take the regional default) is distinct from // a stored false ("the user turned it off"). Without that distinction, a user in @@ -53,9 +61,9 @@ type Settings struct { // launch. type storedSettings struct { SchemaVersion int `json:"schema_version"` - Autostart *bool `json:"autostart"` - PreferMirror *bool `json:"prefer_mirror"` - BackupRetention *int `json:"backup_retention"` + Autostart *bool `json:"autostart,omitempty"` + PreferMirror *bool `json:"prefer_mirror,omitempty"` + BackupRetention *int `json:"backup_retention,omitempty"` } func (u *UseCases) settingsPath() string { @@ -154,30 +162,66 @@ func (u *UseCases) SaveSettings(ctx context.Context, settings Settings) (Setting } else { settings.BackupRetention = normalizeBackupRetention(settings.BackupRetention) } - // Saving is always an explicit choice, so the key is written even for false. - // That is what stops the regional default from re-ticking the box for a user - // who turned it off. chosen := settings.PreferMirror autostart := settings.Autostart retention := settings.BackupRetention - data, err := json.MarshalIndent(storedSettings{ + return u.saveStoredSettings(ctx, storedSettings{ SchemaVersion: settingsSchemaVersion, Autostart: &autostart, PreferMirror: &chosen, BackupRetention: &retention, - }, "", " ") - if err != nil { + }) +} + +// UpdateSettings merges a partial update with the stored preferences. It does +// not materialize a regional mirror default when that preference was untouched. +func (u *UseCases) UpdateSettings(ctx context.Context, patch SettingsPatch) (Settings, error) { + if u == nil { + return Settings{}, nil + } + if err := contextError(ctx, "Settings request was cancelled"); err != nil { return Settings{}, err } - settings.MirrorFromRegion = false - // Same coordinator as the other writers so a settings write cannot interleave - // with an install that is about to read it. u.writeMu.Lock() - defer u.writeMu.Unlock() - if err := u.filesystem.EnsurePrivateDir(ctx, filepath.Dir(u.settingsPath())); err != nil { + stored, _ := u.readStoredSettings() + stored.SchemaVersion = settingsSchemaVersion + if patch.Autostart != nil { + value := *patch.Autostart + stored.Autostart = &value + } + if patch.PreferMirror != nil { + value := *patch.PreferMirror + stored.PreferMirror = &value + } + if patch.BackupRetention != nil && *patch.BackupRetention != 0 { + value := normalizeBackupRetention(*patch.BackupRetention) + stored.BackupRetention = &value + } + err := u.writeStoredSettings(ctx, stored) + u.writeMu.Unlock() + if err != nil { return Settings{}, err } - if _, err := u.filesystem.AtomicWrite(ctx, u.settingsPath(), append(data, '\n'), false); err != nil { + return u.Settings(ctx) +} + +func (u *UseCases) saveStoredSettings(ctx context.Context, stored storedSettings) (Settings, error) { + u.writeMu.Lock() + err := u.writeStoredSettings(ctx, stored) + u.writeMu.Unlock() + if err != nil { return Settings{}, err } - return settings, nil + return u.Settings(ctx) +} + +func (u *UseCases) writeStoredSettings(ctx context.Context, stored storedSettings) error { + data, err := json.MarshalIndent(stored, "", " ") + if err != nil { + return err + } + if err := u.filesystem.EnsurePrivateDir(ctx, filepath.Dir(u.settingsPath())); err != nil { + return err + } + _, err = u.filesystem.AtomicWrite(ctx, u.settingsPath(), append(data, '\n'), false) + return err } // preferMirror reports the effective download preference. Reading it here keeps diff --git a/internal/app/settings_test.go b/internal/app/settings_test.go index 4fdd8d39..c682ceb5 100644 --- a/internal/app/settings_test.go +++ b/internal/app/settings_test.go @@ -98,6 +98,34 @@ func TestAutostartSettingDefaultsOffAndPersists(t *testing.T) { } } +func TestUpdateSettingsPreservesUntouchedPreferences(t *testing.T) { + home := t.TempDir() + core := settingsCore(t, home) + if _, err := core.SaveSettings(context.Background(), Settings{PreferMirror: true, BackupRetention: 7}); err != nil { + t.Fatal(err) + } + enable := true + updated, err := core.UpdateSettings(context.Background(), SettingsPatch{Autostart: &enable}) + if err != nil { + t.Fatal(err) + } + if !updated.Autostart || !updated.PreferMirror || updated.BackupRetention != 7 { + t.Fatalf("partial update changed untouched settings: %#v", updated) + } + + china := settingsCore(t, t.TempDir(), "zh_CN.UTF-8") + if _, err := china.UpdateSettings(context.Background(), SettingsPatch{Autostart: &enable}); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(china.settingsPath()) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(data), "prefer_mirror") { + t.Fatalf("partial update materialized regional mirror default: %s", data) + } +} + func TestBackupRetentionPersistsAndBoundsValues(t *testing.T) { home := t.TempDir() core := settingsCore(t, home) diff --git a/internal/binding/services.go b/internal/binding/services.go index 1f3f3977..0e67252f 100644 --- a/internal/binding/services.go +++ b/internal/binding/services.go @@ -189,7 +189,7 @@ func (s *RuntimeService) GetSettings(ctx context.Context) (app.Settings, error) } // SaveSettings persists the download preferences and returns what was stored. -func (s *RuntimeService) SaveSettings(ctx context.Context, request app.Settings) (app.Settings, error) { +func (s *RuntimeService) SaveSettings(ctx context.Context, request app.SettingsPatch) (app.Settings, error) { if err := contextError(ctx); err != nil { return app.Settings{}, err } @@ -197,20 +197,20 @@ func (s *RuntimeService) SaveSettings(ctx context.Context, request app.Settings) return app.Settings{}, notReady("Runtime service is not configured") } previous, changed := false, false - if s.autostart.SetEnabled != nil { + if request.Autostart != nil && s.autostart.SetEnabled != nil { if s.autostart.IsEnabled != nil { var err error previous, err = s.autostart.IsEnabled() if err != nil { return app.Settings{}, err } - changed = previous != request.Autostart + changed = previous != *request.Autostart } - if err := s.autostart.SetEnabled(request.Autostart); err != nil { + if err := s.autostart.SetEnabled(*request.Autostart); err != nil { return app.Settings{}, err } } - saved, err := s.core.SaveSettings(ctx, request) + saved, err := s.core.UpdateSettings(ctx, request) if err != nil && changed { _ = s.autostart.SetEnabled(previous) } diff --git a/internal/binding/services_test.go b/internal/binding/services_test.go index 41c43bbb..fece0c01 100644 --- a/internal/binding/services_test.go +++ b/internal/binding/services_test.go @@ -111,7 +111,8 @@ func TestRuntimeServiceAppliesAutostartCallbacks(t *testing.T) { if got, err := service.GetSettings(context.Background()); err != nil || got.Autostart { t.Fatalf("initial autostart = %#v, err=%v", got, err) } - if _, err := service.SaveSettings(context.Background(), app.Settings{Autostart: true}); err != nil { + enable := true + if _, err := service.SaveSettings(context.Background(), app.SettingsPatch{Autostart: &enable}); err != nil { t.Fatal(err) } if !enabled { From 44576ae6d3d4d9ed7cd243faf6081b32f825f2db Mon Sep 17 00:00:00 2001 From: Paul Liu <20290410+Paulkm2006@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:04:11 +0800 Subject: [PATCH 5/6] fix: match autostart toggle styling --- frontend/src/components/MirrorSetting.tsx | 1 + frontend/src/pages/SettingsPage.test.tsx | 4 ++-- frontend/src/pages/SettingsPage.tsx | 3 ++- frontend/src/styles/app.css | 6 ++++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/MirrorSetting.tsx b/frontend/src/components/MirrorSetting.tsx index 3273e050..4af0d750 100644 --- a/frontend/src/components/MirrorSetting.tsx +++ b/frontend/src/components/MirrorSetting.tsx @@ -81,6 +81,7 @@ export function MirrorSetting() { void toggle(event.target.checked)} diff --git a/frontend/src/pages/SettingsPage.test.tsx b/frontend/src/pages/SettingsPage.test.tsx index 110b7960..a6b6dc45 100644 --- a/frontend/src/pages/SettingsPage.test.tsx +++ b/frontend/src/pages/SettingsPage.test.tsx @@ -25,7 +25,7 @@ describe("SettingsPage", () => { render(} />transfer child} />); expect(screen.getByRole("combobox", { name: "外观" })).toBeTruthy(); expect(screen.getByRole("combobox", { name: "语言" })).toBeTruthy(); - expect(await screen.findByRole("switch")).toBeTruthy(); + expect(await screen.findByRole("switch", { name: "优先使用国内镜像" })).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: /导入导出/ })); expect(screen.getByRole("heading", { name: "transfer child" })).toBeTruthy(); }); @@ -91,7 +91,7 @@ describe("SettingsPage", () => { backup_retention: 3, }); show(); - const checkbox = await screen.findByRole("checkbox", { name: "开机自启动" }); + const checkbox = await screen.findByRole("switch", { name: "开机自启动" }); await userEvent.click(checkbox); await waitFor(() => expect(saveSettings).toHaveBeenCalledWith(expect.objectContaining({ autostart: true }))); }); diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index f4e80b48..e5854b53 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -169,7 +169,7 @@ export function SettingsPage() {

{t("启动设置")}

-
+