Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
97e8b6a
fix(watch): add WKCompanionAppBundleIdentifier to Info.plist
dimakis Apr 28, 2026
6c4cd54
fix(watch): trust Tailscale certs + enable shared Keychain on iOS
dimakis Apr 28, 2026
8780d88
fix(client): prevent UI freeze from foreground state wipe + 3 related…
dimakis Apr 28, 2026
932d1df
feat(workload): WorkloadStore + WorkSignal intake API (#293)
dimakis Apr 28, 2026
5f0333b
fix(client): add runtime validation for reconnected sessions field (#…
dimakis Apr 28, 2026
46121d5
fix(apns): respect APNS_PRODUCTION env var for sandbox/production gat…
dimakis Apr 29, 2026
d1845f0
fix(ui): rename Todos to Telos, add back button, preserve scroll/filt…
dimakis Apr 29, 2026
c869a2c
feat(observability): record agent output in traces and logs (#303)
dimakis May 1, 2026
609ff49
feat(sse): event bus + session activity overview (#304)
dimakis May 2, 2026
ca92d6e
feat(sse): wire consumer hooks to SSE events — Phase 2 (#307)
dimakis May 2, 2026
6ba2115
feat(sse): health consolidation — Phase 3 (#308)
dimakis May 3, 2026
11efc96
fix(server): buffer pre-sessionId v2 events to prevent data loss on i…
dimakis May 2, 2026
02118a4
fix(server): shorten verbose comment in pre-session buffer path
dimakis May 2, 2026
c83f551
feat(session): link Telos tasks to chat sessions
dimakis May 2, 2026
88259bb
fix(session): wire telosTaskId through server middleware to EventStore
dimakis May 2, 2026
32bf884
feat(protocol): add subagent nesting types
dimakis May 3, 2026
1e186cb
feat(server): subagent detection and event emission
dimakis May 3, 2026
3f14729
feat(client): subagent state management and protocol routing
dimakis May 3, 2026
c5c9dce
feat(ui): subagent card component for nested agent visibility
dimakis May 3, 2026
473e1d5
fix(subagent): correct blockType in subagent_block_end, preserve suba…
dimakis May 3, 2026
90e0e0e
fix(connection-registry): add delivery guarantee with cursors and per…
dimakis May 3, 2026
269028a
fix(harness): address PR #310 review findings — cursor race + session…
dimakis May 3, 2026
f45135d
feat(ui): add desktop page navigation and fix New Chat button (#315)
dimakis May 6, 2026
133a197
fix(protocol,client): resolve subagent type mismatches breaking CI (#…
dimakis May 9, 2026
cd8d593
feat(ui): task board redesign with attention sorting (#314)
dimakis May 9, 2026
f0d4969
feat(ui): desktop Command Center layout (#317)
dimakis May 9, 2026
c8c136f
feat(ui): boot context pill at session start (#313)
dimakis May 9, 2026
cfe3af2
fix(harness): cascade stopTask to subagents on interrupt (#321)
dimakis May 14, 2026
11b67fb
feat(ui): inline markdown preview cards in chat (#319)
dimakis May 14, 2026
7c05145
feat(tracing): nested OTel spans for subagent hierarchy (#318)
dimakis May 14, 2026
2f7c34a
fix(client,server): prevent duplicate messages on iOS foreground reco…
dimakis May 14, 2026
d5908b3
feat(ui): cc-deck attention feed, ATB + Telos visual redesign (#311)
dimakis May 14, 2026
8ac65cd
fix(ui): markdown preview card promotion broken in ReactMarkdown v10 …
dimakis May 16, 2026
8190252
fix(client): recover SSE EventSource on browser visibilitychange (#325)
dimakis May 16, 2026
cbdccd8
fix(ios): forward APNs device token to Capacitor (#328)
dimakis May 16, 2026
f1f2654
feat(watch): relay-only networking + native dictation input
dimakis May 16, 2026
405125d
fix(watch): extract sessionId before Task to fix Sendable data race
dimakis May 16, 2026
64519db
fix(client): reduce SSE fallback delay + cache health for instant mic…
dimakis May 16, 2026
959af61
feat(watch): iOS lifecycle hooks for watch relay background/foreground
dimakis May 16, 2026
23c4e66
feat(watch): add watch auth bridge and frontend integration
dimakis May 16, 2026
676a9ab
fix(watch): use captured locals in @Sendable Task closure
dimakis May 16, 2026
01b810c
fix(watch): activate WCSession in init before auth/login
dimakis May 16, 2026
5e1c3db
chore(watch): add logging to WatchRelay activation and message handling
dimakis May 16, 2026
de652e6
feat(session): add user-initiated session close + visual status indic…
dimakis May 16, 2026
0eabc65
fix(watch): use Tailscale-trusting URLSession in YapperClient + dynam…
dimakis May 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions frontend/ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
1F97793D2FB86A7A00B87829 /* WatchAuthBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F97793C2FB86A7A00B87829 /* WatchAuthBridge.swift */; };
1F97793E2FB86A8000B87829 /* WatchRelayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F97793B2FB86A5700B87829 /* WatchRelayCoordinator.swift */; };
2C807BBEF27900A4B4C713F3 /* VoiceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E438A1E5CA844963819792C4 /* VoiceService.swift */; };
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
30271F81931105049A8D0BCA /* MitzoShared in Frameworks */ = {isa = PBXBuildFile; productRef = 2994CBB574BE2544F671F9BF /* MitzoShared */; };
Expand All @@ -20,19 +22,20 @@
51C82AC7CC9D0CD9BB2B8A2D /* MitzoWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 729F6EAA7E6281CC8E027E58 /* MitzoWatchApp.swift */; };
87D5F3E4B2B79830BDC6F3CA /* VoiceInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF9D046593F17D7DF6E6056B /* VoiceInputBar.swift */; };
91D36789BA83D98F79CA5EA1 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85EF8992F04F5159962BE4 /* ChatViewModel.swift */; };

9A8EE45055EB9BF5AB711065 /* PermissionBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB84607485C9AAA4D695F477 /* PermissionBanner.swift */; };
B87D83BB43FD8D7A331D5C16 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A60979EA195F633EE664E716 /* LoginView.swift */; };
BDF94F63DBEDEAABE3DBA4B8 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6B3AF931A2A562E89BCCA0 /* ChatView.swift */; };
E7C8020332FACEAD32C32377 /* MitzoShared in Frameworks */ = {isa = PBXBuildFile; productRef = A997DA8238CC43203EE0AD73 /* MitzoShared */; };
EB52F01B4D4DE944429DFFB7 /* SessionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3543D568BAEA16F3464C2D /* SessionListView.swift */; };
EBDB8718D00B3CAC47EFD6EC /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB2544C9BF33F98A575F8F9F /* AppState.swift */; };
F1A2B3C4D5E6F70800000001 /* ComposeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A2B3C4D5E6F70800000002 /* ComposeSheet.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
1F97793B2FB86A5700B87829 /* WatchRelayCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchRelayCoordinator.swift; sourceTree = "<group>"; };
1F97793C2FB86A7A00B87829 /* WatchAuthBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchAuthBridge.swift; sourceTree = "<group>"; };
264FB5577C8B9098BE816A36 /* MitzoWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MitzoWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };

50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand All @@ -50,6 +53,7 @@
AB84607485C9AAA4D695F477 /* PermissionBanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PermissionBanner.swift; sourceTree = "<group>"; };
CE85EF8992F04F5159962BE4 /* ChatViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
CF9D046593F17D7DF6E6056B /* VoiceInputBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VoiceInputBar.swift; sourceTree = "<group>"; };
F1A2B3C4D5E6F70800000002 /* ComposeSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComposeSheet.swift; sourceTree = "<group>"; };
E438A1E5CA844963819792C4 /* VoiceService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VoiceService.swift; sourceTree = "<group>"; };
FA1E96663E02190E82402696 /* MitzoWatch.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = MitzoWatch.entitlements; sourceTree = "<group>"; };
FB2544C9BF33F98A575F8F9F /* AppState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -77,7 +81,6 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */

504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
Expand All @@ -104,6 +107,8 @@
A1B2C3D40000000000000001 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
1F97793B2FB86A5700B87829 /* WatchRelayCoordinator.swift */,
1F97793C2FB86A7A00B87829 /* WatchAuthBridge.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
Expand All @@ -121,7 +126,6 @@
CE85EF8992F04F5159962BE4 /* ChatViewModel.swift */,
E438A1E5CA844963819792C4 /* VoiceService.swift */,
);
name = Services;
path = Services;
sourceTree = "<group>";
};
Expand All @@ -145,9 +149,9 @@
AB84607485C9AAA4D695F477 /* PermissionBanner.swift */,
CF9D046593F17D7DF6E6056B /* VoiceInputBar.swift */,
FF6B3AF931A2A562E89BCCA0 /* ChatView.swift */,
F1A2B3C4D5E6F70800000002 /* ComposeSheet.swift */,
9A3543D568BAEA16F3464C2D /* SessionListView.swift */,
);
name = Views;
path = Views;
sourceTree = "<group>";
};
Expand Down Expand Up @@ -233,7 +237,7 @@
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */,
F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "MitzoShared" */,
F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "../MitzoShared" */,
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -273,6 +277,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1F97793D2FB86A7A00B87829 /* WatchAuthBridge.swift in Sources */,
1F97793E2FB86A8000B87829 /* WatchRelayCoordinator.swift in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -289,6 +295,7 @@
9A8EE45055EB9BF5AB711065 /* PermissionBanner.swift in Sources */,
87D5F3E4B2B79830BDC6F3CA /* VoiceInputBar.swift in Sources */,
BDF94F63DBEDEAABE3DBA4B8 /* ChatView.swift in Sources */,
F1A2B3C4D5E6F70800000001 /* ComposeSheet.swift in Sources */,
EB52F01B4D4DE944429DFFB7 /* SessionListView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -565,7 +572,7 @@
isa = XCLocalSwiftPackageReference;
relativePath = "CapApp-SPM";
};
F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "MitzoShared" */ = {
F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "../MitzoShared" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../MitzoShared;
};
Expand All @@ -574,7 +581,7 @@
/* Begin XCSwiftPackageProductDependency section */
2994CBB574BE2544F671F9BF /* MitzoShared */ = {
isa = XCSwiftPackageProductDependency;
package = F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "MitzoShared" */;
package = F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "../MitzoShared" */;
productName = MitzoShared;
};
4D22ABE82AF431CB00220026 /* CapApp-SPM */ = {
Expand All @@ -584,7 +591,7 @@
};
A997DA8238CC43203EE0AD73 /* MitzoShared */ = {
isa = XCSwiftPackageProductDependency;
package = F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "MitzoShared" */;
package = F0C38DA10F17B4BECD2F4E09 /* XCLocalSwiftPackageReference "../MitzoShared" */;
productName = MitzoShared;
};
/* End XCSwiftPackageProductDependency section */
Expand Down
4 changes: 4 additions & 0 deletions frontend/ios/App/App/App.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.mitzo.app</string>
</array>
</dict>
</plist>
16 changes: 12 additions & 4 deletions frontend/ios/App/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import Capacitor
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
private let watchRelay = WatchRelayCoordinator()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
watchRelay.start()
return true
}

Expand All @@ -17,12 +18,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
watchRelay.suspend()
}

