From 74dee6b665afec76bdf47440d450180515721cf6 Mon Sep 17 00:00:00 2001 From: vansour Date: Wed, 15 Apr 2026 23:12:47 +0800 Subject: [PATCH 1/3] fix: respect IME composition in Enter submit flows --- static/boot.js | 8 ++++++- static/sessions.js | 19 +++++++++++++--- static/ui.js | 9 ++++++-- tests/test_ime_composition.py | 43 +++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 tests/test_ime_composition.py diff --git a/static/boot.js b/static/boot.js index 771cd47..75f322b 100644 --- a/static/boot.js +++ b/static/boot.js @@ -446,7 +446,12 @@ $('msg').addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();navigateCmdDropdown(1);return;} if(e.key==='Tab'){e.preventDefault();selectCmdDropdownItem();return;} if(e.key==='Escape'){e.preventDefault();hideCmdDropdown();return;} - if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();selectCmdDropdownItem();return;} + if(e.key==='Enter'&&!e.shiftKey){ + if(e.isComposing){return;} + e.preventDefault(); + selectCmdDropdownItem(); + return; + } } // Send key: respect user preference. // On touch-primary devices (software keyboard), default to Enter = newline @@ -454,6 +459,7 @@ $('msg').addEventListener('keydown',e=>{ // The 'ctrl+enter' setting also uses this behavior (Enter = newline). // Users can override in Settings by explicitly choosing 'enter' mode. if(e.key==='Enter'){ + if(e.isComposing){return;} const _mobileDefault=matchMedia('(pointer:coarse)').matches&&window._sendKey==='enter'; if(window._sendKey==='ctrl+enter'||_mobileDefault){ if(e.ctrlKey||e.metaKey){e.preventDefault();send();} diff --git a/static/sessions.js b/static/sessions.js index f24ce8d..80aaaa2 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -671,7 +671,12 @@ function renderSessionListFromCache(){ setTimeout(()=>{ if(_renamingSid===null) renderSessionListFromCache(); },50); }; inp.onkeydown=e2=>{ - if(e2.key==='Enter'){e2.preventDefault();e2.stopPropagation();finish(true);} + if(e2.key==='Enter'){ + if(e2.isComposing){return;} + e2.preventDefault(); + e2.stopPropagation(); + finish(true); + } if(e2.key==='Escape'){e2.preventDefault();e2.stopPropagation();finish(false);} }; // onblur: cancel only -- no accidental saves @@ -888,7 +893,11 @@ function _startProjectCreate(bar, addBtn){ } }; inp.onkeydown=(e)=>{ - if(e.key==='Enter'){e.preventDefault();finish(true);} + if(e.key==='Enter'){ + if(e.isComposing){return;} + e.preventDefault(); + finish(true); + } if(e.key==='Escape'){e.preventDefault();finish(false);} }; inp.onblur=()=>finish(false); @@ -910,7 +919,11 @@ function _startProjectRename(proj, chip){ } }; inp.onkeydown=(e)=>{ - if(e.key==='Enter'){e.preventDefault();finish(true);} + if(e.key==='Enter'){ + if(e.isComposing){return;} + e.preventDefault(); + finish(true); + } if(e.key==='Escape'){e.preventDefault();finish(false);} }; inp.onblur=()=>finish(false); diff --git a/static/ui.js b/static/ui.js index 3fb2601..e6342df 100644 --- a/static/ui.js +++ b/static/ui.js @@ -767,6 +767,7 @@ function _ensureAppDialogBindings(){ return; } if(e.key==='Enter'){ + if(e.isComposing) return; const target=e.target; const isTextarea=target&&target.tagName==='TEXTAREA'; if(!isTextarea){ @@ -1399,7 +1400,7 @@ function editMessage(btn) { bar.querySelector('.msg-edit-cancel').onclick = () => cancelEdit(row, originalText, body); ta.addEventListener('keydown', e => { - if(e.key==='Enter' && !e.shiftKey) { e.preventDefault(); bar.querySelector('.msg-edit-send').click(); } + if(e.key==='Enter' && !e.shiftKey) { if(e.isComposing) return; e.preventDefault(); bar.querySelector('.msg-edit-send').click(); } if(e.key==='Escape') { e.preventDefault(); cancelEdit(row, originalText, body); } }); } @@ -1719,7 +1720,11 @@ function _renderTreeItems(container, entries, depth){ inp.replaceWith(nameEl); }; inp.onkeydown=(e2)=>{ - if(e2.key==='Enter'){e2.preventDefault();finish(true);} + if(e2.key==='Enter'){ + if(e2.isComposing){return;} + e2.preventDefault(); + finish(true); + } if(e2.key==='Escape'){e2.preventDefault();finish(false);} }; inp.onblur=()=>finish(false); diff --git a/tests/test_ime_composition.py b/tests/test_ime_composition.py new file mode 100644 index 0000000..7b0b0aa --- /dev/null +++ b/tests/test_ime_composition.py @@ -0,0 +1,43 @@ +import pathlib +import re + + +REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() +BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8") +UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8") +SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8") + + +def test_boot_chat_enter_send_respects_ime_composition(): + assert re.search( + r"if\(e\.key==='Enter'\)\{\s*if\(e\.isComposing\)\{return;\}", + BOOT_JS, + ), "Chat composer Enter handler must ignore IME composition Enter in static/boot.js" + assert re.search( + r"if\(e\.key==='Enter'&&!e\.shiftKey\)\{\s*if\(e\.isComposing\)\{return;\}", + BOOT_JS, + ), "Command dropdown Enter handler must ignore IME composition Enter in static/boot.js" + + +def test_ui_enter_submit_paths_respect_ime_composition(): + assert re.search( + r"document\.addEventListener\('keydown',e=>\{[\s\S]*?if\(e\.key==='Enter'\)\{\s*if\(e\.isComposing\) return;", + UI_JS, + ), \ + "App dialog Enter handler must ignore IME composition Enter in static/ui.js" + assert "if(e.key==='Enter' && !e.shiftKey) { if(e.isComposing) return; e.preventDefault();" in UI_JS, \ + "Message edit Enter-to-save handler must ignore IME composition Enter in static/ui.js" + assert re.search( + r"inp\.onkeydown=\(e2\)=>\{\s*if\(e2\.key==='Enter'\)\{\s*if\(e2\.isComposing\)\{return;\}", + UI_JS, + ), \ + "Workspace rename Enter handler must ignore IME composition Enter in static/ui.js" + + +def test_sessions_enter_submit_paths_respect_ime_composition(): + matches = re.findall( + r"if\(e2?\.key==='Enter'\)\{\s*if\(e2?\.isComposing\)\{return;\}", + SESSIONS_JS, + ) + assert len(matches) >= 3, \ + "Session and project rename/create Enter handlers must ignore IME composition Enter in static/sessions.js" From dc43a30af787b19977e7192d0770d257d011cf7f Mon Sep 17 00:00:00 2001 From: vansour Date: Wed, 15 Apr 2026 23:21:56 +0800 Subject: [PATCH 2/3] test: loosen IME guard regression assertions --- tests/test_ime_composition.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/test_ime_composition.py b/tests/test_ime_composition.py index 7b0b0aa..7471689 100644 --- a/tests/test_ime_composition.py +++ b/tests/test_ime_composition.py @@ -8,36 +8,54 @@ UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8") SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8") +def _ime_guarded_enter_pattern(event_var_pattern, require_no_shift=False): + no_shift = rf"\s*&&\s*!\s*{event_var_pattern}\.shiftKey" if require_no_shift else "" + return ( + rf"if\s*\(\s*{event_var_pattern}\.key\s*===\s*'Enter'{no_shift}\s*\)\s*\{{\s*" + rf"if\s*\(\s*{event_var_pattern}\.isComposing\s*\)\s*" + rf"(?:\{{\s*return\s*;?\s*\}}|return\s*;?)" + ) + + def test_boot_chat_enter_send_respects_ime_composition(): assert re.search( - r"if\(e\.key==='Enter'\)\{\s*if\(e\.isComposing\)\{return;\}", + _ime_guarded_enter_pattern("e"), BOOT_JS, + re.DOTALL, ), "Chat composer Enter handler must ignore IME composition Enter in static/boot.js" assert re.search( - r"if\(e\.key==='Enter'&&!e\.shiftKey\)\{\s*if\(e\.isComposing\)\{return;\}", + _ime_guarded_enter_pattern("e", require_no_shift=True), BOOT_JS, + re.DOTALL, ), "Command dropdown Enter handler must ignore IME composition Enter in static/boot.js" def test_ui_enter_submit_paths_respect_ime_composition(): assert re.search( - r"document\.addEventListener\('keydown',e=>\{[\s\S]*?if\(e\.key==='Enter'\)\{\s*if\(e\.isComposing\) return;", + rf"document\.addEventListener\('keydown',e=>\{{[\s\S]*?{_ime_guarded_enter_pattern('e')}", UI_JS, + re.DOTALL, ), \ "App dialog Enter handler must ignore IME composition Enter in static/ui.js" - assert "if(e.key==='Enter' && !e.shiftKey) { if(e.isComposing) return; e.preventDefault();" in UI_JS, \ + assert re.search( + _ime_guarded_enter_pattern("e", require_no_shift=True), + UI_JS, + re.DOTALL, + ), \ "Message edit Enter-to-save handler must ignore IME composition Enter in static/ui.js" assert re.search( - r"inp\.onkeydown=\(e2\)=>\{\s*if\(e2\.key==='Enter'\)\{\s*if\(e2\.isComposing\)\{return;\}", + rf"inp\.onkeydown=\(e2\)=>\{{\s*{_ime_guarded_enter_pattern('e2')}", UI_JS, + re.DOTALL, ), \ "Workspace rename Enter handler must ignore IME composition Enter in static/ui.js" def test_sessions_enter_submit_paths_respect_ime_composition(): matches = re.findall( - r"if\(e2?\.key==='Enter'\)\{\s*if\(e2?\.isComposing\)\{return;\}", + _ime_guarded_enter_pattern(r"e2?"), SESSIONS_JS, + re.DOTALL, ) assert len(matches) >= 3, \ "Session and project rename/create Enter handlers must ignore IME composition Enter in static/sessions.js" From e077d110c330c81a654c3d7f437c837db475dac7 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 15 Apr 2026 16:46:53 +0000 Subject: [PATCH 3/3] chore: bump version to v0.50.49, update CHANGELOG --- CHANGELOG.md | 5 +++++ static/index.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae400f..63b157d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Hermes Web UI -- Changelog +## [v0.50.49] — 2026-04-15 + +### Fixed +- **IME composition** — `isComposing` guard added to every Enter keydown handler so CJK/Japanese/Korean input method users never accidentally send mid-composition (fixes #531). Covers chat composer, command dropdown, session rename, project create/rename, app dialog, message edit, and workspace rename. Adds 3 regression tests. (PR #537 by @vansour) + ## [v0.50.48] fix: toast when model is switched during active session (#419) Synthesized from PRs #516 (armorbreak001), #517 and #518 (cloudyun888). diff --git a/static/index.html b/static/index.html index a1dd8cf..5c20188 100644 --- a/static/index.html +++ b/static/index.html @@ -552,7 +552,7 @@
System
- v0.50.48 + v0.50.49