diff --git a/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt b/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt index bcfd657694e..7076f09a292 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/MainViewModel.kt @@ -26,6 +26,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { val discoveryStatusText: StateFlow = runtime.discoveryStatusText val isConnected: StateFlow = runtime.isConnected + val isNodeConnected: StateFlow = runtime.nodeConnected val statusText: StateFlow = runtime.statusText val serverName: StateFlow = runtime.serverName val remoteAddress: StateFlow = runtime.remoteAddress diff --git a/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt index 9cf06a0b621..f83672aa88a 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt @@ -181,6 +181,8 @@ class NodeRuntime(context: Context) { private val _isConnected = MutableStateFlow(false) val isConnected: StateFlow = _isConnected.asStateFlow() + private val _nodeConnected = MutableStateFlow(false) + val nodeConnected: StateFlow = _nodeConnected.asStateFlow() private val _statusText = MutableStateFlow("Offline") val statusText: StateFlow = _statusText.asStateFlow() @@ -224,7 +226,6 @@ class NodeRuntime(context: Context) { private var didAutoRequestCanvasRehydrate = false private val canvasRehydrateSeq = AtomicLong(0) private var operatorConnected = false - private var nodeConnected = false private var operatorStatusText: String = "Offline" private var nodeStatusText: String = "Offline" @@ -270,7 +271,7 @@ class NodeRuntime(context: Context) { identityStore = identityStore, deviceAuthStore = deviceAuthStore, onConnected = { _, _, _ -> - nodeConnected = true + _nodeConnected.value = true nodeStatusText = "Connected" didAutoRequestCanvasRehydrate = false _canvasA2uiHydrated.value = false @@ -281,7 +282,7 @@ class NodeRuntime(context: Context) { requestCanvasRehydrate(source = "node_connect", force = false) }, onDisconnected = { message -> - nodeConnected = false + _nodeConnected.value = false nodeStatusText = message didAutoRequestCanvasRehydrate = false _canvasA2uiHydrated.value = false @@ -329,9 +330,9 @@ class NodeRuntime(context: Context) { _isConnected.value = operatorConnected _statusText.value = when { - operatorConnected && nodeConnected -> "Connected" - operatorConnected && !nodeConnected -> "Connected (node offline)" - !operatorConnected && nodeConnected -> "Connected (operator offline)" + operatorConnected && _nodeConnected.value -> "Connected" + operatorConnected && !_nodeConnected.value -> "Connected (node offline)" + !operatorConnected && _nodeConnected.value -> "Connected (operator offline)" operatorStatusText.isNotBlank() && operatorStatusText != "Offline" -> operatorStatusText else -> nodeStatusText } @@ -361,7 +362,11 @@ class NodeRuntime(context: Context) { fun requestCanvasRehydrate(source: String = "manual", force: Boolean = true) { scope.launch { - if (!nodeConnected) return@launch + if (!_nodeConnected.value) { + _canvasRehydratePending.value = false + _canvasRehydrateErrorText.value = "Node offline. Reconnect and retry." + return@launch + } if (!force && didAutoRequestCanvasRehydrate) return@launch didAutoRequestCanvasRehydrate = true val requestId = canvasRehydrateSeq.incrementAndGet() @@ -725,7 +730,7 @@ class NodeRuntime(context: Context) { contextJson = contextJson, ) - val connected = nodeConnected + val connected = _nodeConnected.value var error: String? = null if (connected) { try { diff --git a/apps/android/app/src/main/java/ai/openclaw/android/ui/PostOnboardingTabs.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/PostOnboardingTabs.kt index d61a107e7ed..b68c06ff2ff 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/ui/PostOnboardingTabs.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/PostOnboardingTabs.kt @@ -125,12 +125,13 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier) @Composable private fun ScreenTabScreen(viewModel: MainViewModel) { val isConnected by viewModel.isConnected.collectAsState() + val isNodeConnected by viewModel.isNodeConnected.collectAsState() val canvasUrl by viewModel.canvasCurrentUrl.collectAsState() val canvasA2uiHydrated by viewModel.canvasA2uiHydrated.collectAsState() val canvasRehydratePending by viewModel.canvasRehydratePending.collectAsState() val canvasRehydrateErrorText by viewModel.canvasRehydrateErrorText.collectAsState() val isA2uiUrl = canvasUrl?.contains("/__openclaw__/a2ui/") == true - val showRestoreCta = isConnected && (canvasUrl.isNullOrBlank() || (isA2uiUrl && !canvasA2uiHydrated)) + val showRestoreCta = isConnected && isNodeConnected && (canvasUrl.isNullOrBlank() || (isA2uiUrl && !canvasA2uiHydrated)) val restoreCtaText = when { canvasRehydratePending -> "Restore requested. Waiting for agent…"