ui: input bar reasoning chip tweaks (#673)

* ui: input bar reasoning chip tweaks

- Remove emoji from reasoning level labels (dropdown + chip text)
- Reduce reasoning chip horizontal padding (8dp -> 4dp)
- Move reasoning chip to right side (after spacer, next to mic)
- Increase model chip minimum width (120dp -> 140dp)
- Remove unused width import

* fix(ui): add missing widthIn import in ComposerToolbar

Resolves 'Unresolved reference widthIn' compile error at
ComposerToolbar.kt:95 that broke CI (unit-tests, android-lint,
instrumented-tests all fail on compileDebugKotlin).

* ui(chat): enhance input bar layout, reasoning menu header, and card shadow

Give ChatInputBar a floating rounded container with shadow contrast. Make model chip take available space and add header to reasoning menu.

* fix(ui): remove trailing blank line in ComposerToolbar for ktlint

Fix standard:no-consecutive-blank-lines ktlint violation.

* ui(model): enhance ModelPickerDialog with rounded cards and provider badges

Apply card styling, rounded corners (24dp dialog, 12dp cards), provider badges, and shadow contrast to ModelPickerDialog.

* feat(model): add in-dialog model pinning to ModelPickerDialog

Add pin/unpin icon buttons to model cards in ModelPickerDialog across both ModelScreen and ChatScreen.

* ui(model): remove provider badge chip and eliminate bottom whitespace in ModelPickerDialog

Replace bottom Cancel button with top-right Close icon to eliminate bottom whitespace. Remove inline provider badge chip from model cards.

* perf(model): optimize ModelPickerDialog load time with lazy items and O(1) set lookup

Lazy-register individual model items in LazyColumn and memoize pinnedSet to prevent synchronous rendering lag when opening dialog.

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-22 14:21:06 +03:00
1 parent 56de3323f0
commit 0bb418b4a7
7 files changed
+255 -86

No files matched your search

@@ -563,6 +563,7 @@ fun ChatScreen(
title = "Switch model (this chat)",
isLoading = state.modelPickerLoading && state.modelPickerProviders.isEmpty(),
pinnedModels = state.modelPickerPinned,
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
onSelect = { provider, model ->
viewModel.sendSlashModel(provider, model)
},
@@ -1286,6 +1286,21 @@ class ChatViewModel(
_uiState.update { it.copy(showModelPicker = false, modelPickerLoading = false) }
}
fun togglePinModel(
providerSlug: String,
modelName: String,
) {
val currentPinned = AuthManager.getPinnedModels().toMutableList()
val target = PinnedModel(providerSlug, modelName)
if (currentPinned.contains(target)) {
currentPinned.remove(target)
} else {
currentPinned.add(target)
}
AuthManager.savePinnedModels(currentPinned)
_uiState.update { it.copy(modelPickerPinned = currentPinned) }
}
/**
* Hot-swap the CURRENT session's model via the /model slash command.
*
@@ -105,10 +105,19 @@ fun ChatInputBar(
exit = slideOutVertically(targetOffsetY = { it }),
) {
Surface(
modifier = Modifier.fillMaxWidth(),
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 3.dp,
shadowElevation = 0.dp,
border =
BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
),
tonalElevation = 2.dp,
shadowElevation = 4.dp,
) {
Column {
// Commands hidden from the suggestion menu — desktop/CLI-only and
@@ -4,11 +4,10 @@ import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachFile
@@ -17,6 +16,7 @@ import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
@@ -36,7 +36,7 @@ import androidx.compose.ui.unit.dp
/**
* Bottom toolbar row for the chat composer.
*
* Layout: [📎 attach] [model chip] [reasoning chip] [←spacer→] [🎙 mic]
* Layout: [📎 attach] [model chip] [←spacer→] [🧠 reasoning] [🎙 mic]
*
* The reasoning chip opens a dropdown menu to pick a level (instead of cycling).
*/
@@ -76,7 +76,7 @@ fun ComposerToolbar(
)
}
// Model chip — fixed width, text scrolls if too long
// Model chip — takes available space, fixed height
FilterChip(
selected = currentSessionModel != null,
onClick = onModelTap,
@@ -93,11 +93,12 @@ fun ComposerToolbar(
},
modifier =
Modifier
.width(120.dp)
.weight(1f)
.height(28.dp)
.testTag("model_chip"),
)
// Reasoning chip with dropdown menu
// Reasoning chip with dropdown menu (right side, next to mic)
Box {
FilterChip(
selected = reasoningLevel != null,
@@ -110,23 +111,33 @@ fun ComposerToolbar(
overflow = TextOverflow.Ellipsis,
)
},
modifier = Modifier.testTag("reasoning_chip"),
modifier =
Modifier
.height(28.dp)
.testTag("reasoning_chip"),
)
DropdownMenu(
expanded = showReasoningMenu,
onDismissRequest = { showReasoningMenu = false },
) {
Text(
text = "Reasoning",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
)
HorizontalDivider()
val allLevels =
listOf(
"none" to "🧠 None",
"minimal" to "🧠 Minimal",
"low" to "🧠 Low",
"medium" to "🧠 Med",
"high" to "🧠 High",
"xhigh" to "🧠 XHigh",
"max" to "🧠 Max",
"ultra" to "🧠 Ultra",
"none" to "None",
"minimal" to "Minimal",
"low" to "Low",
"medium" to "Med",
"high" to "High",
"xhigh" to "XHigh",
"max" to "Max",
"ultra" to "Ultra",
)
allLevels.forEach { (level, label) ->
DropdownMenuItem(
@@ -156,8 +167,6 @@ fun ComposerToolbar(
}
}
Spacer(modifier = Modifier.weight(1f))
// Mic / Stop button
IconButton(
onClick = onMicTap,
@@ -189,19 +198,19 @@ fun ComposerToolbar(
*
* @param level One of: "none", "minimal", "low", "medium", "high",
* "xhigh", "max", "ultra", or null for model default.
* @return Display string such as "🧠 None", "🧠 Low", "🧠 XHigh", "🧠 Ultra", etc.
* @return Display string such as "None", "Low", "XHigh", "Ultra", etc.
*/
private fun buildReasoningLabel(level: String?): String {
return when (level) {
null -> "🧠 Med"
"none" -> "🧠 None"
"minimal" -> "🧠 Minimal"
"low" -> "🧠 Low"
"medium" -> "🧠 Med"
"high" -> "🧠 High"
"xhigh" -> "🧠 XHigh"
"max" -> "🧠 Max"
"ultra" -> "🧠 Ultra"
else -> "🧠 $level"
null -> "Med"
"none" -> "None"
"minimal" -> "Minimal"
"low" -> "Low"
"medium" -> "Med"
"high" -> "High"
"xhigh" -> "XHigh"
"max" -> "Max"
"ultra" -> "Ultra"
else -> level
}
}