CommandCodeAI / CommandCodeAI/command-code

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

オープン
#770 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

主要言語
言語のデータがありません
スター
4k
フォーク
350
PR マージ指標
30日以内にマージされた PR はありません

説明

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

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

指定されている関連するエントリポイントは、dist/cli.mjs の registerInkControl()、createPhysicalLogUpdate()、physicalLineCount()、hardResetForResize() です。Tauri/xterm.js の PTY で CMD_FLICKER_DEBUG=1 を使って再現し、インクリメンタルレンダリングとリサイズによってトリガーされるリセットパスを比較してください。完了の条件は、その relay で spinner のフレームが行を蓄積するのではなく 1 行を上書きし、通常のターミナルの動作はそのまま維持されることです。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
rust, tauri
領域
cli, desktop
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
48/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。