CommandCodeAI / CommandCodeAI/command-code

Spinner/status line doesn't overwrite in-place when running inside a non-standard PTY relay (Tauri + xterm.js)

Open
#770 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
No language data
Stars
4k
Forks
350
PR merge metrics
No merged PRs in 30d

Description

Summary

flicker.log

Summary

The animated status line ("Preparing… esc to interrupt • Ns • ↓ 0") prints a new line
on every animation frame instead of overwriting the previous one, when Command Code
runs inside a PTY that isn't a native terminal (specifically: a Tauri desktop app
embedding xterm.js, with the shell spawned via portable-pty in Rust). The same
package version works correctly in a normal terminal (tested in kitty).

Environment
  • command-code v1.38.2 (models: claude-sonnet-4.6 · taste-1)
  • Host terminal: kitty (macOS)
  • Reproduces inside: a Tauri app's embedded terminal tab (xterm.js 5.x +
    portable-pty, PTY spawned with a real TTY, TERM=xterm-256color, CI unset,
    TERMINFO unset for the test — none of these changed the behavior)
  • process.stdout.isTTY / process.stdin.isTTY: both true inside the broken session
  • CMD_FLICKER_DEBUG=1 log shows a stable cols=95 on every STATUS_RENDER during
    the broken bursts — the reported terminal width is not fluctuating or falling
    back to a default.
Repro
  1. Run cmd inside the embedded PTY described above.
  2. Ask any question that triggers the "thinking" status spinner.
  3. Every animation frame (~80ms cadence) appears on its own new line instead of
    overwriting the previous frame in place.
What we found digging into dist/cli.mjs

registerInkControl() replaces Ink's internal log/throttledLog with a custom
createPhysicalLogUpdate() renderer that does cursor-up + erase-line (via
ansi-escapes's eraseLines, wrapped in DEC synchronized-output \x1b[?2026h/l)
instead of Ink's built-in log-update. This custom renderer is what fails to
correctly erase the previous frame in our environment.

Notably: hardResetForResize() (triggered on every SIGWINCH-driven resize) does a
full clearTerminal + resets Ink's internal lastOutput/fullStaticOutput state.
While we are actively resizing the host window (continuous SIGWINCH), every frame
goes through this hard-reset path and renders correctly. The instant resizing
stops, rendering falls back to the incremental createPhysicalLogUpdate path and
immediately starts stacking new lines again — even though getCols() keeps
reporting a stable, plausible cols value throughout.

This suggests the incremental cursor-up/erase-line path in
createPhysicalLogUpdate/physicalLineCount has an assumption that doesn't hold
in this PTY relay (order/timing of writes vs. Ink's own "Static" content stream,
possibly), while the "hard reset" path is unaffected because it doesn't rely on
tracked cursor position at all.

Attached

~/.commandcode/logs/flicker.log (CMD_FLICKER_DEBUG=1 session covering a broken burst)

Expected Behavior

While Command Code is streaming its "thinking" status (spinner + "esc to interrupt • Ns • ↓ 0"), each animation frame should overwrite the previous one in place — the status line should stay on a single line/row, not accumulate new lines.

Actual Behavior

Every animation frame (~80ms cadence) is printed as a brand-new line instead of overwriting the previous one, so the terminal fills up with dozens of stacked "Preparing… esc to interrupt • 0s • ↓ 0" / "Sculpting… esc to interrupt • 1s • ↓ 0" lines for a single status update. This only happens inside a PTY relay that isn't a native terminal (a Tauri desktop app embedding xterm.js, shell spawned via Rust's portable-pty) — the exact same cmd install works correctly in a normal terminal (tested in kitty on the same machine).

Diagnostics ruled out: process.stdout.isTTY/process.stdin.isTTY are both true; CI/CONTINUOUS_INTEGRATION are unset; TERMINFO unset makes no difference; CMD_FLICKER_DEBUG=1 shows a stable cols=95 on every STATUS_RENDER during the broken bursts (not falling back to a default, not fluctuating).

Notably: while continuously resizing the host window (which sends repeated SIGWINCH → hardResetForResize() → full clearTerminal + Ink internal state reset), the status line renders correctly — the bug only reappears once resizing stops and rendering falls back to the incremental createPhysicalLogUpdate (cursor-up + eraseLines, wrapped in \x1b[?2026h/l) path that registerInkControl() patches onto Ink's internal logger.

Steps to reproduce the issue
  1. Run command-code (v1.38.2) inside a PTY that is not a native terminal — specifically, a Tauri app with a shell spawned via portable-pty in Rust and rendered in the frontend with xterm.js (env: TERM=xterm-256color, real TTY, CI unset).
  2. Ask any question that triggers the "thinking" status spinner (e.g. "analiza en qué fase vamos").
  3. Observe: each spinner frame appears on a new line instead of overwriting the previous one.
  4. (Optional, to confirm) Run with CMD_FLICKER_DEBUG=1 cmd, reproduce, then inspect ~/.commandcode/logs/flicker.log — cols= stays stable throughout the broken burst.
  5. (Optional, to see the masking effect) While the status is streaming, continuously resize the host window — the spinner overwrites correctly during the resize, then reverts to stacking lines the moment resizing stops.
Command Code Version

1.38.2

Operating System

macOS

Terminal/IDE

Unknown

Shell

zsh

Session file (optional)

No response

Fix prompt (optional)

No response

Additional context

OS: macOS Tahoe 26.6.2

Contributor guide

No contributing guide indexed for this repository

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.

Research direction

The relevant entry points named are registerInkControl(), createPhysicalLogUpdate(), physicalLineCount(), and hardResetForResize() in dist/cli.mjs. Reproduce with CMD_FLICKER_DEBUG=1 in the Tauri/xterm.js PTY and compare incremental rendering with the resize-triggered reset path. Done means spinner frames overwrite one row in that relay instead of accumulating lines, while normal-terminal behavior remains intact.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, tauri
Domain
cli, desktop
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.