The tier axis comes out of the git stack — compose unconditional, postures mode-driven

HistoryStore.compose(boardRoot📒) returns non-optional and runs for
every session — the nil the gate produced was the only nil it ever had.
makeHistoryProvider is a one-axis decision: git-mode boards bind the git
provider, everything else native, in every tier; Session.tier stays
recorded, dormant. BoardGitSection shrinks to the four mode postures
(.absent and .proPointer die, BoardGitNote and the .git probe with them);
every board carries all three popover tabs (BoardInfoTab.available
retired); the titlebar branch shows on any git-mode board; the settings
sheet and card History section stop reading tier. InertGitTests is
repurposed as UntouchedGitTests — the file layer still never opens .git,
now load-bearing for mode-none boards. The accessibility audit reaches the
settings sheet at last: the fixture board hosts it in every tier, so the
free-fixture disabled-row test becomes an open-and-audit test.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-07 20:21:28 -04:00
parent da0d7fd2d7
commit 798a8bac73
32 changed files with 830 additions and 904 deletions
+45 -88
View File
@@ -13,8 +13,10 @@ import SwiftUI
/// which the pre-tab body's mode-aware git section rehomed into whole postures, notes, and the
/// Board Settings row, none of them re-ruled by the move.
///
/// **Tab membership is the git posture's** (`BoardInfoTab.available(tier:mode:hasGitDirectory:)`),
/// and **selection resets to Info on every open** both the Git session's rulings.
/// **Tab membership is the git posture's**, and **selection resets to Info on every open** both
/// the Git session's rulings. Since 12-editions.md PIVOT 2026-08-07 the first of those is a
/// structural rule with nothing left to exclude: every board carries all three tabs (03 Board
/// popover, the same-day pivot note) see `BoardInfoTab`.
///
/// ### One home, deliberately
///
@@ -62,8 +64,8 @@ extension FocusedValues {
// MARK: - The window-title widget
/// The titlebar widget: the board's name and, on a git-mode Pro board, its branch with a
/// trailing disclosure chevron, whose one job is this popover.
/// The titlebar widget: the board's name and, on a git-mode board, its branch with a trailing
/// disclosure chevron, whose one job is this popover.
///
/// **The popover is anchored to the widget itself** it hangs from the button rather than from the
/// window or the board which is what makes the affordance and the surface read as one thing. A
@@ -80,12 +82,14 @@ struct BoardInfoWidget: View {
let store: BoardStore
let recents: StyleRecents
/// The tier and the git state this board's **session** composed with read once, at the moment
/// the widget is installed, and never re-derived (12-editions.md The entitlement: "a lapse
/// never interrupts an open session"). `git` is a reference type and `@Observable`, so add-git
/// flipping the mode, or a branch switch, redraws the widget without anything here being
/// re-created.
let tier: Tier
/// The git state this board's **session** composed with read once, at the moment the widget is
/// installed, and never re-derived (12-editions.md The entitlement: "a lapse never interrupts
/// an open session"). `git` is a reference type and `@Observable`, so add-git flipping the mode,
/// or a branch switch, redraws the widget without anything here being re-created.
///
/// It stopped being a tier signal at 12 PIVOT 2026-08-07 every session composes a git state
/// now, whatever the tier so a `nil` here means only "this caller had no session to hand over"
/// (previews, the accessory-installation tests), which reads as mode `none`.
let git: HistoryStore?
@Bindable var presentation: BoardInfoPresentation
@@ -105,7 +109,6 @@ struct BoardInfoWidget: View {
BoardInfoTitlebarSummary(
snapshotTitle: store.snapshot.title.value,
rootURL: store.rootURL,
tier: tier,
mode: git?.mode ?? .none,
branch: git?.branch
)
@@ -173,7 +176,7 @@ struct BoardInfoWidget: View {
// costs nothing on the other four postures.
.task { await git?.refreshBranch() }
.popover(isPresented: $presentation.isPresented, arrowEdge: .bottom) {
BoardInfoView(store: store, recents: recents, tier: tier, git: git, settings: settings)
BoardInfoView(store: store, recents: recents, git: git, settings: settings)
}
}
@@ -212,23 +215,26 @@ struct BoardInfoWidget: View {
/// with plain values and no fixture board on disk the one duplication this card leaves behind
/// rather than reshaping `AppModel.displayName(of:)`'s signature to fit both call sites.
///
/// **Branch.** Shown only when the board is actually git-mode under Pro `tier == .pro && mode ==
/// .git` with a non-`nil` branch the same condition family `BoardGitSection.resolve`'s `.branch`
/// case covers. The free tier and an inert `.git` (mode `.none` or `.repoNested`) show no branch;
/// neither does a git-mode board whose branch has not been read yet (`HistoryStore.branch` starts
/// `nil` until `refreshBranch()` answers, which the widget's own `.task` kicks off at open).
/// **Branch.** Shown only when the board is actually git-mode `mode == .git` with a non-`nil`
/// branch the same condition `BoardGitSection.resolve`'s `.branch` case covers. **The tier clause
/// is gone** (12-editions.md PIVOT 2026-08-07: git is tier-independent, so a git-mode board is a
/// git-mode board and the widget says so on every tier); the rule it read `tier == .pro && mode ==
/// .git` until that day is recorded here rather than restated in code. A board with no repository or
/// an inert one (mode `.none`, `.repoNested`, `.unverifiable`) shows no branch; neither does a
/// git-mode board whose branch has not been read yet (`HistoryStore.branch` starts `nil` until
/// `refreshBranch()` answers, which the widget's own `.task` kicks off at open).
struct BoardInfoTitlebarSummary: Equatable {
let title: String
let branch: String?
init(snapshotTitle: String?, rootURL: URL, tier: Tier, mode: BoardGitMode, branch: String?) {
init(snapshotTitle: String?, rootURL: URL, mode: BoardGitMode, branch: String?) {
if let snapshotTitle, !snapshotTitle.isEmpty {
self.title = snapshotTitle
} else {
self.title = rootURL.deletingPathExtension().lastPathComponent
}
self.branch = (tier == .pro && mode == .git) ? branch : nil
self.branch = mode == .git ? branch : nil
}
}
@@ -240,14 +246,13 @@ struct BoardInfoTitlebarSummary: Equatable {
/// window, removed on detach for the same reason it owns the delegate proxying: the window is
/// SwiftUI's, and anything hung on it has to be taken back off.
@MainActor
/// `tier`/`git` default to the free tier's posture a popover with no git section at all and
/// `settings` to no sheet, so that a caller with no session in hand (the accessory-installation
/// tests, which are about AppKit plumbing rather than about git) describes a board honestly rather
/// than by accident. The app's own call site passes the session's values explicitly.
/// `git` defaults to no session and `settings` to no sheet, so that a caller with none in hand (the
/// accessory-installation tests, which are about AppKit plumbing rather than about git) describes a
/// board honestly rather than by accident: a Git tab in its no-repository posture, and no
/// Board Settings row behind it. The app's own call site passes the session's values explicitly.
func boardInfoTitlebarAccessory(
store: BoardStore,
recents: StyleRecents,
tier: Tier = .free,
git: HistoryStore? = nil,
presentation: BoardInfoPresentation,
settings: BoardSettingsPresentation? = nil
@@ -256,7 +261,6 @@ func boardInfoTitlebarAccessory(
rootView: BoardInfoWidget(
store: store,
recents: recents,
tier: tier,
git: git,
presentation: presentation,
settings: settings
@@ -291,26 +295,16 @@ enum BoardInfoTab: String, CaseIterable, Identifiable {
var id: Self { self }
/// **Which tabs this board actually offers** membership *is* the git posture (03-board-ui.md
/// § Board popover, the Git session's ruling, 2026-08-07): the Git tab joins the strip only when
/// the git section has something true to say, and drops out exactly where `BoardGitSection`
/// resolves to `.absent` the free tier's ordinary board, which shows Info | Theme alone.
///
/// That carries 12-editions.md's "absent, no placeholder" rule up to the tab strip: a standing
/// Git tab on every free board would be the standing ad for Pro that 12 forbids, and an empty tab
/// behind it would be the placeholder 12 forbids twice over.
///
/// **Delegated, never restated.** The posture matrix is settled, test-pinned design
/// (`BoardGitSectionTests`), and a second reading of "is there git to show here" is a reading
/// that can disagree with the tab it labels so this asks `BoardGitSection.resolve` the same
/// question `BoardGitTabView` asks, from the same three facts.
///
/// Order is `allCases`' own Info, then Theme, then Git so the Git tab, where present, is
/// always last and the two standing tabs never move under a user who switched boards.
static func available(tier: Tier, mode: BoardGitMode, hasGitDirectory: Bool) -> [BoardInfoTab] {
let posture = BoardGitSection.resolve(tier: tier, mode: mode, hasGitDirectory: hasGitDirectory)
return allCases.filter { $0 != .git || posture != .absent }
}
// **Membership is the git posture's, and the posture never says "absent" any more.** The Git
// session ruled (2026-08-07) that the Git tab joins the strip only where `BoardGitSection` has
// something true to say, which then meant dropping it on a free board with no `.git` the
// "absent, no placeholder" rule carried up to the strip. **12-editions.md PIVOT 2026-08-07**,
// the same day, retired the free-only postures wholesale: git left the paywall, `.absent` and
// `.proPointer` died with it, and 03-board-ui.md Board popover records the consequence "the
// absent posture is unreachable and every board carries all three tabs. The membership rule
// stands structurally the strip still asks the posture it just never hears 'absent'
// anymore." So there is no `available()` filter here to ask it with: membership is `allCases`,
// in `allCases`' own order (Info, Theme, Git), which is what the filter answered anyway.
}
// MARK: - The popover's content
@@ -326,7 +320,6 @@ struct BoardInfoView: View {
let store: BoardStore
let recents: StyleRecents
let tier: Tier
let git: HistoryStore?
let settings: BoardSettingsPresentation?
@@ -337,17 +330,6 @@ struct BoardInfoView: View {
/// `BoardInfoWidget` hands `.popover` fresh on every open, is exactly that rule and nothing more.
@State private var tab: BoardInfoTab = .info
/// Whether this board carries a `.git` checked once, off disk, when the view is built (which
/// is every time the popover opens, since `BoardInfoWidget` hands `.popover` a fresh instance).
/// See `BoardGitNote.hasGitDirectory(at:)` for why a live-updating fact isn't needed here.
///
/// **The free tier's input only.** Under Pro the Git tab reads the session's detected mode
/// instead a fact settled at open, which is where 06-history-undo.md puts detection and this
/// stays what it always was: the one quiet question the free tier asks of a board's folder. Since
/// the tab restructure it answers for the tab *strip* as well as for the tab's content
/// (`tabs` below), which is why it is read once here and handed down rather than asked twice.
private let hasGitDirectory: Bool
/// The style editor brings its own padding, so the sections around it carry the same number by
/// hand instead of an outer padding that would double up on it **the editor's own figure**
/// (`StyleEditorLayout.sectionSpacing`), which is font-derived, so the popover's chrome scales
@@ -356,36 +338,16 @@ struct BoardInfoView: View {
StyleEditorLayout.sectionSpacing(bodyPointSize: CardWindowMetrics.bodyPointSize)
}
/// **The tab strip's membership** (`BoardInfoTab.available`), read inside `body` so the mode it
/// delegates to is live `HistoryStore` is `@Observable`, and add-git flipping a board's mode
/// must not leave a stale strip behind it.
///
/// **Stable across an open, in practice and by construction.** `tier` is the session's, fixed at
/// the widget's birth (12-editions.md The entitlement: "a lapse never interrupts an open
/// session"), and `hasGitDirectory` is one `init`-time read; the mode is the only live input, and
/// `BoardGitSection.resolve` answers `.absent` only under the free tier, where it never consults
/// the mode at all. So no live change can move the Git tab in or out from under the selection.
/// Add-git the one gesture that flips a mode runs from the settings sheet anyway, which the
/// Board Settings row dismisses this popover to reach.
private var tabs: [BoardInfoTab] {
BoardInfoTab.available(tier: tier, mode: git?.mode ?? .none, hasGitDirectory: hasGitDirectory)
}
init(
store: BoardStore,
recents: StyleRecents,
tier: Tier = .free,
git: HistoryStore? = nil,
settings: BoardSettingsPresentation? = nil
) {
self.store = store
self.recents = recents
self.tier = tier
self.git = git
self.settings = settings
// Asked only where it is the answer: under Pro the mode already knows, and a free-tier
// board is the only one this question is for (12-editions.md The free tier and `.git`).
self.hasGitDirectory = tier == .free && BoardGitNote.hasGitDirectory(at: store.rootURL)
}
var body: some View {
@@ -432,10 +394,12 @@ struct BoardInfoView: View {
// The tab bar: a segmented control rather than a `TabView`, because the popover is a
// compact settings surface and the segmented idiom is the macOS shape for switching
// between a handful of peer panes inside one. The label is hidden visually but stays
// the control's accessibility name. It iterates `tabs`, not `allCases` membership is
// this board's git posture (`BoardInfoTab.available`).
// the control's accessibility name. It iterates `allCases` the strip's membership is
// still the board's git posture in principle (the Git session's ruling), and since
// 12-editions.md PIVOT 2026-08-07 that posture is never "absent", so every board
// carries all three tabs (03-board-ui.md Board popover, the same-day pivot note).
Picker("Board configuration", selection: $tab) {
ForEach(tabs) { tab in
ForEach(BoardInfoTab.allCases) { tab in
Text(tab.rawValue)
}
}
@@ -452,14 +416,7 @@ struct BoardInfoView: View {
case .theme:
BoardThemeTabView(store: store, inset: inset)
case .git:
BoardGitTabView(
store: store,
tier: tier,
git: git,
settings: settings,
hasGitDirectory: hasGitDirectory,
inset: inset
)
BoardGitTabView(store: store, git: git, settings: settings, inset: inset)
}
}
// The style editor's popover width, taken from the editor rather than restated the number