Merge pull request #363 from manaflow-ai/task-cmdp-add-apply-update-attempt-update

Add command-palette Apply Update and Attempt Update actions
This commit is contained in:
Lawrence Chen 2026-02-23 04:58:12 -08:00 committed by GitHub
commit cb55cc7404
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 203 additions and 0 deletions

View file

@ -1078,6 +1078,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
updateController.checkForUpdates()
}
@objc func applyUpdateIfAvailable(_ sender: Any?) {
updateViewModel.overrideState = nil
updateController.installUpdate()
}
@objc func attemptUpdate(_ sender: Any?) {
updateViewModel.overrideState = nil
updateController.attemptUpdate()
}
private func setupMenuBarExtra() {
let store = TerminalNotificationStore.shared
menuBarExtraController = MenuBarExtraController(

View file

@ -1275,6 +1275,8 @@ struct ContentView: View {
static let panelHasCustomName = "panel.hasCustomName"
static let panelShouldPin = "panel.shouldPin"
static let panelHasUnread = "panel.hasUnread"
static let updateHasAvailable = "update.hasAvailable"
}
private struct CommandPaletteCommandContribution {
@ -3190,6 +3192,10 @@ struct ContentView: View {
snapshot.setBool(CommandPaletteContextKeys.panelHasUnread, hasUnread)
}
if case .updateAvailable = updateViewModel.effectiveState {
snapshot.setBool(CommandPaletteContextKeys.updateHasAvailable, true)
}
return snapshot
}
@ -3330,6 +3336,23 @@ struct ContentView: View {
keywords: ["update", "upgrade", "release"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.applyUpdateIfAvailable",
title: constant("Apply Update (If Available)"),
subtitle: constant("Global"),
keywords: ["apply", "install", "update", "available"],
when: { $0.bool(CommandPaletteContextKeys.updateHasAvailable) }
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.attemptUpdate",
title: constant("Attempt Update"),
subtitle: constant("Global"),
keywords: ["attempt", "check", "update", "upgrade", "release"]
)
)
contributions.append(
CommandPaletteCommandContribution(
@ -3719,6 +3742,12 @@ struct ContentView: View {
registry.register(commandId: "palette.checkForUpdates") {
AppDelegate.shared?.checkForUpdates(nil)
}
registry.register(commandId: "palette.applyUpdateIfAvailable") {
AppDelegate.shared?.applyUpdateIfAvailable(nil)
}
registry.register(commandId: "palette.attemptUpdate") {
AppDelegate.shared?.attemptUpdate(nil)
}
registry.register(commandId: "palette.renameWorkspace") {
beginRenameWorkspaceFlow()

View file

@ -8,6 +8,8 @@ class UpdateController {
private(set) var updater: SPUUpdater
private let userDriver: UpdateDriver
private var installCancellable: AnyCancellable?
private var attemptInstallCancellable: AnyCancellable?
private var didObserveAttemptUpdateProgress: Bool = false
private var noUpdateDismissCancellable: AnyCancellable?
private var noUpdateDismissWorkItem: DispatchWorkItem?
private var readyCheckWorkItem: DispatchWorkItem?
@ -46,6 +48,7 @@ class UpdateController {
deinit {
installCancellable?.cancel()
attemptInstallCancellable?.cancel()
noUpdateDismissCancellable?.cancel()
noUpdateDismissWorkItem?.cancel()
readyCheckWorkItem?.cancel()
@ -107,6 +110,35 @@ class UpdateController {
}
}
/// Check for updates and auto-confirm install if one is found.
func attemptUpdate() {
stopAttemptUpdateMonitoring()
didObserveAttemptUpdateProgress = false
attemptInstallCancellable = viewModel.$state
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
guard let self else { return }
if state.isInstallable || !state.isIdle {
self.didObserveAttemptUpdateProgress = true
}
if case .updateAvailable = state {
UpdateLogStore.shared.append("attemptUpdate auto-confirming available update")
state.confirm()
return
}
guard self.didObserveAttemptUpdateProgress, !state.isInstallable else {
return
}
self.stopAttemptUpdateMonitoring()
}
checkForUpdates()
}
/// Check for updates (used by the menu item).
@objc func checkForUpdates() {
UpdateLogStore.shared.append("checkForUpdates invoked (state=\(viewModel.state.isIdle ? "idle" : "busy"))")
@ -175,6 +207,12 @@ class UpdateController {
return true
}
private func stopAttemptUpdateMonitoring() {
attemptInstallCancellable?.cancel()
attemptInstallCancellable = nil
didObserveAttemptUpdateProgress = false
}
private func installNoUpdateDismissObserver() {
noUpdateDismissCancellable = Publishers.CombineLatest(viewModel.$state, viewModel.$overrideState)
.receive(on: DispatchQueue.main)

View file

@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Regression test for command-palette update command wiring."""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
def get_repo_root() -> Path:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
)
if result.returncode == 0:
return Path(result.stdout.strip())
return Path.cwd()
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def expect_regex(content: str, pattern: str, message: str, failures: list[str]) -> None:
if re.search(pattern, content, flags=re.DOTALL) is None:
failures.append(message)
def main() -> int:
repo_root = get_repo_root()
content_view_path = repo_root / "Sources" / "ContentView.swift"
app_delegate_path = repo_root / "Sources" / "AppDelegate.swift"
controller_path = repo_root / "Sources" / "Update" / "UpdateController.swift"
missing_paths = [
str(path)
for path in [content_view_path, app_delegate_path, controller_path]
if not path.exists()
]
if missing_paths:
print("Missing expected files:")
for path in missing_paths:
print(f" - {path}")
return 1
content_view = read_text(content_view_path)
app_delegate = read_text(app_delegate_path)
controller = read_text(controller_path)
failures: list[str] = []
expect_regex(
content_view,
r'static\s+let\s+updateHasAvailable\s*=\s*"update\.hasAvailable"',
"Missing `CommandPaletteContextKeys.updateHasAvailable`",
failures,
)
expect_regex(
content_view,
r'if\s+case\s+\.updateAvailable\s*=\s*updateViewModel\.effectiveState\s*\{\s*snapshot\.setBool\(CommandPaletteContextKeys\.updateHasAvailable,\s*true\)\s*\}',
"Command palette context no longer tracks update-available state",
failures,
)
expect_regex(
content_view,
r'commandId:\s*"palette\.applyUpdateIfAvailable".*?title:\s*constant\("Apply Update \(If Available\)"\).*?keywords:\s*\[[^\]]*"apply"[^\]]*"install"[^\]]*"update"[^\]]*"available"[^\]]*\].*?when:\s*\{\s*\$0\.bool\(CommandPaletteContextKeys\.updateHasAvailable\)\s*\}',
"Missing or incomplete `palette.applyUpdateIfAvailable` contribution visibility gating",
failures,
)
expect_regex(
content_view,
r'commandId:\s*"palette\.attemptUpdate".*?title:\s*constant\("Attempt Update"\).*?keywords:\s*\[[^\]]*"attempt"[^\]]*"check"[^\]]*"update"[^\]]*\]',
"Missing or incomplete `palette.attemptUpdate` contribution",
failures,
)
expect_regex(
content_view,
r'registry\.register\(commandId:\s*"palette\.applyUpdateIfAvailable"\)\s*\{\s*AppDelegate\.shared\?\.applyUpdateIfAvailable\(nil\)\s*\}',
"Missing handler registration for `palette.applyUpdateIfAvailable`",
failures,
)
expect_regex(
content_view,
r'registry\.register\(commandId:\s*"palette\.attemptUpdate"\)\s*\{\s*AppDelegate\.shared\?\.attemptUpdate\(nil\)\s*\}',
"Missing handler registration for `palette.attemptUpdate`",
failures,
)
expect_regex(
app_delegate,
r'@objc\s+func\s+applyUpdateIfAvailable\(_\s+sender:\s+Any\?\)\s*\{\s*updateViewModel\.overrideState\s*=\s*nil\s*updateController\.installUpdate\(\)\s*\}',
"`AppDelegate.applyUpdateIfAvailable` is missing or does not call `updateController.installUpdate()`",
failures,
)
expect_regex(
app_delegate,
r'@objc\s+func\s+attemptUpdate\(_\s+sender:\s+Any\?\)\s*\{\s*updateViewModel\.overrideState\s*=\s*nil\s*updateController\.attemptUpdate\(\)\s*\}',
"`AppDelegate.attemptUpdate` is missing or does not call `updateController.attemptUpdate()`",
failures,
)
expect_regex(
controller,
r'func\s+attemptUpdate\(\)\s*\{',
"`UpdateController.attemptUpdate()` is missing",
failures,
)
if "state.confirm()" not in controller:
failures.append("`UpdateController.attemptUpdate()` no longer auto-confirms update installation")
if "checkForUpdates()" not in controller:
failures.append("`UpdateController.attemptUpdate()` no longer triggers a check before install")
if failures:
print("FAIL: command-palette update command regression(s) detected")
for failure in failures:
print(f"- {failure}")
return 1
print("PASS: command-palette update commands expose apply + attempt wiring")
return 0
if __name__ == "__main__":
raise SystemExit(main())