Detailed changes
  
  
    
    @@ -16,7 +16,7 @@ require (
 	github.com/aymanbagabas/go-udiff v0.3.1
 	github.com/bmatcuk/doublestar/v4 v4.8.1
 	github.com/charlievieth/fastwalk v1.0.11
-	github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
+	github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
 	github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1
 	github.com/charmbracelet/fang v0.1.0
 	github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
  
  
  
    
    @@ -68,8 +68,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
 github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
 github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 h1:CkMS9Ah9ac1Ego5JDC5NJyZyAAqu23Z+O0yDwsa3IxM=
+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
 github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595 h1:wLMjzOqrwoM7Em9UR9sGbn4375G8WuxcwFB3kjZiqHo=
 github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595/go.mod h1:+Tl7rePElw6OKt382t04zXwtPFoPXxAaJzNrYmtsLds=
 github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
  
  
  
    
    @@ -98,7 +98,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
 	// Start spinner if not in quiet mode
 	var spinner *format.Spinner
 	if !quiet {
-		spinner = format.NewSpinner("Thinking...")
+		spinner = format.NewSpinner(ctx, "Generating")
 		spinner.Start()
 		defer spinner.Stop()
 	}
@@ -129,6 +129,12 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
 	}
 
 	result := <-done
+
+	// Stop spinner before printing output
+	if !quiet && spinner != nil {
+		spinner.Stop()
+	}
+
 	if result.Error != nil {
 		if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
 			slog.Info("Agent processing cancelled", "session_id", sess.ID)
@@ -137,11 +143,6 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
 		return fmt.Errorf("agent processing failed: %w", result.Error)
 	}
 
-	// Stop spinner before printing output
-	if !quiet && spinner != nil {
-		spinner.Stop()
-	}
-
 	// Get the text content from the response
 	content := "No content available"
 	if result.Message.Content().String() != "" {
  
  
  
    
    @@ -2,101 +2,63 @@ package format
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 
-	"github.com/charmbracelet/bubbles/v2/spinner"
 	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/crush/internal/tui/components/anim"
+	"github.com/charmbracelet/crush/internal/tui/styles"
+	"github.com/charmbracelet/x/ansi"
 )
 
 // Spinner wraps the bubbles spinner for non-interactive mode
 type Spinner struct {
-	model  spinner.Model
-	done   chan struct{}
-	prog   *tea.Program
-	ctx    context.Context
-	cancel context.CancelFunc
+	done chan struct{}
+	prog *tea.Program
 }
 
-// spinnerModel is the tea.Model for the spinner
-type spinnerModel struct {
-	spinner  spinner.Model
-	message  string
-	quitting bool
-}
-
-func (m spinnerModel) Init() tea.Cmd {
-	return m.spinner.Tick
-}
-
-func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	switch msg := msg.(type) {
-	case tea.KeyPressMsg:
-		m.quitting = true
-		return m, tea.Quit
-	case spinner.TickMsg:
-		var cmd tea.Cmd
-		m.spinner, cmd = m.spinner.Update(msg)
-		return m, cmd
-	case quitMsg:
-		m.quitting = true
-		return m, tea.Quit
-	default:
-		return m, nil
-	}
-}
-
-func (m spinnerModel) View() string {
-	if m.quitting {
-		return ""
-	}
-	return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
-}
-
-// quitMsg is sent when we want to quit the spinner
-type quitMsg struct{}
-
 // NewSpinner creates a new spinner with the given message
-func NewSpinner(message string) *Spinner {
-	s := spinner.New()
-	s.Spinner = spinner.Dot
-	s.Style = s.Style.Foreground(s.Style.GetForeground())
-
-	ctx, cancel := context.WithCancel(context.Background())
-
-	model := spinnerModel{
-		spinner: s,
-		message: message,
-	}
+func NewSpinner(ctx context.Context, message string) *Spinner {
+	t := styles.CurrentTheme()
+	model := anim.New(anim.Settings{
+		Size:        10,
+		Label:       message,
+		LabelColor:  t.FgBase,
+		GradColorA:  t.Primary,
+		GradColorB:  t.Secondary,
+		CycleColors: true,
+	})
 
-	prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
+	prog := tea.NewProgram(
+		model,
+		tea.WithInput(nil),
+		tea.WithOutput(os.Stderr),
+		tea.WithContext(ctx),
+		tea.WithoutCatchPanics(),
+	)
 
 	return &Spinner{
-		model:  s,
-		done:   make(chan struct{}),
-		prog:   prog,
-		ctx:    ctx,
-		cancel: cancel,
+		prog: prog,
+		done: make(chan struct{}, 1),
 	}
 }
 
 // Start begins the spinner animation
 func (s *Spinner) Start() {
 	go func() {
-		defer close(s.done)
-		go func() {
-			<-s.ctx.Done()
-			s.prog.Send(quitMsg{})
-		}()
 		_, err := s.prog.Run()
-		if err != nil {
+		// ensures line is cleared
+		fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
+		if err != nil && !errors.Is(err, context.Canceled) {
 			fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
 		}
+		close(s.done)
 	}()
 }
 
 // Stop ends the spinner animation
 func (s *Spinner) Stop() {
-	s.cancel()
+	s.prog.Quit()
 	<-s.done
 }
  
  
  
    
    @@ -80,6 +80,10 @@ const (
 	maxAttachments = 5
 )
 
+type openEditorMsg struct {
+	Text string
+}
+
 func (m *editorCmp) openEditor(value string) tea.Cmd {
 	editor := os.Getenv("EDITOR")
 	if editor == "" {
@@ -115,11 +119,8 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
 			return util.ReportWarn("Message is empty")
 		}
 		os.Remove(tmpfile.Name())
-		attachments := m.attachments
-		m.attachments = nil
-		return chat.SendMsg{
-			Text:        string(content),
-			Attachments: attachments,
+		return openEditorMsg{
+			Text: strings.TrimSpace(string(content)),
 		}
 	})
 }
@@ -189,6 +190,9 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m.completionsStartIndex = 0
 			return m, nil
 		}
