Updates tab#65
Conversation
Computed in the @mainactor protocol extension so the Updates tab badge, subtitle, and row projection all observe the same reconciled inventory without touching concrete-repo code or stubs.
Mirrors InstalledViewModel's search/selection/error-mapping verbatim and filters the projection to outdated packages. upgradeAll() submits one upgrade per outdated package; SerialBrewCommandCenter.submit dedupes duplicate in-flight ids, so a re-tap during a batch is a no-op and the existing repository reconcile-on-completion drops each finished row from the list automatically. isUpgradingAny tracks the allPhaseChanges stream for the Update All button's disabled state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UpdatesColumnsRoot reads the env and threads the repo / command center /
factory into UpdatesColumns, which owns the VM and applies the Updates
title + dynamic outdatedSubtitle to the window chrome.
UpdatesPackagesView mirrors InstalledPackagesView with three diffs:
header copy + Update All (n) keyboard-shortcut button when there are
outdated rows, an empty state ("You're all caught up" + Refresh) when
the loaded projection is empty, and a redacted skeleton tinted for the
outdated case while loading. Rows reuse InstalledListRowRoot so the
warning badge and version arrow line stay consistent with Installed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an Updates sidebar entry between Installed and Discover with an UpdatesSidebarBadge that shows a warning-tinted outdated-count capsule when the inventory has outdated rows. The badge reads the installed- packages repository from the env so it recomputes when the existing reconcile-on-completion drops a row from the outdated set. sidebarRow now takes an optional trailing ViewBuilder accessory so the existing rows keep their call sites and the new row can place the badge after the Spacer. MainWindowView routes .updates to UpdatesColumnsRoot, which owns the dynamic navigation subtitle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The empty state previously fired off the filtered projection, so a search that hid every outdated row celebrated "You're all caught up" even when updates were still available. Split it into two: when the inventory has outdated packages but none match the search, show "No matching updates" with the hidden count and a Show All button that clears the search; only when totalOutdatedCount is genuinely zero does the original copy fire. The two empty states share a centredEmptyState helper and the loading skeleton's repeated placeholders collapse to Array(repeating:count:) so the view type stays under the project's body-length lint cap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outdatedSubtitle previously switched on the filtered count, so the list header and window chrome both said "All packages are up to date" the moment a search hid every outdated row — exactly the same false positive the empty state used to give. While the search is active the subtitle now reads "Showing N of M updates" when at least one matches or "No matches in M outdated packages" when none do; with no active search it still reflects the unfiltered inventory. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
content.packages is name-sorted across kinds by BrewInfoJSON.mapping, so the Updates list could pick an alphabetically-earlier cask (e.g. alfred) as the default selection while the view showed the Formulae section first — a confusing mismatch between selection and visual order. allRows now returns formulaPackages + caskPackages so firstVisibleRowID mirrors the list layout: first formula if any, else first cask. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The other tabs (Installed, Discover, Doctor, Configuration) all describe the tab's purpose in their navigation subtitle, not the current content of the screen. Match that pattern: MainWindowView now owns "Review and upgrade outdated packages" for the .updates case, and UpdatesColumns stops applying its own dynamic subtitle. The dynamic "Showing N of M" / "No matches" copy still drives the in-page list header where state-aware information actually belongs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nding Groundwork for cross-tab "Used by" navigation. A new navigateToInstalledPackage env entry will let other tabs request that a specific package open in the Installed list; this commit gets the receiving side ready without changing any user-visible behaviour yet. InstalledViewModel.init now takes an optional initialSelection that seeds selectedPackageID directly. The existing activeSelectedPackageID fallback already drops a candidate that isn't in allRows, so a deep link applied before the inventory loads simply resolves on the first observation-driven re-render once allRows lands — no extra observer or pending-queue plumbing required. InstalledColumnsRoot / InstalledColumns thread a Binding<InstalledBrewPackage.ID?> through and clear it on appear so the deep link is a single-shot signal. Default .constant(nil) keeps all existing call sites compiling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MainWindowView now provides the navigateToInstalledPackage env closure that pairs sidebar switch with a deep-link selection write into pendingInstalledSelection, and threads that pending value as a Binding into InstalledColumnsRoot's deepLinkSelection slot. The Installed column consumes the deep link via its VM's initialSelection and clears the binding on appear so it's a one-shot signal. UpdatesColumnsRoot reads the closure from the env (env reads stay in Roots) and threads it into UpdatesColumns, which uses it as InstalledPackageDetailRoot.onSelectInstalledPackage. The local Updates VM's selectInstalledPackage path stays for the in-list onTapGesture selections — only the detail panel's Dependency / Used-by row taps go cross-tab. Installed tab keeps its existing intra-tab onSelectInstalledPackage wiring; nothing changes for Installed → Installed taps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the bespoke InstalledLoadState enum with the standard LoadState<InstalledPackagesContent, String>, adds Placeholdable conformance on InstalledPackagesContent, and routes both surfaces through AsyncContentView so loading/error/retry match the convention used by Doctor, Config, and Discover. The Updates tab keeps its two distinct empty states (all-caught-up vs no-search-matches) inside the loaded closure, and both error states now render a Retry button wired to viewModel.refresh(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the per-package upgrade loop in UpgradesViewModel.upgradeAll with a single bulk submit. Adds BrewOperationID.bulkUpgrade (singleton) and BrewOperationKind.upgradeAll, wires bulkUpgradeCommand() through the factory protocol, ships BulkUpgradeCommand in BrewCLI, and extends the CommandJob switches so the bulk op renders "brew upgrade" in the console. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructures the Updates header from an HStack to a vertical stack: title / subtitle, then a CommandBlockView showing the user-facing "brew upgrade" command (matching the package detail view chrome), then the "Upgrade All (n)" button styled as bordered-prominent. Adds bulkUpgradeDisplayCommand on UpgradesViewModel so the view and command center share one source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
There was a problem hiding this comment.
Pull request overview
Adds a dedicated Upgrades tab to BrewUI for reviewing and upgrading outdated formulae/casks, including a sidebar badge for the outdated count and a bulk “Upgrade All” flow implemented as a single brew upgrade operation. It also introduces cross-tab navigation into the Installed tab for “Used by”/dependency taps, and aligns Installed/Upgrades list loading/error chrome via AsyncContentView + LoadState.
Changes:
- Introduces
UpgradesViewModel+UpgradesColumns/UpgradesPackagesViewand a sidebar badge powered byInstalledInventoryObserving.outdatedCount. - Adds a bulk upgrade operation (
BrewOperationID.bulkUpgrade,BrewOperationKind.upgradeAll) with a newBulkUpgradeCommand, plus console rendering support. - Adds navigation plumbing (
navigateToInstalledPackageenv entry + Installed deep-link selection binding) and updates terminology from “Update” → “Upgrade”.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift | New unit tests for Upgrades projection/search/selection and bulk upgrade submission behavior. |
| Tests/BrewFeatureInstalledTests/UpgradePackageItemTests.swift | Updates button copy expectations from “Update” to “Upgrade”. |
| Tests/BrewFeatureInstalledTests/InstalledViewModelTestsSupport.swift | Migrates snapshot helper to LoadState<InstalledPackagesContent, String>. |
| Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift | Adds deep-link initial selection tests; updates error-state assertions to .failed. |
| Tests/BrewFeatureInstalledTests/InstalledPackageRowPresentationTests.swift | Updates accessibility summary copy to “Upgrade available…”. |
| Tests/BrewFeatureInstalledTests/InstalledListRowViewModelTests.swift | Renames update-availability API to upgrade-availability in tests. |
| Tests/BrewFeatureInstalledTests/InstalledDetailsViewModelTestsSupport.swift | Adds a submit-recording command center test double used by Upgrades tests. |
| Tests/BrewFeatureConsoleTests/CommandJobTests.swift | Ensures bulk upgrade job renders the shared display command. |
| Tests/BrewCLITests/BulkUpgradeCommandTests.swift | New tests for BulkUpgradeCommand invocation/operation kind. |
| Sources/BrewRepositoryInterfaces/Protocols/InstalledInventoryObserving+Outdated.swift | Adds outdatedPackages/outdatedCount derived from installed inventory state. |
| Sources/BrewRepositoryInterfaces/Fakes/Stubs.swift | Extends stub mutating command factory with bulkUpgradeCommand(). |
| Sources/BrewRepositoryInterfaces/CommandJob.swift | Adds command rendering support for .bulkUpgrade and .upgradeAll. |
| Sources/BrewFeatureInstalled/Views/UpgradesSidebarBadge.swift | New sidebar badge showing outdated-count capsule. |
| Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift | New Upgrades middle-column UI (header, Upgrade All, list, empty/search-empty states). |
| Sources/BrewFeatureInstalled/Views/UpgradesColumns.swift | New Upgrades content/detail split view, reusing Installed detail chrome. |
| Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift | Migrates Installed list chrome to AsyncContentView. |
| Sources/BrewFeatureInstalled/Views/InstalledListRowView.swift | Renames update-availability usage to upgrade-availability. |
| Sources/BrewFeatureInstalled/Views/InstalledColumns.swift | Adds deep-link selection binding and clears it on appear. |
| Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift | New view model that projects outdated rows, supports search/selection, and submits bulk upgrade. |
| Sources/BrewFeatureInstalled/ViewModels/UpgradePackageItem.swift | Updates user-facing copy from “Update to …” to “Upgrade to …”. |
| Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift | Migrates to LoadState, adds optional initial selection for deep links. |
| Sources/BrewFeatureInstalled/ViewModels/InstalledPackagesContent+Placeholdable.swift | Adds placeholder data for redacted skeleton loading under AsyncContentView. |
| Sources/BrewFeatureInstalled/ViewModels/InstalledListRowViewModel.swift | Renames and updates copy to “Upgrade available…”. |
| Sources/BrewCore/Operations/BrewOperationModels.swift | Adds upgradeAll, .bulkUpgrade, and shared bulkUpgradeDisplayCommand. |
| Sources/BrewCore/Operations/BrewMutatingCommandFactory.swift | Adds bulkUpgradeCommand() factory API. |
| Sources/BrewCLI/LiveBrewMutatingCommandFactory.swift | Wires live factory to return BulkUpgradeCommand(). |
| Sources/BrewCLI/BulkUpgradeCommand.swift | New mutating command that runs brew upgrade with no args. |
| Sources/BrewAppEnvironment/UnimplementedRepositories.swift | Adds unimplemented bulkUpgradeCommand() stub to keep environment wiring complete. |
| Sources/BrewAppEnvironment/NavigationEnvironment.swift | Adds navigateToInstalledPackage environment entry for cross-tab deep links. |
| Brew/Views/MainSidebarView.swift | Adds Upgrades sidebar item with badge accessory. |
| Brew/Features/MainWindow/Views/MainWindowView.swift | Adds Upgrades tab routing and deep-link plumbing into Installed. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for await (id, phase) in stream { | ||
| switch phase { | ||
| case .running: | ||
| runningIDs.insert(id) | ||
| case .idle, .failed: | ||
| runningIDs.remove(id) | ||
| } |
| import BrewCore | ||
| import BrewRepositoryInterfaces | ||
| import BrewUIComponents | ||
| import Foundation | ||
| import Observation |
| import BrewCore | ||
| import BrewRepositoryInterfaces | ||
| import BrewUIComponents | ||
| import SwiftUI | ||
|
|
| import BrewCore | ||
| import BrewRepositoryInterfaces | ||
| import Foundation |
| func phaseChanges(for _: BrewOperationID) async -> AsyncStream<BrewOperationPhase> { | ||
| AsyncStream<BrewOperationPhase>(bufferingPolicy: .unbounded) { continuation in | ||
| continuation.yield(.idle) | ||
| } | ||
| } |
PR: Add an Updates tab for reviewing and upgrading outdated packages
Summary
Adds a dedicated Updates tab to BrewUI for reviewing and upgrading outdated formulae and casks. Reuses the Installed tab's row + detail panel chrome, projects outdated rows off the same reconciled inventory, surfaces a sidebar badge for the outdated count, and ships a bulk "Upgrade All" that runs as a single
brew upgradesubprocess.Changes
New Updates surface
UpdatesViewModelmirrorsInstalledViewModel's search/selection/error-mapping and filters the projection to outdated packages.UpdatesPackagesView+UpdatesColumnsreuseInstalledListRowRootandInstalledPackageDetailRootso row/detail chrome stays consistent across tabs.UpdatesSidebarBadgeshows a warning-tinted outdated-count capsule, recomputing as repository reconcile drops rows post-upgrade.outdatedPackages/outdatedCountare derived in theInstalledInventoryObserving@MainActorextension, so the badge, subtitle, and list observe one reconciled source.Bulk upgrade
BrewOperationID.bulkUpgrade+BrewOperationKind.upgradeAlland a newBulkUpgradeCommandrun "Upgrade All" as a singlebrew upgradesubprocess instead of N per-package submissions.CommandJobandCommandBlockViewrender the bulk op asbrew upgradein the console + Updates header.bulkUpgradeDisplayCommandonUpgradesViewModelis the single source of truth shared between the header chip and the command center.Cross-tab "Used by" deep-link
NavigationEnvironmentgainsnavigateToInstalledPackage.MainWindowViewprovides the closure (sidebar switch + pending selection write);InstalledColumnsconsumes a deep-linkBindingthat clears on appear.InstalledViewModel.inittakes an optionalinitialSelectionto seedselectedPackageIDbefore inventory lands.Convention cleanups while in the area
AsyncContentView/LoadState<InstalledPackagesContent, String>(matches Doctor, Config, Discover); both now render a Retry button wired toviewModel.refresh().Showing N of M updates/No matches in M outdated packages) so it stops claiming caught-up when search hides rows.formulae + caskslayout instead of picking an alphabetically-earlier cask.Update‚ÜíUpgradeterminology pass across user-facing copy.Testing
xcrun swift test— per-layer SPM tests (addedUpgradesViewModelTests,BulkUpgradeCommandTests, extendedInstalledViewModelTests,CommandJobTests, etc.)xcodebuild -scheme Brew-Unitfor the app-hosted pathbrew upgradeand the Updates list reconciles; deep-link from Updates → Installed via "Used by" row selects the right package; empty / search-empty / all-caught-up states render correctly; Retry on error works on both tabs.PR checklist
Claude wrote almost all of the code on this branch; I directed scope, reviewed each commit, and gave correction/feedback iterations (e.g. distinguishing "no matches" from "caught up", making the bulk upgrade one subprocess instead of N submissions, fixing the default-selection mismatch, keeping the static-vs-dynamic subtitle split consistent with other tabs). Manual verification: ran the app against real
brewstate, exercised Upgrade All end-to-end, walked the Updates ‚Üí Installed deep-link, and confirmed sidebar badge / empty states under search.