83 lines
4.1 KiB
Swift
83 lines
4.1 KiB
Swift
import Foundation
|
|
import NucleicProtocol
|
|
|
|
/// One row of the transcript as *displayed*: a projected item, or a run of adjacent file-lock
|
|
/// notes gathered into a single row.
|
|
///
|
|
/// This layer exists for the Orchestra supervisor case. `AppStore` emits waiting/acquired/released
|
|
/// notes for every worker's edit attributed to the supervisor (the lock *holder*), but a supervisor
|
|
/// edits nothing itself, so those notes have no edit card for the projection to fold them onto and
|
|
/// each one arrives as its own standalone `.note`. A fan-out over a few dozen files therefore
|
|
/// renders as a few dozen near-identical rows — each with the transcript's full 14pt inter-row gap
|
|
/// — burying the conversation between them.
|
|
///
|
|
/// Gathering them here rather than in `TranscriptProjection` is deliberate: the projection is the
|
|
/// canonical, cross-platform fold, bound by the incremental projector's
|
|
/// `fold(prefix) ++ fold(tail) == fold(whole)` invariant and by the exhaustive `TranscriptItem.Kind`
|
|
/// switch that feeds the Windows renderer through `TranscriptProjectionABI`. Grouping purely for
|
|
/// readability belongs on the far side of both. Pure model code (Foundation + NucleicProtocol, no
|
|
/// SwiftUI) so it builds into the host-testable `NucleicRemoteProjection` target.
|
|
struct TranscriptDisplayRow: Identifiable, Equatable {
|
|
enum Kind: Equatable {
|
|
/// A projected row, passed through untouched.
|
|
case item(TranscriptItem)
|
|
/// Adjacent lock-lifecycle notes shown as one row — literal lines while the trail is
|
|
/// short, a collapsed `LockDigest` once it isn't. Kept in stream order, so the folded
|
|
/// view sees exactly what the individual rows would have said.
|
|
case lockRun([NoteLock])
|
|
}
|
|
|
|
let id: String
|
|
let seq: UInt64
|
|
let kind: Kind
|
|
}
|
|
|
|
enum TranscriptDisplay {
|
|
|
|
/// Gathers each maximal run of adjacent, path-carrying lock notes into one row, passing
|
|
/// everything else through unchanged.
|
|
///
|
|
/// Runs of one are gathered too: a lone lock moment then renders through the same
|
|
/// `LockLineRow` the folded card lines use, so every lock in the transcript reads the same way
|
|
/// whether it landed on an edit card or on its own.
|
|
///
|
|
/// One deliberate exclusion — a lock note carrying no structured `NoteLock` (the re-ground and
|
|
/// update-conflict notes) is prose the user may have to act on. It never joins a run, *and* it
|
|
/// breaks the run around it rather than being stepped over to splice the two halves together.
|
|
static func rows(_ items: [TranscriptItem]) -> [TranscriptDisplayRow] {
|
|
var rows: [TranscriptDisplayRow] = []
|
|
rows.reserveCapacity(items.count)
|
|
var run: [TranscriptItem] = []
|
|
|
|
func flushRun() {
|
|
defer { run.removeAll(keepingCapacity: true) }
|
|
guard let first = run.first else { return }
|
|
// Keyed on the run's first row, so a growing run extends the same row instead of
|
|
// being replaced by a differently-named one — which would drop the reader's expansion
|
|
// and jog the scroll anchor as a live fan-out streams its locks in.
|
|
rows.append(.init(id: "lockrun-\(first.id)", seq: first.seq,
|
|
kind: .lockRun(run.compactMap(structuredLock))))
|
|
}
|
|
|
|
for item in items {
|
|
if structuredLock(item) != nil {
|
|
run.append(item)
|
|
} else {
|
|
flushRun()
|
|
rows.append(.init(id: item.id, seq: item.seq, kind: .item(item)))
|
|
}
|
|
}
|
|
flushRun()
|
|
return rows
|
|
}
|
|
|
|
/// The structured lock a note carries, or nil for anything that isn't a foldable lock moment.
|
|
/// A lock note with no paths yields nothing to gather, so it stays an ordinary row; so does a
|
|
/// process-stall alert, which is a live control surface (Kill / Keep waiting), not a log line.
|
|
private static func structuredLock(_ item: TranscriptItem) -> NoteLock? {
|
|
guard case .note(_, _, let lockEvent, let lock, let processStall) = item.kind,
|
|
lockEvent, processStall == nil, let lock, !lock.paths.isEmpty else { return nil }
|
|
return lock
|
|
}
|
|
}
|