I spent the last few weeks building wimux,
a terminal multiplexer for Windows written in Rust. tmux
and zellij
are
Unix-first — on Windows they only really live inside WSL, detached from the
native shell. I wanted persistent PowerShell sessions, so I wrote them.
Most of the code was written by AI agents. My commits say so explicitly
(Co-Authored-By:
trailers), and I'd rather lead with that than have someone
discover it. What I want to write about isn't the code the agents produced —
it's the three bugs they couldn't fix, because those turned out to be the
interesting part.
A quick sketch of the thing, so the bugs make sense: a detached per-user daemon
owns the sessions, drives child processes through ConPTY (portable-pty
),
and emulates the terminals server-side. Clients — a TUI, a Tauri v2 GUI running
xterm.js, and a scriptable CLI — attach over a Windows named pipe with a
postcard
binary protocol. So: a Rust daemon, a WebView, and a PTY, three
layers that each have their own idea of what the screen looks like.
Symptom: split a pane while a full-screen TUI app was running, and the
display filled with orphaned characters — box-drawing glyphs stranded from a
narrower render.
Everything pointed at the client. xterm.js is the thing drawing pixels; the
GUI reparents DOM nodes when the layout changes; re-attaching to the session
made the corruption disappear. Obvious client bug.
We tried four fixes, in this order:
Four plausible fixes, four failures. Each one was a reasonable theory, and each
one was aimed at the client.
The move that cracked it took thirty seconds: I captured the terminal grid
from the server, using the daemon's own view of the pane — the one that has
never heard of xterm.js.
wimux agent capture -t 0 -p 2
The orphaned glyphs were there too.
That single output eliminated the entire client. The corruption existed before
a single pixel was drawn. The real cause was upstream of everything we'd been
touching: splitting a pane sends a burst of intermediate sizes to the PTY.
The app inside redraws at each one, its incremental redraws don't clear what
they no longer cover, and the leftovers stay in the grid — in the server's grid.
The fix was three lines: debounce resize events so the PTY only ever sees the
final, settled size.
The lesson isn't "debounce your resizes." It's that four consecutive fixes
failing is data. It means the assumption underneath all four is wrong — and no
amount of new fixes at the same layer will help. Go get an observation from a
different layer instead.
Symptom: click a pane, type — the first keypress doesn't appear, and the
machine beeps. Press again and it works.
This one resisted for days, partly because every reasonable theory was wrong.
Here's what I eliminated, and how:
mousedown
→ term.focus()
. The first
keypress went through, every time. GetForegroundWindow
and GetGUIThreadInfo
while typing. Focus never moved. document.hasFocus()
showed all keystrokes landing on the terminal's
textarea. The blur/focus events I'd blamed were me alt-tabbing. ESC [ I
on focus, PSReadLine chokes on it, beeps, and eats the next character. It explains every symptom. I enabled the mode and watched what xterm actually emitted: nothing. ConPTY had swallowed the sequence. Five theories, five refutations, all backed by an observation rather than an
argument. What finally worked was building a reproduction harness: a script
that clicks a pane and sends a keystroke after a delay, sweeping the delay from
0 ms to 150 ms, dozens of times. A human can't hit a 50 ms window on purpose. A
script does it forty times in a row.
Result: zero losses. Clicking inside a pane was never the problem.
So I pointed the harness at the sidebar instead — clicking a session entry to
switch workspaces. Four out of four keystrokes lost, with a log that made
the mechanism unmistakable:
MD pane=- target=SPAN.name active=TEXTAREA.xterm-helper-textarea
== BLUR == the terminal loses focus
KEY "k" active=BODY target=BODY the keystroke lands on <body>
== FOCUS == focus returns ~310 ms later
The sidebar entries are plain <div>
s. They aren't focusable, so clicking one
drops document focus to <body>
. Then two delays stack: a 200 ms timer that
disambiguates double-click-to-rename, plus a 300–450 ms server round-trip to
rebuild the panes. For half a second, keystrokes go nowhere — and Windows
beeps at a key sent to a non-editable element.
The fix follows from the diagnosis: in a terminal app, a keystroke with no
focused field belongs to the active pane. It gets delivered there, and if a
switch is in flight — the target pane doesn't exist yet — it's buffered and
replayed into the pane you switched to.
Verified the same way it was found: 0/4 keystrokes delivered before, 4/4 after,
each in the correct session's pane.
Symptom: with several panes open, typing lagged badly.
This one was quick, and it's the clearest example of a class of bug agents are
bad at: nothing is wrong anywhere. Every line of code is correct. The daemon
read PTY output and emitted one IPC message per chunk. Each message crossed
into the WebView and hit a single JavaScript thread. With several panes
producing output, that thread spent its life in message dispatch instead of
rendering.
The fix was to drain what's already available and merge consecutive chunks from
the same pane into one message, capped at 64 KiB.
No bug was fixed. An accounting decision was changed. That's a category agents
rarely reach for on their own, because there's no error to find.
Worth a mention, because it cost me an hour of confusion.
I fanned a task out across several agents, each in its own isolated git
worktree. All of them reported success in careful, structured prose. Not one
had written a single file: in non-interactive mode they needed an explicit
permission flag to touch the filesystem, and without it they narrated the work
they would have done.
What saved me was that the review step didn't read their reports — it ran
git status
in each worktree and counted changed files. It said 0
, and the
0
was true.
Trust the artifact, never the report. This is the single most useful habit
I've taken from working this way.
Agents produced a working multiplexer faster than I could have alone. They also
produced four consecutive confident wrong fixes for the first bug, and would
have produced a fifth if I'd asked.
The thing they don't do on their own is stop and go get evidence. They
generate plausible next steps, and plausible is exactly the failure mode you
have to defend against. Every one of these three bugs was solved by the same
move: capture the state at a layer nobody was looking at.
None of that is exotic. It's ordinary debugging discipline — the kind that's
easy to skip when something is handing you a confident answer every thirty
seconds. If you take one thing from this: when three fixes in a row fail, stop fixing. Your shared assumption is the bug.
wimux is MIT-licensed and runs on Windows 10/11:
cargo install wimux wimux-server
Repo, demo GIF and the full architecture write-up:
Happy to answer questions — especially about ConPTY, which was by far the
messiest part.