Skip to content
Open
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
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ Both (2) and (3) **must come from the page**, and this is the part that is easy
- **WebView2 is an `HwndHost`.** Mouse input landing on hosted native content raises **no** WPF routed events, tunnelling `Preview*` ones included. A `PreviewMouseLeftButtonDown` on the host Border only ever fires for the thin ring around the terminal (#108). The same fact bites on the way out too — WPF content cannot be *drawn* over a pane either, whatever `Panel.ZIndex` says. See "Session Spinners".
- **xterm's `onData` is not "the user typed".** It also carries replies the terminal generates itself — device attributes (`ESC[?1;2c`), cursor-position reports, OSC colour replies, focus in/out (`ESC[I`/`ESC[O`) — plus mouse reports when the app enables tracking. Filtering those by inspecting the bytes cannot work; a device-attribute reply is not distinguishable from typing by shape. xterm knows internally (`triggerDataEvent`'s `wasUserInput`) but does not expose it on `onData`. `onKey` is the only honest source (#106).

**Setting `ActiveSession` moves the highlight, not the keyboard.** Keystrokes follow Win32 focus, so every path that changes the active session on the user's behalf must also call `FocusTerminal()`, or the highlight and the keyboard drift apart and typing lands in a different pane than the highlighted one. Three places enforce it: a pane click (`PaneActivated` promotes *and* focuses), restart (refocuses the revived session), and `OnWindowActivated`, which refocuses the previous session only when **no mouse button is down**. A click-to-activate is the user choosing the target, and the old unconditional refocus raced the page's `activate` message and sometimes won.

The page-side `mousedown` handler also calls `doFit()`, and the initial fit is re-run on `document.fonts.ready`. xterm derives its column count from the *measured advance width* of the font, so a fit that runs before the font loads computes the wrong `cols` and tells the PTY a width that doesn't match what is drawn — text then overlaps mid-line. The `ResizeObserver` cannot catch that, because the element size never changed, only the glyph metrics (#113).

## A fit that changes nothing must still report the size
Expand Down
28 changes: 28 additions & 0 deletions src/CodeShellManager/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5394,6 +5394,10 @@ private async Task RestartSessionCoreAsync(SessionViewModel vm, string? launched
{
_vm.ActiveSession = revived;
UpdateSidebarActiveState();
// ActiveSession alone moves the highlight, not the keyboard. The old pane's
// WebView2 is gone, so without this focus is left wherever the teardown put
// it — and typing lands in a pane other than the highlighted one.
revived.Bridge?.FocusTerminal();
}
}
}
Expand Down Expand Up @@ -6286,11 +6290,35 @@ private bool TryHandleGlobalShortcut(Key key, ModifierKeys mods)
// Refocus the last-active terminal when the window regains focus (e.g. Alt+Tab).
// Without this, focus lands on whichever WPF control happened to hold it last —
// typically not the WebView2, so keystrokes go nowhere.
//
// But NOT when the window was activated by a click. Then the click is the user saying
// where focus goes: on a pane, the page's mousedown posts `activate`, and
// MainViewModel promotes AND focuses that pane; on the sidebar, FocusSession does it.
// Refocusing the *previous* ActiveSession here raced that: FocusTerminal is a
// BeginInvoke, the page's `activate` is a cross-process message, and when the stale
// focus landed last the highlight moved to the clicked pane while the keystrokes went
// to the old one — "I type in one pane and the letters appear in another".
private void OnWindowActivated(object? sender, EventArgs e)
{
if (IsAnyMouseButtonDown()) return;
_vm.ActiveSession?.Bridge?.FocusTerminal();
}

// GetAsyncKeyState reads the PHYSICAL buttons, so a swapped-button mouse reports its
// primary click as VK_RBUTTON — check both rather than guess which one is "left".
// WPF's Mouse.LeftButton is not usable here: a click on a WebView2 (an HwndHost) is
// native input WPF never sees.
private static bool IsAnyMouseButtonDown()
{
const int VK_LBUTTON = 0x01, VK_RBUTTON = 0x02, VK_MBUTTON = 0x04;
return (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0
|| (GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0
|| (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
}

[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);

private void CycleSession(bool forward)
{
if (_vm.Sessions.Count < 2) return;
Expand Down
13 changes: 12 additions & 1 deletion src/CodeShellManager/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,18 @@ void Promote()
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;
//
// And it must take keyboard focus with it. Promote moves the highlight; the
// keyboard follows Win32 focus, which something else can hold — the window's
// Activated handler refocusing the previous session, or a restart's teardown.
// A click is the user saying where input goes, so make focus agree with the
// highlight. Throttled page-side (300ms); FocusTerminal is one BeginInvoke.
var bridge = vm.Bridge;
vm.Bridge.PaneActivated += () =>
{
Promote();
bridge.FocusTerminal();
};
// UserInput is every onData, which includes xterm's own automatic replies
// (device attributes, cursor reports, focus in/out). For the heuristic that has
// always been the "interaction" signal; for an OSC 7501 session it must not be,
Expand Down
Loading