feat: add setting to toggle model provider visibility in chat composer (#1194)
This commit is contained in:
20 files changed
+204
-32
No files matched your search
+30
-3
@@ -59,6 +59,7 @@ class ComposerInteractionTest {
|
|||||||
composerWidth: Dp? = null,
|
composerWidth: Dp? = null,
|
||||||
modelAfterTap: String? = null,
|
modelAfterTap: String? = null,
|
||||||
layoutDirection: LayoutDirection = LayoutDirection.Ltr,
|
layoutDirection: LayoutDirection = LayoutDirection.Ltr,
|
||||||
|
showModelProvider: Boolean = false,
|
||||||
) {
|
) {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
var input by remember { mutableStateOf(TextFieldValue("")) }
|
var input by remember { mutableStateOf(TextFieldValue("")) }
|
||||||
@@ -93,6 +94,7 @@ class ComposerInteractionTest {
|
|||||||
onCameraTap = { cameraTaps++ },
|
onCameraTap = { cameraTaps++ },
|
||||||
onImageTap = { photosTaps++ },
|
onImageTap = { photosTaps++ },
|
||||||
onFileTap = { fileTaps++ },
|
onFileTap = { fileTaps++ },
|
||||||
|
showModelProvider = showModelProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (composerWidth == null) {
|
if (composerWidth == null) {
|
||||||
@@ -206,9 +208,9 @@ class ComposerInteractionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun longModelName_narrowComposer_scrollsWithoutOpeningPicker_orHidingControls() {
|
fun longModelName_remainsScrollableAndDoesNotTriggerPicker() {
|
||||||
val longModel = "openrouter/some-extremely-long-model-name-preview-2026-with-extra-characters"
|
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("reasoning_chip").assertIsDisplayed()
|
||||||
composeTestRule.onNodeWithTag("mic_button").assertIsDisplayed()
|
composeTestRule.onNodeWithTag("mic_button").assertIsDisplayed()
|
||||||
@@ -252,7 +254,7 @@ class ComposerInteractionTest {
|
|||||||
fun changingModel_resetsScrolledLabelToTheStart() {
|
fun changingModel_resetsScrolledLabelToTheStart() {
|
||||||
val firstModel = "openrouter/some-extremely-long-model-name-preview-2026-with-extra-characters"
|
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"
|
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 firstText = composeTestRule.onNodeWithText(firstModel)
|
||||||
val leftBefore = firstText.getUnclippedBoundsInRoot().left
|
val leftBefore = firstText.getUnclippedBoundsInRoot().left
|
||||||
@@ -344,4 +346,29 @@ class ComposerInteractionTest {
|
|||||||
|
|
||||||
composeTestRule.onNodeWithTag("attachment_tray").assertDoesNotExist()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ data class ServerStoreState(
|
|||||||
val showUserMessageTokens: Boolean = true,
|
val showUserMessageTokens: Boolean = true,
|
||||||
val showAssistantMessageTokens: Boolean = true,
|
val showAssistantMessageTokens: Boolean = true,
|
||||||
val showTokensPerSecond: Boolean = true,
|
val showTokensPerSecond: Boolean = true,
|
||||||
|
val showModelProvider: Boolean = false,
|
||||||
// App display language. "system" = follow device locale; otherwise a BCP-47
|
// App display language. "system" = follow device locale; otherwise a BCP-47
|
||||||
// language code such as "en" or "ko". Applied via ContextWrapper in MainActivity.
|
// language code such as "en" or "ko". Applied via ContextWrapper in MainActivity.
|
||||||
val appLanguage: String = "system",
|
val appLanguage: String = "system",
|
||||||
|
|||||||
@@ -850,6 +850,12 @@ object AuthManager {
|
|||||||
serverStore.update { it.copy(showTokensPerSecond = enabled) }
|
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) ───────────────────────────────────
|
// ── Chat Font Scale (issue #1004) ───────────────────────────────────
|
||||||
|
|
||||||
fun getChatFontScale(): Float = serverStore.getLatestState().chatFontScale
|
fun getChatFontScale(): Float = serverStore.getLatestState().chatFontScale
|
||||||
|
|||||||
@@ -790,6 +790,7 @@ fun ChatScreen(
|
|||||||
},
|
},
|
||||||
// Composer toolbar wiring (PR 1)
|
// Composer toolbar wiring (PR 1)
|
||||||
currentSessionModel = state.currentSessionModel,
|
currentSessionModel = state.currentSessionModel,
|
||||||
|
showModelProvider = state.showModelProvider,
|
||||||
reasoningLevel = state.reasoningLevel,
|
reasoningLevel = state.reasoningLevel,
|
||||||
onModelTap = { viewModel.openModelPicker() },
|
onModelTap = { viewModel.openModelPicker() },
|
||||||
onReasoningTap = { level -> viewModel.setReasoningLevel(level) },
|
onReasoningTap = { level -> viewModel.setReasoningLevel(level) },
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ data class ChatUiState(
|
|||||||
val showUserMessageTokens: Boolean = true,
|
val showUserMessageTokens: Boolean = true,
|
||||||
val showAssistantMessageTokens: Boolean = true,
|
val showAssistantMessageTokens: Boolean = true,
|
||||||
val showTokensPerSecond: Boolean = true,
|
val showTokensPerSecond: Boolean = true,
|
||||||
|
val showModelProvider: Boolean = false,
|
||||||
// Commands catalog
|
// Commands catalog
|
||||||
val commandCatalog: CommandCatalog = CommandCatalog(),
|
val commandCatalog: CommandCatalog = CommandCatalog(),
|
||||||
// Per-command usage counts for the slash-autocomplete ranking (issue
|
// Per-command usage counts for the slash-autocomplete ranking (issue
|
||||||
@@ -2605,6 +2606,7 @@ class ChatViewModel(
|
|||||||
showUserMessageTokens = AuthManager.isUserMessageTokensEnabled(),
|
showUserMessageTokens = AuthManager.isUserMessageTokensEnabled(),
|
||||||
showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled(),
|
showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled(),
|
||||||
showTokensPerSecond = AuthManager.isTokensPerSecondEnabled(),
|
showTokensPerSecond = AuthManager.isTokensPerSecondEnabled(),
|
||||||
|
showModelProvider = AuthManager.isModelProviderShown(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ fun ChatInputBar(
|
|||||||
fastSupported: Boolean = false,
|
fastSupported: Boolean = false,
|
||||||
isFastModeChanging: Boolean = false,
|
isFastModeChanging: Boolean = false,
|
||||||
onToggleFastMode: () -> Unit = {},
|
onToggleFastMode: () -> Unit = {},
|
||||||
|
showModelProvider: Boolean = false,
|
||||||
) {
|
) {
|
||||||
// Allow sending while the agent is mid-turn or awaiting approval: the
|
// 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
|
// gateway's prompt.submit busy-input policy queues it as the next turn
|
||||||
@@ -327,6 +328,7 @@ fun ChatInputBar(
|
|||||||
fastSupported = fastSupported,
|
fastSupported = fastSupported,
|
||||||
isFastModeChanging = isFastModeChanging,
|
isFastModeChanging = isFastModeChanging,
|
||||||
onToggleFastMode = onToggleFastMode,
|
onToggleFastMode = onToggleFastMode,
|
||||||
|
showModelProvider = showModelProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
AttachmentTray(
|
AttachmentTray(
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ fun ComposerToolbar(
|
|||||||
fastSupported: Boolean = false,
|
fastSupported: Boolean = false,
|
||||||
isFastModeChanging: Boolean = false,
|
isFastModeChanging: Boolean = false,
|
||||||
onToggleFastMode: () -> Unit = {},
|
onToggleFastMode: () -> Unit = {},
|
||||||
|
showModelProvider: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var showReasoningMenu by remember { mutableStateOf(false) }
|
var showReasoningMenu by remember { mutableStateOf(false) }
|
||||||
val palette = composerPalette()
|
val palette = composerPalette()
|
||||||
@@ -135,7 +136,11 @@ fun ComposerToolbar(
|
|||||||
// Model + reasoning pill — wraps its content inside the free space,
|
// Model + reasoning pill — wraps its content inside the free space,
|
||||||
// pushing the mic/action buttons to the far end
|
// pushing the mic/action buttons to the far end
|
||||||
val modelScrollState = rememberScrollState()
|
val modelScrollState = rememberScrollState()
|
||||||
LaunchedEffect(currentSessionModel) {
|
val modelLabel =
|
||||||
|
currentSessionModel?.let { model ->
|
||||||
|
composerModelLabel(model, showProvider = showModelProvider)
|
||||||
|
} ?: "Model"
|
||||||
|
LaunchedEffect(modelLabel) {
|
||||||
modelScrollState.scrollTo(0)
|
modelScrollState.scrollTo(0)
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) {
|
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) {
|
||||||
@@ -159,9 +164,7 @@ fun ComposerToolbar(
|
|||||||
.testTag("model_chip"),
|
.testTag("model_chip"),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
// Keeps the provider so same-named models from different providers
|
text = modelLabel,
|
||||||
// stay distinguishable while allowing the full label to be revealed.
|
|
||||||
text = currentSessionModel?.let(::composerModelLabel) ?: "Model",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = palette.onControl,
|
color = palette.onControl,
|
||||||
maxLines = 1,
|
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"
|
* 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.
|
* 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('/')
|
val slash = sessionModel.indexOf('/')
|
||||||
if (slash <= 0) return sessionModel
|
if (slash <= 0) return sessionModel
|
||||||
val provider = sessionModel.substring(0, slash)
|
|
||||||
if (!provider.startsWith(CUSTOM_PROVIDER_PREFIX) || provider.length == CUSTOM_PROVIDER_PREFIX.length) {
|
val rawProvider = sessionModel.substring(0, slash)
|
||||||
return sessionModel
|
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:"
|
private const val CUSTOM_PROVIDER_PREFIX = "custom:"
|
||||||
|
|||||||
@@ -207,6 +207,8 @@ internal fun SettingsChatPage(
|
|||||||
onAssistantMessageTokensChange = viewModel::onAssistantMessageTokensChange,
|
onAssistantMessageTokensChange = viewModel::onAssistantMessageTokensChange,
|
||||||
showTokensPerSecond = state.showTokensPerSecond,
|
showTokensPerSecond = state.showTokensPerSecond,
|
||||||
onTokensPerSecondChange = viewModel::onTokensPerSecondChange,
|
onTokensPerSecondChange = viewModel::onTokensPerSecondChange,
|
||||||
|
showModelProvider = state.showModelProvider,
|
||||||
|
onShowModelProviderChange = viewModel::onShowModelProviderChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ data class SettingsUiState(
|
|||||||
val showUserMessageTokens: Boolean = true,
|
val showUserMessageTokens: Boolean = true,
|
||||||
val showAssistantMessageTokens: Boolean = true,
|
val showAssistantMessageTokens: Boolean = true,
|
||||||
val showTokensPerSecond: Boolean = true,
|
val showTokensPerSecond: Boolean = true,
|
||||||
|
val showModelProvider: Boolean = false,
|
||||||
val profiles: List<ConnectionProfile> = emptyList(),
|
val profiles: List<ConnectionProfile> = emptyList(),
|
||||||
val selectedProfileId: String? = null,
|
val selectedProfileId: String? = null,
|
||||||
val renameProfileName: String = "",
|
val renameProfileName: String = "",
|
||||||
@@ -89,6 +90,7 @@ class SettingsViewModel(
|
|||||||
val showUserMessageTokens = AuthManager.isUserMessageTokensEnabled()
|
val showUserMessageTokens = AuthManager.isUserMessageTokensEnabled()
|
||||||
val showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled()
|
val showAssistantMessageTokens = AuthManager.isAssistantMessageTokensEnabled()
|
||||||
val showTokensPerSecond = AuthManager.isTokensPerSecondEnabled()
|
val showTokensPerSecond = AuthManager.isTokensPerSecondEnabled()
|
||||||
|
val showModelProvider = AuthManager.isModelProviderShown()
|
||||||
val profiles = AuthManager.getConnectionProfiles()
|
val profiles = AuthManager.getConnectionProfiles()
|
||||||
val appLanguage = AuthManager.getAppLanguage()
|
val appLanguage = AuthManager.getAppLanguage()
|
||||||
val renameProfileName =
|
val renameProfileName =
|
||||||
@@ -114,6 +116,7 @@ class SettingsViewModel(
|
|||||||
showUserMessageTokens = showUserMessageTokens,
|
showUserMessageTokens = showUserMessageTokens,
|
||||||
showAssistantMessageTokens = showAssistantMessageTokens,
|
showAssistantMessageTokens = showAssistantMessageTokens,
|
||||||
showTokensPerSecond = showTokensPerSecond,
|
showTokensPerSecond = showTokensPerSecond,
|
||||||
|
showModelProvider = showModelProvider,
|
||||||
profiles = profiles,
|
profiles = profiles,
|
||||||
selectedProfileId = selectedId,
|
selectedProfileId = selectedId,
|
||||||
renameProfileName = renameProfileName,
|
renameProfileName = renameProfileName,
|
||||||
@@ -360,6 +363,11 @@ class SettingsViewModel(
|
|||||||
AuthManager.setTokensPerSecondEnabled(enabled)
|
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. */
|
/** Clear all auth credentials — logs out and returns to landing screen. */
|
||||||
fun logout() {
|
fun logout() {
|
||||||
AuthManager.setToken(null)
|
AuthManager.setToken(null)
|
||||||
@@ -393,6 +401,7 @@ class SettingsViewModel(
|
|||||||
AuthManager.setUserMessageTokensEnabled(state.showUserMessageTokens)
|
AuthManager.setUserMessageTokensEnabled(state.showUserMessageTokens)
|
||||||
AuthManager.setAssistantMessageTokensEnabled(state.showAssistantMessageTokens)
|
AuthManager.setAssistantMessageTokensEnabled(state.showAssistantMessageTokens)
|
||||||
AuthManager.setTokensPerSecondEnabled(state.showTokensPerSecond)
|
AuthManager.setTokensPerSecondEnabled(state.showTokensPerSecond)
|
||||||
|
AuthManager.setModelProviderShown(state.showModelProvider)
|
||||||
ApiClient.rebuild()
|
ApiClient.rebuild()
|
||||||
|
|
||||||
viewModelScope.launch(ioDispatcher) {
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ internal fun ChatSection(
|
|||||||
onAssistantMessageTokensChange: (Boolean) -> Unit = {},
|
onAssistantMessageTokensChange: (Boolean) -> Unit = {},
|
||||||
showTokensPerSecond: Boolean = true,
|
showTokensPerSecond: Boolean = true,
|
||||||
onTokensPerSecondChange: (Boolean) -> Unit = {},
|
onTokensPerSecondChange: (Boolean) -> Unit = {},
|
||||||
|
showModelProvider: Boolean = false,
|
||||||
|
onShowModelProviderChange: (Boolean) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val fontScaleOptions = listOf(0.85f, 1.0f, 1.15f, 1.30f, 1.50f)
|
val fontScaleOptions = listOf(0.85f, 1.0f, 1.15f, 1.30f, 1.50f)
|
||||||
val currentIndex =
|
val currentIndex =
|
||||||
@@ -425,6 +427,38 @@ internal fun ChatSection(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
|||||||
@@ -192,6 +192,8 @@
|
|||||||
<string name="settings_item_user_message_tokens">رموز رسالة المستخدم</string>
|
<string name="settings_item_user_message_tokens">رموز رسالة المستخدم</string>
|
||||||
<string name="settings_item_assistant_message_tokens">رموز رسالة المساعد</string>
|
<string name="settings_item_assistant_message_tokens">رموز رسالة المساعد</string>
|
||||||
<string name="settings_item_tokens_per_second">الرموز في الثانية (TPS)</string>
|
<string name="settings_item_tokens_per_second">الرموز في الثانية (TPS)</string>
|
||||||
|
<string name="settings_item_show_model_provider">إظهار اسم المزود</string>
|
||||||
|
<string name="settings_desc_show_model_provider">إظهار المزود قبل اسم النموذج في شريط كتابة المحادثة.</string>
|
||||||
<string name="chat_agent_status_typing">الوكيل يكتب</string>
|
<string name="chat_agent_status_typing">الوكيل يكتب</string>
|
||||||
<string name="chat_agent_status_thinking">يفكّر…</string>
|
<string name="chat_agent_status_thinking">يفكّر…</string>
|
||||||
<string name="chat_agent_status_searching">يبحث…</string>
|
<string name="chat_agent_status_searching">يبحث…</string>
|
||||||
|
|||||||
@@ -195,6 +195,8 @@
|
|||||||
<string name="settings_item_user_message_tokens">사용자 메시지 토큰</string>
|
<string name="settings_item_user_message_tokens">사용자 메시지 토큰</string>
|
||||||
<string name="settings_item_assistant_message_tokens">어시스턴트 메시지 토큰</string>
|
<string name="settings_item_assistant_message_tokens">어시스턴트 메시지 토큰</string>
|
||||||
<string name="settings_item_tokens_per_second">초당 토큰 수 (TPS)</string>
|
<string name="settings_item_tokens_per_second">초당 토큰 수 (TPS)</string>
|
||||||
|
<string name="settings_item_show_model_provider">제공자 이름 표시</string>
|
||||||
|
<string name="settings_desc_show_model_provider">채팅 작성창에서 모델 이름 앞에 제공자를 표시합니다.</string>
|
||||||
<string name="chat_agent_status_typing">에이전트 입력 중</string>
|
<string name="chat_agent_status_typing">에이전트 입력 중</string>
|
||||||
<string name="chat_agent_status_thinking">생각 중…</string>
|
<string name="chat_agent_status_thinking">생각 중…</string>
|
||||||
<string name="chat_agent_status_searching">검색 중…</string>
|
<string name="chat_agent_status_searching">검색 중…</string>
|
||||||
|
|||||||
@@ -192,6 +192,8 @@
|
|||||||
<string name="settings_item_user_message_tokens">用户消息令牌</string>
|
<string name="settings_item_user_message_tokens">用户消息令牌</string>
|
||||||
<string name="settings_item_assistant_message_tokens">助手消息令牌</string>
|
<string name="settings_item_assistant_message_tokens">助手消息令牌</string>
|
||||||
<string name="settings_item_tokens_per_second">每秒令牌数 (TPS)</string>
|
<string name="settings_item_tokens_per_second">每秒令牌数 (TPS)</string>
|
||||||
|
<string name="settings_item_show_model_provider">显示提供商名称</string>
|
||||||
|
<string name="settings_desc_show_model_provider">在聊天输入栏的模型名称前显示提供商。</string>
|
||||||
<string name="chat_agent_status_typing">助手正在输入</string>
|
<string name="chat_agent_status_typing">助手正在输入</string>
|
||||||
<string name="chat_agent_status_thinking">正在思考…</string>
|
<string name="chat_agent_status_thinking">正在思考…</string>
|
||||||
<string name="chat_agent_status_searching">正在搜索…</string>
|
<string name="chat_agent_status_searching">正在搜索…</string>
|
||||||
|
|||||||
@@ -216,6 +216,8 @@
|
|||||||
<string name="settings_item_user_message_tokens">User message tokens</string>
|
<string name="settings_item_user_message_tokens">User message tokens</string>
|
||||||
<string name="settings_item_assistant_message_tokens">Assistant message tokens</string>
|
<string name="settings_item_assistant_message_tokens">Assistant message tokens</string>
|
||||||
<string name="settings_item_tokens_per_second">Tokens per second (TPS)</string>
|
<string name="settings_item_tokens_per_second">Tokens per second (TPS)</string>
|
||||||
|
<string name="settings_item_show_model_provider">Show provider name</string>
|
||||||
|
<string name="settings_desc_show_model_provider">Show the provider before the model name in the chat composer.</string>
|
||||||
<string name="chat_agent_status_typing">Agent is typing</string>
|
<string name="chat_agent_status_typing">Agent is typing</string>
|
||||||
<string name="chat_agent_status_thinking">Thinking…</string>
|
<string name="chat_agent_status_thinking">Thinking…</string>
|
||||||
<string name="chat_agent_status_searching">Searching…</string>
|
<string name="chat_agent_status_searching">Searching…</string>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.data.config
|
|||||||
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -16,6 +17,7 @@ class ServerStoreTest {
|
|||||||
assertTrue(state.autoReconnect)
|
assertTrue(state.autoReconnect)
|
||||||
assertEquals("token", state.wsAuthParam)
|
assertEquals("token", state.wsAuthParam)
|
||||||
assertEquals(1.0f, state.chatFontScale)
|
assertEquals(1.0f, state.chatFontScale)
|
||||||
|
assertFalse(state.showModelProvider)
|
||||||
assertTrue(state.connectionProfiles.isEmpty())
|
assertTrue(state.connectionProfiles.isEmpty())
|
||||||
assertNull(state.selectedProfileId)
|
assertNull(state.selectedProfileId)
|
||||||
}
|
}
|
||||||
@@ -67,4 +69,12 @@ class ServerStoreTest {
|
|||||||
assertEquals("127.0.0.1", unselectedState.resolvedHost)
|
assertEquals("127.0.0.1", unselectedState.resolvedHost)
|
||||||
assertEquals(9119, unselectedState.resolvedPort)
|
assertEquals(9119, unselectedState.resolvedPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShowModelProviderUpdate() {
|
||||||
|
val state = ServerStoreState()
|
||||||
|
assertFalse(state.showModelProvider)
|
||||||
|
val updated = state.copy(showModelProvider = true)
|
||||||
|
assertTrue(updated.showModelProvider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +93,7 @@ class ChatUpdateCommandTest {
|
|||||||
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
||||||
|
every { AuthManager.isModelProviderShown() } returns false
|
||||||
every { AuthManager.isAutoReconnect() } returns false
|
every { AuthManager.isAutoReconnect() } returns false
|
||||||
every { AuthManager.isRestoreLastSession() } returns false
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
every { AuthManager.getLastOpenedSessionId() } returns null
|
every { AuthManager.getLastOpenedSessionId() } returns null
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ class ChatViewModelTest {
|
|||||||
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
||||||
|
every { AuthManager.isModelProviderShown() } returns false
|
||||||
every { AuthManager.isAutoReconnect() } returns false
|
every { AuthManager.isAutoReconnect() } returns false
|
||||||
every { AuthManager.isRestoreLastSession() } returns false
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
every { AuthManager.getLastOpenedSessionId() } returns null
|
every { AuthManager.getLastOpenedSessionId() } returns null
|
||||||
@@ -4335,6 +4336,7 @@ class ChatViewModelTest {
|
|||||||
assertTrue(showUserMessageTokens)
|
assertTrue(showUserMessageTokens)
|
||||||
assertTrue(showAssistantMessageTokens)
|
assertTrue(showAssistantMessageTokens)
|
||||||
assertTrue(showTokensPerSecond)
|
assertTrue(showTokensPerSecond)
|
||||||
|
assertFalse(showModelProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When settings change after construction and refreshSettings() is re-invoked,
|
// When settings change after construction and refreshSettings() is re-invoked,
|
||||||
@@ -4346,6 +4348,7 @@ class ChatViewModelTest {
|
|||||||
every { AuthManager.isUserMessageTokensEnabled() } returns false
|
every { AuthManager.isUserMessageTokensEnabled() } returns false
|
||||||
every { AuthManager.isAssistantMessageTokensEnabled() } returns false
|
every { AuthManager.isAssistantMessageTokensEnabled() } returns false
|
||||||
every { AuthManager.isTokensPerSecondEnabled() } returns false
|
every { AuthManager.isTokensPerSecondEnabled() } returns false
|
||||||
|
every { AuthManager.isModelProviderShown() } returns true
|
||||||
viewModel.refreshSettings()
|
viewModel.refreshSettings()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
@@ -4357,6 +4360,7 @@ class ChatViewModelTest {
|
|||||||
assertFalse(state.showUserMessageTokens)
|
assertFalse(state.showUserMessageTokens)
|
||||||
assertFalse(state.showAssistantMessageTokens)
|
assertFalse(state.showAssistantMessageTokens)
|
||||||
assertFalse(state.showTokensPerSecond)
|
assertFalse(state.showTokensPerSecond)
|
||||||
|
assertTrue(state.showModelProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -5,31 +5,37 @@ import org.junit.Test
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for [composerModelLabel] — the label shown in the composer's
|
* Unit tests for [composerModelLabel] — the label shown in the composer's
|
||||||
* model/reasoning pill. Only the "custom:" provider marker is dropped; the
|
* model/reasoning pill.
|
||||||
* provider stays so the same model from different providers never collides.
|
* By default ([showProvider] = false), only the model name is shown.
|
||||||
|
* When [showProvider] = true, the provider is kept (with "custom:" stripped).
|
||||||
*/
|
*/
|
||||||
class ComposerToolbarTest {
|
class ComposerToolbarTest {
|
||||||
@Test
|
@Test
|
||||||
fun composerModelLabel_keepsProviderToDisambiguateSameModel() {
|
fun composerModelLabel_defaultsToModelOnly() {
|
||||||
assertEquals("openai/gpt-5.5", composerModelLabel("openai/gpt-5.5"))
|
assertEquals("gpt-5.5", composerModelLabel("openai/gpt-5.5"))
|
||||||
assertEquals("copilot/gpt-5.5", composerModelLabel("copilot/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"))
|
||||||
@Test
|
assertEquals("gpt-5", composerModelLabel("openrouter/openai/gpt-5"))
|
||||||
fun composerModelLabel_dropsOnlyTheCustomMarker() {
|
assertEquals("llama3:8b", composerModelLabel("ollama/llama3:8b"))
|
||||||
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"))
|
|
||||||
assertEquals("llama3:8b", composerModelLabel("llama3:8b"))
|
assertEquals("llama3:8b", composerModelLabel("llama3:8b"))
|
||||||
assertEquals("gpt-5", composerModelLabel("gpt-5"))
|
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
|
@Test
|
||||||
fun composerModelLabel_bareCustomMarker_fallsBackToFullId() {
|
fun composerModelLabel_keepsProviderWhenEnabled() {
|
||||||
assertEquals("custom:/glm-5.3", composerModelLabel("custom:/glm-5.3"))
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,6 +98,7 @@ class SlashCommandDispatchRpcTest {
|
|||||||
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
||||||
|
every { AuthManager.isModelProviderShown() } returns false
|
||||||
every { AuthManager.isAutoReconnect() } returns false
|
every { AuthManager.isAutoReconnect() } returns false
|
||||||
every { AuthManager.isRestoreLastSession() } returns false
|
every { AuthManager.isRestoreLastSession() } returns false
|
||||||
every { AuthManager.getLastOpenedSessionId() } returns null
|
every { AuthManager.getLastOpenedSessionId() } returns null
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class SettingsViewModelTest {
|
|||||||
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
every { AuthManager.isUserMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
every { AuthManager.isAssistantMessageTokensEnabled() } returns true
|
||||||
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
every { AuthManager.isTokensPerSecondEnabled() } returns true
|
||||||
|
every { AuthManager.isModelProviderShown() } returns false
|
||||||
every { AuthManager.getConnectionProfiles() } returns emptyList()
|
every { AuthManager.getConnectionProfiles() } returns emptyList()
|
||||||
every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId }
|
every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId }
|
||||||
every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/"
|
every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/"
|
||||||
@@ -93,6 +94,7 @@ class SettingsViewModelTest {
|
|||||||
every { AuthManager.setUserMessageTokensEnabled(any()) } returns Unit
|
every { AuthManager.setUserMessageTokensEnabled(any()) } returns Unit
|
||||||
every { AuthManager.setAssistantMessageTokensEnabled(any()) } returns Unit
|
every { AuthManager.setAssistantMessageTokensEnabled(any()) } returns Unit
|
||||||
every { AuthManager.setTokensPerSecondEnabled(any()) } returns Unit
|
every { AuthManager.setTokensPerSecondEnabled(any()) } returns Unit
|
||||||
|
every { AuthManager.setModelProviderShown(any()) } returns Unit
|
||||||
every { AuthManager.setSelectedProfileId(any()) } answers {
|
every { AuthManager.setSelectedProfileId(any()) } answers {
|
||||||
storedSelectedProfileId = firstArg()
|
storedSelectedProfileId = firstArg()
|
||||||
}
|
}
|
||||||
@@ -327,4 +329,37 @@ class SettingsViewModelTest {
|
|||||||
|
|
||||||
verify { AuthManager.clearLastOpenedSessionIdsForConnection("prof-1") }
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user