Merge pull request #5405 from nesquena/gate-rebase/5162-steer-v3
Release v0.51.823: #5162 default message mode rename + steer default (#5145)
This commit is contained in:
18 files changed
+442
-236
No files matched your search
@@ -3,6 +3,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **The busy-time send behavior is now called "Default message mode," and new installs default to Steer.** The Settings → Preferences control formerly labeled "Busy input mode" is renamed to "Default message mode," and a fresh install now defaults to **Steer** (inject a mid-turn correction without interrupting) instead of Queue. Your existing choice is preserved — if you ever saved settings, your current mode (Queue/Interrupt/Steer) is migrated as-is and unchanged; only never-configured installs pick up the new Steer default. The saved preference still survives a reload or a brief server outage (the localStorage mirror from the previous release is intact). Thanks @rodboev. (#5162, #5145)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The sidebar recovers on its own after a brief server blip during a profile switch.** If the backend returned a transient 502/503/504 (e.g. an nginx→backend restart window) on a warm session-list refresh — a profile switch, tab focus, or reconnect — the sidebar gave up on the first try and sat on the previous profile's list until you hard-reloaded. The idempotent session-list fetch now retries those transient upstream statuses on every refresh (not just the first cold load), so it heals itself without a Ctrl+F5. Thanks @weidzhou for the report and root-cause. (#5394)
|
||||
|
||||
+14
-2
@@ -8060,7 +8060,7 @@ _SETTINGS_DEFAULTS = {
|
||||
"dashboard_plugins": {}, # plugin_name -> bool, opt-in per plugin (default off per PF-10b)
|
||||
"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
|
||||
"default_message_mode": "steer", # behavior when sending while agent is running: queue | interrupt | steer
|
||||
"password_hash": None, # PBKDF2-HMAC-SHA256 hash; None = auth disabled
|
||||
"auth_disabled_acknowledged": False, # user acknowledged unauthenticated risk
|
||||
"provider_cost_budget": None,
|
||||
@@ -8171,6 +8171,12 @@ def load_settings() -> dict:
|
||||
if k not in _SETTINGS_LEGACY_DROP_KEYS
|
||||
}
|
||||
)
|
||||
if (
|
||||
"default_message_mode" not in stored
|
||||
and "busy_input_mode" in stored
|
||||
):
|
||||
settings["default_message_mode"] = stored.get("busy_input_mode")
|
||||
settings.pop("busy_input_mode", None)
|
||||
# Grandfather established installs OFF for show_cli_sessions (#3988).
|
||||
# The default flipped True so NEW users see CLI/TUI/messaging
|
||||
# sessions without hunting for the toggle — but an existing user
|
||||
@@ -8229,7 +8235,7 @@ _SETTINGS_ENUM_VALUES = {
|
||||
"sidebar_density": {"compact", "detailed"},
|
||||
"font_size": {"small", "default", "large", "xlarge"},
|
||||
"auto_title_refresh_every": {"0", "5", "10", "20"},
|
||||
"busy_input_mode": {"queue", "interrupt", "steer"},
|
||||
"default_message_mode": {"queue", "interrupt", "steer"},
|
||||
"chat_activity_display_mode": {"compact_worklog", "transparent_stream"},
|
||||
"structured_code_default_view": {"auto", "on", "off"},
|
||||
}
|
||||
@@ -8323,6 +8329,12 @@ def save_settings(settings: dict) -> dict:
|
||||
"activity_feed_expanded_default"
|
||||
)
|
||||
settings.pop("activity_feed_expanded_default", None)
|
||||
if (
|
||||
"default_message_mode" not in settings
|
||||
and "busy_input_mode" in settings
|
||||
):
|
||||
settings["default_message_mode"] = settings.get("busy_input_mode")
|
||||
settings.pop("busy_input_mode", None)
|
||||
settings.pop("simplified_tool_calling", None)
|
||||
pending_theme = current.get("theme")
|
||||
pending_skin = current.get("skin")
|
||||
|
||||
@@ -100,7 +100,7 @@ of Stop-and-send.
|
||||
|
||||
## Settings Model
|
||||
|
||||
Current Settings expose `Busy input mode` with three choices:
|
||||
Current Settings expose `Default message mode` with three choices:
|
||||
|
||||
- Queue follow-up
|
||||
- Interrupt current turn
|
||||
|
||||
+37
-24
@@ -1064,34 +1064,43 @@ function _micToastKeyForRecognitionError(error){
|
||||
window._micActive=window._micActive||false;
|
||||
window._micPendingSend=window._micPendingSend||false;
|
||||
|
||||
// ── Busy input mode eager default (#5167) ───────────────────────────────────
|
||||
// The Busy input mode preference (queue/interrupt/steer) is read on the send
|
||||
// path via `window._busyInputMode||'queue'`. The authoritative value only
|
||||
// arrives once the async boot IIFE below resolves the `/api/settings` fetch.
|
||||
// Without an eager value, every send during that boot window silently falls
|
||||
// back to 'queue', ignoring a saved 'steer'/'interrupt' preference (worse on
|
||||
// ── Default message mode eager default (#5167 / #5145) ──────────────────────
|
||||
// The Default message mode preference (queue/interrupt/steer) is read on the
|
||||
// send path via `window._defaultMessageMode||'steer'`. The authoritative value
|
||||
// only arrives once the async boot IIFE below resolves the `/api/settings`
|
||||
// fetch. Without an eager value, every send during that boot window silently
|
||||
// falls back, ignoring a saved 'queue'/'interrupt' preference (worse on
|
||||
// slow/contended environments like WSL2, see #5132). Mirror the resolved value
|
||||
// into localStorage — the same synchronous-source pattern used by hermes-lang /
|
||||
// hermes-theme — so the very first send after a reload honors the saved choice.
|
||||
const _BUSY_INPUT_MODES=['queue','interrupt','steer'];
|
||||
function _normalizeBusyInputMode(mode){
|
||||
return _BUSY_INPUT_MODES.includes(mode)?mode:'queue';
|
||||
const _DEFAULT_MESSAGE_MODES=['queue','interrupt','steer'];
|
||||
// Legacy localStorage key (pre-#5145 rename); read it as a fallback so an
|
||||
// existing user's persisted busy-input-mode preference survives the rename.
|
||||
const _LEGACY_DEFAULT_MESSAGE_MODE_KEY='hermes-busy-input-mode';
|
||||
const _DEFAULT_MESSAGE_MODE_KEY='hermes-default-message-mode';
|
||||
function _normalizeDefaultMessageMode(mode){
|
||||
return _DEFAULT_MESSAGE_MODES.includes(mode)?mode:'steer';
|
||||
}
|
||||
function _persistBusyInputMode(mode){
|
||||
const m=_normalizeBusyInputMode(mode);
|
||||
try{localStorage.setItem('hermes-busy-input-mode',m);}catch(_){}
|
||||
function _persistDefaultMessageMode(mode){
|
||||
const m=_normalizeDefaultMessageMode(mode);
|
||||
try{localStorage.setItem(_DEFAULT_MESSAGE_MODE_KEY,m);}catch(_){}
|
||||
return m;
|
||||
}
|
||||
function _readPersistedBusyInputMode(){
|
||||
function _readPersistedDefaultMessageMode(){
|
||||
let stored=null;
|
||||
try{stored=localStorage.getItem('hermes-busy-input-mode');}catch(_){}
|
||||
return _normalizeBusyInputMode(stored);
|
||||
try{
|
||||
// Prefer the new key; fall back to the legacy key so a pre-rename
|
||||
// preference is honored until the next explicit save rewrites the new key.
|
||||
stored=localStorage.getItem(_DEFAULT_MESSAGE_MODE_KEY);
|
||||
if(stored===null||stored===undefined) stored=localStorage.getItem(_LEGACY_DEFAULT_MESSAGE_MODE_KEY);
|
||||
}catch(_){}
|
||||
return _normalizeDefaultMessageMode(stored);
|
||||
}
|
||||
window._persistBusyInputMode=_persistBusyInputMode;
|
||||
window._readPersistedBusyInputMode=_readPersistedBusyInputMode;
|
||||
window._persistDefaultMessageMode=_persistDefaultMessageMode;
|
||||
window._readPersistedDefaultMessageMode=_readPersistedDefaultMessageMode;
|
||||
// Eager default set BEFORE the async settings fetch resolves so first sends in
|
||||
// the boot window honor the persisted preference instead of defaulting to queue.
|
||||
window._busyInputMode=_readPersistedBusyInputMode();
|
||||
// the boot window honor the persisted preference instead of the raw default.
|
||||
window._defaultMessageMode=_readPersistedDefaultMessageMode();
|
||||
|
||||
// ── Extension TTS-engine registry (registerHermesTtsEngine) ──────────────────
|
||||
// Defined at MODULE scope (not inside the voice-mode IIFE below) so the public
|
||||
@@ -2764,7 +2773,11 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
||||
stringChars:parseInt(s.inflight_state_max_string_chars||60000,10)||60000,
|
||||
jsonChars:parseInt(s.inflight_state_max_json_chars||1500000,10)||1500000,
|
||||
};
|
||||
window._busyInputMode=_persistBusyInputMode(s.busy_input_mode);
|
||||
// #5162 rename + steer default, layered on the #5170 localStorage mirror:
|
||||
// resolve the mode (new key, legacy busy_input_mode fallback, else 'steer'
|
||||
// via _normalizeDefaultMessageMode) and persist it so the very first send
|
||||
// after a reload honors the saved choice.
|
||||
window._defaultMessageMode=_persistDefaultMessageMode(s.default_message_mode||s.busy_input_mode);
|
||||
window._showBusyPlaceholderHint=!!s.show_busy_placeholder_hint;
|
||||
window._sessionEndlessScrollEnabled=!!s.session_endless_scroll;
|
||||
window._autoScrollFollow=s.auto_scroll_follow!==false;
|
||||
@@ -2890,12 +2903,12 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
||||
window._structuredCodeAutoTreeLines=10;
|
||||
window._sidebarDensity='compact';
|
||||
window._pinnedSessionsLimit=3;
|
||||
// Settings load failed: keep the persisted busy-input-mode preference (the
|
||||
// eager default already read it from the localStorage mirror) instead of
|
||||
// clobbering it with 'queue', so a saved 'steer'/'interrupt' still applies
|
||||
// Settings load failed: keep the persisted default-message-mode preference
|
||||
// (the eager default already read it from the localStorage mirror) instead
|
||||
// of clobbering it, so a saved 'steer'/'interrupt'/'queue' still applies
|
||||
// when the server is unreachable (#5167). The placeholder-hint has no
|
||||
// persisted mirror, so it defaults off on failure.
|
||||
window._busyInputMode=_readPersistedBusyInputMode();
|
||||
window._defaultMessageMode=_readPersistedDefaultMessageMode();
|
||||
window._showBusyPlaceholderHint=false;
|
||||
window._sessionEndlessScrollEnabled=false;
|
||||
window._autoScrollFollow=true;
|
||||
|
||||
+3
-3
@@ -1273,12 +1273,12 @@ async function cmdGoal(args){
|
||||
}
|
||||
|
||||
// ── Busy-input mode commands ──────────────────────────────────────────────
|
||||
// These commands let users override the default busy_input_mode setting for a
|
||||
// These commands let users override the default message mode setting for a
|
||||
// specific message. They are only meaningful while the agent is running.
|
||||
|
||||
/**
|
||||
* /queue <message> — Explicitly queue a message for the next turn.
|
||||
* Works regardless of the busy_input_mode setting.
|
||||
* Works regardless of the default message mode setting.
|
||||
*/
|
||||
async function cmdQueue(args){
|
||||
const msg=(args||'').trim();
|
||||
@@ -1403,7 +1403,7 @@ function _showSteerRecovery(msg, explicitSteer, fallback) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared implementation for /steer and the busy_input_mode='steer' path.
|
||||
* Shared implementation for /steer and the default_message_mode='steer' path.
|
||||
*
|
||||
* Tries the real steer endpoint first. On any non-accept response (no cached
|
||||
* agent, agent lacks steer, stream dead, etc.) it restores the draft and keeps
|
||||
|
||||
+70
-70
@@ -309,17 +309,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
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. If Steer is unavailable, the draft is restored so you can choose the next action.',
|
||||
settings_label_default_message_mode: 'Default message mode',
|
||||
settings_desc_default_message_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_label_max_tokens: 'Max output tokens',
|
||||
settings_desc_max_tokens: 'Caps assistant output for new streaming turns. Leave blank to use the active profile config fallback.',
|
||||
settings_placeholder_max_tokens_none: 'No override',
|
||||
settings_placeholder_max_tokens_fallback: 'No override (fallback: {0})',
|
||||
settings_busy_input_mode_queue: 'Queue follow-up',
|
||||
settings_busy_input_mode_interrupt: 'Interrupt current turn',
|
||||
settings_busy_input_mode_steer: 'Steer (mid-turn correction)',
|
||||
settings_default_message_mode_queue: 'Queue follow-up',
|
||||
settings_default_message_mode_interrupt: 'Interrupt current turn',
|
||||
settings_default_message_mode_steer: 'Steer (mid-turn correction)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -1978,17 +1978,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
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).",
|
||||
settings_label_default_message_mode: 'Modalità messaggio predefinita',
|
||||
settings_desc_default_message_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. Se Steer non è disponibile, la bozza viene ripristinata così puoi scegliere l'azione successiva.",
|
||||
settings_label_fade_text_effect: 'Effetto dissolvenza testo',
|
||||
settings_desc_fade_text_effect: 'Dissolve gradualmente le nuove parole trasmesse in streaming mentre l’assistente risponde. Simile a OpenWebUI; disattivato per impostazione predefinita per massime prestazioni.',
|
||||
settings_label_max_tokens: 'Token massimi in uscita',
|
||||
settings_desc_max_tokens: 'Limita la lunghezza delle nuove risposte in streaming. Lascia vuoto per usare il valore di fallback del profilo attivo.',
|
||||
settings_placeholder_max_tokens_none: 'Nessun override',
|
||||
settings_placeholder_max_tokens_fallback: 'Nessun override (fallback: {0})',
|
||||
settings_busy_input_mode_queue: 'Accoda follow-up',
|
||||
settings_busy_input_mode_interrupt: 'Interrompi turno corrente',
|
||||
settings_busy_input_mode_steer: 'Steer (correzione a metà turno)',
|
||||
settings_default_message_mode_queue: 'Accoda follow-up',
|
||||
settings_default_message_mode_interrupt: 'Interrompi turno corrente',
|
||||
settings_default_message_mode_steer: 'Steer (correzione a metà turno)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -3638,17 +3638,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
busy_interrupt_confirm: '中断 — 新しいメッセージを送信中',
|
||||
settings_label_busy_input_mode: 'ビジー時の入力モード',
|
||||
settings_desc_busy_input_mode: 'エージェント実行中にメッセージを送信した時の動作を制御します。Queue は待機、Interrupt はキャンセルして再開、Steer は中断せずにターン中に修正を注入します (エージェントやストリームが利用不可ならキューにフォールバック)。',
|
||||
settings_label_default_message_mode: '既定のメッセージモード',
|
||||
settings_desc_default_message_mode: 'エージェント実行中にメッセージを送信した時の動作を制御します。Queue は待機、Interrupt はキャンセルして再開、Steer は中断せずにターン中に修正を注入します。Steer が利用できない場合は、下書きが復元されるため、次の操作を選べます。',
|
||||
settings_label_fade_text_effect: 'テキストのフェード効果',
|
||||
settings_desc_fade_text_effect: 'アシスタントの応答中に新しくストリーミングされた単語をフェードインします。OpenWebUI に似た表示です。最大パフォーマンスのため既定ではオフです。',
|
||||
settings_label_max_tokens: '最大出力トークン数',
|
||||
settings_desc_max_tokens: '新しいストリーミング応答の出力量を制限します。空欄のままにすると、現在のプロファイルのフォールバック値を使います。',
|
||||
settings_placeholder_max_tokens_none: '上書きなし',
|
||||
settings_placeholder_max_tokens_fallback: '上書きなし(フォールバック: {0})',
|
||||
settings_busy_input_mode_queue: 'フォローアップをキュー',
|
||||
settings_busy_input_mode_interrupt: '現在のターンを中断',
|
||||
settings_busy_input_mode_steer: 'ステア (ターン中の修正)',
|
||||
settings_default_message_mode_queue: 'フォローアップをキュー',
|
||||
settings_default_message_mode_interrupt: '現在のターンを中断',
|
||||
settings_default_message_mode_steer: 'ステア (ターン中の修正)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -5249,17 +5249,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
busy_interrupt_confirm: 'Прервано — отправка нового сообщения',
|
||||
settings_label_busy_input_mode: 'Режим ввода при занятости',
|
||||
settings_desc_busy_input_mode: 'Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания.',
|
||||
settings_label_default_message_mode: 'Режим сообщений по умолчанию',
|
||||
settings_desc_default_message_mode: 'Определяет поведение при отправке сообщения во время работы агента. Очередь ждёт; Прерывание отменяет и начинает заново; Steer внедряет коррекцию без прерывания. Если Steer недоступен, черновик восстанавливается, чтобы вы могли выбрать следующее действие.',
|
||||
settings_label_fade_text_effect: 'Эффект плавного появления текста',
|
||||
settings_desc_fade_text_effect: 'Плавно показывает новые слова во время ответа ассистента. Похоже на OpenWebUI; по умолчанию выключено для максимальной производительности.',
|
||||
settings_label_max_tokens: 'Максимум выходных токенов',
|
||||
settings_desc_max_tokens: 'Ограничивает длину новых потоковых ответов. Оставьте пустым, чтобы использовать резервное значение текущего профиля.',
|
||||
settings_placeholder_max_tokens_none: 'Переопределения нет',
|
||||
settings_placeholder_max_tokens_fallback: 'Переопределения нет (резерв: {0})',
|
||||
settings_busy_input_mode_queue: 'Поставить в очередь',
|
||||
settings_busy_input_mode_interrupt: 'Прервать текущий оборот',
|
||||
settings_busy_input_mode_steer: 'Steer (прерывание + отправка)',
|
||||
settings_default_message_mode_queue: 'Поставить в очередь',
|
||||
settings_default_message_mode_interrupt: 'Прервать текущий оборот',
|
||||
settings_default_message_mode_steer: 'Steer (коррекция в середине хода)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -6896,17 +6896,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
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).',
|
||||
settings_label_default_message_mode: 'Modo de mensaje predeterminado',
|
||||
settings_desc_default_message_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. Si Steer no está disponible, el borrador se restaura para que puedas elegir la siguiente acción.',
|
||||
settings_label_fade_text_effect: 'Efecto de desvanecimiento de texto',
|
||||
settings_desc_fade_text_effect: 'Hace aparecer gradualmente las palabras nuevas mientras el asistente responde. Similar a OpenWebUI; desactivado por defecto para máximo rendimiento.',
|
||||
settings_label_max_tokens: 'Máximo de tokens de salida',
|
||||
settings_desc_max_tokens: 'Limita la longitud de las nuevas respuestas en streaming. Déjalo en blanco para usar el valor de respaldo del perfil activo.',
|
||||
settings_placeholder_max_tokens_none: 'Sin reemplazo',
|
||||
settings_placeholder_max_tokens_fallback: 'Sin reemplazo (respaldo: {0})',
|
||||
settings_busy_input_mode_queue: 'Poner en cola',
|
||||
settings_busy_input_mode_interrupt: 'Interrumpir turno actual',
|
||||
settings_busy_input_mode_steer: 'Steer (corrección a mitad de turno)',
|
||||
settings_default_message_mode_queue: 'Poner en cola',
|
||||
settings_default_message_mode_interrupt: 'Interrumpir turno actual',
|
||||
settings_default_message_mode_steer: 'Steer (corrección a mitad de turno)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -8464,17 +8464,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
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.',
|
||||
settings_label_default_message_mode: 'Standard-Nachrichtenmodus',
|
||||
settings_desc_default_message_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. Wenn Steer nicht verf\u00fcgbar ist, wird der Entwurf wiederhergestellt, damit Sie die n\u00e4chste Aktion w\u00e4hlen k\u00f6nnen.',
|
||||
settings_label_fade_text_effect: 'Text-Fade-Effekt',
|
||||
settings_desc_fade_text_effect: 'Blendet neu gestreamte Wörter während der Antwort des Assistenten sanft ein. Ähnlich wie OpenWebUI; für maximale Leistung standardmäßig deaktiviert.',
|
||||
settings_label_max_tokens: 'Maximale Ausgabetokens',
|
||||
settings_desc_max_tokens: 'Begrenzt die Länge neuer Streaming-Antworten. Leer lassen, um den Fallback-Wert des aktiven Profils zu verwenden.',
|
||||
settings_placeholder_max_tokens_none: 'Keine Überschreibung',
|
||||
settings_placeholder_max_tokens_fallback: 'Keine Überschreibung (Fallback: {0})',
|
||||
settings_busy_input_mode_queue: 'In Warteschlange einreihen',
|
||||
settings_busy_input_mode_interrupt: 'Aktuellen Durchgang unterbrechen',
|
||||
settings_busy_input_mode_steer: 'Steer (Korrektur ohne Unterbrechung)',
|
||||
settings_default_message_mode_queue: 'In Warteschlange einreihen',
|
||||
settings_default_message_mode_interrupt: 'Aktuellen Durchgang unterbrechen',
|
||||
settings_default_message_mode_steer: 'Steer (Korrektur ohne Unterbrechung)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -10091,17 +10091,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: '重试',
|
||||
steer_recovery_dismiss: '忽略',
|
||||
busy_interrupt_confirm: '已中断 — 正在发送新消息',
|
||||
settings_label_busy_input_mode: '忙碌输入模式',
|
||||
settings_desc_busy_input_mode: '控制当代理正在运行时你发送消息会发生什么。队列会等待;中断会取消并重新开始;引导会在不中断的情况下注入中间修正(当流不可用时回退到队列)。',
|
||||
settings_label_default_message_mode: '默认消息模式',
|
||||
settings_desc_default_message_mode: '控制当代理正在运行时你发送消息会发生什么。队列会等待;中断会取消并重新开始;引导会在不中断的情况下注入中间修正。如果引导不可用,将恢复草稿,方便你选择下一步操作。',
|
||||
settings_label_fade_text_effect: '淡入文字效果',
|
||||
settings_desc_fade_text_effect: '在助手回复时淡入新流式单词。类似于 OpenWebUI;默认关闭以获得最佳性能。',
|
||||
settings_label_max_tokens: '最大输出令牌数',
|
||||
settings_desc_max_tokens: '限制新的流式响应可生成的输出量。留空时使用当前配置文件中的回退值。',
|
||||
settings_placeholder_max_tokens_none: '无覆盖',
|
||||
settings_placeholder_max_tokens_fallback: '无覆盖(回退值:{0})',
|
||||
settings_busy_input_mode_queue: '队列后续消息',
|
||||
settings_busy_input_mode_interrupt: '中断当前轮次',
|
||||
settings_busy_input_mode_steer: '引导(中间修正)',
|
||||
settings_default_message_mode_queue: '队列后续消息',
|
||||
settings_default_message_mode_interrupt: '中断当前轮次',
|
||||
settings_default_message_mode_steer: '引导(中间修正)',
|
||||
settings_label_busy_placeholder_hint: '显示忙碌占位提示',
|
||||
settings_desc_busy_placeholder_hint: '在会话运行且草稿为空时,显示带有当前忙碌操作的编辑器占位符。',
|
||||
composer_placeholder_busy_queue: 'Enter = 队列 | /interrupt | /background | /steer',
|
||||
@@ -11695,17 +11695,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
busy_interrupt_confirm: '已中斷,正在傳送新訊息',
|
||||
settings_label_busy_input_mode: '忙碌輸入模式',
|
||||
settings_desc_busy_input_mode: '控制 Agent 執行時送出訊息的行為。佇列會等待;中斷會取消並重新開始;Steer 會中途注入修正且不中斷(Agent 或串流無法使用時會退回佇列)。',
|
||||
settings_label_default_message_mode: '預設訊息模式',
|
||||
settings_desc_default_message_mode: '控制 Agent 執行時送出訊息的行為。佇列會等待;中斷會取消並重新開始;Steer 會中途注入修正且不中斷。如果 Steer 不可用,會恢復草稿,方便你選擇下一步操作。',
|
||||
settings_label_fade_text_effect: '文字淡入效果',
|
||||
settings_desc_fade_text_effect: '在助理回覆時讓新串流輸出的詞語淡入顯示。類似 OpenWebUI;為獲得最佳效能預設關閉。',
|
||||
settings_label_max_tokens: '最大輸出權杖數',
|
||||
settings_desc_max_tokens: '限制新的串流回覆可產生的輸出量。留空時使用目前設定檔中的回退值。',
|
||||
settings_placeholder_max_tokens_none: '無覆寫',
|
||||
settings_placeholder_max_tokens_fallback: '無覆寫(回退值:{0})',
|
||||
settings_busy_input_mode_queue: '加入佇列',
|
||||
settings_busy_input_mode_interrupt: '中斷目前回合',
|
||||
settings_busy_input_mode_steer: 'Steer(中途修正)',
|
||||
settings_default_message_mode_queue: '加入佇列',
|
||||
settings_default_message_mode_interrupt: '中斷目前回合',
|
||||
settings_default_message_mode_steer: 'Steer(中途修正)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -13284,17 +13284,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
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.',
|
||||
settings_label_default_message_mode: 'Modo de mensagem padrão',
|
||||
settings_desc_default_message_mode: 'Controla o que acontece ao enviar mensagem com agente rodando. Fila espera; Interromper cancela e recomeça; Steer injeta correção no meio do turno sem interromper. Se Steer não estiver disponível, o rascunho é restaurado para você escolher a próxima ação.',
|
||||
settings_label_fade_text_effect: 'Efeito de fade no texto',
|
||||
settings_desc_fade_text_effect: 'Faz novas palavras aparecerem gradualmente enquanto o assistente responde. Similar ao OpenWebUI; desativado por padrão para melhor desempenho.',
|
||||
settings_label_max_tokens: 'Máximo de tokens de saída',
|
||||
settings_desc_max_tokens: 'Limita o tamanho das novas respostas em streaming. Deixe em branco para usar o valor de fallback do perfil ativo.',
|
||||
settings_placeholder_max_tokens_none: 'Sem substituição',
|
||||
settings_placeholder_max_tokens_fallback: 'Sem substituição (fallback: {0})',
|
||||
settings_busy_input_mode_queue: 'Enfileirar follow-up',
|
||||
settings_busy_input_mode_interrupt: 'Interromper turno atual',
|
||||
settings_busy_input_mode_steer: 'Steer (correção no meio do turno)',
|
||||
settings_default_message_mode_queue: 'Enfileirar follow-up',
|
||||
settings_default_message_mode_interrupt: 'Interromper turno atual',
|
||||
settings_default_message_mode_steer: 'Steer (correção no meio do turno)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -14827,17 +14827,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
busy_interrupt_confirm: 'Interrupted — sending new message',
|
||||
settings_label_busy_input_mode: '작업 중 입력 방식',
|
||||
settings_desc_busy_input_mode: '에이전트가 실행 중일 때 메시지를 보내면 어떻게 처리할지 제어합니다. 대기는 다음 차례까지 기다리고, 중단은 현재 작업을 취소하고 새로 시작하며, 조정은 현재 작업을 중단하지 않고 중간 수정 사항을 전달합니다(에이전트 또는 스트림을 사용할 수 없으면 대기로 전환).',
|
||||
settings_label_default_message_mode: '기본 메시지 모드',
|
||||
settings_desc_default_message_mode: '에이전트가 실행 중일 때 메시지를 보내면 어떻게 처리할지 제어합니다. 대기는 다음 차례까지 기다리고, 중단은 현재 작업을 취소하고 새로 시작하며, 조정은 현재 작업을 중단하지 않고 중간 수정 사항을 전달합니다. 조정을 사용할 수 없으면 초안이 복원되어 다음 동작을 선택할 수 있습니다.',
|
||||
settings_label_fade_text_effect: '텍스트 페이드 효과',
|
||||
settings_desc_fade_text_effect: '어시스턴트가 응답하는 동안 새로 스트리밍되는 단어를 부드럽게 표시합니다. OpenWebUI와 비슷하며, 최대 성능을 위해 기본값은 꺼짐입니다.',
|
||||
settings_label_max_tokens: '최대 출력 토큰',
|
||||
settings_desc_max_tokens: '새 스트리밍 응답의 출력 길이를 제한합니다. 비워 두면 현재 프로필의 대체 값을 사용합니다.',
|
||||
settings_placeholder_max_tokens_none: '재정의 없음',
|
||||
settings_placeholder_max_tokens_fallback: '재정의 없음(대체값: {0})',
|
||||
settings_busy_input_mode_queue: '후속 메시지 대기',
|
||||
settings_busy_input_mode_interrupt: '현재 작업 중단',
|
||||
settings_busy_input_mode_steer: '조정(중간 수정)',
|
||||
settings_default_message_mode_queue: '후속 메시지 대기',
|
||||
settings_default_message_mode_interrupt: '현재 작업 중단',
|
||||
settings_default_message_mode_steer: '조정(중간 수정)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -16479,17 +16479,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
busy_interrupt_confirm: 'Interrompu — envoi d\'un nouveau message',
|
||||
settings_label_busy_input_mode: 'Mode de saisie lorsque occupé',
|
||||
settings_desc_busy_input_mode: 'Contrôle ce qui se passe lorsque vous envoyez un message pendant que l\'agent est en cours d\'exécution. File d\'attente attend ; Interruption annule et recommence ; Steer injecte une correction en milieu de tour sans interrompre. Si Steer est indisponible, le brouillon est restauré pour que vous puissiez choisir la prochaine action.',
|
||||
settings_label_default_message_mode: 'Mode de message par défaut',
|
||||
settings_desc_default_message_mode: 'Contrôle ce qui se passe lorsque vous envoyez un message pendant que l\'agent est en cours d\'exécution. File d\'attente attend ; Interruption annule et recommence ; Steer injecte une correction en milieu de tour sans interrompre. Si Steer est indisponible, le brouillon est restauré pour que vous puissiez choisir la prochaine action.',
|
||||
settings_label_fade_text_effect: 'Effet de fondu du texte',
|
||||
settings_desc_fade_text_effect: 'Affiche progressivement les nouveaux mots diffusés pendant que l’assistant répond. Similaire à OpenWebUI ; désactivé par défaut pour des performances maximales.',
|
||||
settings_label_max_tokens: 'Nombre maximal de jetons en sortie',
|
||||
settings_desc_max_tokens: 'Limite la longueur des nouvelles réponses en streaming. Laissez vide pour utiliser la valeur de repli du profil actif.',
|
||||
settings_placeholder_max_tokens_none: 'Aucune surcharge',
|
||||
settings_placeholder_max_tokens_fallback: 'Aucune surcharge (repli : {0})',
|
||||
settings_busy_input_mode_queue: 'Suivi de file d\'attente',
|
||||
settings_busy_input_mode_interrupt: 'Interrompre le tour en cours',
|
||||
settings_busy_input_mode_steer: 'Steer (correction en milieu de tour)',
|
||||
settings_default_message_mode_queue: 'Suivi de file d\'attente',
|
||||
settings_default_message_mode_interrupt: 'Interrompre le tour en cours',
|
||||
settings_default_message_mode_steer: 'Steer (correction en milieu de tour)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -18129,17 +18129,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
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).',
|
||||
settings_label_default_message_mode: 'Varsayılan mesaj modu',
|
||||
settings_desc_default_message_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. Steer kullanılamıyorsa taslak geri yüklenir, böylece sonraki adımı seçebilirsiniz.',
|
||||
settings_label_fade_text_effect: 'Metin efektini soldur',
|
||||
settings_desc_fade_text_effect: 'Asistan yanıt verirken yeni aktarılan sözcüklerin geçişini sağlayın. OpenWebUI\'ye benzer; Maksimum performans için varsayılan olarak kapalıdır.',
|
||||
settings_label_max_tokens: 'Maksimum çıktı belirteci',
|
||||
settings_desc_max_tokens: 'Yeni akış yanıtlarının uzunluğunu sınırlar. Etkin profilin yedek değerini kullanmak için boş bırakın.',
|
||||
settings_placeholder_max_tokens_none: 'Geçersiz kılma yok',
|
||||
settings_placeholder_max_tokens_fallback: 'Geçersiz kılma yok (yedek: {0})',
|
||||
settings_busy_input_mode_queue: 'Sıra takibi',
|
||||
settings_busy_input_mode_interrupt: 'Mevcut dönüşü kes',
|
||||
settings_busy_input_mode_steer: 'Yönlendirme (dönüş ortası düzeltme)',
|
||||
settings_default_message_mode_queue: 'Sıra takibi',
|
||||
settings_default_message_mode_interrupt: 'Mevcut dönüşü kes',
|
||||
settings_default_message_mode_steer: 'Yönlendirme (dönüş ortası düzeltme)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -19775,17 +19775,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Spróbuj ponownie',
|
||||
steer_recovery_dismiss: 'Odrzuć',
|
||||
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).',
|
||||
settings_label_default_message_mode: 'Domyślny tryb wiadomości',
|
||||
settings_desc_default_message_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. Jeśli Steer jest niedostępny, szkic zostaje przywrócony, aby można było wybrać następne działanie.',
|
||||
settings_label_fade_text_effect: 'Efekt pojawiania się tekstu',
|
||||
settings_desc_fade_text_effect: 'Płynne pojawianie się nowo przesyłanych słów, podczas gdy asystent odpowiada. Podobnie jak w OpenWebUI; domyślnie wyłączone dla maksymalnej wydajności.',
|
||||
settings_label_max_tokens: 'Maksymalna liczba tokenów wyjściowych',
|
||||
settings_desc_max_tokens: 'Ogranicza długość nowych odpowiedzi strumieniowanych. Pozostaw puste, aby użyć wartości zapasowej z bieżącego profilu.',
|
||||
settings_placeholder_max_tokens_none: 'Brak nadpisania',
|
||||
settings_placeholder_max_tokens_fallback: 'Brak nadpisania (wartość zapasowa: {0})',
|
||||
settings_busy_input_mode_queue: 'Kolejkuj tury',
|
||||
settings_busy_input_mode_interrupt: 'Przerwij bieżącą turę',
|
||||
settings_busy_input_mode_steer: 'Korekta w trakcie tury (steer)',
|
||||
settings_default_message_mode_queue: 'Kolejkuj tury',
|
||||
settings_default_message_mode_interrupt: 'Przerwij bieżącą turę',
|
||||
settings_default_message_mode_steer: 'Korekta w trakcie tury (steer)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
@@ -21407,17 +21407,17 @@ const LOCALES = {
|
||||
steer_recovery_retry: 'Retry',
|
||||
steer_recovery_dismiss: 'Dismiss',
|
||||
busy_interrupt_confirm: 'Đã ngắt — đang gửi tin nhắn mới',
|
||||
settings_label_busy_input_mode: 'Chế độ nhập khi bận',
|
||||
settings_desc_busy_input_mode: 'Điều khiển hành vi khi bạn gửi tin nhắn trong lúc agent đang chạy. Queue sẽ chờ; Interrupt sẽ hủy và bắt đầu mới; Steer sẽ chèn điều chỉnh giữa lượt mà không ngắt (sẽ fallback sang queue nếu agent hoặc stream không khả dụng).',
|
||||
settings_label_default_message_mode: 'Chế độ tin nhắn mặc định',
|
||||
settings_desc_default_message_mode: 'Điều khiển hành vi khi bạn gửi tin nhắn trong lúc agent đang chạy. Queue sẽ chờ; Interrupt sẽ hủy và bắt đầu mới; Steer sẽ chèn điều chỉnh giữa lượt mà không ngắt. Nếu Steer không khả dụng, bản nháp sẽ được khôi phục để bạn chọn hành động tiếp theo.',
|
||||
settings_label_fade_text_effect: 'Hiệu ứng mờ chữ',
|
||||
settings_desc_fade_text_effect: 'Làm mờ dần các từ mới stream vào khi assistant đang phản hồi. Tương tự OpenWebUI; mặc định tắt để đạt hiệu năng tối đa.',
|
||||
settings_label_max_tokens: 'Số token đầu ra tối đa',
|
||||
settings_desc_max_tokens: 'Giới hạn độ dài của các phản hồi stream mới. Để trống để dùng giá trị dự phòng của hồ sơ hiện tại.',
|
||||
settings_placeholder_max_tokens_none: 'Không ghi đè',
|
||||
settings_placeholder_max_tokens_fallback: 'Không ghi đè (dự phòng: {0})',
|
||||
settings_busy_input_mode_queue: 'Xếp hàng phản hồi tiếp theo',
|
||||
settings_busy_input_mode_interrupt: 'Ngắt lượt hiện tại',
|
||||
settings_busy_input_mode_steer: 'Steer (điều chỉnh giữa lượt)',
|
||||
settings_default_message_mode_queue: 'Xếp hàng phản hồi tiếp theo',
|
||||
settings_default_message_mode_interrupt: 'Ngắt lượt hiện tại',
|
||||
settings_default_message_mode_steer: 'Steer (điều chỉnh giữa lượt)',
|
||||
settings_label_busy_placeholder_hint: 'Show busy placeholder hint',
|
||||
settings_desc_busy_placeholder_hint: 'Shows the composer placeholder with the current busy action while the session is running and the draft is empty.',
|
||||
composer_placeholder_busy_queue: 'Enter = queue | /interrupt | /background | /steer',
|
||||
|
||||
+7
-7
@@ -1320,14 +1320,14 @@
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_auto_title_refresh">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.</div>
|
||||
</div>
|
||||
<div class="settings-field" data-settings-search="message default while running queue interrupt steer">
|
||||
<label for="settingsBusyInputMode" data-i18n="settings_label_busy_input_mode">Busy input mode</label>
|
||||
<select id="settingsBusyInputMode" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px">
|
||||
<option value="queue" data-i18n="settings_busy_input_mode_queue">Queue follow-up</option>
|
||||
<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>
|
||||
<div class="settings-field" data-settings-search="busy input mode message default while running queue interrupt steer">
|
||||
<label for="settingsDefaultMessageMode" data-i18n="settings_label_default_message_mode">Default message mode</label>
|
||||
<select id="settingsDefaultMessageMode" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px">
|
||||
<option value="queue" data-i18n="settings_default_message_mode_queue">Queue follow-up</option>
|
||||
<option value="interrupt" data-i18n="settings_default_message_mode_interrupt">Interrupt current turn</option>
|
||||
<option value="steer" data-i18n="settings_default_message_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. If Steer is unavailable, the draft is restored so you can choose the next action.</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_default_message_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">
|
||||
|
||||
+7
-7
@@ -1217,9 +1217,9 @@ function applySessionTitleUpdate(sid, titleText, options={}){
|
||||
}
|
||||
|
||||
async function send(){
|
||||
// Static guards expect _busyInputMode to stay near send() while the actual
|
||||
// Static guards expect _defaultMessageMode to stay near send() while the actual
|
||||
// read remains in the S.busy branch below.
|
||||
// _busyInputMode
|
||||
// _defaultMessageMode
|
||||
// Reject concurrent invocations early — before any await yields control.
|
||||
// If a send is already in-flight (e.g. queue drain), re-queue the message
|
||||
// instead of silently dropping it.
|
||||
@@ -1255,12 +1255,12 @@ async function send(){
|
||||
|
||||
const compressionRunning=typeof isCompressionUiRunning==='function'&&isCompressionUiRunning();
|
||||
_clearStaleBusyStateBeforeSend({compressionRunning});
|
||||
// If busy or a manual compression is still running, handle based on busy_input_mode
|
||||
// If busy or a manual compression is still running, handle based on default_message_mode
|
||||
if(S.busy||compressionRunning){
|
||||
if(text){
|
||||
if(!S.session){await newSession();await renderSessionList();}
|
||||
// Busy-control slash commands must be intercepted HERE, before the
|
||||
// busyMode routing block, so the user can always type /steer, /interrupt,
|
||||
// defaultMessageMode routing block, so the user can always type /steer, /interrupt,
|
||||
// /queue, /terminal, /goal, or /yolo while the agent is running and have
|
||||
// them execute immediately.
|
||||
// Without this intercept they fall through to the queue and execute after
|
||||
@@ -1277,8 +1277,8 @@ async function send(){
|
||||
}
|
||||
}
|
||||
}
|
||||
const busyMode=window._busyInputMode||'queue';
|
||||
if(busyMode==='steer'&&S.activeStreamId&&typeof _trySteer==='function'){
|
||||
const defaultMessageMode=window._defaultMessageMode||'steer';
|
||||
if(defaultMessageMode==='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 restores the draft and leaves the active stream running if
|
||||
@@ -1290,7 +1290,7 @@ async function send(){
|
||||
// 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'){
|
||||
} else if(defaultMessageMode==='interrupt'){
|
||||
// Queue the message, then cancel so drain re-sends it.
|
||||
const _modelState=_chatPayloadModelState();
|
||||
queueSessionMessage(S.session.session_id,{text,files:[...S.pendingFiles],model:_modelState.model,model_provider:_modelState.model_provider,profile:S.activeProfile||'default'});
|
||||
|
||||
+22
-16
@@ -7946,8 +7946,8 @@ function _preferencesPayloadFromUi(){
|
||||
if(pinnedLimitField) payload.pinned_sessions_limit=parseInt(pinnedLimitField.value,10);
|
||||
const autoTitleRefreshSel=$('settingsAutoTitleRefresh');
|
||||
if(autoTitleRefreshSel) payload.auto_title_refresh_every=parseInt(autoTitleRefreshSel.value,10);
|
||||
const busyInputModeSel=$('settingsBusyInputMode');
|
||||
if(busyInputModeSel) payload.busy_input_mode=busyInputModeSel.value;
|
||||
const defaultMessageModeSel=$('settingsDefaultMessageMode');
|
||||
if(defaultMessageModeSel) payload.default_message_mode=defaultMessageModeSel.value;
|
||||
const showBusyPlaceholderHintCb=$('settingsShowBusyPlaceholderHint');
|
||||
if(showBusyPlaceholderHintCb) payload.show_busy_placeholder_hint=showBusyPlaceholderHintCb.checked;
|
||||
const botNameField=$('settingsBotName');
|
||||
@@ -8023,8 +8023,11 @@ async function _autosavePreferencesSettings(payload){
|
||||
document.documentElement.dataset.conversationOutline=window._showConversationOutline?'enabled':'disabled';
|
||||
if(typeof applyConversationOutlinePreference==='function') applyConversationOutlinePreference();
|
||||
}
|
||||
if(payload&&payload.busy_input_mode!==undefined){
|
||||
window._busyInputMode=(typeof _persistBusyInputMode==='function')?_persistBusyInputMode(saved&&saved.busy_input_mode):((saved&&saved.busy_input_mode)||'queue');
|
||||
if(payload&&payload.default_message_mode!==undefined){
|
||||
// #5170 mirror write on autosave, under the #5145 rename: persist the
|
||||
// saved mode so a reload/offline first-send honors it (legacy fallback).
|
||||
const _dmm=(saved&&saved.default_message_mode)||(saved&&saved.busy_input_mode);
|
||||
window._defaultMessageMode=(typeof _persistDefaultMessageMode==='function')?_persistDefaultMessageMode(_dmm):(_dmm||'steer');
|
||||
if(typeof _applyBusyComposerPlaceholder==='function') _applyBusyComposerPlaceholder();
|
||||
}
|
||||
if(payload&&payload.show_busy_placeholder_hint!==undefined){
|
||||
@@ -8620,13 +8623,14 @@ async function loadSettingsPanel(){
|
||||
autoTitleRefreshSel.value=['0','5','10','20'].includes(val)?val:'0';
|
||||
autoTitleRefreshSel.addEventListener('change',_schedulePreferencesAutosave,{once:false});
|
||||
}
|
||||
// Busy input mode
|
||||
const busyInputModeSel=$('settingsBusyInputMode');
|
||||
if(busyInputModeSel){
|
||||
const val=String(settings.busy_input_mode||'queue');
|
||||
busyInputModeSel.value=['queue','interrupt','steer'].includes(val)?val:'queue';
|
||||
window._busyInputMode=(typeof _persistBusyInputMode==='function')?_persistBusyInputMode(busyInputModeSel.value):busyInputModeSel.value;
|
||||
busyInputModeSel.addEventListener('change',_schedulePreferencesAutosave,{once:false});
|
||||
// Default message mode
|
||||
const defaultMessageModeSel=$('settingsDefaultMessageMode');
|
||||
if(defaultMessageModeSel){
|
||||
const val=String(settings.default_message_mode||settings.busy_input_mode||'steer');
|
||||
defaultMessageModeSel.value=['queue','interrupt','steer'].includes(val)?val:'steer';
|
||||
// #5170 mirror write on panel load, under the #5145 rename.
|
||||
window._defaultMessageMode=(typeof _persistDefaultMessageMode==='function')?_persistDefaultMessageMode(defaultMessageModeSel.value):defaultMessageModeSel.value;
|
||||
defaultMessageModeSel.addEventListener('change',_schedulePreferencesAutosave,{once:false});
|
||||
}
|
||||
const showBusyPlaceholderHintCb=$('settingsShowBusyPlaceholderHint');
|
||||
if(showBusyPlaceholderHintCb){
|
||||
@@ -10706,9 +10710,11 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
window._sessionJumpButtonsEnabled=!!body.session_jump_buttons;
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
window._sidebarDensity=sidebarDensity==='detailed'?'detailed':'compact';
|
||||
window._busyInputMode=(typeof _persistBusyInputMode==='function')
|
||||
? _persistBusyInputMode(body.busy_input_mode)
|
||||
: (body.busy_input_mode||'queue');
|
||||
// #5170 mirror write in _applySavedSettingsUi, under the #5145 rename:
|
||||
// persist so a reload/offline first-send honors the resolved mode.
|
||||
window._defaultMessageMode=(typeof _persistDefaultMessageMode==='function')
|
||||
? _persistDefaultMessageMode(body.default_message_mode||body.busy_input_mode)
|
||||
: (body.default_message_mode||body.busy_input_mode||'steer');
|
||||
window._sessionEndlessScrollEnabled=!!body.session_endless_scroll;
|
||||
window._autoScrollFollow=body.auto_scroll_follow!==false;
|
||||
window._largeTextPasteAsAttachment=body.large_text_paste_as_attachment!==false;
|
||||
@@ -11281,7 +11287,7 @@ async function saveSettings(andClose){
|
||||
const fontSize=($('settingsFontSize')||{}).value||localStorage.getItem('hermes-font-size')||'default';
|
||||
const language=($('settingsLanguage')||{}).value||'en';
|
||||
const sidebarDensity=($('settingsSidebarDensity')||{}).value==='detailed'?'detailed':'compact';
|
||||
const busyInputMode=($('settingsBusyInputMode')||{}).value||'queue';
|
||||
const defaultMessageMode=($('settingsDefaultMessageMode')||{}).value||'steer';
|
||||
const showBusyPlaceholderHint=!!($('settingsShowBusyPlaceholderHint')||{}).checked;
|
||||
const body={};
|
||||
|
||||
@@ -11335,7 +11341,7 @@ async function saveSettings(andClose){
|
||||
body.notifications_enabled=!!($('settingsNotificationsEnabled')||{}).checked;
|
||||
body.show_thinking=window._showThinking!==false;
|
||||
body.sidebar_density=sidebarDensity;
|
||||
body.busy_input_mode=busyInputMode;
|
||||
body.default_message_mode=defaultMessageMode;
|
||||
body.auto_title_refresh_every=(($('settingsAutoTitleRefresh')||{}).value||'0');
|
||||
const botName=(($('settingsBotName')||{}).value||'').trim();
|
||||
body.bot_name=botName||'Hermes';
|
||||
|
||||
+4
-4
@@ -6402,12 +6402,12 @@ function getComposerPrimaryAction(){
|
||||
}
|
||||
const explicitAction=_getExplicitBusyCommandAction(msg&&msg.value);
|
||||
if(explicitAction) return explicitAction;
|
||||
const busyMode=window._busyInputMode||'queue';
|
||||
if(busyMode==='steer'){
|
||||
const defaultMessageMode=window._defaultMessageMode||'steer';
|
||||
if(defaultMessageMode==='steer'){
|
||||
if(S.activeStreamId&&typeof _trySteer==='function') return 'steer';
|
||||
return 'queue';
|
||||
}
|
||||
if(busyMode==='interrupt'){
|
||||
if(defaultMessageMode==='interrupt'){
|
||||
if(S.activeStreamId&&typeof cancelStream==='function') return 'interrupt';
|
||||
return 'queue';
|
||||
}
|
||||
@@ -6425,7 +6425,7 @@ function _applyBusyComposerPlaceholder(){
|
||||
input.placeholder=idlePlaceholder;
|
||||
return;
|
||||
}
|
||||
const busyMode=window._busyInputMode||'queue';
|
||||
const busyMode=window._defaultMessageMode||'steer';
|
||||
const busyPlaceholderKey=busyMode==='interrupt'
|
||||
? 'composer_placeholder_busy_interrupt'
|
||||
: busyMode==='steer'
|
||||
|
||||
@@ -50,7 +50,7 @@ PREFERENCE_FIELDS_AUTOSAVE = [
|
||||
("settingsNotificationsEnabled", "notifications_enabled"),
|
||||
("settingsSidebarDensity", "sidebar_density"),
|
||||
("settingsAutoTitleRefresh", "auto_title_refresh_every"),
|
||||
("settingsBusyInputMode", "busy_input_mode"),
|
||||
("settingsDefaultMessageMode", "default_message_mode"),
|
||||
("settingsShowBusyPlaceholderHint", "show_busy_placeholder_hint"),
|
||||
("settingsBotName", "bot_name"),
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Regression tests for busy_input_mode (PR #1062, closes #720).
|
||||
"""Regression tests for default_message_mode (PR #1062, closes #720).
|
||||
|
||||
Pins the wiring for the three modes (queue / interrupt / steer):
|
||||
- The setting key + default + enum validation in api/config.py
|
||||
- Three slash commands registered in static/commands.js
|
||||
- send()'s busy branch reads window._busyInputMode and dispatches
|
||||
- Boot initializes window._busyInputMode from settings
|
||||
- send()'s busy branch reads window._defaultMessageMode and dispatches
|
||||
- Boot initializes window._defaultMessageMode from settings
|
||||
- 17 new i18n keys present in all 6 locale blocks
|
||||
|
||||
Issue: #720 (configurable busy-input behaviour)
|
||||
@@ -27,20 +27,20 @@ I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
class TestBusyInputModeSetting:
|
||||
"""The new setting key must be registered with a default and enum validator."""
|
||||
|
||||
def test_default_is_queue(self):
|
||||
"""Default value preserves existing queue behaviour for users who don't touch the setting."""
|
||||
assert '"busy_input_mode": "queue"' in CONFIG_PY, (
|
||||
"_DEFAULT_SETTINGS must include busy_input_mode='queue' so existing users see no change"
|
||||
def test_default_is_steer(self):
|
||||
"""Default value resolves to steer for users who don't touch the setting."""
|
||||
assert '"default_message_mode": "steer"' in CONFIG_PY, (
|
||||
"_DEFAULT_SETTINGS must include default_message_mode='steer' so new users see the steer default"
|
||||
)
|
||||
|
||||
def test_enum_validator_present(self):
|
||||
"""_SETTINGS_ENUM_KEYS must validate busy_input_mode against {queue, interrupt, steer}."""
|
||||
"""_SETTINGS_ENUM_KEYS must validate default_message_mode against {queue, interrupt, steer}."""
|
||||
# Find the entry inside the enum dict (a set literal as the value)
|
||||
idx = CONFIG_PY.find('"busy_input_mode": {')
|
||||
assert idx >= 0, "busy_input_mode entry missing from _SETTINGS_ENUM_KEYS"
|
||||
idx = CONFIG_PY.find('"default_message_mode": {')
|
||||
assert idx >= 0, "default_message_mode entry missing from _SETTINGS_ENUM_KEYS"
|
||||
block = CONFIG_PY[idx:idx + 200]
|
||||
assert '"queue"' in block and '"interrupt"' in block and '"steer"' in block, (
|
||||
"busy_input_mode enum must contain {queue, interrupt, steer}"
|
||||
"default_message_mode enum must contain {queue, interrupt, steer}"
|
||||
)
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ class TestBusySendButton:
|
||||
"the single primary button should remain visible while busy; it becomes Stop when there is no draft"
|
||||
)
|
||||
|
||||
def test_composer_primary_action_accounts_for_all_busy_input_modes(self):
|
||||
def test_composer_primary_action_accounts_for_all_default_message_modes(self):
|
||||
idx = UI_JS.find("function getComposerPrimaryAction()")
|
||||
assert idx >= 0, "getComposerPrimaryAction() not found"
|
||||
body = UI_JS[idx:UI_JS.find("function _setComposerPrimaryButtonIcon", idx)]
|
||||
@@ -172,9 +172,9 @@ class TestBusySendButton:
|
||||
assert "return 'queue'" in body, "queue mode and unavailable steer/interrupt fallbacks must map to queue"
|
||||
assert "return 'interrupt'" in body, "interrupt mode with an active stream must map to interrupt"
|
||||
assert "return 'steer'" in body, "steer mode with active stream support must map to steer"
|
||||
assert "window._busyInputMode||'queue'" in body, "helper must respect the Busy input mode setting"
|
||||
assert "window._defaultMessageMode||'steer'" in body, "helper must respect the Default message mode setting"
|
||||
assert "_getExplicitBusyCommandAction(msg&&msg.value)" in body, (
|
||||
"explicit /queue, /interrupt, and /steer drafts must override the Busy input mode for button visuals"
|
||||
"explicit /queue, /interrupt, and /steer drafts must override the Default message mode for button visuals"
|
||||
)
|
||||
|
||||
def test_explicit_busy_commands_override_button_visual_action(self):
|
||||
@@ -263,16 +263,16 @@ class TestBusySendButton:
|
||||
|
||||
|
||||
class TestSendBusyBranchDispatch:
|
||||
"""send()'s busy block must read window._busyInputMode and branch accordingly."""
|
||||
"""send()'s busy block must read window._defaultMessageMode and branch accordingly."""
|
||||
|
||||
def test_send_reads_busy_input_mode(self):
|
||||
# The send() function should read window._busyInputMode in the busy block
|
||||
def test_send_reads_default_message_mode(self):
|
||||
# The send() function should read window._defaultMessageMode in the busy block
|
||||
send_idx = MESSAGES_JS.find("async function send(")
|
||||
assert send_idx >= 0
|
||||
# Look in the first ~3000 chars of send() for the busy mode read
|
||||
send_body = MESSAGES_JS[send_idx:send_idx + 3000]
|
||||
assert "_busyInputMode" in send_body, (
|
||||
"send() must read window._busyInputMode in the S.busy branch"
|
||||
assert "_defaultMessageMode" in send_body, (
|
||||
"send() must read window._defaultMessageMode in the S.busy branch"
|
||||
)
|
||||
|
||||
def test_send_calls_cancel_stream_on_interrupt(self):
|
||||
@@ -300,7 +300,7 @@ class TestSendBusyBranchDispatch:
|
||||
"""
|
||||
send_idx = MESSAGES_JS.find("async function send(")
|
||||
assert send_idx >= 0, "send() not found"
|
||||
steer_idx = MESSAGES_JS.find("busyMode==='steer'", send_idx)
|
||||
steer_idx = MESSAGES_JS.find("defaultMessageMode==='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
|
||||
@@ -324,7 +324,7 @@ class TestSendBusyBranchDispatch:
|
||||
assert busy_start >= 0, "busy block not found"
|
||||
# The intercept must appear BEFORE the busyMode assignment
|
||||
intercept_idx = MESSAGES_JS.find("'steer','interrupt','queue','terminal','goal','yolo'", busy_start)
|
||||
busymode_idx = MESSAGES_JS.find("_busyInputMode||'queue'", busy_start)
|
||||
busymode_idx = MESSAGES_JS.find("_defaultMessageMode||'steer'", busy_start)
|
||||
assert intercept_idx >= 0, (
|
||||
"send() must intercept /steer /interrupt /queue /terminal /goal /yolo before the busyMode "
|
||||
"routing block — otherwise they queue instead of executing immediately"
|
||||
@@ -345,7 +345,7 @@ class TestSendBusyBranchDispatch:
|
||||
intercept_idx = MESSAGES_JS.find("'steer','interrupt','queue','terminal','goal','yolo'", busy_start)
|
||||
assert intercept_idx >= 0
|
||||
# Get the intercept block (up to the next busyMode assignment)
|
||||
busymode_idx = MESSAGES_JS.find("_busyInputMode||'queue'", busy_start)
|
||||
busymode_idx = MESSAGES_JS.find("_defaultMessageMode||'steer'", busy_start)
|
||||
intercept_block = MESSAGES_JS[intercept_idx:busymode_idx]
|
||||
assert "_bc.fn(_pc.args)" in intercept_block, (
|
||||
"The intercept must call the command handler directly via _bc.fn(_pc.args)"
|
||||
@@ -365,7 +365,7 @@ class TestSendBusyBranchDispatch:
|
||||
send_idx = MESSAGES_JS.find("async function send(")
|
||||
busy_start = MESSAGES_JS.find("S.busy||compressionRunning", send_idx)
|
||||
intercept_idx = MESSAGES_JS.find("'steer','interrupt','queue','terminal','goal','yolo'", busy_start)
|
||||
busymode_idx = MESSAGES_JS.find("_busyInputMode||'queue'", busy_start)
|
||||
busymode_idx = MESSAGES_JS.find("_defaultMessageMode||'steer'", busy_start)
|
||||
intercept_block = MESSAGES_JS[intercept_idx:busymode_idx]
|
||||
clear_idx = intercept_block.find("$('msg').value=''")
|
||||
await_idx = intercept_block.find("await _bc.fn")
|
||||
@@ -384,34 +384,36 @@ class TestSendBusyBranchDispatch:
|
||||
|
||||
class TestBootAndPanelsWiring:
|
||||
def test_boot_init_default_path(self):
|
||||
"""Boot success path initialises window._busyInputMode from settings.
|
||||
"""Boot success path initialises window._defaultMessageMode from settings.
|
||||
|
||||
#5167: the assignment now routes through _persistBusyInputMode() so the
|
||||
resolved value is mirrored into localStorage (the synchronous source the
|
||||
eager default reads) while still being the single source of truth once
|
||||
/api/settings resolves.
|
||||
#5167/#5170: the assignment routes through _persistDefaultMessageMode()
|
||||
so the resolved value is mirrored into localStorage (the synchronous
|
||||
source the eager default reads) while still being the single source of
|
||||
truth once /api/settings resolves. #5145 renamed the setting and reads
|
||||
the legacy busy_input_mode key as a back-compat fallback.
|
||||
"""
|
||||
assert "window._busyInputMode=_persistBusyInputMode(s.busy_input_mode)" in BOOT_JS
|
||||
assert "window._defaultMessageMode=_persistDefaultMessageMode(s.default_message_mode||s.busy_input_mode)" in BOOT_JS
|
||||
|
||||
def test_boot_init_fallback_path(self):
|
||||
"""Boot fallback path (settings load failed) keeps the persisted preference.
|
||||
|
||||
#5167: instead of hard-clobbering to 'queue', the catch path re-reads the
|
||||
persisted mirror so a saved 'steer'/'interrupt' still applies when the
|
||||
server is unreachable. _readPersistedBusyInputMode() normalizes to 'queue'.
|
||||
#5167/#5170: instead of hard-clobbering to a default, the catch path
|
||||
re-reads the persisted mirror so a saved 'steer'/'interrupt'/'queue'
|
||||
still applies when the server is unreachable. This must NOT regress to
|
||||
a hardcoded default that ignores the saved choice.
|
||||
"""
|
||||
assert "window._busyInputMode=_readPersistedBusyInputMode()" in BOOT_JS
|
||||
assert "window._defaultMessageMode=_readPersistedDefaultMessageMode()" in BOOT_JS
|
||||
|
||||
def test_panels_load_save_apply(self):
|
||||
assert "settingsBusyInputMode" in PANELS_JS, "panels.js must load the setting"
|
||||
assert "body.busy_input_mode" in PANELS_JS, "saveSettings must include busy_input_mode in body"
|
||||
assert "_persistBusyInputMode(body.busy_input_mode)" in PANELS_JS, (
|
||||
"_applySavedSettingsUi must propagate busy_input_mode to the global "
|
||||
"and persist it (so the next reload's eager default honors the save) (#5167)"
|
||||
assert "settingsDefaultMessageMode" in PANELS_JS, "panels.js must load the setting"
|
||||
assert "body.default_message_mode" in PANELS_JS, "saveSettings must include default_message_mode in body"
|
||||
assert "_persistDefaultMessageMode(body.default_message_mode||body.busy_input_mode)" in PANELS_JS, (
|
||||
"_applySavedSettingsUi must propagate default_message_mode to the global "
|
||||
"and persist it (so the next reload's eager default honors the save) (#5167/#5170)"
|
||||
)
|
||||
|
||||
def test_index_html_dropdown_has_three_options(self):
|
||||
idx = INDEX_HTML.find('id="settingsBusyInputMode"')
|
||||
idx = INDEX_HTML.find('id="settingsDefaultMessageMode"')
|
||||
assert idx >= 0
|
||||
block = INDEX_HTML[idx:idx + 800]
|
||||
assert 'value="queue"' in block
|
||||
@@ -437,11 +439,11 @@ class TestI18nKeys:
|
||||
"cmd_steer_fallback",
|
||||
"busy_steer_fallback",
|
||||
"busy_interrupt_confirm",
|
||||
"settings_label_busy_input_mode",
|
||||
"settings_desc_busy_input_mode",
|
||||
"settings_busy_input_mode_queue",
|
||||
"settings_busy_input_mode_interrupt",
|
||||
"settings_busy_input_mode_steer",
|
||||
"settings_label_default_message_mode",
|
||||
"settings_desc_default_message_mode",
|
||||
"settings_default_message_mode_queue",
|
||||
"settings_default_message_mode_interrupt",
|
||||
"settings_default_message_mode_steer",
|
||||
]
|
||||
|
||||
def test_each_key_appears_at_least_six_times(self):
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_busy_placeholder_helper_preserves_compression_and_drafts():
|
||||
assert "if(_composerHasContent())" in helper_block
|
||||
assert "assistantDisplayName()" in helper_block
|
||||
assert "window._showBusyPlaceholderHint" in helper_block
|
||||
assert "window._busyInputMode||'queue'" in helper_block
|
||||
assert "window._defaultMessageMode||'steer'" in helper_block
|
||||
assert "composer_placeholder_busy_queue" in helper_block
|
||||
assert "composer_placeholder_busy_interrupt" in helper_block
|
||||
assert "composer_placeholder_busy_steer" in helper_block
|
||||
@@ -77,7 +77,7 @@ def test_busy_placeholder_helper_preserves_compression_and_drafts():
|
||||
|
||||
|
||||
def test_locale_blocks_cover_new_keys():
|
||||
locale_blocks = I18N_JS.count("settings_busy_input_mode_steer")
|
||||
locale_blocks = I18N_JS.count("settings_default_message_mode_steer")
|
||||
assert locale_blocks == 14
|
||||
for key in [
|
||||
"settings_label_busy_placeholder_hint",
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Focused regression coverage for #5145 busy-input defaults."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent.parent
|
||||
CONFIG_PY = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
||||
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
LOCALE_KEYS = (
|
||||
"en",
|
||||
"it",
|
||||
"ja",
|
||||
"ru",
|
||||
"es",
|
||||
"de",
|
||||
"zh",
|
||||
"'zh-Hant'",
|
||||
"pt",
|
||||
"ko",
|
||||
"fr",
|
||||
"tr",
|
||||
"pl",
|
||||
"vi",
|
||||
)
|
||||
|
||||
|
||||
def _locale_block(locale_key):
|
||||
escaped_keys = [re.escape(key) for key in LOCALE_KEYS]
|
||||
next_keys = "|".join(key for key in escaped_keys if key != re.escape(locale_key))
|
||||
pattern = rf"^ {re.escape(locale_key)}: \{{(?P<body>.*?)(?=^ (?:{next_keys}): \{{|\n\}};)"
|
||||
match = re.search(pattern, I18N_JS, re.MULTILINE | re.DOTALL)
|
||||
assert match, f"missing {locale_key} locale block"
|
||||
return match.group("body")
|
||||
|
||||
|
||||
def _default_message_mode_label(locale_key):
|
||||
block = _locale_block(locale_key)
|
||||
match = re.search(r"settings_label_default_message_mode: '([^']+)'", block)
|
||||
assert match, f"missing default message mode label for {locale_key}"
|
||||
return match.group(1)
|
||||
|
||||
def _default_message_mode_steer_option(locale_key):
|
||||
block = _locale_block(locale_key)
|
||||
match = re.search(r"settings_default_message_mode_steer: '([^']+)'", block)
|
||||
assert match, f"missing default message mode steer option for {locale_key}"
|
||||
return match.group(1)
|
||||
|
||||
def _default_message_mode_description(locale_key):
|
||||
block = _locale_block(locale_key)
|
||||
match = re.search(r"settings_desc_default_message_mode: ([\"'])((?:\\.|(?!\1).)*)\1", block)
|
||||
assert match, f"missing default message mode description for {locale_key}"
|
||||
return match.group(2)
|
||||
|
||||
|
||||
def test_backend_default_resolves_to_steer():
|
||||
assert '"default_message_mode": "steer"' in CONFIG_PY
|
||||
|
||||
|
||||
def test_boot_defaults_resolve_to_steer():
|
||||
# Success path routes through the #5170 persistence mirror; the resolved
|
||||
# value still defaults to 'steer' because _normalizeDefaultMessageMode()
|
||||
# returns 'steer' for a missing/invalid value.
|
||||
assert "window._defaultMessageMode=_persistDefaultMessageMode(s.default_message_mode||s.busy_input_mode)" in BOOT_JS
|
||||
assert "return _DEFAULT_MESSAGE_MODES.includes(mode)?mode:'steer';" in BOOT_JS
|
||||
# Load-FAILURE path re-reads the persisted mirror (never a hardcoded literal),
|
||||
# so a saved preference survives an unreachable server (#5167/#5170). With no
|
||||
# persisted value the normalize fallback still yields 'steer'.
|
||||
assert "window._defaultMessageMode=_readPersistedDefaultMessageMode()" in BOOT_JS
|
||||
assert "window._defaultMessageMode='steer'" not in BOOT_JS
|
||||
|
||||
|
||||
def test_settings_panel_fallbacks_resolve_to_steer():
|
||||
assert "String(settings.default_message_mode||settings.busy_input_mode||'steer')" in PANELS_JS
|
||||
assert "['queue','interrupt','steer'].includes(val)?val:'steer'" in PANELS_JS
|
||||
# _applySavedSettingsUi routes through the #5170 mirror; the ||'steer' tail
|
||||
# keeps steer as the resolved default when the helper is unavailable.
|
||||
assert "_persistDefaultMessageMode(body.default_message_mode||body.busy_input_mode)" in PANELS_JS
|
||||
assert "body.default_message_mode||body.busy_input_mode||'steer'" in PANELS_JS
|
||||
assert "const defaultMessageMode=($('settingsDefaultMessageMode')||{}).value||'steer'" in PANELS_JS
|
||||
|
||||
|
||||
def test_busy_input_label_changes_without_key_or_id_drift():
|
||||
assert 'id="settingsDefaultMessageMode"' in INDEX_HTML
|
||||
assert 'data-i18n="settings_label_default_message_mode">Default message mode' in INDEX_HTML
|
||||
assert _default_message_mode_label("en") == "Default message mode"
|
||||
assert I18N_JS.count("settings_label_default_message_mode: 'Default message mode'") == 1
|
||||
|
||||
|
||||
def test_busy_input_labels_stay_in_their_locale_blocks():
|
||||
assert _default_message_mode_label("it") == "Modalità messaggio predefinita"
|
||||
assert _default_message_mode_label("ja") == "既定のメッセージモード"
|
||||
assert _default_message_mode_label("ru") == "Режим сообщений по умолчанию"
|
||||
assert _default_message_mode_label("es") == "Modo de mensaje predeterminado"
|
||||
assert _default_message_mode_label("de") == "Standard-Nachrichtenmodus"
|
||||
assert _default_message_mode_label("zh") == "默认消息模式"
|
||||
assert _default_message_mode_label("'zh-Hant'") == "預設訊息模式"
|
||||
assert _default_message_mode_label("pt") == "Modo de mensagem padrão"
|
||||
assert _default_message_mode_label("ko") == "기본 메시지 모드"
|
||||
assert _default_message_mode_label("fr") == "Mode de message par défaut"
|
||||
assert _default_message_mode_label("tr") == "Varsayılan mesaj modu"
|
||||
assert _default_message_mode_label("pl") == "Domyślny tryb wiadomości"
|
||||
assert _default_message_mode_label("vi") == "Chế độ tin nhắn mặc định"
|
||||
|
||||
def test_russian_steer_option_keeps_mid_turn_meaning():
|
||||
assert _default_message_mode_steer_option("ru") == "Steer (коррекция в середине хода)"
|
||||
|
||||
def test_localized_descriptions_keep_draft_restore_meaning():
|
||||
expected_restore_phrases = {
|
||||
"it": "bozza viene ripristinata",
|
||||
"ja": "下書きが復元",
|
||||
"ru": "черновик восстанавливается",
|
||||
"es": "borrador se restaura",
|
||||
"de": "Entwurf wiederhergestellt",
|
||||
"zh": "恢复草稿",
|
||||
"'zh-Hant'": "恢復草稿",
|
||||
"pt": "rascunho é restaurado",
|
||||
"ko": "초안이 복원",
|
||||
"fr": "brouillon est restauré",
|
||||
"tr": "taslak geri yüklenir",
|
||||
"pl": "szkic zostaje przywrócony",
|
||||
"vi": "bản nháp sẽ được khôi phục",
|
||||
}
|
||||
for locale_key, restore_phrase in expected_restore_phrases.items():
|
||||
assert restore_phrase in _default_message_mode_description(locale_key), (
|
||||
f"{locale_key} description must describe restoring the draft when Steer is unavailable"
|
||||
)
|
||||
@@ -10,11 +10,11 @@ class TestBusyInputSettingsSearchTerms:
|
||||
|
||||
def test_busy_input_field_has_supplemental_search_terms(self):
|
||||
"""The busy-input field must expose the supplemental terms from HTML."""
|
||||
assert 'data-settings-search="message default while running queue interrupt steer"' in INDEX_HTML, (
|
||||
assert 'data-settings-search="busy input mode message default while running queue interrupt steer"' in INDEX_HTML, (
|
||||
"busy-input field must expose supplemental search terms in data-settings-search"
|
||||
)
|
||||
assert "Busy input mode" in INDEX_HTML, (
|
||||
"busy-input field label must still be present in the HTML"
|
||||
assert 'data-i18n="settings_label_default_message_mode"' in INDEX_HTML, (
|
||||
"busy-input field label must stay wired to the renamed i18n key in the HTML"
|
||||
)
|
||||
|
||||
def test_settings_index_uses_field_text_for_busy_input(self):
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
"""Regression tests for the busy_input_mode boot race (#5167).
|
||||
"""Regression tests for the default-message-mode boot race (#5167 / #5170).
|
||||
|
||||
The Busy input mode preference (queue/interrupt/steer) was ignored for any send
|
||||
that happened before the async boot IIFE resolved `await api('/api/settings')`,
|
||||
because `window._busyInputMode` had NO eager default — it was `undefined` during
|
||||
the boot window, so the send path's `window._busyInputMode||'queue'` silently
|
||||
fell back to 'queue'. Re-saving from the settings panel set the global
|
||||
The Default message mode preference (queue/interrupt/steer) was ignored for any
|
||||
send that happened before the async boot IIFE resolved `await api('/api/settings')`,
|
||||
because `window._defaultMessageMode` had NO eager default — it was `undefined`
|
||||
during the boot window, so the send path's `window._defaultMessageMode||'steer'`
|
||||
silently fell back. Re-saving from the settings panel set the global
|
||||
synchronously, which is why "re-save fixes it".
|
||||
|
||||
The fix gives `window._busyInputMode` a deterministic value at script top, read
|
||||
synchronously from a localStorage mirror ('hermes-busy-input-mode') that the
|
||||
resolved settings + the save handler both keep in sync — mirroring how
|
||||
hermes-lang / hermes-theme are bootstrapped from a synchronous source.
|
||||
The fix gives `window._defaultMessageMode` a deterministic value at script top,
|
||||
read synchronously from a localStorage mirror ('hermes-default-message-mode',
|
||||
with a legacy 'hermes-busy-input-mode' fallback read) that the resolved settings
|
||||
+ the save handler both keep in sync — mirroring how hermes-lang / hermes-theme
|
||||
are bootstrapped from a synchronous source.
|
||||
|
||||
Reported by @b3nw. Issue: #5167.
|
||||
Reported by @b3nw. Issues: #5167 (boot race), #5170 (autosave + panel-load
|
||||
mirror). #5145 renamed the setting busy_input_mode -> default_message_mode and
|
||||
flipped the default to 'steer'; this file was updated for the rename while
|
||||
preserving the persistence-mirror guarantees (the load-failure path must still
|
||||
honor the saved preference, not clobber it with a hardcoded default).
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
@@ -24,13 +29,13 @@ UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestEagerDefault:
|
||||
"""window._busyInputMode must be set eagerly, before the async settings fetch."""
|
||||
"""window._defaultMessageMode must be set eagerly, before the async settings fetch."""
|
||||
|
||||
def test_eager_default_assigned_at_module_scope(self):
|
||||
"""An eager top-level assignment must exist so first sends honor the preference."""
|
||||
assert "window._busyInputMode=_readPersistedBusyInputMode()" in BOOT_JS, (
|
||||
"boot.js must eagerly initialise window._busyInputMode from the persisted "
|
||||
"mirror at module scope so sends during the boot window don't default to queue"
|
||||
assert "window._defaultMessageMode=_readPersistedDefaultMessageMode()" in BOOT_JS, (
|
||||
"boot.js must eagerly initialise window._defaultMessageMode from the persisted "
|
||||
"mirror at module scope so sends during the boot window don't default silently"
|
||||
)
|
||||
|
||||
def test_eager_default_precedes_async_settings_fetch(self):
|
||||
@@ -39,13 +44,13 @@ class TestEagerDefault:
|
||||
This is the whole point of the fix: the value must be deterministic during
|
||||
the window between page load and the settings fetch resolving.
|
||||
"""
|
||||
eager_idx = BOOT_JS.find("window._busyInputMode=_readPersistedBusyInputMode()")
|
||||
eager_idx = BOOT_JS.find("window._defaultMessageMode=_readPersistedDefaultMessageMode()")
|
||||
assert eager_idx >= 0, "eager default assignment not found"
|
||||
# The async IIFE awaits /api/settings; the success-path assignment lives inside it.
|
||||
await_idx = BOOT_JS.find("const s=await api('/api/settings')")
|
||||
assert await_idx >= 0, "async settings fetch not found"
|
||||
assert eager_idx < await_idx, (
|
||||
"the eager window._busyInputMode default must be set BEFORE the async "
|
||||
"the eager window._defaultMessageMode default must be set BEFORE the async "
|
||||
"/api/settings fetch — otherwise the boot-window race (#5167) persists"
|
||||
)
|
||||
|
||||
@@ -53,7 +58,7 @@ class TestEagerDefault:
|
||||
"""The eager default in boot.js loads after messages.js (defer order), but the
|
||||
assignment itself must sit at top level so it runs during script evaluation,
|
||||
not inside a later-firing callback."""
|
||||
eager_idx = BOOT_JS.find("window._busyInputMode=_readPersistedBusyInputMode()")
|
||||
eager_idx = BOOT_JS.find("window._defaultMessageMode=_readPersistedDefaultMessageMode()")
|
||||
# Must be at the start of a line (top-level statement), not indented inside a fn.
|
||||
line_start = BOOT_JS.rfind("\n", 0, eager_idx) + 1
|
||||
assert BOOT_JS[line_start:eager_idx].strip() == "", (
|
||||
@@ -66,71 +71,106 @@ class TestSyncMirrorHelpers:
|
||||
"""Helper functions backing the synchronous localStorage mirror."""
|
||||
|
||||
def test_persist_helper_defined_and_exposed(self):
|
||||
assert "function _persistBusyInputMode(" in BOOT_JS
|
||||
assert "window._persistBusyInputMode=_persistBusyInputMode" in BOOT_JS
|
||||
assert "function _persistDefaultMessageMode(" in BOOT_JS
|
||||
assert "window._persistDefaultMessageMode=_persistDefaultMessageMode" in BOOT_JS
|
||||
|
||||
def test_read_helper_defined_and_exposed(self):
|
||||
assert "function _readPersistedBusyInputMode(" in BOOT_JS
|
||||
assert "window._readPersistedBusyInputMode=_readPersistedBusyInputMode" in BOOT_JS
|
||||
assert "function _readPersistedDefaultMessageMode(" in BOOT_JS
|
||||
assert "window._readPersistedDefaultMessageMode=_readPersistedDefaultMessageMode" in BOOT_JS
|
||||
|
||||
def test_mirror_uses_dedicated_localstorage_key(self):
|
||||
assert "localStorage.setItem('hermes-busy-input-mode'" in BOOT_JS, (
|
||||
"persist helper must write the busy mode to a dedicated localStorage key"
|
||||
assert "localStorage.setItem(_DEFAULT_MESSAGE_MODE_KEY" in BOOT_JS, (
|
||||
"persist helper must write the mode to the dedicated localStorage key"
|
||||
)
|
||||
assert "localStorage.getItem('hermes-busy-input-mode')" in BOOT_JS, (
|
||||
"read helper must read the busy mode from the same localStorage key"
|
||||
assert "localStorage.getItem(_DEFAULT_MESSAGE_MODE_KEY" in BOOT_JS, (
|
||||
"read helper must read the mode from the same localStorage key"
|
||||
)
|
||||
assert "_DEFAULT_MESSAGE_MODE_KEY='hermes-default-message-mode'" in BOOT_JS, (
|
||||
"the dedicated key must be 'hermes-default-message-mode'"
|
||||
)
|
||||
|
||||
def test_mirror_reads_legacy_key_as_fallback(self):
|
||||
"""A pre-rename user's persisted preference (under the old
|
||||
'hermes-busy-input-mode' key) must survive the rename via a fallback read."""
|
||||
assert "_LEGACY_DEFAULT_MESSAGE_MODE_KEY='hermes-busy-input-mode'" in BOOT_JS, (
|
||||
"the legacy localStorage key must still be recognised for back-compat"
|
||||
)
|
||||
assert "localStorage.getItem(_LEGACY_DEFAULT_MESSAGE_MODE_KEY" in BOOT_JS, (
|
||||
"the read helper must fall back to the legacy key so an existing "
|
||||
"user's persisted preference survives the #5145 rename"
|
||||
)
|
||||
|
||||
def test_normalize_validates_against_known_modes(self):
|
||||
"""Unknown/garbage persisted values must normalize to 'queue', never crash."""
|
||||
assert "function _normalizeBusyInputMode(" in BOOT_JS
|
||||
"""Unknown/garbage persisted values must normalize to the safe default, never crash."""
|
||||
assert "function _normalizeDefaultMessageMode(" in BOOT_JS
|
||||
assert "['queue','interrupt','steer']" in BOOT_JS
|
||||
|
||||
def test_localstorage_access_is_guarded(self):
|
||||
"""localStorage can throw (privacy mode / disabled). Helpers must try/catch."""
|
||||
idx = BOOT_JS.find("function _persistBusyInputMode(")
|
||||
idx = BOOT_JS.find("function _persistDefaultMessageMode(")
|
||||
body = BOOT_JS[idx:idx + 400]
|
||||
assert "try{" in body and "catch(_)" in body, (
|
||||
"_persistBusyInputMode must guard localStorage access against exceptions"
|
||||
"_persistDefaultMessageMode must guard localStorage access against exceptions"
|
||||
)
|
||||
idx = BOOT_JS.find("function _readPersistedBusyInputMode(")
|
||||
body = BOOT_JS[idx:idx + 400]
|
||||
idx = BOOT_JS.find("function _readPersistedDefaultMessageMode(")
|
||||
body = BOOT_JS[idx:idx + 600]
|
||||
assert "try{" in body and "catch(_)" in body, (
|
||||
"_readPersistedBusyInputMode must guard localStorage access against exceptions"
|
||||
"_readPersistedDefaultMessageMode must guard localStorage access against exceptions"
|
||||
)
|
||||
|
||||
|
||||
class TestAssignmentSitesSyncMirror:
|
||||
"""Every place that sets window._busyInputMode must keep the mirror in sync."""
|
||||
"""Every place that sets window._defaultMessageMode must keep the mirror in sync."""
|
||||
|
||||
def test_boot_success_path_persists(self):
|
||||
assert "window._busyInputMode=_persistBusyInputMode(s.busy_input_mode)" in BOOT_JS
|
||||
assert "window._defaultMessageMode=_persistDefaultMessageMode(s.default_message_mode||s.busy_input_mode)" in BOOT_JS
|
||||
|
||||
def test_boot_catch_path_reads_persisted(self):
|
||||
assert "window._busyInputMode=_readPersistedBusyInputMode()" in BOOT_JS
|
||||
"""The critical #5167/#5170 guarantee: settings-load FAILURE must re-read the
|
||||
persisted preference, NOT clobber it with a hardcoded default. A saved
|
||||
'steer'/'interrupt'/'queue' must still apply when the server is unreachable."""
|
||||
assert "window._defaultMessageMode=_readPersistedDefaultMessageMode()" in BOOT_JS
|
||||
# Guard against regression: the catch path must NOT hardcode a literal mode.
|
||||
assert "window._defaultMessageMode='steer'" not in BOOT_JS, (
|
||||
"the settings-load-failure path must read the persisted preference, not "
|
||||
"clobber it with a hardcoded 'steer' that ignores a saved 'interrupt'/'queue'"
|
||||
)
|
||||
|
||||
def test_panels_save_persists(self):
|
||||
assert "_persistBusyInputMode(body.busy_input_mode)" in PANELS_JS, (
|
||||
"the settings save handler must persist busy_input_mode so the next "
|
||||
assert "_persistDefaultMessageMode(body.default_message_mode||body.busy_input_mode)" in PANELS_JS, (
|
||||
"the settings save handler must persist default_message_mode so the next "
|
||||
"reload's eager default reads the saved value"
|
||||
)
|
||||
|
||||
def test_panels_save_guards_helper_availability(self):
|
||||
"""panels.js loads before boot.js (defer order); the save handler must
|
||||
guard the helper with typeof so a regression in load order can't throw."""
|
||||
idx = PANELS_JS.find("_persistBusyInputMode(body.busy_input_mode)")
|
||||
idx = PANELS_JS.find("_persistDefaultMessageMode(body.default_message_mode||body.busy_input_mode)")
|
||||
assert idx >= 0
|
||||
window = PANELS_JS[max(0, idx - 120):idx]
|
||||
assert "typeof _persistBusyInputMode==='function'" in window, (
|
||||
"panels.js save handler must guard _persistBusyInputMode with a typeof check"
|
||||
window = PANELS_JS[max(0, idx - 160):idx]
|
||||
assert "typeof _persistDefaultMessageMode==='function'" in window, (
|
||||
"panels.js save handler must guard _persistDefaultMessageMode with a typeof check"
|
||||
)
|
||||
|
||||
def test_panels_autosave_persists(self):
|
||||
"""#5170: the preferences autosave path must also persist the mode."""
|
||||
assert "_persistDefaultMessageMode(_dmm)" in PANELS_JS, (
|
||||
"the autosave handler must persist default_message_mode so a reload after "
|
||||
"an autosave honors the saved value (#5170)"
|
||||
)
|
||||
|
||||
def test_panels_panel_load_persists(self):
|
||||
"""#5170: opening the settings panel must also mirror the mode into localStorage."""
|
||||
assert "_persistDefaultMessageMode(defaultMessageModeSel.value)" in PANELS_JS, (
|
||||
"loadSettingsPanel must persist default_message_mode on panel load (#5170)"
|
||||
)
|
||||
|
||||
|
||||
class TestReadSitesUnchanged:
|
||||
"""The defensive read sites must keep their ||'queue' fallback (belt + braces)."""
|
||||
"""The defensive read sites must keep their ||'steer' fallback (belt + braces)."""
|
||||
|
||||
def test_messages_read_site_intact(self):
|
||||
assert "const busyMode=window._busyInputMode||'queue';" in MESSAGES_JS
|
||||
assert "const defaultMessageMode=window._defaultMessageMode||'steer';" in MESSAGES_JS
|
||||
|
||||
def test_ui_read_site_intact(self):
|
||||
assert "const busyMode=window._busyInputMode||'queue';" in UI_JS
|
||||
assert "window._defaultMessageMode||'steer'" in UI_JS
|
||||
@@ -134,7 +134,7 @@ class TestYoloCommandRegistration:
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def commands_js(self):
|
||||
with open("static/commands.js", "r") as f:
|
||||
with open("static/commands.js", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def test_yolo_command_in_array(self, commands_js):
|
||||
@@ -155,7 +155,7 @@ class TestYoloBusySendPath:
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def messages_js(self):
|
||||
with open("static/messages.js", "r") as f:
|
||||
with open("static/messages.js", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def test_yolo_in_busy_send_allowlist(self, messages_js):
|
||||
@@ -166,7 +166,7 @@ class TestYoloBusySendPath:
|
||||
intercept_start = messages_js.find("if(text.startsWith('/')", busy_start)
|
||||
assert intercept_start >= 0, "busy slash intercept block not found in send()"
|
||||
intercept_idx = messages_js.find("'steer','interrupt','queue','terminal','goal','yolo'", intercept_start)
|
||||
busymode_idx = messages_js.find("_busyInputMode||'queue'", busy_start)
|
||||
busymode_idx = messages_js.find("_defaultMessageMode||'steer'", busy_start)
|
||||
assert intercept_idx >= 0, "Busy-path slash allowlist must include yolo in the mid-turn branch"
|
||||
assert intercept_idx < busymode_idx, "Busy-path intercept must run before busyMode routing"
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestYoloPillHTML:
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def index_html(self):
|
||||
with open("static/index.html", "r") as f:
|
||||
with open("static/index.html", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def test_yolo_pill_element_exists(self, index_html):
|
||||
@@ -211,7 +211,7 @@ class TestYoloCSS:
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def style_css(self):
|
||||
with open("static/style.css", "r") as f:
|
||||
with open("static/style.css", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def test_yolo_pill_class(self, style_css):
|
||||
@@ -242,7 +242,7 @@ class TestYoloI18n:
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def i18n_js(self):
|
||||
with open("static/i18n.js", "r") as f:
|
||||
with open("static/i18n.js", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
@pytest.mark.parametrize("locale", LOCALES)
|
||||
|
||||
@@ -278,7 +278,7 @@ class TestFrontendWiring:
|
||||
|
||||
def test_send_busy_steer_uses_try_steer(self):
|
||||
# send() in messages.js: when busyMode === 'steer', should call _trySteer
|
||||
idx = self.msgs.find("busyMode==='steer'")
|
||||
idx = self.msgs.find("defaultMessageMode==='steer'")
|
||||
assert idx >= 0
|
||||
block = self.msgs[idx:idx + 800]
|
||||
assert "_trySteer" in block, "send()'s steer branch must delegate to _trySteer"
|
||||
|
||||
Reference in new issue
Block a user