Skip to content

Commit f5aa1b6

Browse files
authored
Merge branch 'main' into codex/fix-cron-legacy-migration
2 parents ee962f7 + 4752e9a commit f5aa1b6

10,447 files changed

Lines changed: 34858 additions & 6609 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ jobs:
9292
for attempt in 1 2 3; do
9393
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
9494
-c protocol.version=2 \
95-
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
95+
fetch --no-tags --prune --no-recurse-submodules --depth=2 origin \
9696
"+${ref}:refs/remotes/origin/checkout" && return 0
9797
fetch_status="$?"
9898
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
@@ -146,12 +146,12 @@ jobs:
146146
147147
if [ "${{ github.event_name }}" = "push" ]; then
148148
BASE="${{ github.event.before }}"
149+
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
149150
else
150151
BASE="${{ github.event.pull_request.base.sha }}"
152+
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD --merge-head-first-parent
151153
fi
152154
153-
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
154-
155155
- name: Build CI manifest
156156
id: manifest
157157
env:

.github/workflows/opengrep-precise.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
uses: actions/checkout@v6
4545
with:
4646
ref: ${{ github.sha }}
47-
fetch-depth: 1
47+
fetch-depth: 2
4848
fetch-tags: false
4949
persist-credentials: false
5050
submodules: false
@@ -74,6 +74,7 @@ jobs:
7474
- name: Run opengrep on PR diff
7575
env:
7676
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
77+
OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT: "1"
7778
# Findings from precise rules block this workflow. Pull requests scan
7879
# changed first-party source paths only so findings stay attributable to
7980
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`

.github/workflows/tui-pty.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ env:
2727
jobs:
2828
tui-pty:
2929
runs-on: ubuntu-24.04
30-
timeout-minutes: 5
30+
timeout-minutes: 8
31+
env:
32+
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1"
3133
steps:
3234
- name: Checkout
3335
uses: actions/checkout@v6
@@ -38,4 +40,4 @@ jobs:
3840
install-bun: "false"
3941

4042
- name: Run TUI PTY tests
41-
run: timeout --kill-after=30s 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
43+
run: timeout --kill-after=30s 240s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts

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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,15 @@ private fun CommandSectionLabel(title: String) {
297297
}
298298
}
299299

300-
/** Builds provider quick-action metadata from current gateway/catalog state. */
301-
private fun providerCommandSubtitle(
300+
internal fun providerCommandSubtitle(
302301
isConnected: Boolean,
303302
providers: List<GatewayModelProviderSummary>,
304303
models: List<GatewayModelSummary>,
305304
): String {
306305
if (!isConnected) return "Connect Gateway to load models"
307-
val readyProviderCount = providers.count { modelProviderReady(it.status) }
306+
val expiringProviderCount = expiringModelProviderCount(providers)
307+
if (expiringProviderCount > 0) return "$expiringProviderCount providers expiring"
308+
val readyProviderCount = readyModelProviderCount(providers, models)
308309
if (readyProviderCount > 0) return "$readyProviderCount providers ready"
309310
if (models.isNotEmpty()) return "${models.size} models available"
310311
return "Configure model access"

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

Lines changed: 106 additions & 17 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?.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. */
264305
internal 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. */
274339
private 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
493581
private 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

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,14 +342,16 @@ private fun OverviewScreen(
342342
val cronStatus by viewModel.cronStatus.collectAsState()
343343
val nodesDevicesSummary by viewModel.nodesDevicesSummary.collectAsState()
344344
val channelsSummary by viewModel.channelsSummary.collectAsState()
345-
val readyProviderCount = providers.count { modelProviderReady(it.status) }
345+
val readyProviderCount = readyModelProviderCount(providers, models)
346+
val expiringProviderCount = expiringModelProviderCount(providers)
346347
val attentionRows =
347348
homeAttentionRows(
348349
isConnected = isConnected,
349350
pendingApprovals = pendingToolCalls.size,
350351
channelsSummary = channelsSummary,
351352
nodesDevicesSummary = nodesDevicesSummary,
352353
readyProviderCount = readyProviderCount,
354+
expiringProviderCount = expiringProviderCount,
353355
)
354356

355357
LaunchedEffect(isConnected) {
@@ -460,6 +462,7 @@ private fun OverviewScreen(
460462
when {
461463
!isConnected -> "Offline"
462464
readyProviderCount > 0 -> "$readyProviderCount ready"
465+
expiringProviderCount > 0 -> "$expiringProviderCount expiring"
463466
models.isNotEmpty() -> "${models.size} models"
464467
else -> "Setup"
465468
},
@@ -541,6 +544,7 @@ internal fun homeAttentionRows(
541544
channelsSummary: GatewayChannelsSummary,
542545
nodesDevicesSummary: GatewayNodesDevicesSummary,
543546
readyProviderCount: Int,
547+
expiringProviderCount: Int = 0,
544548
): List<HomeAttentionRow> =
545549
listOfNotNull(
546550
if (!isConnected) {
@@ -563,7 +567,12 @@ internal fun homeAttentionRows(
563567
} else {
564568
null
565569
},
566-
if (isConnected && readyProviderCount == 0) {
570+
if (isConnected && expiringProviderCount > 0) {
571+
HomeAttentionRow("Providers", "Provider auth expires soon", Icons.Outlined.Inventory2, Tab.ProvidersModels)
572+
} else {
573+
null
574+
},
575+
if (isConnected && readyProviderCount == 0 && expiringProviderCount == 0) {
567576
HomeAttentionRow("Providers", "No ready providers", Icons.Outlined.Inventory2, Tab.ProvidersModels)
568577
} else {
569578
null

0 commit comments

Comments
 (0)