An unopenable repository fails loudly — the standing row, the paused surface, the honest heal
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -420,4 +420,13 @@ enum AccessibilityPhrases {
|
||||
/// Reload breakage clearing — the board is reading its files again, which is a smaller claim
|
||||
/// than the lock's and is deliberately phrased as one.
|
||||
static let reloadBreakageCleared = "The board is loading again"
|
||||
|
||||
/// The unreadable repository clearing (06-history-undo.md ▸ Rules, ruled 2026-07-31: "the banner
|
||||
/// clears when a later open or reload finds the repo readable").
|
||||
///
|
||||
/// Stated as the regained capability, `readOnlyLockCleared`'s rule: what the user was waiting on
|
||||
/// is history advancing again, not libgit2 changing its mind about a folder. It stays the
|
||||
/// smaller claim of the two — nothing about editing the board was ever blocked by this
|
||||
/// condition, which is precisely what its own row says.
|
||||
static let repositoryUnreadableCleared = "History is recording again"
|
||||
}
|
||||
|
||||
@@ -14,11 +14,14 @@ import SwiftUI
|
||||
/// - **A read-only board disables them too** (02-architecture.md ▸ The lock's scope, which names "the
|
||||
/// popover's git controls" outright).
|
||||
/// - **A switch in flight disables them**, so a second click cannot start a second checkout.
|
||||
/// - **An unreadable repository reads as broken, never as still loading** (06 ▸ Rules, the
|
||||
/// corrupt-`.git` loud failure, ruled 2026-07-31: "Never a silent placeholder discovered only in
|
||||
/// the popover").
|
||||
struct BoardGitBranchSurface: Equatable {
|
||||
|
||||
/// What the branch line reads — the branch name, the short hash on a detached HEAD
|
||||
/// (`GitRepository.branchName` decides which), or the placeholder while the first read is in
|
||||
/// flight.
|
||||
/// (`GitRepository.branchName` decides which), the placeholder while the first read is in
|
||||
/// flight, or `unavailableLabel` when there is no readable repository for it to name.
|
||||
let branchLabel: String
|
||||
|
||||
/// Whether that label is the placeholder rather than an answer.
|
||||
@@ -27,34 +30,74 @@ struct BoardGitBranchSurface: Equatable {
|
||||
/// The pause's own sentence (`GitRepositoryPause.explanation`), or `nil` when the surface is live.
|
||||
let pauseExplanation: String?
|
||||
|
||||
/// **Whether the pause is the unopenable repository** — the one pause whose surface is not the
|
||||
/// pause note: nothing is in progress and no tool is coming to finish it, so the section says
|
||||
/// its own sentence instead (`unreadableNote`), and the branch line has no answer to wait for.
|
||||
let isRepositoryUnreadable: Bool
|
||||
|
||||
/// Whether the branch controls accept a click — the popover's switch picker, and the settings
|
||||
/// sheet's create field (`BoardSettingsSheet`), which resolves this same surface so that a
|
||||
/// paused repository, a read-only board and a switch in flight close both by one rule.
|
||||
let controlsEnabled: Bool
|
||||
|
||||
/// The line the branch display is read as by VoiceOver.
|
||||
///
|
||||
/// The broken case is spelled out rather than left to fall through "Branch \(label)": the label
|
||||
/// is a *state* there, not a name, and "Branch Unavailable" would read as a branch somebody
|
||||
/// called Unavailable.
|
||||
var accessibilityLabel: String {
|
||||
isReadingBranch ? "Reading branch" : "Branch \(branchLabel)"
|
||||
if isRepositoryUnreadable { return "Branch unavailable" }
|
||||
return isReadingBranch ? "Reading branch" : "Branch \(branchLabel)"
|
||||
}
|
||||
|
||||
static let placeholder = "…"
|
||||
|
||||
/// **What the branch line reads when the repository will not open** — an answer, not a
|
||||
/// placeholder, which is the whole of the ruling's "fails loudly" at this one control: the
|
||||
/// placeholder means "still reading" and would go on meaning it forever here.
|
||||
static let unavailableLabel = "Unavailable"
|
||||
|
||||
/// The second half of the paused sentence — 06's "says resolving it belongs to the tool that
|
||||
/// created it", said in the app's own voice and paired with the promise that makes it safe to
|
||||
/// wait: the app is not going to touch the repository behind the user's back.
|
||||
static let pauseCaption =
|
||||
"Finishing it belongs to the tool that started it; Lanework leaves the repository untouched."
|
||||
|
||||
/// **The popover's own sentence for an unreadable repository** (06 ▸ Rules, the corrupt-`.git`
|
||||
/// loud failure, ruled 2026-07-31: "with the whole git surface paused … and the popover's git
|
||||
/// section naming the state").
|
||||
///
|
||||
/// A sibling of the nested and unverifiable notes (`BoardInfoPopover`) and written in their
|
||||
/// register — one sentence, the state first and the consequence after — rather than the pause
|
||||
/// note's two lines, because both of *those* lines would be wrong here: nothing is "in
|
||||
/// progress", and there is no tool whose job it is to finish it. What it keeps from the pause
|
||||
/// note is the promise that matters most on a repository the app cannot read, in the ruling's
|
||||
/// own words.
|
||||
///
|
||||
/// It is deliberately **not** the banner's sentence (`BannerCenter.repositoryUnreadableMessage`)
|
||||
/// re-used: the strip announces a condition to somebody who has not asked, and this answers
|
||||
/// somebody looking straight at the git section — the register the neighbouring notes set.
|
||||
static let unreadableNote =
|
||||
"Lanework can't read this board's git repository, so history is paused; the repository is left untouched."
|
||||
|
||||
static func resolve(
|
||||
branch: String?,
|
||||
pause: GitRepositoryPause?,
|
||||
isSwitching: Bool,
|
||||
isWritable: Bool
|
||||
) -> BoardGitBranchSurface {
|
||||
BoardGitBranchSurface(
|
||||
branchLabel: branch ?? placeholder,
|
||||
isReadingBranch: branch == nil,
|
||||
// Derived from the pause rather than passed in beside it: the pause *is* how this state is
|
||||
// carried everywhere else (`GitRepositoryPause.unreadable`, seeded by the detection-time
|
||||
// probe and refreshed by every later read), so a second parameter would be a second answer
|
||||
// to one question — and a caller could hold them apart.
|
||||
let unreadable = pause == .unreadable
|
||||
return BoardGitBranchSurface(
|
||||
branchLabel: unreadable ? unavailableLabel : (branch ?? placeholder),
|
||||
// Never "reading" on an unreadable repository: there is nothing in flight, and the line
|
||||
// the ruling forbids is exactly the one that says otherwise forever.
|
||||
isReadingBranch: !unreadable && branch == nil,
|
||||
pauseExplanation: pause?.explanation,
|
||||
isRepositoryUnreadable: unreadable,
|
||||
controlsEnabled: pause == nil && isWritable && !isSwitching
|
||||
)
|
||||
}
|
||||
@@ -95,7 +138,13 @@ struct BoardGitControls: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
branchRow
|
||||
|
||||
if let explanation = surface.pauseExplanation {
|
||||
// **The unreadable repository names itself in its own sentence** (06 ▸ Rules, the
|
||||
// corrupt-`.git` loud failure) — checked before the pause note because it *is* a pause,
|
||||
// and the pause note's second line ("finishing it belongs to the tool that started it")
|
||||
// would be advice about an operation nobody started.
|
||||
if surface.isRepositoryUnreadable {
|
||||
caption(BoardGitBranchSurface.unreadableNote, tone: .primary)
|
||||
} else if let explanation = surface.pauseExplanation {
|
||||
pauseNote(explanation)
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,11 @@ struct BoardInfoView: View {
|
||||
/// controls inside it disable themselves (the Board Info ⌘I rule).
|
||||
@ViewBuilder
|
||||
private var boardSettingsRow: some View {
|
||||
if let settings, BoardSettingsAvailability.resolve(tier: tier, mode: git?.mode ?? .none) {
|
||||
if let settings, BoardSettingsAvailability.resolve(
|
||||
tier: tier,
|
||||
mode: git?.mode ?? .none,
|
||||
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
|
||||
) {
|
||||
Button("Board Settings…") {
|
||||
dismiss()
|
||||
settings.present()
|
||||
|
||||
@@ -69,13 +69,21 @@ final class BoardSettingsPresentation {
|
||||
/// What the sheet would show right now — and therefore, when empty, that there is no sheet to
|
||||
/// show (`BoardSettingsAvailability`).
|
||||
var sections: [BoardSettingsSection] {
|
||||
BoardSettingsSection.resolve(tier: tier, mode: git?.mode ?? .none)
|
||||
BoardSettingsSection.resolve(
|
||||
tier: tier,
|
||||
mode: git?.mode ?? .none,
|
||||
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
|
||||
)
|
||||
}
|
||||
|
||||
/// Both doors' validation: the menu row's `disabled` state and whether the popover shows its row
|
||||
/// at all.
|
||||
var isReachable: Bool {
|
||||
BoardSettingsAvailability.resolve(tier: tier, mode: git?.mode ?? .none)
|
||||
BoardSettingsAvailability.resolve(
|
||||
tier: tier,
|
||||
mode: git?.mode ?? .none,
|
||||
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
|
||||
)
|
||||
}
|
||||
|
||||
/// **Opening, not toggling** — unlike ⌘I. A sheet is modal to its window and carries its own
|
||||
@@ -141,7 +149,15 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
static func resolve(tier: Tier, mode: BoardGitMode) -> [BoardSettingsSection] {
|
||||
/// - Parameter isRepositoryUnreadable: whether the board's `.git` exists and will not open
|
||||
/// (`HistoryStore.isRepositoryUnreadable`). Defaulted, because it can only ever be true in mode
|
||||
/// `git` — every other mode has no repository for the probe to have failed on, and a caller
|
||||
/// that has no git state to ask is describing one of those boards.
|
||||
static func resolve(
|
||||
tier: Tier,
|
||||
mode: BoardGitMode,
|
||||
isRepositoryUnreadable: Bool = false
|
||||
) -> [BoardSettingsSection] {
|
||||
// **The free tier has no setup to host** (12-editions.md ▸ The free tier and `.git`): git is
|
||||
// the Pro subscription's, "any `.git` is inert", and 03 gives the free tier's whole git story
|
||||
// as the popover's one-line pointer. There is nothing for a sheet to be about.
|
||||
@@ -150,7 +166,19 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
|
||||
case .none:
|
||||
return [.git]
|
||||
case .git:
|
||||
return [.branch, .commitIdentity]
|
||||
// **An unreadable repository hosts no setup either** (06-history-undo.md ▸ Rules, the
|
||||
// corrupt-`.git` loud failure, ruled 2026-07-31: "the whole git surface paused …
|
||||
// Lanework leaves the repository untouched"), and it lands on repo-nested's emptiness by
|
||||
// repo-nested's own reasoning, one step further along: both sections here are *writes* to
|
||||
// a repository — a branch created in it, an identity written into its config — and there
|
||||
// is no repository the app can open to write either into. An empty sheet would be the
|
||||
// greyed-out button 06 rules out one level up, so the surface simply does not exist for
|
||||
// such a board and the popover's own prose carries the explanation
|
||||
// (`BoardGitBranchSurface.unreadableNote`).
|
||||
//
|
||||
// The mode stays `.git` throughout — this is emptiness *within* git mode, never a fall
|
||||
// to mode none, which is what would let add-git be offered against an existing `.git`.
|
||||
return isRepositoryUnreadable ? [] : [.branch, .commitIdentity]
|
||||
case .repoNested:
|
||||
// **Nothing setup-shaped can apply** (06 ▸ Rules): the board lives inside a repository
|
||||
// Lanework leaves alone, so there is no add-git (the design is insistent that the option
|
||||
@@ -192,8 +220,16 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
|
||||
/// this board.
|
||||
enum BoardSettingsAvailability {
|
||||
|
||||
static func resolve(tier: Tier, mode: BoardGitMode) -> Bool {
|
||||
!BoardSettingsSection.resolve(tier: tier, mode: mode).isEmpty
|
||||
static func resolve(
|
||||
tier: Tier,
|
||||
mode: BoardGitMode,
|
||||
isRepositoryUnreadable: Bool = false
|
||||
) -> Bool {
|
||||
!BoardSettingsSection.resolve(
|
||||
tier: tier,
|
||||
mode: mode,
|
||||
isRepositoryUnreadable: isRepositoryUnreadable
|
||||
).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user