A failed /steer (or busy-mode steer) no longer silently falls back to interrupt+queue
(which cancelled the active stream). _trySteer now returns delivered:bool, restores the
draft + leaves the stream running on failure; send() only clears staged files when
_steerDelivered===true. Backend hunk comment-only (failure paths already returned
{accepted:False}, never touched CANCEL_FLAGS). i18n value-only on pre-existing keys
(locale-parity preserved, 173 locale tests pass). 48 own tests pass.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
This commit is contained in:
8 files changed
+111
-88
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
+6
-4
@@ -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": "<reason>"}
|
||||
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": "<reason>"}.
|
||||
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]
|
||||
|
||||
+24
-22
@@ -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<boolean>} 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){
|
||||
|
||||
+27
-27
@@ -294,13 +294,13 @@ const LOCALES = {
|
||||
cmd_interrupt_no_msg: 'Usage: /interrupt <message>',
|
||||
cmd_interrupt_confirm: 'Interrupted — sending new message',
|
||||
cmd_steer_no_msg: 'Usage: /steer <message>',
|
||||
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 <messaggio>',
|
||||
cmd_interrupt_confirm: 'Interrotto — invio nuovo messaggio',
|
||||
cmd_steer_no_msg: 'Uso: /steer <messaggio>',
|
||||
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 <mensaje>',
|
||||
cmd_interrupt_confirm: 'Interrumpido \u2014 enviando nuevo mensaje',
|
||||
cmd_steer_no_msg: 'Uso: /steer <mensaje>',
|
||||
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 <Nachricht>',
|
||||
cmd_interrupt_confirm: 'Unterbrochen \u2014 neue Nachricht wird gesendet',
|
||||
cmd_steer_no_msg: 'Verwendung: /steer <Nachricht>',
|
||||
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 <mensagem>',
|
||||
cmd_interrupt_confirm: 'Interrompido — enviando nova mensagem',
|
||||
cmd_steer_no_msg: 'Uso: /steer <mensagem>',
|
||||
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 <message>',
|
||||
cmd_interrupt_confirm: 'Interrupted — sending new message',
|
||||
cmd_steer_no_msg: 'Usage: /steer <message>',
|
||||
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 <message>',
|
||||
cmd_interrupt_confirm: 'Interrompu — envoi d\'un nouveau message',
|
||||
cmd_steer_no_msg: 'Utilisation : /steer <message>',
|
||||
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 <mesaj>',
|
||||
cmd_interrupt_confirm: 'Kesintiye uğradı — yeni mesaj gönderilir',
|
||||
cmd_steer_no_msg: 'Kullanım: /steer <mesaj>',
|
||||
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 <wiadomość>',
|
||||
cmd_interrupt_confirm: 'Przerwano — wysyłanie nowej wiadomości',
|
||||
cmd_steer_no_msg: 'Użycie: /steer <wiadomość>',
|
||||
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).',
|
||||
|
||||
+1
-1
@@ -1305,7 +1305,7 @@
|
||||
<option value="interrupt" data-i18n="settings_busy_input_mode_interrupt">Interrupt current turn</option>
|
||||
<option value="steer" data-i18n="settings_busy_input_mode_steer">Steer (mid-turn correction)</option>
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_busy_input_mode">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).</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_busy_input_mode">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.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
|
||||
+8
-9
@@ -1084,16 +1084,15 @@ async function send(){
|
||||
if(busyMode==='steer'&&S.activeStreamId&&typeof _trySteer==='function'){
|
||||
// Real steer: clear the input first so the user gets immediate
|
||||
// feedback, then ship the steer payload via /api/chat/steer.
|
||||
// _trySteer falls back to queue+cancel internally if the agent
|
||||
// isn't running / cached / steer-capable.
|
||||
// _trySteer restores the draft and leaves the active stream running if
|
||||
// the agent isn't running / cached / steer-capable.
|
||||
$('msg').value='';autoResize();
|
||||
// Do NOT clear pendingFiles yet — _trySteer may fall back to
|
||||
// interrupt+queue and needs the files for queueSessionMessage.
|
||||
// _trySteer clears pendingFiles itself in the fallback path, and
|
||||
// the server returns accepted:true (no files sent) on success.
|
||||
await _trySteer(text, /*explicitSteer=*/false);
|
||||
// After _trySteer: clear any remaining files (success path).
|
||||
S.pendingFiles=[];renderTray();
|
||||
// Do NOT clear pendingFiles yet — a failed steer restores the draft and
|
||||
// must keep staged files available for the user's next explicit action.
|
||||
const _steerDelivered=await _trySteer(text, /*explicitSteer=*/false);
|
||||
// After a delivered steer, clear any staged files because the text-only
|
||||
// steer payload has been handled. On failure, keep files staged.
|
||||
if(_steerDelivered){S.pendingFiles=[];renderTray();}
|
||||
} else if(busyMode==='interrupt'){
|
||||
// Queue the message, then cancel so drain re-sends it.
|
||||
const _modelState=_chatPayloadModelState();
|
||||
|
||||
@@ -95,20 +95,20 @@ class TestSlashCommandHandlers:
|
||||
|
||||
def test_cmd_steer_delegates_to_try_steer(self):
|
||||
"""/steer delegates to _trySteer which calls /api/chat/steer with
|
||||
a queue+cancel fallback. The fallback path is exercised by tests
|
||||
a non-destructive fallback. The fallback path is exercised by tests
|
||||
in test_real_steer.py — this test just pins the delegation."""
|
||||
idx = COMMANDS_JS.find("async function cmdSteer(")
|
||||
assert idx >= 0
|
||||
body = COMMANDS_JS[idx:idx + 800]
|
||||
# cmdSteer now delegates to _trySteer; the fallback (queueSessionMessage
|
||||
# + cancelStream) lives inside _trySteer.
|
||||
# cmdSteer delegates to _trySteer; fallback must not queue+cancel.
|
||||
assert "_trySteer" in body, "cmdSteer must call _trySteer to use the real /api/chat/steer endpoint"
|
||||
# The shared helper must contain the fallback path
|
||||
# The shared helper must contain the non-destructive fallback path.
|
||||
helper_idx = COMMANDS_JS.find("async function _trySteer(")
|
||||
assert helper_idx >= 0, "_trySteer helper must exist"
|
||||
helper_body = COMMANDS_JS[helper_idx:helper_idx + 2000]
|
||||
assert "queueSessionMessage" in helper_body
|
||||
assert "cancelStream" in helper_body
|
||||
assert "queueSessionMessage" not in helper_body
|
||||
assert "cancelStream" not in helper_body
|
||||
assert "inp.value" in helper_body
|
||||
# Toast should differ from interrupt to signal it's the steer path
|
||||
assert "cmd_steer_fallback" in helper_body or "steer_fallback" in helper_body
|
||||
|
||||
@@ -116,14 +116,13 @@ class TestSlashCommandHandlers:
|
||||
# ── send() busy branch ───────────────────────────────────────────────────
|
||||
|
||||
def test_slash_commands_clear_pending_files(self):
|
||||
"""All three busy command handlers must clear S.pendingFiles (directly
|
||||
or via _trySteer) after enqueuing, so staged files are not duplicated.
|
||||
"""Queue/interrupt clear S.pendingFiles after enqueuing; steer failure
|
||||
preserves staged files so the user can choose the next explicit action.
|
||||
|
||||
cmdQueue and cmdInterrupt call queueSessionMessage themselves and clear
|
||||
S.pendingFiles directly. cmdSteer delegates to _trySteer. The fallback/interrupt path clears
|
||||
S.pendingFiles inside _trySteer; the success path returns early and
|
||||
send() handles the post-await clear. Either way files are not
|
||||
duplicated — we verify by checking _trySteer body for the clearing.
|
||||
S.pendingFiles directly. cmdSteer delegates to _trySteer. _trySteer no
|
||||
longer clears files on failure because it no longer falls back to
|
||||
cancel-and-queue behavior.
|
||||
"""
|
||||
# cmdQueue and cmdInterrupt clear pendingFiles directly
|
||||
for fn_name in ("cmdQueue", "cmdInterrupt"):
|
||||
@@ -136,17 +135,13 @@ class TestSlashCommandHandlers:
|
||||
assert "renderTray()" in body, (
|
||||
f"{fn_name} must call renderTray() after clearing pendingFiles"
|
||||
)
|
||||
# cmdSteer delegates to _trySteer; that helper clears pendingFiles
|
||||
# cmdSteer delegates to _trySteer; the helper must not clear files in
|
||||
# the fallback path because the draft is restored instead of queued.
|
||||
idx_try = COMMANDS_JS.find("function _trySteer(")
|
||||
assert idx_try >= 0, "_trySteer not found"
|
||||
try_body = COMMANDS_JS[idx_try:idx_try + 1600]
|
||||
assert "S.pendingFiles=[]" in try_body, (
|
||||
"_trySteer must clear S.pendingFiles in its fallback path — "
|
||||
"without this, files are lost on steer→interrupt fallback"
|
||||
)
|
||||
assert "renderTray()" in try_body, (
|
||||
"_trySteer must call renderTray() after clearing pendingFiles"
|
||||
)
|
||||
assert "S.pendingFiles=[]" not in try_body
|
||||
assert "renderTray()" in try_body
|
||||
|
||||
|
||||
class TestBusySendButton:
|
||||
@@ -296,6 +291,22 @@ class TestSendBusyBranchDispatch:
|
||||
"after setBusy(false) picks up the queued message"
|
||||
)
|
||||
|
||||
def test_send_busy_steer_preserves_files_when_steer_not_delivered(self):
|
||||
"""Busy-mode steer must only clear staged files after delivered steer.
|
||||
|
||||
A failed steer restores the draft and leaves the active stream running,
|
||||
so staged files must remain available for the user's next explicit
|
||||
Queue or Interrupt action.
|
||||
"""
|
||||
send_idx = MESSAGES_JS.find("async function send(")
|
||||
assert send_idx >= 0, "send() not found"
|
||||
steer_idx = MESSAGES_JS.find("busyMode==='steer'", send_idx)
|
||||
assert steer_idx >= 0, "busy steer branch not found"
|
||||
branch = MESSAGES_JS[steer_idx:steer_idx + 900]
|
||||
assert "const _steerDelivered=await _trySteer" in branch
|
||||
assert "if(_steerDelivered){S.pendingFiles=[];renderTray();}" in branch
|
||||
assert branch.index("const _steerDelivered=await _trySteer") < branch.index("if(_steerDelivered){S.pendingFiles=[];renderTray();}")
|
||||
|
||||
|
||||
def test_slash_commands_intercepted_before_busymode_routing(self):
|
||||
"""Busy-control slash commands (/steer /interrupt /queue /yolo) must be
|
||||
|
||||
@@ -7,7 +7,8 @@ text to the next tool-result message — no interruption.
|
||||
|
||||
Falls back to {"accepted": false, "fallback": "<reason>"} when the agent
|
||||
isn't running, isn't cached, or doesn't support steer (older agent versions).
|
||||
The frontend uses the fallback signal to drop back to interrupt mode.
|
||||
The frontend uses the fallback signal to restore the draft without cancelling
|
||||
the active run.
|
||||
|
||||
Plus a leftover-delivery flow: if the agent finishes its turn before the
|
||||
steer is consumed (no tool-call boundary), _drain_pending_steer is called
|
||||
@@ -266,13 +267,14 @@ class TestFrontendWiring:
|
||||
assert "/api/chat/steer" in body, "_trySteer must POST to /api/chat/steer"
|
||||
assert "method:'POST'" in body or 'method:"POST"' in body
|
||||
|
||||
def test_try_steer_handles_fallback(self):
|
||||
def test_try_steer_handles_fallback_without_cancelling(self):
|
||||
idx = self.cmds.find("async function _trySteer(")
|
||||
body = self.cmds[idx:idx + 1500]
|
||||
# Must check result.accepted and fall back via queueSessionMessage + cancelStream
|
||||
# Must check result.accepted and surface fallback without queueing or cancelling.
|
||||
assert "result&&result.accepted" in body or "result.accepted" in body
|
||||
assert "queueSessionMessage" in body
|
||||
assert "cancelStream" in body, "fallback path must cancel the stream"
|
||||
assert "queueSessionMessage" not in body
|
||||
assert "cancelStream" not in body, "fallback path must not cancel the stream"
|
||||
assert "inp.value" in body, "fallback path must restore the composer draft"
|
||||
|
||||
def test_send_busy_steer_uses_try_steer(self):
|
||||
# send() in messages.js: when busyMode === 'steer', should call _trySteer
|
||||
|
||||
Reference in new issue
Block a user