Skip to content

Commit 3846c1d

Browse files
gtrrz-victorclaude
andcommitted
fix(import): gate progress spinner ANSI on ShouldStyle for legacy consoles
The import progress spinner (#1848) drew its animated frames with cursor-control escapes (\r\033[K) whenever the writer was a terminal, gating only on IsTerminalWriter and bypassing interactive.ShouldStyle — the repo's single gate for writer-scoped ANSI. On a console that can't render ANSI (TERM=cygwin renders the ESC byte as a literal "←", GH #1267) or when NO_COLOR is set, every frame emitted "←[K" garbage. The per-frame \033[K this PR added widened a previously stop-line-only escape to the whole animation, so legacy Windows consoles regressed from clean bare-\r frames to visible garbage. - startUpdatableSpinner: fall back to the completion-line-only (ANSI-free) path unless w both is a terminal and ShouldStyle(w) — fixes it at the source for every spinner caller, not just import. - newImportProgressReporter: route non-styleable terminals to the plain per-session line path (same as non-TTY/ACCESSIBLE), so NO_COLOR/cygwin users still get per-session progress instead of a lone final line. - setup_import_test.go: correct a comment naming a test (TestNewImportProgressReporter_TTYAdvancesOnSkip) that does not exist. The terminal+!ShouldStyle branch is only observable against a real TTY, so it carries no unit test — matching the PR's existing PTY-bound coverage limits; the shouldStyle decision itself is already unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KYM4JTVVADFMT258B3020DV9
1 parent 035efd7 commit 3846c1d

3 files changed

Lines changed: 23 additions & 9 deletions

File tree

cmd/entire/cli/import_progress.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import (
1414
// tracks "Importing <agentName> sessions... (session i/N · turn j/M)";
1515
// callers must call the returned stop exactly once when the run finishes —
1616
// on both the success and error paths — so no spinner frame is left
17-
// dangling to corrupt whatever prints next. Otherwise (non-TTY, piped, or
18-
// ACCESSIBLE mode) it prints one plain, ANSI-free line per session from
19-
// SessionStart, and stop is a no-op.
17+
// dangling to corrupt whatever prints next. Otherwise — non-TTY, piped,
18+
// ACCESSIBLE mode, or a terminal that can't render ANSI (NO_COLOR,
19+
// TERM=cygwin; see interactive.ShouldStyle) — it prints one plain, ANSI-free
20+
// line per session from SessionStart, and stop is a no-op. The ShouldStyle
21+
// gate matches startUpdatableSpinner's own gate, so the spinner branch here
22+
// is taken only when the animation it drives can actually be rendered.
2023
func newImportProgressReporter(w io.Writer, agentName string) (progress *agentimport.Progress, stop func(success bool)) {
21-
if !interactive.IsTerminalWriter(w) || IsAccessibleMode() {
24+
if !interactive.IsTerminalWriter(w) || IsAccessibleMode() || !interactive.ShouldStyle(w) {
2225
return &agentimport.Progress{
2326
SessionStart: func(sessionIndex, sessionTotal int, _, _ string, turnCount int) {
2427
fmt.Fprintf(w, "Importing %s session %d/%d (%d %s)...\n",

cmd/entire/cli/progress.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
4148
func startUpdatableSpinner(w io.Writer, msg string) (update func(string), stop func(success bool)) {
4249
var mu sync.Mutex
4350
current := msg
@@ -52,7 +59,7 @@ func startUpdatableSpinner(w io.Writer, msg string) (update func(string), stop f
5259
return current
5360
}
5461

55-
if !interactive.IsTerminalWriter(w) {
62+
if !interactive.IsTerminalWriter(w) || !interactive.ShouldStyle(w) {
5663
return setMsg, func(success bool) {
5764
if success {
5865
fmt.Fprintf(w, "✓ %s\n", getMsg())

cmd/entire/cli/setup_import_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,13 @@ func TestRunSelectedImports_NonTTYProgressLines(t *testing.T) {
380380
// idempotent pass over an already-imported corpus (every turn hits
381381
// agentimport's TurnSkipped path, not TurnWritten) still prints one plain
382382
// progress line per session and reports the correct "0 imported" summary —
383-
// the non-TTY side of the P2 Codex's pre-push review caught (the TTY side is
384-
// covered by agentimport's TestRun_ReimportFiresTurnSkippedNotTurnWritten
385-
// and this package's TestNewImportProgressReporter_TTYAdvancesOnSkip).
383+
// the non-TTY side of the bug the P2 Codex's pre-push review caught. The TTY
384+
// side (a fully-skipped session must still sweep the spinner to turn M/M) is
385+
// covered at the agentimport layer by
386+
// TestRun_ReimportFiresTurnSkippedNotTurnWritten; the cli wiring it depends on
387+
// — newImportProgressReporter routing TurnSkipped and TurnWritten through one
388+
// shared advance path — has no direct test because the spinner branch only
389+
// runs against a real terminal.
386390
func TestRunSelectedImports_NonTTYProgressLines_Reimport(t *testing.T) {
387391
// Not parallel: chdirs into a temp repo and performs real checkpoint writes.
388392
dir := t.TempDir()

0 commit comments

Comments
 (0)