diff --git a/src/CodeShellManager/Assets/terminal-init.js b/src/CodeShellManager/Assets/terminal-init.js index 3fbbfdd..efe5f3f 100644 --- a/src/CodeShellManager/Assets/terminal-init.js +++ b/src/CodeShellManager/Assets/terminal-init.js @@ -58,6 +58,28 @@ window.chrome.webview.postMessage(JSON.stringify({ type: 'userkey' })); }); + // ── "the user clicked into this pane" signal ─────────────────────────────── + // This MUST come from here rather than from WPF. WebView2 is an HwndHost — a + // native child window — and WPF routed mouse events (tunnelling Preview* ones + // included) do not fire for input that lands on hosted native content. A + // PreviewMouseLeftButtonDown on the host Border therefore only ever fires for + // the 2px ring around the terminal, never for a click in the terminal itself, + // so clicking a pane never made it the active session. + // + // fit() here as well: the grid rebuild that used to run on every activation + // incidentally forced a layout pass and hence a re-fit. That rebuild is now + // skipped when nothing visible changes, so re-fit on interaction has to be + // explicit — otherwise xterm's column count can drift from what the PTY was + // told, and redraws land a character off. + var lastActivate = 0; + document.addEventListener('mousedown', function () { + var now = Date.now(); + if (now - lastActivate < 300) return; + lastActivate = now; + try { fitAddon.fit(); } catch (e) {} + window.chrome.webview.postMessage(JSON.stringify({ type: 'activate' })); + }, { capture: true }); + // ── Resize notification ──────────────────────────────────────────────────── term.onResize(({ cols, rows }) => { window.chrome.webview.postMessage(JSON.stringify({ type: 'resize', cols, rows })); diff --git a/src/CodeShellManager/MainWindow.xaml.cs b/src/CodeShellManager/MainWindow.xaml.cs index 88ac440..4a0e99a 100644 --- a/src/CodeShellManager/MainWindow.xaml.cs +++ b/src/CodeShellManager/MainWindow.xaml.cs @@ -3753,13 +3753,11 @@ private Border BuildTerminalWrapper(SessionViewModel vm, WebView2 webView) Tag = accent }; - // Clicking anywhere in a pane makes that session active. Tunnelling (Preview) - // because the WebView2 swallows the bubbling event before it reaches us, and - // Handled is deliberately NOT set so the click still lands in the terminal. - // - // Without this, ActiveSession only followed sidebar clicks, so a pane clicked - // directly kept IsForeground=false and flushed its output at Background - // dispatcher priority — its own echo queued behind every other session's. + // Fires only for the thin ring/chrome AROUND the terminal — WebView2 is an + // HwndHost, so a click on the terminal itself never reaches WPF as a routed + // event. The terminal case is handled by TerminalBridge.PaneActivated, posted + // from the page. Kept because clicking the border should still activate, and + // Handled is deliberately unset so the click still passes through. activeRing.PreviewMouseLeftButtonDown += (_, _) => { if (!ReferenceEquals(_vm.ActiveSession, vm)) _vm.ActiveSession = vm; diff --git a/src/CodeShellManager/Terminal/TerminalBridge.cs b/src/CodeShellManager/Terminal/TerminalBridge.cs index 082ed45..71a7c24 100644 --- a/src/CodeShellManager/Terminal/TerminalBridge.cs +++ b/src/CodeShellManager/Terminal/TerminalBridge.cs @@ -78,6 +78,15 @@ public sealed class TerminalBridge : IDisposable /// public event Action? KeyboardInput; + /// + /// The user clicked into this pane. Posted from the page's mousedown, because + /// WebView2 is an HwndHost: mouse input landing on hosted native content never + /// raises WPF routed events, so a PreviewMouseLeftButtonDown on the host Border + /// only fires for the thin ring around the terminal — never for the terminal itself. + /// That is why clicking a pane did not make it the active session. + /// + public event Action? PaneActivated; + /// /// Fires when the user presses a keyboard accelerator (Ctrl-combo, F-key, etc.) /// while the WebView2 has focus. Subscribers set e.Handled = true to prevent @@ -351,6 +360,13 @@ private void OnWebMessageReceived(object? sender, CoreWebView2WebMessageReceived KeyboardInput?.Invoke(); break; + // Posted from the page on mousedown. WebView2 is an HwndHost, so a click + // in the terminal never reaches WPF as a routed event — this is the only + // way the host learns the user clicked into this pane. + case "activate": + PaneActivated?.Invoke(); + break; + case "resize": { int cols = root.GetProperty("cols").GetInt32(); diff --git a/src/CodeShellManager/ViewModels/MainViewModel.cs b/src/CodeShellManager/ViewModels/MainViewModel.cs index 054d72e..6240e5c 100644 --- a/src/CodeShellManager/ViewModels/MainViewModel.cs +++ b/src/CodeShellManager/ViewModels/MainViewModel.cs @@ -292,12 +292,17 @@ public void RegisterSession(SessionViewModel vm) // app enables mouse tracking — which Claude Code does. Promoting on those // turned this into hover-to-focus and repainted every pane's border on every // mouse move. Hence the mouse-report filter rather than promoting on any input. - vm.Bridge.KeyboardInput += () => + // Guarded on reference equality: these run per keystroke / per click, and the + // assign fans out to UpdateActiveTerminalHighlight across every session. + void Promote() { - // Guarded on reference equality: runs per keystroke, and the assign fans - // out to UpdateActiveTerminalHighlight across every session. if (!ReferenceEquals(ActiveSession, vm)) ActiveSession = vm; - }; + } + + vm.Bridge.KeyboardInput += Promote; + // Clicking into a pane must promote it too. This can only come from the page — + // see TerminalBridge.PaneActivated for why WPF never sees the click. + vm.Bridge.PaneActivated += Promote; vm.Bridge.UserInput += () => vm.AlertDetector?.NotifyUserInteracted(); }