anomalyco / anomalyco/opencode
[Bug] Desktop 1.18.11 (Linux): sound effects never play, while system notifications work
@Hona is already working on this.
Since Aug 4, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
Summary
On Linux, nothing in Settings → General → Sound effects produces audible output — not the Agent / Permissions / Errors events, and not the preview when picking a sound from the dropdown. System notifications from the section directly above work correctly, so the underlying events do reach the UI.
I could not find the cause, but I ruled out the obvious candidates below. A plugin-based workaround for other Linux users is at the end.
Environment
| Version | 1.18.11 (opencode-1.18.11-1.x86_64) |
| Artifact | opencode-desktop-linux-x86_64.rpm from https://opencode.ai/download/stable/linux-x64-rpm |
| Integrity | sha512 matches latest-linux.yml for v1.18.11 |
| OS | Fedora Linux 43, KDE Plasma, Wayland |
| Audio | PipeWire 1.4.11 with PulseAudio compatibility, libpulse.so.0 present |
| Launch flag | --disable-gpu (see "Unrelated note") |
Steps to reproduce
- Install the Linux RPM and launch the app.
- Settings → General → Sound effects — each of Agent / Permissions / Errors has a sound selected (defaults).
- Open the Agent dropdown and pick a different sound. No preview is audible.
- Trigger the real events: run a command that needs permission approval, and let a session finish. No sound.
Reproduced across several restarts.
What I ruled out
The audio assets are present. app.asar contains 26 bundled .aac files (alert-01-BuOD_o_q.aac, bip-bop-03-DXp7Zb0f.aac, …) plus 19 base64 data:audio/aac URIs inlined in JS chunks. The three defaults ship as chunks with inlined data URIs — staplebops-01-CuX2RZ1A.js, staplebops-02-DGL-LyGN.js, nope-03-OvZJOdvj.js. The base64 payloads start //FQ (0xFF 0xF1), i.e. raw ADTS AAC.
The AAC decoder is compiled into the bundled ffmpeg. /opt/OpenCode/libffmpeg.so exports ff_aac_decoder (alongside ff_mp3_decoder, ff_vorbis_decoder, ff_flac_decoder, ff_h264_decoder). This is not a stripped "clean" ffmpeg.
The playback path exists and is wired to the dropdown. The bundle contains playSound / playSoundById, new Audio(src), five .play() call sites and two canPlayType uses. The settings dropdown calls playSoundById(id).then((cleanup) => …), so the preview is intended to be audible.
Host audio is healthy. Firefox and Spotify play to the same sink throughout. Default sink volume ~91%, no per-application mute stored in WirePlumber for this app. The Chromium audio.mojom.AudioService utility process runs and the app registers as a PipeWire client:
$ pactl list clients short | grep opencode
8017 PipeWire ai.opencode.desktop
No renderer error is logged. The app's renderer console output does reach the systemd journal (unrelated ResizeObserver warnings from the same window show up there). While changing the sound in the dropdown, the journal recorded only those ResizeObserver lines — no NotSupportedError, no EncodingError, no autoplay-policy message, nothing audio-related.
So: assets present, decoder present, playback code present, no error raised, no sound.
One thing I could not determine: whether an HTMLAudioElement is actually created and play() reached at runtime. I have no way to inspect the renderer beyond the journal on this build. If there is a debug flag or a DevTools entry point I missed, I am happy to collect more.
I also monitored pactl subscribe for 150 s while triggering events. One new sink-input appeared, but I could not attribute it: an Electron app and a running Chrome instance both report application.name = "Chromium". So I cannot claim the app opened no playback stream.
Expected
Selecting a sound effect and triggering the corresponding event plays the sound, as it appears to on Windows (#40038, #32389).
Workaround for Linux users
A local plugin reproduces the feature using the freedesktop system sounds most Linux desktops already ship. Save as ~/.config/opencode/plugins/system-sounds.js and restart. This is the code running on the machine described above, with its comments translated from Finnish; the logic is unchanged.
import { appendFileSync } from "node:fs"
import { execFile } from "node:child_process"
const SOUND_DIR = "/usr/share/sounds/freedesktop/stereo"
const ERROR_LOG = "/tmp/opencode-sounds.log"
// Only announce completion if the turn took longer than this. A fast reply
// means you were probably watching, so the sound is just noise. Focus
// detection would be more precise but is awkward on Wayland.
const IDLE_MIN_MS = 30_000
const SOUNDS = {
permission: "message-new-instant.oga", // permission needed - always
idle: "complete.oga", // agent done - only after a long turn
error: "dialog-error.oga", // error - always
}
let turnStart = null
// Errors only. Hundreds of events per minute arrive (message.part.delta per
// token), so writing per event would be a synchronous disk write per token.
const logError = (msg) => {
try {
appendFileSync(ERROR_LOG, `${new Date().toISOString()} ${msg}\n`)
} catch {}
}
const play = (file) => {
execFile("paplay", [`${SOUND_DIR}/${file}`], (err) => {
if (err) logError(`paplay ${file}: ${err.message}`)
})
}
export const SystemSounds = async () => ({
event: async ({ event }) => {
const type = event.type
if (type === "session.idle") {
const elapsed = turnStart === null ? 0 : Date.now() - turnStart
turnStart = null
if (elapsed >= IDLE_MIN_MS) play(SOUNDS.idle)
return
}
// Start the clock only on message events. Background events (file watcher,
// LSP, installs) would otherwise start it long before a turn begins.
if (turnStart === null && type.startsWith("message.")) turnStart = Date.now()
if (type === "permission.asked") play(SOUNDS.permission)
else if (type === "session.error") play(SOUNDS.error)
},
})
Two caveats if you use it: turnStart is global rather than per-session, so a session.idle from another session can reset it; and time spent waiting at a permission prompt counts toward IDLE_MIN_MS.
Unrelated note
I run the app with --disable-gpu, because without it the GPU process fails to launch and the app aborts:
ERROR:gpu_process_host.cc:992] GPU process launch failed: error_code=1002 (x6)
FATAL:gpu_data_manager_impl_private.cc:418] GPU process isn't usable. Goodbye.
That is a separate problem (NVIDIA + KDE Wayland) which I have not filed. Noting it only so the flag is not mistaken for a cause — the sound behaviour was identical before I added it.
Related
- #40038 — desktop plays the success notification sound immediately on sending a message
- #32389 — [Desktop] repeated error alert sounds on startup
Both are Windows reports, which is what pointed me at a platform difference.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.