Skip to content

Commit 753f0d7

Browse files
committed
fix(console): clear the spinner line before stopSpinner returns
Stopping only signaled the spinner goroutine, which could sleep up to 80ms before clearing the line — so an error printed right after landed on the leftover "Starting agent" text. Block the stop function until the clear escape has been written.
1 parent f24dc8d commit 753f0d7

2 files changed

Lines changed: 52 additions & 8 deletions

File tree

cmd/lk/console_tui.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"encoding/json"
2020
"fmt"
21+
"io"
2122
"os"
2223
"strings"
2324
"time"
@@ -51,22 +52,30 @@ var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "
5152
// startSpinner shows a braille spinner on stderr with the given message.
5253
// Returns a stop function that clears the spinner line.
5354
func startSpinner(msg string) func() {
55+
return startSpinnerTo(os.Stderr, msg)
56+
}
57+
58+
func startSpinnerTo(w io.Writer, msg string) func() {
5459
done := make(chan struct{})
60+
cleared := make(chan struct{})
5561
go func() {
56-
i := 0
57-
for {
62+
defer close(cleared)
63+
for i := 0; ; i++ {
64+
fmt.Fprintf(w, "\r %s %s", spinnerFrames[i%len(spinnerFrames)], msg)
5865
select {
5966
case <-done:
60-
fmt.Fprintf(os.Stderr, "\r\033[K")
67+
fmt.Fprintf(w, "\r\033[K")
6168
return
62-
default:
63-
fmt.Fprintf(os.Stderr, "\r %s %s", spinnerFrames[i%len(spinnerFrames)], msg)
64-
i++
65-
time.Sleep(80 * time.Millisecond)
69+
case <-time.After(80 * time.Millisecond):
6670
}
6771
}
6872
}()
69-
return func() { close(done) }
73+
// Stopping blocks until the line is cleared, so the caller's next print
74+
// (e.g. an error) never lands on the leftover spinner text.
75+
return func() {
76+
close(done)
77+
<-cleared
78+
}
7079
}
7180

7281
type consoleTickMsg struct{}

cmd/lk/console_tui_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2026 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"bytes"
19+
"strings"
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestStopSpinner_ClearsLineBeforeReturning(t *testing.T) {
26+
// Models the caller printing an error right after stopping the spinner:
27+
// by the time stop returns, the spinner line must already be cleared, or
28+
// the error lands on the same line as the leftover "⠋ Starting agent".
29+
var buf bytes.Buffer
30+
stop := startSpinnerTo(&buf, "Starting agent")
31+
stop()
32+
out := buf.String()
33+
require.True(t, strings.HasSuffix(out, "\r\033[K"),
34+
"spinner line not cleared when stop returned; output: %q", out)
35+
}

0 commit comments

Comments
 (0)