onboarding.go

  1package model
  2
  3import (
  4	"fmt"
  5	"log/slog"
  6	"strings"
  7
  8	"charm.land/bubbles/v2/key"
  9	tea "charm.land/bubbletea/v2"
 10	"charm.land/lipgloss/v2"
 11
 12	"github.com/charmbracelet/crush/internal/agent"
 13	"github.com/charmbracelet/crush/internal/config"
 14	"github.com/charmbracelet/crush/internal/home"
 15	"github.com/charmbracelet/crush/internal/ui/common"
 16	"github.com/charmbracelet/crush/internal/ui/util"
 17)
 18
 19// markProjectInitialized marks the current project as initialized in the config.
 20func (m *UI) markProjectInitialized() tea.Msg {
 21	// TODO: handle error so we show it in the tui footer
 22	err := config.MarkProjectInitialized(m.com.Config())
 23	if err != nil {
 24		slog.Error(err.Error())
 25	}
 26	return nil
 27}
 28
 29// updateInitializeView handles keyboard input for the project initialization prompt.
 30func (m *UI) updateInitializeView(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
 31	switch {
 32	case key.Matches(msg, m.keyMap.Initialize.Enter):
 33		if m.onboarding.yesInitializeSelected {
 34			cmds = append(cmds, m.initializeProject())
 35		} else {
 36			cmds = append(cmds, m.skipInitializeProject())
 37		}
 38	case key.Matches(msg, m.keyMap.Initialize.Switch):
 39		m.onboarding.yesInitializeSelected = !m.onboarding.yesInitializeSelected
 40	case key.Matches(msg, m.keyMap.Initialize.Yes):
 41		cmds = append(cmds, m.initializeProject())
 42	case key.Matches(msg, m.keyMap.Initialize.No):
 43		cmds = append(cmds, m.skipInitializeProject())
 44	}
 45	return cmds
 46}
 47
 48// initializeProject starts project initialization and transitions to the landing view.
 49func (m *UI) initializeProject() tea.Cmd {
 50	// clear the session
 51	var cmds []tea.Cmd
 52	if cmd := m.newSession(); cmd != nil {
 53		cmds = append(cmds, cmd)
 54	}
 55	cfg := m.com.Config()
 56
 57	initialize := func() tea.Msg {
 58		initPrompt, err := agent.InitializePrompt(*cfg)
 59		if err != nil {
 60			return util.InfoMsg{
 61				Type: util.InfoTypeError,
 62				Msg:  fmt.Sprintf("Failed to initialize project: %v", err),
 63			}
 64		}
 65		return sendMessageMsg{Content: initPrompt}
 66	}
 67	// Mark the project as initialized
 68	cmds = append(cmds, initialize, m.markProjectInitialized)
 69
 70	return tea.Sequence(cmds...)
 71}
 72
 73// skipInitializeProject skips project initialization and transitions to the landing view.
 74func (m *UI) skipInitializeProject() tea.Cmd {
 75	// TODO: initialize the project
 76	m.setState(uiLanding, uiFocusEditor)
 77	// mark the project as initialized
 78	return m.markProjectInitialized
 79}
 80
 81// initializeView renders the project initialization prompt with Yes/No buttons.
 82func (m *UI) initializeView() string {
 83	cfg := m.com.Config()
 84	s := m.com.Styles.Initialize
 85	cwd := home.Short(cfg.WorkingDir())
 86	initFile := cfg.Options.InitializeAs
 87
 88	header := s.Header.Render("Would you like to initialize this project?")
 89	path := s.Accent.PaddingLeft(2).Render(cwd)
 90	desc := s.Content.Render(fmt.Sprintf("When I initialize your codebase I examine the project and put the result into an %s file which serves as general context.", initFile))
 91	hint := s.Content.Render("You can also initialize anytime via ") + s.Accent.Render("ctrl+p") + s.Content.Render(".")
 92	prompt := s.Content.Render("Would you like to initialize now?")
 93
 94	buttons := common.ButtonGroup(m.com.Styles, []common.ButtonOpts{
 95		{Text: "Yep!", Selected: m.onboarding.yesInitializeSelected},
 96		{Text: "Nope", Selected: !m.onboarding.yesInitializeSelected},
 97	}, " ")
 98
 99	// max width 60 so the text is compact
100	width := min(m.layout.main.Dx(), 60)
101
102	return lipgloss.NewStyle().
103		Width(width).
104		Height(m.layout.main.Dy()).
105		PaddingBottom(1).
106		AlignVertical(lipgloss.Bottom).
107		Render(strings.Join(
108			[]string{
109				header,
110				path,
111				desc,
112				hint,
113				prompt,
114				buttons,
115			},
116			"\n\n",
117		))
118}