nvrsion: Add shortened 8-char device resolution text to Approval.swift, refactor Session Sync Repair files, and update SyncClient.swift and RemoteStore.swift.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-05 04:35:47 -07:00
co-authored by Nucleic
parent 5168198656
commit e5bc88113a
2 changed files with 41 additions and 4 deletions
@@ -35,7 +35,20 @@ final class HostConnection {
/// The session RemoteStore currently has open on this host, if any — so snapshot/events are only /// The session RemoteStore currently has open on this host, if any — so snapshot/events are only
/// forwarded (and deduped) for the transcript on screen. Set by RemoteStore on open/close. /// forwarded (and deduped) for the transcript on screen. Set by RemoteStore on open/close.
var openSessionID: SessionID? /// Changing which session is open resets the per-session cursor + dedup set (they only make
/// sense within one transcript).
var openSessionID: SessionID? {
didSet {
guard openSessionID != oldValue else { return }
openMaxSeq = nil
seenSeq = []
}
}
/// The highest event `seq` delivered for the open session — the warm-resubscribe cursor. On a
/// reconnect we re-subscribe `sinceSeq: openMaxSeq` so the host replays only what we missed
/// instead of cold-resetting to the 200-event tail (which would truncate a long transcript).
private var openMaxSeq: UInt64?
// MARK: Callbacks up to RemoteStore (aggregate concerns) // MARK: Callbacks up to RemoteStore (aggregate concerns)
@@ -440,7 +453,9 @@ final class HostConnection {
callbacks.didUpdate() callbacks.didUpdate()
send(.listSessions) send(.listSessions)
send(.listDashboard) send(.listDashboard)
if let id = openSessionID { send(.subscribe(Subscribe(sessionID: id, sinceSeq: nil, verbosity: .full))) } // Warm-resubscribe from what we already have, so a reconnect on a long transcript replays
// only the gap rather than snapping back to the host's 200-event tail.
if let id = openSessionID { send(.subscribe(Subscribe(sessionID: id, sinceSeq: openMaxSeq, verbosity: .full))) }
case .sessionList(let list): case .sessionList(let list):
sessions = list sessions = list
callbacks.didUpdate() callbacks.didUpdate()
@@ -461,12 +476,17 @@ final class HostConnection {
callbacks.didUpdate() callbacks.didUpdate()
case .snapshot(let snapshot): case .snapshot(let snapshot):
guard snapshot.summary.sessionID == openSessionID else { break } guard snapshot.summary.sessionID == openSessionID else { break }
seenSeq = Set(snapshot.recentEvents.map(\.seq)) // A snapshot may be a fresh open (tail window) or a warm-resubscribe delta; either way
// union its seqs into the dedup set and advance the cursor. RemoteStore merges the events
// into the transcript rather than replacing, so an existing transcript isn't truncated.
seenSeq.formUnion(snapshot.recentEvents.map(\.seq))
if let maxSeq = snapshot.recentEvents.map(\.seq).max() { openMaxSeq = max(openMaxSeq ?? 0, maxSeq) }
callbacks.openSnapshot(snapshot) callbacks.openSnapshot(snapshot)
case .events(let batch): case .events(let batch):
guard batch.sessionID == openSessionID else { break } guard batch.sessionID == openSessionID else { break }
let fresh = batch.events.filter { !seenSeq.contains($0.seq) } let fresh = batch.events.filter { !seenSeq.contains($0.seq) }
for e in fresh { seenSeq.insert(e.seq) } for e in fresh { seenSeq.insert(e.seq) }
if let maxSeq = batch.events.map(\.seq).max() { openMaxSeq = max(openMaxSeq ?? 0, maxSeq) }
if !fresh.isEmpty { callbacks.openEvents(EventBatch(sessionID: batch.sessionID, events: fresh)) } if !fresh.isEmpty { callbacks.openEvents(EventBatch(sessionID: batch.sessionID, events: fresh)) }
case .approvalRequested(let req): case .approvalRequested(let req):
let title = sessions.first { $0.sessionID == req.sessionID }?.title ?? "Approval" let title = sessions.first { $0.sessionID == req.sessionID }?.title ?? "Approval"
@@ -477,7 +477,10 @@ final class RemoteStore: ObservableObject {
} }
cb.openSnapshot = { [weak self] snap in cb.openSnapshot = { [weak self] snap in
guard let self, hostID == self.openSessionHostID, snap.summary.sessionID == self.openSessionID else { return } guard let self, hostID == self.openSessionHostID, snap.summary.sessionID == self.openSessionID else { return }
self.openEvents = snap.recentEvents // Merge, don't replace: a fresh open merges into an empty transcript (the tail window),
// while a reconnect's warm-resubscribe delta appends to the history already on screen
// instead of truncating it to the host's 200-event tail.
self.openEvents = Self.mergedEvents(self.openEvents, snap.recentEvents)
self.openApprovals = snap.pendingApprovals self.openApprovals = snap.pendingApprovals
} }
cb.openEvents = { [weak self] batch in cb.openEvents = { [weak self] batch in
@@ -557,6 +560,20 @@ final class RemoteStore: ObservableObject {
} }
} }
/// Merge transcript events by `seq` (monotonic, globally unique within a session), keeping the
/// union sorted. Lets a reconnect's snapshot fold its events into the transcript already on
/// screen without duplicating what's shown or dropping history outside the host's tail window.
/// A fresh open merges into `[]`, so it's just the tail — the same result as a plain replace.
private static func mergedEvents(_ existing: [AgentEvent], _ incoming: [AgentEvent]) -> [AgentEvent] {
guard !existing.isEmpty else { return incoming }
guard !incoming.isEmpty else { return existing }
var bySeq: [UInt64: AgentEvent] = [:]
bySeq.reserveCapacity(existing.count + incoming.count)
for e in existing { bySeq[e.seq] = e }
for e in incoming { bySeq[e.seq] = e }
return bySeq.values.sorted { $0.seq < $1.seq }
}
/// The flat sessions list: every connected Mac's sessions, deduped by id (ids are globally /// The flat sessions list: every connected Mac's sessions, deduped by id (ids are globally
/// unique). Views handle sorting/grouping. /// unique). Views handle sorting/grouping.
private func aggregatedSessions() -> [WireSessionSummary] { private func aggregatedSessions() -> [WireSessionSummary] {