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();
}