Skip to content

TUI screen goes completely black when agent starts working (render loop silently stalls) #37803

Description

@AH64-dll

Description

After pressing Enter to send a prompt to the agent, the entire TUI goes black while the agent is working. The process is still alive (keyboard input works, modals render correctly on top of the black screen). Switching to another terminal tab and back restores the display. This happens consistently (every session, every prompt), not intermittently.

OpenCode version: 1.18.3
Platform: Arch Linux
Terminal: (add your terminal info)

Steps to reproduce

  1. Run opencode in any project
  2. Enter any prompt and press Enter
  3. As the agent starts working, the screen goes black
  4. Switch to another terminal tab and back — display recovers
  5. Prompt again → screen goes black again

Root cause

The bug is in the bundled @opentui/core CliRenderer render loop. A race condition in the frame scheduling mechanism causes the render loop to silently stop producing frames during bursts of requestRender() calls.

The race condition in detail

The renderer operates in idle mode (_isRunning = false), where frames are only produced on demand via requestRender(). The flow is:

  1. User presses Enter → burst of events → multiple requestRender() calls
  2. First requestRender() passes the guards and calls activateFrame()loop()
  3. loop() sets this.rendering = true
  4. During loop(), subsequent requestRender() calls hit the this.rendering guard (line 1516) and only set this.immediateRerenderRequested = true
  5. loop() finishes, reads immediateRerenderRequested (line 4478), schedules another loop via setTimeout, and clears the flag (line 4481)
  6. RACE: Between line 4481 (flag cleared) and line 4516 (this.rendering = false in finally), another requestRender() fires — this.rendering is still true → sets immediateRerenderRequested = true again
  7. The finally block (lines 4515–4521) sets this.rendering = false and calls resolveIdleIfNeeded()but does NOT check immediateRerenderRequested
  8. Control returns to activateFrame(), which clears updateScheduled = false in its finally
  9. The scheduled setTimeout from step 5 fires, calls loop() — if no new requestRender() arrives in time, _isRunning is false and immediateRerenderRequested is false, so the else branch (line 4486–4489) clears the timeout and stops scheduling
  10. Render loop is dead. No more frames are produced. The screen stays black.

Why switching tabs fixes it

Switching terminal tabs sends SIGWINCH, which triggers handleResize()requestRender(), which wakes up the dead loop.

Code references

All line numbers refer to @opentui/core packages/core/src/renderer.ts v0.4.5:

  • requestRender() (lines 1491–1534): early-return at line 1516 when this.rendering is true, only sets this.immediateRerenderRequested = true
  • loop() scheduling decision (lines 4478–4489): reads immediateRerenderRequested at line 4478, clears it at line 4481 — but a request arriving between 4481 and the finally is lost
  • loop() finally block (lines 4515–4521): sets this.rendering = false but does NOT re-check immediateRerenderRequested
  • activateFrame() (lines 1536–1548): clears updateScheduled = false in its finally — another window where requestRender() is silently dropped (guarded by !this.updateScheduled at line 1521)

Proposed fix

Based on upstream PR anomalyco/opentui#1086 (which was closed without merging — re-submission needed):

In loop()'s finally block, re-check immediateRerenderRequested and re-issue via setImmediate:

} finally {
  this.rendering = false
  if (this._destroyPending) {
    this.finalizeDestroy()
  } else if (this.immediateRerenderRequested) {
    this.immediateRerenderRequested = false
    setImmediate(() => {
      if (!this._isDestroyed) this.requestRender()
    })
  }
  this.resolveIdleIfNeeded()
}

setImmediate (not process.nextTick) is needed because activateFrame() keeps updateScheduled = true until its own finally after loop() returns. process.nextTick fires before microtasks resume — before activateFrame's finally — so requestRender() would be blocked by the !updateScheduled guard at line 1521.

Upstream issue

This is a re-report of the upstream opentui issue:

The fix needs to be applied in @opentui/core, released, and then pulled into the opencode build. Currently opencode v1.18.3 bundles @opentui/core v0.4.5 which has the bug.

Workarounds

  • opencode spawn — runs backend in a separate process from TUI, may reduce the event burst pressure on the render loop
  • Ctrl+P → "Toggle console" — triggers a re-render via the console overlay toggle
  • Resize the terminal window — sends SIGWINCH, wakes the dead loop

Additional note

There is a related opentui issue anomalyco/opentui#1147 ("TextNodeRenderable.requestRender() silently drops renders when transiently detached") with an open fix PR anomalyco/opentui#1173. This could be a compounding factor, causing text content to freeze even when the render loop is technically alive. However, the primary symptom (entire screen black, not just text) matches the render loop stall described here.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions