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
+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)
}