bug: fixing macos app gateway hang
This commit is contained in:
parent
961b4adc1c
commit
ed14efe470
@ -7,44 +7,78 @@ final class ConnectionModeCoordinator {
|
|||||||
|
|
||||||
private let logger = Logger(subsystem: "com.clawdbot", category: "connection")
|
private let logger = Logger(subsystem: "com.clawdbot", category: "connection")
|
||||||
private var lastMode: AppState.ConnectionMode?
|
private var lastMode: AppState.ConnectionMode?
|
||||||
|
private var applyTask: Task<Void, Never>?
|
||||||
|
|
||||||
/// Apply the requested connection mode by starting/stopping local gateway,
|
/// Apply the requested connection mode by starting/stopping local gateway,
|
||||||
/// managing the control-channel SSH tunnel, and cleaning up chat windows/panels.
|
/// managing the control-channel SSH tunnel, and cleaning up chat windows/panels.
|
||||||
func apply(mode: AppState.ConnectionMode, paused: Bool) async {
|
/// This spawns a background task to avoid blocking the UI during gateway startup.
|
||||||
if let lastMode = self.lastMode, lastMode != mode {
|
/// Cancels any in-flight mode transition to prevent concurrent state changes.
|
||||||
GatewayProcessManager.shared.clearLastFailure()
|
func apply(mode: AppState.ConnectionMode, paused: Bool) {
|
||||||
NodesStore.shared.lastError = nil
|
// Cancel previous mode transition if still running
|
||||||
|
applyTask?.cancel()
|
||||||
|
|
||||||
|
applyTask = Task.detached(priority: .userInitiated) { [weak self] in
|
||||||
|
await self?.applyAsync(mode: mode, paused: paused)
|
||||||
}
|
}
|
||||||
self.lastMode = mode
|
}
|
||||||
|
|
||||||
|
private func applyAsync(mode: AppState.ConnectionMode, paused: Bool) async {
|
||||||
|
// Check for cancellation before starting work
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
let lastMode = await MainActor.run { self.lastMode }
|
||||||
|
if let lastMode, lastMode != mode {
|
||||||
|
await MainActor.run {
|
||||||
|
GatewayProcessManager.shared.clearLastFailure()
|
||||||
|
NodesStore.shared.lastError = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await MainActor.run { self.lastMode = mode }
|
||||||
|
|
||||||
|
// Check for cancellation before mode-specific logic
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case .unconfigured:
|
case .unconfigured:
|
||||||
_ = await NodeServiceManager.stop()
|
_ = await NodeServiceManager.stop()
|
||||||
NodesStore.shared.lastError = nil
|
guard !Task.isCancelled else { return }
|
||||||
|
await MainActor.run { NodesStore.shared.lastError = nil }
|
||||||
await RemoteTunnelManager.shared.stopAll()
|
await RemoteTunnelManager.shared.stopAll()
|
||||||
WebChatManager.shared.resetTunnels()
|
guard !Task.isCancelled else { return }
|
||||||
GatewayProcessManager.shared.stop()
|
await MainActor.run {
|
||||||
|
WebChatManager.shared.resetTunnels()
|
||||||
|
GatewayProcessManager.shared.stop()
|
||||||
|
}
|
||||||
await GatewayConnection.shared.shutdown()
|
await GatewayConnection.shared.shutdown()
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
await ControlChannel.shared.disconnect()
|
await ControlChannel.shared.disconnect()
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
Task.detached { await PortGuardian.shared.sweep(mode: .unconfigured) }
|
Task.detached { await PortGuardian.shared.sweep(mode: .unconfigured) }
|
||||||
|
|
||||||
case .local:
|
case .local:
|
||||||
_ = await NodeServiceManager.stop()
|
_ = await NodeServiceManager.stop()
|
||||||
NodesStore.shared.lastError = nil
|
guard !Task.isCancelled else { return }
|
||||||
|
await MainActor.run { NodesStore.shared.lastError = nil }
|
||||||
await RemoteTunnelManager.shared.stopAll()
|
await RemoteTunnelManager.shared.stopAll()
|
||||||
WebChatManager.shared.resetTunnels()
|
guard !Task.isCancelled else { return }
|
||||||
|
await MainActor.run { WebChatManager.shared.resetTunnels() }
|
||||||
let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused)
|
let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused)
|
||||||
if shouldStart {
|
if shouldStart {
|
||||||
GatewayProcessManager.shared.setActive(true)
|
guard !Task.isCancelled else { return }
|
||||||
|
await MainActor.run { GatewayProcessManager.shared.setActive(true) }
|
||||||
if GatewayAutostartPolicy.shouldEnsureLaunchAgent(
|
if GatewayAutostartPolicy.shouldEnsureLaunchAgent(
|
||||||
mode: .local,
|
mode: .local,
|
||||||
paused: paused)
|
paused: paused)
|
||||||
{
|
{
|
||||||
Task { await GatewayProcessManager.shared.ensureLaunchAgentEnabledIfNeeded() }
|
Task { await GatewayProcessManager.shared.ensureLaunchAgentEnabledIfNeeded() }
|
||||||
}
|
}
|
||||||
_ = await GatewayProcessManager.shared.waitForGatewayReady()
|
// Note: GatewayProcessManager.startIfNeeded() includes its own readiness polling
|
||||||
|
// via enableLaunchdGateway() (which waits up to 6 seconds for the gateway to accept
|
||||||
|
// connections), so no additional wait is needed here.
|
||||||
} else {
|
} else {
|
||||||
GatewayProcessManager.shared.stop()
|
await MainActor.run { GatewayProcessManager.shared.stop() }
|
||||||
}
|
}
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
do {
|
do {
|
||||||
try await ControlChannel.shared.configure(mode: .local)
|
try await ControlChannel.shared.configure(mode: .local)
|
||||||
} catch {
|
} catch {
|
||||||
@ -52,19 +86,24 @@ final class ConnectionModeCoordinator {
|
|||||||
self.logger.error(
|
self.logger.error(
|
||||||
"control channel local configure failed: \(error.localizedDescription, privacy: .public)")
|
"control channel local configure failed: \(error.localizedDescription, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
Task.detached { await PortGuardian.shared.sweep(mode: .local) }
|
Task.detached { await PortGuardian.shared.sweep(mode: .local) }
|
||||||
|
|
||||||
case .remote:
|
case .remote:
|
||||||
// Never run a local gateway in remote mode.
|
// Never run a local gateway in remote mode.
|
||||||
GatewayProcessManager.shared.stop()
|
await MainActor.run { GatewayProcessManager.shared.stop() }
|
||||||
WebChatManager.shared.resetTunnels()
|
guard !Task.isCancelled else { return }
|
||||||
|
await MainActor.run { WebChatManager.shared.resetTunnels() }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
NodesStore.shared.lastError = nil
|
await MainActor.run { NodesStore.shared.lastError = nil }
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
if let error = await NodeServiceManager.start() {
|
if let error = await NodeServiceManager.start() {
|
||||||
NodesStore.shared.lastError = "Node service start failed: \(error)"
|
await MainActor.run { NodesStore.shared.lastError = "Node service start failed: \(error)" }
|
||||||
}
|
}
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
_ = try await GatewayEndpointStore.shared.ensureRemoteControlTunnel()
|
_ = try await GatewayEndpointStore.shared.ensureRemoteControlTunnel()
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
let settings = CommandResolver.connectionSettings()
|
let settings = CommandResolver.connectionSettings()
|
||||||
try await ControlChannel.shared.configure(mode: .remote(
|
try await ControlChannel.shared.configure(mode: .remote(
|
||||||
target: settings.target,
|
target: settings.target,
|
||||||
@ -72,6 +111,7 @@ final class ConnectionModeCoordinator {
|
|||||||
} catch {
|
} catch {
|
||||||
self.logger.error("remote tunnel/configure failed: \(error.localizedDescription, privacy: .public)")
|
self.logger.error("remote tunnel/configure failed: \(error.localizedDescription, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
Task.detached { await PortGuardian.shared.sweep(mode: .remote) }
|
Task.detached { await PortGuardian.shared.sweep(mode: .remote) }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,10 +138,18 @@ actor GatewayConnection {
|
|||||||
let mode = await MainActor.run { AppStateStore.shared.connectionMode }
|
let mode = await MainActor.run { AppStateStore.shared.connectionMode }
|
||||||
switch mode {
|
switch mode {
|
||||||
case .local:
|
case .local:
|
||||||
|
// Trigger gateway start. This suspends (non-blocking) until the MainActor schedules it.
|
||||||
await MainActor.run { GatewayProcessManager.shared.setActive(true) }
|
await MainActor.run { GatewayProcessManager.shared.setActive(true) }
|
||||||
|
|
||||||
var lastError: Error = error
|
var lastError: Error = error
|
||||||
for delayMs in [150, 400, 900] {
|
// Short retry delays (150ms total) for Canvas responsiveness. Gateway startup happens
|
||||||
|
// asynchronously in the background, so these fast retries typically succeed on the
|
||||||
|
// 2nd or 3rd attempt once the gateway accepts connections.
|
||||||
|
//
|
||||||
|
// Tradeoff: Cold-start scenarios (first launch, after sleep) may take 2-3 seconds,
|
||||||
|
// but Canvas operations prioritize fast failure feedback over waiting. If retries
|
||||||
|
// fail, the user sees an error immediately rather than a frozen UI.
|
||||||
|
for delayMs in [50, 100] {
|
||||||
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
||||||
do {
|
do {
|
||||||
return try await client.request(method: method, params: params, timeoutMs: timeoutMs)
|
return try await client.request(method: method, params: params, timeoutMs: timeoutMs)
|
||||||
@ -155,7 +163,9 @@ actor GatewayConnection {
|
|||||||
let fallback = await GatewayEndpointStore.shared.maybeFallbackToTailnet(from: cfg.url)
|
let fallback = await GatewayEndpointStore.shared.maybeFallbackToTailnet(from: cfg.url)
|
||||||
{
|
{
|
||||||
await self.configure(url: fallback.url, token: fallback.token, password: fallback.password)
|
await self.configure(url: fallback.url, token: fallback.token, password: fallback.password)
|
||||||
for delayMs in [150, 400, 900] {
|
// Fast retries for Tailnet fallback (150ms total). Tailnet connections should be
|
||||||
|
// quick since the gateway is already running; short delays provide fast feedback.
|
||||||
|
for delayMs in [50, 100] {
|
||||||
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
||||||
do {
|
do {
|
||||||
guard let client = self.client else {
|
guard let client = self.client else {
|
||||||
@ -184,7 +194,9 @@ actor GatewayConnection {
|
|||||||
lastError = error
|
lastError = error
|
||||||
}
|
}
|
||||||
|
|
||||||
for delayMs in [150, 400, 900] {
|
// Fast retries for remote tunnel recovery (150ms total). SSH tunnel setup is relatively
|
||||||
|
// quick; short delays allow fast recovery or fast failure feedback.
|
||||||
|
for delayMs in [50, 100] {
|
||||||
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
|
||||||
do {
|
do {
|
||||||
let cfg = try await self.configProvider()
|
let cfg = try await self.configProvider()
|
||||||
|
|||||||
@ -6,17 +6,43 @@ import Observation
|
|||||||
final class GatewayProcessManager {
|
final class GatewayProcessManager {
|
||||||
static let shared = GatewayProcessManager()
|
static let shared = GatewayProcessManager()
|
||||||
|
|
||||||
|
/// Represents the lifecycle state of the local gateway process.
|
||||||
|
///
|
||||||
|
/// Valid state transitions:
|
||||||
|
/// - `.stopped` → `.starting(phase)` → (`.running` | `.attachedExisting` | `.failed`)
|
||||||
|
/// - `.failed` → `.starting(phase)` (user retry)
|
||||||
|
/// - any state → `.stopped` (manual stop)
|
||||||
enum Status: Equatable {
|
enum Status: Equatable {
|
||||||
|
/// Gateway is not running and not attempting to start.
|
||||||
case stopped
|
case stopped
|
||||||
case starting
|
|
||||||
|
/// Gateway startup is in progress.
|
||||||
|
/// - Parameter phase: Optional startup phase for user feedback:
|
||||||
|
/// - `"checking"`: Initial state, determining what to do
|
||||||
|
/// - `"attaching"`: Attempting to connect to existing gateway process
|
||||||
|
/// - `"launching"`: Launching new gateway via launchd
|
||||||
|
/// - `"waiting for ready"`: Polling for gateway to accept connections
|
||||||
|
case starting(phase: String?)
|
||||||
|
|
||||||
|
/// Gateway was freshly launched by this app and is now accepting connections.
|
||||||
|
/// - Parameter details: Optional process details (e.g., "pid 12345")
|
||||||
case running(details: String?)
|
case running(details: String?)
|
||||||
|
|
||||||
|
/// Gateway was already running (launched externally or by previous app instance)
|
||||||
|
/// and this app successfully attached to it.
|
||||||
|
/// - Parameter details: Optional process and health details
|
||||||
case attachedExisting(details: String?)
|
case attachedExisting(details: String?)
|
||||||
|
|
||||||
|
/// Gateway failed to start or attach.
|
||||||
|
/// - Parameter message: Human-readable failure reason shown to user
|
||||||
case failed(String)
|
case failed(String)
|
||||||
|
|
||||||
var label: String {
|
var label: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .stopped: return "Stopped"
|
case .stopped: return "Stopped"
|
||||||
case .starting: return "Starting…"
|
case let .starting(phase):
|
||||||
|
if let phase, !phase.isEmpty { return "Starting (\(phase))…" }
|
||||||
|
return "Starting…"
|
||||||
case let .running(details):
|
case let .running(details):
|
||||||
if let details, !details.isEmpty { return "Running (\(details))" }
|
if let details, !details.isEmpty { return "Running (\(details))" }
|
||||||
return "Running"
|
return "Running"
|
||||||
@ -110,15 +136,17 @@ final class GatewayProcessManager {
|
|||||||
case .stopped, .failed:
|
case .stopped, .failed:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
self.status = .starting
|
self.status = .starting(phase: "checking")
|
||||||
self.logger.debug("gateway start requested")
|
self.logger.debug("gateway start requested")
|
||||||
|
|
||||||
// First try to latch onto an already-running gateway to avoid spawning a duplicate.
|
// First try to latch onto an already-running gateway to avoid spawning a duplicate.
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
await MainActor.run { self.status = .starting(phase: "attaching") }
|
||||||
if await self.attachExistingGatewayIfAvailable() {
|
if await self.attachExistingGatewayIfAvailable() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await MainActor.run { self.status = .starting(phase: "launching") }
|
||||||
await self.enableLaunchdGateway()
|
await self.enableLaunchdGateway()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,10 +233,12 @@ final class GatewayProcessManager {
|
|||||||
let data = try await attemptAttach()
|
let data = try await attemptAttach()
|
||||||
let snap = decodeHealthSnapshot(from: data)
|
let snap = decodeHealthSnapshot(from: data)
|
||||||
let details = self.describe(details: instanceText, port: port, snap: snap)
|
let details = self.describe(details: instanceText, port: port, snap: snap)
|
||||||
self.existingGatewayDetails = details
|
await MainActor.run {
|
||||||
self.clearLastFailure()
|
self.existingGatewayDetails = details
|
||||||
self.status = .attachedExisting(details: details)
|
self.clearLastFailure()
|
||||||
self.appendLog("[gateway] using existing instance: \(details)\n")
|
self.status = .attachedExisting(details: details)
|
||||||
|
self.appendLog("[gateway] using existing instance: \(details)\n")
|
||||||
|
}
|
||||||
self.logger.info("gateway using existing instance details=\(details)")
|
self.logger.info("gateway using existing instance details=\(details)")
|
||||||
self.refreshControlChannelIfNeeded(reason: "attach existing")
|
self.refreshControlChannelIfNeeded(reason: "attach existing")
|
||||||
self.refreshLog()
|
self.refreshLog()
|
||||||
@ -221,21 +251,23 @@ final class GatewayProcessManager {
|
|||||||
|
|
||||||
if hasListener {
|
if hasListener {
|
||||||
let reason = self.describeAttachFailure(error, port: port, instance: instance)
|
let reason = self.describeAttachFailure(error, port: port, instance: instance)
|
||||||
self.existingGatewayDetails = instanceText
|
await MainActor.run {
|
||||||
self.status = .failed(reason)
|
self.existingGatewayDetails = instanceText
|
||||||
self.lastFailureReason = reason
|
self.status = .failed(reason)
|
||||||
self.appendLog("[gateway] existing listener on port \(port) but attach failed: \(reason)\n")
|
self.lastFailureReason = reason
|
||||||
|
self.appendLog("[gateway] existing listener on port \(port) but attach failed: \(reason)\n")
|
||||||
|
}
|
||||||
self.logger.warning("gateway attach failed reason=\(reason)")
|
self.logger.warning("gateway attach failed reason=\(reason)")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// No reachable gateway (and no listener) — fall through to spawn.
|
// No reachable gateway (and no listener) — fall through to spawn.
|
||||||
self.existingGatewayDetails = nil
|
await MainActor.run { self.existingGatewayDetails = nil }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.existingGatewayDetails = nil
|
await MainActor.run { self.existingGatewayDetails = nil }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,7 +330,7 @@ final class GatewayProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func enableLaunchdGateway() async {
|
private func enableLaunchdGateway() async {
|
||||||
self.existingGatewayDetails = nil
|
await MainActor.run { self.existingGatewayDetails = nil }
|
||||||
let resolution = await Task.detached(priority: .utility) {
|
let resolution = await Task.detached(priority: .utility) {
|
||||||
GatewayEnvironment.resolveGatewayCommand()
|
GatewayEnvironment.resolveGatewayCommand()
|
||||||
}.value
|
}.value
|
||||||
@ -313,26 +345,31 @@ final class GatewayProcessManager {
|
|||||||
|
|
||||||
if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() {
|
if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() {
|
||||||
let message = "Launchd disabled; start the Gateway manually or disable attach-only."
|
let message = "Launchd disabled; start the Gateway manually or disable attach-only."
|
||||||
self.status = .failed(message)
|
await MainActor.run {
|
||||||
self.lastFailureReason = "launchd disabled"
|
self.status = .failed(message)
|
||||||
self.appendLog("[gateway] launchd disabled; skipping auto-start\n")
|
self.lastFailureReason = "launchd disabled"
|
||||||
|
self.appendLog("[gateway] launchd disabled; skipping auto-start\n")
|
||||||
|
}
|
||||||
self.logger.info("gateway launchd enable skipped (disable marker set)")
|
self.logger.info("gateway launchd enable skipped (disable marker set)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let bundlePath = Bundle.main.bundleURL.path
|
let bundlePath = Bundle.main.bundleURL.path
|
||||||
let port = GatewayEnvironment.gatewayPort()
|
let port = GatewayEnvironment.gatewayPort()
|
||||||
self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n")
|
await MainActor.run { self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n") }
|
||||||
self.logger.info("gateway enabling launchd port=\(port)")
|
self.logger.info("gateway enabling launchd port=\(port)")
|
||||||
let err = await GatewayLaunchAgentManager.set(enabled: true, bundlePath: bundlePath, port: port)
|
let err = await GatewayLaunchAgentManager.set(enabled: true, bundlePath: bundlePath, port: port)
|
||||||
if let err {
|
if let err {
|
||||||
self.status = .failed(err)
|
await MainActor.run {
|
||||||
self.lastFailureReason = err
|
self.status = .failed(err)
|
||||||
|
self.lastFailureReason = err
|
||||||
|
}
|
||||||
self.logger.error("gateway launchd enable failed: \(err)")
|
self.logger.error("gateway launchd enable failed: \(err)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort: wait for the gateway to accept connections.
|
// Best-effort: wait for the gateway to accept connections.
|
||||||
|
await MainActor.run { self.status = .starting(phase: "waiting for ready") }
|
||||||
let deadline = Date().addingTimeInterval(6)
|
let deadline = Date().addingTimeInterval(6)
|
||||||
while Date() < deadline {
|
while Date() < deadline {
|
||||||
if !self.desiredActive { return }
|
if !self.desiredActive { return }
|
||||||
@ -340,8 +377,10 @@ final class GatewayProcessManager {
|
|||||||
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
||||||
let instance = await PortGuardian.shared.describe(port: port)
|
let instance = await PortGuardian.shared.describe(port: port)
|
||||||
let details = instance.map { "pid \($0.pid)" }
|
let details = instance.map { "pid \($0.pid)" }
|
||||||
self.clearLastFailure()
|
await MainActor.run {
|
||||||
self.status = .running(details: details)
|
self.clearLastFailure()
|
||||||
|
self.status = .running(details: details)
|
||||||
|
}
|
||||||
self.logger.info("gateway started details=\(details ?? "ok")")
|
self.logger.info("gateway started details=\(details ?? "ok")")
|
||||||
self.refreshControlChannelIfNeeded(reason: "gateway started")
|
self.refreshControlChannelIfNeeded(reason: "gateway started")
|
||||||
self.refreshLog()
|
self.refreshLog()
|
||||||
@ -351,8 +390,10 @@ final class GatewayProcessManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.status = .failed("Gateway did not start in time")
|
await MainActor.run {
|
||||||
self.lastFailureReason = "launchd start timeout"
|
self.status = .failed("Gateway did not start in time")
|
||||||
|
self.lastFailureReason = "launchd start timeout"
|
||||||
|
}
|
||||||
self.logger.warning("gateway start timed out")
|
self.logger.warning("gateway start timed out")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ struct ClawdbotApp: App {
|
|||||||
self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping)
|
self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping)
|
||||||
}
|
}
|
||||||
.onChange(of: self.state.connectionMode) { _, mode in
|
.onChange(of: self.state.connectionMode) { _, mode in
|
||||||
Task { await ConnectionModeCoordinator.shared.apply(mode: mode, paused: self.state.isPaused) }
|
ConnectionModeCoordinator.shared.apply(mode: mode, paused: self.state.isPaused)
|
||||||
CLIInstallPrompter.shared.checkAndPromptIfNeeded(reason: "connection-mode")
|
CLIInstallPrompter.shared.checkAndPromptIfNeeded(reason: "connection-mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +272,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
self.state = AppStateStore.shared
|
self.state = AppStateStore.shared
|
||||||
AppActivationPolicy.apply(showDockIcon: self.state?.showDockIcon ?? false)
|
AppActivationPolicy.apply(showDockIcon: self.state?.showDockIcon ?? false)
|
||||||
if let state {
|
if let state {
|
||||||
Task { await ConnectionModeCoordinator.shared.apply(mode: state.connectionMode, paused: state.isPaused) }
|
ConnectionModeCoordinator.shared.apply(mode: state.connectionMode, paused: state.isPaused)
|
||||||
}
|
}
|
||||||
TerminationSignalWatcher.shared.start()
|
TerminationSignalWatcher.shared.start()
|
||||||
NodePairingApprovalPrompter.shared.start()
|
NodePairingApprovalPrompter.shared.start()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user