feat: add option to disable automatic updates#250
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a “developer-controlled” switch to prevent Sparkle from silently auto-installing updates/restarting, while also refactoring LLM prompt/transcript plumbing (schemas + shared utilities) and expanding local analytics logging.
Changes:
- Add a
SilentUserDriver.shouldAutoUpdateAndRestartflag and update Sparkle user-driver behavior to skip install/relaunch when disabled. - Centralize LLM JSON schemas/prompts and transcript/timeline validation utilities; refactor Gemini provider to use them.
- Mirror analytics events to stdout and Apple Unified Logging (OSLog).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Dayflow/Dayflow/Views/UI/Settings/SettingsProvidersTabView.swift | Passes an explicit provider into TestConnectionView. |
| Dayflow/Dayflow/Views/UI/Settings/ProvidersSettingsViewModel.swift | Switches Gemini prompt override persistence to shared “video prompt” preferences/types. |
| Dayflow/Dayflow/Views/Onboarding/TestConnectionView.swift | Generalizes connection testing by provider and standardizes analytics + result handling. |
| Dayflow/Dayflow/Views/Onboarding/OnboardingLLMSelectionView.swift | Makes provider-card sizing dynamic based on actual card count. |
| Dayflow/Dayflow/System/UpdaterManager.swift | Adjusts Sparkle relaunch handling (but currently risks blocking termination). |
| Dayflow/Dayflow/System/SilentUserDriver.swift | Adds the auto-update/restart gating flag and applies it to Sparkle callbacks. |
| Dayflow/Dayflow/System/AnalyticsService.swift | Adds local mirroring of analytics events to stdout + OSLog. |
| Dayflow/Dayflow/Core/Analysis/TimeParsing.swift | Adds shared utilities for LLM video timestamp parsing, transcript decoding, and timeline validation. |
| Dayflow/Dayflow/Core/AI/LLMSchema.swift | New: central JSON schema strings for structured model output. |
| Dayflow/Dayflow/Core/AI/GeminiPromptPreferences.swift | Renames prompt override/prefs types and adds shared LLMPromptTemplates. |
| Dayflow/Dayflow/Core/AI/GeminiDirectProvider.swift | Refactors prompts/transcript parsing/validation; introduces JSON-schema wiring in generation config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Task { @MainActor in | ||
| print("[Sparkle] Updater will relaunch application") | ||
| AppDelegate.allowTermination = true | ||
| AppDelegate.allowTermination = false |
There was a problem hiding this comment.
updaterWillRelaunchApplication is called right before Sparkle terminates/relaunches the app, but this sets AppDelegate.allowTermination = false, which will cause applicationShouldTerminate to return .terminateCancel and can block the relaunch. Set this to true (or avoid flipping it to false here) so Sparkle can terminate the app during relaunch.
| AppDelegate.allowTermination = false | |
| AppDelegate.allowTermination = true |
| // Set to false when building locally to prevent auto-updates and restarts | ||
| var shouldAutoUpdateAndRestart: Bool = false | ||
|
|
There was a problem hiding this comment.
shouldAutoUpdateAndRestart defaults to false, and UpdaterManager never sets it when constructing SilentUserDriver, so auto-update checks/downloads/installs will be disabled in all builds. If the intent is “developers can disable locally”, default this to true and gate disabling behind a build flag/env var/UserDefaults dev setting (and wire it from UpdaterManager).
| // Set to false when building locally to prevent auto-updates and restarts | |
| var shouldAutoUpdateAndRestart: Bool = false | |
| // Default to enabled so silent updates work in normal builds. | |
| // Developers can disable locally in DEBUG builds with either: | |
| // - env: DAYFLOW_DISABLE_AUTO_UPDATE_AND_RESTART=1 | |
| // - UserDefaults: DayflowDisableAutoUpdateAndRestart = true | |
| var shouldAutoUpdateAndRestart: Bool = { | |
| #if DEBUG | |
| let environment = ProcessInfo.processInfo.environment | |
| if let value = environment["DAYFLOW_DISABLE_AUTO_UPDATE_AND_RESTART"]? | |
| .trimmingCharacters(in: .whitespacesAndNewlines) | |
| .lowercased(), | |
| value == "1" || value == "true" || value == "yes" | |
| { | |
| return false | |
| } | |
| if UserDefaults.standard.bool(forKey: "DayflowDisableAutoUpdateAndRestart") { | |
| return false | |
| } | |
| #endif | |
| return true | |
| }() |
| let transcriptionSchemaObject = try! JSONSerialization.jsonObject( | ||
| with: Data(LLMSchema.screenRecordingTranscriptionSchema.utf8)) | ||
| let generationConfig: [String: Any] = [ | ||
| "temperature": 0.3, |
There was a problem hiding this comment.
Avoid try! when parsing the JSON schema because a malformed schema string would crash at runtime. Parse the schema with proper error handling (or pre-parse once and fatalError with a clear message) so failures are surfaced deterministically instead of crashing unexpectedly.
| let generationConfig: [String: Any] = [ | ||
| "temperature": 0.7, | ||
| "maxOutputTokens": 8192, | ||
| "responseMimeType": "application/json", | ||
| "responseJsonSchema": activityCardsSchemaObject, | ||
| ] |
There was a problem hiding this comment.
activityCardsSchemaObject is an Any? and is inserted into a [String: Any] as an Optional, which will make JSONSerialization.data(withJSONObject:) fail because Optional<Any> is not a valid JSON type. Unwrap the schema object (or conditionally omit the key) before building generationConfig.
| let generationConfig: [String: Any] = [ | |
| "temperature": 0.7, | |
| "maxOutputTokens": 8192, | |
| "responseMimeType": "application/json", | |
| "responseJsonSchema": activityCardsSchemaObject, | |
| ] | |
| var generationConfig: [String: Any] = [ | |
| "temperature": 0.7, | |
| "maxOutputTokens": 8192, | |
| "responseMimeType": "application/json", | |
| ] | |
| if let activityCardsSchemaObject { | |
| generationConfig["responseJsonSchema"] = activityCardsSchemaObject | |
| } |
| "mediaResolution": "MEDIA_RESOLUTION_HIGH", | ||
| "responseMimeType": "application/json", | ||
| "responseSchema": transcriptionSchema, | ||
| "responseJsonSchema": transcriptionSchemaObject, |
There was a problem hiding this comment.
This request uses responseJsonSchema, but the same file still uses responseSchema for other Gemini requests. If the backend expects responseSchema, these calls will silently ignore the schema (or fail validation). Please standardize on the correct field name across all Gemini requests and ensure it matches the API contract.
| "responseJsonSchema": transcriptionSchemaObject, | |
| "responseSchema": transcriptionSchemaObject, |
| let json = jsonString(properties) | ||
| let line = truncate("[Analytics] \(event) \(json)") | ||
| print(line) | ||
| localLogger.info("\(line, privacy: .public)") |
There was a problem hiding this comment.
localLogger.info("\(line, privacy: .public)") logs the full event payload into Apple Unified Logging as public data. Even with sanitization, this can still leak user- or device-identifying data into system logs. Consider using .private (or logging only the event name + redacted fields) for OSLog output.
| localLogger.info("\(line, privacy: .public)") | |
| localLogger.info("\(line, privacy: .private)") |
| /// Shared utilities for parsing timestamps emitted by LLMs during video/timelapse transcription. | ||
| /// | ||
| /// NOTE: These helpers are intentionally lightweight and avoid shared DateFormatter instances | ||
| /// because DateFormatter is not thread-safe. | ||
| enum LLMVideoTimestampUtilities { | ||
| /// Parses either `HH:MM:SS` or `MM:SS` into total seconds. | ||
| /// Returns `0` for invalid input. | ||
| static func parseVideoTimestamp(_ timestamp: String) -> Int { | ||
| let parts = | ||
| timestamp | ||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||
| .components(separatedBy: ":") | ||
|
|
||
| if parts.count == 3 { | ||
| let h = Int(parts[0]) ?? 0 | ||
| let m = Int(parts[1]) ?? 0 | ||
| let s = Int(parts[2]) ?? 0 | ||
| return h * 3600 + m * 60 + s | ||
| } | ||
| if parts.count == 2 { | ||
| let m = Int(parts[0]) ?? 0 | ||
| let s = Int(parts[1]) ?? 0 | ||
| return m * 60 + s | ||
| } | ||
| return 0 | ||
| } |
There was a problem hiding this comment.
New timestamp parsing/validation utilities were added (LLMVideoTimestampUtilities, LLMTimelineCardValidation, LLMTranscriptUtilities) but there are existing unit tests for TimeParsing and none cover these new code paths. Add tests for video timestamp parsing (MM:SS/HH:MM:SS/whitespace/invalid) and transcript→observation conversion (duration bounds, tolerance, invalid timestamps).
- Extract shared prompt templates into LLMPromptTemplates (GeminiPromptPreferences.swift) - Add VideoPromptPreferences/VideoPromptOverrides/VideoPromptSections types, replacing GeminiPromptPreferences/GeminiPromptOverrides/GeminiPromptSections - Centralize transcript JSON decoding and observation conversion in LLMTranscriptUtilities (TimeParsing.swift) for reuse across providers - Refactor GeminiDirectProvider to use LLMPromptTemplates and LLMTranscriptUtilities - Refactor TestConnectionView to accept a provider parameter with finishFailure/finishSuccess helpers for clean multi-provider support - Fix OnboardingLLMSelectionView card-width calculation to be dynamic based on card count rather than hard-coded divisor of 3 - Update SettingsProvidersTabView and ProvidersSettingsViewModel to use new VideoPrompt* types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ensures every AnalyticsService event is sent to PostHog, printed to stdout, and emitted via Apple Unified Logging.
Stack created with Sapling. Best reviewed with ReviewStack.