Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/CodeShellManager/Assets/terminal-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down
12 changes: 5 additions & 7 deletions src/CodeShellManager/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/CodeShellManager/Terminal/TerminalBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ public sealed class TerminalBridge : IDisposable
/// </summary>
public event Action? KeyboardInput;

/// <summary>
/// The user clicked into this pane. Posted from the page's <c>mousedown</c>, because
/// WebView2 is an <c>HwndHost</c>: mouse input landing on hosted native content never
/// raises WPF routed events, so a <c>PreviewMouseLeftButtonDown</c> 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.
/// </summary>
public event Action? PaneActivated;

/// <summary>
/// Fires when the user presses a keyboard accelerator (Ctrl-combo, F-key, etc.)
/// while the WebView2 has focus. Subscribers set <c>e.Handled = true</c> to prevent
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 9 additions & 4 deletions src/CodeShellManager/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down