Skip to content

tui: garbled display / raw ANSI on WSL when native audio backend writes ALSA errors to stderr #52793

Closed as duplicate

Description

@180Gio

Summary

On WSL, when OpenCode's in-process native audio backend fails (e.g. ALSA cannot find a sound card on a stock WSLg setup), the library writes errors to stderr. OpenTUI does not capture stderr, so those bytes go to the same pty as the renderer and interleave with frame writes. Escape sequences get split and the terminal prints raw ANSI / garbled output. The corruption triggers exactly when a turn finishes (the attention "done" sound is played). It reproduces on any terminal and makes the TUI unreadable.

Environment

  • opencode version: v2.0.22
  • OS: Linux 6.6.87.2-microsoft-standard-WSL2 (Ubuntu 24.04.5 LTS), host Windows 11
  • Terminal: Windows Terminal and Alacritty — both affected (also native wsl), so it is not terminal-specific. TERM=xterm-256color, COLORTERM unset.
  • Shell: /bin/bash
  • Install/channel: latest, native binary installed via the curl installer (~/.opencode/bin/opencode, Bun-compiled ELF)
  • Active plugins: @slkiser/opencode-quota@latest — ruled out (plugin was installed recently while the bug is weeks old, and the issue also occurs without it)

Reproduction

  1. In WSL, have ~/.config/opencode/cli.json with "attention": { "sound": true } (playback enabled) and a system where WSLg's Pulse server exists (/mnt/wslg/PulseServer) but the ALSA→Pulse bridge is missing (libasound2-plugins / libpulse0 not installed — the default on a fresh Ubuntu WSL image).
  2. Run opencode and send any prompt; let a turn finish (so the attention "done" sound is played).
  3. The ALSA library fails and prints errors to stderr; the TUI display becomes garbled and raw ANSI escape sequences are printed to the terminal.

Consistent (every turn), independent of the terminal and of content length.

Expected Behavior

An audio playback failure should not affect the TUI. The renderer output should stay intact, and native-library errors should not be written directly over the UI.

Actual Behavior

At the moment the turn ends, a burst of ALSA errors is written to stderr and lands on the same pty as the renderer. Escape sequences are split and the terminal shows raw ANSI / corrupted output.

Additional Context

I captured the full raw pty stream with script -q -c "opencode" out.raw (~267 KB) and analyzed it:

  • All CSI/OSC/DCS sequences are well-formed: 0 literal SGR/CSI sequences (i.e. no escape printed as text) and 0 unterminated OSC/DCS.
  • The only foreign bytes in the stream are 35 ALSA error lines in a single burst, immediately after the final \x1b[?2026l (end of the synchronized-output block) of the first completed turn, at byte offset ~136155.
  • Renderer frames use mode 2026 (synchronized output) heavily; observed sync blocks up to ~22 KB.

Excerpt around the burst:

...\x1b[0m\x1b[65;6H\x1b[?25h\x1b[?2026l
ALSA lib confmisc.c:855:(parse_card) cannot find card '0'
ALSA lib conf.c:5208:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib pcm.c:2721:(snd_pcm_open_noupdate) Unknown PCM default
ALSA lib confmisc.c:855:(parse_card) cannot find card '0'
... (35 lines total)

Hypothesized mechanism: the audio backend runs in-process and writes to stderr; per OpenTUI docs "stderr remains separate" (not captured), so it shares the controlling terminal. Frame writes can exceed PIPE_BUF and pty writes larger than that are not guaranteed to be atomic, so a stderr write can land in the middle of an escape sequence and split it — the terminal then renders the tail as literal text. This also matches the OpenTUI troubleshooting notes "Logs overwrite or corrupt the UI" and "Main-screen or split-footer rows remain damaged".

The binary references both libasound.so.2 and libpulse.so.0; on this system only libasound2 is installed, so playback goes through ALSA and fails ([ALSA] ... strings are embedded in the binary).

Workarounds that fully resolve the corruption:

  • Install libasound2-plugins + libpulse0 and route ALSA to WSLg Pulse (~/.asoundrc with pcm.!default { type pulse }), so audio works and no errors are emitted; or
  • Set "attention": { "sound": false } in ~/.config/opencode/cli.json.

Suggestions: capture/redirect stderr coming from native backends so it cannot write directly over the renderer, and/or suppress native library logging on the UI path.

Related: #12664, #2821, #15388, #16652, #17793.

Activity

  1. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    • #41763: ALSA initialization errors from the audio backend are written over and corrupt the TUI; its discussion also identifies turn-completion sounds as a trigger.
    • #51841: Enabling turn sounds on an audio-less device prints ALSA errors into the terminal and corrupts the TUI.
  2. AlexeyShirshov commented on Oct 14, 2026

    @AlexeyShirshov

    Confirming from another WSL2 host (v2.0.28) — same trigger, plus a repro of the noise source without OpenCode

    This matches my host exactly (WSL2 + attention sound + no audio device), and it is the closest match to what I hit, so I am adding my evidence here. My original report (#55302) is being closed as a duplicate of #41763 / this issue.

    Environment: opencode v2.0.28 (latest channel, ~/.opencode/bin/opencode), Ubuntu 22.04 on WSL2 (6.18.33.2-microsoft-standard-WSL2), Windows Terminal; TERM=xterm-256color, TERM_PROGRAM and COLORTERM unset.

    Differences from the report above:

    • The trigger does not require the turn to finish: a permission request is enough. Anything that plays an attention sound does it.
    • The server side stays clean in my case: ~/.local/share/opencode/log/opencode.log has no alsa|audio|sound lines, and opencode service status (healthy at http://127.0.0.1:49374) plus GET /api/info are fine — consistent with the write happening in the TUI client's process, not the service.

    The burst is alsa-lib's, reproducible without OpenCode:

    python3 -c "import ctypes; a=ctypes.CDLL('libasound.so.2'); h=ctypes.c_void_p(); print('rc=', a.snd_pcm_open(ctypes.byref(h), b'default', 0, 0))"
    # rc= -2, with the same 8 "ALSA lib confmisc.c ... Unknown PCM default" lines

    That confirms the bytes are emitted at the library level to stderr, not produced by the TUI renderer.

    Audio device state:

    ls /dev/snd                    # only "timer", no pcm*
    cat /proc/asound/cards         # No such file or directory
    ls /run/user/$(id -u)/pulse    # empty
    command -v paplay pactl aplay ffplay   # none installed

    Workarounds: attention.sound: false (confirmed working), or opencode 2>/tmp/opencode.err (noise kept off-screen).

    Additional detail on v2.0.28: the client ships a miniaudio-based audio engine (loadSoundFile/play) with bundled mp3 packs mapped per event, and dlopens libasound.so.2 / libpulse.so.0. PulseAudio is attempted before ALSA, so hosts with a working Pulse server do not reach the ALSA failure path — which is probably why this is easy to miss.

    Related: #41763 (tracking issue), #51854 (fix: skip audio init when no playback device exists).

  3. neriousy commented on Oct 20, 2026

    @neriousy
    Member

    Thanks for the detailed report and terminal capture! This is the same attention-sound problem tracked in #41763, where ALSA errors overwrite the TUI, including on WSL. Please subscribe or add a 👍 there, and feel free to add your terminal capture details to that issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions