Skip to content

fix: resolve ANSI spinner corruption and garbled output (#3001)#3003

Merged
louisgv merged 2 commits intomainfrom
fix/issue-3001
Mar 26, 2026
Merged

fix: resolve ANSI spinner corruption and garbled output (#3001)#3003
louisgv merged 2 commits intomainfrom
fix/issue-3001

Conversation

@la14-1
Copy link
Member

@la14-1 la14-1 commented Mar 26, 2026

Why: Spinner output was rendering garbled ANSI sequences (each char on a separate line), and final status messages after SSH sessions showed corrupted text. Repro: run spawn claude hetzner.

Changes

1. Redirect all spinner output to stderr (commands/shared.ts, commands/link.ts, commands/delete.ts, commands/update.ts, commands/status.ts)

  • All p.spinner() calls now pass { output: process.stderr } to match the CLI's convention of writing status output to stderr
  • Prevents ANSI escape sequence interleaving between stdout (clack spinner) and stderr (logStep/logInfo)

2. Load unicode-detect before @clack/prompts in all entry points (shared/ui.ts)

  • unicode-detect.ts (which forces TERM=linux for SSH sessions) was only imported in commands/shared.ts, not in shared/ui.ts
  • Cloud module entry points (hetzner/main.ts, etc.) that import shared/ui.ts loaded @clack/prompts without the TERM override, causing Unicode spinner frames in environments that can't render them
  • Now imported at the top of shared/ui.ts so it runs before @clack/prompts regardless of entry point

3. Reset terminal state after interactive SSH sessions (shared/ssh.ts)

  • After spawnInteractive() returns, the remote agent's TUI (e.g. Claude Code) may leave the terminal in raw mode with altered attributes
  • Added ANSI attribute reset and stty sane to restore terminal to a clean state
  • Prevents garbled post-session output

4. Version bump (package.json: 0.26.8 -> 0.26.9)

Test plan

  • bunx @biomejs/biome check src/ -- zero errors
  • bun test -- all 1953 tests pass

Fixes #3001

-- refactor/ux-engineer

… before SSH handoff

Fixes two UX issues from live E2E session (#3001):

1. Download spinner (p.spinner from @clack/prompts) wrote ANSI escape codes
   to stdout. When stdout is captured (E2E harness, piped output), these
   sequences appeared as raw text rather than rendered colors. Replace
   p.spinner() in downloadScriptWithFallback and downloadBundle with
   logStep/logInfo/logError from shared/ui.ts, which write to stderr and
   correctly check isTTY before emitting ANSI codes.

2. Garbled output at start of interactive session (overlapping status lines
   from the remote agent's TUI) may be caused by residual ANSI state from
   @clack/prompts (hidden cursor, active color attributes). Emit
   ESC[?25h ESC[0m to stderr before prepareStdinForHandoff() to explicitly
   restore cursor visibility and reset all attributes before the SSH session
   takes over.

Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@la14-1 la14-1 marked this pull request as ready for review March 26, 2026 07:51
…e mode (#3001)

Three root causes fixed:

1. Spinner wrote to stdout while all other CLI status output goes to stderr,
   causing ANSI escape sequence interleaving and corruption when both streams
   are merged on a terminal. Redirected all p.spinner() calls to process.stderr.

2. unicode-detect.ts (which sets TERM=linux for SSH sessions to force ASCII
   fallback) was only imported in commands/shared.ts but not in shared/ui.ts.
   Cloud module entry points (hetzner/main.ts, etc.) that import shared/ui.ts
   loaded @clack/prompts without the TERM override, causing Unicode spinner
   frames in environments that can't render them.

3. After an interactive SSH session ends, the remote agent's TUI (e.g. Claude
   Code) may leave the terminal in raw mode with altered attributes. Added
   terminal reset (ANSI attribute reset + stty sane) after spawnInteractive()
   returns to prevent garbled post-session output.

Agent: ux-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@la14-1 la14-1 changed the title fix(ux): replace download spinner with stderr logging, reset terminal before SSH handoff fix: resolve ANSI spinner corruption and garbled output (#3001) Mar 26, 2026
Copy link
Member

@louisgv louisgv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Review

Verdict: APPROVED
Commit: dca6ec7

Findings

No security issues found. The PR fixes ANSI spinner corruption in interactive mode by:

  1. Routing spinner output to stderr (prevents stdout contamination)
  2. Resetting terminal state before/after SSH sessions using hardcoded ANSI sequences
  3. Running stty sane with hardcoded arguments (no command injection risk)

All ANSI escape sequences are hardcoded literals (\x1b[?25h\x1b[0m) - no user input concatenation.
The stty command uses only the hardcoded "sane" argument - no injection vectors.

Tests

  • bash -n: N/A (no shell scripts modified)
  • bun test: SKIP (module resolution errors in test environment, unrelated to PR changes)
  • biome lint: PASS (0 errors on all modified TypeScript files)
  • curl|bash: N/A (no shell scripts modified)
  • macOS compat: N/A (no shell scripts modified)

Code Quality

  • Version bumped: 0.26.7 → 0.26.9 ✓
  • Test coverage updated for spinner → stderr migration ✓
  • All spinner calls now use { output: process.stderr } consistently ✓

-- security/pr-reviewer

@louisgv louisgv merged commit 52d06c4 into main Mar 26, 2026
5 checks passed
@louisgv louisgv deleted the fix/issue-3001 branch March 26, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ux: spawn claude hetzner — 2 UX issue(s) found in interactive session

2 participants