@@ -33,11 +33,18 @@ func startSpinner(w io.Writer, msg string) func(success bool) {
3333
3434// startUpdatableSpinner is startSpinner's variant for an operation whose
3535// status text changes while it runs (e.g. "session 2/5 · turn 3/10"). update
36- // replaces the message the next frame draws — or, on a non-terminal writer ,
36+ // replaces the message the next frame draws — or, on the non-animated path ,
3737// the message stop's completion line uses. update is safe to call at any
3838// point, including before the spinner's first frame draws and after stop
3939// returns. stop behaves exactly like startSpinner's, rendering whichever
4040// message update last set (or msg, if update was never called).
41+ //
42+ // The live animation is emitted only when w both is a terminal and can render
43+ // ANSI (interactive.ShouldStyle) — the frames use cursor-control escapes
44+ // (\r\033[K), which a legacy console that can't handle ANSI (e.g.
45+ // TERM=cygwin) renders as literal "←[K" garbage, and which NO_COLOR asks us
46+ // to suppress. When styling is off we fall back to the completion-line-only
47+ // path, so no escape byte is ever written to such a writer.
4148func startUpdatableSpinner (w io.Writer , msg string ) (update func (string ), stop func (success bool )) {
4249 var mu sync.Mutex
4350 current := msg
@@ -52,7 +59,9 @@ func startUpdatableSpinner(w io.Writer, msg string) (update func(string), stop f
5259 return current
5360 }
5461
55- if ! interactive .IsTerminalWriter (w ) {
62+ // ShouldStyle already returns false for a non-terminal writer, so this
63+ // single gate also covers the plain non-TTY case.
64+ if ! interactive .ShouldStyle (w ) {
5665 return setMsg , func (success bool ) {
5766 if success {
5867 fmt .Fprintf (w , "✓ %s\n " , getMsg ())
0 commit comments