Devolutions Terminal can run as a static Avalonia WebAssembly app. This is a minimal, usable port of the terminal surface — not a full desktop clone.
- Avalonia 12 Fluent UI in the browser (
ISingleViewApplicationLifetime) TermControl+ Skia renderer + the built-in VT engine- An in-process
dt-wasmshell (BrowserShellConnection) withhelp,echo,ls,cat,clear,date,uname,color,history, andexit - A loopback host bridge that renders a real ConPTY / Unix PTY shell in
TermControlwhen the page is served byDevolutions.Terminal.Browser.Host - A
/dtcontrol channel that lists settings profiles and opens each launch as its own tab with a dedicated WebSocket transport - Headless TermControl tests plus an optional Playwright pass against the
published
wwwroot
| Desktop capability | Why it is out of scope |
|---|---|
ConPTY / forkpty inside the page |
No process or PTY APIs in the WASM sandbox. A loopback host process can own the PTY and bridge it over WebSocket. |
Ghostty (libghostty-vt) |
Native library, not compiled to WASM |
dt CLI, broker, multi-window |
Separate OS processes and named endpoints |
| Package identity, toasts, jump lists, global hotkeys | Windows / desktop shell |
| Native file dialogs, unrestricted filesystem | Browser sandbox (MEMFS only) |
| Azure Cloud Shell | Possible later (HTTP + WebSocket), not in this host |
Clipboard, IME, and accessibility are whatever the browser + Avalonia.Browser backend. Performance and download size are worse than NativeAOT desktop.
Requires the wasm-tools workload matching global.json:
dotnet workload install wasm-tools
dotnet run --project src/Devolutions.Terminal.BrowserPublish a static site:
dotnet publish src/Devolutions.Terminal.Browser -c Release -o artifacts/browser-wasmServe artifacts/browser-wasm/wwwroot (or the SDK publish/wwwroot) with any
static file server. MIME type application/wasm is required for .wasm.
The host disables WASM asset fingerprinting so wwwroot/main.js can import
_framework/dotnet.js by a stable name. A plain static server has no
/pty/health bridge, so the page stays on dt-wasm.
Devolutions.Terminal.Browser.Host binds http://127.0.0.1 only, serves the
published site, and exposes the dt settings catalog over a WebSocket control
channel at /dt. Launching a profile opens a tab whose bytes travel on their
own /pty/{sessionId} socket. The transport scheme follows the page: wss
when the page is HTTPS, ws on this HTTP loopback host. The browser sends a
profile id, never a command line. The host does not send command lines or
starting directories back to the page. Profiles with elevate are listed and
cannot be launched. Open http://127.0.0.1:<port>/ — not a LAN
address.
dotnet run --project src/Devolutions.Terminal.Browser.Host -- --wwwroot artifacts/browser-wasm/wwwrootOn Linux and macOS, building the loopback host also builds and copies the
dt-pty-host helper needed to launch local shells.
This is a local development bridge, not remote access. GET /pty/health
returns X-Dterm-Host: pty and X-Dterm-Control: /dt. Azure Cloud Shell
profiles are listed and cannot be launched here. A page with no control
channel stays on the single dt-wasm or legacy /pty shell. On Windows the
host spaces ConPTY input writes: a raw/no-echo reader such as sudo drops a
burst, so a quickly typed password would otherwise be truncated. A paste
remains one write. Modifier key-downs are not forwarded: ConPTY would write
them as NUL and reject a shifted password. Ctrl+C is sent as ETX so the
prompt can be cancelled.
Headless engine/control coverage always runs:
dotnet test tests/Devolutions.Terminal.Connection.Tests --filter BrowserShell
dotnet test tests/Devolutions.Terminal.Control.Tests --filter TermControlBrowserShellRawModeBurstReachesWslReader covers the ConPTY input pace. It skips when no
WSL distribution is installed.
End-to-end against Chromium (installs wasm-tools browsers on demand):
scripts/Test-BrowserWasm.ps1Set DTERM_BROWSER_E2E=1 to opt the Playwright test in. Leave it unset so
dotnet test Devolutions.Terminal.slnx stays free of wasm-tools.