mirror of https://github.com/openclaw/openclaw.git
fix: keep android canvas home visible after restart
This commit is contained in:
parent
f6e5b6758e
commit
2ae8837987
|
|
@ -176,6 +176,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
runtime.requestCanvasRehydrate(source = source, force = true)
|
runtime.requestCanvasRehydrate(source = source, force = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshHomeCanvasOverviewIfConnected() {
|
||||||
|
runtime.refreshHomeCanvasOverviewIfConnected()
|
||||||
|
}
|
||||||
|
|
||||||
fun loadChat(sessionKey: String) {
|
fun loadChat(sessionKey: String) {
|
||||||
runtime.loadChat(sessionKey)
|
runtime.loadChat(sessionKey)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
@ -210,7 +212,8 @@ class NodeRuntime(context: Context) {
|
||||||
private val _isForeground = MutableStateFlow(true)
|
private val _isForeground = MutableStateFlow(true)
|
||||||
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
|
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
|
||||||
|
|
||||||
private var lastAutoA2uiUrl: String? = null
|
private var gatewayDefaultAgentId: String? = null
|
||||||
|
private var gatewayAgents: List<GatewayAgentSummary> = emptyList()
|
||||||
private var didAutoRequestCanvasRehydrate = false
|
private var didAutoRequestCanvasRehydrate = false
|
||||||
private val canvasRehydrateSeq = AtomicLong(0)
|
private val canvasRehydrateSeq = AtomicLong(0)
|
||||||
private var operatorConnected = false
|
private var operatorConnected = false
|
||||||
|
|
@ -232,7 +235,7 @@ class NodeRuntime(context: Context) {
|
||||||
updateStatus()
|
updateStatus()
|
||||||
micCapture.onGatewayConnectionChanged(true)
|
micCapture.onGatewayConnectionChanged(true)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
refreshBrandingFromGateway()
|
refreshHomeCanvasOverviewIfConnected()
|
||||||
if (voiceReplySpeakerLazy.isInitialized()) {
|
if (voiceReplySpeakerLazy.isInitialized()) {
|
||||||
voiceReplySpeaker.refreshConfig()
|
voiceReplySpeaker.refreshConfig()
|
||||||
}
|
}
|
||||||
|
|
@ -270,7 +273,7 @@ class NodeRuntime(context: Context) {
|
||||||
_canvasRehydratePending.value = false
|
_canvasRehydratePending.value = false
|
||||||
_canvasRehydrateErrorText.value = null
|
_canvasRehydrateErrorText.value = null
|
||||||
updateStatus()
|
updateStatus()
|
||||||
maybeNavigateToA2uiOnConnect()
|
showLocalCanvasOnConnect()
|
||||||
},
|
},
|
||||||
onDisconnected = { message ->
|
onDisconnected = { message ->
|
||||||
_nodeConnected.value = false
|
_nodeConnected.value = false
|
||||||
|
|
@ -396,6 +399,7 @@ class NodeRuntime(context: Context) {
|
||||||
_mainSessionKey.value = trimmed
|
_mainSessionKey.value = trimmed
|
||||||
talkMode.setMainSessionKey(trimmed)
|
talkMode.setMainSessionKey(trimmed)
|
||||||
chat.applyMainSessionKey(trimmed)
|
chat.applyMainSessionKey(trimmed)
|
||||||
|
updateHomeCanvasState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatus() {
|
private fun updateStatus() {
|
||||||
|
|
@ -415,6 +419,7 @@ class NodeRuntime(context: Context) {
|
||||||
operator.isNotBlank() && operator != "Offline" -> operator
|
operator.isNotBlank() && operator != "Offline" -> operator
|
||||||
else -> node
|
else -> node
|
||||||
}
|
}
|
||||||
|
updateHomeCanvasState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveMainSessionKey(): String {
|
private fun resolveMainSessionKey(): String {
|
||||||
|
|
@ -422,23 +427,31 @@ class NodeRuntime(context: Context) {
|
||||||
return if (trimmed.isEmpty()) "main" else trimmed
|
return if (trimmed.isEmpty()) "main" else trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeNavigateToA2uiOnConnect() {
|
private fun showLocalCanvasOnConnect() {
|
||||||
val a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: return
|
|
||||||
val current = canvas.currentUrl()?.trim().orEmpty()
|
|
||||||
if (current.isEmpty() || current == lastAutoA2uiUrl) {
|
|
||||||
lastAutoA2uiUrl = a2uiUrl
|
|
||||||
canvas.navigate(a2uiUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLocalCanvasOnDisconnect() {
|
|
||||||
lastAutoA2uiUrl = null
|
|
||||||
_canvasA2uiHydrated.value = false
|
_canvasA2uiHydrated.value = false
|
||||||
_canvasRehydratePending.value = false
|
_canvasRehydratePending.value = false
|
||||||
_canvasRehydrateErrorText.value = null
|
_canvasRehydrateErrorText.value = null
|
||||||
canvas.navigate("")
|
canvas.navigate("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLocalCanvasOnDisconnect() {
|
||||||
|
_canvasA2uiHydrated.value = false
|
||||||
|
_canvasRehydratePending.value = false
|
||||||
|
_canvasRehydrateErrorText.value = null
|
||||||
|
canvas.navigate("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshHomeCanvasOverviewIfConnected() {
|
||||||
|
if (!operatorConnected) {
|
||||||
|
updateHomeCanvasState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
refreshBrandingFromGateway()
|
||||||
|
refreshAgentsFromGateway()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun requestCanvasRehydrate(source: String = "manual", force: Boolean = true) {
|
fun requestCanvasRehydrate(source: String = "manual", force: Boolean = true) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (!_nodeConnected.value) {
|
if (!_nodeConnected.value) {
|
||||||
|
|
@ -602,6 +615,8 @@ class NodeRuntime(context: Context) {
|
||||||
canvas.setDebugStatus(status, server ?: remote)
|
canvas.setDebugStatus(status, server ?: remote)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateHomeCanvasState()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setForeground(value: Boolean) {
|
fun setForeground(value: Boolean) {
|
||||||
|
|
@ -928,11 +943,177 @@ class NodeRuntime(context: Context) {
|
||||||
|
|
||||||
val parsed = parseHexColorArgb(raw)
|
val parsed = parseHexColorArgb(raw)
|
||||||
_seamColorArgb.value = parsed ?: DEFAULT_SEAM_COLOR_ARGB
|
_seamColorArgb.value = parsed ?: DEFAULT_SEAM_COLOR_ARGB
|
||||||
|
updateHomeCanvasState()
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshAgentsFromGateway() {
|
||||||
|
if (!operatorConnected) return
|
||||||
|
try {
|
||||||
|
val res = operatorSession.request("agents.list", "{}")
|
||||||
|
val root = json.parseToJsonElement(res).asObjectOrNull() ?: return
|
||||||
|
val defaultAgentId = root["defaultId"].asStringOrNull()?.trim().orEmpty()
|
||||||
|
val mainKey = normalizeMainKey(root["mainKey"].asStringOrNull())
|
||||||
|
val agents =
|
||||||
|
(root["agents"] as? JsonArray)?.mapNotNull { item ->
|
||||||
|
val obj = item.asObjectOrNull() ?: return@mapNotNull null
|
||||||
|
val id = obj["id"].asStringOrNull()?.trim().orEmpty()
|
||||||
|
if (id.isEmpty()) return@mapNotNull null
|
||||||
|
val name = obj["name"].asStringOrNull()?.trim()
|
||||||
|
val emoji = obj["identity"].asObjectOrNull()?.get("emoji").asStringOrNull()?.trim()
|
||||||
|
GatewayAgentSummary(
|
||||||
|
id = id,
|
||||||
|
name = name?.takeIf { it.isNotEmpty() },
|
||||||
|
emoji = emoji?.takeIf { it.isNotEmpty() },
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
gatewayDefaultAgentId = defaultAgentId.ifEmpty { null }
|
||||||
|
gatewayAgents = agents
|
||||||
|
applyMainSessionKey(mainKey)
|
||||||
|
updateHomeCanvasState()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateHomeCanvasState() {
|
||||||
|
val payload =
|
||||||
|
try {
|
||||||
|
json.encodeToString(makeHomeCanvasPayload())
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
canvas.updateHomeCanvasState(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeHomeCanvasPayload(): HomeCanvasPayload {
|
||||||
|
val state = resolveHomeCanvasGatewayState()
|
||||||
|
val gatewayName = normalized(_serverName.value)
|
||||||
|
val gatewayAddress = normalized(_remoteAddress.value)
|
||||||
|
val gatewayLabel = gatewayName ?: gatewayAddress ?: "Gateway"
|
||||||
|
val activeAgentId = resolveActiveAgentId()
|
||||||
|
val agents = homeCanvasAgents(activeAgentId)
|
||||||
|
|
||||||
|
return when (state) {
|
||||||
|
HomeCanvasGatewayState.Connected ->
|
||||||
|
HomeCanvasPayload(
|
||||||
|
gatewayState = "connected",
|
||||||
|
eyebrow = "Connected to $gatewayLabel",
|
||||||
|
title = "Your agents are ready",
|
||||||
|
subtitle =
|
||||||
|
"This phone stays dormant until the gateway needs it, then wakes, syncs, and goes back to sleep.",
|
||||||
|
gatewayLabel = gatewayLabel,
|
||||||
|
activeAgentName = resolveActiveAgentName(activeAgentId),
|
||||||
|
activeAgentBadge = agents.firstOrNull { it.isActive }?.badge ?: "OC",
|
||||||
|
activeAgentCaption = "Selected on this phone",
|
||||||
|
agentCount = agents.size,
|
||||||
|
agents = agents.take(6),
|
||||||
|
footer = "The overview refreshes on reconnect and when this screen opens.",
|
||||||
|
)
|
||||||
|
HomeCanvasGatewayState.Connecting ->
|
||||||
|
HomeCanvasPayload(
|
||||||
|
gatewayState = "connecting",
|
||||||
|
eyebrow = "Reconnecting",
|
||||||
|
title = "OpenClaw is syncing back up",
|
||||||
|
subtitle =
|
||||||
|
"The gateway session is coming back online. Agent shortcuts should settle automatically in a moment.",
|
||||||
|
gatewayLabel = gatewayLabel,
|
||||||
|
activeAgentName = resolveActiveAgentName(activeAgentId),
|
||||||
|
activeAgentBadge = "OC",
|
||||||
|
activeAgentCaption = "Gateway session in progress",
|
||||||
|
agentCount = agents.size,
|
||||||
|
agents = agents.take(4),
|
||||||
|
footer = "If the gateway is reachable, reconnect should complete without intervention.",
|
||||||
|
)
|
||||||
|
HomeCanvasGatewayState.Error, HomeCanvasGatewayState.Offline ->
|
||||||
|
HomeCanvasPayload(
|
||||||
|
gatewayState = if (state == HomeCanvasGatewayState.Error) "error" else "offline",
|
||||||
|
eyebrow = "Welcome to OpenClaw",
|
||||||
|
title = "Your phone stays quiet until it is needed",
|
||||||
|
subtitle =
|
||||||
|
"Pair this device to your gateway to wake it only for real work, keep a live agent overview handy, and avoid battery-draining background loops.",
|
||||||
|
gatewayLabel = gatewayLabel,
|
||||||
|
activeAgentName = "Main",
|
||||||
|
activeAgentBadge = "OC",
|
||||||
|
activeAgentCaption = "Connect to load your agents",
|
||||||
|
agentCount = agents.size,
|
||||||
|
agents = agents.take(4),
|
||||||
|
footer = "When connected, the gateway can wake the phone with a silent push instead of holding an always-on session.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveHomeCanvasGatewayState(): HomeCanvasGatewayState {
|
||||||
|
val lower = _statusText.value.trim().lowercase()
|
||||||
|
return when {
|
||||||
|
_isConnected.value -> HomeCanvasGatewayState.Connected
|
||||||
|
lower.contains("connecting") || lower.contains("reconnecting") -> HomeCanvasGatewayState.Connecting
|
||||||
|
lower.contains("error") || lower.contains("failed") -> HomeCanvasGatewayState.Error
|
||||||
|
else -> HomeCanvasGatewayState.Offline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveActiveAgentId(): String {
|
||||||
|
val mainKey = _mainSessionKey.value.trim()
|
||||||
|
if (mainKey.startsWith("agent:")) {
|
||||||
|
val agentId = mainKey.removePrefix("agent:").substringBefore(':').trim()
|
||||||
|
if (agentId.isNotEmpty()) return agentId
|
||||||
|
}
|
||||||
|
return gatewayDefaultAgentId?.trim().orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveActiveAgentName(activeAgentId: String): String {
|
||||||
|
if (activeAgentId.isNotEmpty()) {
|
||||||
|
gatewayAgents.firstOrNull { it.id == activeAgentId }?.let { agent ->
|
||||||
|
return normalized(agent.name) ?: agent.id
|
||||||
|
}
|
||||||
|
return activeAgentId
|
||||||
|
}
|
||||||
|
return gatewayAgents.firstOrNull()?.let { normalized(it.name) ?: it.id } ?: "Main"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun homeCanvasAgents(activeAgentId: String): List<HomeCanvasAgentCard> {
|
||||||
|
val defaultAgentId = gatewayDefaultAgentId?.trim().orEmpty()
|
||||||
|
return gatewayAgents
|
||||||
|
.map { agent ->
|
||||||
|
val isActive = activeAgentId.isNotEmpty() && agent.id == activeAgentId
|
||||||
|
val isDefault = defaultAgentId.isNotEmpty() && agent.id == defaultAgentId
|
||||||
|
HomeCanvasAgentCard(
|
||||||
|
id = agent.id,
|
||||||
|
name = normalized(agent.name) ?: agent.id,
|
||||||
|
badge = homeCanvasBadge(agent),
|
||||||
|
caption =
|
||||||
|
when {
|
||||||
|
isActive -> "Active on this phone"
|
||||||
|
isDefault -> "Default agent"
|
||||||
|
else -> "Ready"
|
||||||
|
},
|
||||||
|
isActive = isActive,
|
||||||
|
)
|
||||||
|
}.sortedWith(compareByDescending<HomeCanvasAgentCard> { it.isActive }.thenBy { it.name.lowercase() })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun homeCanvasBadge(agent: GatewayAgentSummary): String {
|
||||||
|
val emoji = normalized(agent.emoji)
|
||||||
|
if (emoji != null) return emoji
|
||||||
|
val initials =
|
||||||
|
(normalized(agent.name) ?: agent.id)
|
||||||
|
.split(' ', '-', '_')
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.take(2)
|
||||||
|
.mapNotNull { token -> token.firstOrNull()?.uppercaseChar()?.toString() }
|
||||||
|
.joinToString("")
|
||||||
|
return if (initials.isNotEmpty()) initials else "OC"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalized(value: String?): String? {
|
||||||
|
val trimmed = value?.trim().orEmpty()
|
||||||
|
return trimmed.ifEmpty { null }
|
||||||
|
}
|
||||||
|
|
||||||
private fun triggerCameraFlash() {
|
private fun triggerCameraFlash() {
|
||||||
// Token is used as a pulse trigger; value doesn't matter as long as it changes.
|
// Token is used as a pulse trigger; value doesn't matter as long as it changes.
|
||||||
_cameraFlashToken.value = SystemClock.elapsedRealtimeNanos()
|
_cameraFlashToken.value = SystemClock.elapsedRealtimeNanos()
|
||||||
|
|
@ -951,3 +1132,40 @@ class NodeRuntime(context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class HomeCanvasGatewayState {
|
||||||
|
Connected,
|
||||||
|
Connecting,
|
||||||
|
Error,
|
||||||
|
Offline,
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class GatewayAgentSummary(
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val emoji: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class HomeCanvasPayload(
|
||||||
|
val gatewayState: String,
|
||||||
|
val eyebrow: String,
|
||||||
|
val title: String,
|
||||||
|
val subtitle: String,
|
||||||
|
val gatewayLabel: String,
|
||||||
|
val activeAgentName: String,
|
||||||
|
val activeAgentBadge: String,
|
||||||
|
val activeAgentCaption: String,
|
||||||
|
val agentCount: Int,
|
||||||
|
val agents: List<HomeCanvasAgentCard>,
|
||||||
|
val footer: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class HomeCanvasAgentCard(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val badge: String,
|
||||||
|
val caption: String,
|
||||||
|
val isActive: Boolean,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class CanvasController {
|
||||||
@Volatile private var debugStatusEnabled: Boolean = false
|
@Volatile private var debugStatusEnabled: Boolean = false
|
||||||
@Volatile private var debugStatusTitle: String? = null
|
@Volatile private var debugStatusTitle: String? = null
|
||||||
@Volatile private var debugStatusSubtitle: String? = null
|
@Volatile private var debugStatusSubtitle: String? = null
|
||||||
|
@Volatile private var homeCanvasStateJson: String? = null
|
||||||
private val _currentUrl = MutableStateFlow<String?>(null)
|
private val _currentUrl = MutableStateFlow<String?>(null)
|
||||||
val currentUrl: StateFlow<String?> = _currentUrl.asStateFlow()
|
val currentUrl: StateFlow<String?> = _currentUrl.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -56,6 +57,7 @@ class CanvasController {
|
||||||
this.webView = webView
|
this.webView = webView
|
||||||
reload()
|
reload()
|
||||||
applyDebugStatus()
|
applyDebugStatus()
|
||||||
|
applyHomeCanvasState()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun detach(webView: WebView) {
|
fun detach(webView: WebView) {
|
||||||
|
|
@ -88,6 +90,12 @@ class CanvasController {
|
||||||
|
|
||||||
fun onPageFinished() {
|
fun onPageFinished() {
|
||||||
applyDebugStatus()
|
applyDebugStatus()
|
||||||
|
applyHomeCanvasState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHomeCanvasState(json: String?) {
|
||||||
|
homeCanvasStateJson = json
|
||||||
|
applyHomeCanvasState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun withWebViewOnMain(crossinline block: (WebView) -> Unit) {
|
private inline fun withWebViewOnMain(crossinline block: (WebView) -> Unit) {
|
||||||
|
|
@ -142,6 +150,22 @@ class CanvasController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyHomeCanvasState() {
|
||||||
|
val payload = homeCanvasStateJson ?: "null"
|
||||||
|
withWebViewOnMain { wv ->
|
||||||
|
val js = """
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const api = globalThis.__openclaw;
|
||||||
|
if (!api || typeof api.renderHome !== 'function') return;
|
||||||
|
api.renderHome($payload);
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
""".trimIndent()
|
||||||
|
wv.evaluateJavascript(js, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun eval(javaScript: String): String =
|
suspend fun eval(javaScript: String): String =
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val wv = webView ?: throw IllegalStateException("no webview")
|
val wv = webView ?: throw IllegalStateException("no webview")
|
||||||
|
|
|
||||||
|
|
@ -134,43 +134,14 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenTabScreen(viewModel: MainViewModel) {
|
private fun ScreenTabScreen(viewModel: MainViewModel) {
|
||||||
val isConnected by viewModel.isConnected.collectAsState()
|
val isConnected by viewModel.isConnected.collectAsState()
|
||||||
val isNodeConnected by viewModel.isNodeConnected.collectAsState()
|
LaunchedEffect(isConnected) {
|
||||||
val canvasUrl by viewModel.canvasCurrentUrl.collectAsState()
|
if (isConnected) {
|
||||||
val canvasA2uiHydrated by viewModel.canvasA2uiHydrated.collectAsState()
|
viewModel.refreshHomeCanvasOverviewIfConnected()
|
||||||
val canvasRehydratePending by viewModel.canvasRehydratePending.collectAsState()
|
|
||||||
val canvasRehydrateErrorText by viewModel.canvasRehydrateErrorText.collectAsState()
|
|
||||||
val isA2uiUrl = canvasUrl?.contains("/__openclaw__/a2ui/") == true
|
|
||||||
val showRestoreCta = isConnected && isNodeConnected && (canvasUrl.isNullOrBlank() || (isA2uiUrl && !canvasA2uiHydrated))
|
|
||||||
val restoreCtaText =
|
|
||||||
when {
|
|
||||||
canvasRehydratePending -> "Restore requested. Waiting for agent…"
|
|
||||||
!canvasRehydrateErrorText.isNullOrBlank() -> canvasRehydrateErrorText!!
|
|
||||||
else -> "Canvas reset. Tap to restore dashboard."
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
CanvasScreen(viewModel = viewModel, modifier = Modifier.fillMaxSize())
|
CanvasScreen(viewModel = viewModel, modifier = Modifier.fillMaxSize())
|
||||||
|
|
||||||
if (showRestoreCta) {
|
|
||||||
Surface(
|
|
||||||
onClick = {
|
|
||||||
if (canvasRehydratePending) return@Surface
|
|
||||||
viewModel.requestCanvasRehydrate(source = "screen_tab_cta")
|
|
||||||
},
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter).padding(horizontal = 16.dp, vertical = 16.dp),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = mobileSurface.copy(alpha = 0.9f),
|
|
||||||
border = BorderStroke(1.dp, mobileBorder),
|
|
||||||
shadowElevation = 4.dp,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = restoreCtaText,
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
|
||||||
style = mobileCallout.copy(fontWeight = FontWeight.Medium),
|
|
||||||
color = mobileText,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue