Skip to content

Commit b96fe07

Browse files
committed
feat: replace spinner with KITT-style red scanning animation
Drop the points spinner and message label in favor of a Knight Rider scanner that bounces a red glow across 8 small squares. Frames are pre-rendered with a 3-level intensity trail (#FF0000, #990000, #440000) at 14 FPS.
1 parent 676a6b0 commit b96fe07

File tree

2 files changed

+67
-18
lines changed

2 files changed

+67
-18
lines changed

cmd/root.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
876876

877877
// Start initial spinner (skip if quiet)
878878
if !config.Quiet && cli != nil {
879-
currentSpinner = ui.NewSpinner("Thinking...")
879+
currentSpinner = ui.NewSpinner("")
880880
currentSpinner.Start()
881881
}
882882

@@ -1060,7 +1060,7 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
10601060
responseWasStreamed = false
10611061
streamingStarted = false
10621062
// Start spinner again for next LLM call
1063-
currentSpinner = ui.NewSpinner("Thinking...")
1063+
currentSpinner = ui.NewSpinner("")
10641064
currentSpinner.Start()
10651065
}
10661066
},
@@ -1086,7 +1086,7 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
10861086
_ = cli.DisplayAssistantMessageWithModel(content, config.ModelName)
10871087
lastDisplayedContent = content
10881088
// Start spinner again for tool calls
1089-
currentSpinner = ui.NewSpinner("Thinking...")
1089+
currentSpinner = ui.NewSpinner("")
10901090
currentSpinner.Start()
10911091
} else if responseWasStreamed {
10921092
// Content was already streamed, just track it and manage spinner
@@ -1096,7 +1096,7 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
10961096
currentSpinner = nil
10971097
}
10981098
// Start spinner again for tool calls
1099-
currentSpinner = ui.NewSpinner("Thinking...")
1099+
currentSpinner = ui.NewSpinner("")
11001100
currentSpinner.Start()
11011101
}
11021102
},
@@ -1116,7 +1116,7 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
11161116
return false, err
11171117
}
11181118
// Start spinner again for tool calls
1119-
currentSpinner = ui.NewSpinner("Thinking...")
1119+
currentSpinner = ui.NewSpinner("")
11201120
currentSpinner.Start()
11211121

11221122
return allow, nil

internal/ui/spinner.go

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"image/color"
66
"os"
7+
"strings"
78
"sync"
89
"time"
910

@@ -12,13 +13,54 @@ import (
1213

1314
// spinnerFrames defines available spinner animation styles.
1415
var (
15-
pointsFrames = []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"}
16-
pointsFPS = time.Second / 7
17-
1816
dotFrames = []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "}
1917
dotFPS = time.Second / 10
2018
)
2119

20+
// knightRiderFrames generates a KITT-style scanning animation where a bright
21+
// red light bounces back and forth across a row of dots with a trailing glow.
22+
func knightRiderFrames() []string {
23+
const numDots = 8
24+
const dot = "▪"
25+
26+
bright := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
27+
med := lipgloss.NewStyle().Foreground(lipgloss.Color("#990000"))
28+
dim := lipgloss.NewStyle().Foreground(lipgloss.Color("#440000"))
29+
off := lipgloss.NewStyle().Foreground(lipgloss.Color("#222222"))
30+
31+
// Scanner bounces: 0→7→0
32+
positions := make([]int, 0, 2*numDots-2)
33+
for i := 0; i < numDots; i++ {
34+
positions = append(positions, i)
35+
}
36+
for i := numDots - 2; i > 0; i-- {
37+
positions = append(positions, i)
38+
}
39+
40+
frames := make([]string, len(positions))
41+
for f, pos := range positions {
42+
var b strings.Builder
43+
for i := 0; i < numDots; i++ {
44+
d := pos - i
45+
if d < 0 {
46+
d = -d
47+
}
48+
switch {
49+
case d == 0:
50+
b.WriteString(bright.Render(dot))
51+
case d == 1:
52+
b.WriteString(med.Render(dot))
53+
case d == 2:
54+
b.WriteString(dim.Render(dot))
55+
default:
56+
b.WriteString(off.Render(dot))
57+
}
58+
}
59+
frames[f] = b.String()
60+
}
61+
return frames
62+
}
63+
2264
// Spinner provides an animated loading indicator that displays while
2365
// long-running operations are in progress. It writes directly to stderr
2466
// using a goroutine-based animation loop, avoiding Bubble Tea's terminal
@@ -27,19 +69,19 @@ type Spinner struct {
2769
message string
2870
frames []string
2971
fps time.Duration
30-
color color.Color
72+
color color.Color // nil when frames are pre-rendered with embedded colors
3173
done chan struct{}
3274
once sync.Once
3375
}
3476

3577
// NewSpinner creates a new animated spinner with the specified message.
36-
// The spinner uses the theme's primary color and a points animation style.
78+
// Uses a KITT-style red scanning animation.
3779
func NewSpinner(message string) *Spinner {
3880
return &Spinner{
3981
message: message,
40-
frames: pointsFrames,
41-
fps: pointsFPS,
42-
color: GetTheme().Primary,
82+
frames: knightRiderFrames(),
83+
fps: time.Second / 14,
84+
color: nil, // frames are pre-rendered
4385
done: make(chan struct{}),
4486
}
4587
}
@@ -72,14 +114,17 @@ func (s *Spinner) Stop() {
72114
func (s *Spinner) run() {
73115
theme := GetTheme()
74116

75-
spinnerStyle := lipgloss.NewStyle().
76-
Foreground(s.color).
77-
Bold(true)
78-
79117
messageStyle := lipgloss.NewStyle().
80118
Foreground(theme.Text).
81119
Italic(true)
82120

121+
var spinnerStyle lipgloss.Style
122+
if s.color != nil {
123+
spinnerStyle = lipgloss.NewStyle().
124+
Foreground(s.color).
125+
Bold(true)
126+
}
127+
83128
ticker := time.NewTicker(s.fps)
84129
defer ticker.Stop()
85130

@@ -92,8 +137,12 @@ func (s *Spinner) run() {
92137
return
93138
case <-ticker.C:
94139
f := s.frames[frame%len(s.frames)]
140+
rendered := f
141+
if s.color != nil {
142+
rendered = spinnerStyle.Render(f)
143+
}
95144
fmt.Fprintf(os.Stderr, "\r %s %s",
96-
spinnerStyle.Render(f),
145+
rendered,
97146
messageStyle.Render(s.message))
98147
frame++
99148
}

0 commit comments

Comments
 (0)