diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da334f6..2236b708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +## v0.50.214 — 2026-04-26 + +### Added +- **Busy input mode setting** — new `Settings → Preferences → Busy input mode` dropdown with three options: `Queue` (default, preserves existing behavior), `Interrupt` (cancel the current stream and re-send immediately), `Steer` (placeholder for future mid-stream injection, currently falls back to Interrupt with a toast). (`api/config.py`, `static/messages.js`, `static/boot.js`, `static/panels.js`, `static/index.html`, `static/i18n.js`) Closes #720 [#1062 @bergeouss] +- **`/queue`, `/interrupt`, `/steer` slash commands** — per-message overrides for the busy mode regardless of the current setting. `/queue ` enqueues explicitly; `/interrupt ` cancels the current turn and re-sends; `/steer ` same today with a future-upgrade toast. (`static/commands.js`) [#1062 @bergeouss] + +### Fixed +- **`/queue` command double-bubble** — missing `noEcho:true` caused the raw slash text to be echoed as a user bubble, then the drained message appeared again as a second bubble. (`static/commands.js`) +- **Staged-file duplication via slash commands** — `cmdQueue`, `cmdInterrupt`, and `cmdSteer` captured `S.pendingFiles` but never cleared the tray, so staged files were re-attached on the next send. Added `S.pendingFiles=[];renderTray()` after enqueue in all three handlers. (`static/commands.js`) + ## v0.50.213 — 2026-04-26 ### Fixed diff --git a/ROADMAP.md b/ROADMAP.md index 25012459..38ff5387 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ > Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI. > Everything you can do from the CLI terminal, you can do from this UI. > -> Last updated: v0.50.213 (April 26, 2026) — 2281 tests collected +> Last updated: v0.50.214 (April 26, 2026) — 2299 tests collected > Tests: 2107 collected (`pytest tests/ --collect-only -q`) > Source: / diff --git a/api/config.py b/api/config.py index 341c0027..75909f31 100644 --- a/api/config.py +++ b/api/config.py @@ -1859,6 +1859,7 @@ _SETTINGS_DEFAULTS = { "show_thinking": True, # show/hide thinking/reasoning blocks in chat view "sidebar_density": "compact", # compact | detailed "auto_title_refresh_every": "0", # adaptive title refresh: 0=off, 5/10/20=every N exchanges + "busy_input_mode": "queue", # behavior when sending while agent is running: queue | interrupt | steer "password_hash": None, # PBKDF2-HMAC-SHA256 hash; None = auth disabled } _SETTINGS_LEGACY_DROP_KEYS = {"assistant_language", "bubble_layout", "default_model"} @@ -1960,6 +1961,7 @@ _SETTINGS_ENUM_VALUES = { "send_key": {"enter", "ctrl+enter"}, "sidebar_density": {"compact", "detailed"}, "auto_title_refresh_every": {"0", "5", "10", "20"}, + "busy_input_mode": {"queue", "interrupt", "steer"}, } _SETTINGS_BOOL_KEYS = { "onboarding_completed", diff --git a/static/boot.js b/static/boot.js index 94a85aa8..234b9213 100644 --- a/static/boot.js +++ b/static/boot.js @@ -794,6 +794,7 @@ function applyBotName(){ window._notificationsEnabled=!!s.notifications_enabled; window._showThinking=s.show_thinking!==false; window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact'); + window._busyInputMode=(s.busy_input_mode||'queue'); window._botName=s.bot_name||'Hermes'; if(s.default_model) window._defaultModel=s.default_model; // Persist default workspace so the blank new-chat page can show it @@ -820,6 +821,7 @@ function applyBotName(){ window._notificationsEnabled=false; window._showThinking=true; window._sidebarDensity='compact'; + window._busyInputMode='queue'; window._botName='Hermes'; _bootSettings={check_for_updates:false}; if(typeof setLocale==='function'){ diff --git a/static/commands.js b/static/commands.js index 8a934114..bbb9f973 100644 --- a/static/commands.js +++ b/static/commands.js @@ -17,6 +17,9 @@ const COMMANDS=[ {name:'personality', desc:t('cmd_personality'), fn:cmdPersonality, arg:'name', subArgs:'personalities'}, {name:'skills', desc:t('cmd_skills'), fn:cmdSkills, arg:'query'}, {name:'stop', desc:t('cmd_stop'), fn:cmdStop, noEcho:true}, + {name:'queue', desc:t('cmd_queue'), fn:cmdQueue, arg:'message', noEcho:true}, + {name:'interrupt', desc:t('cmd_interrupt'), fn:cmdInterrupt, arg:'message', noEcho:true}, + {name:'steer', desc:t('cmd_steer'), fn:cmdSteer, arg:'message', noEcho:true}, {name:'title', desc:t('cmd_title'), fn:cmdTitle, arg:'[title]'}, {name:'retry', desc:t('cmd_retry'), fn:cmdRetry, noEcho:true}, {name:'undo', desc:t('cmd_undo'), fn:cmdUndo, noEcho:true}, @@ -531,6 +534,64 @@ async function cmdStop(){ if(typeof cancelStream==='function'){await cancelStream();showToast(t('stream_stopped'));} else showToast(t('cancel_unavailable')); } + +// ── Busy-input mode commands ────────────────────────────────────────────── +// These commands let users override the default busy_input_mode setting for a +// specific message. They are only meaningful while the agent is running. + +/** + * /queue — Explicitly queue a message for the next turn. + * Works regardless of the busy_input_mode setting. + */ +async function cmdQueue(args){ + const msg=(args||'').trim(); + if(!msg){showToast(t('cmd_queue_no_msg'));return;} + if(!S.busy){showToast(t('cmd_queue_not_busy'));return;} + if(!S.session){showToast(t('no_active_session'));return;} + queueSessionMessage(S.session.session_id,{text:msg,files:[...S.pendingFiles],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'}); + updateQueueBadge(S.session.session_id); + S.pendingFiles=[];renderTray(); + showToast(t('cmd_queue_confirm'),2000); +} + +/** + * /interrupt — Cancel the current turn and send a new message. + * Calls cancelStream() then queues the message so the drain picks it up. + */ +async function cmdInterrupt(args){ + const msg=(args||'').trim(); + if(!msg){showToast(t('cmd_interrupt_no_msg'));return;} + if(!S.busy||!S.activeStreamId){showToast(t('no_active_task'));return;} + if(!S.session){showToast(t('no_active_session'));return;} + // Queue the message first (before cancel sets busy=false and drains) + queueSessionMessage(S.session.session_id,{text:msg,files:[...S.pendingFiles],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'}); + updateQueueBadge(S.session.session_id); + S.pendingFiles=[];renderTray(); + // Cancel the active stream; setBusy(false) will drain the queue + if(typeof cancelStream==='function'){await cancelStream();} + showToast(t('cmd_interrupt_confirm'),2000); +} + +/** + * /steer — Inject a steering hint mid-task. + * Currently falls back to interrupt behaviour because the WebUI cannot + * inject messages into an in-flight agent thread. Shows a toast to + * inform the user that true steering is not yet available. + */ +async function cmdSteer(args){ + const msg=(args||'').trim(); + if(!msg){showToast(t('cmd_steer_no_msg'));return;} + if(!S.busy||!S.activeStreamId){showToast(t('no_active_task'));return;} + if(!S.session){showToast(t('no_active_session'));return;} + // True steer (inject without cancelling) requires agent-side support + // that is not yet available in the WebUI. Fall back to interrupt. + queueSessionMessage(S.session.session_id,{text:msg,files:[...S.pendingFiles],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'}); + updateQueueBadge(S.session.session_id); + S.pendingFiles=[];renderTray(); + if(typeof cancelStream==='function'){await cancelStream();} + showToast(t('cmd_steer_fallback'),2500); +} + async function cmdTitle(args){ if(!S.session){showToast(t('no_active_session'));return;} const name=(args||'').trim(); diff --git a/static/i18n.js b/static/i18n.js index fd8cc5df..467d00c3 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -99,6 +99,23 @@ const LOCALES = { theme_usage: 'Usage: /theme ', theme_set: 'Theme: ', no_active_session: 'No active session', + cmd_queue: 'Queue a message for the next turn', + cmd_interrupt: 'Cancel current turn and send a new message', + cmd_steer: 'Steer the agent with a correction (falls back to interrupt)', + cmd_queue_no_msg: 'Usage: /queue ', + cmd_queue_not_busy: 'No active task — just send normally', + cmd_queue_confirm: 'Message queued', + cmd_interrupt_no_msg: 'Usage: /interrupt ', + cmd_interrupt_confirm: 'Interrupted — sending new message', + cmd_steer_no_msg: 'Usage: /steer ', + cmd_steer_fallback: 'Steer is not yet available — interrupted and queued instead', + busy_steer_fallback: 'Steer not available — interrupted instead', + busy_interrupt_confirm: 'Interrupted — sending new message', + settings_label_busy_input_mode: 'Busy input mode', + settings_desc_busy_input_mode: 'Controls what happens when you send a message while the agent is running. Queue waits; Interrupt cancels and starts fresh; Steer sends a correction (currently falls back to interrupt).', + settings_busy_input_mode_queue: 'Queue follow-up', + settings_busy_input_mode_interrupt: 'Interrupt current turn', + settings_busy_input_mode_steer: 'Steer (interrupt + send)', slash_skill_badge:'Skill', slash_skill_desc:'Invoke this skill', @@ -708,6 +725,23 @@ const LOCALES = { theme_usage: 'Использование: /theme ', theme_set: 'Тема: ', no_active_session: 'Нет активной сессии', + cmd_queue: 'Поставить сообщение в очередь на следующий оборот', + cmd_interrupt: 'Прервать текущий оборот и отправить новое сообщение', + cmd_steer: 'Направить агента исправлением (переходит к прерыванию)', + cmd_queue_no_msg: 'Использование: /queue <сообщение>', + cmd_queue_not_busy: 'Нет активной задачи — просто отправьте', + cmd_queue_confirm: 'Сообщение в очереди', + cmd_interrupt_no_msg: 'Использование: /interrupt <сообщение>', + cmd_interrupt_confirm: 'Прервано — отправка нового сообщения', + cmd_steer_no_msg: 'Использование: /steer <сообщение>', + cmd_steer_fallback: 'Steer пока недоступен — прервано и поставлено в очередь', + busy_steer_fallback: 'Steer недоступен — прервано', + busy_interrupt_confirm: 'Прервано — отправка нового сообщения', + settings_label_busy_input_mode: 'Режим ввода при занятости', + settings_desc_busy_input_mode: 'Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer отправляет исправление (сейчас как прерывание).', + settings_busy_input_mode_queue: 'Поставить в очередь', + settings_busy_input_mode_interrupt: 'Прервать текущий оборот', + settings_busy_input_mode_steer: 'Steer (прерывание + отправка)', no_personalities: 'Личности не найдены (добавьте их в ~/.hermes/personalities/)', clarify_heading: 'Требуется уточнение', @@ -1300,6 +1334,23 @@ const LOCALES = { theme_usage: 'Uso: /theme ', theme_set: 'Tema: ', no_active_session: 'No hay ninguna sesión activa', + cmd_queue: 'Poner mensaje en cola para el siguiente turno', + cmd_interrupt: 'Cancelar turno actual y enviar nuevo mensaje', + cmd_steer: 'Redirigir al agente con una correcci\u00f3n (usa interrupci\u00f3n)', + cmd_queue_no_msg: 'Uso: /queue ', + cmd_queue_not_busy: 'Sin tarea activa \u2014 env\u00eda normalmente', + cmd_queue_confirm: 'Mensaje en cola', + cmd_interrupt_no_msg: 'Uso: /interrupt ', + cmd_interrupt_confirm: 'Interrumpido \u2014 enviando nuevo mensaje', + cmd_steer_no_msg: 'Uso: /steer ', + cmd_steer_fallback: 'Steer no disponible a\u00fan \u2014 interrumpido y encolado', + busy_steer_fallback: 'Steer no disponible \u2014 interrumpido', + busy_interrupt_confirm: 'Interrumpido \u2014 enviando nuevo mensaje', + settings_label_busy_input_mode: 'Modo de entrada ocupada', + settings_desc_busy_input_mode: 'Controla qu\u00e9 sucede al enviar un mensaje mientras el agente est\u00e1 activo. Cola espera; Interrumpir cancela y empieza de nuevo; Steer env\u00eda una correcci\u00f3n (actualmente usa interrupci\u00f3n).', + settings_busy_input_mode_queue: 'Poner en cola', + settings_busy_input_mode_interrupt: 'Interrumpir turno actual', + settings_busy_input_mode_steer: 'Steer (interrupci\u00f3n + env\u00edo)', no_personalities: 'No se encontraron personalidades (añádelas a ~/.hermes/personalities/)', available_personalities: 'Personalidades disponibles:', personality_switch_hint: '\n\nUsa `/personality ` para cambiar, o `/personality none` para limpiar.', @@ -1861,6 +1912,23 @@ const LOCALES = { theme_usage: 'Nutzung: /theme ', theme_set: 'Theme: ', no_active_session: 'Keine aktive Sitzung', + cmd_queue: 'Nachricht f\u00fcr den n\u00e4chsten Durchgang einreihen', + cmd_interrupt: 'Aktuellen Durchgang abbrechen und neue Nachricht senden', + cmd_steer: 'Agent mit Korrektur lenken (f\u00e4llt zur\u00fcck auf Unterbrechung)', + cmd_queue_no_msg: 'Verwendung: /queue ', + cmd_queue_not_busy: 'Keine aktive Aufgabe \u2014 normal senden', + cmd_queue_confirm: 'Nachricht eingereiht', + cmd_interrupt_no_msg: 'Verwendung: /interrupt ', + cmd_interrupt_confirm: 'Unterbrochen \u2014 neue Nachricht wird gesendet', + cmd_steer_no_msg: 'Verwendung: /steer ', + cmd_steer_fallback: 'Steer noch nicht verf\u00fcgbar \u2014 unterbrochen und eingereiht', + busy_steer_fallback: 'Steer nicht verf\u00fcgbar \u2014 unterbrochen', + busy_interrupt_confirm: 'Unterbrochen \u2014 neue Nachricht wird gesendet', + settings_label_busy_input_mode: 'Eingabemodus bei Besch\u00e4ftigung', + settings_desc_busy_input_mode: 'Steuert, was passiert, wenn Sie w\u00e4hrend der Agentenaktivit\u00e4t eine Nachricht senden. Warteschlange wartet; Unterbrechen bricht ab und startet neu; Steer sendet eine Korrektur (aktuell wie Unterbrechen).', + settings_busy_input_mode_queue: 'In Warteschlange einreihen', + settings_busy_input_mode_interrupt: 'Aktuellen Durchgang unterbrechen', + settings_busy_input_mode_steer: 'Steer (Unterbrechen + Senden)', no_personalities: 'Keine Persönlichkeiten gefunden (füge sie in ~/.hermes/personalities/ hinzu)', available_personalities: 'Verfügbare Persönlichkeiten:', personality_switch_hint: '\n\nNutze `/personality ` zum Wechseln, oder `/personality none` zum Löschen.', @@ -2212,6 +2280,23 @@ const LOCALES = { theme_usage: '\u7528\u6cd5\uff1a/theme ', theme_set: '\u4e3b\u9898\uff1a', no_active_session: '\u5f53\u524d\u6ca1\u6709\u6d3b\u52a8\u4f1a\u8bdd', + cmd_queue: '\u5c06\u6d88\u606f\u52a0\u5165\u4e0b\u4e00\u8f6e\u7684\u961f\u5217', + cmd_interrupt: '\u53d6\u6d88\u5f53\u524d\u56de\u5408\u5e76\u53d1\u9001\u65b0\u6d88\u606f', + cmd_steer: '\u7528\u7ea0\u6b63\u4fe1\u606f\u5f15\u5bfc\u4ee3\u7406\uff08\u56de\u9000\u4e3a\u4e2d\u65ad\uff09', + cmd_queue_no_msg: '\u7528\u6cd5\uff1a/queue <\u6d88\u606f>', + cmd_queue_not_busy: '\u6ca1\u6709\u6d3b\u52a8\u4efb\u52a1 \u2014 \u76f4\u63a5\u53d1\u9001\u5373\u53ef', + cmd_queue_confirm: '\u6d88\u606f\u5df2\u52a0\u5165\u961f\u5217', + cmd_interrupt_no_msg: '\u7528\u6cd5\uff1a/interrupt <\u6d88\u606f>', + cmd_interrupt_confirm: '\u5df2\u4e2d\u65ad \u2014 \u6b63\u5728\u53d1\u9001\u65b0\u6d88\u606f', + cmd_steer_no_msg: '\u7528\u6cd5\uff1a/steer <\u6d88\u606f>', + cmd_steer_fallback: 'Steer \u5c1a\u4e0d\u53ef\u7528 \u2014 \u5df2\u4e2d\u65ad\u5e76\u52a0\u5165\u961f\u5217', + busy_steer_fallback: 'Steer \u4e0d\u53ef\u7528 \u2014 \u5df2\u4e2d\u65ad', + busy_interrupt_confirm: '\u5df2\u4e2d\u65ad \u2014 \u6b63\u5728\u53d1\u9001\u65b0\u6d88\u606f', + settings_label_busy_input_mode: '\u5fd9\u788c\u8f93\u5165\u6a21\u5f0f', + settings_desc_busy_input_mode: '\u63a7\u5236\u5728\u4ee3\u7406\u8fd0\u884c\u65f6\u53d1\u9001\u6d88\u606f\u7684\u884c\u4e3a\u3002\u961f\u5217\u7b49\u5f85\uff1b\u4e2d\u65ad\u53d6\u6d88\u5e76\u91cd\u65b0\u5f00\u59cb\uff1bSteer \u53d1\u9001\u7ea0\u6b63\uff08\u76ee\u524d\u56de\u9000\u4e3a\u4e2d\u65ad\uff09\u3002', + settings_busy_input_mode_queue: '\u52a0\u5165\u961f\u5217', + settings_busy_input_mode_interrupt: '\u4e2d\u65ad\u5f53\u524d\u56de\u5408', + settings_busy_input_mode_steer: 'Steer\uff08\u4e2d\u65ad + \u53d1\u9001\uff09', workspace_empty_no_path: '未选择工作区。请在 设置 → 工作区 中设置工作区以浏览文件。', workspace_empty_dir: '此工作区为空。', @@ -3118,6 +3203,23 @@ const LOCALES = { name_required: '\u9700\u8981\u540d\u7a31', never: '\u5f9e\u4e0d', no_active_session: '\u7121\u6d3b\u8e8d\u6703\u8a71', + cmd_queue: '\u5c07\u8a0a\u606f\u52a0\u5165\u4e0b\u4e00\u8f2a\u7684\u4f47\u5217', + cmd_interrupt: '\u53d6\u6d88\u7576\u524d\u56de\u5408\u4e26\u767c\u9001\u65b0\u8a0a\u606f', + cmd_steer: '\u7528\u7d0a\u6b63\u8a0a\u606f\u5f15\u5c0e\u4ee3\u7406\uff08\u56de\u9000\u70ba\u4e2d\u65ad\uff09', + cmd_queue_no_msg: '\u7528\u6cd5\uff1a/queue <\u8a0a\u606f>', + cmd_queue_not_busy: '\u6c92\u6709\u6d3b\u52d5\u4efb\u52d9 \u2014 \u76f4\u63a5\u767c\u9001\u5373\u53ef', + cmd_queue_confirm: '\u8a0a\u606f\u5df2\u52a0\u5165\u4f47\u5217', + cmd_interrupt_no_msg: '\u7528\u6cd5\uff1a/interrupt <\u8a0a\u606f>', + cmd_interrupt_confirm: '\u5df2\u4e2d\u65ad \u2014 \u6b63\u5728\u767c\u9001\u65b0\u8a0a\u606f', + cmd_steer_no_msg: '\u7528\u6cd5\uff1a/steer <\u8a0a\u606f>', + cmd_steer_fallback: 'Steer \u5c1a\u4e0d\u53ef\u7528 \u2014 \u5df2\u4e2d\u65ad\u4e26\u52a0\u5165\u4f47\u5217', + busy_steer_fallback: 'Steer \u4e0d\u53ef\u7528 \u2014 \u5df2\u4e2d\u65ad', + busy_interrupt_confirm: '\u5df2\u4e2d\u65ad \u2014 \u6b63\u5728\u767c\u9001\u65b0\u8a0a\u606f', + settings_label_busy_input_mode: '\u5fd9\u788c\u8f38\u5165\u6a21\u5f0f', + settings_desc_busy_input_mode: '\u63a7\u5236\u5728\u4ee3\u7406\u904b\u884c\u6642\u767c\u9001\u8a0a\u606f\u7684\u884c\u70ba\u3002\u4f47\u5217\u7b49\u5f85\uff1b\u4e2d\u65ad\u53d6\u6d88\u4e26\u91cd\u65b0\u958b\u59cb\uff1bSteer \u767c\u9001\u7d0a\u6b63\uff08\u76ee\u524d\u56de\u9000\u70ba\u4e2d\u65ad\uff09\u3002', + settings_busy_input_mode_queue: '\u52a0\u5165\u4f47\u5217', + settings_busy_input_mode_interrupt: '\u4e2d\u65ad\u7576\u524d\u56de\u5408', + settings_busy_input_mode_steer: 'Steer\uff08\u4e2d\u65ad + \u767c\u9001\uff09', no_active_task: '\u7121\u57f7\u884c\u4e2d\u7684\u4efb\u52d9\u53ef\u505c\u6b62\u3002', no_notes_yet: '\u5c1a\u7121\u5099\u8a3b\u3002', no_profile_yet: '\u5c1a\u7121\u8a2d\u5b9a\u6a94\u3002', diff --git a/static/index.html b/static/index.html index 6c11da75..1f05fe7e 100644 --- a/static/index.html +++ b/static/index.html @@ -662,6 +662,15 @@
Automatically re-generates the session title based on the latest exchange, keeping it relevant as the conversation evolves. Requires an LLM title generation model to be configured.
+
+ + +
Controls what happens when you send a message while the agent is running. Queue waits for the current task; Interrupt cancels and starts fresh; Steer sends a correction (currently falls back to interrupt).
+