on main help sign in
Merge remote-tracking branch 'origin/main' into onboarding
cdb0bdaKujtim Hoxha
go.mod+1−1
@@ -16,7 +16,7 @@ require (
16 github.com/aymanbagabas/go-udiff v0.3.1
16 github.com/aymanbagabas/go-udiff v0.3.1
17 github.com/bmatcuk/doublestar/v4 v4.8.1
17 github.com/bmatcuk/doublestar/v4 v4.8.1
18 github.com/charlievieth/fastwalk v1.0.11
18 github.com/charlievieth/fastwalk v1.0.11
19− github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
19+ github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
20 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1
20 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1
21 github.com/charmbracelet/fang v0.1.0
21 github.com/charmbracelet/fang v0.1.0
22 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
22 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
go.sum+2−2
@@ -68,8 +68,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
68github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
68github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
69github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
69github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8=
70github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
70github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
71−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
71+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 h1:CkMS9Ah9ac1Ego5JDC5NJyZyAAqu23Z+O0yDwsa3IxM=
72−github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
72+github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
73github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595 h1:wLMjzOqrwoM7Em9UR9sGbn4375G8WuxcwFB3kjZiqHo=
73github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595 h1:wLMjzOqrwoM7Em9UR9sGbn4375G8WuxcwFB3kjZiqHo=
74github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595/go.mod h1:+Tl7rePElw6OKt382t04zXwtPFoPXxAaJzNrYmtsLds=
74github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595/go.mod h1:+Tl7rePElw6OKt382t04zXwtPFoPXxAaJzNrYmtsLds=
75github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
75github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
internal/app/app.go+7−6
@@ -98,7 +98,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
98 // Start spinner if not in quiet mode
98 // Start spinner if not in quiet mode
99 var spinner *format.Spinner
99 var spinner *format.Spinner
100 if !quiet {
100 if !quiet {
101− spinner = format.NewSpinner("Thinking...")
101+ spinner = format.NewSpinner(ctx, "Generating")
102 spinner.Start()
102 spinner.Start()
103 defer spinner.Stop()
103 defer spinner.Stop()
104 }
104 }
@@ -129,6 +129,12 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
129 }
129 }
130
130
131 result := <-done
131 result := <-done
132+
133+ // Stop spinner before printing output
134+ if !quiet && spinner != nil {
135+ spinner.Stop()
136+ }
137+
132 if result.Error != nil {
138 if result.Error != nil {
133 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
139 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
134 slog.Info("Agent processing cancelled", "session_id", sess.ID)
140 slog.Info("Agent processing cancelled", "session_id", sess.ID)
@@ -137,11 +143,6 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
137 return fmt.Errorf("agent processing failed: %w", result.Error)
143 return fmt.Errorf("agent processing failed: %w", result.Error)
138 }
144 }
139
145
140− // Stop spinner before printing output
141− if !quiet && spinner != nil {
142− spinner.Stop()
143− }
144−
145 // Get the text content from the response
146 // Get the text content from the response
146 content := "No content available"
147 content := "No content available"
147 if result.Message.Content().String() != "" {
148 if result.Message.Content().String() != "" {
internal/format/spinner.go+30−68
@@ -2,101 +2,63 @@ package format
2
2
3import (
3import (
4 "context"
4 "context"
5+ "errors"
5 "fmt"
6 "fmt"
6 "os"
7 "os"
7
8
8− "github.com/charmbracelet/bubbles/v2/spinner"
9 tea "github.com/charmbracelet/bubbletea/v2"
9 tea "github.com/charmbracelet/bubbletea/v2"
10+ "github.com/charmbracelet/crush/internal/tui/components/anim"
11+ "github.com/charmbracelet/crush/internal/tui/styles"
12+ "github.com/charmbracelet/x/ansi"
10)
13)
11
14
12// Spinner wraps the bubbles spinner for non-interactive mode
15// Spinner wraps the bubbles spinner for non-interactive mode
13type Spinner struct {
16type Spinner struct {
14− model spinner.Model
15− done chan struct{}
17+ done chan struct{}
16− prog *tea.Program
18+ prog *tea.Program
17− ctx context.Context
18− cancel context.CancelFunc
19}
19}
20
20
21−// spinnerModel is the tea.Model for the spinner
22−type spinnerModel struct {
23− spinner spinner.Model
24− message string
25− quitting bool
26−}
27−
28−func (m spinnerModel) Init() tea.Cmd {
29− return m.spinner.Tick
30−}
31−
32−func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
33− switch msg := msg.(type) {
34− case tea.KeyPressMsg:
35− m.quitting = true
36− return m, tea.Quit
37− case spinner.TickMsg:
38− var cmd tea.Cmd
39− m.spinner, cmd = m.spinner.Update(msg)
40− return m, cmd
41− case quitMsg:
42− m.quitting = true
43− return m, tea.Quit
44− default:
45− return m, nil
46− }
47−}
48−
49−func (m spinnerModel) View() string {
50− if m.quitting {
51− return ""
52− }
53− return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
54−}
55−
56−// quitMsg is sent when we want to quit the spinner
57−type quitMsg struct{}
58−
59// NewSpinner creates a new spinner with the given message
21// NewSpinner creates a new spinner with the given message
60−func NewSpinner(message string) *Spinner {
22+func NewSpinner(ctx context.Context, message string) *Spinner {
61− s := spinner.New()
23+ t := styles.CurrentTheme()
62− s.Spinner = spinner.Dot
24+ model := anim.New(anim.Settings{
63− s.Style = s.Style.Foreground(s.Style.GetForeground())
25+ Size: 10,
64−
26+ Label: message,
65− ctx, cancel := context.WithCancel(context.Background())
27+ LabelColor: t.FgBase,
66−
28+ GradColorA: t.Primary,
67− model := spinnerModel{
29+ GradColorB: t.Secondary,
68− spinner: s,
30+ CycleColors: true,
69− message: message,
70− }
31+ })
71
32
33+ prog := tea.NewProgram(
34+ model,
35+ tea.WithInput(nil),
72− prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
36+ tea.WithOutput(os.Stderr),
37+ tea.WithContext(ctx),
38+ tea.WithoutCatchPanics(),
39+ )
73
40
74 return &Spinner{
41 return &Spinner{
75− model: s,
42+ prog: prog,
76− done: make(chan struct{}),
43+ done: make(chan struct{}, 1),
77− prog: prog,
78− ctx: ctx,
79− cancel: cancel,
80 }
44 }
81}
45}
82
46
83// Start begins the spinner animation
47// Start begins the spinner animation
84func (s *Spinner) Start() {
48func (s *Spinner) Start() {
85 go func() {
49 go func() {
86− defer close(s.done)
87− go func() {
88− <-s.ctx.Done()
89− s.prog.Send(quitMsg{})
90− }()
91 _, err := s.prog.Run()
50 _, err := s.prog.Run()
92− if err != nil {
51+ // ensures line is cleared
52+ fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
53+ if err != nil && !errors.Is(err, context.Canceled) {
93 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
54 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
94 }
55 }
56+ close(s.done)
95 }()
57 }()
96}
58}
97
59
98// Stop ends the spinner animation
60// Stop ends the spinner animation
99func (s *Spinner) Stop() {
61func (s *Spinner) Stop() {
100− s.cancel()
62+ s.prog.Quit()
101 <-s.done
63 <-s.done
102}
64}
internal/tui/components/chat/editor/editor.go+12−8
@@ -80,6 +80,10 @@ const (
80 maxAttachments = 5
80 maxAttachments = 5
81)
81)
82
82
83+type openEditorMsg struct {
84+ Text string
85+}
86+
83func (m *editorCmp) openEditor(value string) tea.Cmd {
87func (m *editorCmp) openEditor(value string) tea.Cmd {
84 editor := os.Getenv("EDITOR")
88 editor := os.Getenv("EDITOR")
85 if editor == "" {
89 if editor == "" {
@@ -115,11 +119,8 @@ func (m *editorCmp) openEditor(value string) tea.Cmd {
115 return util.ReportWarn("Message is empty")
119 return util.ReportWarn("Message is empty")
116 }
120 }
117 os.Remove(tmpfile.Name())
121 os.Remove(tmpfile.Name())
118− attachments := m.attachments
122+ return openEditorMsg{
119− m.attachments = nil
120− return chat.SendMsg{
121− Text: string(content),
123+ Text: strings.TrimSpace(string(content)),
122− Attachments: attachments,
123 }
124 }
124 })
125 })
125}
126}
@@ -189,6 +190,9 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
189 m.completionsStartIndex = 0
190 m.completionsStartIndex = 0
190 return m, nil
191 return m, nil
191 }
192 }
193+ case openEditorMsg:
194+ m.textarea.SetValue(msg.Text)
195+ m.textarea.MoveToEnd()
192 case tea.KeyPressMsg:
196 case tea.KeyPressMsg:
193 switch {
197 switch {
194 // Completions
198 // Completions
@@ -402,11 +406,11 @@ func New(app *app.App) Editor {
402 t := styles.CurrentTheme()
406 t := styles.CurrentTheme()
403 ta := textarea.New()
407 ta := textarea.New()
404 ta.SetStyles(t.S().TextArea)
408 ta.SetStyles(t.S().TextArea)
405− ta.SetPromptFunc(4, func(lineIndex int, focused bool) string {
409+ ta.SetPromptFunc(4, func(info textarea.PromptInfo) string {
406− if lineIndex == 0 {
410+ if info.LineNumber == 0 {
407 return " > "
411 return " > "
408 }
412 }
409− if focused {
413+ if info.Focused {
410 return t.S().Base.Foreground(t.GreenDark).Render("::: ")
414 return t.S().Base.Foreground(t.GreenDark).Render("::: ")
411 } else {
415 } else {
412 return t.S().Muted.Render("::: ")
416 return t.S().Muted.Render("::: ")
vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go+19−9
@@ -133,6 +133,13 @@ type LineInfo struct {
133 CharOffset int
133 CharOffset int
134}
134}
135
135
136+// PromptInfo is a struct that can be used to store information about the
137+// prompt.
138+type PromptInfo struct {
139+ LineNumber int
140+ Focused bool
141+}
142+
136// CursorStyle is the style for real and virtual cursors.
143// CursorStyle is the style for real and virtual cursors.
137type CursorStyle struct {
144type CursorStyle struct {
138 // Style styles the cursor block.
145 // Style styles the cursor block.
@@ -287,7 +294,7 @@ type Model struct {
287
294
288 // If promptFunc is set, it replaces Prompt as a generator for
295 // If promptFunc is set, it replaces Prompt as a generator for
289 // prompt strings at the beginning of each line.
296 // prompt strings at the beginning of each line.
290− promptFunc func(line int, focused bool) string
297+ promptFunc func(PromptInfo) string
291
298
292 // promptWidth is the width of the prompt.
299 // promptWidth is the width of the prompt.
293 promptWidth int
300 promptWidth int
@@ -983,14 +990,14 @@ func (m Model) Width() int {
983 return m.width
990 return m.width
984}
991}
985
992
986−// moveToBegin moves the cursor to the beginning of the input.
993+// MoveToBegin moves the cursor to the beginning of the input.
987−func (m *Model) moveToBegin() {
994+func (m *Model) MoveToBegin() {
988 m.row = 0
995 m.row = 0
989 m.SetCursorColumn(0)
996 m.SetCursorColumn(0)
990}
997}
991
998
992−// moveToEnd moves the cursor to the end of the input.
999+// MoveToEnd moves the cursor to the end of the input.
993−func (m *Model) moveToEnd() {
1000+func (m *Model) MoveToEnd() {
994 m.row = len(m.value) - 1
1001 m.row = len(m.value) - 1
995 m.SetCursorColumn(len(m.value[m.row]))
1002 m.SetCursorColumn(len(m.value[m.row]))
996}
1003}
@@ -1052,7 +1059,7 @@ func (m *Model) SetWidth(w int) {
1052// promptWidth, it will be padded to the left. If it returns a prompt that is
1059// promptWidth, it will be padded to the left. If it returns a prompt that is
1053// longer, display artifacts may occur; the caller is responsible for computing
1060// longer, display artifacts may occur; the caller is responsible for computing
1054// an adequate promptWidth.
1061// an adequate promptWidth.
1055−func (m *Model) SetPromptFunc(promptWidth int, fn func(lineIndex int, focused bool) string) {
1062+func (m *Model) SetPromptFunc(promptWidth int, fn func(PromptInfo) string) {
1056 m.promptFunc = fn
1063 m.promptFunc = fn
1057 m.promptWidth = promptWidth
1064 m.promptWidth = promptWidth
1058}
1065}
@@ -1170,9 +1177,9 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
1170 case key.Matches(msg, m.KeyMap.WordBackward):
1177 case key.Matches(msg, m.KeyMap.WordBackward):
1171 m.wordLeft()
1178 m.wordLeft()
1172 case key.Matches(msg, m.KeyMap.InputBegin):
1179 case key.Matches(msg, m.KeyMap.InputBegin):
1173− m.moveToBegin()
1180+ m.MoveToBegin()
1174 case key.Matches(msg, m.KeyMap.InputEnd):
1181 case key.Matches(msg, m.KeyMap.InputEnd):
1175− m.moveToEnd()
1182+ m.MoveToEnd()
1176 case key.Matches(msg, m.KeyMap.LowercaseWordForward):
1183 case key.Matches(msg, m.KeyMap.LowercaseWordForward):
1177 m.lowercaseRight()
1184 m.lowercaseRight()
1178 case key.Matches(msg, m.KeyMap.UppercaseWordForward):
1185 case key.Matches(msg, m.KeyMap.UppercaseWordForward):
@@ -1320,7 +1327,10 @@ func (m Model) promptView(displayLine int) (prompt string) {
1320 if m.promptFunc == nil {
1327 if m.promptFunc == nil {
1321 return prompt
1328 return prompt
1322 }
1329 }
1323− prompt = m.promptFunc(displayLine, m.focus)
1330+ prompt = m.promptFunc(PromptInfo{
1331+ LineNumber: displayLine,
1332+ Focused: m.focus,
1333+ })
1324 width := lipgloss.Width(prompt)
1334 width := lipgloss.Width(prompt)
1325 if width < m.promptWidth {
1335 if width < m.promptWidth {
1326 prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
1336 prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
vendor/modules.txt+1−1
@@ -242,7 +242,7 @@ github.com/bmatcuk/doublestar/v4
242github.com/charlievieth/fastwalk
242github.com/charlievieth/fastwalk
243github.com/charlievieth/fastwalk/internal/dirent
243github.com/charlievieth/fastwalk/internal/dirent
244github.com/charlievieth/fastwalk/internal/fmtdirent
244github.com/charlievieth/fastwalk/internal/fmtdirent
245−# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
245+# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198
246## explicit; go 1.23.0
246## explicit; go 1.23.0
247github.com/charmbracelet/bubbles/v2/cursor
247github.com/charmbracelet/bubbles/v2/cursor
248github.com/charmbracelet/bubbles/v2/filepicker
248github.com/charmbracelet/bubbles/v2/filepicker