feat(chat): integrate fast mode toggle into generation controls dropdown (#1093)

This commit is contained in:
M57 authored and GitHub committed 2026-09-12 15:15:22 +03:00
1 parent 5dc3896237
commit 0bb087c41e
11 files changed
+388 -19

No files matched your search

@@ -40,7 +40,7 @@ class ChatModelSwitchDelegateTest {
slug = "openai",
name = "OpenAI",
models = listOf("gpt-4o"),
capabilities = mapOf("gpt-4o" to ModelCapabilities(reasoning = true)),
capabilities = mapOf("gpt-4o" to ModelCapabilities(fast = true, reasoning = true)),
),
),
)
@@ -138,4 +138,73 @@ class ChatModelSwitchDelegateTest {
assertNull(uiState.value.modelSwitchConfirmMessage)
assertEquals("anthropic/claude-3", uiState.value.currentSessionModel)
}
@Test
fun toggleFastMode_sendsConfigSetFast_andUpdatesOnAck() =
testScope.runTest {
delegate.preloadModelOptions()
advanceUntilIdle()
// Switch to model with fast capability
delegate.sendSlashModel("openai", "gpt-4o")
advanceUntilIdle()
sentMethods.clear()
sentParams.clear()
assertTrue(uiState.value.currentModelCapabilities?.fast == true)
assertFalse(uiState.value.fastMode)
assertFalse(uiState.value.isFastModeChanging)
delegate.toggleFastMode()
advanceUntilIdle()
assertTrue(uiState.value.isFastModeChanging)
assertFalse(uiState.value.fastMode) // Still unconfirmed!
assertEquals(listOf(WsMethods.CONFIG_SET), sentMethods)
assertEquals("fast", sentParams.first()["key"])
assertEquals("fast", sentParams.first()["value"])
assertEquals("runtime-1", sentParams.first()["session_id"])
// Backend acknowledges
delegate.handleConfigSetResult("req-1", mapOf("key" to "fast", "value" to "fast"))
assertTrue(uiState.value.fastMode)
assertFalse(uiState.value.isFastModeChanging)
// Toggle off
delegate.toggleFastMode()
advanceUntilIdle()
assertTrue(uiState.value.isFastModeChanging)
assertEquals("normal", sentParams.last()["value"])
delegate.handleConfigSetResult("req-2", mapOf("key" to "fast", "value" to "normal"))
assertFalse(uiState.value.fastMode)
assertFalse(uiState.value.isFastModeChanging)
}
@Test
fun toggleFastMode_whenRejectedByBackend_clearsChangingAndLatchesUnavailable() =
testScope.runTest {
delegate.preloadModelOptions()
advanceUntilIdle()
delegate.sendSlashModel("openai", "gpt-4o")
advanceUntilIdle()
sentMethods.clear()
sentParams.clear()
assertTrue(uiState.value.currentModelCapabilities?.fast == true)
delegate.toggleFastMode()
advanceUntilIdle()
assertTrue(uiState.value.isFastModeChanging)
// Backend returns error indicating fast mode not available
delegate.handleConfigSetError("req-1", mapOf("message" to "fast mode is not available for this model"))
assertFalse(uiState.value.isFastModeChanging)
assertFalse(uiState.value.fastMode)
assertEquals(false, uiState.value.currentModelCapabilities?.fast)
// Further toggle attempts are no-ops
val sentCount = sentMethods.size
delegate.toggleFastMode()
advanceUntilIdle()
assertEquals(sentCount, sentMethods.size)
}
}
@@ -2951,6 +2951,92 @@ class ChatViewModelTest {
assertEquals("nous/tencent/hy3:free", viewModel.uiState.value.currentSessionModel)
}
@Test
fun testSessionInfo_hydratesFastModeState() =
runTest {
stubEmptySessionRests("session-a")
val (viewModel, _) = createViewModelWithSession()
advanceUntilIdle()
assertFalse(viewModel.uiState.value.fastMode)
// Fast = true in session.info
mockEventsFlow.emit(
WsEvent.SessionInfo(
mapOf("fast" to true, "service_tier" to "priority"),
),
)
advanceUntilIdle()
assertTrue(viewModel.uiState.value.fastMode)
assertFalse(viewModel.uiState.value.isFastModeChanging)
// Fast = false / normal in session.info
mockEventsFlow.emit(
WsEvent.SessionInfo(
mapOf("fast" to false, "service_tier" to "normal"),
),
)
advanceUntilIdle()
assertFalse(viewModel.uiState.value.fastMode)
// Partial session.info without fast/service_tier does NOT clear state
mockEventsFlow.emit(
WsEvent.SessionInfo(
mapOf("fast" to true),
),
)
advanceUntilIdle()
assertTrue(viewModel.uiState.value.fastMode)
mockEventsFlow.emit(
WsEvent.SessionInfo(
mapOf("terminal_backend" to "tmux"),
),
)
advanceUntilIdle()
assertTrue(viewModel.uiState.value.fastMode) // preserved!
}
@Test
fun testSessionResume_hydratesFastModeState() =
runTest {
stubEmptySessionRests("session-b")
val (viewModel, _) = createViewModelWithSession()
var resumeReqId = ""
every { HermesWsClient.send(WsMethods.SESSION_RESUME, any(), any()) } answers {
val reqId = "resume-req-1"
resumeReqId = reqId
arg<((String) -> Unit)?>(2)?.invoke(reqId)
reqId
}
viewModel.switchSession("session-b")
runCurrent()
// Simulate SESSION_RESUME response with fast=true in info
mockEventsFlow.emit(
WsEvent.RpcResult(
id = resumeReqId,
result =
mapOf(
"session_id" to "sess-runtime-1",
"resumed" to "session-b",
"info" to
mapOf(
"model" to "gpt-4o",
"provider" to "openai",
"fast" to true,
"service_tier" to "priority",
),
),
),
)
advanceUntilIdle()
assertTrue(viewModel.uiState.value.fastMode)
assertFalse(viewModel.uiState.value.isFastModeChanging)
}
@Test
fun testResumeNotFound_recoversWithNewSession() =
runTest {