From cb9580c4dbc1c17b21f33005226a9dfca8027e0b Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 25 Feb 2026 15:13:22 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20new=20alert=20for=20startup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 88 +++++++++++ phpmon/Common/Monitoring/CommandTracker.swift | 4 - phpmon/Domain/App/EnvironmentCheck.swift | 7 +- phpmon/Domain/App/Startup+Alert.swift | 37 +---- phpmon/Domain/App/Startup.swift | 74 +++++---- .../Domain/SwiftUI/Common/MarkdownText.swift | 27 ++++ .../Startup/StartupAlertButtonRow.swift | 78 ++++++++++ .../Startup/StartupAlertHeaderView.swift | 44 ++++++ .../SwiftUI/Startup/StartupAlertView.swift | 145 ++++++++++++++++++ .../Startup/StartupAlertViewModel.swift | 93 +++++++++++ .../StartupAlertWindowController.swift | 69 +++++++++ .../Startup/StartupFixCommandView.swift | 59 +++++++ .../SwiftUI/Startup/StartupOutputView.swift | 70 +++++++++ phpmon/en.lproj/Localizable.strings | 6 +- 14 files changed, 734 insertions(+), 67 deletions(-) create mode 100644 phpmon/Domain/SwiftUI/Common/MarkdownText.swift create mode 100644 phpmon/Domain/SwiftUI/Startup/StartupAlertButtonRow.swift create mode 100644 phpmon/Domain/SwiftUI/Startup/StartupAlertHeaderView.swift create mode 100644 phpmon/Domain/SwiftUI/Startup/StartupAlertView.swift create mode 100644 phpmon/Domain/SwiftUI/Startup/StartupAlertViewModel.swift create mode 100644 phpmon/Domain/SwiftUI/Startup/StartupAlertWindowController.swift create mode 100644 phpmon/Domain/SwiftUI/Startup/StartupFixCommandView.swift create mode 100644 phpmon/Domain/SwiftUI/Startup/StartupOutputView.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index d756716b..0991ba37 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -221,6 +221,38 @@ 03EC943D2F47297B00231276 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC943B2F47297100231276 /* ErrorView.swift */; }; 03EC943E2F47297B00231276 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC943B2F47297100231276 /* ErrorView.swift */; }; 03EC943F2F47297B00231276 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EC943B2F47297100231276 /* ErrorView.swift */; }; + EE4FA5D074AD5C9D09C25A55 /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; }; + 7E19EE251E743286BC95EBA3 /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; }; + FEA10615F601BEC79BBDA45B /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; }; + 1D1173AE9AC9C8315E899D06 /* StartupAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */; }; + 959BE2F3A5743862AC8D1E7D /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; }; + C57636BF2B81A9D147B28D1C /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; }; + 9297DB3F2A8903D70E1D6426 /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; }; + 88DD3A08057A23955913FB70 /* StartupAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EE572012BA8A554E276E4 /* StartupAlertView.swift */; }; + E56493248D049CA2EBB82B21 /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; }; + 148796E43977CCBC22AAB5CC /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; }; + 8A8AB3D1A6DAE99C91DFBEFF /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; }; + A42F1F6EF455F42769E63FF5 /* StartupAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */; }; + BFF65E73753B67381C23D65E /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; }; + 288730259CFF37771C773EA2 /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; }; + 4AAB0E1F7037BEFAE0037AFF /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; }; + C277F9F96197AD07FA91E3CB /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C0BBF856B022EA51606492 /* MarkdownText.swift */; }; + 56306F63B84C9E99C2C26375 /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; }; + 5D754EAE78FF1E264AD02A46 /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; }; + 95BB6DF72FB35A5A8F4E24E7 /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; }; + B54FBBEFE5E1782AFF7C434E /* StartupAlertHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */; }; + 67E571F5200F9C57CC673499 /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; }; + F644E222C9D096B94E363E60 /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; }; + 632A06CFEEC7A76C34992F38 /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; }; + F4B7E3F9B46C5AD8B8B3FEBA /* StartupFixCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */; }; + CCF5FE7B49F3B0BC77BAF7D7 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; }; + 2F9D926CBFE51F3B21A76536 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; }; + FF788D404D414CC970301C81 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; }; + FB95EF9B99E4ED160E751E44 /* StartupOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E531FA9DE31575AF518941 /* StartupOutputView.swift */; }; + 1C91CB7232F304AA7F6296CB /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; }; + 1224231FF18A15CB876EEF50 /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; }; + DDAA59E032BB88D66D71F736 /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; }; + F6E9803FDF532887CF899ED9 /* StartupAlertButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */; }; 03FE39E72E81682800B7B5AC /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E52E81682800B7B5AC /* AppIcon.icon */; }; 03FE39E82E81682800B7B5AC /* AppIconEAP.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E62E81682800B7B5AC /* AppIconEAP.icon */; }; 03FE39EA2E81694500B7B5AC /* AppIconUD.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E92E81694500B7B5AC /* AppIconUD.icon */; }; @@ -1151,6 +1183,14 @@ 03D846312EB64E35006EFE3C /* CrashReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporter.swift; sourceTree = ""; }; 03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainListVC+Certs.swift"; sourceTree = ""; }; 03EC943B2F47297100231276 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertViewModel.swift; sourceTree = ""; }; + 349EE572012BA8A554E276E4 /* StartupAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertView.swift; sourceTree = ""; }; + D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertWindowController.swift; sourceTree = ""; }; + A4C0BBF856B022EA51606492 /* MarkdownText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownText.swift; sourceTree = ""; }; + 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertHeaderView.swift; sourceTree = ""; }; + 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupFixCommandView.swift; sourceTree = ""; }; + E3E531FA9DE31575AF518941 /* StartupOutputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupOutputView.swift; sourceTree = ""; }; + 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAlertButtonRow.swift; sourceTree = ""; }; 03FE39E52E81682800B7B5AC /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 03FE39E62E81682800B7B5AC /* AppIconEAP.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconEAP.icon; sourceTree = ""; }; 03FE39E92E81694500B7B5AC /* AppIconUD.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconUD.icon; sourceTree = ""; }; @@ -2346,6 +2386,7 @@ isa = PBXGroup; children = ( 03EC943B2F47297100231276 /* ErrorView.swift */, + A4C0BBF856B022EA51606492 /* MarkdownText.swift */, 031D747B2F46225600D4FF48 /* SimpleButton.swift */, C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */, C451AFF52969E40F0078E617 /* HelpButton.swift */, @@ -2511,9 +2552,24 @@ path = Onboarding; sourceTree = ""; }; + 3169708AAEC47AE3F4C33F74 /* Startup */ = { + isa = PBXGroup; + children = ( + B900AAECE035041E9BE09DCF /* StartupAlertViewModel.swift */, + 349EE572012BA8A554E276E4 /* StartupAlertView.swift */, + 7EF8DC9BC21B1EFB1F6CBBA5 /* StartupAlertHeaderView.swift */, + 68063C53EE0F48E91ADFE92F /* StartupFixCommandView.swift */, + E3E531FA9DE31575AF518941 /* StartupOutputView.swift */, + 2DBEBCF2527961D5D13F68A8 /* StartupAlertButtonRow.swift */, + D51912F2DA8FD7D8A1578644 /* StartupAlertWindowController.swift */, + ); + path = Startup; + sourceTree = ""; + }; C4EE55B027708BB2001DF387 /* SwiftUI */ = { isa = PBXGroup; children = ( + 3169708AAEC47AE3F4C33F74 /* Startup */, C4B609182853AAA700C95265 /* Domains */, C4B609172853AA9E00C95265 /* Menu */, C4B609162853AA9A00C95265 /* Common */, @@ -3022,6 +3078,14 @@ C4C0E8E727F88B41002D32A9 /* DomainScanner.swift in Sources */, C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */, 03EC943D2F47297B00231276 /* ErrorView.swift in Sources */, + 7E19EE251E743286BC95EBA3 /* StartupAlertViewModel.swift in Sources */, + C57636BF2B81A9D147B28D1C /* StartupAlertView.swift in Sources */, + 148796E43977CCBC22AAB5CC /* StartupAlertWindowController.swift in Sources */, + 288730259CFF37771C773EA2 /* MarkdownText.swift in Sources */, + 5D754EAE78FF1E264AD02A46 /* StartupAlertHeaderView.swift in Sources */, + F644E222C9D096B94E363E60 /* StartupFixCommandView.swift in Sources */, + 2F9D926CBFE51F3B21A76536 /* StartupOutputView.swift in Sources */, + 1224231FF18A15CB876EEF50 /* StartupAlertButtonRow.swift in Sources */, 54B48B5F275F66AE006D90C5 /* Application.swift in Sources */, C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, C4CE3BB827B31F2E0086CA49 /* MainMenu+Switcher.swift in Sources */, @@ -3383,6 +3447,14 @@ 032DAC2D2E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */, C4513F902B13E2E6001AD760 /* PhpExtensionManagerWindowController.swift in Sources */, 03EC943F2F47297B00231276 /* ErrorView.swift in Sources */, + FEA10615F601BEC79BBDA45B /* StartupAlertViewModel.swift in Sources */, + 9297DB3F2A8903D70E1D6426 /* StartupAlertView.swift in Sources */, + 8A8AB3D1A6DAE99C91DFBEFF /* StartupAlertWindowController.swift in Sources */, + 4AAB0E1F7037BEFAE0037AFF /* MarkdownText.swift in Sources */, + 95BB6DF72FB35A5A8F4E24E7 /* StartupAlertHeaderView.swift in Sources */, + 632A06CFEEC7A76C34992F38 /* StartupFixCommandView.swift in Sources */, + FF788D404D414CC970301C81 /* StartupOutputView.swift in Sources */, + DDAA59E032BB88D66D71F736 /* StartupAlertButtonRow.swift in Sources */, C471E81528F9BAE80021E251 /* ArrayExtension.swift in Sources */, C471E7DA28F9BA8F0021E251 /* TestableCommand.swift in Sources */, C471E7E528F9BAC20021E251 /* Events.swift in Sources */, @@ -3522,6 +3594,14 @@ 03C29A7A2EC88E3100FBA25E /* ValetServicesDataManager.swift in Sources */, C471E8D928F9BB8F0021E251 /* HotkeyPreferenceView.swift in Sources */, 03EC943E2F47297B00231276 /* ErrorView.swift in Sources */, + 1D1173AE9AC9C8315E899D06 /* StartupAlertViewModel.swift in Sources */, + 88DD3A08057A23955913FB70 /* StartupAlertView.swift in Sources */, + A42F1F6EF455F42769E63FF5 /* StartupAlertWindowController.swift in Sources */, + C277F9F96197AD07FA91E3CB /* MarkdownText.swift in Sources */, + B54FBBEFE5E1782AFF7C434E /* StartupAlertHeaderView.swift in Sources */, + F4B7E3F9B46C5AD8B8B3FEBA /* StartupFixCommandView.swift in Sources */, + FB95EF9B99E4ED160E751E44 /* StartupOutputView.swift in Sources */, + F6E9803FDF532887CF899ED9 /* StartupAlertButtonRow.swift in Sources */, C4611E5D2AEAD2FA0010BE24 /* ConfigManagerView.swift in Sources */, C471E8DA28F9BB8F0021E251 /* Keys.swift in Sources */, C471E8DB28F9BB8F0021E251 /* TerminalProgressWindowController.swift in Sources */, @@ -3715,6 +3795,14 @@ C4F319C927B034A500AFF46F /* Stats.swift in Sources */, C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */, 03EC943C2F47297B00231276 /* ErrorView.swift in Sources */, + EE4FA5D074AD5C9D09C25A55 /* StartupAlertViewModel.swift in Sources */, + 959BE2F3A5743862AC8D1E7D /* StartupAlertView.swift in Sources */, + E56493248D049CA2EBB82B21 /* StartupAlertWindowController.swift in Sources */, + BFF65E73753B67381C23D65E /* MarkdownText.swift in Sources */, + 56306F63B84C9E99C2C26375 /* StartupAlertHeaderView.swift in Sources */, + 67E571F5200F9C57CC673499 /* StartupFixCommandView.swift in Sources */, + CCF5FE7B49F3B0BC77BAF7D7 /* StartupOutputView.swift in Sources */, + 1C91CB7232F304AA7F6296CB /* StartupAlertButtonRow.swift in Sources */, 54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */, C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */, diff --git a/phpmon/Common/Monitoring/CommandTracker.swift b/phpmon/Common/Monitoring/CommandTracker.swift index 0fe9f7c7..fe28f243 100644 --- a/phpmon/Common/Monitoring/CommandTracker.swift +++ b/phpmon/Common/Monitoring/CommandTracker.swift @@ -19,10 +19,6 @@ class CommandTracker: ObservableObject { commands.filter { !$0.isCompleted } } - var isActive: Bool { - !activeCommands.isEmpty - } - @discardableResult func track(_ command: String, id: UUID = UUID()) -> UUID { let tracked = LoggedCommand(id: id, command: command, startedAt: Date()) diff --git a/phpmon/Domain/App/EnvironmentCheck.swift b/phpmon/Domain/App/EnvironmentCheck.swift index cf16b746..bcd5e734 100644 --- a/phpmon/Domain/App/EnvironmentCheck.swift +++ b/phpmon/Domain/App/EnvironmentCheck.swift @@ -14,7 +14,8 @@ import Foundation */ struct EnvironmentCheck { let command: (_ container: Container) async -> Bool - let fixCommand: ((_ container: Container) async throws -> Void)? + let fixCommand: ((_ container: Container, _ didReceiveOutput: @escaping (String, ShellStream) -> Void) async throws -> Void)? + let fixDescription: String? let name: String let titleText: String let subtitleText: String @@ -24,7 +25,8 @@ struct EnvironmentCheck { init( command: @escaping (_ container: Container) async -> Bool, - fix: ((_ container: Container) async throws -> Void)? = nil, + fix: ((_ container: Container, _ didReceiveOutput: @escaping (String, ShellStream) -> Void) async throws -> Void)? = nil, + fixDescription: String? = nil, name: String, titleText: String, subtitleText: String, @@ -34,6 +36,7 @@ struct EnvironmentCheck { ) { self.command = command self.fixCommand = fix + self.fixDescription = fixDescription self.name = name self.titleText = titleText self.subtitleText = subtitleText diff --git a/phpmon/Domain/App/Startup+Alert.swift b/phpmon/Domain/App/Startup+Alert.swift index 118b5161..11358f63 100644 --- a/phpmon/Domain/App/Startup+Alert.swift +++ b/phpmon/Domain/App/Startup+Alert.swift @@ -15,7 +15,7 @@ extension Startup { The potential outcome of an environment check failure alert. */ enum EnvironmentAlertOutcome { - /** The automatic fix was requested, will try and continue if it worked. */ + /** The automatic fix ran and succeeded. Continue to the next check. */ case shouldRunFix /** No automatic fix was requested, show alert and require retry of all startup checks. */ @@ -23,11 +23,11 @@ extension Startup { } /** - Displays an alert for a particular check. There are two types of alerts: - - ones that require an app restart, which prompt the user to exit the app - - ones that allow the app to continue, which allow the user to retry + Displays an alert for a particular check. For checks that require an app restart, + a simple NVAlert is shown with a quit button. For all other checks, the new + StartupAlertWindowController is used to show the enhanced startup alert. */ - @MainActor internal func showAlert(for check: EnvironmentCheck) -> EnvironmentAlertOutcome { + @MainActor internal func showAlert(for check: EnvironmentCheck) async -> EnvironmentAlertOutcome { // Ensure that the timeout does not fire until we restart Self.startupTimer?.invalidate() @@ -43,29 +43,8 @@ extension Startup { }).show(urgency: .bringToFront) } - // Verify if an automatic fix is available - let hasAutomaticFix = check.fixCommand != nil - - // Present an alert with one or two buttons (depending on fix) - let outcome = NVAlert() - .withInformation( - title: check.titleText, - subtitle: check.subtitleText, - description: check.descriptionText - ) - .withPrimary(text: hasAutomaticFix ? "startup.fix_for_me".localized : "startup.fix_manually".localized) - .withSecondary(if: hasAutomaticFix, text: "startup.fix_manually".localized) - .withTertiary(if: hasAutomaticFix, text: "", action: { _ in - NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions) - }) - .runModal(urgency: .bringToFront) - - // If there's an automatic fix and we chose to fix it, return outcome - if hasAutomaticFix && outcome == .alertFirstButtonReturn { - return .shouldRunFix - } - - // In any other situation, we will require a retry of the startup - return .shouldRetryStartup + // Create and show the enhanced startup alert window + let controller = StartupAlertWindowController.create(for: check) + return await controller.showModal() } } diff --git a/phpmon/Domain/App/Startup.swift b/phpmon/Domain/App/Startup.swift index 18fe2c50..b1092e27 100644 --- a/phpmon/Domain/App/Startup.swift +++ b/phpmon/Domain/App/Startup.swift @@ -48,26 +48,12 @@ class Startup { // We will present the user with an option (potentially) let outcome = await showAlert(for: check) - // If the user requested an automatic fix, do this + // The fix ran and succeeded — continue to the next check if outcome == .shouldRunFix { - // Verify a fix actually exists - guard let command = check.fixCommand else { - return false - } - - // We will try to run the fix, it may fail! - do { - try await command(App.shared.container) - guard await check.succeeds() else { - return false - } - continue // continue to the next check! - } catch { - return false - } + continue } - // No fix requested, this is just a failure + // No fix requested or fix failed — requires full restart return false } } else { @@ -113,10 +99,15 @@ class Startup { return await !container.shell .pipe("ls \(container.paths.optPath) | grep php").out.contains("php") }, - fix: { container in + fix: { container, didReceiveOutput in let brew = container.paths.brew - await container.shell.pipe("\(brew) tap shivammathur/php && \(brew) install shivammathur/php/php") + try await container.shell.attach( + "\(brew) tap shivammathur/php && \(brew) install shivammathur/php/php", + didReceiveOutput: didReceiveOutput, + withTimeout: 120 + ) }, + fixDescription: "brew tap shivammathur/php && brew install shivammathur/php/php", name: "`ls \(App.shared.container.paths.optPath) | grep php` returned php result", titleText: "startup.errors.php_opt.title".localized, subtitleText: "startup.errors.php_opt.subtitle".localized( @@ -131,11 +122,15 @@ class Startup { command: { container in return !container.filesystem.fileExists(container.paths.php) }, - fix: { container in - // See if we can't link PHP + fix: { container, didReceiveOutput in let brew = container.paths.brew - await container.shell.pipe("\(brew) link php") + try await container.shell.attach( + "\(brew) link php", + didReceiveOutput: didReceiveOutput, + withTimeout: 120 + ) }, + fixDescription: "brew link php", name: "`\(App.shared.container.paths.php)` exists", titleText: "startup.errors.php_binary.title".localized, subtitleText: "startup.errors.php_binary.subtitle".localized, @@ -152,10 +147,15 @@ class Startup { return await container.shell.pipe("\(container.paths.binPath)/php -v").err .contains("Library not loaded") && container.phpEnvs.currentInstall != nil }, - fix: { container in - let brew = App.shared.container.paths.brew - await container.shell.pipe("\(brew) tap shivammathur/php && \(brew) reinstall shivammathur/php/php && \(brew) link php") + fix: { container, didReceiveOutput in + let brew = container.paths.brew + try await container.shell.attach( + "\(brew) tap shivammathur/php && \(brew) reinstall shivammathur/php/php && \(brew) link php", + didReceiveOutput: didReceiveOutput, + withTimeout: 120 + ) }, + fixDescription: "brew reinstall php && brew link php", name: "no `dyld` issue (`Library not loaded`) detected", titleText: "startup.errors.dyld_library.title".localized, subtitleText: "startup.errors.dyld_library.subtitle".localized( @@ -187,10 +187,15 @@ class Startup { await container.phpEnvs.determinePhpAlias() return PhpEnvironments.brewPhpAlias == nil }, - fix: { container in + fix: { container, didReceiveOutput in let brew = container.paths.brew - await container.shell.pipe("\(brew) update") + try await container.shell.attach( + "\(brew) update", + didReceiveOutput: didReceiveOutput, + withTimeout: 120 + ) }, + fixDescription: "brew update", name: "`brew` alias is not nil and valid", titleText: "startup.errors.could_not_determine_alias.title".localized, subtitleText: "startup.errors.could_not_determine_alias.subtitle".localized, @@ -224,10 +229,12 @@ class Startup { .pipe("cat /private/etc/sudoers.d/brew") .out.contains(container.paths.brew) }, - fix: { container in + fix: { container, didReceiveOutput in let valet = container.paths.binPath.appending("/valet") - try AppleScript.runShellAsAdmin("\(valet) trust") + let result = try AppleScript.runShellAsAdmin("\(valet) trust") + didReceiveOutput(result, .stdOut) }, + fixDescription: "valet trust", name: "`/private/etc/sudoers.d/brew` contains brew", titleText: "startup.errors.sudoers_brew.title".localized, subtitleText: "startup.errors.sudoers_brew.subtitle".localized, @@ -250,10 +257,15 @@ class Startup { command: { container in return !container.filesystem.directoryExists("~/.config/valet") }, - fix: { container in + fix: { container, didReceiveOutput in let valet = container.paths.binPath.appending("/valet") - await container.shell.pipe("\(valet) install") + try await container.shell.attach( + "\(valet) install", + didReceiveOutput: didReceiveOutput, + withTimeout: 120 + ) }, + fixDescription: "valet install", name: "`.config/valet` not empty (Valet installed)", titleText: "startup.errors.valet_not_installed.title".localized, subtitleText: "startup.errors.valet_not_installed.subtitle".localized, diff --git a/phpmon/Domain/SwiftUI/Common/MarkdownText.swift b/phpmon/Domain/SwiftUI/Common/MarkdownText.swift new file mode 100644 index 00000000..57cd10c7 --- /dev/null +++ b/phpmon/Domain/SwiftUI/Common/MarkdownText.swift @@ -0,0 +1,27 @@ +// +// MarkdownText.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +extension Text { + init(markdown string: String, fontSize: CGFloat? = nil) { + if var attributed = try? AttributedString( + markdown: string, + options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace) + ) { + for run in attributed.runs { + if run.inlinePresentationIntent?.contains(.code) == true { + attributed[run.range].backgroundColor = Color(nsColor: .quaternaryLabelColor) + } + } + self.init(attributed) + } else { + self.init(string) + } + } +} diff --git a/phpmon/Domain/SwiftUI/Startup/StartupAlertButtonRow.swift b/phpmon/Domain/SwiftUI/Startup/StartupAlertButtonRow.swift new file mode 100644 index 00000000..ff993f4d --- /dev/null +++ b/phpmon/Domain/SwiftUI/Startup/StartupAlertButtonRow.swift @@ -0,0 +1,78 @@ +// +// StartupAlertButtonRow.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +struct StartupAlertButtonRow: View { + let state: StartupAlertViewModel.State + let hasFix: Bool + let onQuit: () -> Void + let onRetry: () -> Void + let onFix: () -> Void + + var body: some View { + HStack { + Button("startup.alert.quit".localized) { + onQuit() + } + .disabled(state == .running) + + Spacer() + + switch state { + case .idle where hasFix: + Button("startup.fix_manually".localized) { + onRetry() + } + Button("startup.alert.fix_automatically".localized) { + onFix() + } + .buttonStyle(.custom) + + case .running: + HStack(spacing: 10) { + Text("Applying. Please wait...") + .font(.subheadline) + .foregroundStyle(.secondary) + ProgressView() + .controlSize(.small) + } + + case .idle, .completed: + Button("startup.alert.retry".localized) { + onRetry() + } + } + } + .padding(20) + } +} + +#Preview("Fix available") { + StartupAlertButtonRow( + state: .idle, hasFix: true, + onQuit: {}, onRetry: {}, onFix: {} + ) + .frame(width: 460) +} + +#Preview("Running") { + StartupAlertButtonRow( + state: .running, hasFix: true, + onQuit: {}, onRetry: {}, onFix: {} + ) + .frame(width: 460) +} + +#Preview("No fix") { + StartupAlertButtonRow( + state: .completed, hasFix: false, + onQuit: {}, onRetry: {}, onFix: {} + ) + .frame(width: 460) +} diff --git a/phpmon/Domain/SwiftUI/Startup/StartupAlertHeaderView.swift b/phpmon/Domain/SwiftUI/Startup/StartupAlertHeaderView.swift new file mode 100644 index 00000000..82acf919 --- /dev/null +++ b/phpmon/Domain/SwiftUI/Startup/StartupAlertHeaderView.swift @@ -0,0 +1,44 @@ +// +// StartupAlertHeaderView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +struct StartupAlertHeaderView: View { + let titleText: String + let subtitleText: String + + var body: some View { + HStack(spacing: 12) { + Image(nsImage: NSApp.applicationIconImage) + .resizable() + .frame(width: 60, height: 60) + + VStack(alignment: .leading, spacing: 5) { + Text(markdown: titleText) + .font(.system(size: 15, weight: .bold)) + .textSelection(.enabled) + Text(markdown: subtitleText) + .font(.system(size: 12)) + .fixedSize(horizontal: false, vertical: true) + .textSelection(.enabled) + } + + Spacer() + } + .padding(15) + .padding(.top, 0) + } +} + +#Preview { + StartupAlertHeaderView( + titleText: "startup.errors.php_binary.title".localized, + subtitleText: "startup.errors.php_binary.subtitle".localized + ) + .frame(width: 460) +} diff --git a/phpmon/Domain/SwiftUI/Startup/StartupAlertView.swift b/phpmon/Domain/SwiftUI/Startup/StartupAlertView.swift new file mode 100644 index 00000000..2d53338b --- /dev/null +++ b/phpmon/Domain/SwiftUI/Startup/StartupAlertView.swift @@ -0,0 +1,145 @@ +// +// StartupAlertView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +struct StartupAlertView: View { + @ObservedObject var viewModel: StartupAlertViewModel + + var body: some View { + VStack(spacing: 0) { + StartupAlertHeaderView( + titleText: viewModel.check.titleText, + subtitleText: viewModel.check.subtitleText + ) + + Divider() + + VStack(alignment: .leading, spacing: 12) { + if viewModel.state == .running { + StartupOutputView( + lines: viewModel.outputLines, + isRunning: true + ) + } else if viewModel.hasFix, viewModel.state == .idle { + StartupFixCommandView( + command: viewModel.check.fixDescription ?? "" + ) + } + + if !viewModel.check.descriptionText.isEmpty, viewModel.state != .running { + Text(markdown: viewModel.check.descriptionText, fontSize: 12) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + .textSelection(.enabled) + } + + if !viewModel.outputLines.isEmpty, viewModel.state == .idle { + StartupOutputView( + lines: viewModel.outputLines, + isRunning: false + ) + } + } + .padding(20) + .frame(maxWidth: .infinity, alignment: .leading) + + Divider() + + StartupAlertButtonRow( + state: viewModel.state, + hasFix: viewModel.hasFix, + onQuit: { viewModel.quit() }, + onRetry: { viewModel.retry() }, + onFix: { viewModel.runFix() } + ) + } + .frame(width: 460) + } +} + +// MARK: - Previews + +#Preview("Fix Available — brew link php") { + let check = EnvironmentCheck( + command: { _ in return true }, + fix: { _, output in output("Running brew link php...", .stdOut) }, + fixDescription: "brew link php", + name: "preview_php_binary", + titleText: "startup.errors.php_binary.title".localized, + subtitleText: "startup.errors.php_binary.subtitle".localized, + descriptionText: "startup.errors.php_binary.desc".localized( + App.shared.container.paths.php + ) + ) + let vm = StartupAlertViewModel(check: check) + return StartupAlertView(viewModel: vm) +} + +#Preview("Fix Available — valet trust") { + let check = EnvironmentCheck( + command: { _ in return true }, + fix: { _, output in output("Password required...", .stdOut) }, + fixDescription: "valet trust", + name: "preview_sudoers_brew", + titleText: "startup.errors.sudoers_brew.title".localized, + subtitleText: "startup.errors.sudoers_brew.subtitle".localized, + descriptionText: "startup.errors.sudoers_brew.desc".localized + ) + let vm = StartupAlertViewModel(check: check) + return StartupAlertView(viewModel: vm) +} + +#Preview("Fix Running — brew link php") { + StartupAlertView(viewModel: StartupAlertViewModel( + check: EnvironmentCheck( + command: { _ in return true }, + fix: { _, output in output("Running...", .stdOut) }, + fixDescription: "brew link php", + name: "preview_php_binary_running", + titleText: "startup.errors.php_binary.title".localized, + subtitleText: "startup.errors.php_binary.subtitle".localized, + descriptionText: "startup.errors.php_binary.desc".localized( + App.shared.container.paths.php + ) + ), + state: .running, + outputLines: [ + OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut), + OutputLine(text: "==> Downloading https://formulae.brew.sh/api/formula.jws.json", stream: .stdOut), + OutputLine(text: "Already downloaded: /Users/nico/Library/Caches/Homebrew/downloads/abc123.json", stream: .stdOut), + OutputLine(text: "Warning: php is keg-only and must be linked with --force", stream: .stdErr), + OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut), + ] + )) +} + +#Preview("No Fix — Valet version unsupported") { + let check = EnvironmentCheck( + command: { _ in return true }, + name: "preview_valet_version", + titleText: "startup.errors.valet_version_not_supported.title".localized, + subtitleText: "startup.errors.valet_version_not_supported.subtitle".localized, + descriptionText: "startup.errors.valet_version_not_supported.desc".localized + ) + let vm = StartupAlertViewModel(check: check) + return StartupAlertView(viewModel: vm) +} + +#Preview("No Fix — Herd running") { + let check = EnvironmentCheck( + command: { _ in return true }, + name: "preview_herd_running", + titleText: "startup.errors.herd_running.title".localized, + subtitleText: "startup.errors.herd_running.subtitle".localized, + descriptionText: "startup.errors.herd_running.desc".localized + ) + let vm = StartupAlertViewModel(check: check) + return StartupAlertView(viewModel: vm) +} diff --git a/phpmon/Domain/SwiftUI/Startup/StartupAlertViewModel.swift b/phpmon/Domain/SwiftUI/Startup/StartupAlertViewModel.swift new file mode 100644 index 00000000..9ee444a3 --- /dev/null +++ b/phpmon/Domain/SwiftUI/Startup/StartupAlertViewModel.swift @@ -0,0 +1,93 @@ +// +// StartupAlertViewModel.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import Foundation + +struct OutputLine: Identifiable { + let id = UUID() + let text: String + let stream: ShellStream +} + +class StartupAlertViewModel: ObservableObject { + enum State { + case idle + case running + case completed + } + + @Published var state: State = .idle + @Published var outputLines: [OutputLine] = [] + + let check: EnvironmentCheck + + /// Callback to dismiss the window with a result + var onComplete: ((Startup.EnvironmentAlertOutcome) -> Void)? + + init(check: EnvironmentCheck) { + self.check = check + self.state = check.fixCommand != nil ? .idle : .completed + } + + init(check: EnvironmentCheck, state: State, outputLines: [OutputLine] = []) { + self.check = check + self.state = state + self.outputLines = outputLines + } + + var hasFix: Bool { + return check.fixCommand != nil + } + + @MainActor + func runFix() { + guard let fixCommand = check.fixCommand else { return } + state = .running + outputLines = [] + + Task { + do { + try await fixCommand(App.shared.container) { [weak self] text, stream in + DispatchQueue.main.async { + self?.outputLines.append(OutputLine(text: text, stream: stream)) + } + } + + // Fix completed — re-run the check + let stillFails = await check.succeeds() + await MainActor.run { + if !stillFails { + // Check still fails after fix + self.state = .idle + self.outputLines.append( + OutputLine(text: "\nFix did not resolve the issue.", stream: .stdErr) + ) + } else { + // Check passed + self.onComplete?(.shouldRunFix) + } + } + } catch { + await MainActor.run { + self.outputLines.append( + OutputLine(text: "\nError: \(error.localizedDescription)", stream: .stdErr) + ) + self.state = .idle + } + } + } + } + + func quit() { + exit(1) + } + + func retry() { + onComplete?(.shouldRetryStartup) + } +} diff --git a/phpmon/Domain/SwiftUI/Startup/StartupAlertWindowController.swift b/phpmon/Domain/SwiftUI/Startup/StartupAlertWindowController.swift new file mode 100644 index 00000000..ea50ac9a --- /dev/null +++ b/phpmon/Domain/SwiftUI/Startup/StartupAlertWindowController.swift @@ -0,0 +1,69 @@ +// +// StartupAlertWindowController.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import Cocoa +import SwiftUI + +class StartupAlertWindowController: PMWindowController { + + override var windowName: String { + return "StartupAlert" + } + + private var viewModel: StartupAlertViewModel? + private var didResolve = false + + static func create(for check: EnvironmentCheck) -> StartupAlertWindowController { + let windowController = StartupAlertWindowController() + let viewModel = StartupAlertViewModel(check: check) + windowController.viewModel = viewModel + + let window = NSWindow() + window.title = "" + window.styleMask = [.titled, .closable] + window.titlebarAppearsTransparent = true + window.delegate = windowController + window.contentView = NSHostingView(rootView: StartupAlertView(viewModel: viewModel)) + window.setContentSize(window.contentView!.fittingSize) + window.isReleasedWhenClosed = false + + windowController.window = window + return windowController + } + + @MainActor + func showModal() async -> Startup.EnvironmentAlertOutcome { + return await withCheckedContinuation { continuation in + guard let viewModel = self.viewModel else { + continuation.resume(returning: .shouldRetryStartup) + return + } + + viewModel.onComplete = { [weak self] outcome in + guard let self, !self.didResolve else { return } + self.didResolve = true + self.close() + continuation.resume(returning: outcome) + } + + self.showWindow(nil) + self.window?.center() + NSApp.activate(ignoringOtherApps: true) + self.window?.orderFrontRegardless() + } + } + + override func windowWillClose(_ notification: Notification) { + super.windowWillClose(notification) + + // If closed via the close button without resolving, quit the app + if !didResolve { + exit(1) + } + } +} diff --git a/phpmon/Domain/SwiftUI/Startup/StartupFixCommandView.swift b/phpmon/Domain/SwiftUI/Startup/StartupFixCommandView.swift new file mode 100644 index 00000000..b63dfc16 --- /dev/null +++ b/phpmon/Domain/SwiftUI/Startup/StartupFixCommandView.swift @@ -0,0 +1,59 @@ +// +// StartupFixCommandView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +struct StartupFixCommandView: View { + let command: String + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("AUTOMATIC FIX") + .foregroundStyle(Color.app) + .font(.system(size: 10)) + HStack(spacing: 8) { + Image(systemName: "terminal") + .foregroundStyle(.secondary) + Text(command) + .font(.system(size: 12, design: .monospaced)) + .foregroundStyle(.primary) + Spacer() + Button { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(command, forType: .string) + } label: { + Image(systemName: "doc.on.doc") + } + .buttonStyle(.plain) + .foregroundStyle(.secondary) + } + } + .padding(10) + .background(Color.black) + .foregroundStyle(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } +} + +#Preview("brew link php") { + StartupFixCommandView(command: "brew link php") + .padding(20) + .frame(width: 460) +} + +#Preview("valet trust") { + StartupFixCommandView(command: "valet trust") + .padding(20) + .frame(width: 460) +} + +#Preview("Long command") { + StartupFixCommandView(command: "brew tap shivammathur/php && brew install shivammathur/php/php") + .padding(20) + .frame(width: 460) +} diff --git a/phpmon/Domain/SwiftUI/Startup/StartupOutputView.swift b/phpmon/Domain/SwiftUI/Startup/StartupOutputView.swift new file mode 100644 index 00000000..1979605f --- /dev/null +++ b/phpmon/Domain/SwiftUI/Startup/StartupOutputView.swift @@ -0,0 +1,70 @@ +// +// StartupOutputView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 25/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +struct StartupOutputView: View { + let lines: [OutputLine] + let isRunning: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 1) { + ForEach(lines) { line in + Text(line.text) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(line.stream == .stdErr ? Color.red : .white) + .textSelection(.enabled) + .id(line.id) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + } + .frame(height: 160) + .background(Color.black) + .foregroundStyle(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .onChange(of: lines.count) { _ in + if let last = lines.last { + proxy.scrollTo(last.id, anchor: .bottom) + } + } + } + } + } +} + +#Preview("With output") { + StartupOutputView( + lines: [ + OutputLine(text: "==> Linking Binary 'php' to '/opt/homebrew/bin/php'", stream: .stdOut), + OutputLine(text: "==> Downloading https://formulae.brew.sh/api/formula.jws.json", stream: .stdOut), + OutputLine(text: "Already downloaded: /Users/nico/Library/Caches/Homebrew/downloads/abc123.json", stream: .stdOut), + OutputLine(text: "Warning: php is keg-only and must be linked with --force", stream: .stdErr), + OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut), + ], + isRunning: true + ) + .padding(20) + .frame(width: 460) +} + +#Preview("Idle with prior output") { + StartupOutputView( + lines: [ + OutputLine(text: "==> Linking php... linked 25 files", stream: .stdOut), + OutputLine(text: "\nFix did not resolve the issue.", stream: .stdErr), + ], + isRunning: false + ) + .padding(20) + .frame(width: 460) +} diff --git a/phpmon/en.lproj/Localizable.strings b/phpmon/en.lproj/Localizable.strings index 2cc62497..23190e8a 100644 --- a/phpmon/en.lproj/Localizable.strings +++ b/phpmon/en.lproj/Localizable.strings @@ -665,7 +665,7 @@ You can do this by running `composer global update` in your terminal. After that // PHP binary not found "startup.errors.php_binary.title" = "PHP is not correctly installed"; "startup.errors.php_binary.subtitle" = "You must install PHP via Homebrew. The app will not work correctly until you resolve this issue."; -"startup.errors.php_binary.desc" = "Usually running `brew link php` in your Terminal will resolve this issue.\n\nTo diagnose what is wrong, you can try running `which php` in your Terminal, it should return `%@`."; +"startup.errors.php_binary.desc" = "Usually, running `brew link php` in your Terminal will resolve this issue.\n\nTo diagnose what is wrong, you can try running `which php` in your Terminal, it should return `%@`."; // Invalid brew info php output "startup.errors.php_brew_info_invalid.title" = "Homebrew returned invalid output for `brew info php --json` which requires valid JSON as output."; @@ -966,6 +966,10 @@ PHP Monitor will tell Valet to unsecure and re-secure all expired domains for yo "startup.fix_for_me" = "Fix For Me"; "startup.fix_manually" = "I Fixed It"; +"startup.alert.quit" = "Quit"; +"startup.alert.fix_automatically" = "Fix Automatically"; +"startup.alert.retry" = "Retry"; + // COMMAND HISTORY "command_history.title" = "Command History";