diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index ce8e335..10cf465 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -867,12 +867,54 @@ final class RemoteStore: ObservableObject { // MARK: - Plumbing - /// Route an intent to the active host's connection (mesh P3). Every current intent — subscribe, - /// send-input, approvals, favorite/archive/delete, start-chat — targets a session or project on - /// the Mac whose list is on screen, i.e. the active one. + /// Route an intent to the Mac that owns its target (mesh P3). Sessions/projects/to-dos are shown + /// merged across every connected Mac, so an intent goes to the connection that owns the session, + /// project, or to-do it names — not to a single "active" host. Host-agnostic pulls (list + /// sessions/dashboard) broadcast to every live Mac; a project-less to-do capture (no owner) goes + /// to the first live Mac. Demo has no connections and mutates the seeded aggregate directly. private func send(_ msg: ClientMsg) { if demoMode { demoHandle(msg); return } - activeConnection?.send(msg) + switch msg { + // Session-owning intents → the Mac that has this session. + case .subscribe(let s): + connection(owningSession: s.sessionID)?.send(msg) + case .unsubscribe(let id), .interrupt(let id), .deleteSession(let id), .discard(let id), + .integrate(let id, _), .renameSession(let id, _), .setFavorite(let id, _), + .setArchived(let id, _), .setSessionModel(let id, _), .setSessionEffort(let id, _), + .setSessionAuto(let id, _), .setSessionAutoShip(let id, _), .setSessionShipBranch(let id, _), + .sendInput(let id, _), .cancelQueuedMessage(let id, _), .fetchDiff(let id): + connection(owningSession: id)?.send(msg) + + // Project-owning intents → the Mac that has this project. + case .startChat(let req): + connection(owningProject: req.projectID)?.send(msg) + case .dispatchTodo(_, let projectID): + connection(owningProject: projectID)?.send(msg) + + // To-do-owning intents → the Mac that has this to-do. + case .setTodoStatus(let id, _), .deleteTodo(let id): + connection(owningTodo: id)?.send(msg) + + // A capture with a project goes to that Mac; a project-less one has no owner → first live. + case .captureTodo(let req): + if let pid = req.projectID, let conn = connection(owningProject: pid) { conn.send(msg) } + else { firstLiveConnection?.send(msg) } + + // Approvals carry no sessionID — `respond(_:_:)` routes by the approval's session directly; + // a stray one here (or a notification-driven decision) broadcasts and the owner resolves it. + case .approvalRespond: + broadcastLive(msg) + + // Host-agnostic pulls → every live Mac. + case .listSessions, .listDashboard: + broadcastLive(msg) + + // Never originated from here (connection-internal, or handled by dedicated loops). + case .hello, .ping, .listPeers, .addressUpdate, .meshRoster, + .registerLiveActivity, .endLiveActivity, + .transferOffer, .transferChunk, .transferCommit, .transferCancel, .fetchTranscript: + break + } } // MARK: - Live Activity push registration (UX_IOS §5.3)