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:
2026-08-06 18:44:12 -04:00
parent 52df210284
commit 0d8ecdb78b
20 changed files with 938 additions and 42 deletions
+9
View File
@@ -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"
}
+56 -7
View File
@@ -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)
}
+5 -1
View File
@@ -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()
+42 -6
View File
@@ -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
}
}