Skip to content

feat: replace WebView2+xterm.js with native Microsoft.Terminal.Wpf#8

Merged
sailro merged 13 commits into
mainfrom
native-terminal
Apr 13, 2026
Merged

feat: replace WebView2+xterm.js with native Microsoft.Terminal.Wpf#8
sailro merged 13 commits into
mainfrom
native-terminal

Conversation

@sailro
Copy link
Copy Markdown
Owner

@sailro sailro commented Apr 12, 2026

Replace WebView2+xterm.js with native Microsoft.Terminal.Wpf

Summary

Replace the Chromium-based terminal renderer (WebView2 + xterm.js) with VS's built-in native terminal control (Microsoft.Terminal.Wpf) — the same DirectWrite rendering engine used by Windows Terminal and VS's own integrated terminal.

Motivation

The embedded terminal (added in PR #7) used WebView2 to host xterm.js. While functional, this carried significant overhead:

WebView2 + xterm.js (before) Microsoft.Terminal.Wpf (after)
Renderer Chromium Canvas/WebGL Native Win32 DirectWrite
VT parsing JavaScript (xterm.js) Native C++ (Windows Terminal engine)
Memory ~80MB (Chromium process) ~5MB (in-process WPF control)
Startup Slow (Chromium init + page load) Instant (WPF control)
Dependencies WebView2 runtime + 6 JS files None (ships with VS)
Focus Complex WPF↔Chromium recovery hack Native WPF focus model
Theme Hardcoded dark theme in JS VS theme integration via SetTheme()
Box-drawing Needed WebGL addon workaround Perfect natively

How it works

Microsoft.Terminal.Wpf.dll ships with every Visual Studio installation at CommonExtensions\Microsoft\Terminal\. Our extension references it at build time and resolves it at runtime via an AssemblyResolve handler that locates the DLL from the VS install directory.

The new TerminalToolWindowControl implements ITerminalConnection — a 4-member interface:

  • WriteInput(string data) — user keystroke → ConPTY
  • TerminalOutput event — ConPTY output → native renderer
  • Resize(uint rows, uint columns) — dimensions changed
  • Close() — cleanup

The ConPTY layer (TerminalProcess, TerminalSessionService) is completely unchanged — same I/O pipeline, just a different consumer.

Changes

New files

  • TerminalThemer.cs (79 lines) — VS theme → TerminalTheme with dark/light ANSI palettes (COLORREF/BGR format), auto-switches on VSColorTheme.ThemeChanged
  • TerminalToolWindowControl.cs — rewritten as ITerminalConnection implementation (183 lines, down from 209)

Removed files

  • Resources/Terminal/terminal.html — WebView2 host page
  • Resources/Terminal/terminal-app.js — xterm.js bridge, resize, I/O
  • Resources/Terminal/lib/xterm.js — xterm.js core
  • Resources/Terminal/lib/xterm.css — xterm.js styles
  • Resources/Terminal/lib/addon-fit.js — FitAddon
  • Resources/Terminal/lib/addon-webgl.js — WebglAddon

Modified files

  • CopilotCliIde.csproj — removed WebView2 NuGet + Terminal resources, added Terminal.Wpf assembly reference
  • CopilotCliIdePackage.cs — added static AssemblyResolve handler for Microsoft.Terminal.Wpf
  • Directory.Packages.props — removed Microsoft.Web.WebView2 package version
  • TerminalToolWindow.cs — simplified (removed WebView2-specific PreProcessMessage)
  • TerminalSessionService.cs — removed stale comment
  • package-lock.json — cleaned ghost xterm references

Documentation updated

  • .github/copilot-instructions.md — architecture now references Microsoft.Terminal.Wpf
  • README.md — terminal stack updated
  • CHANGELOG.md — migration documented under [Unreleased]

Unchanged (ConPTY layer)

  • TerminalProcess.cs — ConPTY process management
  • TerminalSessionService.cs — session lifecycle
  • ConPty.cs — P/Invoke bindings
  • All MCP server files — terminal is client-side only

Stats

16 files changed, 317 insertions(+), 639 deletions(-)

Net removal of 322 lines — simpler codebase with better rendering.

Testing

  • Build clean (0 errors, 0 warnings)
  • 284 server tests pass
  • Roslyn dead code analysis clean (1 pre-existing unused constant)
  • Manual testing: Copilot CLI renders with full ANSI colors, box-drawing characters, status bar, interactive prompt
  • Theme integration: dark background from VS ToolWindowBackgroundColorKey
  • Session restart (Enter after process exit) works
  • Resize handling (dock panel drag) works
  • VS 2022 compatibility (built/tested on VS 2026 Insiders only)
  • Light theme testing

Notes

  • Microsoft.Terminal.Wpf.dll is not bundled in the VSIX — it resolves from the VS install directory at runtime. This avoids redistribution concerns and ensures version compatibility with the host VS.
  • The AssemblyResolve handler in the package's static constructor finds devenv.exe's directory via Process.GetCurrentProcess().MainModule.FileName, then looks for the DLL at CommonExtensions\Microsoft\Terminal\.
  • ITerminalConnection.Resize may be called from a non-UI thread by the native control. The session start (which needs GetWorkspaceFolder() on the UI thread) is marshaled via Dispatcher.BeginInvoke.
image image

Use VS's built-in terminal rendering control (same as Windows Terminal).
Eliminates Chromium dependency, ~80MB memory reduction, instant startup,
native DirectWrite rendering, VS theme integration.

- Implement ITerminalConnection in TerminalToolWindowControl
- Add TerminalThemer for VS dark/light theme colors
- Remove WebView2, xterm.js, addon-fit, addon-webgl
- Reference VS-deployed Microsoft.Terminal.Wpf.dll (Private=false)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sailro
Copy link
Copy Markdown
Owner Author

sailro commented Apr 12, 2026

Hey @bommerts, thank you again for the initial prototype. I tried to move to the native terminal support that we have in VS, and it worked. I need more testing of course, but it could be great.

- Replace WebView2+xterm.js terminal with VS's built-in Microsoft.Terminal.Wpf
- Use VsInstallRoot HintPath for CI-compatible assembly resolution
- Add AppDomain.AssemblyResolve handler for runtime DLL loading
- Add TerminalThemer for VS dark/light theme integration
- Remove WebView2 NuGet, xterm.js resources, and related code
- Update docs, changelog, and squad files for new terminal stack

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sailro and others added 2 commits April 13, 2026 09:48
Integrate with VS's Unified Settings API to expose terminal font
family and size configuration under Settings > Copilot CLI IDE Bridge.

- Add IExternalSettingsProvider implementation (TerminalSettingsProvider)
  that dynamically enumerates installed monospace fonts via GDI+
- Add registration.json with external settings callback pattern
- Add TerminalSettings helper to read font preferences from store
- Wire font settings into TerminalToolWindowControl.SetTheme()
- Proffer TerminalSettingsProvider service from the package

The font family dropdown uses allowsFreeformInput so users can also
type arbitrary font names. GetEnumChoicesAsync uses Task.Yield() as
VS requires truly async completion for external enum providers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
VS 18 Canary moved Microsoft.Terminal.Wpf.dll from
CommonExtensions\Microsoft\Terminal\ to
CommonExtensions\Microsoft\Terminal\Terminal.Wpf\.

Update the csproj HintPath to probe both locations at build time
and the AssemblyResolve handler to check both paths at runtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sailro sailro force-pushed the native-terminal branch 6 times, most recently from 7b5f169 to 2cf43d4 Compare April 13, 2026 09:14
When RestartSession is called (solution open), the native TerminalControl
still has old buffer state and doesn't re-fire Resize since its own size
hasn't changed. This causes garbled rendering until a manual resize.

Fix: On SessionRestarted, send VT RIS (Reset to Initial State) to clear
the control's buffer, then re-sync ConPTY dimensions with the control's
last known size.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sailro and others added 2 commits April 13, 2026 11:53
TerminalControl doesn't re-fire Resize when only the underlying process
changes (WPF size unchanged). Read Rows/Columns from the control's
already-computed character grid and forward to the session service.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove verbose per-resize, per-settings-call, and routine lifecycle
logging. Keep only error logs and important session start/stop/restart
messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bommerts
Copy link
Copy Markdown
Contributor

Pulled the branch, built clean, all 284 tests pass. This is a huge improvement over the WebView2 approach. Some observations from reading through the code:

What's great:

  • The ITerminalConnection interface is really elegant. The control IS the connection instead of needing a separate JS bridge layer. Much cleaner.
  • e.Handled = true in OnGotFocus (line 116) is exactly the fix for the infinite focus loop we fought with WebView2. Native WPF focus just works.
  • No more deferred/lazy WebView2 init, no Chromium process, no cache directory. The constructor is 18 lines instead of 300+.
  • OnSessionRestarted re-syncing ConPTY dimensions is a nice touch that we missed in the WebView2 version.
  • Keeping OnUnloaded as a no-op (session survives hide/show) is the right call.

Minor observations:

  1. ITerminalConnection.Resize() can be called from a non-UI thread by the native control. The StartSession path correctly marshals via Dispatcher.BeginInvoke, but the else branch (line 89) calls _sessionService?.Resize() directly on that thread. This works because TerminalProcess.Resize() uses a lock internally, but it might be worth a comment noting the thread safety assumption.
  2. In the AssemblyResolve handler, if neither path exists it silently returns null. Might be helpful to log a warning so users on unusual VS installs get a diagnostic hint instead of a confusing load failure later.

I'm going to do hands-on testing in VS 2026 Enterprise and will report back with results.

@bommerts
Copy link
Copy Markdown
Contributor

Tested the native-terminal branch in VS 2026 Enterprise. Everything works: terminal renders, typing is responsive, process restart on Enter works, resize works. Looks identical to the WebView2 version from a user perspective, which is the point. Nice work!

One minor UX thing I noticed (pre-existing, not from this PR): clicking Tools > Copilot CLI Window doesn't auto-focus the terminal. I'll open a separate issue for that after this merges.

sailro and others added 3 commits April 13, 2026 14:07
- TerminalThemer: rename DarkTheme/LightTheme to _darkTheme/_lightTheme
- TerminalThemer: use collection expressions for ColorTable arrays
- TerminalSettingsProvider: use collection expression for empty array
- TerminalToolWindowControl: use object initializer for TerminalControl

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- copilot-instructions.md: Add Unified Settings subsystem section,
  document TerminalSettings/TerminalSettingsProvider/registration.json,
  fix solution close lifecycle (RestartSession not StopSession)
- CHANGELOG.md: Add missing [Unreleased] entries (font settings,
  Terminal.Wpf path resolution, ConPTY dimension sync)
- README.md: Fix menu text to match .vsct (Launch/Show verbs)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sailro and others added 3 commits April 13, 2026 14:48
VS maps Escape to 'deactivate tool window', causing focus to jump to
the editor when pressing Escape in the terminal. Fix by intercepting
WM_KEYDOWN(VK_ESCAPE) in PreProcessMessage and forwarding the Kitty
keyboard protocol escape sequence to ConPTY, matching VS's own
TerminalWindowBase.EscKeyCode pattern exactly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add comment on Resize() thread safety assumption (lock in TerminalProcess)
- Log warning when Microsoft.Terminal.Wpf.dll not found in either path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Focus the TerminalControl after frame.Show() by yielding to a
background thread first, then switching back to the UI thread.
This lets VS finish its frame activation sequence before we set
focus on the native terminal HWND. Without the yield, VS steals
focus back during its own activation handling.

Pattern taken from VS's own TerminalWindowBase.OnActiveFrameChanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sailro
Copy link
Copy Markdown
Owner Author

sailro commented Apr 13, 2026

We should be good now, I addressed observations

@sailro sailro merged commit 22a0103 into main Apr 13, 2026
1 check passed
@sailro sailro deleted the native-terminal branch April 13, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants