* fix(#589): show in-session model picker when user sends /model Typing a bare '/model' in the chat composer now opens the model picker so the user can hot-swap the CURRENT session's model via the existing slash-command path, instead of hand-typing the provider/model string. - ChatViewModel.preloadModelOptions() caches GET /api/model/options at GatewayReady so the picker opens instantly; falls back to on-demand refresh if opened before preload finishes. - sendMessage() intercepts a bare '/model' (no arg) -> showModelPicker=true instead of dispatching; a typed '/model provider/model' still dispatches via handleSlashCommand. - sendSlashModel() routes through command.dispatch with session_id, so the swap is session-scoped (not the global POST /api/model/set). - Extracted ModelPickerDialog into ui/model/components and reused by both ModelScreen and ChatScreen (adds loading + empty states). - Added a top-bar model chip (current session model label) that opens the same picker. - Added unit tests: bare /model opens picker, picker selection hot-swaps via slash dispatch with session_id, typed /model with arg dispatches directly. Note: CI shows 2 pre-existing test failures (ChatViewModelTest.testToggleSearch, HermesWsClientTest.testAutoReconnect) that also fail on a clean main full suite (MockK static-mock leakage between classes). Unrelated to this change; compile + ktlint + the 3 new tests pass. * fix(#589): use backend-valid session-scoped /model command syntax The model picker now dispatches the canonical backend command form '/model <model> --provider <slug> --session' instead of the unsupported 'provider/model' slash form. The '--session' flag keeps the switch scoped to the current chat only (per-session override) and never writes the global model config, which matches the backend model-switch contract. - sendSlashModel builds '/model $model --provider $provider --session'. - Updated unit tests to assert the corrected arg payload ('gpt-4o --provider openai --session') and the fully-typed dispatch path. * fix(#589): add pinned-models section to in-session model picker - ModelPickerDialog now renders a Pinned section (mirrors global model screen) - search filter matches model names too, not just provider name/slug - ChatUiState gains modelPickerPinned, fed from AuthManager.getPinnedModels() - top-bar model chip removed; picker opens on /model only - test mock for getPinnedModels() added * fix(#589): route /model via prompt.submit instead of command.dispatch (resolves 4018) /model is a real backend slash command handled in the backend's message pipeline (_handle_message -> _handle_model_command), so it must arrive as a normal prompt.submit message, not the command.dispatch RPC (which only knows quick/plugin/bundle/skill commands and 4018s on /model). - SlashCommandDispatcher: add SlashResult.ModelSwitch, map /model -> ModelSwitch - ChatViewModel: handle ModelSwitch -> handleModelSwitch() sends via WsMethods.PROMPT_SUBMIT with {session_id, text} - Tests: rewrite the two /model tests to assert prompt.submit (previously asserted the buggy command.dispatch path); add /model -> ModelSwitch test Independent agy review: APPROVE, no blocking issues. * fix(#589): route /model via slash.exec (correct transport; prompt.submit made LLM treat it as text) Earlier attempt routed /model through prompt.submit, but the TUI gateway's prompt.submit handler does NOT parse slash commands — it runs the text as a normal agent turn, so the LLM received '/model ...' as a user message (the exact reported bug). The correct RPC is slash.exec, which runs the command through the slash worker and _mirror_slash_side_effects -> _apply_model_switch to hot-swap the live session model. command.dispatch is also wrong (only knows quick/plugin/bundle/skill commands -> 4018). - WsMethods: add SLASH_EXEC = "slash.exec" - ChatViewModel.handleModelSwitch: send slash.exec with {command, session_id} (previously prompt.submit with {text}); corrected doc comment - SlashCommandDispatcher: corrected comments (slash.exec, not prompt.submit) - ChatViewModelTest: rewrite both /model tests to assert slash.exec with the /model <model> --provider <slug> --session command string - SlashCommandDispatcherTest: /model -> ModelSwitch already covered Verified against hermes-agent tui_gateway/server.py: slash.exec (L13190) -> worker.run + _mirror_slash_side_effects (L13299) -> _apply_model_switch (L13118); parse_model_flags accepts --provider/--session (hermes_cli/model_switch.py:355). Independent agy review of the prior attempt: APPROVE (the wrong-transport assumption was the author's, not agy's). * fix(#589): route /model via config.set key=model (slash.exec 4001'd; prompt.submit made LLM treat it as text) Third transport attempt, now verified against the gateway source: - prompt.submit: gateway runs text as a normal agent turn -> LLM sees '/model' as a message (the original reported bug). WRONG. - slash.exec: gateway returns 4001 'session not found'. The slash worker path resolves the session differently / requires agent-built state the mobile session isn't in at switch time. WRONG. - config.set key=model: gateway (server.py config.set L10253) routes key=='model' straight to _apply_model_switch using the SAME _sessions.get(session_id) lookup that the working command.dispatch uses. CORRECT. sendSlashModel still builds '/model <model> --provider <slug> --session'; parse_model_flags (hermes_cli/model_switch.py:355) extracts model/provider/ session from it. handleModelSwitch now sends config.set {key:model, value: command, session_id}. - WsMethods: add CONFIG_SET = 'config.set' - ChatViewModel.handleModelSwitch: config.set instead of slash.exec - SlashCommandDispatcher: comments updated (config.set, not slash.exec) - ChatViewModelTest: both /model tests assert config.set key=model + command - also fixes the MockK testToggleSearch flake by preloading byte-buddy agent via JAVA_TOOL_OPTIONS locally (container blocks JVM self-attach) * fix(#589): strip /model prefix before config.set key=model (5001 'model names can't have spaces') config.set key=model passes the value DIRECTLY to parse_model_flags — it does NOT strip a leading '/model' the way slash.exec / prompt.submit do. Sending '/model <model> ...' made parse_model_flags treat '/model <model>' as the model name (contains a space) -> 5001. handleModelSwitch now strips the '/model' prefix and sends the bare spec '<model> --provider <slug> --session', matching the TUI client's modelValueForConfigSet (ui-tui session.ts:23) used by useMainApp.ts:123. - ChatViewModel.handleModelSwitch: spec = command.removePrefix('/model').trim() - ChatViewModelTest: assert bare spec 'gpt-4o --provider openai --session' - comment updated * fix(#589): strip /model prefix case-insensitively before config.set SlashCommandDispatcher and isModelPickerCommand match /model case-insensitively, but handleModelSwitch used removePrefix("/model") (case-sensitive). A typed /MODEL ... forwarded the literal slash prefix to config.set and the backend's parse_model_flags wouldn't recognize it — silent hot-swap failure. Use startsWith("/model", ignoreCase=true) + substring(6) so any casing is stripped. Added a regression test asserting /MODEL does not forward a leading slash. * fix(test): inject searchDispatcher to fix MockKException in testToggleSearch --------- Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
1 parent
c543eb6470
commit
48f5ffbe52
8 files changed
+565
-66
No files matched your search
@@ -25,6 +25,8 @@ object WsMethods {
|
||||
const val COMMANDS_CATALOG = "commands.catalog"
|
||||
const val COMMAND_DISPATCH = "command.dispatch"
|
||||
const val COMMAND_RESOLVE = "command.resolve"
|
||||
const val SLASH_EXEC = "slash.exec"
|
||||
const val CONFIG_SET = "config.set"
|
||||
|
||||
// ── Attachments ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in new issue
Block a user