Add the loss-row banner class and retone degraded paste

02 ratified a warning-tone class for non-failure losses — content that
didn't arrive though nothing failed: a degraded paste, folders skipped
from a Finder drop, their future kin. Loss rows take the one-shot's
dismissable-untimed lifecycle (a loss the user didn't notice is the
harm) and rank below the true failures, above commit and attachment
notices. BannerCenter grows LossBanner, postLoss, and the
skipped-folders phrasing; the degraded-paste notice moves off its
signpost onto the new class, ending its too-quiet ranking.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 07:17:27 -04:00
parent 15006ad233
commit 756e936291
6 changed files with 312 additions and 46 deletions
+23
View File
@@ -764,6 +764,29 @@ struct DegradedPasteBannerTests {
func postingNothing() {
let center = BannerCenter()
center.postDegradedPaste([])
#expect(center.losses.isEmpty)
#expect(center.signposts.isEmpty)
}
@Test("A degraded paste lands in the loss class, not the signpost class")
@MainActor
func postingLandsAsALossRow() {
// Settled 2026-07-28 (DESIGN/02-architecture.md § The banner surface, "Loss rows"): the
// degraded paste retoned from a signpost onto the new warning-tone loss class content
// that didn't arrive though nothing failed, ranking below the true failures and above the
// ambient notices rather than at the bottom of the strip.
let center = BannerCenter()
center.postDegradedPaste([.init(title: "Fix login", attachments: 3)])
#expect(center.losses.count == 1)
#expect(center.losses.first?.message == "Pasted 'Fix login' without its 3 attachments")
#expect(center.signposts.isEmpty, "the degraded paste no longer posts a signpost")
let rows = BannerCenter.rows(
lock: nil, breakage: nil, oneShots: [], losses: center.losses, suspension: nil, operations: []
)
#expect(rows.count == 1)
#expect(rows[0].tone == .warning)
#expect(rows[0].dismissID == center.losses.first?.id)
}
}