From ea8fead087f0f33e8034e13fdf294ff8e678e673 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Mon, 10 Aug 2026 01:53:47 +0300 Subject: [PATCH] feat(chat): surface session terminal_backend (ssh/docker) in header (#869) Co-authored-by: M57 --- .../data/model/SessionListResponse.kt | 1 + .../m57/hermescontrol/ui/chat/ChatScreen.kt | 20 +++++++++++++++++++ .../hermescontrol/ui/chat/ChatViewModel.kt | 6 ++++++ .../ui/chat/ChatViewModelTest.kt | 9 ++++++++- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt index 4dbc7bf2..68387351 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt @@ -22,6 +22,7 @@ data class SessionInfo( val parent_session_id: String? = null, val display_name: String? = null, val model: String? = null, + val terminal_backend: String? = null, ) @Serializable 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 7f29520f..44f3652b 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 @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close @@ -44,6 +45,7 @@ import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -423,6 +425,24 @@ fun ChatScreen( .background(LocalHermesStatusColors.current.error), ) } + // Terminal backend chip (issue #860) — ssh/docker/... next to the + // title; hidden when local (default) or absent. + val terminalBackend = state.terminalBackend + if (!terminalBackend.isNullOrBlank() && terminalBackend != "local") { + Spacer(modifier = Modifier.width(8.dp)) + Surface( + shape = RoundedCornerShape(6.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) { + Text( + text = terminalBackend, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 1.dp), + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + ) + } + } } }, navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) }, 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 1e0e8e65..03938b86 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 @@ -282,6 +282,7 @@ data class ChatUiState( val currentSessionModel: String? = null, // Reasoning effort level for the current session val reasoningLevel: String? = null, + val terminalBackend: String? = null, // Context-window meter (issue #756): tokens currently used by the session // prompt (numerator) and the active model's full context window (denominator). // Both null until the first successful fetch. @@ -727,6 +728,7 @@ class ChatViewModel( val model = info["model"] as? String val provider = info["provider"] as? String val reasoningEffort = info["reasoning_effort"] as? String + val terminalBackend = info["terminal_backend"] as? String val newModelLabel = if (model != null && provider != null) { "$provider/$model" @@ -752,6 +754,7 @@ class ChatViewModel( } else { reasoningEffort }, + terminalBackend = terminalBackend ?: state.terminalBackend, fullContextTokens = if (modelSwapped) null else state.fullContextTokens, ) } @@ -988,6 +991,7 @@ class ChatViewModel( val model = infoMap?.get("model") as? String val provider = infoMap?.get("provider") as? String val reasoningEffort = infoMap?.get("reasoning_effort") as? String + val terminalBackend = infoMap?.get("terminal_backend") as? String // B8 (Jun 20 2026, kanban t_session_resume): do NOT reload // cached messages here — switchSession() already did so before @@ -1011,6 +1015,7 @@ class ChatViewModel( } else { reasoningEffort }, + terminalBackend = terminalBackend ?: it.terminalBackend, ) } // Mirror the active runtime session id app-wide (issue #532). @@ -1983,6 +1988,7 @@ class ChatViewModel( modelPickerLoading = false, currentSessionModel = null, reasoningLevel = null, + terminalBackend = null, usedContextTokens = null, fullContextTokens = null, contextBreakdown = 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 1d5d3ea3..b7164488 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 @@ -1494,7 +1494,12 @@ class ChatViewModelTest { mockEventsFlow.emit(WsEvent.SecretRequest("secret-1", sessionId)) mockEventsFlow.emit( WsEvent.SessionInfo( - mapOf("model" to "model-a", "provider" to "provider-a", "reasoning_effort" to "high"), + mapOf( + "model" to "model-a", + "provider" to "provider-a", + "reasoning_effort" to "high", + "terminal_backend" to "docker", + ), ), ) mockEventsFlow.emit( @@ -1520,6 +1525,7 @@ class ChatViewModelTest { assertNotNull(oldState.sudoPrompt) assertNotNull(oldState.secretPrompt) assertNotNull(oldState.currentSessionModel) + assertEquals("docker", oldState.terminalBackend) assertTrue(oldState.subagentIndicators.isNotEmpty()) assertTrue(oldState.todos.isNotEmpty()) assertTrue(oldState.showModelPicker) @@ -1536,6 +1542,7 @@ class ChatViewModelTest { assertNull(state.secretPrompt) assertNull(state.currentSessionModel) assertNull(state.reasoningLevel) + assertNull(state.terminalBackend) assertTrue(state.subagentIndicators.isEmpty()) assertTrue(state.todos.isEmpty()) assertFalse(state.showModelPicker)