Sessions Integration Feature

Nucleic-Session: FC94353F-9F0D-4BCC-AD7C-0FB14E38C2E5
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-05 01:53:43 -07:00
co-authored by Nucleic
parent 6316e7d6b5
commit a0dd6f6a7b
@@ -527,32 +527,66 @@ final class RemoteStore: ObservableObject {
return cb
}
/// Mirror the active connection's projection onto the flat @Published state the UI binds to.
private func mirrorActive() {
guard let c = activeConnection else { return }
connectivity = c.connectivity
hostName = c.hostName
sessions = c.sessions
capabilities = c.capabilities
grantedScope = c.grantedScope
modelCatalog = c.modelCatalog
dashboard = c.dashboard
meshPeers = c.meshPeers
/// Re-merge every connected Mac's projection into the flat @Published state the UI binds to
/// (mesh P3): all hosts' sessions and dashboards shown together, with the host-specific values
/// (host name, capabilities, catalog, connectivity, scope) following the open session's Mac —
/// or a representative one. No-op in demo, which seeds the aggregate directly.
private func rebuildAggregate() {
guard !demoMode else { return }
sessions = aggregatedSessions()
dashboard = DashboardSnapshot.merged(connections.values.map(\.dashboard))
meshPeers = connections.values.flatMap(\.meshPeers)
let ctx = contextConnection
hostName = ctx?.hostName ?? ""
capabilities = ctx?.capabilities
?? WireCapabilities(canModifyToolInput: false, allowAlwaysScopes: [])
modelCatalog = ctx?.modelCatalog ?? .empty
if let id = openSessionHostID, let conn = connections[id] {
// While a transcript is open, the composer/controls act on *that* Mac — its connectivity
// gates send and its scope drives the control affordances.
connectivity = conn.connectivity
grantedScope = conn.grantedScope
} else {
connectivity = aggregateConnectivity()
// Optimistic new-chat gating: enabled if *any* Mac grants control (the owning Mac still
// enforces scope when the intent lands there).
grantedScope = connections.values
.filter { $0.connectivity.isLive }.map(\.grantedScope).max() ?? .approve
}
}
/// Every connected Mac's live (non-archived) sessions — the aggregate the app-icon badge and
/// Live Activity summarize across hosts.
private var allLiveSessions: [WireSessionSummary] {
if demoMode { return demoHosts.flatMap(\.sessions).filter { !$0.archived } }
return connections.values.flatMap(\.sessions).filter { !$0.archived }
/// The flat sessions list: every connected Mac's sessions, deduped by id (ids are globally
/// unique). Views handle sorting/grouping.
private func aggregatedSessions() -> [WireSessionSummary] {
var seen = Set<SessionID>()
var result: [WireSessionSummary] = []
for conn in connections.values {
for summary in conn.sessions where seen.insert(summary.sessionID).inserted {
result.append(summary)
}
}
return result
}
/// One connectivity value for the whole app when no transcript is open (the chip + global
/// gating): connected if any Mac is live, else the most-informative in-progress/offline state.
private func aggregateConnectivity() -> Connectivity {
let all = connections.values.map(\.connectivity)
guard !all.isEmpty else { return .unpaired }
if let live = all.first(where: { $0.isLive }) { return live }
if all.contains(where: { if case .connecting = $0 { return true } else { return false } }) { return .connecting }
if all.contains(where: { if case .reconnecting = $0 { return true } else { return false } }) { return .reconnecting }
if all.contains(.hostOffline) { return .hostOffline }
return all.first ?? .unpaired
}
/// Refresh cross-host aggregates: the app-icon badge (needs-you across *all* Macs) + the Live
/// Activity summary.
/// Activity summary. `sessions`/`liveSessions` are already the merged aggregate.
private func refreshAggregate() {
let needs = allLiveSessions.filter { $0.status.needsYou($0.disposition) }.count
NotificationRouter.shared.updateBadge(needs)
LiveActivityManager.shared.sync(hostName: hostName, sessions: allLiveSessions)
NotificationRouter.shared.updateBadge(needsYouCount)
LiveActivityManager.shared.sync(hostName: hostName, sessions: liveSessions)
}
/// Tear down every live connection (leaving the paired registry intact) — for entering demo.