detached e1fdf4d

tweak(tui): stable spinner phrase per turn

The status-bar quip used to rotate every 2.5s through
funnyWorkingLines while a turn ran. On a long response that
meant five or six different phrases during one reply, which
reads as activity that isn't actually happening (the model is
just still streaming the same answer).

Start() now picks a random index once, Message() returns that
same index until the next Start(). Next turn picks a different
phrase so the set still feels varied across a session.

Dropped the unused lastSwap timer field that drove the rotation
and its assignments in Start/StartFixed.
commit
e1fdf4d42e741d2ec43bc68c7526567fda82ac68
author
patriceckhart · · 2026-04-20 14:00 UTC
parent
8927ac1
signature
unsigned
refs
tag: v0.0.68
1 file · +11 -12
M internal/agent/modes/spinner.go +11 -12
@@ -13,7 +13,6 @@ type spinner struct {
1313 messages []string
1414 startedAt time.Time
1515 msgIdx int
16- lastSwap time.Time
1716
1817 // fixedMsg overrides the rotating funnyWorkingLines message when
1918 // set. Used for auto-compaction so the spinner clearly says what's
@@ -80,12 +79,17 @@ func newSpinner() *spinner {
8079 return s
8180 }
8281
83-// Start resets the spinner to the beginning of its animation and picks
84-// a random opening message.
82+// Start resets the spinner to the beginning of its animation and
83+// picks a random message that stays fixed for the whole run. A
84+// rotating rollodex of quips during a single turn felt noisy in
85+// practice — you'd see five different phrases for one
86+// long-running response, which implies progress that isn't
87+// actually happening. One stable phrase per turn reads calmer
88+// and the variety across turns (next Start picks another index)
89+// still keeps the set fresh over a session.
8590 func (s *spinner) Start() {
8691 s.startedAt = time.Now()
8792 s.msgIdx = rand.Intn(len(s.messages))
88- s.lastSwap = s.startedAt
8993 s.fixedMsg = ""
9094 }
9195
@@ -93,7 +97,6 @@ func (s *spinner) Start() {
9397 // duration of this spinner run. Cleared by the next Start() call.
9498 func (s *spinner) StartFixed(msg string) {
9599 s.startedAt = time.Now()
96- s.lastSwap = s.startedAt
97100 s.fixedMsg = msg
98101 }
99102
@@ -109,18 +112,14 @@ func (s *spinner) Frame() string {
109112 return s.frames[idx]
110113 }
111114
112-// Message returns the current rotating status text. The text changes
113-// every ~2.5 seconds so the spinner doesn't look frozen. When the
114-// spinner was started via StartFixed, the pinned message is returned
115+// Message returns the spinner's status text. One random phrase
116+// per Start call, pinned until the next turn. When the spinner
117+// was started via StartFixed, the pinned message is returned
115118 // unchanged.
116119 func (s *spinner) Message() string {
117120 if s.fixedMsg != "" {
118121 return s.fixedMsg
119122 }
120- if time.Since(s.lastSwap) > 2500*time.Millisecond {
121- s.msgIdx = (s.msgIdx + 1) % len(s.messages)
122- s.lastSwap = time.Now()
123- }
124123 return s.messages[s.msgIdx]
125124 }
126125
@@ -13,7 +13,6 @@ type spinner struct {
13 messages []string13 messages []string
14 startedAt time.Time14 startedAt time.Time
15 msgIdx int15 msgIdx int
16- lastSwap time.Time
17 16
18 // fixedMsg overrides the rotating funnyWorkingLines message when17 // fixedMsg overrides the rotating funnyWorkingLines message when
19 // set. Used for auto-compaction so the spinner clearly says what's18 // set. Used for auto-compaction so the spinner clearly says what's
@@ -80,12 +79,17 @@ func newSpinner() *spinner {
80 return s79 return s
81 }80 }
82 81
83-// Start resets the spinner to the beginning of its animation and picks82+// Start resets the spinner to the beginning of its animation and
84-// a random opening message.83+// picks a random message that stays fixed for the whole run. A
84+// rotating rollodex of quips during a single turn felt noisy in
85+// practice — you'd see five different phrases for one
86+// long-running response, which implies progress that isn't
87+// actually happening. One stable phrase per turn reads calmer
88+// and the variety across turns (next Start picks another index)
89+// still keeps the set fresh over a session.
85 func (s *spinner) Start() {90 func (s *spinner) Start() {
86 s.startedAt = time.Now()91 s.startedAt = time.Now()
87 s.msgIdx = rand.Intn(len(s.messages))92 s.msgIdx = rand.Intn(len(s.messages))
88- s.lastSwap = s.startedAt
89 s.fixedMsg = ""93 s.fixedMsg = ""
90 }94 }
91 95
@@ -93,7 +97,6 @@ func (s *spinner) Start() {
93 // duration of this spinner run. Cleared by the next Start() call.97 // duration of this spinner run. Cleared by the next Start() call.
94 func (s *spinner) StartFixed(msg string) {98 func (s *spinner) StartFixed(msg string) {
95 s.startedAt = time.Now()99 s.startedAt = time.Now()
96- s.lastSwap = s.startedAt
97 s.fixedMsg = msg100 s.fixedMsg = msg
98 }101 }
99 102
@@ -109,18 +112,14 @@ func (s *spinner) Frame() string {
109 return s.frames[idx]112 return s.frames[idx]
110 }113 }
111 114
112-// Message returns the current rotating status text. The text changes115+// Message returns the spinner's status text. One random phrase
113-// every ~2.5 seconds so the spinner doesn't look frozen. When the116+// per Start call, pinned until the next turn. When the spinner
114-// spinner was started via StartFixed, the pinned message is returned117+// was started via StartFixed, the pinned message is returned
115 // unchanged.118 // unchanged.
116 func (s *spinner) Message() string {119 func (s *spinner) Message() string {
117 if s.fixedMsg != "" {120 if s.fixedMsg != "" {
118 return s.fixedMsg121 return s.fixedMsg
119 }122 }
120- if time.Since(s.lastSwap) > 2500*time.Millisecond {
121- s.msgIdx = (s.msgIdx + 1) % len(s.messages)
122- s.lastSwap = time.Now()
123- }
124 return s.messages[s.msgIdx]123 return s.messages[s.msgIdx]
125 }124 }
126 125

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close