diff --git a/desktop/app.go b/desktop/app.go index 420f51b300..0399bb432f 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -693,9 +693,8 @@ func (a *App) restoreOrBuildTabs() { // Run legacy config migration before the first config load so the // freshly written config (including the user's default_model) is // picked up by Load instead of falling back to built-in defaults. - f := desktopTabsFile{} _, _ = config.MigrateLegacyIfNeeded() - f = loadTabsFile() + f := loadTabsFile() _, _ = recoverLegacyProjectSidebarRoots(f) _, _ = config.ApplyUserConfigUpgradesOnStartup(config.UserConfigPath()) _, _ = config.MigrateMCPToUserConfigOnUpgrade(desktopMCPMigrationRoots(f)) diff --git a/desktop/frontend/src/locales/en.ts b/desktop/frontend/src/locales/en.ts index 556e2f89b1..0e53cf28a5 100644 --- a/desktop/frontend/src/locales/en.ts +++ b/desktop/frontend/src/locales/en.ts @@ -2683,7 +2683,6 @@ export const en = { "notice.mcpServersFailed": "Some MCP servers failed to start; run /mcp for details.", "notice.guardianModelMissing": "Guardian was disabled because its model was not found.", "notice.guardianStartFailed": "Guardian was disabled because it could not start.", - "guard.safeMode": "Reasonix started in Safe Mode. Plugins, MCP servers, hooks, bots, automations, and saved tabs are disabled for this run.", "questionNav.label": "Question navigation", "questionNav.progress": "Question {current} / {total}", "questionNav.jump": "Jump to question {n}", diff --git a/desktop/frontend/src/locales/zh-TW.ts b/desktop/frontend/src/locales/zh-TW.ts index 654a801bec..186b84f287 100644 --- a/desktop/frontend/src/locales/zh-TW.ts +++ b/desktop/frontend/src/locales/zh-TW.ts @@ -1837,7 +1837,6 @@ export const zhTW: Record = { "notice.mcpServersFailed": "部分 MCP 服務啟動失敗,可執行 /mcp 查看詳情。", "notice.guardianModelMissing": "Guardian 已停用:未找到對應模型。", "notice.guardianStartFailed": "Guardian 啟動失敗,已停用。", - "guard.safeMode": "Reasonix 已進入安全模式。本次執行已停用外掛、MCP、Hooks、機器人、自動化和上次儲存的分頁。", "questionNav.label": "問題導航", "questionNav.progress": "問題 {current} / {total}", "questionNav.jump": "跳轉到問題 {n}", diff --git a/desktop/frontend/src/locales/zh.ts b/desktop/frontend/src/locales/zh.ts index 39a3d65a78..bf91df7065 100644 --- a/desktop/frontend/src/locales/zh.ts +++ b/desktop/frontend/src/locales/zh.ts @@ -2686,7 +2686,6 @@ export const zh: Record = { "notice.mcpServersFailed": "部分 MCP 服务启动失败,可运行 /mcp 查看详情。", "notice.guardianModelMissing": "Guardian 已停用:未找到对应模型。", "notice.guardianStartFailed": "Guardian 启动失败,已停用。", - "guard.safeMode": "Reasonix 已进入安全模式。本次运行已禁用插件、MCP、Hooks、机器人、自动化和上次保存的标签页。", "questionNav.label": "问题导航", "questionNav.progress": "问题 {current} / {total}", "questionNav.jump": "跳转到问题 {n}", diff --git a/desktop/install_profile_other.go b/desktop/install_profile_other.go deleted file mode 100644 index a61b337255..0000000000 --- a/desktop/install_profile_other.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !windows - -package main - -func telemetryInstallProfile() string { - return metricBucket(detectInstallProfile().Mode) -} diff --git a/desktop/install_profile_windows.go b/desktop/install_profile_windows.go deleted file mode 100644 index cb115557b5..0000000000 --- a/desktop/install_profile_windows.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build windows - -package main - -import ( - "os" - "path/filepath" - "strings" -) - -// telemetryInstallProfile returns only a bounded distribution label. Executable -// paths are never included in telemetry or reports. -func telemetryInstallProfile() string { - executable, err := os.Executable() - if err != nil { - return "unknown" - } - normalized := strings.ToLower(filepath.Clean(executable)) - if strings.Contains(normalized, `\scoop\apps\reasonix-desktop\`) || - strings.Contains(normalized, `\scoop\apps\reasonix\`) { - return "scoop" - } - // The NSIS package always places its per-user uninstaller beside the - // executable, including when the user selected a custom install directory. - if info, statErr := os.Stat(filepath.Join(filepath.Dir(executable), "uninstall.exe")); statErr == nil && !info.IsDir() { - return "installer" - } - if localAppData := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); localAppData != "" { - installed := strings.ToLower(filepath.Join(localAppData, "Programs", "Reasonix")) - if normalized == installed || strings.HasPrefix(normalized, installed+string(os.PathSeparator)) { - return "installer" - } - } - return "portable" -} diff --git a/desktop/main.go b/desktop/main.go index e5a19c2ca3..2a6fe1e863 100644 --- a/desktop/main.go +++ b/desktop/main.go @@ -115,8 +115,6 @@ func main() { // Observe previous run for crash diagnostics only. Startup tracking must // never force Safe Mode, disable plugins, or select a previous binary. previousRun := repair.NewStartupTracker("").ObservePreviousRun() - // Always boot the normal WebKit path. - configureWebKitRendererRecovery(false) app := NewApp() app.previousRun = previousRun diff --git a/desktop/startup_recovery.go b/desktop/startup_recovery.go deleted file mode 100644 index eebd619985..0000000000 --- a/desktop/startup_recovery.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -import ( - "errors" - "fmt" - - "reasonix/internal/repair" -) - -type desktopRecoveryChoice int - -const ( - desktopRecoveryQuit desktopRecoveryChoice = iota - desktopRecoverySafeMode - desktopRecoveryRepair -) - -type desktopStartupRecoveryDeps struct { - recoverFailedInstall func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error) - reconcilePendingUpdate func() (repair.PendingUpdateReconcileResult, error) - rollbackPendingUpdate func() (repair.UpdateRollbackResult, error) - repairGlobalConfig func() error - choose func() desktopRecoveryChoice - markClean func() error - relaunch func(string) error - report func(string) -} - -// runDesktopStartupRecovery mirrors Guard's preflight for the macOS bundle, -// whose native executable must be the Wails process so LaunchServices, the Dock, -// and the window all share one process identity. Windows and Linux still enter -// through Guard and do not use this path. -func runDesktopStartupRecovery(recommended, explicitSafeMode bool, deps desktopStartupRecoveryDeps) (safeMode, continueLaunch bool) { - safeMode = explicitSafeMode - report := func(message string) { - if deps.report != nil { - deps.report(message) - } - } - relaunchRestored := func(result repair.UpdateRollbackResult) bool { - if deps.markClean != nil { - if err := deps.markClean(); err != nil { - report("could not reset startup recovery state: " + err.Error()) - } - } - if deps.relaunch == nil { - report("the previous Reasonix version was restored, but it could not be relaunched") - return false - } - if err := deps.relaunch(result.TargetPath); err != nil { - report("the previous Reasonix version was restored, but relaunch failed: " + err.Error()) - } - return false - } - - if deps.recoverFailedInstall != nil { - result, _, err := deps.recoverFailedInstall() - if err != nil { - report("update rollback after a failed install failed: " + err.Error()) - if !result.RolledBack { - return safeMode, false - } - } - if result.RolledBack { - return safeMode, relaunchRestored(result) - } - } - - // Reconcile abandoned pre-publish transactions on every launch instead of - // waiting for a crash loop. A target release still in its health-confirmation - // window remains probationary and is intentionally left untouched. - if deps.reconcilePendingUpdate != nil { - result, err := deps.reconcilePendingUpdate() - if err != nil && !errors.Is(err, repair.ErrPendingUpdateAwaitingHealth) { - report("previous update recovery failed: " + err.Error()) - if result.MixedInstall { - return safeMode, false - } - return true, true - } - if result.RolledBack { - return safeMode, relaunchRestored(repair.UpdateRollbackResult{ - RolledBack: true, - FromVersion: result.ToVersion, - ToVersion: result.FromVersion, - TargetPath: result.TargetPath, - }) - } - } - - // An explicit Safe Mode request is an operator override. Preserve Guard's - // behavior by skipping crash-loop rollback and the recovery prompt. - if explicitSafeMode || !recommended { - return safeMode, true - } - - if deps.rollbackPendingUpdate != nil { - result, err := deps.rollbackPendingUpdate() - if err != nil { - report("update rollback failed: " + err.Error()) - if result.MixedInstall { - return safeMode, false - } - // Staging/verification failures leave the current release coherent. - // Safe Mode is therefore the conservative fallback while the pending - // transaction remains available for a later retry. - return true, true - } - if result.RolledBack { - return safeMode, relaunchRestored(result) - } - } - - choice := desktopRecoverySafeMode - if deps.choose != nil { - choice = deps.choose() - } - switch choice { - case desktopRecoveryRepair: - if deps.repairGlobalConfig != nil { - if err := deps.repairGlobalConfig(); err != nil { - report("configuration repair failed: " + err.Error()) - } - } - return true, true - case desktopRecoverySafeMode: - return true, true - case desktopRecoveryQuit: - return safeMode, false - default: - report(fmt.Sprintf("unknown recovery choice %d; starting in Safe Mode", choice)) - return true, true - } -} diff --git a/desktop/startup_recovery_darwin.go b/desktop/startup_recovery_darwin.go deleted file mode 100644 index fb511d7908..0000000000 --- a/desktop/startup_recovery_darwin.go +++ /dev/null @@ -1,63 +0,0 @@ -//go:build darwin - -package main - -import ( - "fmt" - "os" - "os/exec" - "strings" - - "reasonix/internal/repair" -) - -func preparePackagedStartupRecovery(tracker *repair.StartupTracker, recommended, explicitSafeMode bool) (bool, bool) { - return runDesktopStartupRecovery(recommended, explicitSafeMode, desktopStartupRecoveryDeps{ - recoverFailedInstall: repair.RecoverFailedInstall, - reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) { - return repair.ReconcilePendingUpdate(version) - }, - rollbackPendingUpdate: repair.RollbackPendingUpdate, - repairGlobalConfig: func() error { - _, err := repair.InspectAndRepairConfig(repair.ConfigOptions{Apply: true, OnlyScope: "global"}) - return err - }, - choose: nativeDesktopRecoveryChoice, - markClean: tracker.MarkClean, - relaunch: func(appBundle string) error { - if strings.TrimSpace(appBundle) == "" { - return fmt.Errorf("restored application bundle path is empty") - } - return exec.Command("/usr/bin/open", "-na", appBundle).Run() - }, - report: func(message string) { - fmt.Fprintln(os.Stderr, "Reasonix recovery:", message) - showDesktopRecoveryError(message) - }, - }) -} - -func nativeDesktopRecoveryChoice() desktopRecoveryChoice { - script := `button returned of (display dialog "Reasonix failed to start repeatedly. You can repair damaged global configuration or start with external integrations disabled." with title "Reasonix Recovery" buttons {"Quit", "Safe Mode", "Repair and Start"} default button "Repair and Start" with icon caution)` - out, err := exec.Command("osascript", "-e", script).Output() - if err != nil { - return desktopRecoverySafeMode - } - switch strings.TrimSpace(string(out)) { - case "Repair and Start": - return desktopRecoveryRepair - case "Safe Mode": - return desktopRecoverySafeMode - default: - return desktopRecoveryQuit - } -} - -func showDesktopRecoveryError(message string) { - // Pass the message as argv rather than interpolating AppleScript so file - // paths and command errors containing quotes cannot alter the script. - script := `on run argv -display dialog (item 1 of argv) with title "Reasonix Recovery" buttons {"OK"} default button "OK" with icon stop -end run` - _ = exec.Command("osascript", "-e", script, message).Run() -} diff --git a/desktop/startup_recovery_other.go b/desktop/startup_recovery_other.go deleted file mode 100644 index c667141a98..0000000000 --- a/desktop/startup_recovery_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !darwin - -package main - -import "reasonix/internal/repair" - -func preparePackagedStartupRecovery(_ *repair.StartupTracker, recommended, explicitSafeMode bool) (bool, bool) { - return explicitSafeMode || recommended, true -} diff --git a/desktop/startup_recovery_test.go b/desktop/startup_recovery_test.go deleted file mode 100644 index 9f4c537536..0000000000 --- a/desktop/startup_recovery_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package main - -import ( - "errors" - "strings" - "testing" - - "reasonix/internal/repair" -) - -func TestDesktopStartupRecoveryNormalLaunch(t *testing.T) { - recovered := false - reconciled := false - safeMode, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{ - recoverFailedInstall: func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error) { - recovered = true - return repair.UpdateRollbackResult{}, nil, nil - }, - reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) { - reconciled = true - return repair.PendingUpdateReconcileResult{}, nil - }, - }) - if !recovered || !reconciled || safeMode || !proceed { - t.Fatalf("recovered=%v reconciled=%v safeMode=%v proceed=%v", recovered, reconciled, safeMode, proceed) - } -} - -func TestDesktopStartupRecoveryReconcilesAbandonedUpdateBeforeCrashThreshold(t *testing.T) { - var relaunched string - markedClean := false - safeMode, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{ - reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) { - return repair.PendingUpdateReconcileResult{ - Pending: true, - RolledBack: true, - FromVersion: "v1", - ToVersion: "v2", - TargetPath: "/Applications/Reasonix.app", - }, nil - }, - markClean: func() error { markedClean = true; return nil }, - relaunch: func(path string) error { relaunched = path; return nil }, - }) - if safeMode || proceed || !markedClean || relaunched != "/Applications/Reasonix.app" { - t.Fatalf("safeMode=%v proceed=%v markedClean=%v relaunched=%q", safeMode, proceed, markedClean, relaunched) - } -} - -func TestDesktopStartupRecoveryLeavesProbationaryUpdate(t *testing.T) { - safeMode, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{ - reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) { - return repair.PendingUpdateReconcileResult{Pending: true, AwaitingHealth: true}, repair.ErrPendingUpdateAwaitingHealth - }, - }) - if safeMode || !proceed { - t.Fatalf("safeMode=%v proceed=%v", safeMode, proceed) - } -} - -func TestDesktopStartupRecoveryRelaunchesRestoredBundle(t *testing.T) { - var relaunched string - markedClean := false - safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{ - recoverFailedInstall: func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error) { - return repair.UpdateRollbackResult{}, nil, nil - }, - rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) { - return repair.UpdateRollbackResult{RolledBack: true, TargetPath: "/Applications/Reasonix.app"}, nil - }, - markClean: func() error { markedClean = true; return nil }, - relaunch: func(path string) error { relaunched = path; return nil }, - }) - if safeMode || proceed || !markedClean || relaunched != "/Applications/Reasonix.app" { - t.Fatalf("safeMode=%v proceed=%v markedClean=%v relaunched=%q", safeMode, proceed, markedClean, relaunched) - } -} - -func TestDesktopStartupRecoveryFailedInstallFailsClosed(t *testing.T) { - var reports []string - _, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{ - recoverFailedInstall: func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error) { - return repair.UpdateRollbackResult{}, &repair.UpdateApplyFailure{}, errors.New("restore failed") - }, - report: func(message string) { reports = append(reports, message) }, - }) - if proceed { - t.Fatal("desktop proceeded after an incomplete failed-install rollback") - } - if len(reports) != 1 || !strings.Contains(reports[0], "restore failed") { - t.Fatalf("reports = %#v", reports) - } -} - -func TestDesktopStartupRecoveryCoherentRollbackFailureUsesSafeMode(t *testing.T) { - safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{ - rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) { - return repair.UpdateRollbackResult{}, errors.New("backup unavailable") - }, - }) - if !safeMode || !proceed { - t.Fatalf("safeMode=%v proceed=%v, want true true", safeMode, proceed) - } -} - -func TestDesktopStartupRecoveryMixedInstallFailsClosed(t *testing.T) { - safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{ - rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) { - return repair.UpdateRollbackResult{MixedInstall: true}, errors.New("compensation failed") - }, - }) - if safeMode || proceed { - t.Fatalf("safeMode=%v proceed=%v, want false false", safeMode, proceed) - } -} - -func TestDesktopStartupRecoveryRepairStartsSafeMode(t *testing.T) { - repaired := false - safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{ - rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) { - return repair.UpdateRollbackResult{}, nil - }, - choose: func() desktopRecoveryChoice { return desktopRecoveryRepair }, - repairGlobalConfig: func() error { - repaired = true - return nil - }, - }) - if !repaired || !safeMode || !proceed { - t.Fatalf("repaired=%v safeMode=%v proceed=%v, want true true true", repaired, safeMode, proceed) - } -} - -func TestDesktopStartupRecoveryExplicitSafeModeSkipsCrashRollback(t *testing.T) { - rollbackCalled := false - safeMode, proceed := runDesktopStartupRecovery(true, true, desktopStartupRecoveryDeps{ - rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) { - rollbackCalled = true - return repair.UpdateRollbackResult{}, nil - }, - }) - if rollbackCalled || !safeMode || !proceed { - t.Fatalf("rollbackCalled=%v safeMode=%v proceed=%v", rollbackCalled, safeMode, proceed) - } -} diff --git a/desktop/theme_app.go b/desktop/theme_app.go index 902574ebe1..a98cbfe3a4 100644 --- a/desktop/theme_app.go +++ b/desktop/theme_app.go @@ -68,17 +68,13 @@ func (a *App) ListThemePacks() ([]ThemePackView, error) { activeID := resolveActiveThemeID(st) baseStyle := a.desktopBaseStyleLocked() - safe := a.themeSafeMode() var out []ThemePackView for _, m := range builtinThemePacks() { cp := m // Base "active" = no pack applied and this is the configured base style. - baseActive := !safe && activeID == "" && baseStyle == m.ID + baseActive := activeID == "" && baseStyle == m.ID out = append(out, manifestToView(&cp, themeKindBase, baseActive, "", "")) } - if safe { - return out, nil - } for _, ot := range officialThemes() { m := ot.manifest bgURL := officialAssetURL(m.ID, m.Background.Image) @@ -107,18 +103,13 @@ func (a *App) ListThemePacks() ([]ThemePackView, error) { return out, nil } -// GetActiveThemePack returns the currently enabled pack (nil pack when none / safe mode). -// Safe mode suppresses pack application but does not delete the stored id. +// GetActiveThemePack returns the currently enabled pack (nil pack when none). func (a *App) GetActiveThemePack() (ThemeActiveView, error) { themeMu.Lock() defer themeMu.Unlock() - view := ThemeActiveView{SafeMode: a.themeSafeMode()} + view := ThemeActiveView{} st := a.migrateThemeDesktopStateLocked() - if view.SafeMode { - // Do not clear ActiveThemeID — safe mode only blocks loading. - return view, nil - } activeID := resolveActiveThemeID(st) if st.ActiveThemeID != "" && activeID == "" { // Broken or migrated-away pointer: clear so the next launch is clean. @@ -146,7 +137,6 @@ func (a *App) GetThemeExperience() (ThemeExperienceView, error) { themeMu.Lock() defer themeMu.Unlock() - safe := a.themeSafeMode() // Migrate first so a v1 base-style activeThemeId lands in desktop.theme_style // before we read appearance. st := a.migrateThemeDesktopStateLocked() @@ -155,10 +145,6 @@ func (a *App) GetThemeExperience() (ThemeExperienceView, error) { ThemeMode: themeMode, BaseStyle: baseStyle, EffectiveStyle: baseStyle, - SafeMode: safe, - } - if safe { - return view, nil } activeID := resolveActiveThemeID(st) if st.ActiveThemeID != "" && activeID == "" { @@ -219,9 +205,6 @@ func (a *App) ActivateThemePack(id string) error { defer themeMu.Unlock() id = strings.TrimSpace(id) - if a.themeSafeMode() && id != "" { - return fmt.Errorf("safe mode does not load external themes") - } st := a.migrateThemeDesktopStateLocked() if id == "" { st.ActiveThemeID = "" @@ -357,9 +340,6 @@ func (a *App) SaveThemePack(input ThemeSaveInput) (ThemePackView, error) { themeMu.Lock() defer themeMu.Unlock() - if a.themeSafeMode() { - return ThemePackView{}, fmt.Errorf("safe mode cannot save themes") - } m := &ThemePackManifest{ SchemaVersion: themePackSchemaVersion, ID: strings.TrimSpace(input.ID), @@ -520,9 +500,6 @@ func (a *App) CopyThemePack(sourceID, newID, newName string) (ThemePackView, err themeMu.Lock() defer themeMu.Unlock() - if a.themeSafeMode() { - return ThemePackView{}, fmt.Errorf("safe mode cannot copy themes") - } sourceID = strings.TrimSpace(sourceID) newID = strings.TrimSpace(newID) if !themePackIDRe.MatchString(newID) || isReservedThemeID(newID) { @@ -607,10 +584,6 @@ func (a *App) ImportThemePack(sourcePath string, replace bool) (ThemeImportResul themeMu.Lock() defer themeMu.Unlock() - if a.themeSafeMode() { - return ThemeImportResult{}, fmt.Errorf("safe mode cannot import themes") - } - // Confirm a previously staged conflict without re-picking a file. path := strings.TrimSpace(sourcePath) if path == "" && replace { diff --git a/desktop/theme_assets.go b/desktop/theme_assets.go index a97619cf4d..6c0498e640 100644 --- a/desktop/theme_assets.go +++ b/desktop/theme_assets.go @@ -8,8 +8,6 @@ import ( "path/filepath" "strings" "time" - - "reasonix/internal/config" ) const themeAssetURLPrefix = "/__reasonix_theme_asset/" @@ -31,11 +29,6 @@ func (a *App) themeAssetMiddleware() func(http.Handler) http.Handler { w.WriteHeader(http.StatusMethodNotAllowed) return } - // Safe mode never serves external theme assets. - if a.themeSafeMode() { - http.NotFound(w, r) - return - } rest := strings.TrimPrefix(r.URL.Path, themeAssetURLPrefix) parts := strings.Split(rest, "/") if len(parts) != 3 { @@ -145,12 +138,6 @@ func serveOfficialThemeAsset(w http.ResponseWriter, r *http.Request, themeID, di http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(data)) } -func (a *App) themeSafeMode() bool { - // DesktopStartupSettings exposes SafeMode; keep a local helper so asset - // middleware and list/load paths share the same gate. - return config.SafeModeRequested() -} - func themeBackgroundURL(themeID, imageName string) string { if imageName == "" || isBuiltinThemeID(themeID) { return "" diff --git a/desktop/theme_pack.go b/desktop/theme_pack.go index 6dc3b1261a..b1f300aa48 100644 --- a/desktop/theme_pack.go +++ b/desktop/theme_pack.go @@ -166,7 +166,8 @@ type ThemeContrastWarning struct { type ThemeActiveView struct { ActiveThemeID string `json:"activeThemeId,omitempty"` Pack *ThemePackView `json:"pack,omitempty"` - SafeMode bool `json:"safeMode"` + // SafeMode is retained as an always-false compatibility field for older frontends. + SafeMode bool `json:"safeMode"` } // ThemeExperienceView is the unified appearance state for the redesigned @@ -178,7 +179,8 @@ type ThemeExperienceView struct { EffectiveStyle string `json:"effectiveStyle"` // pack.baseStyle when pack active, else baseStyle ActiveThemeID string `json:"activeThemeId,omitempty"` // official/user only; never a base id ActivePack *ThemePackView `json:"activePack,omitempty"` - SafeMode bool `json:"safeMode"` + // SafeMode is retained as an always-false compatibility field for older frontends. + SafeMode bool `json:"safeMode"` } // ThemeSaveInput is the editor payload for creating/updating a user theme. diff --git a/desktop/updater_deb_other.go b/desktop/updater_deb_other.go index a79edea702..b489d00e27 100644 --- a/desktop/updater_deb_other.go +++ b/desktop/updater_deb_other.go @@ -5,11 +5,7 @@ package main import "fmt" var ( - errUpdateAuthCancelled = fmt.Errorf("update: authorization cancelled") errUpdateAuthFailed = fmt.Errorf("update: authorization failed") - errUpdatePkgBusy = fmt.Errorf("update: package manager busy") - errUpdatePkgInstall = fmt.Errorf("update: package install failed") - errUpdatePkgVerify = fmt.Errorf("update: package verify failed") errUpdateCacheMismatch = fmt.Errorf("update: cached artifact does not match current install mode") ) diff --git a/desktop/updater_install.go b/desktop/updater_install.go index 567a71cff2..acccd222d6 100644 --- a/desktop/updater_install.go +++ b/desktop/updater_install.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "path/filepath" "runtime" "strings" @@ -126,25 +125,6 @@ func selectUpdateAsset(m *update.Manifest, profile installProfile) (update.Asset return update.Asset{}, "", false } -// dirIsWritable reports whether the process can create a temporary file in dir. -func dirIsWritable(dir string) bool { - if dir == "" { - return false - } - info, err := os.Stat(dir) - if err != nil || !info.IsDir() { - return false - } - f, err := os.CreateTemp(dir, ".reasonix-write-test-*") - if err != nil { - return false - } - name := f.Name() - _ = f.Close() - _ = os.Remove(name) - return true -} - func linuxDebHelperReady() bool { if runtime.GOOS != "linux" { return false @@ -158,18 +138,6 @@ func linuxDebHelperReady() bool { return true } -// resolveExecutablePath returns the real path of the running binary. -func resolveExecutablePath() string { - exe, err := os.Executable() - if err != nil { - return "" - } - if resolved, err := filepath.EvalSymlinks(exe); err == nil { - exe = resolved - } - return exe -} - func manualDebInstallHint() string { return "Install manually with: sudo apt install ./Reasonix-linux-amd64.deb" } diff --git a/desktop/updater_install_linux.go b/desktop/updater_install_linux.go index a70ab010b2..b5ab80e404 100644 --- a/desktop/updater_install_linux.go +++ b/desktop/updater_install_linux.go @@ -10,6 +10,37 @@ import ( "strings" ) +// dirIsWritable reports whether the process can create a temporary file in dir. +func dirIsWritable(dir string) bool { + if dir == "" { + return false + } + info, err := os.Stat(dir) + if err != nil || !info.IsDir() { + return false + } + f, err := os.CreateTemp(dir, ".reasonix-write-test-*") + if err != nil { + return false + } + name := f.Name() + _ = f.Close() + _ = os.Remove(name) + return true +} + +// resolveExecutablePath returns the real path of the running binary. +func resolveExecutablePath() string { + exe, err := os.Executable() + if err != nil { + return "" + } + if resolved, err := filepath.EvalSymlinks(exe); err == nil { + exe = resolved + } + return exe +} + // detectLinuxInstallProfile classifies the running Linux install. // // deb — dpkg owns the absolute executable path as reasonix-desktop, and the diff --git a/desktop/webkit_compat_linux.go b/desktop/webkit_compat_linux.go index 46b8ceabac..4d50ab5d5e 100644 --- a/desktop/webkit_compat_linux.go +++ b/desktop/webkit_compat_linux.go @@ -108,18 +108,6 @@ import "C" // the verified JavaScriptCore signal-handler race, but is not intended to cover // failures that occur earlier while WebKit constructs the window. -// configureWebKitRendererRecovery applies WebKit's costly DMA-BUF fallback only -// during Safe Mode on NVIDIA systems. Normal launches keep accelerated backing -// store enabled; NVIDIA Wayland sessions continue to use the narrower explicit- -// sync workaround in nvidia_wayland_linux.go. Call this before wails.Run so the -// WebKit process inherits the environment. -func configureWebKitRendererRecovery(safeMode bool) { - if !safeMode { - return - } - configureWebKitRendererRecoveryForGPU(safeMode, hasNVIDIAGPU()) -} - // scheduleWebKitSignalHandlerRepair starts the bounded GLib timer immediately // before wails.Run. The callback begins executing when Wails enters GTK's main // loop, which anchors the five-second repair window to WebKit startup instead diff --git a/desktop/webkit_compat_other.go b/desktop/webkit_compat_other.go index 7b7e5d2a04..d324cc389e 100644 --- a/desktop/webkit_compat_other.go +++ b/desktop/webkit_compat_other.go @@ -2,8 +2,6 @@ package main -func configureWebKitRendererRecovery(bool) {} - func scheduleWebKitSignalHandlerRepair() {} func repairWebKitSignalHandlers() {} diff --git a/desktop/webkit_compat_other_test.go b/desktop/webkit_compat_other_test.go deleted file mode 100644 index e727172de4..0000000000 --- a/desktop/webkit_compat_other_test.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build !linux - -package main - -import ( - "os" - "testing" -) - -func TestConfigureWebKitRendererRecoveryIsNoop(t *testing.T) { - const key = "WEBKIT_DISABLE_DMABUF_RENDERER" - previous, set := os.LookupEnv(key) - if err := os.Unsetenv(key); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if set { - _ = os.Setenv(key, previous) - } else { - _ = os.Unsetenv(key) - } - }) - - configureWebKitRendererRecovery(true) - if value, exists := os.LookupEnv(key); exists { - t.Fatalf("non-Linux recovery unexpectedly set %s=%q", key, value) - } -} diff --git a/desktop/webkit_compat_policy.go b/desktop/webkit_compat_policy.go deleted file mode 100644 index da1f82673e..0000000000 --- a/desktop/webkit_compat_policy.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import "os" - -// configureWebKitRendererRecoveryForGPU keeps the environment policy separate -// from Linux GPU detection so every platform can regression-test the decision. -// Only the Linux wrapper invokes it in production. -func configureWebKitRendererRecoveryForGPU(safeMode, nvidiaGPU bool) { - if !safeMode || !nvidiaGPU { - return - } - if _, explicitlySet := os.LookupEnv("WEBKIT_DISABLE_DMABUF_RENDERER"); explicitlySet { - return - } - _ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1") -} diff --git a/desktop/webkit_compat_policy_test.go b/desktop/webkit_compat_policy_test.go deleted file mode 100644 index be25ce3ad1..0000000000 --- a/desktop/webkit_compat_policy_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "os" - "testing" -) - -func TestConfigureWebKitRendererRecoveryForGPU(t *testing.T) { - const key = "WEBKIT_DISABLE_DMABUF_RENDERER" - tests := []struct { - name string - safeMode bool - nvidiaGPU bool - explicit *string - wantSet bool - wantValue string - }{ - {name: "normal NVIDIA launch keeps acceleration", nvidiaGPU: true}, - {name: "safe mode without NVIDIA keeps acceleration", safeMode: true}, - {name: "safe mode NVIDIA launch disables DMA-BUF", safeMode: true, nvidiaGPU: true, wantSet: true, wantValue: "1"}, - {name: "preconfigured zero value is preserved", safeMode: true, nvidiaGPU: true, explicit: stringPointer("0"), wantSet: true, wantValue: "0"}, - {name: "preconfigured enabled value is preserved", safeMode: true, nvidiaGPU: true, explicit: stringPointer("1"), wantSet: true, wantValue: "1"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - restoreEnv(t, key) - if tt.explicit != nil { - if err := os.Setenv(key, *tt.explicit); err != nil { - t.Fatal(err) - } - } - - configureWebKitRendererRecoveryForGPU(tt.safeMode, tt.nvidiaGPU) - - got, set := os.LookupEnv(key) - if set != tt.wantSet || got != tt.wantValue { - t.Fatalf("%s = %q, set=%v; want %q, set=%v", key, got, set, tt.wantValue, tt.wantSet) - } - }) - } -} - -func stringPointer(value string) *string { return &value } - -func restoreEnv(t *testing.T, key string) { - t.Helper() - previous, set := os.LookupEnv(key) - if err := os.Unsetenv(key); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if set { - _ = os.Setenv(key, previous) - } else { - _ = os.Unsetenv(key) - } - }) -} diff --git a/desktop/webview2_startup.go b/desktop/webview2_startup.go index dd3c373564..0126c7d3ed 100644 --- a/desktop/webview2_startup.go +++ b/desktop/webview2_startup.go @@ -12,9 +12,9 @@ const windowsWebView2StartupFallbackDelay = 15 * time.Second const windowsWebView2StartupFallbackMessage = "The desktop interface did not become ready within 15 seconds. " + "An unavailable Windows system proxy or a WebView2 failure may be blocking startup. " + - "Check the system proxy, then restart Reasonix. If the problem continues, start Reasonix with --safe-mode.\n\n" + + "Check the system proxy, then restart Reasonix. If the problem continues, repair Microsoft Edge WebView2 Runtime or reinstall Reasonix.\n\n" + "桌面界面在 15 秒内未能就绪。不可用的 Windows 系统代理或 WebView2 故障可能阻塞了启动。" + - "请检查系统代理后重启 Reasonix;如果问题仍然存在,请使用 --safe-mode 启动。" + "请检查系统代理后重启 Reasonix;如果问题仍然存在,请修复 Microsoft Edge WebView2 Runtime 或重新安装 Reasonix。" // startWindowsWebView2StartupFallback prevents StartHidden from turning a slow // or failed WebView2 navigation into an apparently missing application. Proxy