From e76abac24ee23561d2ac287a4f290d9a4b156da2 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 17 Dec 2025 14:37:18 -0500 Subject: [PATCH 01/13] app: add upgrade configuration to settings page --- app/cmd/app/app.go | 22 +++- app/cmd/app/app_darwin.go | 8 ++ app/cmd/app/app_darwin.h | 1 + app/cmd/app/app_darwin.m | 12 ++ app/cmd/app/app_windows.go | 4 + app/store/database.go | 32 ++++- app/store/store.go | 3 + app/ui/app/codegen/gotypes.gen.ts | 42 +++++++ app/ui/app/src/api.ts | 65 ++++++++++ app/ui/app/src/components/Settings.tsx | 75 +++++++++++- app/ui/responses/types.go | 11 ++ app/ui/ui.go | 157 +++++++++++++++++++++++-- app/updater/updater.go | 60 +++++++++- app/version/version.go | 35 ++++++ app/wintray/menus.go | 28 +++++ app/wintray/tray.go | 32 ++--- app/wintray/w32api.go | 1 + 17 files changed, 552 insertions(+), 36 deletions(-) diff --git a/app/cmd/app/app.go b/app/cmd/app/app.go index 7e183b8df..0b1880234 100644 --- a/app/cmd/app/app.go +++ b/app/cmd/app/app.go @@ -253,6 +253,8 @@ func main() { done <- osrv.Run(octx) }() + upd := &updater.Updater{Store: st} + uiServer := ui.Server{ Token: token, Restart: func() { @@ -267,6 +269,13 @@ func main() { ToolRegistry: toolRegistry, Dev: devMode, Logger: slog.Default(), + Updater: upd, + UpdateAvailableFunc: func() { + UpdateAvailable("") + }, + ClearUpdateAvailableFunc: func() { + ClearUpdateAvailable() + }, } srv := &http.Server{ @@ -284,8 +293,7 @@ func main() { slog.Debug("background desktop server done") }() - updater := &updater.Updater{Store: st} - updater.StartBackgroundUpdaterChecker(ctx, UpdateAvailable) + upd.StartBackgroundUpdaterChecker(ctx, UpdateAvailable) hasCompletedFirstRun, err := st.HasCompletedFirstRun() if err != nil { @@ -348,6 +356,16 @@ func startHiddenTasks() { // CLI triggered app startup use-case slog.Info("deferring pending update for fast startup") } else { + // Check if auto-update is enabled before upgrading + st := &store.Store{} + settings, err := st.Settings() + if err != nil { + slog.Warn("failed to load settings for upgrade check", "error", err) + } else if !settings.AutoUpdateEnabled { + slog.Info("auto-update disabled, skipping automatic upgrade at startup") + return + } + if err := updater.DoUpgradeAtStartup(); err != nil { slog.Info("unable to perform upgrade at startup", "error", err) // Make sure the restart to upgrade menu shows so we can attempt an interactive upgrade to get authorization diff --git a/app/cmd/app/app_darwin.go b/app/cmd/app/app_darwin.go index 8e886a124..5d5138304 100644 --- a/app/cmd/app/app_darwin.go +++ b/app/cmd/app/app_darwin.go @@ -172,6 +172,14 @@ func UpdateAvailable(ver string) error { return nil } +func ClearUpdateAvailable() error { + slog.Debug("clearing update notification") + if updater.BundlePath != "" { + C.clearUpdateAvailable() + } + return nil +} + func osRun(_ func(), hasCompletedFirstRun, startHidden bool) { registerLaunchAgent(hasCompletedFirstRun) diff --git a/app/cmd/app/app_darwin.h b/app/cmd/app/app_darwin.h index 4a5ba055f..c9c510872 100644 --- a/app/cmd/app/app_darwin.h +++ b/app/cmd/app/app_darwin.h @@ -30,6 +30,7 @@ void StartUpdate(); void darwinStartHiddenTasks(); void launchApp(const char *appPath); void updateAvailable(); +void clearUpdateAvailable(); void quit(); void uiRequest(char *path); void registerSelfAsLoginItem(bool firstTimeRun); diff --git a/app/cmd/app/app_darwin.m b/app/cmd/app/app_darwin.m index d5095ab25..dd817ce18 100644 --- a/app/cmd/app/app_darwin.m +++ b/app/cmd/app/app_darwin.m @@ -241,6 +241,12 @@ bool firstTimeRun,startHidden; // Set in run before initialization [self showIcon]; } +- (void)clearUpdateAvailable { + self.updateAvailable = NO; + [self.statusItem.menu.itemArray[3] setHidden:YES]; + [self.statusItem.menu.itemArray[4] setHidden:YES]; +} + - (void)aboutOllama { [[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil]; [NSApp activateIgnoringOtherApps:YES]; @@ -973,6 +979,12 @@ void updateAvailable() { }); } +void clearUpdateAvailable() { + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate clearUpdateAvailable]; + }); +} + void quit() { dispatch_async(dispatch_get_main_queue(), ^{ [appDelegate quit]; diff --git a/app/cmd/app/app_windows.go b/app/cmd/app/app_windows.go index 9caeb178a..76af39f19 100644 --- a/app/cmd/app/app_windows.go +++ b/app/cmd/app/app_windows.go @@ -157,6 +157,10 @@ func UpdateAvailable(ver string) error { return app.t.UpdateAvailable(ver) } +func ClearUpdateAvailable() error { + return app.t.ClearUpdateAvailable() +} + func osRun(shutdown func(), hasCompletedFirstRun, startHidden bool) { var err error app.shutdown = shutdown diff --git a/app/store/database.go b/app/store/database.go index 0f268c6fa..3443f974b 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -14,7 +14,7 @@ import ( // currentSchemaVersion defines the current database schema version. // Increment this when making schema changes that require migrations. -const currentSchemaVersion = 12 +const currentSchemaVersion = 13 // database wraps the SQLite connection. // SQLite handles its own locking for concurrent access: @@ -85,6 +85,7 @@ func (db *database) init() error { think_enabled BOOLEAN NOT NULL DEFAULT 0, think_level TEXT NOT NULL DEFAULT '', remote TEXT NOT NULL DEFAULT '', -- deprecated + auto_update_enabled BOOLEAN NOT NULL DEFAULT 1, schema_version INTEGER NOT NULL DEFAULT %d ); @@ -244,6 +245,12 @@ func (db *database) migrate() error { return fmt.Errorf("migrate v11 to v12: %w", err) } version = 12 + case 12: + // add auto_update_enabled column to settings table + if err := db.migrateV12ToV13(); err != nil { + return fmt.Errorf("migrate v12 to v13: %w", err) + } + version = 13 default: // If we have a version we don't recognize, just set it to current // This might happen during development @@ -452,6 +459,21 @@ func (db *database) migrateV11ToV12() error { return nil } +// migrateV12ToV13 adds the auto_update_enabled column to the settings table +func (db *database) migrateV12ToV13() error { + _, err := db.conn.Exec(`ALTER TABLE settings ADD COLUMN auto_update_enabled BOOLEAN NOT NULL DEFAULT 1`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add auto_update_enabled column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 13`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + // cleanupOrphanedData removes orphaned records that may exist due to the foreign key bug func (db *database) cleanupOrphanedData() error { _, err := db.conn.Exec(` @@ -1108,9 +1130,9 @@ func (db *database) getSettings() (Settings, error) { var s Settings err := db.conn.QueryRow(` - SELECT expose, survey, browser, models, agent, tools, working_dir, context_length, airplane_mode, turbo_enabled, websearch_enabled, selected_model, sidebar_open, think_enabled, think_level + SELECT expose, survey, browser, models, agent, tools, working_dir, context_length, airplane_mode, turbo_enabled, websearch_enabled, selected_model, sidebar_open, think_enabled, think_level, auto_update_enabled FROM settings - `).Scan(&s.Expose, &s.Survey, &s.Browser, &s.Models, &s.Agent, &s.Tools, &s.WorkingDir, &s.ContextLength, &s.AirplaneMode, &s.TurboEnabled, &s.WebSearchEnabled, &s.SelectedModel, &s.SidebarOpen, &s.ThinkEnabled, &s.ThinkLevel) + `).Scan(&s.Expose, &s.Survey, &s.Browser, &s.Models, &s.Agent, &s.Tools, &s.WorkingDir, &s.ContextLength, &s.AirplaneMode, &s.TurboEnabled, &s.WebSearchEnabled, &s.SelectedModel, &s.SidebarOpen, &s.ThinkEnabled, &s.ThinkLevel, &s.AutoUpdateEnabled) if err != nil { return Settings{}, fmt.Errorf("get settings: %w", err) } @@ -1121,8 +1143,8 @@ func (db *database) getSettings() (Settings, error) { func (db *database) setSettings(s Settings) error { _, err := db.conn.Exec(` UPDATE settings - SET expose = ?, survey = ?, browser = ?, models = ?, agent = ?, tools = ?, working_dir = ?, context_length = ?, airplane_mode = ?, turbo_enabled = ?, websearch_enabled = ?, selected_model = ?, sidebar_open = ?, think_enabled = ?, think_level = ? - `, s.Expose, s.Survey, s.Browser, s.Models, s.Agent, s.Tools, s.WorkingDir, s.ContextLength, s.AirplaneMode, s.TurboEnabled, s.WebSearchEnabled, s.SelectedModel, s.SidebarOpen, s.ThinkEnabled, s.ThinkLevel) + SET expose = ?, survey = ?, browser = ?, models = ?, agent = ?, tools = ?, working_dir = ?, context_length = ?, airplane_mode = ?, turbo_enabled = ?, websearch_enabled = ?, selected_model = ?, sidebar_open = ?, think_enabled = ?, think_level = ?, auto_update_enabled = ? + `, s.Expose, s.Survey, s.Browser, s.Models, s.Agent, s.Tools, s.WorkingDir, s.ContextLength, s.AirplaneMode, s.TurboEnabled, s.WebSearchEnabled, s.SelectedModel, s.SidebarOpen, s.ThinkEnabled, s.ThinkLevel, s.AutoUpdateEnabled) if err != nil { return fmt.Errorf("set settings: %w", err) } diff --git a/app/store/store.go b/app/store/store.go index 052fcd617..ac3111dd5 100644 --- a/app/store/store.go +++ b/app/store/store.go @@ -169,6 +169,9 @@ type Settings struct { // SidebarOpen indicates if the chat sidebar is open SidebarOpen bool + + // AutoUpdateEnabled indicates if automatic updates should be downloaded + AutoUpdateEnabled bool } type Store struct { diff --git a/app/ui/app/codegen/gotypes.gen.ts b/app/ui/app/codegen/gotypes.gen.ts index 0bf86f2b4..347e2c157 100644 --- a/app/ui/app/codegen/gotypes.gen.ts +++ b/app/ui/app/codegen/gotypes.gen.ts @@ -413,6 +413,7 @@ export class Settings { ThinkLevel: string; SelectedModel: string; SidebarOpen: boolean; + AutoUpdateEnabled: boolean; constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); @@ -431,6 +432,7 @@ export class Settings { this.ThinkLevel = source["ThinkLevel"]; this.SelectedModel = source["SelectedModel"]; this.SidebarOpen = source["SidebarOpen"]; + this.AutoUpdateEnabled = source["AutoUpdateEnabled"]; } } export class SettingsResponse { @@ -467,6 +469,46 @@ export class HealthResponse { this.healthy = source["healthy"]; } } +export class UpdateInfo { + currentVersion: string; + availableVersion: string; + updateAvailable: boolean; + updateDownloaded: boolean; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.currentVersion = source["currentVersion"]; + this.availableVersion = source["availableVersion"]; + this.updateAvailable = source["updateAvailable"]; + this.updateDownloaded = source["updateDownloaded"]; + } +} +export class UpdateCheckResponse { + updateInfo: UpdateInfo; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.updateInfo = this.convertValues(source["updateInfo"], UpdateInfo); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} export class User { id: string; email: string; diff --git a/app/ui/app/src/api.ts b/app/ui/app/src/api.ts index 273850d6b..80debe508 100644 --- a/app/ui/app/src/api.ts +++ b/app/ui/app/src/api.ts @@ -414,3 +414,68 @@ export async function fetchHealth(): Promise { return false; } } + +export async function getCurrentVersion(): Promise { + try { + const response = await fetch(`${API_BASE}/api/version`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (response.ok) { + const data = await response.json(); + return data.version || "Unknown"; + } + return "Unknown"; + } catch (error) { + console.error("Error fetching version:", error); + return "Unknown"; + } +} + +export async function checkForUpdate(): Promise<{ + currentVersion: string; + availableVersion: string; + updateAvailable: boolean; + updateDownloaded: boolean; +}> { + const response = await fetch(`${API_BASE}/api/v1/update/check`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + throw new Error("Failed to check for update"); + } + const data = await response.json(); + return data.updateInfo; +} + +export async function downloadUpdate(version: string): Promise { + const response = await fetch(`${API_BASE}/api/v1/update/download`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ version }), + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(error || "Failed to download update"); + } +} + +export async function installUpdate(): Promise { + const response = await fetch(`${API_BASE}/api/v1/update/install`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(error || "Failed to install update"); + } +} diff --git a/app/ui/app/src/components/Settings.tsx b/app/ui/app/src/components/Settings.tsx index 057f7477d..e628f25a7 100644 --- a/app/ui/app/src/components/Settings.tsx +++ b/app/ui/app/src/components/Settings.tsx @@ -14,12 +14,13 @@ import { XMarkIcon, CogIcon, ArrowLeftIcon, + ArrowDownTrayIcon, } from "@heroicons/react/20/solid"; import { Settings as SettingsType } from "@/gotypes"; import { useNavigate } from "@tanstack/react-router"; import { useUser } from "@/hooks/useUser"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { getSettings, updateSettings } from "@/api"; +import { getSettings, updateSettings, checkForUpdate } from "@/api"; function AnimatedDots() { return ( @@ -39,6 +40,12 @@ export default function Settings() { const queryClient = useQueryClient(); const [showSaved, setShowSaved] = useState(false); const [restartMessage, setRestartMessage] = useState(false); + const [updateInfo, setUpdateInfo] = useState<{ + currentVersion: string; + availableVersion: string; + updateAvailable: boolean; + updateDownloaded: boolean; + } | null>(null); const { user, isAuthenticated, @@ -76,8 +83,22 @@ export default function Settings() { useEffect(() => { refetchUser(); + // Check for updates + checkForUpdate() + .then(setUpdateInfo) + .catch((err) => console.error("Error checking for update:", err)); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Refresh update info when auto-update toggle changes + useEffect(() => { + if (settings?.AutoUpdateEnabled !== undefined) { + checkForUpdate() + .then(setUpdateInfo) + .catch((err) => console.error("Error checking for update:", err)); + } + }, [settings?.AutoUpdateEnabled]); + + useEffect(() => { const handleFocus = () => { if (isAwaitingConnection && pollingInterval) { @@ -344,6 +365,58 @@ export default function Settings() { {/* Local Configuration */}
+ {/* Auto Update */} + +
+
+ +
+ + + {settings.AutoUpdateEnabled ? ( + <> + Automatically downloads updates and restarts the app. +
+ Current version: {updateInfo?.currentVersion || "Loading..."} +
+ + ) : ( + <> + You must manually check for updates. +
+
+
+ Current version: {updateInfo?.currentVersion || "Loading..."} +
+ {updateInfo?.availableVersion && ( +
+ Available version: {updateInfo?.availableVersion} +
+ )} +
+ + Download new version → + +
+ + )} +
+
+
+
+ handleChange("AutoUpdateEnabled", checked)} + /> +
+
+
+ {/* Expose Ollama */}
diff --git a/app/ui/responses/types.go b/app/ui/responses/types.go index 2da6623fd..edbf08049 100644 --- a/app/ui/responses/types.go +++ b/app/ui/responses/types.go @@ -100,6 +100,17 @@ type HealthResponse struct { Healthy bool `json:"healthy"` } +type UpdateInfo struct { + CurrentVersion string `json:"currentVersion"` + AvailableVersion string `json:"availableVersion"` + UpdateAvailable bool `json:"updateAvailable"` + UpdateDownloaded bool `json:"updateDownloaded"` +} + +type UpdateCheckResponse struct { + UpdateInfo UpdateInfo `json:"updateInfo"` +} + type User struct { ID string `json:"id"` Email string `json:"email"` diff --git a/app/ui/ui.go b/app/ui/ui.go index 26de71422..cdd006c98 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -28,6 +28,7 @@ import ( "github.com/ollama/ollama/app/tools" "github.com/ollama/ollama/app/types/not" "github.com/ollama/ollama/app/ui/responses" + "github.com/ollama/ollama/app/updater" "github.com/ollama/ollama/app/version" ollamaAuth "github.com/ollama/ollama/auth" "github.com/ollama/ollama/envconfig" @@ -94,18 +95,30 @@ const ( ) type Server struct { - Logger *slog.Logger - Restart func() - Token string - Store *store.Store - ToolRegistry *tools.Registry - Tools bool // if true, the server will use single-turn tools to fulfill the user's request - WebSearch bool // if true, the server will use single-turn browser tool to fulfill the user's request - Agent bool // if true, the server will use multi-turn tools to fulfill the user's request - WorkingDir string // Working directory for all agent operations + Logger *slog.Logger + Restart func() + Token string + Store *store.Store + ToolRegistry *tools.Registry + Tools bool // if true, the server will use single-turn tools to fulfill the user's request + WebSearch bool // if true, the server will use single-turn browser tool to fulfill the user's request + Agent bool // if true, the server will use multi-turn tools to fulfill the user's request + WorkingDir string // Working directory for all agent operations // Dev is true if the server is running in development mode Dev bool + + // Updater for checking and downloading updates + Updater UpdaterInterface + UpdateAvailableFunc func() + ClearUpdateAvailableFunc func() +} + +// UpdaterInterface defines the methods we need from the updater +type UpdaterInterface interface { + CheckForUpdate(ctx context.Context) (bool, string, error) + DownloadUpdate(ctx context.Context, updateVersion string) error + InstallAndRestart() error } func (s *Server) log() *slog.Logger { @@ -250,7 +263,7 @@ func (s *Server) Handler() http.Handler { }() w.Header().Set("X-Frame-Options", "DENY") - w.Header().Set("X-Version", version.Version) + w.Header().Set("X-Version", version.GetVersion()) w.Header().Set("X-Request-ID", requestID) ctx := r.Context() @@ -284,6 +297,9 @@ func (s *Server) Handler() http.Handler { mux.Handle("POST /api/v1/model/upstream", handle(s.modelUpstream)) mux.Handle("GET /api/v1/settings", handle(s.getSettings)) mux.Handle("POST /api/v1/settings", handle(s.settings)) + mux.Handle("GET /api/v1/update/check", handle(s.checkForUpdate)) + mux.Handle("POST /api/v1/update/download", handle(s.downloadUpdate)) + mux.Handle("POST /api/v1/update/install", handle(s.installUpdate)) // Ollama proxy endpoints ollamaProxy := s.ollamaProxy() @@ -1448,6 +1464,19 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error { return fmt.Errorf("failed to save settings: %w", err) } + // Update tray notification based on auto-update toggle + if old.AutoUpdateEnabled && !settings.AutoUpdateEnabled { + // Auto-update disabled: clear tray notification + if s.ClearUpdateAvailableFunc != nil { + s.ClearUpdateAvailableFunc() + } + } else if !old.AutoUpdateEnabled && settings.AutoUpdateEnabled { + // Auto-update enabled: show tray notification if update is pending + if (updater.IsUpdatePending() || updater.UpdateDownloaded) && s.UpdateAvailableFunc != nil { + s.UpdateAvailableFunc() + } + } + if old.ContextLength != settings.ContextLength || old.Models != settings.Models || old.Expose != settings.Expose { @@ -1524,6 +1553,114 @@ func (s *Server) modelUpstream(w http.ResponseWriter, r *http.Request) error { return json.NewEncoder(w).Encode(response) } +func (s *Server) checkForUpdate(w http.ResponseWriter, r *http.Request) error { + if s.Updater == nil { + return fmt.Errorf("updater not available") + } + + updateAvailable, updateVersion, err := s.Updater.CheckForUpdate(r.Context()) + if err != nil { + s.log().Warn("failed to check for update", "error", err) + // Don't return error, just log it and continue with no update available + } + + // Get current version with fallbacks for development + currentVersion := version.GetVersion() + + response := responses.UpdateCheckResponse{ + UpdateInfo: responses.UpdateInfo{ + CurrentVersion: currentVersion, + AvailableVersion: updateVersion, + UpdateAvailable: updateAvailable, + UpdateDownloaded: updater.UpdateDownloaded, + }, + } + + w.Header().Set("Content-Type", "application/json") + return json.NewEncoder(w).Encode(response) +} + +func (s *Server) downloadUpdate(w http.ResponseWriter, r *http.Request) error { + if r.Method != "POST" { + return fmt.Errorf("method not allowed") + } + + if s.Updater == nil { + return fmt.Errorf("updater not available") + } + + var req struct { + Version string `json:"version"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return fmt.Errorf("invalid request body: %w", err) + } + + if req.Version == "" { + return fmt.Errorf("version is required") + } + + err := s.Updater.DownloadUpdate(r.Context(), req.Version) + if err != nil { + s.log().Error("failed to download update", "error", err, "version", req.Version) + return fmt.Errorf("failed to download update: %w", err) + } + + response := map[string]any{ + "success": true, + "message": "Update downloaded successfully", + } + + w.Header().Set("Content-Type", "application/json") + return json.NewEncoder(w).Encode(response) +} + +func (s *Server) installUpdate(w http.ResponseWriter, r *http.Request) error { + if r.Method != "POST" { + return fmt.Errorf("method not allowed") + } + + if s.Updater == nil { + s.log().Error("install failed: updater not available") + return fmt.Errorf("updater not available") + } + + // Check if update is downloaded + if !updater.UpdateDownloaded { + s.log().Error("install failed: no update downloaded") + return fmt.Errorf("no update downloaded") + } + + // Check if we can actually upgrade (not in dev mode) + if updater.BundlePath == "" { + return fmt.Errorf("cannot install updates in development mode") + } + + // Send response before restarting + response := map[string]any{ + "success": true, + "message": "Installing update and restarting...", + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + return err + } + + // Give the response time to be sent + time.Sleep(500 * time.Millisecond) + + // Trigger the upgrade and restart + go func() { + time.Sleep(500 * time.Millisecond) + if err := s.Updater.InstallAndRestart(); err != nil { + s.log().Error("failed to install update", "error", err) + } + }() + + return nil +} + func userAgent() string { buildinfo, _ := debug.ReadBuildInfo() diff --git a/app/updater/updater.go b/app/updater/updater.go index 473ecf466..4e10d71d6 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -58,8 +58,11 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { query := requestURL.Query() query.Add("os", runtime.GOOS) query.Add("arch", runtime.GOARCH) - query.Add("version", version.Version) + currentVersion := version.GetVersion() + query.Add("version", currentVersion) query.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) + + slog.Debug("checking for update", "current_version", currentVersion, "os", runtime.GOOS, "arch", runtime.GOARCH) // The original macOS app used to use the device ID // to check for updates so include it if present @@ -94,7 +97,7 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { if signature != "" { req.Header.Set("Authorization", signature) } - ua := fmt.Sprintf("ollama/%s %s Go/%s %s", version.Version, runtime.GOARCH, runtime.Version(), UserAgentOS) + ua := fmt.Sprintf("ollama/%s %s Go/%s %s", version.GetVersion(), runtime.GOARCH, runtime.Version(), UserAgentOS) req.Header.Set("User-Agent", ua) slog.Debug("checking for available update", "requestURL", requestURL, "User-Agent", ua) @@ -176,6 +179,7 @@ func (u *Updater) DownloadNewRelease(ctx context.Context, updateResp UpdateRespo _, err = os.Stat(stageFilename) if err == nil { slog.Info("update already downloaded", "bundle", stageFilename) + UpdateDownloaded = true return nil } @@ -253,6 +257,22 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str time.Sleep(UpdateCheckInitialDelay) slog.Info("beginning update checker", "interval", UpdateCheckInterval) for { + // Check if auto-update is enabled + settings, err := u.Store.Settings() + if err != nil { + slog.Error("failed to load settings", "error", err) + time.Sleep(UpdateCheckInterval) + continue + } + + if !settings.AutoUpdateEnabled { + // When auto-update is disabled, don't check or download anything + slog.Debug("auto-update disabled, skipping check") + time.Sleep(UpdateCheckInterval) + continue + } + + // Auto-update is enabled - proceed with normal flow available, resp := u.checkForUpdate(ctx) if available { err := u.DownloadNewRelease(ctx, resp) @@ -275,3 +295,39 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str } }() } + +func (u *Updater) CheckForUpdate(ctx context.Context) (bool, string, error) { + available, resp := u.checkForUpdate(ctx) + return available, resp.UpdateVersion, nil +} + +func (u *Updater) DownloadUpdate(ctx context.Context, updateVersion string) error { + // First check for update to get the actual download URL + available, updateResp := u.checkForUpdate(ctx) + + if !available { + return fmt.Errorf("no update available") + } + if updateResp.UpdateVersion != updateVersion { + slog.Error("version mismatch", "requested", updateVersion, "available", updateResp.UpdateVersion) + return fmt.Errorf("version mismatch: requested %s, available %s", updateVersion, updateResp.UpdateVersion) + } + + slog.Info("downloading update", "version", updateVersion) + err := u.DownloadNewRelease(ctx, updateResp) + if err != nil { + return err + } + + slog.Info("update downloaded successfully", "version", updateVersion) + return nil +} + +func (u *Updater) InstallAndRestart() error { + if !UpdateDownloaded { + return fmt.Errorf("no update downloaded") + } + + slog.Info("installing update and restarting") + return DoUpgrade(true) +} diff --git a/app/version/version.go b/app/version/version.go index 5955d601c..7c0bdc5ac 100644 --- a/app/version/version.go +++ b/app/version/version.go @@ -2,4 +2,39 @@ package version +import ( + "os/exec" + "runtime/debug" + "strings" +) + var Version string = "0.0.0" + +// GetVersion returns the version, with fallback to git or build info +func GetVersion() string { + // If version is set via ldflags, use it + if Version != "" && Version != "0.0.0" { + return Version + } + + // Try to get from build info + if buildinfo, ok := debug.ReadBuildInfo(); ok { + if buildinfo.Main.Version != "" && buildinfo.Main.Version != "(devel)" { + return buildinfo.Main.Version + } + } + + // In development, try to get from git + if cmd := exec.Command("git", "describe", "--tags", "--first-parent", "--abbrev=7", "--long", "--dirty", "--always"); cmd != nil { + if output, err := cmd.Output(); err == nil { + version := strings.TrimSpace(string(output)) + version = strings.TrimPrefix(version, "v") + if version != "" { + return version + } + } + } + + // Fallback + return "dev" +} diff --git a/app/wintray/menus.go b/app/wintray/menus.go index 106953b7a..6e752ef57 100644 --- a/app/wintray/menus.go +++ b/app/wintray/menus.go @@ -47,6 +47,34 @@ func (t *winTray) initMenus() error { return nil } +func (t *winTray) ClearUpdateAvailable() error { + if t.updateNotified { + slog.Debug("clearing update notification and menu items") + if err := t.removeMenuItem(updateSeparatorMenuID, 0); err != nil { + return fmt.Errorf("unable to remove menu entries %w", err) + } + if err := t.removeMenuItem(updateAvailableMenuID, 0); err != nil { + return fmt.Errorf("unable to remove menu entries %w", err) + } + if err := t.removeMenuItem(updateMenuID, 0); err != nil { + return fmt.Errorf("unable to remove menu entries %w", err) + } + if err := t.removeMenuItem(separatorMenuID, 0); err != nil { + return fmt.Errorf("unable to remove menu entries %w", err) + } + iconFilePath, err := iconBytesToFilePath(wt.normalIcon) + if err != nil { + return fmt.Errorf("unable to write icon data to temp file: %w", err) + } + if err := t.setIcon(iconFilePath); err != nil { + return fmt.Errorf("unable to set icon: %w", err) + } + t.updateNotified = false + t.pendingUpdate = false + } + return nil +} + func (t *winTray) UpdateAvailable(ver string) error { if !t.updateNotified { slog.Debug("updating menu and sending notification for new update") diff --git a/app/wintray/tray.go b/app/wintray/tray.go index 71d4bc767..0c1a2ed0f 100644 --- a/app/wintray/tray.go +++ b/app/wintray/tray.go @@ -369,24 +369,24 @@ func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { return nil } -// func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { -// const ERROR_SUCCESS syscall.Errno = 0 +func (t *winTray) removeMenuItem(menuItemId, parentId uint32) error { + const ERROR_SUCCESS syscall.Errno = 0 -// t.muMenus.RLock() -// menu := uintptr(t.menus[parentId]) -// t.muMenus.RUnlock() -// res, _, err := pRemoveMenu.Call( -// menu, -// uintptr(menuItemId), -// MF_BYCOMMAND, -// ) -// if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { -// return err -// } -// t.delFromVisibleItems(parentId, menuItemId) + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pRemoveMenu.Call( + menu, + uintptr(menuItemId), + MF_BYCOMMAND, + ) + if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { + return err + } + t.delFromVisibleItems(parentId, menuItemId) -// return nil -// } + return nil +} func (t *winTray) showMenu() error { p := point{} diff --git a/app/wintray/w32api.go b/app/wintray/w32api.go index 10e255816..792813a34 100644 --- a/app/wintray/w32api.go +++ b/app/wintray/w32api.go @@ -30,6 +30,7 @@ var ( pPostQuitMessage = u32.NewProc("PostQuitMessage") pRegisterClass = u32.NewProc("RegisterClassExW") pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW") + pRemoveMenu = u32.NewProc("RemoveMenu") pSendMessage = u32.NewProc("SendMessageW") pSetForegroundWindow = u32.NewProc("SetForegroundWindow") pSetMenuInfo = u32.NewProc("SetMenuInfo") From cf7e5e88bc6e1475e3963867f46718992d4aa9e8 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 17 Dec 2025 16:16:53 -0500 Subject: [PATCH 02/13] fix test --- app/ui/ui.go | 18 +++++++++--------- app/updater/updater.go | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/ui/ui.go b/app/ui/ui.go index cdd006c98..a5d27739e 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -95,15 +95,15 @@ const ( ) type Server struct { - Logger *slog.Logger - Restart func() - Token string - Store *store.Store - ToolRegistry *tools.Registry - Tools bool // if true, the server will use single-turn tools to fulfill the user's request - WebSearch bool // if true, the server will use single-turn browser tool to fulfill the user's request - Agent bool // if true, the server will use multi-turn tools to fulfill the user's request - WorkingDir string // Working directory for all agent operations + Logger *slog.Logger + Restart func() + Token string + Store *store.Store + ToolRegistry *tools.Registry + Tools bool // if true, the server will use single-turn tools to fulfill the user's request + WebSearch bool // if true, the server will use single-turn browser tool to fulfill the user's request + Agent bool // if true, the server will use multi-turn tools to fulfill the user's request + WorkingDir string // Working directory for all agent operations // Dev is true if the server is running in development mode Dev bool diff --git a/app/updater/updater.go b/app/updater/updater.go index 4e10d71d6..7b03aa841 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -61,7 +61,7 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { currentVersion := version.GetVersion() query.Add("version", currentVersion) query.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) - + slog.Debug("checking for update", "current_version", currentVersion, "os", runtime.GOOS, "arch", runtime.GOARCH) // The original macOS app used to use the device ID @@ -264,7 +264,7 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str time.Sleep(UpdateCheckInterval) continue } - + if !settings.AutoUpdateEnabled { // When auto-update is disabled, don't check or download anything slog.Debug("auto-update disabled, skipping check") @@ -304,7 +304,7 @@ func (u *Updater) CheckForUpdate(ctx context.Context) (bool, string, error) { func (u *Updater) DownloadUpdate(ctx context.Context, updateVersion string) error { // First check for update to get the actual download URL available, updateResp := u.checkForUpdate(ctx) - + if !available { return fmt.Errorf("no update available") } @@ -312,13 +312,13 @@ func (u *Updater) DownloadUpdate(ctx context.Context, updateVersion string) erro slog.Error("version mismatch", "requested", updateVersion, "available", updateResp.UpdateVersion) return fmt.Errorf("version mismatch: requested %s, available %s", updateVersion, updateResp.UpdateVersion) } - + slog.Info("downloading update", "version", updateVersion) err := u.DownloadNewRelease(ctx, updateResp) if err != nil { return err } - + slog.Info("update downloaded successfully", "version", updateVersion) return nil } @@ -327,7 +327,7 @@ func (u *Updater) InstallAndRestart() error { if !UpdateDownloaded { return fmt.Errorf("no update downloaded") } - + slog.Info("installing update and restarting") return DoUpgrade(true) } From 5a5d3260f40542584acaeb2d0aa9a126e43466af Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 17 Dec 2025 16:57:43 -0500 Subject: [PATCH 03/13] fix behaviour when switching between enabled and disabled --- app/ui/ui.go | 41 ++++++++++++++++++++++++++++++++--------- app/updater/updater.go | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/app/ui/ui.go b/app/ui/ui.go index a5d27739e..f75ec1b43 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -119,6 +119,7 @@ type UpdaterInterface interface { CheckForUpdate(ctx context.Context) (bool, string, error) DownloadUpdate(ctx context.Context, updateVersion string) error InstallAndRestart() error + CancelOngoingDownload() } func (s *Server) log() *slog.Logger { @@ -1466,15 +1467,41 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error { // Update tray notification based on auto-update toggle if old.AutoUpdateEnabled && !settings.AutoUpdateEnabled { - // Auto-update disabled: clear tray notification + // Auto-update disabled: cancel any ongoing download and clear tray notification + if s.Updater != nil { + s.Updater.CancelOngoingDownload() + } if s.ClearUpdateAvailableFunc != nil { s.ClearUpdateAvailableFunc() } } else if !old.AutoUpdateEnabled && settings.AutoUpdateEnabled { - // Auto-update enabled: show tray notification if update is pending - if (updater.IsUpdatePending() || updater.UpdateDownloaded) && s.UpdateAvailableFunc != nil { - s.UpdateAvailableFunc() - } + // Auto-update enabled: check for updates and download if available + go func() { + // First, show notification if update is already pending + if (updater.IsUpdatePending() || updater.UpdateDownloaded) && s.UpdateAvailableFunc != nil { + s.UpdateAvailableFunc() + } else if s.Updater != nil { + // Otherwise, immediately check for and download new updates + slog.Info("auto-update re-enabled, checking for updates") + available, updateVersion, err := s.Updater.CheckForUpdate(r.Context()) + if err != nil { + slog.Error("failed to check for update after re-enabling auto-update", "error", err) + return + } + if available { + slog.Info("update available, starting download", "version", updateVersion) + err := s.Updater.DownloadUpdate(r.Context(), updateVersion) + if err != nil { + slog.Error("failed to download update", "error", err) + return + } + // Show tray notification after successful download + if s.UpdateAvailableFunc != nil { + s.UpdateAvailableFunc() + } + } + } + }() } if old.ContextLength != settings.ContextLength || @@ -1631,10 +1658,6 @@ func (s *Server) installUpdate(w http.ResponseWriter, r *http.Request) error { return fmt.Errorf("no update downloaded") } - // Check if we can actually upgrade (not in dev mode) - if updater.BundlePath == "" { - return fmt.Errorf("cannot install updates in development mode") - } // Send response before restarting response := map[string]any{ diff --git a/app/updater/updater.go b/app/updater/updater.go index 7b03aa841..e40916dde 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -19,6 +19,7 @@ import ( "runtime" "strconv" "strings" + "sync" "time" "github.com/ollama/ollama/app/store" @@ -134,15 +135,27 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { } func (u *Updater) DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error { + // Create a cancellable context for this download + downloadCtx, cancel := context.WithCancel(ctx) + u.cancelDownloadLock.Lock() + u.cancelDownload = cancel + u.cancelDownloadLock.Unlock() + defer func() { + u.cancelDownloadLock.Lock() + u.cancelDownload = nil + u.cancelDownloadLock.Unlock() + cancel() + }() + // Do a head first to check etag info - req, err := http.NewRequestWithContext(ctx, http.MethodHead, updateResp.UpdateURL, nil) + req, err := http.NewRequestWithContext(downloadCtx, http.MethodHead, updateResp.UpdateURL, nil) if err != nil { return err } // In case of slow downloads, continue the update check in the background - bgctx, cancel := context.WithCancel(ctx) - defer cancel() + bgctx, bgcancel := context.WithCancel(downloadCtx) + defer bgcancel() go func() { for { select { @@ -248,7 +261,20 @@ func cleanupOldDownloads(stageDir string) { } type Updater struct { - Store *store.Store + Store *store.Store + cancelDownload context.CancelFunc + cancelDownloadLock sync.Mutex +} + +// CancelOngoingDownload cancels any currently running download +func (u *Updater) CancelOngoingDownload() { + u.cancelDownloadLock.Lock() + defer u.cancelDownloadLock.Unlock() + if u.cancelDownload != nil { + slog.Info("cancelling ongoing update download") + u.cancelDownload() + u.cancelDownload = nil + } } func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) { From dc573715c4fded9486f9d06f905b39041c773063 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 17 Dec 2025 17:13:03 -0500 Subject: [PATCH 04/13] clean up --- app/ui/ui.go | 49 ++++++++++++++---------------------------- app/updater/updater.go | 37 +++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/app/ui/ui.go b/app/ui/ui.go index f75ec1b43..c7da1239a 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -120,6 +120,7 @@ type UpdaterInterface interface { DownloadUpdate(ctx context.Context, updateVersion string) error InstallAndRestart() error CancelOngoingDownload() + TriggerImmediateCheck() } func (s *Server) log() *slog.Logger { @@ -1465,43 +1466,25 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error { return fmt.Errorf("failed to save settings: %w", err) } - // Update tray notification based on auto-update toggle - if old.AutoUpdateEnabled && !settings.AutoUpdateEnabled { - // Auto-update disabled: cancel any ongoing download and clear tray notification - if s.Updater != nil { - s.Updater.CancelOngoingDownload() - } - if s.ClearUpdateAvailableFunc != nil { - s.ClearUpdateAvailableFunc() - } - } else if !old.AutoUpdateEnabled && settings.AutoUpdateEnabled { - // Auto-update enabled: check for updates and download if available - go func() { - // First, show notification if update is already pending + // Handle auto-update toggle changes + if old.AutoUpdateEnabled != settings.AutoUpdateEnabled { + if !settings.AutoUpdateEnabled { + // Auto-update disabled: cancel any ongoing download and clear tray notification + if s.Updater != nil { + s.Updater.CancelOngoingDownload() + } + if s.ClearUpdateAvailableFunc != nil { + s.ClearUpdateAvailableFunc() + } + } else { + // Auto-update re-enabled: show notification if update is already staged, or trigger immediate check if (updater.IsUpdatePending() || updater.UpdateDownloaded) && s.UpdateAvailableFunc != nil { s.UpdateAvailableFunc() } else if s.Updater != nil { - // Otherwise, immediately check for and download new updates - slog.Info("auto-update re-enabled, checking for updates") - available, updateVersion, err := s.Updater.CheckForUpdate(r.Context()) - if err != nil { - slog.Error("failed to check for update after re-enabling auto-update", "error", err) - return - } - if available { - slog.Info("update available, starting download", "version", updateVersion) - err := s.Updater.DownloadUpdate(r.Context(), updateVersion) - if err != nil { - slog.Error("failed to download update", "error", err) - return - } - // Show tray notification after successful download - if s.UpdateAvailableFunc != nil { - s.UpdateAvailableFunc() - } - } + // Trigger the background checker to run immediately + s.Updater.TriggerImmediateCheck() } - }() + } } if old.ContextLength != settings.ContextLength || diff --git a/app/updater/updater.go b/app/updater/updater.go index e40916dde..862a1a36e 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -264,6 +264,7 @@ type Updater struct { Store *store.Store cancelDownload context.CancelFunc cancelDownloadLock sync.Mutex + checkNow chan struct{} } // CancelOngoingDownload cancels any currently running download @@ -277,24 +278,45 @@ func (u *Updater) CancelOngoingDownload() { } } +// TriggerImmediateCheck signals the background checker to check for updates immediately +func (u *Updater) TriggerImmediateCheck() { + if u.checkNow != nil { + u.checkNow <- struct{}{} + } +} + func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) { + u.checkNow = make(chan struct{}, 1) + go func() { // Don't blast an update message immediately after startup time.Sleep(UpdateCheckInitialDelay) slog.Info("beginning update checker", "interval", UpdateCheckInterval) + + ticker := time.NewTicker(UpdateCheckInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + slog.Debug("stopping background update checker") + return + case <-u.checkNow: + // Immediate check triggered + case <-ticker.C: + // Regular interval check + } + // Check if auto-update is enabled settings, err := u.Store.Settings() if err != nil { slog.Error("failed to load settings", "error", err) - time.Sleep(UpdateCheckInterval) continue } if !settings.AutoUpdateEnabled { // When auto-update is disabled, don't check or download anything slog.Debug("auto-update disabled, skipping check") - time.Sleep(UpdateCheckInterval) continue } @@ -303,21 +325,14 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str if available { err := u.DownloadNewRelease(ctx, resp) if err != nil { - slog.Error(fmt.Sprintf("failed to download new release: %s", err)) + slog.Error("failed to download new release", "error", err) } else { err = cb(resp.UpdateVersion) if err != nil { - slog.Warn(fmt.Sprintf("failed to register update available with tray: %s", err)) + slog.Warn("failed to register update available with tray", "error", err) } } } - select { - case <-ctx.Done(): - slog.Debug("stopping background update checker") - return - default: - time.Sleep(UpdateCheckInterval) - } } }() } From ba25f4a898a863b33092d7327d515a635e0f7ee7 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 17 Dec 2025 17:17:31 -0500 Subject: [PATCH 05/13] fix test --- app/ui/ui.go | 1 - app/updater/updater.go | 1 - 2 files changed, 2 deletions(-) diff --git a/app/ui/ui.go b/app/ui/ui.go index c7da1239a..1e8e51fc9 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -1641,7 +1641,6 @@ func (s *Server) installUpdate(w http.ResponseWriter, r *http.Request) error { return fmt.Errorf("no update downloaded") } - // Send response before restarting response := map[string]any{ "success": true, diff --git a/app/updater/updater.go b/app/updater/updater.go index 862a1a36e..eeeb25bdf 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -287,7 +287,6 @@ func (u *Updater) TriggerImmediateCheck() { func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) { u.checkNow = make(chan struct{}, 1) - go func() { // Don't blast an update message immediately after startup time.Sleep(UpdateCheckInitialDelay) From 880b4f95b4a20d7070f19c9b9c810b21720e87ac Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 17 Dec 2025 17:28:21 -0500 Subject: [PATCH 06/13] fix test --- app/updater/updater.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/updater/updater.go b/app/updater/updater.go index eeeb25bdf..f6baec0cf 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -290,8 +290,7 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str go func() { // Don't blast an update message immediately after startup time.Sleep(UpdateCheckInitialDelay) - slog.Info("beginning update checker", "interval", UpdateCheckInterval) - + slog.Info("beginning update checker", "interval", UpdateCheckInterval) ticker := time.NewTicker(UpdateCheckInterval) defer ticker.Stop() From 0eb320e74c18fdd3c6d6529db8e0e0f257e764ec Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Wed, 17 Dec 2025 17:36:10 -0500 Subject: [PATCH 07/13] fix format --- app/updater/updater.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/updater/updater.go b/app/updater/updater.go index f6baec0cf..e7f123317 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -290,10 +290,10 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str go func() { // Don't blast an update message immediately after startup time.Sleep(UpdateCheckInitialDelay) - slog.Info("beginning update checker", "interval", UpdateCheckInterval) + slog.Info("beginning update checker", "interval", UpdateCheckInterval) ticker := time.NewTicker(UpdateCheckInterval) defer ticker.Stop() - + for { select { case <-ctx.Done(): @@ -304,7 +304,7 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str case <-ticker.C: // Regular interval check } - + // Check if auto-update is enabled settings, err := u.Store.Settings() if err != nil { From c6f941adb37dd008e6602c78a259db9b8aec9131 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Thu, 18 Dec 2025 10:29:07 -0500 Subject: [PATCH 08/13] fix test --- app/store/database.go | 14 +++----------- app/updater/updater_test.go | 10 ++++++++++ app/wintray/tray.go | 1 + 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/store/database.go b/app/store/database.go index 3443f974b..b0b1155c2 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -9,7 +9,7 @@ import ( "strings" "time" - sqlite3 "github.com/mattn/go-sqlite3" + _ "github.com/mattn/go-sqlite3" ) // currentSchemaVersion defines the current database schema version. @@ -504,19 +504,11 @@ func (db *database) cleanupOrphanedData() error { } func duplicateColumnError(err error) bool { - if sqlite3Err, ok := err.(sqlite3.Error); ok { - return sqlite3Err.Code == sqlite3.ErrError && - strings.Contains(sqlite3Err.Error(), "duplicate column name") - } - return false + return err != nil && strings.Contains(err.Error(), "duplicate column name") } func columnNotExists(err error) bool { - if sqlite3Err, ok := err.(sqlite3.Error); ok { - return sqlite3Err.Code == sqlite3.ErrError && - strings.Contains(sqlite3Err.Error(), "no such column") - } - return false + return err != nil && strings.Contains(err.Error(), "no such column") } func (db *database) getAllChats() ([]Chat, error) { diff --git a/app/updater/updater_test.go b/app/updater/updater_test.go index dea820c28..b4e8bea6b 100644 --- a/app/updater/updater_test.go +++ b/app/updater/updater_test.go @@ -86,6 +86,16 @@ func TestBackgoundChecker(t *testing.T) { updater := &Updater{Store: &store.Store{}} defer updater.Store.Close() // Ensure database is closed + + settings, err := updater.Store.Settings() + if err != nil { + t.Fatal(err) + } + settings.AutoUpdateEnabled = true + if err := updater.Store.SetSettings(settings); err != nil { + t.Fatal(err) + } + updater.StartBackgroundUpdaterChecker(ctx, cb) select { case <-stallTimer.C: diff --git a/app/wintray/tray.go b/app/wintray/tray.go index 0c1a2ed0f..0271f47d4 100644 --- a/app/wintray/tray.go +++ b/app/wintray/tray.go @@ -41,6 +41,7 @@ type TrayCallbacks interface { Quit() TrayRun() UpdateAvailable(ver string) error + ClearUpdateAvailable() error GetIconHandle() windows.Handle } From e55fbf2475e065baa501e6978657e1144c5065e6 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Thu, 18 Dec 2025 11:06:40 -0500 Subject: [PATCH 09/13] fix: gofmt formatting in updater_test.go --- app/updater/updater_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/updater/updater_test.go b/app/updater/updater_test.go index b4e8bea6b..60fab9ad1 100644 --- a/app/updater/updater_test.go +++ b/app/updater/updater_test.go @@ -85,8 +85,8 @@ func TestBackgoundChecker(t *testing.T) { UpdateCheckURLBase = server.URL + "/update.json" updater := &Updater{Store: &store.Store{}} - defer updater.Store.Close() // Ensure database is closed - + defer updater.Store.Close() + settings, err := updater.Store.Settings() if err != nil { t.Fatal(err) @@ -95,7 +95,7 @@ func TestBackgoundChecker(t *testing.T) { if err := updater.Store.SetSettings(settings); err != nil { t.Fatal(err) } - + updater.StartBackgroundUpdaterChecker(ctx, cb) select { case <-stallTimer.C: From 75500c88554ecce560fa4c15100449207f8de825 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Thu, 18 Dec 2025 16:37:24 -0500 Subject: [PATCH 10/13] address comment --- app/ui/ui.go | 7 +++---- app/updater/updater.go | 33 ++++++++++++++++++--------------- app/version/version.go | 35 ----------------------------------- 3 files changed, 21 insertions(+), 54 deletions(-) diff --git a/app/ui/ui.go b/app/ui/ui.go index 1e8e51fc9..d55f848b2 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -265,7 +265,7 @@ func (s *Server) Handler() http.Handler { }() w.Header().Set("X-Frame-Options", "DENY") - w.Header().Set("X-Version", version.GetVersion()) + w.Header().Set("X-Version", version.Version) w.Header().Set("X-Request-ID", requestID) ctx := r.Context() @@ -1564,6 +1564,8 @@ func (s *Server) modelUpstream(w http.ResponseWriter, r *http.Request) error { } func (s *Server) checkForUpdate(w http.ResponseWriter, r *http.Request) error { + currentVersion := version.Version + if s.Updater == nil { return fmt.Errorf("updater not available") } @@ -1574,9 +1576,6 @@ func (s *Server) checkForUpdate(w http.ResponseWriter, r *http.Request) error { // Don't return error, just log it and continue with no update available } - // Get current version with fallbacks for development - currentVersion := version.GetVersion() - response := responses.UpdateCheckResponse{ UpdateInfo: responses.UpdateInfo{ CurrentVersion: currentVersion, diff --git a/app/updater/updater.go b/app/updater/updater.go index e7f123317..b7a3d0dfd 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -59,7 +59,7 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { query := requestURL.Query() query.Add("os", runtime.GOOS) query.Add("arch", runtime.GOARCH) - currentVersion := version.GetVersion() + currentVersion := version.Version query.Add("version", currentVersion) query.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) @@ -98,7 +98,7 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { if signature != "" { req.Header.Set("Authorization", signature) } - ua := fmt.Sprintf("ollama/%s %s Go/%s %s", version.GetVersion(), runtime.GOARCH, runtime.Version(), UserAgentOS) + ua := fmt.Sprintf("ollama/%s %s Go/%s %s", version.Version, runtime.GOARCH, runtime.Version(), UserAgentOS) req.Header.Set("User-Agent", ua) slog.Debug("checking for available update", "requestURL", requestURL, "User-Agent", ua) @@ -305,7 +305,13 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str // Regular interval check } - // Check if auto-update is enabled + // Always check for updates + available, resp := u.checkForUpdate(ctx) + if !available { + continue + } + + // Update is available - check if auto-update is enabled settings, err := u.Store.Settings() if err != nil { slog.Error("failed to load settings", "error", err) @@ -313,22 +319,19 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str } if !settings.AutoUpdateEnabled { - // When auto-update is disabled, don't check or download anything - slog.Debug("auto-update disabled, skipping check") + // Auto-update disabled - don't download, just log + slog.Debug("update available but auto-update disabled", "version", resp.UpdateVersion) continue } - // Auto-update is enabled - proceed with normal flow - available, resp := u.checkForUpdate(ctx) - if available { - err := u.DownloadNewRelease(ctx, resp) + // Auto-update is enabled - download and notify + err = u.DownloadNewRelease(ctx, resp) + if err != nil { + slog.Error("failed to download new release", "error", err) + } else { + err = cb(resp.UpdateVersion) if err != nil { - slog.Error("failed to download new release", "error", err) - } else { - err = cb(resp.UpdateVersion) - if err != nil { - slog.Warn("failed to register update available with tray", "error", err) - } + slog.Warn("failed to register update available with tray", "error", err) } } } diff --git a/app/version/version.go b/app/version/version.go index 7c0bdc5ac..5955d601c 100644 --- a/app/version/version.go +++ b/app/version/version.go @@ -2,39 +2,4 @@ package version -import ( - "os/exec" - "runtime/debug" - "strings" -) - var Version string = "0.0.0" - -// GetVersion returns the version, with fallback to git or build info -func GetVersion() string { - // If version is set via ldflags, use it - if Version != "" && Version != "0.0.0" { - return Version - } - - // Try to get from build info - if buildinfo, ok := debug.ReadBuildInfo(); ok { - if buildinfo.Main.Version != "" && buildinfo.Main.Version != "(devel)" { - return buildinfo.Main.Version - } - } - - // In development, try to get from git - if cmd := exec.Command("git", "describe", "--tags", "--first-parent", "--abbrev=7", "--long", "--dirty", "--always"); cmd != nil { - if output, err := cmd.Output(); err == nil { - version := strings.TrimSpace(string(output)) - version = strings.TrimPrefix(version, "v") - if version != "" { - return version - } - } - } - - // Fallback - return "dev" -} From 391fb88bce4e66c3f734242e9a1af38ac527431c Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Thu, 18 Dec 2025 16:48:14 -0500 Subject: [PATCH 11/13] address comment --- app/updater/updater.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/updater/updater.go b/app/updater/updater.go index b7a3d0dfd..d05a375d8 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -63,8 +63,6 @@ func (u *Updater) checkForUpdate(ctx context.Context) (bool, UpdateResponse) { query.Add("version", currentVersion) query.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) - slog.Debug("checking for update", "current_version", currentVersion, "os", runtime.GOOS, "arch", runtime.GOARCH) - // The original macOS app used to use the device ID // to check for updates so include it if present if runtime.GOOS == "darwin" { From 9a5c14c58b6ed1a5fba38c4712c14cd354e45d18 Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Fri, 19 Dec 2025 15:27:15 -0500 Subject: [PATCH 12/13] address comments --- app/cmd/app/app.go | 19 ++++--- app/cmd/app/app_darwin.go | 8 --- app/cmd/app/app_darwin.h | 1 - app/cmd/app/app_darwin.m | 12 ---- app/ui/app/src/api.ts | 14 ----- app/ui/app/src/components/Settings.tsx | 4 +- app/ui/ui.go | 47 +--------------- app/updater/updater.go | 76 ++++++++++---------------- app/wintray/menus.go | 28 ---------- app/wintray/tray.go | 1 - 10 files changed, 45 insertions(+), 165 deletions(-) diff --git a/app/cmd/app/app.go b/app/cmd/app/app.go index 0b1880234..7bc73cd75 100644 --- a/app/cmd/app/app.go +++ b/app/cmd/app/app.go @@ -267,15 +267,12 @@ func main() { }, Store: st, ToolRegistry: toolRegistry, - Dev: devMode, - Logger: slog.Default(), - Updater: upd, + Dev: devMode, + Logger: slog.Default(), + Updater: upd, UpdateAvailableFunc: func() { UpdateAvailable("") }, - ClearUpdateAvailableFunc: func() { - ClearUpdateAvailable() - }, } srv := &http.Server{ @@ -295,6 +292,12 @@ func main() { upd.StartBackgroundUpdaterChecker(ctx, UpdateAvailable) + // Check for pending updates on startup (show tray notification if update is ready) + if updater.IsUpdatePending() { + slog.Debug("update pending on startup, showing tray notification") + UpdateAvailable("") + } + hasCompletedFirstRun, err := st.HasCompletedFirstRun() if err != nil { slog.Error("failed to load has completed first run", "error", err) @@ -356,13 +359,15 @@ func startHiddenTasks() { // CLI triggered app startup use-case slog.Info("deferring pending update for fast startup") } else { - // Check if auto-update is enabled before upgrading + // Check if auto-update is enabled before automatically upgrading st := &store.Store{} settings, err := st.Settings() if err != nil { slog.Warn("failed to load settings for upgrade check", "error", err) } else if !settings.AutoUpdateEnabled { slog.Info("auto-update disabled, skipping automatic upgrade at startup") + // Still show tray notification so user knows update is ready + UpdateAvailable("") return } diff --git a/app/cmd/app/app_darwin.go b/app/cmd/app/app_darwin.go index 5d5138304..8e886a124 100644 --- a/app/cmd/app/app_darwin.go +++ b/app/cmd/app/app_darwin.go @@ -172,14 +172,6 @@ func UpdateAvailable(ver string) error { return nil } -func ClearUpdateAvailable() error { - slog.Debug("clearing update notification") - if updater.BundlePath != "" { - C.clearUpdateAvailable() - } - return nil -} - func osRun(_ func(), hasCompletedFirstRun, startHidden bool) { registerLaunchAgent(hasCompletedFirstRun) diff --git a/app/cmd/app/app_darwin.h b/app/cmd/app/app_darwin.h index c9c510872..4a5ba055f 100644 --- a/app/cmd/app/app_darwin.h +++ b/app/cmd/app/app_darwin.h @@ -30,7 +30,6 @@ void StartUpdate(); void darwinStartHiddenTasks(); void launchApp(const char *appPath); void updateAvailable(); -void clearUpdateAvailable(); void quit(); void uiRequest(char *path); void registerSelfAsLoginItem(bool firstTimeRun); diff --git a/app/cmd/app/app_darwin.m b/app/cmd/app/app_darwin.m index dd817ce18..d5095ab25 100644 --- a/app/cmd/app/app_darwin.m +++ b/app/cmd/app/app_darwin.m @@ -241,12 +241,6 @@ bool firstTimeRun,startHidden; // Set in run before initialization [self showIcon]; } -- (void)clearUpdateAvailable { - self.updateAvailable = NO; - [self.statusItem.menu.itemArray[3] setHidden:YES]; - [self.statusItem.menu.itemArray[4] setHidden:YES]; -} - - (void)aboutOllama { [[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil]; [NSApp activateIgnoringOtherApps:YES]; @@ -979,12 +973,6 @@ void updateAvailable() { }); } -void clearUpdateAvailable() { - dispatch_async(dispatch_get_main_queue(), ^{ - [appDelegate clearUpdateAvailable]; - }); -} - void quit() { dispatch_async(dispatch_get_main_queue(), ^{ [appDelegate quit]; diff --git a/app/ui/app/src/api.ts b/app/ui/app/src/api.ts index 80debe508..689051f9b 100644 --- a/app/ui/app/src/api.ts +++ b/app/ui/app/src/api.ts @@ -453,20 +453,6 @@ export async function checkForUpdate(): Promise<{ return data.updateInfo; } -export async function downloadUpdate(version: string): Promise { - const response = await fetch(`${API_BASE}/api/v1/update/download`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ version }), - }); - if (!response.ok) { - const error = await response.text(); - throw new Error(error || "Failed to download update"); - } -} - export async function installUpdate(): Promise { const response = await fetch(`${API_BASE}/api/v1/update/install`, { method: "POST", diff --git a/app/ui/app/src/components/Settings.tsx b/app/ui/app/src/components/Settings.tsx index e628f25a7..c3ae9b82d 100644 --- a/app/ui/app/src/components/Settings.tsx +++ b/app/ui/app/src/components/Settings.tsx @@ -375,14 +375,14 @@ export default function Settings() { {settings.AutoUpdateEnabled ? ( <> - Automatically downloads updates and restarts the app. + Automatically downloads updates when available.
Current version: {updateInfo?.currentVersion || "Loading..."}
) : ( <> - You must manually check for updates. + Manually download updates.
diff --git a/app/ui/ui.go b/app/ui/ui.go index d55f848b2..0b8beb599 100644 --- a/app/ui/ui.go +++ b/app/ui/ui.go @@ -109,15 +109,13 @@ type Server struct { Dev bool // Updater for checking and downloading updates - Updater UpdaterInterface - UpdateAvailableFunc func() - ClearUpdateAvailableFunc func() + Updater UpdaterInterface + UpdateAvailableFunc func() } // UpdaterInterface defines the methods we need from the updater type UpdaterInterface interface { CheckForUpdate(ctx context.Context) (bool, string, error) - DownloadUpdate(ctx context.Context, updateVersion string) error InstallAndRestart() error CancelOngoingDownload() TriggerImmediateCheck() @@ -300,7 +298,6 @@ func (s *Server) Handler() http.Handler { mux.Handle("GET /api/v1/settings", handle(s.getSettings)) mux.Handle("POST /api/v1/settings", handle(s.settings)) mux.Handle("GET /api/v1/update/check", handle(s.checkForUpdate)) - mux.Handle("POST /api/v1/update/download", handle(s.downloadUpdate)) mux.Handle("POST /api/v1/update/install", handle(s.installUpdate)) // Ollama proxy endpoints @@ -1469,13 +1466,10 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) error { // Handle auto-update toggle changes if old.AutoUpdateEnabled != settings.AutoUpdateEnabled { if !settings.AutoUpdateEnabled { - // Auto-update disabled: cancel any ongoing download and clear tray notification + // Auto-update disabled: cancel any ongoing download if s.Updater != nil { s.Updater.CancelOngoingDownload() } - if s.ClearUpdateAvailableFunc != nil { - s.ClearUpdateAvailableFunc() - } } else { // Auto-update re-enabled: show notification if update is already staged, or trigger immediate check if (updater.IsUpdatePending() || updater.UpdateDownloaded) && s.UpdateAvailableFunc != nil { @@ -1589,41 +1583,6 @@ func (s *Server) checkForUpdate(w http.ResponseWriter, r *http.Request) error { return json.NewEncoder(w).Encode(response) } -func (s *Server) downloadUpdate(w http.ResponseWriter, r *http.Request) error { - if r.Method != "POST" { - return fmt.Errorf("method not allowed") - } - - if s.Updater == nil { - return fmt.Errorf("updater not available") - } - - var req struct { - Version string `json:"version"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fmt.Errorf("invalid request body: %w", err) - } - - if req.Version == "" { - return fmt.Errorf("version is required") - } - - err := s.Updater.DownloadUpdate(r.Context(), req.Version) - if err != nil { - s.log().Error("failed to download update", "error", err, "version", req.Version) - return fmt.Errorf("failed to download update: %w", err) - } - - response := map[string]any{ - "success": true, - "message": "Update downloaded successfully", - } - - w.Header().Set("Content-Type", "application/json") - return json.NewEncoder(w).Encode(response) -} - func (s *Server) installUpdate(w http.ResponseWriter, r *http.Request) error { if r.Method != "POST" { return fmt.Errorf("method not allowed") diff --git a/app/updater/updater.go b/app/updater/updater.go index d05a375d8..b29a4f83a 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -303,35 +303,37 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str // Regular interval check } - // Always check for updates - available, resp := u.checkForUpdate(ctx) - if !available { - continue - } + // Always check for updates + available, resp := u.checkForUpdate(ctx) + if !available { + continue + } - // Update is available - check if auto-update is enabled - settings, err := u.Store.Settings() - if err != nil { - slog.Error("failed to load settings", "error", err) - continue - } + // Update is available - check if auto-update is enabled for downloading + settings, err := u.Store.Settings() + if err != nil { + slog.Error("failed to load settings", "error", err) + continue + } - if !settings.AutoUpdateEnabled { - // Auto-update disabled - don't download, just log - slog.Debug("update available but auto-update disabled", "version", resp.UpdateVersion) - continue - } + if !settings.AutoUpdateEnabled { + // Auto-update disabled - don't download, just log + slog.Debug("update available but auto-update disabled", "version", resp.UpdateVersion) + continue + } - // Auto-update is enabled - download and notify - err = u.DownloadNewRelease(ctx, resp) - if err != nil { - slog.Error("failed to download new release", "error", err) - } else { - err = cb(resp.UpdateVersion) - if err != nil { - slog.Warn("failed to register update available with tray", "error", err) - } - } + // Auto-update is enabled - download + err = u.DownloadNewRelease(ctx, resp) + if err != nil { + slog.Error("failed to download new release", "error", err) + continue + } + + // Download successful - show tray notification (regardless of toggle state) + err = cb(resp.UpdateVersion) + if err != nil { + slog.Warn("failed to register update available with tray", "error", err) + } } }() } @@ -341,28 +343,6 @@ func (u *Updater) CheckForUpdate(ctx context.Context) (bool, string, error) { return available, resp.UpdateVersion, nil } -func (u *Updater) DownloadUpdate(ctx context.Context, updateVersion string) error { - // First check for update to get the actual download URL - available, updateResp := u.checkForUpdate(ctx) - - if !available { - return fmt.Errorf("no update available") - } - if updateResp.UpdateVersion != updateVersion { - slog.Error("version mismatch", "requested", updateVersion, "available", updateResp.UpdateVersion) - return fmt.Errorf("version mismatch: requested %s, available %s", updateVersion, updateResp.UpdateVersion) - } - - slog.Info("downloading update", "version", updateVersion) - err := u.DownloadNewRelease(ctx, updateResp) - if err != nil { - return err - } - - slog.Info("update downloaded successfully", "version", updateVersion) - return nil -} - func (u *Updater) InstallAndRestart() error { if !UpdateDownloaded { return fmt.Errorf("no update downloaded") diff --git a/app/wintray/menus.go b/app/wintray/menus.go index 6e752ef57..106953b7a 100644 --- a/app/wintray/menus.go +++ b/app/wintray/menus.go @@ -47,34 +47,6 @@ func (t *winTray) initMenus() error { return nil } -func (t *winTray) ClearUpdateAvailable() error { - if t.updateNotified { - slog.Debug("clearing update notification and menu items") - if err := t.removeMenuItem(updateSeparatorMenuID, 0); err != nil { - return fmt.Errorf("unable to remove menu entries %w", err) - } - if err := t.removeMenuItem(updateAvailableMenuID, 0); err != nil { - return fmt.Errorf("unable to remove menu entries %w", err) - } - if err := t.removeMenuItem(updateMenuID, 0); err != nil { - return fmt.Errorf("unable to remove menu entries %w", err) - } - if err := t.removeMenuItem(separatorMenuID, 0); err != nil { - return fmt.Errorf("unable to remove menu entries %w", err) - } - iconFilePath, err := iconBytesToFilePath(wt.normalIcon) - if err != nil { - return fmt.Errorf("unable to write icon data to temp file: %w", err) - } - if err := t.setIcon(iconFilePath); err != nil { - return fmt.Errorf("unable to set icon: %w", err) - } - t.updateNotified = false - t.pendingUpdate = false - } - return nil -} - func (t *winTray) UpdateAvailable(ver string) error { if !t.updateNotified { slog.Debug("updating menu and sending notification for new update") diff --git a/app/wintray/tray.go b/app/wintray/tray.go index 0271f47d4..0c1a2ed0f 100644 --- a/app/wintray/tray.go +++ b/app/wintray/tray.go @@ -41,7 +41,6 @@ type TrayCallbacks interface { Quit() TrayRun() UpdateAvailable(ver string) error - ClearUpdateAvailable() error GetIconHandle() windows.Handle } From 68a3414761247592158f36eb83fc09a2cd62fd6b Mon Sep 17 00:00:00 2001 From: Eva Ho Date: Mon, 5 Jan 2026 13:24:41 -0500 Subject: [PATCH 13/13] fix test --- app/cmd/app/app.go | 6 ++--- app/updater/updater.go | 54 +++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/cmd/app/app.go b/app/cmd/app/app.go index 7bc73cd75..904807fd7 100644 --- a/app/cmd/app/app.go +++ b/app/cmd/app/app.go @@ -267,9 +267,9 @@ func main() { }, Store: st, ToolRegistry: toolRegistry, - Dev: devMode, - Logger: slog.Default(), - Updater: upd, + Dev: devMode, + Logger: slog.Default(), + Updater: upd, UpdateAvailableFunc: func() { UpdateAvailable("") }, diff --git a/app/updater/updater.go b/app/updater/updater.go index b29a4f83a..c5a2f2efa 100644 --- a/app/updater/updater.go +++ b/app/updater/updater.go @@ -303,37 +303,37 @@ func (u *Updater) StartBackgroundUpdaterChecker(ctx context.Context, cb func(str // Regular interval check } - // Always check for updates - available, resp := u.checkForUpdate(ctx) - if !available { - continue - } + // Always check for updates + available, resp := u.checkForUpdate(ctx) + if !available { + continue + } - // Update is available - check if auto-update is enabled for downloading - settings, err := u.Store.Settings() - if err != nil { - slog.Error("failed to load settings", "error", err) - continue - } + // Update is available - check if auto-update is enabled for downloading + settings, err := u.Store.Settings() + if err != nil { + slog.Error("failed to load settings", "error", err) + continue + } - if !settings.AutoUpdateEnabled { - // Auto-update disabled - don't download, just log - slog.Debug("update available but auto-update disabled", "version", resp.UpdateVersion) - continue - } + if !settings.AutoUpdateEnabled { + // Auto-update disabled - don't download, just log + slog.Debug("update available but auto-update disabled", "version", resp.UpdateVersion) + continue + } - // Auto-update is enabled - download - err = u.DownloadNewRelease(ctx, resp) - if err != nil { - slog.Error("failed to download new release", "error", err) - continue - } + // Auto-update is enabled - download + err = u.DownloadNewRelease(ctx, resp) + if err != nil { + slog.Error("failed to download new release", "error", err) + continue + } - // Download successful - show tray notification (regardless of toggle state) - err = cb(resp.UpdateVersion) - if err != nil { - slog.Warn("failed to register update available with tray", "error", err) - } + // Download successful - show tray notification (regardless of toggle state) + err = cb(resp.UpdateVersion) + if err != nil { + slog.Warn("failed to register update available with tray", "error", err) + } } }() }