func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
watchRelay.reconnect()
}

func applicationDidBecomeActive(_ application: UIApplication) {
Expand All @@ -46,4 +46,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}

}
4 changes: 3 additions & 1 deletion frontend/ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CAPACITOR_DEBUG</key>
<key>AppIdentifierPrefix</key>
<string>$(AppIdentifierPrefix)</string>
<key>CAPACITOR_DEBUG</key>
<string>$(CAPACITOR_DEBUG)</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
Expand Down
45 changes: 45 additions & 0 deletions frontend/ios/App/App/WatchAuthBridge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Capacitor plugin that bridges web auth tokens into the native shared Keychain.
// When the web app logs in, it calls WatchAuthBridge.saveToken() so the
// watch can read the JWT from the shared Keychain access group.

import Capacitor
import MitzoShared

@objc(WatchAuthBridge)
public class WatchAuthBridge: CAPPlugin, CAPBridgedPlugin {
public let identifier = "WatchAuthBridge"
public let jsName = "WatchAuthBridge"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "saveToken", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clearToken", returnType: CAPPluginReturnPromise),
]

private let authManager = AuthManager()

@objc func saveToken(_ call: CAPPluginCall) {
guard let token = call.getString("token") else {
call.reject("Missing token")
return
}

Task {
do {
try await authManager.saveToken(token)
call.resolve()
} catch {
call.reject("Failed to save token: \(error.localizedDescription)")
}
}
}

