anomalyco / anomalyco/opencode

TUI input caret strobes during response streaming in WezTerm (per-frame cursor show/hide churn)

Open
#44,211 0 comments 0 reactions 1 assignee View on GitHub

@simonklee is already working on this.

Since Aug 22, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Description

While the agent streams a response, the text caret in the TUI input field visibly flickers/strobes. The caret is perfectly steady when idle and while typing — the strobing happens only while tokens are streaming.

We root-caused this to the renderer emitting a cursor hide/show pair (ESC[?25l / ESC[?25h) around every streamed frame inside beginRenderFrame() / the end-of-frame cursor-restore path. Terminals that treat the synchronized-output region as fully atomic (e.g. Windows Terminal) never render the intermediate states, so the caret is steady. WezTerm repaints cursor-visibility transitions even inside the sync region, so each frame's hide→show pair becomes visible as one flicker — hence per-frame strobing.

Evidence: same machine, same binary, same session
Terminal Caret during streaming
Windows Terminal ✅ steady
WezTerm stable 20240203 ❌ strobes
WezTerm nightly 20260820-095106-770d8e1a ❌ still strobes
Ruled out
  • tui.json"cursor": { "style": "line", "blinking": false } (confirmed loaded) — not blink-related
  • Terminal-side cursor_blink_rate = 0 + steady cursor styles — no effect
  • GPU/DWM compositing effects disabled (Acrylic/transparency off) — no effect
  • Plain PowerShell prompt at rest in all terminals — always steady

So this is not a blink setting on either side; it is per-frame DECTCEM churn during rendering.

Where it comes from

packages/native/src/renderer.zig (opentui):

// runs at the start of EVERY frame:
fn beginRenderFrame(writer: anytype) void {
    writer.writeAll(ansi.ANSI.syncSet) catch {};      // ESC[?2026h
    writer.writeAll(ansi.ANSI.hideCursor) catch {};   // ESC[?25l  <- every frame
}

// ...frame diff...

// end-of-frame cursor restore (~line 2704):
const needsCursorRestore = frame_started or styleChanged or colorChanged or positionChanged or visibilityChanged;
if (needsCursorRestore) {
    ...
    ansi.ANSI.moveToOutput(writer, cursorX, cursorY) catch {};
    writer.writeAll(ansi.ANSI.showCursor) catch {};   // ESC[?25h  <- every frame
}

Net byte pattern per streamed frame: ?2026h ?25l [diff] moveTo ?25h ?2026l. Windows Terminal batches this atomically; WezTerm surfaces each visibility transition.

This is adjacent to #287 (opentui), where the same hide-per-frame behavior was discussed as a deliberate trade-off to prevent the hardware cursor from being painted over mid-diff on other terminals.

Suggested direction

Avoid toggling DECTCEM when the frame will not actually repaint over the cell the hardware cursor currently occupies (overlap check against the dirty region + lastCursorX/lastCursorY). During typical streaming, incoming content lands above the input box, so most frames would skip the hide/show entirely, eliminating the strobe while preserving the existing protection when a diff genuinely overlaps the caret cell.

Happy to attempt a PR along these lines if maintainers agree with the approach.

Plugins

None

OpenCode version

1.18.21

Steps to reproduce
  1. Run opencode in WezTerm (stable 20240203 or nightly 20260820)
  2. Submit any prompt that produces a streamed response
  3. Watch the input caret while tokens stream — it visibly strobes until the response finishes

Same steps in Windows Terminal: caret stays completely steady.

Operating System

Windows 11

Terminal

WezTerm (both stable 20240203 and nightly 20260820); Windows Terminal used as the clean control

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.