43 posts

Engineering blog

How tuios and the tools around it were built, measured and fixed. Every number comes from a run you can repeat from the repository.

Subscribe:RSSAtom

  1. 6 min readPerformance, Web

    Most of the page was four fonts

    A first visit to a sip page moved 11.7 MB. Most of it was four TTF files that nobody had compressed. WOFF2 and gzip took it to 4.4 MB, and dropping a fallback that no browser could reach took the binary from 30.1 MB to 24.6 MB.

  2. 7 min readWeb

    The shell exited, and Firefox opened a new one

    Over WebTransport, a sip session that the program ended came back as a fresh shell in 4 runs of 6. The server's last frame was a STOP_SENDING that one of my own September fixes had added, and Firefox threw away the close message it had already received.

  3. 9 min readTesting, Sessions

    The daemon sent a client an older state after a newer one

    A flaky tuios test turned out to be the daemon delivering session state out of order. The first fix ordered by the state Version, and the review found the one kind of change that never moves the Version. The second fix counts every change.

  4. 9 min readInput

    The Option key meant two things, and tuios only knew one

    On macOS, tuios read a composed • as opt+8 and switched workspaces, and on French AZERTY Option and the 1 key moved a window to workspace 7. Both came from tables that assumed a US keyboard. The fix took four commits, and two of them undid choices the first one made.

  5. 8 min readRendering, Sessions

    A reattach lost the state that no cell shows

    One CI failure, "scrollback differs on 2 of 396 lines, first at column 37", was a pane snapshot that forgot a wrap was pending. Its review found five more pieces of terminal state a snapshot dropped, and the fix for one of them broke the character sets of a program that had already quit.

  6. 5 min readAgents, Sessions

    Two reads of one transcript took the daemon down

    A tuios daemon on Windows died twice in one afternoon, with every pane in it. The trace ended inside encoding/json. Two goroutines were reading one agent transcript through one buffer, because stopping a timer does not stop a callback that has already started.

  7. 7 min readPerformance, Agents

    A wait that made the pane 197 times slower

    An agent that waits for output with tuios wait-for made a flooding pane three times slower. I fixed how often the wait looked. On the libghostty-vt backend the same test still said 197 times, because each look read the history one cell at a time.

  8. 7 min readRendering, Testing

    One fuzz failure was the test, one was the emulator

    Two fuzz targets failed in tuios on the same evening. The kitty graphics target had an oracle that an allocator change left behind, and my first fix made it too lenient. FuzzModel found a real bug, a cut-off UTF-8 rune that ate the escape after it.

  9. 8 min readInput

    Why your link click did nothing

    The hover label in tuios said shift+click to open a link, and in most terminals shift+click never reached tuios. The fix moved the click to ctrl, sent every link to the outer terminal as OSC 8, and then got the Mac wrong once before it got it right.

  10. 7 min readAgents

    460 characters that draw nothing

    tuios fences the text other programs write before an agent reads it. The fence removed 22 invisible characters. Tag characters, variation selectors and the line separator went through. What it removes now, and the one joiner it keeps.

  11. 8 min readAgents

    Crush reports to herdr, so tuios answers as herdr

    Crush tells herdr when it works, waits and finishes, and Collie drives herdr through its socket. Neither knows tuios. How every tuios pane came to answer herdr's protocol, and what that cost.

  12. 7 min readTesting

    Three flaky tests, and what they were hiding

    Nine tests failed now and then on CI in the last days of September. Two were product bugs, and the work turned up a third. A hook that waited on itself, a broken pipe that should have been an error, and a command line the shell echoed twice.

  13. 4 min readAgents, Sessions

    tuios v0.8.0

    The first release in six months. tuios now knows what the coding agents in your panes are doing, keeps your sessions in a daemon that brings them back, and runs panes on other machines.

  14. 8 min readTesting

    A window named db

    An end-to-end test lost an agent's state about twice in a hundred runs. It was not a race. The pane was named db, db is hex, and -w tried a window id prefix before an exact name.

  15. 8 min readPerformance

    Making the binary smaller, and the idea that made it bigger

    Seven commits took the tuios release binary from 30.0 MB to 23.8 MB. A plan to leave screen saver data out of the binary made it 23.5 KB larger instead.

  16. 7 min readSessions

    TERM=dumb followed the session

    A session created by a command with no terminal of its own gave every pane TERM=dumb for its whole life. A screenshot test found it by failing on CI with the same numbers every time.

  17. 8 min readTesting

    Tests that could not fail

    A resize fuzzer whose only screen check sat behind a constant false, a palette test that measured the length of a fixed-size array, and a pinned repro that lost one byte to encoding/json. All three were green, and none of them could go red.

  18. 9 min readTesting

    Deleting two thirds of the unit tests, then putting a third back

    In one day I cut the tuios unit tests from 4,239 to 1,470 under a strict rule, then restored 1,395 of them. The rule asked what kind of test it was. The review asked whether a bug gets past the E2E suite without it.

  19. 6 min readRendering

    The background vanished under a tab

    Under tmux, a few cells beside the Inbox title showed the terminal's own background instead of the panel's. The bytes tuios sent were right. bubbletea moved the cursor with a hard tab, and tmux 3.6 and later keep that tab as one cell that a later write breaks.

  20. 12 min readPerformance

    A full scrollback cost 232MB per pane, twice: once in the daemon and once in each client

    A 112-byte cell made every scrollback line cost the full pane width. Packing cells, then storing lines as text, took a full ring from 232MB to 2.2MB.

  21. 16 min readAgents

    Agents drive tuios through one-shot commands, so the event bus could not reach them

    A hub only reaches whoever is subscribed right now, and agents almost never are. How tuios got a mailbox, ask-agent and an inbox for the person.

  22. 15 min readSessions

    The pane ran on another machine, and ctrl+D waited for the next keypress

    tuios daemons now talk to each other over one ssh link per machine. It shipped in stages, and each stage had a bug that taught me something.

  23. 17 min readSessions

    A PTY has one size, and two clients had two opinions about it

    Two clients on one session each laid the panes out around their own chrome and fought over the shared shells. The fix, and the harness that found the rest.

  24. 8 min readSessions

    A session switch forgot my column widths

    Switching sessions and back reset a widened column in the scrolling layout and split stacked panes apart, because the session state saved the strip's offset and not its columns. The same gap had two clients on one session drawing different widths.

  25. 11 min readPerformance

    Most of a scroll was spent on blank cells

    A ten-character line scrolling off a 207-column screen walked the whole row twice. One int per row cut the short-line scroll from 1,120 ns to 240 ns, measured on a machine that would not sit still.

  26. 10 min readTesting

    Nothing failed, so nothing was fixed

    A whole-codebase audit of tuios found a flag nothing set, an interface with one implementation behind 140 call sites, and copies of code that had quietly drifted apart. Dead code survives because nothing fails when it is wrong.

  27. 13 min readSessions

    Session state was saved every 30 seconds and never read back

    tuios wrote every session to disk from April and no daemon ever loaded it. Wiring up the read side, and then making reattached panes redraw correctly.

  28. 13 min readAgents

    The one moment an agent needed you was the one moment tuios could not see

    A coding agent blocked on a permission prompt paints it once and goes silent. How tuios learned to tell that silence apart from a finished turn.

  29. 9 min readRendering

    The underline depended on which code drew it

    A focused tuios pane dropped SGR underline, and some unfocused panes dropped almost every attribute. tuios had five ways to draw a pane and only one kept everything, so the text changed when focus moved.

  30. 3 min readRendering

    The screenshot cell was half measured and half guessed

    Pane screenshots came out stretched. Two rounds of font maths got the cell shape wrong. The fix asks the host terminal for its real cell size in pixels.

  31. 3 min readRendering, Performance

    The renderer was drawing a backlog it had caused

    A flooded tuios pane kept painting 1.2 s after its source exited. The client's own renderer caused the backlog, and pacing by queued bytes halved the tail.

  32. 7 min readTesting

    The fuzzer that found nothing, and the two questions that found everything

    590,000 fuzz runs against the tuios terminal emulator found nothing, because they checked that the screen was well formed, not that it was right. Two properties that compare the emulator with itself found two real bugs.

  33. 5 min readRendering, Performance

    A fifth of every frame went to changing nothing

    A profile put 19.8% of the tuios client in lipgloss.Wrap, rewrapping pane text already the right width. Skipping it was easy. Proving the skip safe was the work.

  34. 6 min readRendering, Testing

    My differential tests passed and the screen was pink

    A style cache keyed by recycled libghostty-vt style IDs painted ls output hot pink, and a differential test suite that compared once, at the end, missed it.

  35. 8 min readTesting

    The daemon said yes to an option that did not exist

    Driving the tuios control socket the way a program would found a command that lost its spaces, a parameter that was silently dropped, and 88 settings of which six applied live.

  36. 4 min readRendering, Web

    My multiplexer lied about what it could do

    kitty icat drew nothing when tuios ran in a browser client, because the graphics passthrough said yes to file transmission without asking the host.

  37. 8 min readTesting

    My fuzzer was optimising for my own bug

    A flaky TUI fuzz suite came down to a PTY line discipline I never configured, and a shrinker that minimised straight toward the race.

  38. 9 min readPerformance

    The benchmark said 27x faster. It felt worse.

    Fixing pane resize lag in TUIOS with Go benchmarks and pprof. Two of four theories were wrong, and the benchmark measured the wrong thing for three rounds.

  39. 6 min readInput

    Three terminal bugs in one month turned out to be one bug

    Terminals answer queries like OSC 11 and CPR. Three bugs in three codebases were one bug: the reply was dropped, sent to the wrong place, or echoed back.

  40. 8 min readLayout

    I dragged one divider and five windows moved

    Dragging one divider in the tuios tiling layout moved five windows. It was three defects: neighbours found by geometry, a restarting ease, a truncated ratio.

  41. 7 min readRendering, Web

    The bug I closed three times: a speckled sprite in a WebGL terminal

    A speckled sprite in the tuios browser terminal. Three wrong closures, a test suite blind by construction, and a browser that salted its own pixel readback.

  42. 5 min readRendering, Web

    I ported a correct emoji width table and it fixed nothing

    Emoji widths were wrong in the tuios browser client. I ported ghostty's per-codepoint width table and nothing changed, because a terminal gives width to grapheme clusters, not to codepoints.

  43. 6 min readRendering

    I shipped three fixes for a bug I had not found

    A tuios pane went blank on focus changes. I shipped three real fixes for other bugs before I reproduced it, and the cause was a window width read from its first line.