From a61a1b9974a004655c65f55330abeb2fdca98393 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:56:25 +0300 Subject: [PATCH] feat: add setting to toggle model provider visibility in chat composer (#1194) --- .../components/ComposerInteractionTest.kt | 33 ++++++++++++-- .../data/config/ServerStoreState.kt | 1 + .../hermescontrol/data/local/AuthManager.kt | 6 +++ .../m57/hermescontrol/ui/chat/ChatScreen.kt | 1 + .../hermescontrol/ui/chat/ChatViewModel.kt | 2 + .../ui/chat/components/ChatComposer.kt | 2 + .../ui/chat/components/ComposerToolbar.kt | 45 ++++++++++++++----- .../ui/settings/SettingsSubPages.kt | 2 + .../ui/settings/SettingsViewModel.kt | 9 ++++ .../settings/components/AppearanceSection.kt | 34 ++++++++++++++ app/src/main/res/values-ar/strings.xml | 2 + app/src/main/res/values-ko/strings.xml | 2 + app/src/main/res/values-zh/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../data/config/ServerStoreTest.kt | 10 +++++ .../ui/chat/ChatUpdateCommandTest.kt | 1 + .../ui/chat/ChatViewModelTest.kt | 4 ++ .../ui/chat/ComposerToolbarTest.kt | 42 +++++++++-------- .../ui/chat/SlashCommandDispatchRpcTest.kt | 1 + .../ui/settings/SettingsViewModelTest.kt | 35 +++++++++++++++ 20 files changed, 204 insertions(+), 32 deletions(-) diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt index b9e04410..2da52438 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ComposerInteractionTest.kt @@ -59,6 +59,7 @@ class ComposerInteractionTest { composerWidth: Dp? = null, modelAfterTap: String? = null, layoutDirection: LayoutDirection = LayoutDirection.Ltr, + showModelProvider: Boolean = false, ) { composeTestRule.setContent { var input by remember { mutableStateOf(TextFieldValue("")) } @@ -93,6 +94,7 @@ class ComposerInteractionTest { onCameraTap = { cameraTaps++ }, onImageTap = { photosTaps++ }, onFileTap = { fileTaps++ }, + showModelProvider = showModelProvider, ) } if (composerWidth == null) { @@ -206,9 +208,9 @@ class ComposerInteractionTest { } @Test - fun longModelName_narrowComposer_scrollsWithoutOpeningPicker_orHidingControls() { + fun longModelName_remainsScrollableAndDoesNotTriggerPicker() { val longModel = "openrouter/some-extremely-long-model-name-preview-2026-with-extra-characters" - setComposer(model = longModel, composerWidth = 280.dp) + setComposer(model = longModel, composerWidth = 280.dp, showModelProvider = true) composeTestRule.onNodeWithTag("reasoning_chip").assertIsDisplayed() composeTestRule.onNodeWithTag("mic_button").assertIsDisplayed() @@ -252,7 +254,7 @@ class ComposerInteractionTest { fun changingModel_resetsScrolledLabelToTheStart() { val firstModel = "openrouter/some-extremely-long-model-name-preview-2026-with-extra-characters" val secondModel = "anthropic/another-extremely-long-model-name-preview-2026-with-extra-characters" - setComposer(model = firstModel, modelAfterTap = secondModel, composerWidth = 280.dp) + setComposer(model = firstModel, modelAfterTap = secondModel, composerWidth = 280.dp, showModelProvider = true) val firstText = composeTestRule.onNodeWithText(firstModel) val leftBefore = firstText.getUnclippedBoundsInRoot().left @@ -344,4 +346,29 @@ class ComposerInteractionTest { composeTestRule.onNodeWithTag("attachment_tray").assertDoesNotExist() } + + @Test + fun defaultComposer_showsOnlyModelNameWithoutProvider() { + setComposer(model = "openai/gpt-5.5", showModelProvider = false) + + composeTestRule.onNodeWithText("gpt-5.5").assertIsDisplayed() + composeTestRule.onNodeWithText("openai/gpt-5.5").assertDoesNotExist() + } + + @Test + fun enabledComposer_showsProviderAndModelName() { + setComposer(model = "openai/gpt-5.5", showModelProvider = true) + + composeTestRule.onNodeWithText("openai/gpt-5.5").assertIsDisplayed() + composeTestRule.onNodeWithText("gpt-5.5").assertDoesNotExist() + } + + @Test + fun defaultComposer_showsOnlyFinalModelSegmentForNestedProvider() { + setComposer(model = "openrouter/openai/gpt-5", showModelProvider = false) + + composeTestRule.onNodeWithText("gpt-5").assertIsDisplayed() + composeTestRule.onNodeWithText("openai/gpt-5").assertDoesNotExist() + composeTestRule.onNodeWithText("openrouter/openai/gpt-5").assertDoesNotExist() + } } diff --git a/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt b/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt index 097094b1..b5fd31cf 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/config/ServerStoreState.kt @@ -28,6 +28,7 @@ data class ServerStoreState( val showUserMessageTokens: Boolean = true, val showAssistantMessageTokens: Boolean = true, val showTokensPerSecond: Boolean = true, + val showModelProvider: Boolean = false, // App display language. "system" = follow device locale; otherwise a BCP-47 // language code such as "en" or "ko". Applied via ContextWrapper in MainActivity. val appLanguage: String = "system", diff --git a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt index eb3665e9..ec338ffc 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt @@ -850,6 +850,12 @@ object AuthManager { serverStore.update { it.copy(showTokensPerSecond = enabled) } } + fun isModelProviderShown(): Boolean = serverStore.getLatestState().showModelProvider + + fun setModelProviderShown(shown: Boolean) { + serverStore.update { it.copy(showModelProvider = shown) } + } + // ── Chat Font Scale (issue #1004) ─────────────────────────────────── fun getChatFontScale(): Float = serverStore.getLatestState().chatFontScale diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index 43c67627..2c6d0adf 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -790,6 +790,7 @@ fun ChatScreen( }, // Composer toolbar wiring (PR 1) currentSessionModel = state.currentSessionModel, + showModelProvider = state.showModelProvider, reasoningLevel = state.reasoningLevel, onModelTap = { viewModel.openModelPicker() }, onReasoningTap = { level -> viewModel.setReasoningLevel(level) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt index 2bd30c3e..b4a87a99 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt @@ -118,6 +118,7 @@ data class ChatUiState( val showUserMessageTokens: Boolean = true, val showAssistantMessageTokens: Boolean = true, val showTokensPerSecond: Boolean = true, + val showModelProvider: Boolean = false, // Commands catalog val commandCatalog: CommandCatalog = CommandCatalog(), // Per-command usage counts for the slash-autocomplete ranking (issue @@ -2605,6 +2606,7 @@ class ChatViewModel( showUserMessageTokens = AuthManager.isUserMessageTokensEnabled(), showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled(), showTokensPerSecond = AuthManager.isTokensPerSecondEnabled(), + showModelProvider = AuthManager.isModelProviderShown(), ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt index 50900bd3..67efc277 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt @@ -100,6 +100,7 @@ fun ChatInputBar( fastSupported: Boolean = false, isFastModeChanging: Boolean = false, onToggleFastMode: () -> Unit = {}, + showModelProvider: Boolean = false, ) { // Allow sending while the agent is mid-turn or awaiting approval: the // gateway's prompt.submit busy-input policy queues it as the next turn @@ -327,6 +328,7 @@ fun ChatInputBar( fastSupported = fastSupported, isFastModeChanging = isFastModeChanging, onToggleFastMode = onToggleFastMode, + showModelProvider = showModelProvider, ) AttachmentTray( diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt index e5165cc9..ac1de39d 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerToolbar.kt @@ -103,6 +103,7 @@ fun ComposerToolbar( fastSupported: Boolean = false, isFastModeChanging: Boolean = false, onToggleFastMode: () -> Unit = {}, + showModelProvider: Boolean = false, ) { var showReasoningMenu by remember { mutableStateOf(false) } val palette = composerPalette() @@ -135,7 +136,11 @@ fun ComposerToolbar( // Model + reasoning pill — wraps its content inside the free space, // pushing the mic/action buttons to the far end val modelScrollState = rememberScrollState() - LaunchedEffect(currentSessionModel) { + val modelLabel = + currentSessionModel?.let { model -> + composerModelLabel(model, showProvider = showModelProvider) + } ?: "Model" + LaunchedEffect(modelLabel) { modelScrollState.scrollTo(0) } Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { @@ -159,9 +164,7 @@ fun ComposerToolbar( .testTag("model_chip"), ) { Text( - // Keeps the provider so same-named models from different providers - // stay distinguishable while allowing the full label to be revealed. - text = currentSessionModel?.let(::composerModelLabel) ?: "Model", + text = modelLabel, style = MaterialTheme.typography.bodyMedium, color = palette.onControl, maxLines = 1, @@ -454,19 +457,39 @@ private fun listeningIconButtonColors(): IconButtonColors = ) /** - * Model label for the composer pill: the session's "provider/model" id with + * Model label for the composer pill: + * When [showProvider] is true, shows the session's "provider/model" id with * only the "custom:" marker removed from the provider ("custom:acme/glm-5.3" - * → "acme/glm-5.3"). The provider itself is kept so the same model served by + * → "acme/glm-5.3"). The provider is kept so the same model served by * different providers never renders identically. + * When [showProvider] is false (default), shows only the model name ("openai/gpt-5" + * → "gpt-5", "custom:acme/glm-5.3" → "glm-5.3"). */ -internal fun composerModelLabel(sessionModel: String): String { +internal fun composerModelLabel( + sessionModel: String, + showProvider: Boolean = false, +): String { val slash = sessionModel.indexOf('/') if (slash <= 0) return sessionModel - val provider = sessionModel.substring(0, slash) - if (!provider.startsWith(CUSTOM_PROVIDER_PREFIX) || provider.length == CUSTOM_PROVIDER_PREFIX.length) { - return sessionModel + + val rawProvider = sessionModel.substring(0, slash) + val model = sessionModel.substring(slash + 1) + if (model.isBlank()) return sessionModel + + if (rawProvider == CUSTOM_PROVIDER_PREFIX) return sessionModel + + if (!showProvider) { + val leaf = sessionModel.substringAfterLast('/') + return if (leaf.isNotBlank()) leaf else sessionModel } - return provider.removePrefix(CUSTOM_PROVIDER_PREFIX) + sessionModel.substring(slash) + + val provider = + if (rawProvider.startsWith(CUSTOM_PROVIDER_PREFIX) && rawProvider.length > CUSTOM_PROVIDER_PREFIX.length) { + rawProvider.removePrefix(CUSTOM_PROVIDER_PREFIX) + } else { + rawProvider + } + return "$provider/$model" } private const val CUSTOM_PROVIDER_PREFIX = "custom:" diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt index caaf22df..2450d723 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsSubPages.kt @@ -207,6 +207,8 @@ internal fun SettingsChatPage( onAssistantMessageTokensChange = viewModel::onAssistantMessageTokensChange, showTokensPerSecond = state.showTokensPerSecond, onTokensPerSecondChange = viewModel::onTokensPerSecondChange, + showModelProvider = state.showModelProvider, + onShowModelProviderChange = viewModel::onShowModelProviderChange, ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index e1732dba..59c5336a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -43,6 +43,7 @@ data class SettingsUiState( val showUserMessageTokens: Boolean = true, val showAssistantMessageTokens: Boolean = true, val showTokensPerSecond: Boolean = true, + val showModelProvider: Boolean = false, val profiles: List = emptyList(), val selectedProfileId: String? = null, val renameProfileName: String = "", @@ -89,6 +90,7 @@ class SettingsViewModel( val showUserMessageTokens = AuthManager.isUserMessageTokensEnabled() val showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled() val showTokensPerSecond = AuthManager.isTokensPerSecondEnabled() + val showModelProvider = AuthManager.isModelProviderShown() val profiles = AuthManager.getConnectionProfiles() val appLanguage = AuthManager.getAppLanguage() val renameProfileName = @@ -114,6 +116,7 @@ class SettingsViewModel( showUserMessageTokens = showUserMessageTokens, showAssistantMessageTokens = showAssistantMessageTokens, showTokensPerSecond = showTokensPerSecond, + showModelProvider = showModelProvider, profiles = profiles, selectedProfileId = selectedId, renameProfileName = renameProfileName, @@ -360,6 +363,11 @@ class SettingsViewModel( AuthManager.setTokensPerSecondEnabled(enabled) } + fun onShowModelProviderChange(enabled: Boolean) { + _uiState.update { it.copy(showModelProvider = enabled, isSaved = false) } + AuthManager.setModelProviderShown(enabled) + } + /** Clear all auth credentials — logs out and returns to landing screen. */ fun logout() { AuthManager.setToken(null) @@ -393,6 +401,7 @@ class SettingsViewModel( AuthManager.setUserMessageTokensEnabled(state.showUserMessageTokens) AuthManager.setAssistantMessageTokensEnabled(state.showAssistantMessageTokens) AuthManager.setTokensPerSecondEnabled(state.showTokensPerSecond) + AuthManager.setModelProviderShown(state.showModelProvider) ApiClient.rebuild() viewModelScope.launch(ioDispatcher) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt index 43f83481..87d2e175 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt @@ -213,6 +213,8 @@ internal fun ChatSection( onAssistantMessageTokensChange: (Boolean) -> Unit = {}, showTokensPerSecond: Boolean = true, onTokensPerSecondChange: (Boolean) -> Unit = {}, + showModelProvider: Boolean = false, + onShowModelProviderChange: (Boolean) -> Unit = {}, ) { val fontScaleOptions = listOf(0.85f, 1.0f, 1.15f, 1.30f, 1.50f) val currentIndex = @@ -425,6 +427,38 @@ internal fun ChatSection( Spacer(modifier = Modifier.height(20.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f).padding(end = 16.dp)) { + Text( + text = stringResource(R.string.settings_item_show_model_provider), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(R.string.settings_desc_show_model_provider), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + } + Switch( + checked = showModelProvider, + onCheckedChange = onShowModelProviderChange, + modifier = Modifier.testTag("settings_show_model_provider"), + colors = + SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.primary, + checkedTrackColor = MaterialTheme.colorScheme.primaryContainer, + ), + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 349fd3dd..569fa1be 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -192,6 +192,8 @@ رموز رسالة المستخدم رموز رسالة المساعد الرموز في الثانية (TPS) + إظهار اسم المزود + إظهار المزود قبل اسم النموذج في شريط كتابة المحادثة. الوكيل يكتب يفكّر… يبحث… diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 7133a64a..b22dc93a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -195,6 +195,8 @@ 사용자 메시지 토큰 어시스턴트 메시지 토큰 초당 토큰 수 (TPS) + 제공자 이름 표시 + 채팅 작성창에서 모델 이름 앞에 제공자를 표시합니다. 에이전트 입력 중 생각 중… 검색 중… diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 36383e7a..cb5ea892 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -192,6 +192,8 @@ 用户消息令牌 助手消息令牌 每秒令牌数 (TPS) + 显示提供商名称 + 在聊天输入栏的模型名称前显示提供商。 助手正在输入 正在思考… 正在搜索… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89a19465..f076cde8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,6 +216,8 @@ User message tokens Assistant message tokens Tokens per second (TPS) + Show provider name + Show the provider before the model name in the chat composer. Agent is typing Thinking… Searching… diff --git a/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt b/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt index 2996d277..bb694ae7 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/config/ServerStoreTest.kt @@ -2,6 +2,7 @@ package com.m57.hermescontrol.data.config import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -16,6 +17,7 @@ class ServerStoreTest { assertTrue(state.autoReconnect) assertEquals("token", state.wsAuthParam) assertEquals(1.0f, state.chatFontScale) + assertFalse(state.showModelProvider) assertTrue(state.connectionProfiles.isEmpty()) assertNull(state.selectedProfileId) } @@ -67,4 +69,12 @@ class ServerStoreTest { assertEquals("127.0.0.1", unselectedState.resolvedHost) assertEquals(9119, unselectedState.resolvedPort) } + + @Test + fun testShowModelProviderUpdate() { + val state = ServerStoreState() + assertFalse(state.showModelProvider) + val updated = state.copy(showModelProvider = true) + assertTrue(updated.showModelProvider) + } } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatUpdateCommandTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatUpdateCommandTest.kt index 604362b0..daf6f132 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatUpdateCommandTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatUpdateCommandTest.kt @@ -93,6 +93,7 @@ class ChatUpdateCommandTest { every { AuthManager.isUserMessageTokensEnabled() } returns true every { AuthManager.isAssistantMessageTokensEnabled() } returns true every { AuthManager.isTokensPerSecondEnabled() } returns true + every { AuthManager.isModelProviderShown() } returns false every { AuthManager.isAutoReconnect() } returns false every { AuthManager.isRestoreLastSession() } returns false every { AuthManager.getLastOpenedSessionId() } returns null diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt index ee5330df..2874b7c2 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt @@ -171,6 +171,7 @@ class ChatViewModelTest { every { AuthManager.isUserMessageTokensEnabled() } returns true every { AuthManager.isAssistantMessageTokensEnabled() } returns true every { AuthManager.isTokensPerSecondEnabled() } returns true + every { AuthManager.isModelProviderShown() } returns false every { AuthManager.isAutoReconnect() } returns false every { AuthManager.isRestoreLastSession() } returns false every { AuthManager.getLastOpenedSessionId() } returns null @@ -4335,6 +4336,7 @@ class ChatViewModelTest { assertTrue(showUserMessageTokens) assertTrue(showAssistantMessageTokens) assertTrue(showTokensPerSecond) + assertFalse(showModelProvider) } // When settings change after construction and refreshSettings() is re-invoked, @@ -4346,6 +4348,7 @@ class ChatViewModelTest { every { AuthManager.isUserMessageTokensEnabled() } returns false every { AuthManager.isAssistantMessageTokensEnabled() } returns false every { AuthManager.isTokensPerSecondEnabled() } returns false + every { AuthManager.isModelProviderShown() } returns true viewModel.refreshSettings() advanceUntilIdle() @@ -4357,6 +4360,7 @@ class ChatViewModelTest { assertFalse(state.showUserMessageTokens) assertFalse(state.showAssistantMessageTokens) assertFalse(state.showTokensPerSecond) + assertTrue(state.showModelProvider) } @Test diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerToolbarTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerToolbarTest.kt index c0a90495..cd3df878 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerToolbarTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerToolbarTest.kt @@ -5,31 +5,37 @@ import org.junit.Test /** * Unit tests for [composerModelLabel] — the label shown in the composer's - * model/reasoning pill. Only the "custom:" provider marker is dropped; the - * provider stays so the same model from different providers never collides. + * model/reasoning pill. + * By default ([showProvider] = false), only the model name is shown. + * When [showProvider] = true, the provider is kept (with "custom:" stripped). */ class ComposerToolbarTest { @Test - fun composerModelLabel_keepsProviderToDisambiguateSameModel() { - assertEquals("openai/gpt-5.5", composerModelLabel("openai/gpt-5.5")) - assertEquals("copilot/gpt-5.5", composerModelLabel("copilot/gpt-5.5")) - } - - @Test - fun composerModelLabel_dropsOnlyTheCustomMarker() { - assertEquals("my-provider/glm-5.3", composerModelLabel("custom:my-provider/glm-5.3")) - assertEquals("openrouter/openai/gpt-5.5", composerModelLabel("openrouter/openai/gpt-5.5")) - } - - @Test - fun composerModelLabel_leavesModelTagsAndPlainIdsAlone() { - assertEquals("ollama/llama3:8b", composerModelLabel("ollama/llama3:8b")) + fun composerModelLabel_defaultsToModelOnly() { + assertEquals("gpt-5.5", composerModelLabel("openai/gpt-5.5")) + assertEquals("gpt-5.5", composerModelLabel("copilot/gpt-5.5")) + assertEquals("glm-5.3", composerModelLabel("custom:my-provider/glm-5.3")) + assertEquals("gpt-5.5", composerModelLabel("openrouter/openai/gpt-5.5")) + assertEquals("gpt-5", composerModelLabel("openrouter/openai/gpt-5")) + assertEquals("llama3:8b", composerModelLabel("ollama/llama3:8b")) assertEquals("llama3:8b", composerModelLabel("llama3:8b")) assertEquals("gpt-5", composerModelLabel("gpt-5")) + assertEquals("custom:/glm-5.3", composerModelLabel("custom:/glm-5.3")) + assertEquals("openai/", composerModelLabel("openai/")) + assertEquals("/gpt-5", composerModelLabel("/gpt-5")) } @Test - fun composerModelLabel_bareCustomMarker_fallsBackToFullId() { - assertEquals("custom:/glm-5.3", composerModelLabel("custom:/glm-5.3")) + fun composerModelLabel_keepsProviderWhenEnabled() { + assertEquals("openai/gpt-5.5", composerModelLabel("openai/gpt-5.5", showProvider = true)) + assertEquals("copilot/gpt-5.5", composerModelLabel("copilot/gpt-5.5", showProvider = true)) + assertEquals("my-provider/glm-5.3", composerModelLabel("custom:my-provider/glm-5.3", showProvider = true)) + assertEquals("openrouter/openai/gpt-5.5", composerModelLabel("openrouter/openai/gpt-5.5", showProvider = true)) + assertEquals("ollama/llama3:8b", composerModelLabel("ollama/llama3:8b", showProvider = true)) + assertEquals("llama3:8b", composerModelLabel("llama3:8b", showProvider = true)) + assertEquals("gpt-5", composerModelLabel("gpt-5", showProvider = true)) + assertEquals("custom:/glm-5.3", composerModelLabel("custom:/glm-5.3", showProvider = true)) + assertEquals("openai/", composerModelLabel("openai/", showProvider = true)) + assertEquals("/gpt-5", composerModelLabel("/gpt-5", showProvider = true)) } } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/SlashCommandDispatchRpcTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/SlashCommandDispatchRpcTest.kt index 9df5f047..95c33f4a 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/SlashCommandDispatchRpcTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/SlashCommandDispatchRpcTest.kt @@ -98,6 +98,7 @@ class SlashCommandDispatchRpcTest { every { AuthManager.isUserMessageTokensEnabled() } returns true every { AuthManager.isAssistantMessageTokensEnabled() } returns true every { AuthManager.isTokensPerSecondEnabled() } returns true + every { AuthManager.isModelProviderShown() } returns false every { AuthManager.isAutoReconnect() } returns false every { AuthManager.isRestoreLastSession() } returns false every { AuthManager.getLastOpenedSessionId() } returns null diff --git a/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt index bc42cec4..40ec3506 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/settings/SettingsViewModelTest.kt @@ -77,6 +77,7 @@ class SettingsViewModelTest { every { AuthManager.isUserMessageTokensEnabled() } returns true every { AuthManager.isAssistantMessageTokensEnabled() } returns true every { AuthManager.isTokensPerSecondEnabled() } returns true + every { AuthManager.isModelProviderShown() } returns false every { AuthManager.getConnectionProfiles() } returns emptyList() every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId } every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/" @@ -93,6 +94,7 @@ class SettingsViewModelTest { every { AuthManager.setUserMessageTokensEnabled(any()) } returns Unit every { AuthManager.setAssistantMessageTokensEnabled(any()) } returns Unit every { AuthManager.setTokensPerSecondEnabled(any()) } returns Unit + every { AuthManager.setModelProviderShown(any()) } returns Unit every { AuthManager.setSelectedProfileId(any()) } answers { storedSelectedProfileId = firstArg() } @@ -327,4 +329,37 @@ class SettingsViewModelTest { verify { AuthManager.clearLastOpenedSessionIdsForConnection("prof-1") } } + + @Test + fun testShowModelProvider_loadsAndTogglesImmediately() = + runTest { + every { AuthManager.isModelProviderShown() } returns false + + val viewModel = SettingsViewModel(ioDispatcher = testDispatcher) + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(false, viewModel.uiState.value.showModelProvider) + + viewModel.onShowModelProviderChange(true) + assertEquals(true, viewModel.uiState.value.showModelProvider) + assertEquals(false, viewModel.uiState.value.isSaved) + verify { AuthManager.setModelProviderShown(true) } + + viewModel.onShowModelProviderChange(false) + assertEquals(false, viewModel.uiState.value.showModelProvider) + verify { AuthManager.setModelProviderShown(false) } + } + + @Test + fun testSave_persistsShowModelProvider() = + runTest { + val viewModel = SettingsViewModel(ioDispatcher = testDispatcher) + testDispatcher.scheduler.advanceUntilIdle() + + viewModel.onShowModelProviderChange(true) + viewModel.save() + testDispatcher.scheduler.advanceUntilIdle() + + verify { AuthManager.setModelProviderShown(true) } + assertEquals(true, viewModel.uiState.value.isSaved) + } }