Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
5ab7ad16df
commit
a9c3cd0691
9 files changed
+244
-3
No files matched your search
@@ -45,4 +45,15 @@ object ChatInputPolicy {
|
||||
* selection.
|
||||
*/
|
||||
fun commandFieldValue(command: String): TextFieldValue = TextFieldValue(command, TextRange(command.length))
|
||||
|
||||
/**
|
||||
* Rank slash-command suggestions by how often the user has dispatched them
|
||||
* (issue #865): most-used first, ties broken by the current (catalog)
|
||||
* order via the stable sort. Commands with no recorded usage keep their
|
||||
* catalog position, so a fresh install behaves exactly as before.
|
||||
*/
|
||||
fun sortSlashSuggestions(
|
||||
commands: List<String>,
|
||||
usageCounts: Map<String, Int>,
|
||||
): List<String> = commands.sortedByDescending { usageCounts[it.lowercase()] ?: 0 }
|
||||
}
|
||||
@@ -681,6 +681,7 @@ fun ChatScreen(
|
||||
isAgentTyping = state.isAgentTyping,
|
||||
isConnected = state.isConnected,
|
||||
commandCatalog = state.commandCatalog,
|
||||
slashUsageCounts = state.slashUsageCounts,
|
||||
pendingAttachments = state.pendingAttachments,
|
||||
onCameraTap = {
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.local.HermesDatabase
|
||||
import com.m57.hermescontrol.data.local.SlashUsageStore
|
||||
import com.m57.hermescontrol.data.model.Attachment
|
||||
import com.m57.hermescontrol.data.model.AttachmentSource
|
||||
import com.m57.hermescontrol.data.model.ModelProvider
|
||||
@@ -274,6 +275,10 @@ data class ChatUiState(
|
||||
val typingEffectDelayMs: Int = 30,
|
||||
// Commands catalog
|
||||
val commandCatalog: CommandCatalog = CommandCatalog(),
|
||||
// Per-command usage counts for the slash-autocomplete ranking (issue
|
||||
// #865). Empty until the local store loads; commands without recorded
|
||||
// usage keep their catalog order.
|
||||
val slashUsageCounts: Map<String, Int> = emptyMap(),
|
||||
// In-session model picker (issue #589) — surfaced when the user types /model
|
||||
// (or taps the top-bar model chip). Mirror of the global model screen's
|
||||
// picker, but the selection hot-swaps the CURRENT session via the slash path.
|
||||
@@ -378,6 +383,7 @@ class ChatViewModel(
|
||||
ChatPersistenceRepository(
|
||||
HermesDatabase.get(application).chatMessageDao(),
|
||||
),
|
||||
slashUsageStore: SlashUsageStore = SlashUsageStore(application.applicationContext),
|
||||
searchDispatcher: kotlinx.coroutines.CoroutineDispatcher = kotlinx.coroutines.Dispatchers.Default,
|
||||
private val ioDispatcher: kotlinx.coroutines.CoroutineDispatcher = kotlinx.coroutines.Dispatchers.IO,
|
||||
) : AndroidViewModel(application) {
|
||||
@@ -456,6 +462,7 @@ class ChatViewModel(
|
||||
|
||||
// ── Session persistence ──────────────────────────────────────────────
|
||||
private val repo: ChatPersistenceRepository = repo
|
||||
private val slashUsageStore: SlashUsageStore = slashUsageStore
|
||||
private val slashDispatcher = SlashCommandDispatcher()
|
||||
|
||||
/**
|
||||
@@ -565,6 +572,14 @@ class ChatViewModel(
|
||||
resetSessionState(sessionId = null, title = "Hermes", isLoading = true)
|
||||
}
|
||||
}
|
||||
// Slash-command usage ranking (issue #865): mirror the local usage
|
||||
// counts into state so the autocomplete can surface most-used
|
||||
// commands first. Best-effort — the store never throws.
|
||||
viewModelScope.launch {
|
||||
slashUsageStore.counts().collect { counts ->
|
||||
_uiState.update { it.copy(slashUsageCounts = counts) }
|
||||
}
|
||||
}
|
||||
if (wsClient.connectionStatus.value == ConnectionStatus.CONNECTED) {
|
||||
handleGatewayReady()
|
||||
}
|
||||
@@ -1390,6 +1405,15 @@ class ChatViewModel(
|
||||
return
|
||||
}
|
||||
|
||||
// Track per-command usage for the slash-autocomplete ranking (issue
|
||||
// #865). Counted here, AFTER the blocklist guard, so commands that
|
||||
// can never dispatch don't climb the ranking. Best-effort — a store
|
||||
// failure must never block the dispatch itself.
|
||||
val commandName = command.split(" ", limit = 2)[0].lowercase()
|
||||
viewModelScope.launch {
|
||||
slashUsageStore.recordUse(commandName)
|
||||
}
|
||||
|
||||
when (val result = slashDispatcher.dispatch(command)) {
|
||||
is SlashResult.Interrupt -> {
|
||||
interruptSession()
|
||||
|
||||
@@ -78,6 +78,7 @@ fun ChatInputBar(
|
||||
isAgentTyping: Boolean,
|
||||
isConnected: Boolean,
|
||||
commandCatalog: CommandCatalog,
|
||||
slashUsageCounts: Map<String, Int> = emptyMap(),
|
||||
pendingAttachments: List<Attachment> = emptyList(),
|
||||
onCameraTap: () -> Unit = {},
|
||||
onImageTap: () -> Unit = {},
|
||||
@@ -136,7 +137,10 @@ fun ChatInputBar(
|
||||
exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically(),
|
||||
) {
|
||||
val filteredCommands =
|
||||
commandNames.filter { it.startsWith(inputFieldValue.text, ignoreCase = true) }
|
||||
ChatInputPolicy.sortSlashSuggestions(
|
||||
commandNames.filter { it.startsWith(inputFieldValue.text, ignoreCase = true) },
|
||||
slashUsageCounts,
|
||||
)
|
||||
if (filteredCommands.isNotEmpty()) {
|
||||
androidx.compose.material3.Surface(
|
||||
modifier =
|
||||
|
||||
Reference in new issue
Block a user