umux is a free, open-source terminal workspace manager for Linux, Windows and macOS. One workspace per project, tabs inside it, and every tab splits into as many panels as you need. The feature the app is built around: umux reads the OSC 9;9, 99 and 777 escape sequences that AI CLIs like Claude Code emit when a long task finishes, and turns them into a desktop notification.
Landing: https://umux.pages.dev
GitHub: https://github.com/CrystalPlatforms/umux
This covers four releases in five days. v1.5.1 gave workspaces colors and taught the cmux import to carry them over. v1.5.2 was the Windows smoothness pass. v1.5.3 fixed five things on macOS. And v1.5.4 killed the bug this release is named after: a terminal that worked in every dev build and died in every packaged one.
v1.5.2: what it takes to stop a terminal from stuttering
The Windows build passed every test and felt bad anyway. Output stuttered while a command streamed. Empty console windows flashed on hover. Four causes, four fixes.
PTY bytes crossed to the WebView as a JSON array of numbers, so one byte became up to four characters of JSON. They travel as base64 now, four times smaller, one decode on arrival. OS work ran on the UI thread: process lookups, working directories, listening ports, git branches, store saves. All of it is asynchronous now, and the PTY mutex is held only long enough to read a pid. The foreground-process lookup spawned tasklist.exe per check; it is one call to OpenProcess plus QueryFullProcessImageNameW now. And every shell-out passes CREATE_NO_WINDOW now: netstat, the PowerShell CIM queries, the toast notifier. The phantom console windows are gone.
The terminal also renders through xterm.js's WebGL addon since this release, with a fallback to the DOM renderer if the GPU context dies.
v1.5.3: five fixes from one macOS report
One report turned into five fixes. The WebGL addon renders a black canvas on Apple WebKit and WebKitGTK: the context creates fine and presents nothing. It loads on Chromium-based webviews only now, with the DOM renderer everywhere else. A GUI-launched app has no $SHELL, so panels ran /bin/sh and loaded none of your config; the shell resolves through getpwuid now. The bare monospace font renders Courier in WKWebView; the stack is ui-monospace/Menlo/Consolas/DejaVu. A dead working directory fell back to the app's own cwd instead of your home. And a relative "." from lsof no longer gets stored as a panel directory.
v1.5.4: works in dev, black in production
Then the report this release is named after. Third-party CLIs like Mistral Vibe and OpenCode start inside umux, and the panel goes black. Claude Code runs fine. Typing feels dead. Closing the tab is the only way out.
The first suspect was ours and wrong. The OSC parser really did eat Kitty notification queries (ESC]99;…p=?) and really did mistake ConEmu progress reports (ESC]9;4;…) for task completions. Both fixed. The phantom "4;0;" notifications from Claude Code panels stopped. But byte captures showed the TUI stream passing through the backend intact, so the loss lived between the IPC bridge and the pixels.
Instrumentation came next: an opt-in, flag-file-gated dump that logs byte counts per chunk and DOM character counts per panel. Numbers only, never terminal content. It showed something that made no sense: the bytes arrived, the DOM held the TUI's text, and the screen stayed black. That pushed the hunt toward compositing theories. All of them were wrong.
What cracked it was a harness. The real production bundle, served the way the packaged app serves it, a mocked IPC layer, and a recorded byte stream replayed into real WebKit. The black screen reproduced in minutes, and the console held the answer:
ReferenceError: i is not defined
at requestMode
xterm.js 6.0.0 ships pre-minified. Inside its requestMode handler, a closure captures a parameter, and an enum initializes through a polyfill pattern. Vite's build step re-minifies that code a second time, the names stop existing, and the first mode query a modern TUI sends at startup throws. That query is ESC[?2026$p, the synchronized-output probe. The exception kills xterm's write worker. Nothing renders after it. Nothing you type changes what you see.
Upstream issue xtermjs/xterm.js#5800 describes this bug word for word, down to the console line. Another terminal project hit the same crash. The fix ships with umux as a patch-package patch: the enum polyfill becomes a plain object, and the flag moves into a local constant before any closure can capture it. npm install applies the patch, and a contract test locks the synchronized-output paint behavior in place.
The same release paints DEC 2026 frames the moment their block closes. xterm otherwise debounces the paint into a window the next frame's sync block can invalidate, which freezes a fast full-screen TUI to about one frame per second. And the diagnostics mode stays in the build, flag-gated, for whoever hunts the next ghost.
The numbers
Four releases. v1.5.4 lands as +779/−28 across eight files plus the xterm patch. The frontend suite sits at 616 passing tests, the Rust suite at 97, plus a WebKit harness that runs the real bundle against a recorded stream. That check would have caught this bug before any user did.
The honest list
Import from cmux still does not work on Windows. X11 sessions are untested; Wayland is the reference platform. Agent status is still the roughest feature in the app. The umux Terminal TUI, with the herdr importer, is the next big rock. Planned for v1.7.
The installers: .deb, .AppImage and .rpm on Linux, a universal .dmg on macOS 11+, an NSIS installer on Windows 10+. MIT licensed, no accounts, and one anonymous app-open event is the entire telemetry.
Landing: https://umux.pages.dev
GitHub: https://github.com/CrystalPlatforms/umux
If a terminal inside umux ever went black on you: v1.5.4 is the one to install.
Top comments (0)