feat(#216): dynamic command autocomplete from server catalog, remove stop button (#403)

* feat(#216): dynamic command autocomplete from server catalog, remove stop button

- Replace hardcoded 8-command autocomplete with dynamic catalog from server's commands.catalog RPC
- Show ALL commands including skills via commandCatalog.pairs
- Remove physical Stop button (use /stop or /interrupt slash commands instead)
- Remove onInterrupt callback from ChatInputBar

* fix(#216): hide /stop and /interrupt from slash suggestions, remove stale strings

- Filters /stop and /interrupt from autocomplete popup while keeping
  them functional when manually typed
- Drops 7 unused command description string resources (now from server
  catalog) plus unused interrupt string resource

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-27 00:46:09 +03:00
1 parent 85c4662537
commit 96e754dd3b
2 files changed
+11 -56

No files matched your search

@@ -54,14 +54,12 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -106,6 +104,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.NavigationController import com.m57.hermescontrol.NavigationController
import com.m57.hermescontrol.R import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.ws.CommandCatalog
import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.notification.NotificationHelper import com.m57.hermescontrol.notification.NotificationHelper
import com.m57.hermescontrol.theme.StatusRed import com.m57.hermescontrol.theme.StatusRed
@@ -294,9 +293,9 @@ fun ChatScreen(
} }
} }
}, },
onInterrupt = viewModel::interruptSession,
isAgentTyping = state.isAgentTyping, isAgentTyping = state.isAgentTyping,
isConnected = state.isConnected, isConnected = state.isConnected,
commandCatalog = state.commandCatalog,
) )
} }
} }
@@ -365,9 +364,9 @@ private fun ChatInputBar(
inputText: String, inputText: String,
onInputChange: (String) -> Unit, onInputChange: (String) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
onInterrupt: () -> Unit,
isAgentTyping: Boolean, isAgentTyping: Boolean,
isConnected: Boolean, isConnected: Boolean,
commandCatalog: CommandCatalog,
) { ) {
// Allow sending slash commands even while agent is typing // Allow sending slash commands even while agent is typing
val isSlashCommand = inputText.startsWith("/") val isSlashCommand = inputText.startsWith("/")
@@ -396,23 +395,18 @@ private fun ChatInputBar(
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
) { ) {
Column { Column {
val commands = // Build command list from dynamic catalog (pairs = [name, description])
listOf( val allCommands = commandCatalog.pairs.associate { (name, desc) -> name to desc }
"/help", // Commands hidden from suggestion menu (still work when manually typed)
"/status", val hiddenSlashDisplay = setOf("/stop", "/interrupt")
"/sessions", val commandNames = allCommands.keys.filter { it !in hiddenSlashDisplay }
"/stats",
"/system",
"/new",
"/stop",
"/interrupt",
)
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = inputText.startsWith("/") && !inputText.contains(" "), visible = inputText.startsWith("/") && !inputText.contains(" "),
enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(), enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(),
exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically(), exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically(),
) { ) {
val filteredCommands = commands.filter { it.startsWith(inputText, ignoreCase = true) } val filteredCommands = commandNames.filter { it.startsWith(inputText, ignoreCase = true) }
if (filteredCommands.isNotEmpty()) { if (filteredCommands.isNotEmpty()) {
androidx.compose.material3.Surface( androidx.compose.material3.Surface(
modifier = modifier =
@@ -431,18 +425,7 @@ private fun ChatInputBar(
modifier = Modifier.heightIn(max = 200.dp), modifier = Modifier.heightIn(max = 200.dp),
) { ) {
items(filteredCommands, key = { it }) { cmd -> items(filteredCommands, key = { it }) { cmd ->
val description = val description = allCommands[cmd] ?: ""
when (cmd) {
"/help" -> stringResource(R.string.command_help_desc)
"/status" -> stringResource(R.string.command_status_desc)
"/sessions" -> stringResource(R.string.command_sessions_desc)
"/stats" -> stringResource(R.string.command_stats_desc)
"/system" -> stringResource(R.string.command_system_desc)
"/new" -> stringResource(R.string.command_new_desc)
"/stop" -> stringResource(R.string.command_stop_desc)
"/interrupt" -> stringResource(R.string.command_stop_desc)
else -> ""
}
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row( Row(
@@ -527,26 +510,6 @@ private fun ChatInputBar(
contentDescription = stringResource(R.string.chat_send_desc), contentDescription = stringResource(R.string.chat_send_desc),
) )
} }
// Compact stop icon — visible only while agent is streaming
if (isAgentTyping) {
Spacer(modifier = Modifier.width(4.dp))
FilledTonalIconButton(
onClick = onInterrupt,
modifier =
Modifier
.size(32.dp)
.testTag("interrupt_button"),
shape = CircleShape,
) {
Icon(
imageVector = Icons.Filled.Stop,
contentDescription = stringResource(R.string.content_desc_interrupt),
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.error,
)
}
}
} }
} }
} }
-8
View File
@@ -91,16 +91,8 @@
<string name="chat_tool_fallback">Tool</string> <string name="chat_tool_fallback">Tool</string>
<string name="chat_tool_show_raw">Show Raw JSON</string> <string name="chat_tool_show_raw">Show Raw JSON</string>
<string name="chat_tool_show_parsed">Show Parsed Output</string> <string name="chat_tool_show_parsed">Show Parsed Output</string>
<string name="command_help_desc">Show help menu</string>
<string name="command_status_desc">Check gateway and platform status</string>
<string name="command_sessions_desc">List all chat sessions</string>
<string name="command_stats_desc">Check system resource usage (stats)</string>
<string name="command_system_desc">Check system resource usage (system)</string>
<string name="command_new_desc">Create a new chat session</string>
<string name="command_stop_desc">Interrupt the active run</string>
<string name="content_desc_new_chat">New Chat</string> <string name="content_desc_new_chat">New Chat</string>
<string name="content_desc_scroll_to_bottom">Scroll to bottom</string> <string name="content_desc_scroll_to_bottom">Scroll to bottom</string>
<string name="content_desc_interrupt">Interrupt</string>
<string name="content_desc_copy">Copy</string> <string name="content_desc_copy">Copy</string>
<string name="content_desc_edit">Edit</string> <string name="content_desc_edit">Edit</string>