Skip to content

Commit d0b12ec

Browse files
eatnugclaude
andcommitted
Fix terminal input loss after app switch and launcher IME proxy leak in tide-app
Browser pane's sync_browser_webview_frames() was unconditionally making visible browser panes first responder, stealing it from the terminal's IME proxy after windowDidBecomeKey restored focus. Now only the focused browser pane can become first responder. Also fix launcher resolution reusing the same PaneId without removing the stale IME proxy — platform skipped proxy creation, leaving input routed to the old launcher proxy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 41bb459 commit d0b12ec

5 files changed

Lines changed: 41 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ resolver = "2"
1414

1515
[workspace.package]
1616
edition = "2021"
17-
version = "0.37.5"
17+
version = "0.37.6"
1818
license = "MIT"
1919
repository = "https://github.com/eatnug/tide"
2020
authors = ["eatnug"]

crates/tide-app/src/action/pane_lifecycle.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,22 @@ impl App {
122122
pub(crate) fn resolve_launcher(&mut self, launcher_id: tide_core::PaneId, choice: LauncherChoice) {
123123
match choice {
124124
LauncherChoice::Terminal => {
125+
// Remove the old launcher's IME proxy before creating the replacement.
126+
// The new pane reuses the same PaneId, so without this the platform's
127+
// ime_proxies map still holds the stale launcher proxy and
128+
// create_ime_proxy() skips creation — leaving first responder on the
129+
// old proxy and routing keyboard input to the wrong pane.
130+
self.ime.pending_removes.push(launcher_id);
125131
let cwd = self.focused_terminal_cwd();
126132
self.panes.remove(&launcher_id);
127133
self.create_terminal_pane(launcher_id, cwd);
128134
}
129135
LauncherChoice::NewFile => {
136+
self.ime.pending_removes.push(launcher_id);
130137
let mut pane = crate::editor_pane::EditorPane::new_empty(launcher_id);
131138
pane.editor.set_dark_mode(self.dark_mode);
132139
self.panes.insert(launcher_id, PaneKind::Editor(pane));
140+
self.ime.pending_creates.push(launcher_id);
133141
}
134142
LauncherChoice::OpenFile => {
135143
// Keep the launcher alive — the file finder will replace it
@@ -138,8 +146,10 @@ impl App {
138146
return;
139147
}
140148
LauncherChoice::Browser => {
149+
self.ime.pending_removes.push(launcher_id);
141150
let pane = crate::browser_pane::BrowserPane::new(launcher_id);
142151
self.panes.insert(launcher_id, PaneKind::Browser(pane));
152+
self.ime.pending_creates.push(launcher_id);
143153
}
144154
}
145155
self.focused = Some(launcher_id);

crates/tide-app/src/behavior_tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,21 @@ mod launcher_behavior {
823823
// Launcher should remain
824824
assert!(matches!(app.panes.get(&id), Some(PaneKind::Launcher(_))));
825825
}
826+
827+
#[test]
828+
fn resolve_launcher_queues_ime_proxy_remove_and_create_for_same_id() {
829+
let (mut app, id) = app_with_launcher();
830+
// Simulate the launcher already having a proxy (as it would in real use)
831+
app.ime.pending_creates.clear();
832+
833+
app.handle_ime_commit("e"); // resolve as editor
834+
835+
// The old launcher proxy must be queued for removal so the platform
836+
// recreates it for the new pane kind — otherwise the stale proxy
837+
// stays as first responder and input goes to the wrong pane.
838+
assert!(app.ime.pending_removes.contains(&id), "old launcher proxy not queued for removal");
839+
assert!(app.ime.pending_creates.contains(&id), "new editor proxy not queued for creation");
840+
}
826841
}
827842

828843
#[cfg(test)]

crates/tide-app/src/layout_compute.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -594,9 +594,12 @@ impl App {
594594
bp.needs_initial_navigate = false;
595595
}
596596

597-
// First responder management: when URL bar is NOT focused,
598-
// let the webview receive keyboard events directly.
599-
let should_be_first_responder = !bp.url_input_focused;
597+
// First responder management: only the focused browser pane
598+
// should become first responder. A visible-but-unfocused
599+
// browser pane must NOT steal first responder from the
600+
// terminal's IME proxy (causes input loss after app switch).
601+
let is_focused_pane = self.focused == Some(id);
602+
let should_be_first_responder = is_focused_pane && !bp.url_input_focused;
600603
if should_be_first_responder && !bp.is_first_responder {
601604
if let (Some(wv), Some(win_ptr)) =
602605
(&bp.webview, self.window_ptr)

0 commit comments

Comments
 (0)