v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
"""Regression tests for busy_input_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
|
|
|
|
|
|
- 17 new i18n keys present in all 6 locale blocks
|
|
|
|
|
|
|
|
|
|
|
|
Issue: #720 (configurable busy-input behaviour)
|
|
|
|
|
|
"""
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).parent.parent
|
|
|
|
|
|
CONFIG_PY = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
|
|
|
|
|
COMMANDS_JS = (ROOT / "static" / "commands.js").read_text(encoding="utf-8")
|
|
|
|
|
|
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
2026-04-28 03:15:50 +00:00
|
|
|
|
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Backend: setting registration + enum validation ─────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
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_enum_validator_present(self):
|
|
|
|
|
|
"""_SETTINGS_ENUM_KEYS must validate busy_input_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"
|
|
|
|
|
|
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}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Frontend: slash commands ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestSlashCommandRegistration:
|
|
|
|
|
|
"""The three new slash commands must be registered in COMMANDS array."""
|
|
|
|
|
|
|
|
|
|
|
|
def test_queue_command_registered(self):
|
|
|
|
|
|
assert "name:'queue'" in COMMANDS_JS and "fn:cmdQueue" in COMMANDS_JS
|
|
|
|
|
|
|
|
|
|
|
|
def test_interrupt_command_registered(self):
|
|
|
|
|
|
assert "name:'interrupt'" in COMMANDS_JS and "fn:cmdInterrupt" in COMMANDS_JS
|
|
|
|
|
|
|
|
|
|
|
|
def test_steer_command_registered(self):
|
|
|
|
|
|
assert "name:'steer'" in COMMANDS_JS and "fn:cmdSteer" in COMMANDS_JS
|
|
|
|
|
|
|
|
|
|
|
|
def test_all_three_busy_commands_are_no_echo(self):
|
|
|
|
|
|
"""All three busy commands must set noEcho:true so the slash invocation
|
|
|
|
|
|
is not echoed as a visible user bubble. Without noEcho, /queue causes a
|
|
|
|
|
|
double-bubble: the raw slash text appears, then the queued message appears
|
|
|
|
|
|
again when the drain fires.
|
|
|
|
|
|
"""
|
|
|
|
|
|
for name in ("queue", "interrupt", "steer"):
|
|
|
|
|
|
idx = COMMANDS_JS.find(f"name:'{name}'")
|
|
|
|
|
|
assert idx >= 0, f"{name} not registered"
|
|
|
|
|
|
block = COMMANDS_JS[idx:idx + 250]
|
|
|
|
|
|
assert "noEcho:true" in block, (
|
|
|
|
|
|
f"/{name} registration must set noEcho:true — "
|
|
|
|
|
|
"without it the command text is echoed as a user bubble, causing duplicates"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSlashCommandHandlers:
|
|
|
|
|
|
"""The three handler functions must guard properly and call cancelStream where appropriate."""
|
|
|
|
|
|
|
2026-04-26 04:35:51 +00:00
|
|
|
|
def test_cmd_queue_handles_idle_state(self):
|
|
|
|
|
|
"""/queue when idle now sends the message normally instead of showing an
|
|
|
|
|
|
error toast. The if(!S.busy) guard must still exist — it routes to the
|
|
|
|
|
|
idle-send path rather than the queue path."""
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
idx = COMMANDS_JS.find("async function cmdQueue(")
|
|
|
|
|
|
assert idx >= 0
|
|
|
|
|
|
body = COMMANDS_JS[idx:idx + 600]
|
2026-04-26 04:35:51 +00:00
|
|
|
|
assert "if(!S.busy)" in body, "/queue must have an if(!S.busy) guard that routes to send()"
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
|
|
|
|
|
|
def test_cmd_interrupt_calls_cancel_stream(self):
|
|
|
|
|
|
idx = COMMANDS_JS.find("async function cmdInterrupt(")
|
|
|
|
|
|
assert idx >= 0
|
2026-04-26 04:35:51 +00:00
|
|
|
|
body = COMMANDS_JS[idx:idx + 1300] # expanded: idle-fallback block added before the busy path
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
assert "queueSessionMessage" in body, "/interrupt must queue the new message before cancelling"
|
|
|
|
|
|
assert "cancelStream" in body, "/interrupt must call cancelStream() so the drain re-sends"
|
|
|
|
|
|
|
2026-04-26 02:21:00 +00:00
|
|
|
|
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
|
|
|
|
|
|
in test_real_steer.py — this test just pins the delegation."""
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
idx = COMMANDS_JS.find("async function cmdSteer(")
|
|
|
|
|
|
assert idx >= 0
|
|
|
|
|
|
body = COMMANDS_JS[idx:idx + 800]
|
2026-04-26 02:21:00 +00:00
|
|
|
|
# cmdSteer now delegates to _trySteer; the fallback (queueSessionMessage
|
|
|
|
|
|
# + cancelStream) lives inside _trySteer.
|
|
|
|
|
|
assert "_trySteer" in body, "cmdSteer must call _trySteer to use the real /api/chat/steer endpoint"
|
|
|
|
|
|
# The shared helper must contain the 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 + 1500]
|
|
|
|
|
|
assert "queueSessionMessage" in helper_body
|
|
|
|
|
|
assert "cancelStream" 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
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── send() busy branch ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def test_slash_commands_clear_pending_files(self):
|
2026-04-26 02:21:00 +00:00
|
|
|
|
"""All three busy command handlers must clear S.pendingFiles (directly
|
|
|
|
|
|
or via _trySteer) after enqueuing, so staged files are not duplicated.
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
"""
|
2026-04-26 02:21:00 +00:00
|
|
|
|
# cmdQueue and cmdInterrupt clear pendingFiles directly
|
|
|
|
|
|
for fn_name in ("cmdQueue", "cmdInterrupt"):
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
idx = COMMANDS_JS.find(f"function {fn_name}(")
|
|
|
|
|
|
assert idx >= 0, f"{fn_name} not found"
|
|
|
|
|
|
body = COMMANDS_JS[idx:idx + 800]
|
|
|
|
|
|
assert "S.pendingFiles=[]" in body, (
|
2026-04-26 02:21:00 +00:00
|
|
|
|
f"{fn_name} must clear S.pendingFiles after queueSessionMessage"
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
assert "renderTray()" in body, (
|
|
|
|
|
|
f"{fn_name} must call renderTray() after clearing pendingFiles"
|
|
|
|
|
|
)
|
2026-04-26 02:21:00 +00:00
|
|
|
|
# cmdSteer delegates to _trySteer; that helper clears pendingFiles
|
|
|
|
|
|
idx_try = COMMANDS_JS.find("function _trySteer(")
|
|
|
|
|
|
assert idx_try >= 0, "_trySteer not found"
|
|
|
|
|
|
try_body = COMMANDS_JS[idx_try:idx_try + 1200]
|
|
|
|
|
|
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"
|
|
|
|
|
|
)
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-28 03:15:50 +00:00
|
|
|
|
class TestBusySendButton:
|
|
|
|
|
|
"""The composer send button must remain usable for busy-input actions."""
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_send_btn_uses_single_primary_action_button(self):
|
|
|
|
|
|
idx = UI_JS.find("function updateSendBtn()")
|
|
|
|
|
|
assert idx >= 0, "updateSendBtn() not found"
|
|
|
|
|
|
body = UI_JS[idx:UI_JS.find("function setBusy", idx)]
|
|
|
|
|
|
assert "getComposerPrimaryAction()" in body, (
|
|
|
|
|
|
"updateSendBtn must derive icon/color/enabled state from one composer-primary action helper"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "btn.dataset.action=action" in body, (
|
|
|
|
|
|
"btnSend should expose its current action for CSS, tests, and accessibility"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "btn.classList.toggle('stop',action==='stop')" in body, (
|
|
|
|
|
|
"busy/no-draft state should turn the single primary button into the red stop action"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "btn.style.display=''" in body, (
|
|
|
|
|
|
"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):
|
|
|
|
|
|
idx = UI_JS.find("function getComposerPrimaryAction()")
|
|
|
|
|
|
assert idx >= 0, "getComposerPrimaryAction() not found"
|
|
|
|
|
|
body = UI_JS[idx:UI_JS.find("function _setComposerPrimaryButtonIcon", idx)]
|
|
|
|
|
|
assert "return 'stop'" in body, "busy/no-draft + active stream must map to stop"
|
|
|
|
|
|
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"
|
2026-04-28 05:48:15 +00:00
|
|
|
|
assert "_getExplicitBusyCommandAction(msg&&msg.value)" in body, (
|
|
|
|
|
|
"explicit /queue, /interrupt, and /steer drafts must override the Busy input mode for button visuals"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_explicit_busy_commands_override_button_visual_action(self):
|
|
|
|
|
|
idx = UI_JS.find("function _getExplicitBusyCommandAction(")
|
|
|
|
|
|
assert idx >= 0, "_getExplicitBusyCommandAction() not found"
|
|
|
|
|
|
body = UI_JS[idx:UI_JS.find("function getComposerPrimaryAction", idx)]
|
|
|
|
|
|
assert "name==='queue'" in body and "return 'queue'" in body, (
|
|
|
|
|
|
"typing /queue <message> should show the queue/list-end button even in another busy mode"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "name==='steer'" in body and "return 'steer'" in body, (
|
|
|
|
|
|
"typing /steer <message> should show the steer/compass button even when the global mode is queue"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "name==='interrupt'" in body and "return 'interrupt'" in body, (
|
|
|
|
|
|
"typing /interrupt <message> should show the interrupt/skip-forward button even in another busy mode"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "if(!args) return null" in body, (
|
|
|
|
|
|
"partial slash commands without a payload should not override the primary button while the user is still typing"
|
|
|
|
|
|
)
|
2026-04-28 03:15:50 +00:00
|
|
|
|
|
|
|
|
|
|
def test_send_button_click_uses_primary_action_handler(self):
|
|
|
|
|
|
assert "function handleComposerPrimaryAction()" in UI_JS, (
|
|
|
|
|
|
"btnSend click should route through a primary action handler so Stop can cancel instead of sending"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "handleComposerPrimaryAction" in BOOT_JS, (
|
|
|
|
|
|
"boot.js should wire btnSend to handleComposerPrimaryAction(), not directly to send()"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
class TestSendBusyBranchDispatch:
|
|
|
|
|
|
"""send()'s busy block must read window._busyInputMode and branch accordingly."""
|
|
|
|
|
|
|
|
|
|
|
|
def test_send_reads_busy_input_mode(self):
|
|
|
|
|
|
# The send() function should read window._busyInputMode 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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_send_calls_cancel_stream_on_interrupt(self):
|
|
|
|
|
|
send_idx = MESSAGES_JS.find("async function send(")
|
|
|
|
|
|
send_body = MESSAGES_JS[send_idx:send_idx + 3000]
|
|
|
|
|
|
# The interrupt branch must call cancelStream
|
|
|
|
|
|
assert "cancelStream" in send_body
|
|
|
|
|
|
# And queue before cancel (otherwise the drain has nothing to pick up)
|
|
|
|
|
|
# Verify the order textually: queueSessionMessage appears before cancelStream
|
|
|
|
|
|
# within the busy block's interrupt branch
|
|
|
|
|
|
cancel_idx = send_body.find("cancelStream")
|
|
|
|
|
|
queue_idx = send_body.find("queueSessionMessage")
|
|
|
|
|
|
assert queue_idx >= 0 and cancel_idx >= 0
|
|
|
|
|
|
assert queue_idx < cancel_idx, (
|
|
|
|
|
|
"queueSessionMessage must run before cancelStream so the drain "
|
|
|
|
|
|
"after setBusy(false) picks up the queued message"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
v0.50.216: compression chains, renderer fixes, HTML preview, approval z-index, /steer fix, reasoning chip (#1075)
* fix(workspace): add .html/.htm to MIME_MAP so HTML preview renders correctly
MIME_MAP was missing entries for .html and .htm. The server fell back to
Content-Type: application/octet-stream, which browsers refuse to render as
HTML in an iframe — causing a blank white preview.
The rest of the pipeline was already correct: the iframe exists in
static/index.html, openFile() in static/workspace.js routes .html to
showPreview('html'), and _handle_file_raw() in api/routes.py sets the
correct CSP sandbox header when ?inline=1 is present. The only missing
piece was the MIME type.
* test(workspace): lock in MIME_MAP entry for .html/.htm
PR #1070 added .html/.htm → text/html to MIME_MAP in api/config.py
to fix the blank workspace HTML preview iframe. Without a direct
assertion on the MIME_MAP entries, the fix could silently regress
(the existing test_779_html_preview.py tests cover the iframe wiring,
the inline=1 query handling, and the CSP sandbox header — but none of
them touch MIME_MAP itself).
Add a single regression test that asserts MIME_MAP['.html'] and
MIME_MAP['.htm'] are both 'text/html' so any future removal of those
entries fails CI immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(composer): raise .approval-card.visible z-index above .queue-card
.queue-card has z-index:2. .approval-card.visible had no z-index, so the
queue flyout would render on top of the approval card when both were visible
simultaneously — obscuring the Allow/Deny buttons.
Fix: add z-index:3 to .approval-card.visible so approvals always render
above the queue flyout. Approval is a blocking, security-relevant interaction
and must never be obscured by passive UI elements.
* test(composer): pin approval-card z-index > queue-card invariant
PR #1071 raises .approval-card.visible to z-index:3 so the security-
relevant Allow / Deny buttons stay clickable when the queue flyout is
also open. Without a regression test, a future CSS edit could silently
drop the z-index back below queue-card (z-index:2) and reintroduce the
bug — there is no automated UI test covering this stacking interaction.
Add a focused regex check that pins the invariant:
.approval-card.visible z-index must be strictly greater than
.queue-card z-index.
Modeled on the existing CSS-regex regression style in
tests/test_mobile_layout.py (test_profile_dropdown_not_clipped_by_overflow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: intercept /steer /interrupt /queue before busy-mode routing in send()
Root cause: slash commands entered while the agent is busy never reached
the command dispatcher. send() enters the busy block and returns early at
line ~50, so the slash-command intercept (~line 56) is never reached.
The text was queued as a plain message. When it drained after the turn
ended, cmdSteer / cmdInterrupt ran on an idle session, saw no active stream,
and showed "No active task to stop."
Fix: at the top of the busy block, before checking busyMode, check if the
text starts with / and is one of the three control commands. If so, dispatch
the handler immediately and return. This lets the user type /steer, /interrupt,
or /queue at any time — including while the agent is mid-stream — and have
them execute against the live session.
Two new regression tests added:
- test_slash_commands_intercepted_before_busymode_routing: verifies the
intercept appears before the busyMode routing in the busy block
- test_steer_intercept_calls_handler_directly: verifies the intercept calls
_bc.fn(_pc.args) and returns, not queues
* test(busy-intercept): pin sync input-clear before await in slash intercept
PR #1072's intercept clears the msg input before awaiting the handler.
Order matters: if the await happens first (or if the clear is moved
inside the handler), the input still shows '/steer foo' for the duration
of the await. A reflexive second Enter press during that window — common
while waiting for the toast — re-runs send(): either re-fires the
handler (double-steer) or, if the turn just ended, falls through to the
non-busy slash dispatcher and drops a confusing "No active task to stop."
Add test_steer_intercept_clears_input_before_await pinning the order so
this UX invariant cannot silently regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: update steer i18n and settings copy — steer no longer interrupts
With the real /steer implementation (agent.steer() via /api/chat/steer),
steer injects a correction mid-turn WITHOUT interrupting the current stream.
The previous copy said "falls back to interrupt", "Steer (interrupt + send)",
etc. — accurate only for the old placeholder, not the real implementation.
Changes across all 6 locales (en/ru/es/de/zh/zh-Hant):
cmd_steer: "falls back to interrupt" removed
settings_busy_input_mode_steer: "interrupt + send" → "mid-turn correction"
cmd_steer_fallback: "interrupted" → "queued for next turn"
busy_steer_fallback: "interrupted instead" → "queued for next turn"
settings_desc_busy_input_mode: "currently falls back to interrupt" removed
Also:
static/index.html: inline fallback text updated to match
static/commands.js: internal comment clarified (fallback = queue+cancel,
not "interrupt mode" which implies the primary action)
* fix(renderer): group consecutive blockquote lines into single element
Root cause: the old rule `s.replace(/^> (.+)$/gm, ...)` had three bugs:
1. `.+` required at least one character — bare `>` lines (blank
continuation lines) did not match and passed through as literal `>`
2. Each matching line became its own `<blockquote>` element — a 10-line
blockquote produced 10 stacked `<blockquote>` tags with no grouping
3. When a fenced code block sat inside a blockquote, the fence-stash
pass consumed the code content and left orphaned `>` lines that the
old `.+` pattern could not match
Fix: replace the single-line regex with a group-based approach that matches
one or more consecutive `>` lines as a single block, strips the `>` prefix
from each line, passes each non-empty line through inlineMd(), turns blank
`>` lines into `<br>`, and wraps the entire group in one `<blockquote>`.
14 regression tests added covering:
- Single-line blockquotes (regression)
- Multi-line grouping (2 and 10 lines)
- Two separate blockquotes staying separate
- Bare `>` and `>text` (no space) edge cases
- Blank continuation lines → <br>
- Bold / italic / inline-code inside blockquotes
- Blockquote followed by normal paragraph
* fix(renderer): drop empty trailing line from blockquote match
The new group-based blockquote rule introduced in this PR captures the
trailing newline in its (?:\n|$) clause. After block.split('\n') that
trailing newline produces an empty final element. The original filter
only dropped lone bare '>' artifacts on the last line, so the empty
final element survived, and the .map(blank → '<br>') step turned it
into a phantom <br> immediately before </blockquote>.
Visible symptom: any blockquote whose source ends with \n (the common
case — a quote followed by another paragraph or end-of-message) renders
with an extra blank line at the bottom of the quote.
Reproducer:
'> Hello\n\nThe rest of the message.'
→ '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.'
^^^ phantom <br>
Fix: replace the single-line filter with a while-loop that pops trailing
lines while they are either empty OR a bare '>'. This matches the
intent the Python test mirror in tests/test_blockquote_rendering.py
already had (the mirror was correct; the JS was not — that's why
the original tests passed despite the bug).
Also add four new regression tests in TestNoPhantomTrailingBr that pin
the no-trailing-<br> invariant for the common shapes:
- input ending with \n
- quote followed by paragraph (the real-world case)
- multi-line quote ending with \n
- quote with blank continuation + trailing \n (internal <br> stays,
trailing <br> does not)
Verified end-to-end with node against the actual JS regex.
244 renderer-adjacent tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(renderer): comprehensive markdown fixes — strikethrough, task lists, CRLF, nested blockquotes
Five additional fixes on top of the blockquote grouping from the initial commit:
1. CRLF normalisation: strip \r\n → \n at start of renderMd so Windows
line endings do not produce stray \r characters in rendered output
2. Strikethrough: ~~text~~ → <del>text</del> in both inlineMd() (for use
inside blockquotes/lists) and the outer pass (for plain paragraphs).
Added <del> to SAFE_TAGS and SAFE_INLINE so it is not HTML-escaped.
3. Task lists: - [x] / - [ ] items in unordered lists render as ✅/☐
via task-done/task-todo span wrappers. Checks [X] (uppercase) too.
4. Nested blockquotes: >> / >>> etc. now recurse so each level gets its
own <blockquote> element rather than passing through as literal >.
Implemented by extracting the blockquote rule into _applyBlockquotes()
which calls itself recursively on the stripped inner content.
5. Lists inside blockquotes: > - item now renders <ul><li> inside the
blockquote instead of a literal "- item" string. Task list items work
inside blockquotes too (> - [x] done → ✅ inside <blockquote><ul>).
Also fixed test_issue342.py search window (5000→10000 chars) — the CRLF
strip at the top of renderMd pushed the autolink regex past the old limit.
68 new tests in test_renderer_comprehensive.py + test_blockquote_rendering.py
covering all constructs, edge cases, and combinations.
* fix(renderer): restore space in blockquote prefix-strip regex
Commit 04e7b53 changed the blockquote prefix-strip regex from
/^>[ \t]?/ (consume "> ", "\t>", or just ">")
to
/^>[\t]?/ (only consume "\t>" or just ">")
The space character was dropped from the character class. Since
practically every blockquote an LLM produces is "> " (greater-than
followed by a space), this leaves a leading space artifact on every
stripped blockquote line. Worse, the leading space breaks the
list-detection regex `^(?: )?[-*+] ` inside the new `_applyBlockquotes`
helper — that regex requires either zero or two leading spaces, never
one — so the new "list inside blockquote" feature never fired for
the canonical input shape `> - item`.
Reproducer (against the actual ui.js via node, before the fix):
> Hello world → <blockquote> Hello world</blockquote>
^ phantom leading space
> Steps: → <blockquote>Steps:
> - one - one
> - two - two</blockquote>
^ literal text, NOT a <ul>; lists-in-quote feature broken
> - [x] done → blockquote with literal "[x] done", no checkbox span
Tests passed despite the bug because tests/test_blockquote_rendering.py
and tests/test_renderer_comprehensive.py validate against a Python
mirror (`_apply_blockquotes`) whose strip regex is `^>[ \t]?` — i.e.
the mirror is correct, the JS is not, and the static-mirror tests
can't catch the divergence. Same shape of bug as commit 94d63d0
(phantom <br> in trailing line) where the mirror was right and the JS
was wrong.
Fix: restore the space character in the strip regex's character class.
Add tests/test_renderer_js_behaviour.py — 11 tests that drive the
ACTUAL renderMd via node and assert on rendered output for the most
common LLM shapes (single-line quote, multi-line quote, list inside
quote, task list inside quote, nested >>>, strikethrough inside and
outside quote, top-level task list, quote followed by heading,
multi-paragraph quote with list, CRLF normalisation).
Verified: the buggy regex makes 6 of those 11 tests fail; the corrected
regex makes all 11 pass.
Suite: 2354 passed, 0 new failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Collapse agent session compression chains
* Restore upstream changelog entries
* fix(agent_sessions): bubble active compression chains to top by tip last_activity
The original PR merge kept the chain head's id/title/started_at and overrode
id/model/message_count/ended_at/end_reason from the tip — but did NOT override
last_activity. Since the projected list is sorted by last_activity DESC and
the WebUI sidebar surfaces updated_at = last_activity, an actively-used
compression chain whose tip is being edited NOW would sort by the ROOT's
old last_activity and fall below recently touched standalone sessions.
Reproducer (with the harness against actual code, before the fix):
- root: started 30 days ago, last msg 30 days ago
- tip: started 28 days ago (parent_session_id=root), last msg 5 seconds ago
- standalone: last msg 2 days ago
Sidebar order with original PR:
[0] standalone (48h ago)
[1] active_tip (last_activity=root's 720h ago) ← wrong
Sidebar order after fix:
[0] active_tip (last_activity=tip's 0h ago) ← correct
[1] standalone (48h ago)
This matches Hermes Agent's own list_sessions_rich projection at
hermes_state.py:903-909, which overrides "last_active" from the tip
exactly so that the agent CLI's session list orders the same way.
Add ``last_activity`` to the merge-from-tip key list, update the existing
test_compression_chain_collapses_to_latest_tip_in_sidebar assertion to
expect tip-derived updated_at, and add
test_compression_chain_bubbles_to_top_by_tip_activity locking in the
bubble-to-top invariant — without this regression test the previous
behaviour passed CI because no test exercised the sort order against a
mixed set of chains and standalone sessions.
The chain head's started_at (created_at) and title remain preserved, so
users can still find the conversation by its original date and name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: v0.50.216 release notes and version bump
Compression chains, renderer fixes, HTML preview, approval z-index, /steer fix.
* chore: gitignore local-only review harness directory
Adds .local-review/ to .gitignore so renderer drivers, sample inputs,
fixture builders, and other reviewer scratch files do not accidentally
get committed. Nothing under that path is ever shared in the repo;
keeping the entry tracked makes the boundary explicit for any future
contributor who creates the directory locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Keep reasoning chip visible for None effort
* test(reasoning): pin chip render output via node, not just source regex
The PR's static checks in test_reasoning_chip_btw_fixes.py validate the
shape of _applyReasoningChip (no display='none' literal, the right
classList.toggle call exists, the right label literals are in the
function body) but pass even if the runtime detail is wrong — for
example if `inactive` were inverted, _normalizeReasoningEffort
mishandled whitespace, or _formatReasoningEffortLabel returned the
wrong literal for an unknown input.
Add tests/test_reasoning_chip_js_behaviour.py — 11 tests that drive
the actual _applyReasoningChip() via node and assert on the rendered
DOM state for each effort value:
TestChipAlwaysVisible
- empty / null -> "Default" label, inactive=true
- "none" -> "None" label, inactive=true
- "low"/"high" -> verbatim label, inactive=false
TestNormalizationEdgeCases
- "NONE" -> normalises to "None"
- " none " -> trims and normalises
- unknown junk -> falls through visible, never hidden
TestTitleAttributeAccessibility
- title attribute carries the human-readable label for tooltip /
screen-reader use
Sanity-checked against master's pre-fix ui.js: 11/11 fail (bug caught).
Against this PR's ui.js: 11/11 pass.
This pattern (drive the actual JS via node) caught two regex-only
regressions in PR #1073 where the Python mirror was correct while the
JS was broken. Same protection added here so the chip-visibility
contract can't silently break in a future refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add #1074 to v0.50.216 changelog, bump test count to 2428
* fix(i18n): restore broken Unicode in Russian and Spanish steer strings
Commit 56c7a14 (fix: update steer i18n and settings copy) accidentally
stripped the `\u` prefix from Unicode escape sequences in two locales,
producing garbled literal hex strings visible to users:
Spanish (es):
- cmd_steer: correcci00f3n → corrección
- cmd_steer_fallback: 2014 en cola → — en cola
- busy_steer_fallback: 2014 en cola → — en cola
- settings_desc_busy_input_mode: qu00e9, est00e1, correcci00f3n → qué, está, corrección
- settings_busy_input_mode_steer: correcci00f3n → corrección
Russian (ru):
- settings_desc_busy_input_mode: the entire Cyrillic string was
replaced with raw 4-hex-char code-points without the \u prefix
(041e043f... instead of actual Cyrillic). Decoded:
"Определяет поведение при отправке сообщения во время работы
агента. Очередь ждёт; Прерывание отменяет и начинает заново;
Steer внедряет коррекцию без прерывания."
Fix: write the correct characters directly (UTF-8 is the file encoding
so embedding them literally is cleaner than \u escapes for long text).
All other locales (en, de, zh, zh-Hant) were not affected — confirmed
by grepping for bare hex run-ons in the updated file.
Verified: node --check static/i18n.js passes; full pytest suite green
(2365 passed, 47 skipped).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: remove duplicate compression chain entry from [Unreleased]
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-04-26 04:06:31 +00:00
|
|
|
|
def test_slash_commands_intercepted_before_busymode_routing(self):
|
|
|
|
|
|
"""The three busy-control slash commands (/steer /interrupt /queue) must be
|
|
|
|
|
|
intercepted at the TOP of the busy block — before the busyMode routing — so
|
|
|
|
|
|
they execute immediately while the agent is running.
|
|
|
|
|
|
|
|
|
|
|
|
Without this intercept, typing /steer while busy queues the text as a plain
|
|
|
|
|
|
message. When it drains after the turn ends there is no active stream, so
|
|
|
|
|
|
cmdSteer says "No active task to stop." and the steer is lost entirely.
|
|
|
|
|
|
"""
|
|
|
|
|
|
send_idx = MESSAGES_JS.find("async function send(")
|
|
|
|
|
|
assert send_idx >= 0, "send() not found"
|
|
|
|
|
|
# Look in the first 500 chars of the busy block for the intercept
|
|
|
|
|
|
busy_start = MESSAGES_JS.find("S.busy||compressionRunning", send_idx)
|
|
|
|
|
|
assert busy_start >= 0, "busy block not found"
|
|
|
|
|
|
# The intercept must appear BEFORE the busyMode assignment
|
|
|
|
|
|
intercept_idx = MESSAGES_JS.find("'steer','interrupt','queue'", busy_start)
|
|
|
|
|
|
busymode_idx = MESSAGES_JS.find("_busyInputMode||'queue'", busy_start)
|
|
|
|
|
|
assert intercept_idx >= 0, (
|
|
|
|
|
|
"send() must intercept /steer /interrupt /queue before the busyMode "
|
|
|
|
|
|
"routing block — otherwise they queue instead of executing immediately"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert intercept_idx < busymode_idx, (
|
|
|
|
|
|
"The slash-command intercept must come BEFORE the busyMode routing "
|
|
|
|
|
|
"so /steer executes while the agent is running, not after the turn ends"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_steer_intercept_calls_handler_directly(self):
|
|
|
|
|
|
"""The busy-intercept must dispatch via _bc.fn(_pc.args), not queue the text."""
|
|
|
|
|
|
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'", 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)
|
|
|
|
|
|
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)"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert "return;" in intercept_block, (
|
|
|
|
|
|
"The intercept must return after dispatching so send() does not also queue"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_steer_intercept_clears_input_before_await(self):
|
|
|
|
|
|
"""The intercept must clear $('msg').value BEFORE awaiting the handler.
|
|
|
|
|
|
|
|
|
|
|
|
Without the sync clear, the input field still shows '/steer foo' after
|
|
|
|
|
|
the steer fires. If the user presses Enter again (a common reflex while
|
|
|
|
|
|
waiting for the toast), send() re-runs and either re-fires the command
|
|
|
|
|
|
or — once the turn ended — drops a confusing 'No active task to stop.'
|
|
|
|
|
|
"""
|
|
|
|
|
|
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'", busy_start)
|
|
|
|
|
|
busymode_idx = MESSAGES_JS.find("_busyInputMode||'queue'", 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")
|
|
|
|
|
|
assert clear_idx >= 0, (
|
|
|
|
|
|
"The intercept must clear $('msg').value (so the field doesn't keep "
|
|
|
|
|
|
"showing /steer foo after the command fires)"
|
|
|
|
|
|
)
|
|
|
|
|
|
assert await_idx >= 0, "await _bc.fn(...) must be present in the intercept"
|
|
|
|
|
|
assert clear_idx < await_idx, (
|
|
|
|
|
|
"$('msg').value='' must be cleared BEFORE awaiting the handler — "
|
|
|
|
|
|
"otherwise a reflexive Enter press during the await re-fires the command"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067)
* feat: busy input modes with queue/interrupt/steer slash commands
- Add busy_input_mode setting (queue/interrupt/steer) to config defaults
- Add /queue, /interrupt, /steer slash commands with handlers
- Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior)
- Add settings dropdown in settings panel with load/save/apply wiring
- Initialize window._busyInputMode at boot and on settings save
- Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant)
Addresses #720
* test: 17 regression tests for busy_input_mode + slash commands
PR description noted manual testing only. Added structural tests
matching the pattern used by recent contributor PRs (#1010, #1011,
#1018, #1022, #1058) so future refactors don't silently regress
the wiring:
Backend (api/config.py):
- default 'queue' is set in _DEFAULT_SETTINGS
- enum validator restricts to {queue, interrupt, steer}
Slash commands (static/commands.js):
- /queue, /interrupt, /steer all registered with correct fns
- /interrupt and /steer set noEcho:true (the queued payload
becomes the visible turn, not the slash invocation)
- cmdQueue requires S.busy
- cmdInterrupt + cmdSteer call queueSessionMessage before
cancelStream (otherwise the drain has nothing to pick up)
send() busy branch (static/messages.js):
- reads window._busyInputMode
- calls cancelStream on interrupt/steer
- queues before cancelling (ordering invariant)
Boot init + panels.js wiring (static/boot.js, static/panels.js):
- both success and fallback paths set window._busyInputMode
- load/save/apply path threads busy_input_mode through
i18n (static/i18n.js):
- all 17 new keys present in each of the 6 locale blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers
1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text
as a user bubble, then the drain would send the queued message, causing a
double-bubble in the conversation (#840 pattern).
2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue
payload but never cleared S.pendingFiles or called renderTray(). Staged files
would remain in the tray and be re-attached on the next send(), duplicating
attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge().
3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only
interrupt + steer), now documents that all three must set noEcho:true.
4. test_slash_commands_clear_pending_files: new test that all three handlers clear
S.pendingFiles and call renderTray() after enqueuing.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
* docs: v0.50.214 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
|
|
|
|
# ── Boot init + settings panel wiring ───────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestBootAndPanelsWiring:
|
|
|
|
|
|
def test_boot_init_default_path(self):
|
|
|
|
|
|
"""Boot success path initialises window._busyInputMode from settings."""
|
|
|
|
|
|
assert "window._busyInputMode=(s.busy_input_mode||'queue')" in BOOT_JS
|
|
|
|
|
|
|
|
|
|
|
|
def test_boot_init_fallback_path(self):
|
|
|
|
|
|
"""Boot fallback path (settings load failed) initialises to safe default."""
|
|
|
|
|
|
# The fallback should set window._busyInputMode='queue'
|
|
|
|
|
|
assert "window._busyInputMode='queue'" 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 "window._busyInputMode=body.busy_input_mode" in PANELS_JS, (
|
|
|
|
|
|
"_applySavedSettingsUi must propagate busy_input_mode to the global"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_index_html_dropdown_has_three_options(self):
|
|
|
|
|
|
idx = INDEX_HTML.find('id="settingsBusyInputMode"')
|
|
|
|
|
|
assert idx >= 0
|
|
|
|
|
|
block = INDEX_HTML[idx:idx + 800]
|
|
|
|
|
|
assert 'value="queue"' in block
|
|
|
|
|
|
assert 'value="interrupt"' in block
|
|
|
|
|
|
assert 'value="steer"' in block
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── i18n locale coverage ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
class TestI18nKeys:
|
|
|
|
|
|
"""All 17 new keys must appear in each of the 6 locale blocks."""
|
|
|
|
|
|
|
|
|
|
|
|
REQUIRED_KEYS = [
|
|
|
|
|
|
"cmd_queue",
|
|
|
|
|
|
"cmd_interrupt",
|
|
|
|
|
|
"cmd_steer",
|
|
|
|
|
|
"cmd_queue_no_msg",
|
|
|
|
|
|
"cmd_queue_not_busy",
|
|
|
|
|
|
"cmd_queue_confirm",
|
|
|
|
|
|
"cmd_interrupt_no_msg",
|
|
|
|
|
|
"cmd_interrupt_confirm",
|
|
|
|
|
|
"cmd_steer_no_msg",
|
|
|
|
|
|
"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",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def test_each_key_appears_at_least_six_times(self):
|
|
|
|
|
|
"""Each key should appear once per locale (en, ru, es, de, zh, zh-Hant) = 6 occurrences minimum."""
|
|
|
|
|
|
for key in self.REQUIRED_KEYS:
|
|
|
|
|
|
count = I18N_JS.count(f"{key}:")
|
|
|
|
|
|
assert count >= 6, (
|
|
|
|
|
|
f"i18n key {key!r} appears {count} times; expected ≥6 (one per locale block)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_key_count_total(self):
|
|
|
|
|
|
"""17 keys × 6 locales = 102 minimum occurrences across the file."""
|
|
|
|
|
|
total = sum(I18N_JS.count(f"{key}:") for key in self.REQUIRED_KEYS)
|
|
|
|
|
|
assert total >= 17 * 6, (
|
|
|
|
|
|
f"Total i18n occurrences = {total}; expected ≥ {17*6}"
|
|
|
|
|
|
)
|