Skip to content

Commit 0de67ef

Browse files
committed
Fix update install feedback
1 parent 04ccfde commit 0de67ef

7 files changed

Lines changed: 170 additions & 13 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file is for coding agents and maintainers working in this repository. Keep
88

99
The macOS engineering skills used by this repository are vendored in `.agents/skills/` and are tracked as project files. When the runtime lists those skills with the `r7` root, expand `r7` to this repository's `.agents/skills` directory. Do not look for these project-local skills under Codex plugin cache paths such as `~/.codex/plugins/cache/openai-primary-runtime`.
1010

11-
Current release metadata: `0.1.4`. This is a pre-stable product; breaking changes to local install state, registry format, CLI UX, and trust policy are acceptable when they improve security or clarity.
11+
Current release metadata: `0.1.5`. This is a pre-stable product; breaking changes to local install state, registry format, CLI UX, and trust policy are acceptable when they improve security or clarity.
1212

1313
## Product Boundary
1414

Sources/App/Services/AppUpdateInstaller.swift

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
import Foundation
22

3+
struct AppUpdateProgress: Equatable, Sendable {
4+
var step: Int
5+
var totalSteps: Int
6+
var title: String
7+
var detail: String
8+
9+
var fractionCompleted: Double {
10+
guard totalSteps > 0 else { return 0 }
11+
return min(max(Double(step) / Double(totalSteps), 0), 1)
12+
}
13+
}
14+
15+
typealias AppUpdateProgressHandler = @MainActor @Sendable (AppUpdateProgress) -> Void
16+
317
protocol AppUpdateInstalling: Sendable {
4-
func install(update: AppUpdateRelease) async throws
18+
func install(update: AppUpdateRelease, onProgress: AppUpdateProgressHandler?) async throws
519
}
620

