anomalyco / anomalyco/opencode
TUI input caret strobes during response streaming in WezTerm (per-frame cursor show/hide churn)
@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
- Run
opencodein WezTerm (stable 20240203 or nightly 20260820) - Submit any prompt that produces a streamed response
- 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
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.