Repository navigation
Description
Summary
Between v0.1.92 and v0.1.103 bufferDrawBox grew from ~2.4 KB to ~14.2 KB and started panicking under load on macOS arm64 (SIGTRAP/brk 1) and rendering with corrupt colors on Linux WSL2 + Windows Terminal. The most likely culprit is the buffer.zig refactor in #920 (14480ef feat: add support for bottom title and alignment in BoxRenderable), which the PR description states was a deliberate rewrite of drawBox so title and bottomTitle share a draw path.
A downstream consumer report with the original .ips crash dump and a Linux render-corruption confirmation is here: automagik-si/genie#1390
Evidence
_bufferDrawBox size jumped 6× in 0.1.103
From nm -U on libopentui.dylib (darwin-arm64), computing the gap to the next exported symbol:
@opentui/core |
_bufferDrawBox start |
Approx function size |
|---|---|---|
| 0.1.92 | 0x43e58 |
~2,372 bytes |
| 0.1.97 | 0x42274 |
~2,372 bytes |
| 0.1.99 | 0x41f54 |
larger |
| 0.1.102 | 0x44b80 |
larger |
| 0.1.103 | 0x46a90 |
~14,164 bytes |
macOS arm64 crash report
Exception: EXC_BREAKPOINT (SIGTRAP) / brk 1
Faulting frame: libopentui.dylib`_bufferDrawBox + 2672
Image UUID: 8F6BC5C3-5E73-3736-BC94-9F889C9DF632 (0.1.103, darwin-arm64)
OS: macOS 15.6.1, Mac16,1, arm64
Bun: 1.3.12
Terminal: Warp v0.2026.04.15.08.stable_02
Process age: ~18 minutes between launch and crash
Reproducibility: sporadic; survived 91 s + 9× SIGWINCH in our short-window repro
The crash dump JSON is attached on the downstream issue (gist 4c824b03fe87964ff1b6ef2b2f5d8ea5). Key detail: +2672 is outside the entire 0.1.92 function (which is only 2,372 bytes total), so this panic is in code that did not exist before the refactor.
Linux render corruption (no SIGTRAP, same regression)
A second consumer reports on Linux WSL2 + Windows Terminal + tmux + bun, after upgrading to 0.1.103:
- Sidebar selection highlight renders salmon/coral where a
#7c3aedviolet was emitted - Header bar background renders salmon where a
#241838dark navy was emitted - Intermittent flicker, desaturated palette
- Manually replacing 0.1.103 with 0.1.91 in
node_modulesand restarting the renderer fixes it cleanly, with no other config change
Full report: automagik-si/genie#1390 (comment) (linked back here).
New panic-class strings introduced in 0.1.103 (absent in 0.1.92)
comm -13 of the strings tables across the two dylibs:
Invalid buffer dimensions: Failed to create optimized buffer: Failed to send pending queries:
LinkTracker.addCellRef getOrPut failed: GraphemeTracker.add failed: GraphemeTracker.add incref failed:
Two new bookkeeping subsystems (GraphemeTracker, LinkTracker) appear to be invoked from inside bufferDrawBox (or its callees). Any allocation/getOrPut failure in those paths panics across the FFI boundary, which surfaces as EXC_BREAKPOINT on macOS and (presumably) silent state corruption on Linux when the panic is masked.
ABI break shipped as a patch bump
-bufferDrawBox(buffer, x, y, width, height, borderChars, packedOptions, borderColor, backgroundColor, title)
+bufferDrawBox(buffer, x, y, width, height, borderChars, packedOptions, borderColor, backgroundColor, title, bottomTitle)-function packDrawOptions(border2, shouldFill, titleAlignment) { ... }
+function packDrawOptions(border2, shouldFill, titleAlignment, bottomTitleAlignment) { ... }The JS wrapper handles the new parameter, so consumers who call drawBox(options) from JS aren't visibly broken at the API level — but the new code paths inside the native side panic on legitimate inputs that 0.1.92 accepted.
Reproduction (downstream)
We could not deterministically trigger the macOS panic in a 91-second window with periodic SIGWINCH; it requires longer runs and (we suspect) specific state transitions in the parent React reconciler. The downstream genie TUI reproduces it after several minutes of normal use.
If a maintainer wants to bisect, the suspected starting point is the buffer.zig drawBox refactor in #920. The Linux render corruption is the more accessible repro: install genie with @opentui/[email protected] on WSL2 + Windows Terminal and look at the sidebar.
Asks
- Treat 0.1.103 as a regression worth a follow-up patch, not just a doc note. The downstream
^0.1.91semver convention means anyone who installs after2026-04-23T22:15:08Z(when 0.1.103 hit npm) gets it automatically. - Confirm whether feat: add support for bottom title and alignment in BoxRenderable #920 plus the surrounding
GraphemeTracker/LinkTrackerwork is the source. If yes, would it make sense to:- Validate
(x, y, width, height)at the public boundary ofbufferDrawBoxand return an error/no-op instead of@panic/unreachable? Panic across an FFI boundary is hard to recover from for embedders (Bun aborts). - Include the offending
(x, y, w, h, buffer.w, buffer.h)in the panic message so the symbol + stderr can pinpoint the bad call site?
- Validate
- If a 0.1.104 is feasible that gates the new bookkeeping behind a flag or fixes the bounds, that's the cleanest path. Otherwise downstream consumers are pinning to 0.1.91/0.1.92.
Happy to provide additional artifacts, run a custom debug build with extra instrumentation, or test a candidate fix on the macOS arm64 + Warp setup.
Activity
kommander commentedon Apr 29, 2026
@viralcodex could you maybe check that?
viralcodex commentedon Apr 29, 2026
Yeah sure, will look into it later today after work
viralcodex commentedon Apr 30, 2026
Hi @kommander @simonklee , did some finding, and from what I can find, there's a real Grapheme handling issue on composite characters (unicode, CJK, etc), this comes because of adding the title to the right side on clipping.
The path i was able to find goes from
computeBoxTitleLayouttodrawTextwhich allocates grapheme pool id before it knows that it can be stored. InsidesetInternalif grapheme overflows, it takesif (x + width > self.width)branch and returns without freeing the ID allocation. Then ingrapheme.zig, when stale packed grapheme ID is used after slot reuse, it panics with this error in terminal:GraphemeTracker.add incref failed: error.WrongGeneration...In my understanding, this happens with any right title being clipped & not being able to render on the terminal with graphemeID.
I could be wrong, but this is what I was able to uncover.
viralcodex commentedon Apr 30, 2026
ok so I did some more checking so mostly I could see that the issue boils down to GraphemePool where we try to add a stale id but panics due to older generation, this seems to happen randomly as the pool ids are exhausted which seems to corroborate with the OP's random crash timings.
From my side, I can say either we stop throwing and handle it gracefully by removing that Id from the pool and then show a warning. Or we can send an error instead of panicking which cannot be handled by the Typescript which crashes the TUI process
simonklee commentedon Aug 24, 2026
Current native code prevents the reported clipped-grapheme allocation path
before tracker ownership, and relevant native regressions pass.