The git surfaces leave the glass — tab, trail, branch line, and the remote pair, peeled

Step 3 of strategy/01-git-excision.md: the popover strip is Info/Theme/Sync, the titlebar widget says the name alone, the card window's History section and its slot go, Board ▸ Pull/Push comes out with the RemoteCommands scaffold, and View ▸ History re-tags from the commit trail to the deferred foreign-change journal. The Sync placeholder re-annotates to the future ops-based sync service. Four git test suites leave with the surfaces they pinned (BoardGitSetup, BoardInfoPopover, BranchSwitch, GitUndo). The git engine still compiles underneath, unreferenced by UI. 2,890 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-08 10:52:57 -04:00
parent ae7be98eaa
commit 1d97a2931c
16 changed files with 119 additions and 4525 deletions
-191
View File
@@ -1,191 +0,0 @@
import Observation
import SwiftUI
// MARK: - One row
/// One **History** row: a commit that touched this card's folder (05-card-window.md History).
///
/// A value rather than the `GitCommitRecord` itself, so the view renders strings a test has already
/// checked and never formats a date in a `body`.
struct CardHistoryRow: Identifiable, Equatable, Sendable {
/// The commit's oid the identity, and nothing the row shows.
let id: String
/// The commit's subject, exactly as the message engine wrote it.
let subject: String
/// "2 days ago · Claude" the row's second line.
let attribution: String
}
// MARK: - The seam
/// What the History section shows, as a pure function of commits and a clock
/// (05-card-window.md History: "newest first semantic subject, relative date, author").
enum CardHistoryRows {
/// The rows for one card's commits, newest first which is the order the walk already answers
/// in, so nothing here re-sorts and nothing can disagree with git about what "newest" means.
nonisolated static func rows(
for commits: [GitCommitRecord],
now: Date = Date(),
locale: Locale = .autoupdatingCurrent
) -> [CardHistoryRow] {
commits.map { commit in
CardHistoryRow(
id: commit.oid,
subject: commit.subject,
attribution: attribution(of: commit, now: now, locale: locale)
)
}
}
/// "relative date · author", with the author dropped when there is none to name.
///
/// The author is the commit's, which is where origin lives (06-history-undo.md Interaction with
/// external writers) so a foreign commit reads `Lanework External` and a `modified-by` agent
/// reads its own name, with no rendering rule of this section's own. That is 05's claim that the
/// trail "reads as a story, agent and hand edits included" arriving for free.
nonisolated static func attribution(
of commit: GitCommitRecord,
now: Date = Date(),
locale: Locale = .autoupdatingCurrent
) -> String {
let when = relativeDate(commit.date, now: now, locale: locale)
let author = commit.authorName.trimmingCharacters(in: .whitespacesAndNewlines)
return author.isEmpty ? when : "\(when) · \(author)"
}
/// A relative date in the system's own words ("2 days ago"), with **"just now"** for anything
/// inside a minute.
///
/// The floor is a judgment call, recorded: `RelativeFormatStyle` renders a five-second-old commit
/// as "in 0 seconds" whenever the clock rounds the wrong way, and a trail whose newest row reads
/// as the future is worse than one that rounds down. Everything past a minute is the platform's
/// answer verbatim, localized and abbreviated to suit a 26-character-wide sidebar.
nonisolated static func relativeDate(
_ date: Date,
now: Date = Date(),
locale: Locale = .autoupdatingCurrent
) -> String {
guard now.timeIntervalSince(date) >= 60 else { return "just now" }
var style = Date.RelativeFormatStyle(presentation: .named, unitsStyle: .wide)
style.locale = locale
return date.formatted(style.locale(locale))
}
}
// MARK: - The loader
/// **One card window's commit trail** the object the sidebar renders and the host refreshes.
///
/// ### Its existence is the section's visibility rule
///
/// "The section is **absent** on boards without app-managed git (mode none, repo-nested) same
/// honesty rule as the popover's git section" (05 History). So the host builds one of these only
/// where the board's history is genuinely git-backed its `HistoryStore` in mode `git` and `nil`
/// is the whole of the absence: no placeholder, no empty header, nothing to explain.
///
/// **Per board, never per tier** (12-editions.md PIVOT 2026-08-07). 12's old tier matrix listed
/// the card History sidebar as a Pro row, so the section's absence used to have two causes at once
/// a gitless board, or a free-tier session with no git state to ask. Git is tier-independent now:
/// the one question left is whether *this board* has a repository the app manages, which is the
/// question 05 was always asking.
///
/// ### It re-reads rather than subscribes
///
/// The trail changes when a commit lands, which is exactly what `GitAutoCommitter.commitCount`
/// counts. The host re-asks on that number and on the card's own folder, so a trail refreshes after
/// every commit the app's own, an agent's the watcher committed, and a restore's without this
/// object learning what a committer is.
@MainActor
@Observable
final class CardHistory {
/// The rows, newest first. Empty until the first load answers, which is also the honest answer for
/// a card whose folder no commit has touched yet.
private(set) var rows: [CardHistoryRow] = []
/// Whether a load is in flight what keeps the section from flashing "no history yet" during the
/// first walk of a large repository.
private(set) var isLoading = false
init() {}
/// Reads the commits that touched `cardFolderName` under `boardRoot`, off the main actor.
func load(boardRoot: URL, cardFolderName: String) async {
isLoading = true
defer { isLoading = false }
let commits = await Task.detached(priority: .utility) {
GitHistoryWalk.commitsTouching(folderNamed: cardFolderName, at: boardRoot)
}.value
rows = CardHistoryRows.rows(for: commits)
}
}
// MARK: - The section
/// The sidebar's **History** section: the card's commit trail, read-only, newest first
/// (05-card-window.md History).
///
/// ### No actions, deliberately
///
/// "Rows are focusable (arrows), but carry **no actions in v1** restoring an old version stays a
/// git-client task for now; a per-row forward-restore and lane history are wishlist items,
/// deliberately." So the rows are text: selectable, copyable, and nothing else. The one restore this
/// milestone ships is board-level Z, which is a different gesture with a different target.
///
/// ### Empty says so, rather than disappearing
///
/// Contrast Details beside it, which vanishes when a card has no unknown keys. The distinction is
/// what an empty state would *imply*: an absent Details section implies nothing (most cards have no
/// unknown keys), while an absent History section on a git board would imply the board has no
/// history the exact claim 05 reserves for boards that genuinely have none. A card whose folder is
/// newer than its last commit is a real and temporary state, and one quiet line is the honest way to
/// say so.
struct CardHistorySection: View {
let history: CardHistory
private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }
var body: some View {
VStack(alignment: .leading, spacing: CardWindowMetrics.sidebarRowSpacing(bodyPointSize: pointSize)) {
CardSidebarSectionHeader(title: "History")
if history.rows.isEmpty {
Text(history.isLoading ? "Reading history…" : "No commits yet")
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
ForEach(history.rows) { row in
self.row(row)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
/// Subject over attribution `CardDetailsSection.row`'s shape, inverted: there the quiet line is
/// the key and the loud one the value; here the *subject* is what a reader scans for and the date
/// and author are the qualifier. Same two fonts, same wrap-rather-than-truncate rule, so the two
/// sections read as one column at any text size.
private func row(_ row: CardHistoryRow) -> some View {
VStack(alignment: .leading, spacing: 1) {
Text(row.subject)
.font(.callout)
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
Text(row.attribution)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement(children: .combine)
.accessibilityLabel("\(row.subject), \(row.attribution)")
}
}
+11 -38
View File
@@ -28,9 +28,7 @@ import UniformTypeIdentifiers
/// raw-source outlet swapping the pair of columns out entirely when it is active. Everything that
/// reads or writes beyond that is later work and is marked where it lands:
///
/// - the title as an editable field (commit on Return / focus loss, Escape abandons),
/// - the sidebar's History section, whose place in the stack is reserved and whose content waits on
/// a git mode to be honest about (`historySlot`).
/// - the title as an editable field (commit on Return / focus loss, Escape abandons).
///
/// The placeholders are structural rather than apologetic: the sidebar's inventory and its order are
/// settled (05 The attributes sidebar), so the shell states them and the sections fill in
@@ -96,10 +94,6 @@ struct CardWindowView: View {
/// issued in this window registers on this window's stack. It lives on the window's session so
/// the close can fold it, which is why it arrives here rather than being made here.
let undo: CardWindowUndo
/// **This card's commit trail** (05 History), or `nil` on every board with no app-managed git
/// mode none, repo-nested, unverifiable. The `nil` *is* the section's absence rule; see
/// `historySlot`. A per-board question on every tier since 12-editions.md PIVOT 2026-08-07.
let history: CardHistory?
/// The whole-window file drop (05 Attachments: "the drop surface remains the **whole
/// window**"). `nil` only where a caller has no store to import through.
let fileDrop: CardWindowDropDelegate?
@@ -284,8 +278,8 @@ struct CardWindowView: View {
/// (05 Composition) the whole line disappears when the card carries none of the three.
///
/// The "by" segment renders only with the self-reported provenance stamp present
/// (01-storage-format.md), which is the point of showing it at all: provenance made visible where
/// git history may not exist.
/// (01-storage-format.md), which is the point of showing it at all: provenance made visible with
/// no commit trail to read it from.
private var dateLine: String? {
var parts: [String] = []
if let created = card.created.value {
@@ -306,14 +300,16 @@ struct CardWindowView: View {
// MARK: - Attributes sidebar
/// The sidebar's sections, **in 05's settled order**: Attachments, Style, Details, History,
/// Actions.
/// The sidebar's sections, **in 05's settled order**: Attachments, Style, Details, Actions.
///
/// Two of the five are conditional, and both conditions are the section's own rather than a rule
/// One of the four is conditional, and the condition is the section's own rather than a rule
/// restated here: **Details** renders nothing when the card carries no unknown frontmatter keys
/// ("shown only when any exist"), and **History** is absent on boards without app-managed git.
/// Everything else in the stack is unconditional, so the composition a user learns on one card is
/// the composition they get on the next.
/// ("shown only when any exist"). Everything else in the stack is unconditional, so the
/// composition a user learns on one card is the composition they get on the next.
///
/// The History section that once sat between Details and Actions left with app-managed git
/// (strategy/01-git-excision.md, 2026-08-08); View History (`FutureCommands.swift`) is the only
/// surviving reservation of that slot, and it anticipates the foreign-change journal successor.
private var sidebar: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: bodyPointSize * 1.25) {
@@ -325,35 +321,12 @@ struct CardWindowView: View {
// keys and their order included, and `Card` has carried it since (`BoardModel`).
CardDetailsSection(rows: CardDetails.rows(of: card.document))
historySlot
CardActionsSection(store: store, cardID: card.id, cardFolder: cardFolder)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(CardWindowMetrics.gutter(bodyPointSize: bodyPointSize))
}
}
/// **The History section** between Details and Actions, 05's order (05 History: "the card's
/// commit trail, read-only newest first semantic subject, relative date, author").
///
/// **Absence is the `nil`, and it is the whole rule.** "The section is absent on boards without
/// app-managed git (mode none, repo-nested) same honesty rule as the popover's git section".
/// The host builds a `CardHistory` only where the board's history is git-backed (mode `git`), so
/// there is no placeholder here to decide about: what the slot reserves is the **position**, and
/// on every other board that position is empty.
///
/// **The tier is not one of the inputs** (12-editions.md PIVOT 2026-08-07 git left the
/// paywall, retiring 12's tier matrix row that made this a Pro surface): a git board shows its
/// trail on any tier, a gitless one shows nothing on any tier.
///
// A later card: View History, which focuses this section (11-command-nexus.md).
@ViewBuilder
private var historySlot: some View {
if let history {
CardHistorySection(history: history)
}
}
}
// MARK: - The window-wide drop