From ca5f02bbff3ec2b886c2b0ebcbd6a661795fcec8 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Wed, 8 Jul 2026 16:56:51 -0400 Subject: [PATCH] feat(#5759): add composer focus shortcut --- static/boot.js | 9 +++ .../test_issue5759_composer_focus_shortcut.py | 74 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/test_issue5759_composer_focus_shortcut.py diff --git a/static/boot.js b/static/boot.js index 4454e7ad..97ebb598 100644 --- a/static/boot.js +++ b/static/boot.js @@ -2320,6 +2320,15 @@ document.addEventListener('keydown',async e=>{ return; } } + // Cmd/Ctrl+Alt+/ focuses the message composer without creating a chat. + if((e.metaKey||e.ctrlKey)&&e.altKey&&!e.shiftKey&&(e.key==='/'||e.code==='Slash')){ + const t=e.target; + const isText=t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable); + if(isText) return; + const composer=$('msg'); + if(composer){e.preventDefault();composer.focus();} + return; + } // Enter on approval card = Allow once (when a button inside the card is focused or // card is visible and focus is not on an input/textarea/select) if(e.key==='Enter'&&!e.metaKey&&!e.ctrlKey&&!e.shiftKey){ diff --git a/tests/test_issue5759_composer_focus_shortcut.py b/tests/test_issue5759_composer_focus_shortcut.py new file mode 100644 index 00000000..a9595d82 --- /dev/null +++ b/tests/test_issue5759_composer_focus_shortcut.py @@ -0,0 +1,74 @@ +"""Regression tests for #5759 composer focus shortcut.""" + +from pathlib import Path + + +BOOT_JS = (Path(__file__).parent.parent / "static" / "boot.js").read_text(encoding="utf-8") +CHORD = "(e.metaKey||e.ctrlKey)&&e.altKey&&!e.shiftKey&&(e.key==='/'||e.code==='Slash')" +CTRL_K = "(e.metaKey||e.ctrlKey)&&e.key==='k'" + + +def _block_from(start: int) -> str: + depth = 0 + end = start + for end in range(start, len(BOOT_JS)): + char = BOOT_JS[end] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + return BOOT_JS[start : end + 1] + raise AssertionError("shortcut block not closed") + + +def _composer_focus_branch() -> str: + start = BOOT_JS.index(CHORD) + return _block_from(start) + + +def _ctrl_k_branch() -> str: + start = BOOT_JS.index(CTRL_K) + return _block_from(start) + + +def test_composer_focus_chord_uses_alt_slash_and_stays_before_ctrl_k(): + assert CHORD in BOOT_JS + assert "(e.metaKey||e.ctrlKey)&&e.key==='/'" not in BOOT_JS + assert "e.code==='Slash'" in BOOT_JS + assert BOOT_JS.index("(e.metaKey||e.ctrlKey)&&!e.shiftKey&&!e.altKey&&(e.key==='b'||e.key==='B')") < BOOT_JS.index(CHORD) + assert BOOT_JS.index(CHORD) < BOOT_JS.index(CTRL_K) + + +def test_editable_targets_return_before_prevent_default(): + branch = _composer_focus_branch() + guard = "const isText=t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable);" + assert "const t=e.target;" in branch + assert guard in branch + guard_idx = branch.index("if(isText) return;") + prevent_idx = branch.index("e.preventDefault();") + assert guard_idx < prevent_idx + + +def test_composer_focus_branch_only_focuses_msg_and_does_not_mutate_session_state(): + branch = _composer_focus_branch() + assert "const composer=$('msg');" in branch + assert "composer.focus();" in branch + assert "newSession()" not in branch + assert "renderSessionList()" not in branch + assert "closeMobileSidebar()" not in branch + assert "send()" not in branch + assert "clearDraft" not in branch + assert "draft" not in branch.lower() + + +def test_mode_matrix_covers_focus_return_no_alt_and_unchanged_ctrl_k(): + branch = _composer_focus_branch() + ctrl_k = _ctrl_k_branch() + + assert "if(composer){e.preventDefault();composer.focus();}" in branch + assert "if(isText) return;" in branch + assert "(e.metaKey||e.ctrlKey)&&e.key==='/'" not in BOOT_JS + + assert "if(_currentSessionIsReusableEmptyChat()){" in ctrl_k + assert "await newSession();await renderSessionList();closeMobileSidebar();$('msg').focus();" in ctrl_k