feat(chat): surface session terminal_backend (ssh/docker) in header (#869)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
57dc4cdf5c
commit
ea8fead087
4 files changed
+35
-1
No files matched your search
@@ -22,6 +22,7 @@ data class SessionInfo(
|
|||||||
val parent_session_id: String? = null,
|
val parent_session_id: String? = null,
|
||||||
val display_name: String? = null,
|
val display_name: String? = null,
|
||||||
val model: String? = null,
|
val model: String? = null,
|
||||||
|
val terminal_backend: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Close
|
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.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@@ -423,6 +425,24 @@ fun ChatScreen(
|
|||||||
.background(LocalHermesStatusColors.current.error),
|
.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) },
|
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ data class ChatUiState(
|
|||||||
val currentSessionModel: String? = null,
|
val currentSessionModel: String? = null,
|
||||||
// Reasoning effort level for the current session
|
// Reasoning effort level for the current session
|
||||||
val reasoningLevel: String? = null,
|
val reasoningLevel: String? = null,
|
||||||
|
val terminalBackend: String? = null,
|
||||||
// Context-window meter (issue #756): tokens currently used by the session
|
// Context-window meter (issue #756): tokens currently used by the session
|
||||||
// prompt (numerator) and the active model's full context window (denominator).
|
// prompt (numerator) and the active model's full context window (denominator).
|
||||||
// Both null until the first successful fetch.
|
// Both null until the first successful fetch.
|
||||||
@@ -727,6 +728,7 @@ class ChatViewModel(
|
|||||||
val model = info["model"] as? String
|
val model = info["model"] as? String
|
||||||
val provider = info["provider"] as? String
|
val provider = info["provider"] as? String
|
||||||
val reasoningEffort = info["reasoning_effort"] as? String
|
val reasoningEffort = info["reasoning_effort"] as? String
|
||||||
|
val terminalBackend = info["terminal_backend"] as? String
|
||||||
val newModelLabel =
|
val newModelLabel =
|
||||||
if (model != null && provider != null) {
|
if (model != null && provider != null) {
|
||||||
"$provider/$model"
|
"$provider/$model"
|
||||||
@@ -752,6 +754,7 @@ class ChatViewModel(
|
|||||||
} else {
|
} else {
|
||||||
reasoningEffort
|
reasoningEffort
|
||||||
},
|
},
|
||||||
|
terminalBackend = terminalBackend ?: state.terminalBackend,
|
||||||
fullContextTokens = if (modelSwapped) null else state.fullContextTokens,
|
fullContextTokens = if (modelSwapped) null else state.fullContextTokens,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -988,6 +991,7 @@ class ChatViewModel(
|
|||||||
val model = infoMap?.get("model") as? String
|
val model = infoMap?.get("model") as? String
|
||||||
val provider = infoMap?.get("provider") as? String
|
val provider = infoMap?.get("provider") as? String
|
||||||
val reasoningEffort = infoMap?.get("reasoning_effort") 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
|
// B8 (Jun 20 2026, kanban t_session_resume): do NOT reload
|
||||||
// cached messages here — switchSession() already did so before
|
// cached messages here — switchSession() already did so before
|
||||||
@@ -1011,6 +1015,7 @@ class ChatViewModel(
|
|||||||
} else {
|
} else {
|
||||||
reasoningEffort
|
reasoningEffort
|
||||||
},
|
},
|
||||||
|
terminalBackend = terminalBackend ?: it.terminalBackend,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Mirror the active runtime session id app-wide (issue #532).
|
// Mirror the active runtime session id app-wide (issue #532).
|
||||||
@@ -1983,6 +1988,7 @@ class ChatViewModel(
|
|||||||
modelPickerLoading = false,
|
modelPickerLoading = false,
|
||||||
currentSessionModel = null,
|
currentSessionModel = null,
|
||||||
reasoningLevel = null,
|
reasoningLevel = null,
|
||||||
|
terminalBackend = null,
|
||||||
usedContextTokens = null,
|
usedContextTokens = null,
|
||||||
fullContextTokens = null,
|
fullContextTokens = null,
|
||||||
contextBreakdown = null,
|
contextBreakdown = null,
|
||||||
|
|||||||
@@ -1494,7 +1494,12 @@ class ChatViewModelTest {
|
|||||||
mockEventsFlow.emit(WsEvent.SecretRequest("secret-1", sessionId))
|
mockEventsFlow.emit(WsEvent.SecretRequest("secret-1", sessionId))
|
||||||
mockEventsFlow.emit(
|
mockEventsFlow.emit(
|
||||||
WsEvent.SessionInfo(
|
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(
|
mockEventsFlow.emit(
|
||||||
@@ -1520,6 +1525,7 @@ class ChatViewModelTest {
|
|||||||
assertNotNull(oldState.sudoPrompt)
|
assertNotNull(oldState.sudoPrompt)
|
||||||
assertNotNull(oldState.secretPrompt)
|
assertNotNull(oldState.secretPrompt)
|
||||||
assertNotNull(oldState.currentSessionModel)
|
assertNotNull(oldState.currentSessionModel)
|
||||||
|
assertEquals("docker", oldState.terminalBackend)
|
||||||
assertTrue(oldState.subagentIndicators.isNotEmpty())
|
assertTrue(oldState.subagentIndicators.isNotEmpty())
|
||||||
assertTrue(oldState.todos.isNotEmpty())
|
assertTrue(oldState.todos.isNotEmpty())
|
||||||
assertTrue(oldState.showModelPicker)
|
assertTrue(oldState.showModelPicker)
|
||||||
@@ -1536,6 +1542,7 @@ class ChatViewModelTest {
|
|||||||
assertNull(state.secretPrompt)
|
assertNull(state.secretPrompt)
|
||||||
assertNull(state.currentSessionModel)
|
assertNull(state.currentSessionModel)
|
||||||
assertNull(state.reasoningLevel)
|
assertNull(state.reasoningLevel)
|
||||||
|
assertNull(state.terminalBackend)
|
||||||
assertTrue(state.subagentIndicators.isEmpty())
|
assertTrue(state.subagentIndicators.isEmpty())
|
||||||
assertTrue(state.todos.isEmpty())
|
assertTrue(state.todos.isEmpty())
|
||||||
assertFalse(state.showModelPicker)
|
assertFalse(state.showModelPicker)
|
||||||
|
|||||||
Reference in new issue
Block a user