Merge remote-tracking branch 'origin/main' into onboarding

This commit is contained in:
Kujtim Hoxha
2025-07-10 21:06:15 +02:00
7 changed files with 72 additions and 95 deletions

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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() != "" {

View File

@@ -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())
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,
})
ctx, cancel := context.WithCancel(context.Background())
model := spinnerModel{
spinner: s,
message: message,
}
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
}

View File

@@ -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("::: ")

View File

@@ -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)

2
vendor/modules.txt vendored
View File

@@ -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