+	case openEditorMsg:
+		m.textarea.SetValue(msg.Text)
+		m.textarea.MoveToEnd()
 	case tea.KeyPressMsg:
 		switch {
 		// Completions
@@ -402,11 +406,11 @@ func New(app *app.App) Editor {
 	t := styles.CurrentTheme()
 	ta := textarea.New()
 	ta.SetStyles(t.S().TextArea)
-	ta.SetPromptFunc(4, func(lineIndex int, focused bool) string {
-		if lineIndex == 0 {
+	ta.SetPromptFunc(4, func(info textarea.PromptInfo) string {
+		if info.LineNumber == 0 {
 			return "  > "
 		}
-		if focused {
+		if info.Focused {
 			return t.S().Base.Foreground(t.GreenDark).Render("::: ")
 		} else {
 			return t.S().Muted.Render("::: ")
  
  
  
    
    @@ -133,6 +133,13 @@ type LineInfo struct {
 	CharOffset int
 }
 
+// PromptInfo is a struct that can be used to store information about the
+// prompt.
+type PromptInfo struct {
+	LineNumber int
+	Focused    bool
+}
+
 // CursorStyle is the style for real and virtual cursors.
 type CursorStyle struct {
 	// Style styles the cursor block.
@@ -287,7 +294,7 @@ type Model struct {
 
 	// If promptFunc is set, it replaces Prompt as a generator for
 	// prompt strings at the beginning of each line.
-	promptFunc func(line int, focused bool) string
+	promptFunc func(PromptInfo) string
 
 	// promptWidth is the width of the prompt.
 	promptWidth int
@@ -983,14 +990,14 @@ func (m Model) Width() int {
 	return m.width
 }
 
-// moveToBegin moves the cursor to the beginning of the input.
-func (m *Model) moveToBegin() {
+// MoveToBegin moves the cursor to the beginning of the input.
+func (m *Model) MoveToBegin() {
 	m.row = 0
 	m.SetCursorColumn(0)
 }
 
-// moveToEnd moves the cursor to the end of the input.
-func (m *Model) moveToEnd() {
+// MoveToEnd moves the cursor to the end of the input.
+func (m *Model) MoveToEnd() {
 	m.row = len(m.value) - 1
 	m.SetCursorColumn(len(m.value[m.row]))
 }
@@ -1052,7 +1059,7 @@ func (m *Model) SetWidth(w int) {
 // promptWidth, it will be padded to the left. If it returns a prompt that is
 // longer, display artifacts may occur; the caller is responsible for computing
 // an adequate promptWidth.
-func (m *Model) SetPromptFunc(promptWidth int, fn func(lineIndex int, focused bool) string) {
+func (m *Model) SetPromptFunc(promptWidth int, fn func(PromptInfo) string) {
 	m.promptFunc = fn
 	m.promptWidth = promptWidth
 }
@@ -1170,9 +1177,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 		case key.Matches(msg, m.KeyMap.WordBackward):
 			m.wordLeft()
 		case key.Matches(msg, m.KeyMap.InputBegin):
-			m.moveToBegin()
+			m.MoveToBegin()
 		case key.Matches(msg, m.KeyMap.InputEnd):
-			m.moveToEnd()
+			m.MoveToEnd()
 		case key.Matches(msg, m.KeyMap.LowercaseWordForward):
 			m.lowercaseRight()
 		case key.Matches(msg, m.KeyMap.UppercaseWordForward):
@@ -1320,7 +1327,10 @@ func (m Model) promptView(displayLine int) (prompt string) {
 	if m.promptFunc == nil {
 		return prompt
 	}
-	prompt = m.promptFunc(displayLine, m.focus)
+	prompt = m.promptFunc(PromptInfo{
+		LineNumber: displayLine,
+		Focused:    m.focus,
+	})
 	width := lipgloss.Width(prompt)
 	if width < m.promptWidth {
 		prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
  
  
  
    
    @@ -242,7 +242,7 @@ github.com/bmatcuk/doublestar/v4
 github.com/charlievieth/fastwalk
 github.com/charlievieth/fastwalk/internal/dirent
 github.com/charlievieth/fastwalk/internal/fmtdirent
-# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
+# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
 ## explicit; go 1.23.0
 github.com/charmbracelet/bubbles/v2/cursor
 github.com/charmbracelet/bubbles/v2/filepicker