diff --git a/internal/config/config.go b/internal/config/config.go index ff948b874ea1613ca126053547dcf9b7d4cc3297..8c9af2a6c6837a5bc36eb3313a78e5c753aedada 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -128,8 +128,9 @@ type LSPConfig struct { } type TUIOptions struct { - CompactMode bool `json:"compact_mode,omitempty" jsonschema:"description=Enable compact mode for the TUI interface,default=false"` - DiffMode string `json:"diff_mode,omitempty" jsonschema:"description=Diff mode for the TUI interface,enum=unified,enum=split"` + CompactMode bool `json:"compact_mode,omitempty" jsonschema:"description=Enable compact mode for the TUI interface,default=false"` + DiffMode string `json:"diff_mode,omitempty" jsonschema:"description=Diff mode for the TUI interface,enum=unified,enum=split"` + ReduceAnimations bool `json:"reduce_animations,omitempty" jsonschema:"description=Reduce animations in the TUI,default=false"` // Here we can add themes later or any TUI related options // diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index 52556bb9bf16ebc6ff258bb13f960659b6c0d502..2c930e7a52d0771c6335b3b4f4e0bbd38faff9af 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -124,6 +124,14 @@ type anim struct { // New creates a new anim instance with the specified width and label. func New(opts Settings) Spinner { + if colorIsUnset(opts.LabelColor) { + opts.LabelColor = defaultLabelColor + } + + if opts.Static { + return newStatic(opts.Label, opts.LabelColor) + } + // Validate settings. if opts.Size < 1 { opts.Size = defaultNumCyclingChars @@ -134,13 +142,7 @@ func New(opts Settings) Spinner { if colorIsUnset(opts.GradColorB) { opts.GradColorB = defaultGradColorB } - if colorIsUnset(opts.LabelColor) { - opts.LabelColor = defaultLabelColor - } - if opts.Static { - return newStatic(opts.Label, opts.LabelColor) - } a := &anim{} a.id = nextID() a.startTime = time.Now() @@ -321,9 +323,7 @@ func (a *anim) Width() (w int) { } // Init starts the animation. -func (a *anim) Init() tea.Cmd { - return a.Step() -} +func (a *anim) Init() tea.Cmd { return stepCmd(a.id) } // Update processes animation steps (or not). func (a *anim) Update(msg tea.Msg) (Spinner, tea.Cmd) { @@ -348,7 +348,7 @@ func (a *anim) Update(msg tea.Msg) (Spinner, tea.Cmd) { } else if !a.initialized.Load() && time.Since(a.startTime) >= maxBirthOffset { a.initialized.Store(true) } - return a, a.Step() + return a, stepCmd(a.id) default: return a, nil } @@ -388,10 +388,10 @@ func (a *anim) View() string { return b.String() } -// Step is a command that triggers the next step in the animation. -func (a *anim) Step() tea.Cmd { +// stepCmd is a command that triggers the next stepCmd in the animation. +func stepCmd(id int) tea.Cmd { return tea.Tick(time.Second/time.Duration(fps), func(t time.Time) tea.Msg { - return StepMsg{id: a.id} + return StepMsg{id: id} }) } diff --git a/internal/tui/components/anim/static.go b/internal/tui/components/anim/static.go index fff8c6d458720094dcf9a62a33c3beb1edd62918..3e9b9d8775c6d4885f86fc3b82a26ef7804c786f 100644 --- a/internal/tui/components/anim/static.go +++ b/internal/tui/components/anim/static.go @@ -1,6 +1,7 @@ package anim import ( + "cmp" "image/color" tea "github.com/charmbracelet/bubbletea/v2" @@ -10,18 +11,33 @@ import ( type noAnim struct { Color color.Color rendered string + id int } func newStatic(label string, foreground color.Color) Spinner { a := &noAnim{Color: foreground} a.SetLabel(label) + a.id = nextID() return a } func (s *noAnim) SetLabel(label string) { - s.rendered = lipgloss.NewStyle().Foreground(s.Color).Render(label + ellipsisFrames[2]) + s.rendered = lipgloss.NewStyle(). + Foreground(s.Color). + Render(cmp.Or(label, "Working") + ellipsisFrames[2]) } -func (s noAnim) Init() tea.Cmd { return nil } -func (s *noAnim) Update(tea.Msg) (Spinner, tea.Cmd) { return s, nil } -func (s *noAnim) View() string { return s.rendered } +func (s noAnim) Init() tea.Cmd { return stepCmd(s.id) } +func (s *noAnim) View() string { return s.rendered } +func (s *noAnim) Update(msg tea.Msg) (Spinner, tea.Cmd) { + switch msg := msg.(type) { + case StepMsg: + if msg.id != s.id { + // Reject messages that are not for this instance. + return s, nil + } + return s, stepCmd(s.id) + default: + return s, nil + } +} diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index 1374df25867919b6f7e4e2a8875c41cd26eee9ba..0e579db33402d249773e2295cca19e1e688c29c3 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -75,6 +75,7 @@ func NewMessageCmp(msg message.Message) MessageCmp { m := &messageCmp{ message: msg, anim: anim.New(anim.Settings{ + Static: isReduceAnimations(), Size: 15, GradColorA: t.Primary, GradColorB: t.Secondary, @@ -425,3 +426,10 @@ func (m *assistantSectionModel) IsSectionHeader() bool { func (m *messageCmp) ID() string { return m.message.ID } + +func isReduceAnimations() bool { + cfg := config.Get() + return cfg.Options != nil && + cfg.Options.TUI != nil && + cfg.Options.TUI.ReduceAnimations +} diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index e4b00510a7ed3505694ad452795b77ed812697d6..cb7e648efc01b27b3ca0805859960d91050a4a4c 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -120,6 +120,7 @@ func NewToolCallCmp(parentMessageID string, tc message.ToolCall, permissions per } t := styles.CurrentTheme() m.anim = anim.New(anim.Settings{ + Static: isReduceAnimations(), Size: 15, Label: "Working", GradColorA: t.Primary, @@ -129,6 +130,7 @@ func NewToolCallCmp(parentMessageID string, tc message.ToolCall, permissions per }) if m.isNested { m.anim = anim.New(anim.Settings{ + Static: isReduceAnimations(), Size: 10, GradColorA: t.Primary, GradColorB: t.Secondary,