Skip to content

Commit 8b66003

Browse files
Tosko4obviyus
authored andcommitted
fix(android): clarify provider attention state
1 parent 12a5691 commit 8b66003

14 files changed

Lines changed: 528 additions & 46 deletions

File tree

apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,6 +2085,7 @@ class NodeRuntime(
20852085
id = id,
20862086
name = obj["name"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: id,
20872087
provider = provider,
2088+
available = obj.optionalBoolean("available"),
20882089
supportsVision = "image" in inputTypes,
20892090
supportsAudio = "audio" in inputTypes,
20902091
supportsDocuments = "document" in inputTypes,
@@ -2701,6 +2702,7 @@ data class GatewayModelSummary(
27012702
val id: String,
27022703
val name: String,
27032704
val provider: String,
2705+
val available: Boolean?,
27042706
val supportsVision: Boolean,
27052707
val supportsAudio: Boolean,
27062708
val supportsDocuments: Boolean,
@@ -2883,6 +2885,15 @@ private fun JsonObject?.double(key: String): Double? = (this?.get(key) as? JsonP
28832885

28842886
private fun JsonObject?.boolean(key: String): Boolean = (this?.get(key) as? JsonPrimitive)?.content?.trim() == "true"
28852887

2888+
private fun JsonObject?.optionalBoolean(key: String): Boolean? =
2889+
(this?.get(key) as? JsonPrimitive)?.content?.trim()?.lowercase()?.let { value ->
2890+
when (value) {
2891+
"true" -> true
2892+
"false" -> false
2893+
else -> null
2894+
}
2895+
}
2896+
28862897
internal fun cronJobLastRunStatus(state: JsonObject?): String? =
28872898
state
28882899
.cronStatus("lastStatus")

apps/android/app/src/main/java/ai/openclaw/app/ui/CommandPalette.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,10 @@ private fun providerCommandSubtitle(
304304
models: List<GatewayModelSummary>,
305305
): String {
306306
if (!isConnected) return "Connect Gateway to load models"
307-
val readyProviderCount = providers.count { modelProviderReady(it.status) }
307+
val readyProviderCount = readyModelProviderCount(providers, models)
308308
if (readyProviderCount > 0) return "$readyProviderCount providers ready"
309+
val expiringProviderCount = expiringModelProviderCount(providers)
310+
if (expiringProviderCount > 0) return "$expiringProviderCount providers expiring"
309311
if (models.isNotEmpty()) return "${models.size} models available"
310312
return "Configure model access"
311313
}

apps/android/app/src/main/java/ai/openclaw/app/ui/ProvidersModelsScreen.kt

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,44 +192,70 @@ private data class ProviderSetupRow(
192192
val name: String,
193193
val subtitle: String,
194194
val ready: Boolean,
195+
val available: Boolean,
196+
val statusLabel: String,
197+
val warning: Boolean,
195198
)
196199

197200
private data class ProviderRow(
198201
val id: String,
199202
val name: String,
200203
val status: String,
201204
val ready: Boolean,
205+
val available: Boolean,
206+
val setupRequired: Boolean,
207+
val warning: Boolean,
202208
val modelCount: Int,
203209
)
204210

205-
/** Combines auth-provider readiness rows with catalog-only providers. */
211+
/** Combines auth-provider readiness rows with catalog-only browse providers. */
206212
private fun providerRows(
207213
providers: List<GatewayModelProviderSummary>,
208214
models: List<GatewayModelSummary>,
209215
): List<ProviderRow> {
210216
val modelCounts = models.groupingBy { it.provider }.eachCount()
217+
val availableProviderIds =
218+
models
219+
.filter(::modelAvailabilityUsable)
220+
.map { it.provider.normalizedProviderId() }
221+
.toSet()
211222
val authRows =
212223
providers.map { provider ->
213-
val ready = modelProviderReady(provider.status)
224+
val providerId = provider.id.normalizedProviderId()
225+
val authReady = modelProviderReady(provider.status)
226+
val expiring = modelProviderExpiring(provider.status)
227+
val available = providerId in availableProviderIds
214228
ProviderRow(
215229
id = provider.id,
216230
name = provider.displayName,
217-
status = if (ready) "Ready" else "Needs setup",
218-
ready = ready,
231+
status =
232+
when {
233+
authReady -> "Ready"
234+
expiring -> "Expiring"
235+
available -> "Available"
236+
else -> "Needs setup"
237+
},
238+
ready = authReady,
239+
available = available || authReady || expiring,
240+
setupRequired = !authReady && !available && !expiring,
241+
warning = expiring,
219242
modelCount = modelCounts[provider.id] ?: 0,
220243
)
221244
}
222-
// Static/catalog-only providers may expose models without a matching auth
223-
// provider row; keep them visible as ready providers.
245+
// Catalog-only providers can be browsed but are not a readiness signal.
224246
val missingAuthRows =
225247
modelCounts.keys
226248
.filter { provider -> authRows.none { it.id == provider } }
227249
.map { provider ->
250+
val available = provider.normalizedProviderId() in availableProviderIds
228251
ProviderRow(
229252
id = provider,
230253
name = providerDisplayName(provider),
231-
status = "Ready",
232-
ready = true,
254+
status = if (available) "Available" else "Catalog",
255+
ready = available,
256+
available = available,
257+
setupRequired = false,
258+
warning = false,
233259
modelCount = modelCounts[provider] ?: 0,
234260
)
235261
}
@@ -245,6 +271,9 @@ private fun providerSetupRows(providerRows: List<ProviderRow>): List<ProviderSet
245271
name = providerDisplayName(id),
246272
subtitle = providerSetupSubtitle(id, row),
247273
ready = row?.ready == true,
274+
available = row?.available == true,
275+
statusLabel = providerSetupStatusLabel(row),
276+
warning = row?.warning == true || row?.setupRequired == true || row == null,
248277
)
249278
}
250279
}
@@ -254,12 +283,24 @@ private fun providerSetupSubtitle(
254283
row: ProviderRow?,
255284
): String =
256285
when {
286+
row?.status == "Expiring" -> "Credential expires soon"
257287
row?.ready == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Ready"
258-
row != null -> "Finish setup to use ${row.name}"
288+
row?.available == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Available"
289+
row?.setupRequired == true -> "Finish setup to use ${row.name}"
290+
row != null && row.modelCount > 0 -> "${row.modelCount} catalog models"
259291
id == "ollama" -> "Use models running on your network"
260292
else -> "Add provider credentials on your Gateway"
261293
}
262294

295+
private fun providerSetupStatusLabel(row: ProviderRow?): String =
296+
when {
297+
row?.ready == true -> "Ready"
298+
row?.status == "Expiring" -> "Expiring"
299+
row?.available == true -> "Available"
300+
row?.setupRequired == false -> "Catalog"
301+
else -> "Setup"
302+
}
303+
263304
/** Normalizes gateway provider status strings into a ready/not-ready boolean. */
264305
internal fun modelProviderReady(status: String): Boolean {
265306
val normalized = status.trim().lowercase()
@@ -270,6 +311,32 @@ internal fun modelProviderReady(status: String): Boolean {
270311
normalized == "static"
271312
}
272313

314+
private fun modelProviderExpiring(status: String): Boolean = status.trim().lowercase() == "expiring"
315+
316+
/** Counts providers with either ready auth status or currently available configured models. */
317+
internal fun readyModelProviderCount(
318+
providers: List<GatewayModelProviderSummary>,
319+
models: List<GatewayModelSummary>,
320+
): Int {
321+
val authReadyProviders = providers.filter { modelProviderReady(it.status) }.map { it.id.normalizedProviderId() }
322+
val availableModelProviders = models.filter(::modelAvailabilityUsable).map { it.provider.normalizedProviderId() }
323+
return (authReadyProviders + availableModelProviders).distinct().size
324+
}
325+
326+
// Older gateways did not emit `available`; keep those rows on the legacy
327+
// readiness path while still honoring explicit false from upgraded gateways.
328+
private fun modelAvailabilityUsable(model: GatewayModelSummary): Boolean = model.available != false
329+
330+
/** Counts auth-backed providers that can serve now but need renewal soon. */
331+
internal fun expiringModelProviderCount(providers: List<GatewayModelProviderSummary>): Int =
332+
providers
333+
.filter { modelProviderExpiring(it.status) }
334+
.map { it.id.normalizedProviderId() }
335+
.distinct()
336+
.size
337+
338+
private fun String.normalizedProviderId(): String = trim().lowercase()
339+
273340
/** Groups models by provider using the same display priority as provider rows. */
274341
private fun sortedModelGroups(models: List<GatewayModelSummary>): List<Pair<String, List<GatewayModelSummary>>> =
275342
models
@@ -299,7 +366,18 @@ private fun ProviderList(
299366
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
300367
Column {
301368
if (rows.isEmpty()) {
302-
ProviderListRow(ProviderRow(id = "loading", name = "Provider catalog", status = if (refreshing) "Loading" else "No providers", ready = false, modelCount = 0))
369+
ProviderListRow(
370+
ProviderRow(
371+
id = "loading",
372+
name = "Provider catalog",
373+
status = if (refreshing) "Loading" else "No providers",
374+
ready = false,
375+
available = false,
376+
setupRequired = false,
377+
warning = false,
378+
modelCount = 0,
379+
),
380+
)
303381
} else {
304382
val visibleRows = rows.take(5)
305383
visibleRows.forEachIndexed { index, row ->
@@ -322,12 +400,12 @@ private fun ProviderOverviewPanel(
322400
onRefresh: () -> Unit,
323401
onSetup: () -> Unit,
324402
) {
325-
val readyCount = providerRows.count { it.ready }
326-
val needsSetupCount = providerRows.count { !it.ready }
403+
val readyCount = providerRows.count { it.available }
404+
val needsSetupCount = providerRows.count { it.setupRequired }
327405
ClawPanel(contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)) {
328406
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
329407
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
330-
ProviderMetricTile(label = "Ready", value = readyCount.toString(), modifier = Modifier.weight(1f))
408+
ProviderMetricTile(label = "Available", value = readyCount.toString(), modifier = Modifier.weight(1f))
331409
ProviderMetricTile(label = "Models", value = modelCount.toString(), modifier = Modifier.weight(1f))
332410
ProviderMetricTile(label = "Setup", value = needsSetupCount.toString(), modifier = Modifier.weight(1f))
333411
}
@@ -398,8 +476,14 @@ private fun ProviderSetupListRow(
398476
Text(text = row.subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
399477
}
400478
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
401-
Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
402-
Text(text = if (row.ready) "Ready" else "Setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
479+
val statusColor =
480+
when {
481+
row.warning -> ClawTheme.colors.warning
482+
row.ready || row.available -> ClawTheme.colors.success
483+
else -> ClawTheme.colors.textMuted
484+
}
485+
Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(statusColor))
486+
Text(text = row.statusLabel, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
403487
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${row.name}", modifier = Modifier.size(17.dp), tint = ClawTheme.colors.text)
404488
}
405489
}
@@ -415,7 +499,13 @@ private fun ProviderListRow(row: ProviderRow) {
415499
Text(text = if (row.modelCount > 0) "${row.modelCount} models" else "Provider setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
416500
}
417501
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
418-
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
502+
val statusColor =
503+
when {
504+
row.warning || row.setupRequired -> ClawTheme.colors.warning
505+
row.ready || row.available -> ClawTheme.colors.success
506+
else -> ClawTheme.colors.textMuted
507+
}
508+
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(statusColor))
419509
Text(text = row.status, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
420510
}
421511
}

apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ private val shellNavTabs = listOf(Tab.Overview, Tab.Chat, Tab.Voice, Tab.Setting
103103
private val shellContentInsets: WindowInsets
104104
@Composable get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
105105

106-
internal fun shellBottomNavVisible(keyboardVisible: Boolean, commandOpen: Boolean): Boolean = !keyboardVisible && !commandOpen
106+
internal fun shellBottomNavVisible(
107+
keyboardVisible: Boolean,
108+
commandOpen: Boolean,
109+
): Boolean = !keyboardVisible && !commandOpen
107110

108111
/** Main post-onboarding shell that owns top-level Android navigation state. */
109112
@Composable
@@ -342,14 +345,16 @@ private fun OverviewScreen(
342345
val cronStatus by viewModel.cronStatus.collectAsState()
343346
val nodesDevicesSummary by viewModel.nodesDevicesSummary.collectAsState()
344347
val channelsSummary by viewModel.channelsSummary.collectAsState()
345-
val readyProviderCount = providers.count { modelProviderReady(it.status) }
348+
val readyProviderCount = readyModelProviderCount(providers, models)
349+
val expiringProviderCount = expiringModelProviderCount(providers)
346350
val attentionRows =
347351
homeAttentionRows(
348352
isConnected = isConnected,
349353
pendingApprovals = pendingToolCalls.size,
350354
channelsSummary = channelsSummary,
351355
nodesDevicesSummary = nodesDevicesSummary,
352356
readyProviderCount = readyProviderCount,
357+
expiringProviderCount = expiringProviderCount,
353358
)
354359

355360
LaunchedEffect(isConnected) {
@@ -460,6 +465,7 @@ private fun OverviewScreen(
460465
when {
461466
!isConnected -> "Offline"
462467
readyProviderCount > 0 -> "$readyProviderCount ready"
468+
expiringProviderCount > 0 -> "$expiringProviderCount expiring"
463469
models.isNotEmpty() -> "${models.size} models"
464470
else -> "Setup"
465471
},
@@ -541,6 +547,7 @@ internal fun homeAttentionRows(
541547
channelsSummary: GatewayChannelsSummary,
542548
nodesDevicesSummary: GatewayNodesDevicesSummary,
543549
readyProviderCount: Int,
550+
expiringProviderCount: Int = 0,
544551
): List<HomeAttentionRow> =
545552
listOfNotNull(
546553
if (!isConnected) {
@@ -563,7 +570,12 @@ internal fun homeAttentionRows(
563570
} else {
564571
null
565572
},
566-
if (isConnected && readyProviderCount == 0) {
573+
if (isConnected && expiringProviderCount > 0) {
574+
HomeAttentionRow("Providers", "Provider auth expires soon", Icons.Outlined.Inventory2, Tab.ProvidersModels)
575+
} else {
576+
null
577+
},
578+
if (isConnected && readyProviderCount == 0 && expiringProviderCount == 0) {
567579
HomeAttentionRow("Providers", "No ready providers", Icons.Outlined.Inventory2, Tab.ProvidersModels)
568580
} else {
569581
null

0 commit comments

Comments
 (0)