Skip to content

bug(cli): the TUI emits no ANSI on native Windows shells — an unset TERM is read as colorless before any platform check #3536

Description

@shaokeyibb

What happened

The TUI renders entirely without color on native Windows shells — no status disc tints, no accent, no red exit codes. The same build on WSL is fully colored. This is not a downgrade to 16 colors; not a single escape sequence is emitted.

Cause is the first rule in the detector:

// packages/cli/src/tui-ansi.ts
const term = env.TERM ?? '';
if (term === 'dumb' || term === '') return 0;

PowerShell and cmd.exe do not set TERM at all, so an unset TERM is read as "colorless" before any other signal is considered. Git Bash and WSL do set it, which is exactly where color still works.

The capability is present — this is a detection bug, not a terminal limitation. Measured on the same machine (Windows 11, 10.0.26200, inside Windows Terminal):

shell TERM tty.getColorDepth current level
PowerShell 5.1 unset 24 (truecolor) 0
cmd.exe unset 24 (truecolor) 0
Git Bash xterm-256color 24 2
WSL2 (Ubuntu) xterm-256color 8 2

Node reports 24-bit support in the two shells where the TUI emits nothing.

Second reachable case

COLORTERM is ignored whenever TERM is unset, because the empty-TERM return happens first. A terminal that declares COLORTERM=truecolor without setting TERM gets level 0 on every platform, not just Windows.

How to reproduce

  1. Open PowerShell or cmd.exe (not Git Bash, not WSL) on Windows.
  2. Start the TUI from source.
  3. Every tool row, status disc, and diff renders as plain text.

Or, without a terminal, compare the built module under two env shapes:

TERM=xterm-256color  →  "�[32mX�[39m�[38;5;75mY�[39m…"
COLORTERM=truecolor  →  "�[38;2;87;163;239m…"
TERM unset           →  "XY●"          ← no escapes at all

Environment

  • Maka commit: f19eede03
  • OS: Windows 11 (10.0.26200), Windows Terminal
  • Surface: TUI / CLI
  • Node.js: v22.18.0

Logs, screenshots, or additional context

This contradicts the detector's own stated benchmark

The function documents what it is modelled on:

Benchmark: codex supports-color3-level; pitheme.ts 256 fallback.

supports-color never treats a missing TERM as colorless. It returns on platform before consulting TERM at all:

if (env.TERM === 'dumb') return min;

if (process.platform === 'win32') {
  // Windows 10 build 10586 is the first Windows release that supports 256 colors.
  // Windows 10 build 14931 is the first release that supports 16m/TrueColor.
  const osRelease = os.release().split('.');
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
    return Number(osRelease[2]) >= 14931 ? 3 : 2;
  }
  return 1;
}
// TERM / COLORTERM heuristics only after this

So the gap is a transcription one: the TERM ladder was carried over, the platform branch was not, and "TERM empty" became an early return 0 instead of falling through.

Node's built-in tty.WriteStream.getColorDepth() implements the same Windows build check, which is why it answers 24 where the hand-rolled ladder answers 0.

Suggested direction

Keep the two intent overrides (NO_COLOR, TERM=dumb) and the explicit truecolor upgrade, then ask the terminal instead of re-deriving capability from TERM:

  1. NO_COLOR non-empty → 0
  2. TERM=dumb → 0
  3. COLORTERM=truecolor|24bit, or TERM ending -truecolor/-24bit → 3
  4. otherwise map process.stdout.getColorDepth() — 24 → 3, 8 → 2, 4 → 1, else 0
  5. when there is no TTY to ask, fall back to today's TERM ladder

This deletes the hand-maintained platform ladder rather than bolting a Windows special case onto it, and leaves Linux/macOS behavior unchanged. Adopting supports-color as a direct dependency would also work, but it carries third-party-notice obligations that the built-in does not.

packages/cli has no test for the detector today; it is a pure function over an env snapshot and is straightforward to pin.

Investigated with AI assistance (Claude Code); every number above measured on the machine described.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions