fix(tabs): Escape in the rail search box clears only the search (#580)

The global key handler in setupEventListeners() sits on document in the
capture phase, so it ran before the box's inline onkeydown: the Escape that
cleared the search also ran closeAllPanels() (collapsing the Monitor and
Subagents panels), closeHelp() and closeSessionManager(). Calling
stopPropagation() from the inline handler came too late.

The global Escape branch now claims an Escape whose target is
#tabRailSearch while the box holds text, the same way it already claims
one for the group menu, the grouped-rail drag and the Tiles count menu,
and routes it to handleTabRailSearchKeydown(). An empty box still leaves
Escape to the global handler, and an Escape that cancels an IME
composition is left to the IME.

Tests: the unit test installs the real global listener and asserts that
Escape with text in the box closes nothing, that an empty box closes as
before, and that an Escape outside the box is not claimed. The browser
test installs setupEventListeners() for real in Chromium, so the
capture-before-inline ordering is the shipped one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-10 04:05:57 +02:00
parent 2881ef5fa5
commit ac5f2aa3f4
3 changed files with 114 additions and 3 deletions
+16 -3
View File
@@ -1427,6 +1427,13 @@ class CodemanApp {
this.closeTileCountMenu({ refocus: true });
return;
}
// And so does the rail's search box while it holds text: that Escape
// clears the search and nothing else. This listener runs in the capture
// phase, before the box's own onkeydown, so the box cannot claim it there.
if (e.target?.id === 'tabRailSearch' && this._tabRailSearch) {
this.handleTabRailSearchKeydown(e);
return;
}
this.closeAllPanels();
this.closeHelp();
if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory();
@@ -5859,10 +5866,16 @@ class CodemanApp {
input?.focus();
}
/**
* Escape in a box that holds text clears the search and nothing else. The
* global key handler (setupEventListeners) runs in the CAPTURE phase, before
* the box's inline onkeydown, so it is the one that routes the key here and
* returns before its close-every-panel branch; stopping propagation from the
* inline handler would come too late. An empty box leaves Escape to it, and
* an Escape that cancels an IME composition is the IME's.
*/
handleTabRailSearchKeydown(event) {
if (event.key !== 'Escape' || !this._tabRailSearch) return;
// Consumed here: the global Escape handler would otherwise close whatever
// else is open on the same keypress.
if (event.key !== 'Escape' || event.isComposing || !this._tabRailSearch) return;
event.preventDefault();
event.stopPropagation();
this.clearTabRailSearch();