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:
@@ -35,7 +35,20 @@ final class HostConnection {
|
||||
|
||||
/// 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.
|
||||
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)
|
||||
|
||||
@@ -440,7 +453,9 @@ final class HostConnection {
|
||||
callbacks.didUpdate()
|
||||
send(.listSessions)
|
||||
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):
|
||||
sessions = list
|
||||
callbacks.didUpdate()
|
||||
@@ -461,12 +476,17 @@ final class HostConnection {
|
||||
callbacks.didUpdate()
|
||||
case .snapshot(let snapshot):
|
||||
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)
|
||||
case .events(let batch):
|
||||
guard batch.sessionID == openSessionID else { break }
|
||||
let fresh = batch.events.filter { !seenSeq.contains($0.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)) }
|
||||
case .approvalRequested(let req):
|
||||
let title = sessions.first { $0.sessionID == req.sessionID }?.title ?? "Approval"
|
||||
|
||||
@@ -477,7 +477,10 @@ final class RemoteStore: ObservableObject {
|
||||
}
|
||||
cb.openSnapshot = { [weak self] snap in
|
||||
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
|
||||
}
|
||||
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
|
||||
/// unique). Views handle sorting/grouping.
|
||||
private func aggregatedSessions() -> [WireSessionSummary] {
|
||||
|
||||
Reference in New Issue
Block a user