721
struct SourceArchiveAppUpdateInstaller: AppUpdateInstalling {
822
var session: URLSession = .shared
923

10-
func install(update: AppUpdateRelease) async throws {
24+
func install(update: AppUpdateRelease, onProgress: AppUpdateProgressHandler? = nil) async throws {
1125
guard let archiveURL = update.sourceArchiveURL else {
1226
throw AppUpdateInstallError.missingArchive
1327
}
@@ -21,31 +35,75 @@ struct SourceArchiveAppUpdateInstaller: AppUpdateInstalling {
2135

2236
try fileManager.createDirectory(at: extractDirectory, withIntermediateDirectories: true)
2337
do {
38+
await report(
39+
step: 1,
40+
title: "Downloading update",
41+
detail: "Downloading \(update.displayName) from GitHub.",
42+
onProgress: onProgress
43+
)
2444
let (downloadedURL, response) = try await session.download(from: archiveURL)
2545
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
2646
throw AppUpdateInstallError.downloadHTTPStatus(http.statusCode)
2747
}
2848
try fileManager.moveItem(at: downloadedURL, to: archive)
49+
await report(
50+
step: 2,
51+
title: "Unpacking update",
52+
detail: "Preparing the downloaded source archive.",
53+
onProgress: onProgress
54+
)
2955
try await runProcess(
3056
executable: URL(fileURLWithPath: "/usr/bin/ditto"),
3157
arguments: ["-x", "-k", archive.path, extractDirectory.path],
3258
currentDirectory: nil,
3359
logURL: logURL
3460
)
3561
let sourceRoot = try findSourceRoot(in: extractDirectory)
62+
await report(
63+
step: 3,
64+
title: "Verifying update",
65+
detail: "Checking that the archive matches version \(update.versionLabel).",
66+
onProgress: onProgress
67+
)
3668
try validateVersion(update: update, sourceRoot: sourceRoot)
69+
await report(
70+
step: 4,
71+
title: "Installing local build",
72+
detail: "Running scripts/install_local.sh --load. The app may reopen when this finishes.",
73+
onProgress: onProgress
74+
)
3775
try await runProcess(
3876
executable: sourceRoot.appendingPathComponent("scripts/install_local.sh"),
3977
arguments: ["--load", "--cleanup-path", workDirectory.path],
4078
currentDirectory: sourceRoot,
4179
logURL: logURL
4280
)
81+
await report(
82+
step: 5,
83+
title: "Cleaning up",
84+
detail: "Removing the temporary downloaded files.",
85+
onProgress: onProgress
86+
)
4387
try? fileManager.removeItem(at: workDirectory)
4488
} catch {
4589
throw AppUpdateInstallError.failed(error, logURL: logURL)
4690
}
4791
}
4892

93+
private func report(
94+
step: Int,
95+
title: String,
96+
detail: String,
97+
onProgress: AppUpdateProgressHandler?
98+
) async {
99+
await onProgress?(AppUpdateProgress(
100+
step: step,
101+
totalSteps: 5,
102+
title: title,
103+
detail: detail
104+
))
105+
}
106+
49107
private func findSourceRoot(in directory: URL) throws -> URL {
50108
let fileManager = FileManager.default
51109
let contents = try fileManager.contentsOfDirectory(

Sources/App/Services/UISmokeRunner.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,7 @@ enum UISmokeRunner {
10581058
try expect(store.successMessage == "Agentic Secrets 9.0.0 is available", "manual update check reports available release")
10591059
await store.installUpdate(latest)
10601060
try expect(store.availableUpdate == nil, "successful update install clears available release")
1061+
try expect(store.updateInstallProgress == nil, "successful update install clears progress feedback")
10611062
try expect(store.successMessage == "Agentic Secrets 9.0.0 installed", "successful update install reports installed release")
10621063
store.availableUpdate = latest
10631064
try verifyHostingLayout(
@@ -1550,7 +1551,14 @@ private struct StubAppUpdateChecker: AppUpdateChecking {
15501551
}
15511552

15521553
private struct StubAppUpdateInstaller: AppUpdateInstalling {
1553-
func install(update: AppUpdateRelease) async throws {}
1554+
func install(update: AppUpdateRelease, onProgress: AppUpdateProgressHandler?) async throws {
1555+
await onProgress?(AppUpdateProgress(
1556+
step: 1,
1557+
totalSteps: 1,
1558+
title: "Installing local build",
1559+
detail: "Running synthetic update installer."
1560+
))
1561+
}
15541562
}
15551563

15561564
private enum UISmokeRunnerSnapshotFactory {

Sources/App/Stores/ControlPlaneStore.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ final class ControlPlaneStore {
132132
var availableUpdate: AppUpdateRelease?
133133
var isCheckingForUpdates = false
134134
var isInstallingUpdate = false
135+
var updateInstallProgress: AppUpdateProgress?
135136
var lastUpdateCheck: Date?
136137

137138
private let client: any ControlPlaneClient
@@ -374,6 +375,9 @@ final class ControlPlaneStore {
374375
func clearFeedback() {
375376
errorMessage = nil
376377
successMessage = nil
378+
if !isInstallingUpdate {
379+
updateInstallProgress = nil
380+
}
377381
}
378382

379383
func clearSuccessIfCurrent(_ message: String) {
@@ -434,16 +438,30 @@ final class ControlPlaneStore {
434438
func installUpdate(_ update: AppUpdateRelease) async {
435439
guard !isInstallingUpdate else { return }
436440
isInstallingUpdate = true
437-
successMessage = "Installing \(update.displayName)"
441+
updateInstallProgress = AppUpdateProgress(
442+
step: 0,
443+
totalSteps: 5,
444+
title: "Starting update",
445+
detail: "Preparing to install \(update.displayName)."
446+
)
447+
successMessage = nil
438448
errorMessage = nil
439-
defer { isInstallingUpdate = false }
449+
defer {
450+
isInstallingUpdate = false
451+
}
440452
do {
441-
try await updateInstaller.install(update: update)
453+
try await updateInstaller.install(update: update) { [weak self] progress in
454+
self?.updateInstallProgress = progress
455+
self?.successMessage = nil
456+
self?.errorMessage = nil
457+
}
442458
availableUpdate = nil
459+
updateInstallProgress = nil
443460
successMessage = "\(update.displayName) installed"
444461
errorMessage = nil
445462
await refresh()
446463
} catch {
464+
updateInstallProgress = nil
447465
successMessage = nil
448466
errorMessage = "Could not install update: \(userFacingError(error))"
449467
}

Sources/App/Views/ContentView.swift

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,9 @@ private struct SidebarReleaseFooter: View {
307307
.font(.caption2)
308308
.foregroundStyle(.secondary)
309309
.lineLimit(1)
310-
if let update = store.availableUpdate {
310+
if let progress = store.updateInstallProgress {
311+
SidebarUpdateProgress(progress: progress)
312+
} else if let update = store.availableUpdate {
311313
SidebarUpdateButton(store: store, update: update)
312314
} else {
313315
HStack(spacing: 8) {
@@ -371,6 +373,33 @@ private struct SidebarUpdateButton: View {
371373
}
372374
}
373375

376+
private struct SidebarUpdateProgress: View {
377+
var progress: AppUpdateProgress
378+
379+
var body: some View {
380+
VStack(alignment: .leading, spacing: 5) {
381+
HStack(spacing: 6) {
382+
ProgressView(value: progress.fractionCompleted)
383+
.frame(maxWidth: 74)
384+
Text("\(progress.step)/\(progress.totalSteps)")
385+
.font(.caption2.monospacedDigit())
386+
.foregroundStyle(.secondary)
387+
}
388+
Text(progress.title)
389+
.font(.caption.weight(.semibold))
390+
.lineLimit(1)
391+
Text(progress.detail)
392+
.font(.caption2)
393+
.foregroundStyle(.secondary)
394+
.lineLimit(2)
395+
}
396+
.padding(.top, 2)
397+
.accessibilityElement(children: .combine)
398+
.accessibilityLabel("Installing update, step \(progress.step) of \(progress.totalSteps), \(progress.title)")
399+
.accessibilityValue(progress.detail)
400+
}
401+
}
402+
374403
private struct SidebarTextLink: View {
375404
var store: ControlPlaneStore
376405
var title: String
@@ -490,6 +519,9 @@ struct DetailView: View {
490519
.padding(10)
491520
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8))
492521
}
522+
if let progress = store.updateInstallProgress {
523+
UpdateProgressBanner(progress: progress)
524+
}
493525
if store.successMessage != nil || store.errorMessage != nil {
494526
FeedbackBanner(store: store)
495527
}
@@ -499,6 +531,38 @@ struct DetailView: View {
499531
}
500532
}
501533

534+
private struct UpdateProgressBanner: View {
535+
var progress: AppUpdateProgress
536+
537+
var body: some View {
538+
VStack(alignment: .leading, spacing: 7) {
539+
HStack(spacing: 10) {
540+
ProgressView()
541+
.controlSize(.small)
542+
.accessibilityHidden(true)
543+
Text(progress.title)
544+
.font(.callout.weight(.semibold))
545+
Spacer(minLength: 12)
546+
Text("Step \(progress.step) of \(progress.totalSteps)")
547+
.font(.caption.monospacedDigit())
548+
.foregroundStyle(.secondary)
549+
}
550+
ProgressView(value: progress.fractionCompleted)
551+
Text(progress.detail)
552+
.font(.caption)
553+
.foregroundStyle(.secondary)
554+
.fixedSize(horizontal: false, vertical: true)
555+
}
556+
.padding(.horizontal, 12)
557+
.padding(.vertical, 10)
558+
.frame(maxWidth: 520, alignment: .leading)
559+
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8))
560+
.accessibilityElement(children: .combine)
561+
.accessibilityLabel("Installing update, \(progress.title)")
562+
.accessibilityValue("Step \(progress.step) of \(progress.totalSteps). \(progress.detail)")
563+
}
564+
}
565+
502566
private struct FeedbackBanner: View {
503567
@Bindable var store: ControlPlaneStore
504568

Sources/CommandShim/main.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ struct AgenticSecretsCommandShim {
7171

7272
private static func runIPCHealth(_ args: [String]) throws {
7373
let socket = try requiredValue(after: "--socket", in: args)
74-
_ = try requiredValue(after: "--manifest", in: args)
75-
let version = value(after: "--version", in: args) ?? bundleShortVersion()
74+
let manifestPath = try requiredValue(after: "--manifest", in: args)
75+
let version = value(after: "--version", in: args)
76+
?? versionFromInstallManifest(path: manifestPath)
77+
?? bundleShortVersion()
7678
let path = CommandLine.arguments.first ?? "agentic-secrets-shim"
7779
let peer = try SelfBuildPeerValidator.identity(helperName: "agentic-secrets-shim", path: path, version: version)
7880
let request = BrokerIPCRequest(requestID: "req_" + shortDigest(UUID().uuidString, length: 12), operation: .health, peer: peer)
@@ -96,7 +98,14 @@ struct AgenticSecretsCommandShim {
9698

9799
private static func bundleShortVersion() -> String {
98100
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
99-
return version?.isEmpty == false ? version! : "0.1.3"
101+
return version?.isEmpty == false ? version! : "0.1.5"
102+
}
103+
104+
private static func versionFromInstallManifest(path: String) -> String? {
105+
guard let manifest = try? InstallManifestStore.load(path: path) else {
106+
return nil
107+
}
108+
return manifest.requirement(for: "agentic-secrets-shim")?.minimumVersion
100109
}
101110

102111
private static func coreDaemonPath() throws -> String {

version.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ APP_NAME=AgenticSecrets
22
APP_DISPLAY_NAME="Agentic Secrets"
33
APP_EXECUTABLE_NAME=AgenticSecrets
44
BUNDLE_ID=com.agenticsecrets.AgenticSecrets
5-
MARKETING_VERSION=0.1.4
5+
MARKETING_VERSION=0.1.5
66
RELEASE_CHANNEL=
7-
BUILD_NUMBER=6
7+
BUILD_NUMBER=7
88
MENU_BAR_APP=0

0 commit comments

Comments
 (0)