diff --git a/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt b/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt index 8e1f4a560985..bf85fff4267f 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt @@ -289,8 +289,8 @@ internal object AndroidScreenshotFixture { add( chatMessage( "assistant", - "The main thread asks for a regression test around session restore, and the second one wants the new " + - "config key documented before merge. Both are small; I can draft patches for each if you want.", + "The release check is ready:\n\n```kotlin\nval ready = lint && tests\n```\n\n" + + "Review https://openclaw.ai before tagging.", 1_783_555_200_000, ), ) @@ -343,6 +343,13 @@ internal object AndroidScreenshotFixture { put("contextTokens", JsonPrimitive(200_000)) }, ) + put( + "inFlightRun", + buildJsonObject { + put("runId", JsonPrimitive("android-screenshot-active-run")) + put("text", JsonPrimitive("")) + }, + ) }.toString() private fun chatMessage( diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt deleted file mode 100644 index 8bd9baff18aa..000000000000 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt +++ /dev/null @@ -1,205 +0,0 @@ -package ai.openclaw.app.ui - -import ai.openclaw.app.R -import androidx.compose.runtime.Composable -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -internal data class MobileColors( - val surface: Color, - val surfaceStrong: Color, - val cardSurface: Color, - val border: Color, - val borderStrong: Color, - val text: Color, - val textSecondary: Color, - val textTertiary: Color, - val accent: Color, - val accentSoft: Color, - val accentBorderStrong: Color, - val success: Color, - val successSoft: Color, - val warning: Color, - val warningSoft: Color, - val danger: Color, - val dangerSoft: Color, - val codeBg: Color, - val codeText: Color, - val codeBorder: Color, - val codeAccent: Color, - // Syntax token colors; every value must keep >= 4.5:1 contrast against codeBg in its theme. - val codeKeyword: Color, - val codeString: Color, - val codeComment: Color, - val codeNumber: Color, - val chipBorderConnected: Color, - val chipBorderConnecting: Color, - val chipBorderWarning: Color, - val chipBorderError: Color, -) - -internal fun lightMobileColors() = - MobileColors( - surface = Color(0xFFFAFBFC), - surfaceStrong = Color(0xFFEFF3F8), - cardSurface = Color(0xFFFFFFFF), - border = Color(0xFFDDE3EC), - borderStrong = Color(0xFFC7D0DC), - text = Color(0xFF16181D), - textSecondary = Color(0xFF505B6A), - textTertiary = Color(0xFF8E98A7), - accent = Color(0xFF1B5ACB), - accentSoft = Color(0xFFEAF2FF), - accentBorderStrong = Color(0xFF174CA9), - success = Color(0xFF287F52), - successSoft = Color(0xFFEAF7F0), - warning = Color(0xFFAF7418), - warningSoft = Color(0xFFFFF4DF), - danger = Color(0xFFC94343), - dangerSoft = Color(0xFFFFECEC), - codeBg = Color(0xFFEFF3F8), - codeText = Color(0xFF172033), - codeBorder = Color(0xFFD7DDE7), - codeAccent = Color(0xFF287F52), - codeKeyword = Color(0xFF1B5ACB), - codeString = Color(0xFF1F6B45), - codeComment = Color(0xFF505B6A), - codeNumber = Color(0xFF8F5D10), - chipBorderConnected = Color(0xFFCFEBD8), - chipBorderConnecting = Color(0xFFD5E2FA), - chipBorderWarning = Color(0xFFEED8B8), - chipBorderError = Color(0xFFF3C8C8), - ) - -internal fun darkMobileColors() = - MobileColors( - surface = Color(0xFF1A1C20), - surfaceStrong = Color(0xFF24262B), - cardSurface = Color(0xFF1E2024), - border = Color(0xFF2E3038), - borderStrong = Color(0xFF3A3D46), - text = Color(0xFFE4E5EA), - textSecondary = Color(0xFFA0A6B4), - textTertiary = Color(0xFF6B7280), - accent = Color(0xFF6EA8FF), - accentSoft = Color(0xFF1A2A44), - accentBorderStrong = Color(0xFF5B93E8), - success = Color(0xFF5FBB85), - successSoft = Color(0xFF152E22), - warning = Color(0xFFE8A844), - warningSoft = Color(0xFF2E2212), - danger = Color(0xFFE87070), - dangerSoft = Color(0xFF2E1616), - codeBg = Color(0xFF111317), - codeText = Color(0xFFE8EAEE), - codeBorder = Color(0xFF2B2E35), - codeAccent = Color(0xFF3FC97A), - codeKeyword = Color(0xFF6EA8FF), - codeString = Color(0xFF3FC97A), - codeComment = Color(0xFFA0A6B4), - codeNumber = Color(0xFFE8A844), - chipBorderConnected = Color(0xFF1E4A30), - chipBorderConnecting = Color(0xFF1E3358), - chipBorderWarning = Color(0xFF3E3018), - chipBorderError = Color(0xFF3E1E1E), - ) - -// Defaulting to light tokens keeps previews/tests usable when a screen forgets to -// provide the app theme; production roots override this composition local. -internal val LocalMobileColors = staticCompositionLocalOf { lightMobileColors() } - -internal object MobileColorsAccessor { - val current: MobileColors - @Composable get() = LocalMobileColors.current -} - -// Keep these accessors while screens migrate to `MobileColorsAccessor.current`. -// Each getter must stay composable so callers always read the active theme. -internal val mobileSurface: Color @Composable get() = LocalMobileColors.current.surface -internal val mobileCardSurface: Color @Composable get() = LocalMobileColors.current.cardSurface -internal val mobileBorder: Color @Composable get() = LocalMobileColors.current.border -internal val mobileBorderStrong: Color @Composable get() = LocalMobileColors.current.borderStrong -internal val mobileText: Color @Composable get() = LocalMobileColors.current.text -internal val mobileTextSecondary: Color @Composable get() = LocalMobileColors.current.textSecondary -internal val mobileTextTertiary: Color @Composable get() = LocalMobileColors.current.textTertiary -internal val mobileAccent: Color @Composable get() = LocalMobileColors.current.accent -internal val mobileAccentSoft: Color @Composable get() = LocalMobileColors.current.accentSoft -internal val mobileAccentBorderStrong: Color @Composable get() = LocalMobileColors.current.accentBorderStrong -internal val mobileWarning: Color @Composable get() = LocalMobileColors.current.warning -internal val mobileWarningSoft: Color @Composable get() = LocalMobileColors.current.warningSoft -internal val mobileDanger: Color @Composable get() = LocalMobileColors.current.danger -internal val mobileDangerSoft: Color @Composable get() = LocalMobileColors.current.dangerSoft -internal val mobileCodeBg: Color @Composable get() = LocalMobileColors.current.codeBg -internal val mobileCodeText: Color @Composable get() = LocalMobileColors.current.codeText -internal val mobileCodeBorder: Color @Composable get() = LocalMobileColors.current.codeBorder - -// Build the page backdrop from semantic surfaces so light/dark palettes keep -// their contrast relationship without duplicating raw color stops. -internal val mobileBackgroundGradient: Brush - @Composable get() { - val colors = LocalMobileColors.current - return Brush.verticalGradient( - listOf( - colors.surface, - colors.surfaceStrong, - colors.surfaceStrong, - ), - ) - } - -internal val mobileFontFamily = - FontFamily( - Font(resId = R.font.manrope_400_regular, weight = FontWeight.Normal), - Font(resId = R.font.manrope_500_medium, weight = FontWeight.Medium), - Font(resId = R.font.manrope_600_semibold, weight = FontWeight.SemiBold), - Font(resId = R.font.manrope_700_bold, weight = FontWeight.Bold), - ) - -internal val mobileHeadline = - TextStyle( - fontFamily = mobileFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - lineHeight = 22.sp, - letterSpacing = (-0.1).sp, - ) - -internal val mobileBody = - TextStyle( - fontFamily = mobileFontFamily, - fontWeight = FontWeight.Medium, - fontSize = 15.sp, - lineHeight = 22.sp, - ) - -internal val mobileCallout = - TextStyle( - fontFamily = mobileFontFamily, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - ) - -internal val mobileCaption1 = - TextStyle( - fontFamily = mobileFontFamily, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.2.sp, - ) - -internal val mobileCaption2 = - TextStyle( - fontFamily = mobileFontFamily, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 14.sp, - letterSpacing = 0.4.sp, - ) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/OpenClawTheme.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/OpenClawTheme.kt index b6eb50242252..08a25ac70ff4 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/OpenClawTheme.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/OpenClawTheme.kt @@ -17,7 +17,7 @@ import androidx.core.view.WindowCompat internal val LocalResolvedAppearanceIsDark = staticCompositionLocalOf { false } /** - * App theme wrapper that installs dynamic Material colors and legacy mobile color tokens. + * App theme wrapper that resolves the requested appearance for system surfaces and child themes. */ @Composable fun OpenClawTheme( @@ -27,12 +27,10 @@ fun OpenClawTheme( val context = LocalContext.current val isDark = themeMode.isDark(systemDark = isSystemInDarkTheme()) val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - val mobileColors = if (isDark) darkMobileColors() else lightMobileColors() OpenClawSystemBarAppearance(lightAppearance = !isDark) CompositionLocalProvider( - LocalMobileColors provides mobileColors, LocalResolvedAppearanceIsDark provides isDark, ) { MaterialTheme(colorScheme = colorScheme, content = content) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMarkdown.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMarkdown.kt index 9d4b9c47fd4f..acff67eee255 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMarkdown.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMarkdown.kt @@ -3,12 +3,6 @@ package ai.openclaw.app.ui.chat import ai.openclaw.app.chat.CHAT_IMAGE_MAX_BASE64_CHARS import ai.openclaw.app.i18n.nativeString import ai.openclaw.app.ui.design.ClawTheme -import ai.openclaw.app.ui.mobileAccent -import ai.openclaw.app.ui.mobileCallout -import ai.openclaw.app.ui.mobileCaption1 -import ai.openclaw.app.ui.mobileCodeBg -import ai.openclaw.app.ui.mobileCodeText -import ai.openclaw.app.ui.mobileTextSecondary import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -120,7 +114,12 @@ fun ChatMarkdown( ) { val blocks = remember(text, isStreaming) { segmentChatMarkdown(text, isStreaming) } val inlineStyles = - InlineStyles(inlineCodeBg = mobileCodeBg, inlineCodeColor = mobileCodeText, linkColor = mobileAccent, baseCallout = mobileCallout) + InlineStyles( + inlineCodeBg = ClawTheme.colors.codeBg, + inlineCodeColor = ClawTheme.colors.codeText, + linkColor = ClawTheme.colors.accent, + baseCallout = ClawTheme.type.body, + ) Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { for (block in blocks) { @@ -225,7 +224,7 @@ private fun RenderCommonMarkBlock( Modifier .width(2.dp) .fillMaxHeight() - .background(mobileTextSecondary.copy(alpha = 0.35f)), + .background(ClawTheme.colors.textMuted.copy(alpha = 0.35f)), ) Column( modifier = Modifier.weight(1f), @@ -272,7 +271,7 @@ private fun RenderCommonMarkBlock( Modifier .fillMaxWidth() .height(1.dp) - .background(mobileTextSecondary.copy(alpha = 0.25f)), + .background(ClawTheme.colors.textMuted.copy(alpha = 0.25f)), ) } is HtmlBlock -> { @@ -290,7 +289,7 @@ private fun RenderLiteralHtml( if (literal.isNotEmpty()) { Text( text = literal, - style = mobileCallout.copy(fontFamily = FontFamily.Monospace), + style = ClawTheme.type.body.copy(fontFamily = FontFamily.Monospace), color = textColor, ) } @@ -326,11 +325,11 @@ private fun RenderMarkdownDisclosure( ) { Text( text = if (isExpanded) "▾" else "▸", - style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), + style = ClawTheme.type.body.copy(fontWeight = FontWeight.SemiBold), ) Text( text = summary, - style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), + style = ClawTheme.type.body.copy(fontWeight = FontWeight.SemiBold), color = textColor, ) } @@ -499,7 +498,7 @@ private fun RenderTableBlock( Modifier .fillMaxWidth() .horizontalScroll(scrollState) - .border(1.dp, mobileTextSecondary.copy(alpha = 0.25f)), + .border(1.dp, ClawTheme.colors.textMuted.copy(alpha = 0.25f)), ) { for (row in rows) { Row( @@ -509,11 +508,11 @@ private fun RenderTableBlock( val cell = row.cells.getOrNull(index) ?: AnnotatedString("") Text( text = cell, - style = if (row.isHeader) mobileCaption1.copy(fontWeight = FontWeight.SemiBold) else inlineStyles.baseCallout, + style = if (row.isHeader) ClawTheme.type.caption.copy(fontWeight = FontWeight.SemiBold) else inlineStyles.baseCallout, color = textColor, modifier = Modifier - .border(1.dp, mobileTextSecondary.copy(alpha = 0.22f)) + .border(1.dp, ClawTheme.colors.textMuted.copy(alpha = 0.22f)) .padding(horizontal = 8.dp, vertical = 6.dp) .width(160.dp), ) @@ -1213,8 +1212,8 @@ private fun InlineBase64Image( Text( text = nativeString("Image unavailable"), modifier = Modifier.padding(vertical = 2.dp), - style = mobileCaption1, - color = mobileTextSecondary, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, ) } } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt index 27a8bc4fe8c9..22ab10e79ae1 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageViews.kt @@ -9,26 +9,9 @@ import ai.openclaw.app.chat.normalizeVisibleChatMessageRole import ai.openclaw.app.gateway.GatewayLoadedImage import ai.openclaw.app.i18n.nativeString import ai.openclaw.app.i18n.nativeStringResource -import ai.openclaw.app.ui.MobileColorsAccessor import ai.openclaw.app.ui.design.ClawTheme import ai.openclaw.app.ui.image.RemoteImageResult import ai.openclaw.app.ui.image.safeRemoteImageStore -import ai.openclaw.app.ui.mobileAccent -import ai.openclaw.app.ui.mobileAccentBorderStrong -import ai.openclaw.app.ui.mobileAccentSoft -import ai.openclaw.app.ui.mobileBorder -import ai.openclaw.app.ui.mobileBorderStrong -import ai.openclaw.app.ui.mobileCallout -import ai.openclaw.app.ui.mobileCaption1 -import ai.openclaw.app.ui.mobileCaption2 -import ai.openclaw.app.ui.mobileCardSurface -import ai.openclaw.app.ui.mobileCodeBg -import ai.openclaw.app.ui.mobileCodeBorder -import ai.openclaw.app.ui.mobileCodeText -import ai.openclaw.app.ui.mobileDanger -import ai.openclaw.app.ui.mobileText -import ai.openclaw.app.ui.mobileTextSecondary -import ai.openclaw.app.ui.mobileWarning import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -94,8 +77,8 @@ private fun ChatBubbleContainer( ) { Surface( shape = RoundedCornerShape(12.dp), - border = BorderStroke(1.dp, borderColor ?: if (user) mobileAccentBorderStrong else mobileBorderStrong), - color = if (user) mobileAccentSoft else mobileCardSurface, + border = BorderStroke(1.dp, borderColor ?: if (user) ClawTheme.colors.accentBorder else ClawTheme.colors.borderStrong), + color = if (user) ClawTheme.colors.accentSoft else ClawTheme.colors.surfaceRaised, tonalElevation = 0.dp, shadowElevation = 0.dp, modifier = @@ -147,8 +130,8 @@ private fun ChatLinkPreview( Surface( onClick = { expanded = true }, shape = RoundedCornerShape(8.dp), - color = mobileCardSurface, - border = BorderStroke(1.dp, mobileBorder), + color = ClawTheme.colors.surfaceRaised, + border = BorderStroke(1.dp, ClawTheme.colors.border), ) { Row( modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), @@ -157,8 +140,8 @@ private fun ChatLinkPreview( ) { Text( text = nativeString("Preview · \$domain", domain), - style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold), - color = mobileTextSecondary, + style = ClawTheme.type.caption.copy(fontWeight = FontWeight.SemiBold), + color = ClawTheme.colors.textMuted, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -166,7 +149,7 @@ private fun ChatLinkPreview( androidx.compose.material3.Icon( imageVector = Icons.Default.ExpandMore, contentDescription = nativeString("Expand link preview"), - tint = mobileTextSecondary, + tint = ClawTheme.colors.textMuted, ) } } @@ -190,8 +173,8 @@ private fun ChatLinkPreview( Surface( onClick = { uriHandler.openUri(url) }, shape = cardShape, - color = mobileCardSurface, - border = BorderStroke(1.dp, mobileBorder), + color = ClawTheme.colors.surfaceRaised, + border = BorderStroke(1.dp, ClawTheme.colors.border), ) { Column(modifier = Modifier.fillMaxWidth()) { previewImage?.let { image -> @@ -206,16 +189,16 @@ private fun ChatLinkPreview( modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(3.dp), ) { - Text(domain, style = mobileCaption2, color = mobileTextSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(domain, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis) when (val preview = result) { - null -> Text(nativeString("Loading preview…"), style = mobileCaption1, color = mobileTextSecondary) - LinkPreviewResult.Failed -> Text(nativeString("No preview available"), style = mobileCallout, color = mobileTextSecondary) + null -> Text(nativeString("Loading preview…"), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted) + LinkPreviewResult.Failed -> Text(nativeString("No preview available"), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted) is LinkPreviewResult.Loaded -> { preview.metadata.title?.let { title -> Text( text = title, - style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), - color = mobileText, + style = ClawTheme.type.body.copy(fontWeight = FontWeight.SemiBold), + color = ClawTheme.colors.text, maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -223,8 +206,8 @@ private fun ChatLinkPreview( preview.metadata.description?.let { description -> Text( text = description, - style = mobileCaption1, - color = mobileTextSecondary, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -262,17 +245,17 @@ fun ChatTypingIndicatorBubble( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - WorkingClawIcon(runKey = runKey, color = mobileAccent) + WorkingClawIcon(runKey = runKey, color = ClawTheme.colors.accent) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - Text(formatLocalizedChatDurationCompact(elapsedMs), style = mobileCallout, color = mobileTextSecondary) + Text(formatLocalizedChatDurationCompact(elapsedMs), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted) tokens?.let { - Text(nativeStringResource("·"), style = mobileCallout, color = mobileTextSecondary) - Text(it, style = mobileCallout, color = mobileTextSecondary) + Text(nativeStringResource("·"), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted) + Text(it, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted) } - phrase?.let { Text(nativeStringResource("· \$phrase", it), style = mobileCallout, color = mobileTextSecondary) } + phrase?.let { Text(nativeStringResource("· \$phrase", it), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted) } } } } @@ -287,7 +270,7 @@ fun ChatOutboxBubble( onDelete: () -> Unit, ) { val failed = item.status == ChatOutboxStatus.Failed - val statusColor = if (failed) mobileDanger else mobileWarning + val statusColor = if (failed) ClawTheme.colors.danger else ClawTheme.colors.warning val statusLabel = when (item.status) { ChatOutboxStatus.Queued -> nativeString("Queued — sends when reconnected") @@ -314,13 +297,13 @@ fun ChatOutboxBubble( borderColor = statusColor.copy(alpha = 0.6f), ) { if (item.text.isNotBlank()) { - ChatMarkdown(text = item.text, textColor = mobileText) + ChatMarkdown(text = item.text, textColor = ClawTheme.colors.text) } item.attachments.forEach { attachment -> Text( text = nativeString("📎 \${attachment.fileName}", attachment.fileName), - style = mobileCaption1, - color = mobileTextSecondary, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, ) } Row( @@ -329,17 +312,17 @@ fun ChatOutboxBubble( ) { Text( text = statusLabel, - style = mobileCaption1, + style = ClawTheme.type.caption, color = statusColor, modifier = Modifier.weight(1f), ) if (failed && retryEnabled) { - ChatOutboxAction(label = nativeString("Retry"), color = mobileAccent, onClick = onRetry) + ChatOutboxAction(label = nativeString("Retry"), color = ClawTheme.colors.accent, onClick = onRetry) } // Sending rows are mid-dispatch and accepted rows may already be delivered; both stay // action-free until reconciliation resolves them, so a delete can never race a send. if (item.status == ChatOutboxStatus.Queued || failed) { - ChatOutboxAction(label = nativeString("Delete"), color = mobileTextSecondary, onClick = onDelete) + ChatOutboxAction(label = nativeString("Delete"), color = ClawTheme.colors.textMuted, onClick = onDelete) } } } @@ -360,7 +343,7 @@ private fun ChatOutboxAction( ) { Text( text = label, - style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold), + style = ClawTheme.type.caption.copy(fontWeight = FontWeight.SemiBold), modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), ) } @@ -377,7 +360,7 @@ internal fun ChatBase64Image( if (image != null) { ChatImagePreview(image = image, description = mimeType ?: nativeString("Attachment"), stateKey = base64) } else if (imageState.failed) { - Text(nativeString("Unsupported attachment"), style = mobileCaption1, color = mobileTextSecondary) + Text(nativeString("Unsupported attachment"), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted) } } @@ -414,23 +397,23 @@ internal fun ChatManagedImage( Surface( onClick = { retryGeneration += 1 }, shape = RoundedCornerShape(10.dp), - border = BorderStroke(1.dp, mobileBorder), - color = mobileCardSurface, + border = BorderStroke(1.dp, ClawTheme.colors.border), + color = ClawTheme.colors.surfaceRaised, modifier = Modifier.fillMaxWidth(), ) { Text( nativeString("Image unavailable · Tap to retry"), modifier = Modifier.padding(12.dp), - style = mobileCaption1, - color = mobileTextSecondary, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, ) } else -> Text( nativeString("Loading image…"), modifier = Modifier.padding(12.dp), - style = mobileCaption1, - color = mobileTextSecondary, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, ) } } @@ -445,8 +428,8 @@ private fun ChatImagePreview( Surface( onClick = { previewVisible = true }, shape = RoundedCornerShape(10.dp), - border = BorderStroke(1.dp, mobileBorder), - color = mobileCardSurface, + border = BorderStroke(1.dp, ClawTheme.colors.border), + color = ClawTheme.colors.surfaceRaised, modifier = Modifier.fillMaxWidth(), ) { Box { @@ -515,14 +498,14 @@ fun ChatCodeBlock( isComplete: Boolean = true, ) { val display = code.trimEnd() - // Token colors come from the theme's code palette so light/dark both keep readable contrast. - val palette = MobileColorsAccessor.current + // Syntax roles reuse semantic colors that keep at least 4.5:1 contrast against codeBg; + // changing these mappings can make highlighted code less readable than plain code. val tokenColors = CodeTokenColors( - keyword = palette.codeKeyword, - string = palette.codeString, - comment = palette.codeComment, - number = palette.codeNumber, + keyword = ClawTheme.colors.accent, + string = ClawTheme.colors.success, + comment = ClawTheme.colors.textMuted, + number = ClawTheme.colors.danger, ) // Keyed on content: streaming re-renders of unchanged blocks reuse the tokenized result, // and still-open fences stay plain until the closing fence arrives. @@ -532,23 +515,23 @@ fun ChatCodeBlock( } Surface( shape = RoundedCornerShape(8.dp), - color = mobileCodeBg, - border = BorderStroke(1.dp, mobileCodeBorder), + color = ClawTheme.colors.codeBg, + border = BorderStroke(1.dp, ClawTheme.colors.codeBorder), modifier = Modifier.fillMaxWidth(), ) { Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { if (!language.isNullOrBlank()) { Text( text = language.uppercase(Locale.US), - style = mobileCaption2.copy(letterSpacing = 0.4.sp), - color = mobileTextSecondary, + style = ClawTheme.type.caption.copy(letterSpacing = 0.4.sp), + color = ClawTheme.colors.textMuted, ) } Text( text = highlighted, fontFamily = FontFamily.Monospace, - style = mobileCallout, - color = mobileCodeText, + style = ClawTheme.type.body, + color = ClawTheme.colors.codeText, ) } } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt index 86459b6aa1e4..417e1b63d603 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt @@ -63,7 +63,6 @@ import ai.openclaw.app.ui.design.agentAvatarSource import ai.openclaw.app.ui.gatewayDiagnosticsEndpoint import ai.openclaw.app.ui.gatewayStatusForDisplay import ai.openclaw.app.ui.localizedUppercase -import ai.openclaw.app.ui.mobileCallout import ai.openclaw.app.ui.relativeSessionTime import ai.openclaw.app.ui.rememberSystemAnimationsEnabled import android.os.SystemClock @@ -1922,7 +1921,7 @@ private fun ChatUserMessageText( if (preview != null && !expanded) { Text( text = preview, - style = mobileCallout, + style = ClawTheme.type.body, color = ClawTheme.colors.text, ) } else { @@ -1944,7 +1943,7 @@ private fun ChatUserMessageText( ) { Text( text = toggleLabel, - style = mobileCallout.copy(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold), + style = ClawTheme.type.body.copy(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold), modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), ) } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawTheme.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawTheme.kt index 22b973308ccc..112c1c562989 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawTheme.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawTheme.kt @@ -1,9 +1,6 @@ package ai.openclaw.app.ui.design -import ai.openclaw.app.ui.LocalMobileColors -import ai.openclaw.app.ui.darkMobileColors -import ai.openclaw.app.ui.lightMobileColors -import ai.openclaw.app.ui.mobileFontFamily +import ai.openclaw.app.R import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Shapes import androidx.compose.material3.Typography @@ -16,12 +13,21 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +internal val clawFontFamily = + FontFamily( + Font(resId = R.font.manrope_400_regular, weight = FontWeight.Normal), + Font(resId = R.font.manrope_500_medium, weight = FontWeight.Medium), + Font(resId = R.font.manrope_600_semibold, weight = FontWeight.SemiBold), + Font(resId = R.font.manrope_700_bold, weight = FontWeight.Bold), + ) + /** * App color tokens consumed by ClawTheme and bridged into Material components. */ @@ -31,6 +37,7 @@ internal data class ClawColors( val surface: Color, val surfaceRaised: Color, val surfacePressed: Color, + val accent: Color, val accentSoft: Color, val accentBorder: Color, val border: Color, @@ -46,6 +53,9 @@ internal data class ClawColors( val warningSoft: Color, val danger: Color, val dangerSoft: Color, + val codeBg: Color, + val codeText: Color, + val codeBorder: Color, ) /** @@ -97,6 +107,7 @@ private val ClawDarkColors = surface = Color(0xFF0A0A0A), surfaceRaised = Color(0xFF111111), surfacePressed = Color(0xFF1A1A1A), + accent = Color(0xFF6EA8FF), accentSoft = Color(0xFF1A2A44), accentBorder = Color(0xFF5B93E8), border = Color(0xFF242424), @@ -112,6 +123,9 @@ private val ClawDarkColors = warningSoft = Color(0xFF2B2412), danger = Color(0xFFFF6B6B), dangerSoft = Color(0xFF2C1414), + codeBg = Color(0xFF111317), + codeText = Color(0xFFE8EAEE), + codeBorder = Color(0xFF2B2E35), ) private val ClawLightColors = @@ -120,6 +134,7 @@ private val ClawLightColors = surface = Color(0xFFFFFEFB), surfaceRaised = Color(0xFFFFFFFF), surfacePressed = Color(0xFFE9EDF3), + accent = Color(0xFF1B5ACB), accentSoft = Color(0xFFEAF2FF), accentBorder = Color(0xFF174CA9), border = Color(0xFFDDE3EC), @@ -135,12 +150,15 @@ private val ClawLightColors = warningSoft = Color(0xFFFFF3DC), danger = Color(0xFFB82929), dangerSoft = Color(0xFFFFE9E9), + codeBg = Color(0xFFEFF3F8), + codeText = Color(0xFF172033), + codeBorder = Color(0xFFD7DDE7), ) private val LocalClawColors = staticCompositionLocalOf { ClawDarkColors } private val LocalClawSpacing = staticCompositionLocalOf { ClawSpacing() } private val LocalClawRadii = staticCompositionLocalOf { ClawRadii() } -private val LocalClawTypography = staticCompositionLocalOf { clawTypography(mobileFontFamily) } +private val LocalClawTypography = staticCompositionLocalOf { clawTypography(clawFontFamily) } /** * Composition-local access point for OpenClaw Android design tokens. @@ -176,12 +194,10 @@ internal fun ClawDesignTheme( content: @Composable () -> Unit, ) { val colors = if (dark) ClawDarkColors else ClawLightColors - val mobileColors = if (dark) darkMobileColors() else lightMobileColors() - val typography = clawTypography(mobileFontFamily) + val typography = clawTypography(clawFontFamily) CompositionLocalProvider( LocalClawColors provides colors, - LocalMobileColors provides mobileColors, LocalClawSpacing provides ClawSpacing(), LocalClawRadii provides ClawRadii(), LocalClawTypography provides typography, diff --git a/apps/android/app/src/test/java/ai/openclaw/app/AndroidScreenshotFixtureTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/AndroidScreenshotFixtureTest.kt index 3d1578cfaec3..5164f793447c 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/AndroidScreenshotFixtureTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/AndroidScreenshotFixtureTest.kt @@ -99,12 +99,11 @@ class AndroidScreenshotFixtureTest { @Test fun providesDeterministicChatHistory() { - val messages = + val history = json .parseToJsonElement(AndroidScreenshotFixture.request("chat.history", null)) - .jsonObject["messages"] - ?.jsonArray - .orEmpty() + .jsonObject + val messages = history["messages"]?.jsonArray.orEmpty() assertEquals( listOf( @@ -120,8 +119,8 @@ class AndroidScreenshotFixtureTest { listOf("user", "Summarize the open review feedback for me.", "1783555140000"), listOf( "assistant", - "The main thread asks for a regression test around session restore, and the second one wants the new " + - "config key documented before merge. Both are small; I can draft patches for each if you want.", + "The release check is ready:\n\n```kotlin\nval ready = lint && tests\n```\n\n" + + "Review https://openclaw.ai before tagging.", "1783555200000", ), listOf("system", "Compaction", "1783555220000"), @@ -157,6 +156,9 @@ class AndroidScreenshotFixtureTest { val reset = messages[7].jsonObject["__openclaw"]?.jsonObject assertEquals("reset", reset?.get("kind")?.jsonPrimitive?.content) assertEquals("android-screenshot-reset", reset?.get("id")?.jsonPrimitive?.content) + val inFlightRun = history["inFlightRun"]?.jsonObject + assertEquals("android-screenshot-active-run", inFlightRun?.get("runId")?.jsonPrimitive?.content) + assertEquals("", inFlightRun?.get("text")?.jsonPrimitive?.content) } @Test