@objc func clearToken(_ call: CAPPluginCall) {
Task {
do {
try await authManager.clearToken()
call.resolve()
} catch {
call.reject("Failed to clear token: \(error.localizedDescription)")
}
}
}
}
69 changes: 69 additions & 0 deletions frontend/ios/App/App/WatchRelayCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Coordinates native WS connection + WatchRelay for Apple Watch communication.
// The Capacitor web layer has its own WS; this is a second native connection
// used exclusively by WatchRelayHost to bridge watch ↔ server messages.

import UIKit
import MitzoShared

final class WatchRelayCoordinator: @unchecked Sendable {
private let authManager = AuthManager()
private let watchRelay: WatchRelayHost
private var wsClient: MitzoWSClient?
private let lock = NSLock()

private var serverURL: URL {
let stored = UserDefaults.standard.string(forKey: "mitzo_server_url")
return URL(string: stored ?? "https://dimakis-mac.tail:3100")!
}

init() {
watchRelay = WatchRelayHost(authManager: authManager)
// Activate WCSession immediately so watch can reach us even before login
watchRelay.activate(wsClient: nil, apiClient: nil)
}

func start() {
Task { await connect() }
}

func reconnect() {
Task { await connect() }
}

func suspend() {
Task {
let client: MitzoWSClient? = lock.withLock { wsClient }
if let client {
let sessions = await client.getSuspendSessions()
if !sessions.isEmpty {
try? await client.suspend(sessions: sessions)
}
}
}
}

private func connect() async {
guard let token = try? await authManager.getToken() else { return }

var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)!
components.scheme = components.scheme == "https" ? "wss" : "ws"
components.path = "/ws/chat"
components.queryItems = [URLQueryItem(name: "token", value: token)]

guard let wsURL = components.url else { return }

let client = MitzoWSClient(url: wsURL)
lock.withLock { wsClient = client }

let api = MitzoAPIClient(baseURL: serverURL, authManager: authManager)
// Update relay with authenticated clients (WCSession already activated in init)
watchRelay.activate(wsClient: client, apiClient: api)

let relay = watchRelay
await client.connect { event in
if case .message(let msg) = event {
relay.forwardToWatch(msg)
}
}
}
}
74 changes: 74 additions & 0 deletions frontend/ios/App/WatchRelayCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Coordinates native WS connection + WatchRelay for Apple Watch communication.
// The Capacitor web layer has its own WS; this is a second native connection
// used exclusively by WatchRelayHost to bridge watch ↔ server messages.

