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:
commit
cb55cc7404
4 changed files with 203 additions and 0 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
126
tests/test_command_palette_update_commands.py
Executable file
126
tests/test_command_palette_update_commands.py
Executable 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue