@@ -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
197200private 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. */
206212private 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?.warning == true -> " 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?.warning == true -> " 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. */
264305internal fun modelProviderReady (status : String ): Boolean {
265306 val normalized = status.trim().lowercase()
@@ -270,6 +311,30 @@ internal fun modelProviderReady(status: String): Boolean {
270311 normalized == " static"
271312}
272313
314+ private fun modelProviderExpiring (status : String ): Boolean = status.trim().lowercase() == " expiring"
315+
316+ internal fun readyModelProviderCount (
317+ providers : List <GatewayModelProviderSummary >,
318+ models : List <GatewayModelSummary >,
319+ ): Int {
320+ val authReadyProviders = providers.filter { modelProviderReady(it.status) }.map { it.id.normalizedProviderId() }
321+ val availableModelProviders = models.filter(::modelAvailabilityUsable).map { it.provider.normalizedProviderId() }
322+ return (authReadyProviders + availableModelProviders).distinct().size
323+ }
324+
325+ // Older gateways did not emit `available`; keep those rows on the legacy
326+ // readiness path while still honoring explicit false from upgraded gateways.
327+ internal fun modelAvailabilityUsable (model : GatewayModelSummary ): Boolean = model.available != false
328+
329+ internal fun expiringModelProviderCount (providers : List <GatewayModelProviderSummary >): Int =
330+ providers
331+ .filter { modelProviderExpiring(it.status) }
332+ .map { it.id.normalizedProviderId() }
333+ .distinct()
334+ .size
335+
336+ private fun String.normalizedProviderId (): String = trim().lowercase()
337+
273338/* * Groups models by provider using the same display priority as provider rows. */
274339private fun sortedModelGroups (models : List <GatewayModelSummary >): List <Pair <String , List <GatewayModelSummary >>> =
275340 models
@@ -299,7 +364,18 @@ private fun ProviderList(
299364 ClawPanel (contentPadding = PaddingValues (horizontal = 0 .dp, vertical = 0 .dp)) {
300365 Column {
301366 if (rows.isEmpty()) {
302- ProviderListRow (ProviderRow (id = " loading" , name = " Provider catalog" , status = if (refreshing) " Loading" else " No providers" , ready = false , modelCount = 0 ))
367+ ProviderListRow (
368+ ProviderRow (
369+ id = " loading" ,
370+ name = " Provider catalog" ,
371+ status = if (refreshing) " Loading" else " No providers" ,
372+ ready = false ,
373+ available = false ,
374+ setupRequired = false ,
375+ warning = false ,
376+ modelCount = 0 ,
377+ ),
378+ )
303379 } else {
304380 val visibleRows = rows.take(5 )
305381 visibleRows.forEachIndexed { index, row ->
@@ -322,12 +398,12 @@ private fun ProviderOverviewPanel(
322398 onRefresh : () -> Unit ,
323399 onSetup : () -> Unit ,
324400) {
325- val readyCount = providerRows.count { it.ready }
326- val needsSetupCount = providerRows.count { ! it.ready }
401+ val readyCount = providerRows.count { it.available }
402+ val needsSetupCount = providerRows.count { it.setupRequired }
327403 ClawPanel (contentPadding = PaddingValues (horizontal = 12 .dp, vertical = 12 .dp)) {
328404 Column (verticalArrangement = Arrangement .spacedBy(10 .dp)) {
329405 Row (modifier = Modifier .fillMaxWidth(), horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
330- ProviderMetricTile (label = " Ready " , value = readyCount.toString(), modifier = Modifier .weight(1f ))
406+ ProviderMetricTile (label = " Available " , value = readyCount.toString(), modifier = Modifier .weight(1f ))
331407 ProviderMetricTile (label = " Models" , value = modelCount.toString(), modifier = Modifier .weight(1f ))
332408 ProviderMetricTile (label = " Setup" , value = needsSetupCount.toString(), modifier = Modifier .weight(1f ))
333409 }
@@ -398,8 +474,14 @@ private fun ProviderSetupListRow(
398474 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 )
399475 }
400476 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 )
477+ val statusColor =
478+ when {
479+ row.warning -> ClawTheme .colors.warning
480+ row.ready || row.available -> ClawTheme .colors.success
481+ else -> ClawTheme .colors.textMuted
482+ }
483+ Box (modifier = Modifier .size(5 .dp).clip(CircleShape ).background(statusColor))
484+ Text (text = row.statusLabel, style = ClawTheme .type.caption.copy(fontSize = 12.5 .sp, lineHeight = 16 .sp), color = ClawTheme .colors.textMuted, maxLines = 1 )
403485 Icon (imageVector = Icons .AutoMirrored .Filled .KeyboardArrowRight , contentDescription = " Open ${row.name} " , modifier = Modifier .size(17 .dp), tint = ClawTheme .colors.text)
404486 }
405487 }
@@ -415,7 +497,13 @@ private fun ProviderListRow(row: ProviderRow) {
415497 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 )
416498 }
417499 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))
500+ val statusColor =
501+ when {
502+ row.warning || row.setupRequired -> ClawTheme .colors.warning
503+ row.ready || row.available -> ClawTheme .colors.success
504+ else -> ClawTheme .colors.textMuted
505+ }
506+ Box (modifier = Modifier .size(4.5 .dp).clip(CircleShape ).background(statusColor))
419507 Text (text = row.status, style = ClawTheme .type.caption.copy(fontSize = 12.5 .sp, lineHeight = 16 .sp), color = ClawTheme .colors.textMuted, maxLines = 1 )
420508 }
421509 }
@@ -491,12 +579,13 @@ private fun ModelGroup(
491579
492580@Composable
493581private fun ModelRow (model : GatewayModelSummary ) {
582+ val available = modelAvailabilityUsable(model)
494583 Row (modifier = Modifier .fillMaxWidth().heightIn(min = 48 .dp).padding(horizontal = 10 .dp, vertical = 5 .dp), verticalAlignment = Alignment .CenterVertically , horizontalArrangement = Arrangement .spacedBy(6 .dp)) {
495584 Text (text = model.name, style = ClawTheme .type.mono, color = ClawTheme .colors.text, modifier = Modifier .weight(1f ), maxLines = 1 , overflow = TextOverflow .Ellipsis )
496585 modelCapabilityLabels(model).take(3 ).forEach { label ->
497586 ProviderMiniTag (text = label)
498587 }
499- Box (modifier = Modifier .size(4.5 .dp).clip(CircleShape ).background(ClawTheme .colors.success))
588+ Box (modifier = Modifier .size(4.5 .dp).clip(CircleShape ).background(if (available) ClawTheme .colors.success else ClawTheme .colors.warning ))
500589 }
501590}
502591
0 commit comments