import UIKit
import MitzoShared

final class WatchRelayCoordinator: @unchecked Sendable {
private let authManager = AuthManager()
private let watchRelay: WatchRelayHost
private var wsClient: MitzoWSClient?
private let lock = NSLock()

private var serverURL: URL {
let stored = UserDefaults.standard.string(forKey: "mitzo_server_url")
return URL(string: stored ?? "https://dimakis-mac.tail:3100")!
}

init() {
watchRelay = WatchRelayHost(authManager: authManager)
}

func start() {
// Activate WCSession unconditionally so the watch relay works
// even before auth. This lets the auth_token relay bootstrap
// the token, and list_sessions/get_messages return proper errors
// instead of silently hanging with no delegate.
let apiClient = MitzoAPIClient(baseURL: serverURL, authManager: authManager)
watchRelay.activate(wsClient: nil, apiClient: apiClient)

Task { await connectWS() }
}

func reconnect() {
Task { await connectWS() }
}

func suspend() {
Task {
let client: MitzoWSClient? = lock.withLock { wsClient }
if let client {
let sessions = await client.getSuspendSessions()
if !sessions.isEmpty {
try? await client.suspend(sessions: sessions)
}
}
}
}

/// Connect the native WS (for forwarding server events to watch).
/// WCSession is already active from start() — this only adds WS.
private func connectWS() async {
guard let token = try? await authManager.getToken() else { return }

var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)!
components.scheme = components.scheme == "https" ? "wss" : "ws"
components.path = "/ws/chat"
components.queryItems = [URLQueryItem(name: "token", value: token)]

guard let wsURL = components.url else { return }

let client = MitzoWSClient(url: wsURL)
let apiClient = MitzoAPIClient(baseURL: serverURL, authManager: authManager)
lock.withLock { wsClient = client }
watchRelay.activate(wsClient: client, apiClient: apiClient)

let relay = watchRelay
await client.connect { event in
if case .message(let msg) = event {
relay.forwardToWatch(msg)
}
}
}
}
Loading