Compare commits

..

2 commits

Author SHA1 Message Date
Bruno Deanoz
163a2cee28 fix 3 bugs: pills with keyboard, token count, chat model
1. Mode pills stay visible when keyboard open (were hidden).
   Spacing reduced (6dp vs 24dp) to keep dock compact.

2. Token counter works for loaded chats — sum inputTokens +
   outputTokens from all StoredMessages when fetching from
   server. StoredMessage now parses model/inputTokens/outputTokens.

3. Model pill shows the chat's actual model (from last assistant
   message) instead of always the global default. chatModel state
   tracks per-conversation model, falls back to session default
   for new chats.
2026-06-22 01:27:29 +02:00
Bruno Deanoz
0db20bfda8 docs: document keyboard handling approach (offset, not imePadding) 2026-06-22 01:23:30 +02:00
3 changed files with 26 additions and 19 deletions

View file

@ -231,7 +231,7 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
- Mesh gradient background with dithered blobs - Mesh gradient background with dithered blobs
- Phase-aware haptics (click → thinkingStart → responseStart → responseEnd) - Phase-aware haptics (click → thinkingStart → responseStart → responseEnd)
- Tablet/landscape optimization (width capping) - Tablet/landscape optimization (width capping)
- Dynamic keyboard handling (suggestion pills hide, spacer shrinks) - Dynamic keyboard handling: `WindowInsets.ime` offset (NOT `imePadding()` — that inflates the dock; NOT `adjustResize` — hides the dock with edge-to-edge). Mode pills hide when keyboard open. `windowSoftInputMode=adjustNothing` in manifest.
- Settings screen with SettingsViewModel - Settings screen with SettingsViewModel
### Bugs fixed (2026-06-19/20/21) ### Bugs fixed (2026-06-19/20/21)

View file

@ -108,6 +108,7 @@ fun ChatScreen(
var reasoningPreset by remember { mutableStateOf(ReasoningPreset.Standard) } var reasoningPreset by remember { mutableStateOf(ReasoningPreset.Standard) }
var samplingPrefs by remember { mutableStateOf(SamplingPrefs()) } var samplingPrefs by remember { mutableStateOf(SamplingPrefs()) }
var usedTokens by remember { mutableStateOf(0) } var usedTokens by remember { mutableStateOf(0) }
var chatModel by remember { mutableStateOf<String?>(null) }
// Navigation State // Navigation State
var currentScreen by remember { mutableStateOf(AppScreen.Chat) } var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
@ -225,6 +226,7 @@ fun ChatScreen(
conversationId = null conversationId = null
viewingLockedChat = false viewingLockedChat = false
usedTokens = 0 usedTokens = 0
chatModel = null
} }
fun unlockAndOpen(summary: ConversationSummary) { fun unlockAndOpen(summary: ConversationSummary) {
@ -314,6 +316,9 @@ fun ChatScreen(
if (stored.isNotEmpty()) { if (stored.isNotEmpty()) {
val entities = stored.mapIndexed { i, m -> m.toEntity(summary.id, i) } val entities = stored.mapIndexed { i, m -> m.toEntity(summary.id, i) }
chat.messageRepo.cacheMessages(summary.id, entities) chat.messageRepo.cacheMessages(summary.id, entities)
val lastAssistant = stored.lastOrNull { it.role == "assistant" }
lastAssistant?.model?.let { chatModel = it }
usedTokens = stored.sumOf { (it.inputTokens ?: 0) + (it.outputTokens ?: 0) }
if (stored.size != cachedList.size || stored.zip(cachedList).any { (s, c) -> s.id != c.id || s.content != c.content }) { if (stored.size != cachedList.size || stored.zip(cachedList).any { (s, c) -> s.id != c.id || s.content != c.content }) {
messages.clear() messages.clear()
stored.forEach { m -> stored.forEach { m ->
@ -673,12 +678,13 @@ fun ChatScreen(
} }
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
val displayModel = chatModel ?: session.config?.model ?: ""
ModelPill( ModelPill(
label = modelDisplayName(session.config?.model ?: "", models), label = modelDisplayName(displayModel, models),
onClick = { haptics.tick(); showModelSheet = true }, onClick = { haptics.tick(); showModelSheet = true },
) )
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
val ctxLen = models.find { it.id == session.config?.model }?.context_length ?: 1_000_000 val ctxLen = models.find { it.id == displayModel }?.context_length ?: 1_000_000
TokenBadge(used = usedTokens, contextLength = ctxLen) TokenBadge(used = usedTokens, contextLength = ctxLen)
} }
} }
@ -730,21 +736,19 @@ fun ChatScreen(
) )
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier) .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
) { ) {
if (!imeVisible) { Spacer(Modifier.height(if (imeVisible) 6.dp else 24.dp))
Spacer(Modifier.height(24.dp)) ModePillsRow(
ModePillsRow( activeMode = activeMode,
activeMode = activeMode, onToggle = { mode ->
onToggle = { mode -> activeMode = if (activeMode == mode) null else mode
activeMode = if (activeMode == mode) null else mode haptics.tick()
haptics.tick() },
}, reasoningPreset = reasoningPreset,
reasoningPreset = reasoningPreset, onReasoningChange = { reasoningPreset = it; haptics.tick() },
onReasoningChange = { reasoningPreset = it; haptics.tick() }, samplingPrefs = samplingPrefs,
samplingPrefs = samplingPrefs, onSamplingChange = { samplingPrefs = it },
onSamplingChange = { samplingPrefs = it }, )
) Spacer(Modifier.height(if (imeVisible) 6.dp else 10.dp))
Spacer(Modifier.height(10.dp))
}
ChatInput( ChatInput(
value = input, value = input,
onValueChange = { input = it }, onValueChange = { input = it },
@ -791,7 +795,7 @@ fun ChatScreen(
models = models, models = models,
selectedModelId = session.config?.model ?: "", selectedModelId = session.config?.model ?: "",
favorites = session.favorites, favorites = session.favorites,
onSelectModel = { session.setModel(it); haptics.tick() }, onSelectModel = { session.setModel(it); chatModel = it; haptics.tick() },
onToggleFavorite = { session.toggleFavorite(it) }, onToggleFavorite = { session.toggleFavorite(it) },
onDismiss = { showModelSheet = false }, onDismiss = { showModelSheet = false },
) )

View file

@ -87,6 +87,9 @@ data class StoredMessage(
val attachments: List<Attachment> = emptyList(), val attachments: List<Attachment> = emptyList(),
val reasoning: String? = null, val reasoning: String? = null,
val sources: List<SearchSource>? = null, val sources: List<SearchSource>? = null,
val model: String? = null,
val inputTokens: Int? = null,
val outputTokens: Int? = null,
) )
@Serializable private data class ConversationDetail(val messages: List<StoredMessage> = emptyList()) @Serializable private data class ConversationDetail(val messages: List<StoredMessage> = emptyList())