diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8b2149..2dcc0751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,19 @@ ## [Unreleased] +## [v0.51.577] — 2026-06-22 — Release UJ (failed steer no longer cancels active run) + +### Fixed + +- **A failed Steer no longer cancels your active run.** When `/steer` (or the steer busy-input mode) couldn't be delivered — the agent isn't running/cached or doesn't support steer — WebUI used to silently fall back to interrupt+queue, cancelling the in-flight stream. It now restores your draft to the composer and leaves the active run untouched, so you explicitly choose Queue or Interrupt instead. Staged files are kept on a failed steer and only cleared once a steer is actually delivered. Thanks @dso2ng. (#4577) + ## [v0.51.576] — 2026-06-22 — Release UI (scroll read-position + mobile horizontal-pan fixes) ### Fixed - **The transcript no longer yanks you back to the bottom while you're reading mid-stream.** Scroll re-pinning now requires a deliberate move toward the bottom (and ignores tiny scroll jitter), so reading earlier messages during an active stream stays put instead of getting hijacked back to the latest token. Thanks @rodboev. (#4584, fixes #4295) - **No more accidental horizontal panning of the mobile transcript.** Wide content (long code lines, URLs) could let the message area pan sideways on phones; the transcript now clips horizontal overflow and wraps long words. Thanks @rodboev. (#4583, fixes #4553) +>>>>>>> baa5869e4 (stage #4577 (dso2ng): keep failed steer from cancelling active runs) ## [v0.51.575] — 2026-06-22 — Release UH (session-list perf for long histories) diff --git a/api/streaming.py b/api/streaming.py index f86bf49c..81d2a3da 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -8932,9 +8932,10 @@ def _handle_chat_steer(handler, body: dict) -> bool: interrupted. If no agent is cached, the agent is too old to support steer, or no - stream is active, return {"accepted": False, "fallback": ""} - so the frontend can fall back to interrupt or queue mode. The - fallback path is the existing behaviour from PR #1062. + stream is active, return {"accepted": False, "fallback": ""}. + The frontend must surface that failure without cancelling the active run; + Steer is active-run guidance, not implicit permission to Queue, Interrupt, + or Stop-and-send. Returns 200 with {"accepted": bool, "fallback": str|None, "stream_id": str|None}. @@ -8968,7 +8969,8 @@ def _handle_chat_steer(handler, body: dict) -> bool: except Exception: logger.debug("Failed to close steer identity-mismatched cached agent for session %s", sid, exc_info=True) if not cached: - # No active agent for this session — caller falls back to interrupt + # No active agent for this session — caller surfaces a steer failure + # without cancelling the active run. return j(handler, {"accepted": False, "fallback": "no_cached_agent", "stream_id": None}) agent = cached[0] diff --git a/static/commands.js b/static/commands.js index ec7daaf7..63bf57c2 100644 --- a/static/commands.js +++ b/static/commands.js @@ -1197,8 +1197,10 @@ async function cmdInterrupt(args){ * steer text to the next tool-result message so the model sees it on its * next iteration — same pathway as the CLI's /steer command. * - * Falls back to interrupt mode when the agent isn't running, isn't cached, - * or doesn't support steer (older hermes-agent versions). + * Leaves the active stream alone when the agent isn't running, isn't cached, + * or doesn't support steer (older hermes-agent versions). The failed steer text + * is restored to the composer so the user can choose Queue or Interrupt + * explicitly instead of WebUI silently cancelling the current run. */ async function cmdSteer(args){ const msg=(args||'').trim(); @@ -1218,12 +1220,15 @@ async function cmdSteer(args){ * Shared implementation for /steer and the busy_input_mode='steer' path. * * Tries the real steer endpoint first. On any non-accept response (no cached - * agent, agent lacks steer, stream dead, etc.) falls back to interrupt+queue: - * queues the message and cancels the stream so the drain re-sends it. + * agent, agent lacks steer, stream dead, etc.) it restores the draft and keeps + * the active stream running. Steer belongs to the active run; a failed Steer + * must not be silently upgraded into Queue, Interrupt, or Stop-and-send. * * @param {string} msg - The steer text. * @param {boolean} explicitSteer - True if the user explicitly invoked /steer - * (vs the busy-mode auto-fallback). Affects toast wording only. + * (vs the busy-mode auto-fallback). Affects toast wording and draft restore. + * @returns {Promise} true when the steer was delivered, false when the + * draft was restored and the active stream was left untouched. */ function _showSteerIndicator(text){ const inner=document.getElementById('msgInner'); @@ -1253,7 +1258,7 @@ async function _trySteer(msg, explicitSteer){ body:JSON.stringify({session_id:S.session.session_id,text:msg}), }); }catch(e){ - // Network or server error — fall back to interrupt + // Network or server error — keep the active stream running and restore the draft. result={accepted:false, fallback:'network_error'}; } if(result&&result.accepted){ @@ -1263,26 +1268,23 @@ async function _trySteer(msg, explicitSteer){ // all call renderMessages which rebuilds msgInner). _showSteerIndicator(msg); showToast(t('cmd_steer_delivered'),2500); - return; + return true; } - // Fall back to interrupt: queue the message + cancel the stream so the - // drain in setBusy(false) re-sends it as a fresh turn. - 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();} - // Toast wording differs based on why we're falling back so the user - // understands what just happened. - const reason=(result&&result.fallback)||'unknown'; + // Do not fall back to interrupt: Steer failure is not permission to cancel + // the active run. Restore the draft so the user can explicitly Queue or + // Interrupt if that is what they want next. Pending files remain staged. + const inp=$('msg'); + if(inp){ + inp.value=explicitSteer?`/steer ${msg}`:msg; + if(typeof autoResize==='function')autoResize(); + } + if(typeof renderTray==='function')renderTray(); if(explicitSteer){ - showToast(t('cmd_steer_fallback'),2500); - } else if(reason==='no_cached_agent'||reason==='not_running'||reason==='stream_dead'){ - // Busy mode hit the steer path before the agent was ready — - // interrupt is the natural fallback, no need to call out steer. - showToast(t('busy_interrupt_confirm'),2000); + showToast(t('cmd_steer_fallback'),3500); } else { - showToast(t('busy_steer_fallback'),2500); + showToast(t('busy_steer_fallback'),3500); } + return false; } async function cmdTitle(args){ diff --git a/static/i18n.js b/static/i18n.js index c2be776f..d78c52e7 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -294,13 +294,13 @@ const LOCALES = { cmd_interrupt_no_msg: 'Usage: /interrupt ', cmd_interrupt_confirm: 'Interrupted — sending new message', cmd_steer_no_msg: 'Usage: /steer ', - cmd_steer_fallback: 'Steer unavailable — queued for next turn instead', + cmd_steer_fallback: 'Steer unavailable — draft restored', cmd_steer_delivered: 'Steer delivered — agent will see it on its next tool result', steer_leftover_queued: 'Steer queued for next turn', - busy_steer_fallback: 'Steer unavailable — queued for next turn', + busy_steer_fallback: 'Steer unavailable — draft restored', 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 injects a correction mid-turn without interrupting (falls back to queue when agent or stream unavailable).', + 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 injects a correction mid-turn without interrupting. If Steer is unavailable, the draft is restored so you can choose the next action.', settings_label_fade_text_effect: 'Fade text effect', settings_desc_fade_text_effect: 'Fade newly streamed words in while the assistant is responding. Similar to OpenWebUI; off by default for maximum performance.', settings_busy_input_mode_queue: 'Queue follow-up', @@ -1866,10 +1866,10 @@ const LOCALES = { cmd_interrupt_no_msg: 'Uso: /interrupt ', cmd_interrupt_confirm: 'Interrotto — invio nuovo messaggio', cmd_steer_no_msg: 'Uso: /steer ', - cmd_steer_fallback: 'Steer non disponibile — messaggio in coda per il prossimo turno', + cmd_steer_fallback: 'Steer non disponibile — bozza ripristinata', cmd_steer_delivered: "Steer consegnato — l'agente lo vedrà al prossimo risultato strumento", steer_leftover_queued: 'Steer in coda per il prossimo turno', - busy_steer_fallback: 'Steer non disponibile — messaggio in coda per il prossimo turno', + busy_steer_fallback: 'Steer non disponibile — bozza ripristinata', busy_interrupt_confirm: 'Interrotto — invio nuovo messaggio', settings_label_busy_input_mode: 'Modalità input occupato', settings_desc_busy_input_mode: "Controlla cosa succede quando invii un messaggio mentre l'agente è in esecuzione. Coda attende; Interrompi annulla e ricomincia; Steer inietta una correzione a metà turno senza interrompere (fallback a coda se agente o stream non disponibili).", @@ -3429,10 +3429,10 @@ const LOCALES = { cmd_interrupt_no_msg: '使い方: /interrupt <メッセージ>', cmd_interrupt_confirm: '中断 — 新しいメッセージを送信中', cmd_steer_no_msg: '使い方: /steer <メッセージ>', - cmd_steer_fallback: 'ステア利用不可 — 代わりに次のターンへキュー', + cmd_steer_fallback: 'ステア利用不可 — 下書きを復元しました', cmd_steer_delivered: 'ステア送信完了 — 次のツール結果でエージェントが認識します', steer_leftover_queued: 'ステアを次のターンへキュー', - busy_steer_fallback: 'ステア利用不可 — 次のターンへキュー', + busy_steer_fallback: 'ステア利用不可 — 下書きを復元しました', busy_interrupt_confirm: '中断 — 新しいメッセージを送信中', settings_label_busy_input_mode: 'ビジー時の入力モード', settings_desc_busy_input_mode: 'エージェント実行中にメッセージを送信した時の動作を制御します。Queue は待機、Interrupt はキャンセルして再開、Steer は中断せずにターン中に修正を注入します (エージェントやストリームが利用不可ならキューにフォールバック)。', @@ -4943,10 +4943,10 @@ const LOCALES = { cmd_interrupt_no_msg: 'Использование: /interrupt <сообщение>', cmd_interrupt_confirm: 'Прервано — отправка нового сообщения', cmd_steer_no_msg: 'Использование: /steer <сообщение>', - cmd_steer_fallback: 'Steer недоступен — прервано и поставлено в очередь', + cmd_steer_fallback: 'Steer недоступен — черновик восстановлен', cmd_steer_delivered: 'Steer доставлен — агент увидит его в следующем ответе инструмента', steer_leftover_queued: 'Steer поставлен в очередь на следующий ход', - busy_steer_fallback: 'Steer недоступен — прервано', + busy_steer_fallback: 'Steer недоступен — черновик восстановлен', busy_interrupt_confirm: 'Прервано — отправка нового сообщения', settings_label_busy_input_mode: 'Режим ввода при занятости', settings_desc_busy_input_mode: 'Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания.', @@ -6493,10 +6493,10 @@ const LOCALES = { 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 — en cola para el siguiente turno', + cmd_steer_fallback: 'Steer no disponible — borrador restaurado', cmd_steer_delivered: 'Steer entregado \u2014 el agente lo ver\u00e1 en su pr\u00f3ximo resultado de herramienta', steer_leftover_queued: 'Steer en cola para el pr\u00f3ximo turno', - busy_steer_fallback: 'Steer no disponible — en cola para el siguiente turno', + busy_steer_fallback: 'Steer no disponible — borrador restaurado', busy_interrupt_confirm: 'Interrumpido \u2014 enviando nuevo mensaje', settings_label_busy_input_mode: 'Modo de entrada ocupada', settings_desc_busy_input_mode: 'Controla qué sucede al enviar mensajes mientras el agente está activo. Cola espera; Interrumpir cancela y empieza de nuevo; Steer inyecta una corrección sin interrumpir (usa cola si el agente no está disponible).', @@ -7964,10 +7964,10 @@ const LOCALES = { 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 nicht verf\u00fcgbar \u2014 f\u00fcr n\u00e4chsten Durchgang eingereiht', + cmd_steer_fallback: 'Steer nicht verf\u00fcgbar \u2014 Entwurf wiederhergestellt', cmd_steer_delivered: 'Steer geliefert \u2014 der Agent sieht es bei seinem n\u00e4chsten Tool-Ergebnis', steer_leftover_queued: 'Steer f\u00fcr n\u00e4chsten Durchgang eingereiht', - busy_steer_fallback: 'Steer nicht verf\u00fcgbar \u2014 f\u00fcr n\u00e4chsten Durchgang eingereiht', + busy_steer_fallback: 'Steer nicht verf\u00fcgbar \u2014 Entwurf wiederhergestellt', 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 f\u00fcgt eine Korrektur ein ohne zu unterbrechen.', @@ -9494,10 +9494,10 @@ const LOCALES = { cmd_interrupt_no_msg: '用法:/interrupt <消息>', cmd_interrupt_confirm: '已中断 — 正在发送新消息', cmd_steer_no_msg: '用法:/steer <消息>', - cmd_steer_fallback: 'Steer 不可用 — 已中断并加入队列', + cmd_steer_fallback: 'Steer 不可用 — 草稿已恢复', cmd_steer_delivered: 'Steer 已交付 — 代理将在下一个工具结果中看到', steer_leftover_queued: 'Steer 已加入下轮队列', - busy_steer_fallback: 'Steer 不可用 — 已中断', + busy_steer_fallback: 'Steer 不可用 — 草稿已恢复', busy_interrupt_confirm: '已中断 — 正在发送新消息', settings_label_busy_input_mode: '忙碌输入模式', settings_desc_busy_input_mode: '控制当代理正在运行时你发送消息会发生什么。队列会等待;中断会取消并重新开始;引导会在不中断的情况下注入中间修正(当流不可用时回退到队列)。', @@ -11001,10 +11001,10 @@ const LOCALES = { cmd_interrupt_no_msg: '用法:/interrupt <訊息>', cmd_interrupt_confirm: '已中斷,正在傳送新訊息', cmd_steer_no_msg: '用法:/steer <訊息>', - cmd_steer_fallback: 'Steer 不可用 — 已加入下一輪佇列', + cmd_steer_fallback: 'Steer 不可用 — 已恢復草稿', cmd_steer_delivered: 'Steer 已送達,Agent 會在下一個工具結果中看到', steer_leftover_queued: 'Steer 已加入下一輪佇列', - busy_steer_fallback: 'Steer 不可用 — 已加入下一輪佇列', + busy_steer_fallback: 'Steer 不可用 — 已恢復草稿', busy_interrupt_confirm: '已中斷,正在傳送新訊息', settings_label_busy_input_mode: '忙碌輸入模式', settings_desc_busy_input_mode: '控制 Agent 執行時送出訊息的行為。佇列會等待;中斷會取消並重新開始;Steer 會中途注入修正且不中斷(Agent 或串流無法使用時會退回佇列)。', @@ -12492,10 +12492,10 @@ const LOCALES = { cmd_interrupt_no_msg: 'Uso: /interrupt ', cmd_interrupt_confirm: 'Interrompido — enviando nova mensagem', cmd_steer_no_msg: 'Uso: /steer ', - cmd_steer_fallback: 'Steer indisponível — enfileirado para próximo turno', + cmd_steer_fallback: 'Steer indisponível — rascunho restaurado', cmd_steer_delivered: 'Steer entregue — agente verá no próximo resultado', steer_leftover_queued: 'Steer enfileirado para próximo turno', - busy_steer_fallback: 'Steer indisponível — enfileirado para próximo turno', + busy_steer_fallback: 'Steer indisponível — rascunho restaurado', busy_interrupt_confirm: 'Interrompido — enviando nova mensagem', settings_label_busy_input_mode: 'Modo de input ocupado', settings_desc_busy_input_mode: 'Controla o que acontece ao enviar mensagem com agente rodando. Fila espera; Interromper cancela; Steer injeta correção.', @@ -13941,10 +13941,10 @@ const LOCALES = { cmd_interrupt_no_msg: 'Usage: /interrupt ', cmd_interrupt_confirm: 'Interrupted — sending new message', cmd_steer_no_msg: 'Usage: /steer ', - cmd_steer_fallback: 'Steer unavailable — queued for next turn instead', + cmd_steer_fallback: 'Steer unavailable — draft restored', cmd_steer_delivered: 'Steer delivered — agent will see it on its next tool result', steer_leftover_queued: 'Steer queued for next turn', - busy_steer_fallback: 'Steer unavailable — queued for next turn', + busy_steer_fallback: 'Steer unavailable — draft restored', busy_interrupt_confirm: 'Interrupted — sending new message', settings_label_busy_input_mode: '작업 중 입력 방식', settings_desc_busy_input_mode: '에이전트가 실행 중일 때 메시지를 보내면 어떻게 처리할지 제어합니다. 대기는 다음 차례까지 기다리고, 중단은 현재 작업을 취소하고 새로 시작하며, 조정은 현재 작업을 중단하지 않고 중간 수정 사항을 전달합니다(에이전트 또는 스트림을 사용할 수 없으면 대기로 전환).', @@ -15488,10 +15488,10 @@ const LOCALES = { cmd_interrupt_no_msg: 'Utilisation : /interruption ', cmd_interrupt_confirm: 'Interrompu — envoi d\'un nouveau message', cmd_steer_no_msg: 'Utilisation : /steer ', - cmd_steer_fallback: 'Steer indisponible – mis en file d’attente pour le prochain tour à la place', + cmd_steer_fallback: 'Steer indisponible – brouillon restauré', cmd_steer_delivered: 'Steer livré - l\'agent le verra sur son prochain résultat d\'outil', steer_leftover_queued: 'Bœuf en attente pour le prochain tour', - busy_steer_fallback: 'Steer indisponible – en file d’attente pour le prochain tour', + busy_steer_fallback: 'Steer indisponible – brouillon restauré', busy_interrupt_confirm: 'Interrompu — envoi d\'un nouveau message', settings_label_busy_input_mode: 'Mode de saisie occupé', settings_desc_busy_input_mode: 'Contrôle ce qui se passe lorsque vous envoyez un message pendant l\'exécution de l\'agent. La file d\'attente attend ; L\'interruption s\'annule et recommence ; Steer injecte une correction à mi-tour sans interrompre (revient en file d\'attente lorsque l\'agent ou le flux est indisponible).', @@ -17011,10 +17011,10 @@ const LOCALES = { cmd_interrupt_no_msg: 'Kullanım: /interrupt ', cmd_interrupt_confirm: 'Kesintiye uğradı — yeni mesaj gönderilir', cmd_steer_no_msg: 'Kullanım: /steer ', - cmd_steer_fallback: 'Yönlendirme kullanılamıyor - bunun yerine bir sonraki dönüş için sıraya alındı', + cmd_steer_fallback: 'Yönlendirme kullanılamıyor — taslak geri yüklendi', cmd_steer_delivered: 'Yönlendirme teslim edildi — temsilci bunu bir sonraki takım sonucunda görecek', steer_leftover_queued: 'Direksiyon bir sonraki dönüş için kuyruğa alındı', - busy_steer_fallback: 'Yönlendirme kullanılamıyor — bir sonraki dönüş için kuyruğa alındı', + busy_steer_fallback: 'Yönlendirme kullanılamıyor — taslak geri yüklendi', busy_interrupt_confirm: 'Kesintiye uğradı — yeni mesaj gönderilir', settings_label_busy_input_mode: 'Meşgul giriş modu', settings_desc_busy_input_mode: 'Aracı çalışırken bir mesaj gönderdiğinizde ne olacağını kontrol eder. Sıra bekler; Interrupt iptal eder ve yeniden başlar; Steer, dönüşün ortasında kesintiye uğramadan bir düzeltme enjekte eder (aracı veya akış mevcut olmadığında kuyruğa geri döner).', @@ -18560,10 +18560,10 @@ const LOCALES = { cmd_interrupt_no_msg: 'Użycie: /interrupt ', cmd_interrupt_confirm: 'Przerwano — wysyłanie nowej wiadomości', cmd_steer_no_msg: 'Użycie: /steer ', - cmd_steer_fallback: 'Korekta niedostępna — dodano do kolejki na następną turę', + cmd_steer_fallback: 'Korekta niedostępna — przywrócono szkic', cmd_steer_delivered: 'Korekta dostarczona — agent zobaczy ją przy następnym wyniku narzędzia', steer_leftover_queued: 'Korekta dodana do kolejki na następną turę', - busy_steer_fallback: 'Korekta niedostępna — dodano do kolejki na następną turę', + busy_steer_fallback: 'Korekta niedostępna — przywrócono szkic', busy_interrupt_confirm: 'Przerwano — wysyłanie nowej wiadomości', settings_label_busy_input_mode: 'Tryb wprowadzania przy zajętości', settings_desc_busy_input_mode: 'Kontroluje, co dzieje się, gdy wysyłasz wiadomość, podczas gdy agent pracuje. Kolejkowanie czeka; Przerwanie anuluje i zaczyna od nowa; Kierowanie (Steer) wprowadza korektę w środku tury bez przerywania (cofa się do kolejkowania, gdy agent lub strumień jest niedostępny).', diff --git a/static/index.html b/static/index.html index daf1616a..619744c0 100644 --- a/static/index.html +++ b/static/index.html @@ -1305,7 +1305,7 @@ -
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 injects a mid-turn correction without interrupting (falls back to interrupt when the agent is not yet cached or the stream has ended).
+
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 injects a mid-turn correction without interrupting. If Steer is unavailable, the draft is restored so you can choose the next action.