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{Type: util.InfoTypeError, Msg: err.Error()}
 61		}
 62		return sendMessageMsg{Content: initPrompt}
 63	}
 64	// Mark the project as initialized
 65	cmds = append(cmds, initialize, m.markProjectInitialized)
 66
 67	return tea.Sequence(cmds...)
 68}
 69
 70// skipInitializeProject skips project initialization and transitions to the landing view.
 71func (m *UI) skipInitializeProject() tea.Cmd {
 72	// TODO: initialize the project
 73	m.setState(uiLanding, uiFocusEditor)
 74	// mark the project as initialized
 75	return m.markProjectInitialized
 76}
 77
 78// initializeView renders the project initialization prompt with Yes/No buttons.
 79func (m *UI) initializeView() string {
 80	cfg := m.com.Config()
 81	s := m.com.Styles.Initialize
 82	cwd := home.Short(cfg.WorkingDir())
 83	initFile := cfg.Options.InitializeAs
 84
 85	header := s.Header.Render("Would you like to initialize this project?")
 86	path := s.Accent.PaddingLeft(2).Render(cwd)
 87	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))
 88	hint := s.Content.Render("You can also initialize anytime via ") + s.Accent.Render("ctrl+p") + s.Content.Render(".")
 89	prompt := s.Content.Render("Would you like to initialize now?")
 90
 91	buttons := common.ButtonGroup(m.com.Styles, []common.ButtonOpts{
 92		{Text: "Yep!", Selected: m.onboarding.yesInitializeSelected},
 93		{Text: "Nope", Selected: !m.onboarding.yesInitializeSelected},
 94	}, " ")
 95
 96	// max width 60 so the text is compact
 97	width := min(m.layout.main.Dx(), 60)
 98
 99	return lipgloss.NewStyle().
100		Width(width).
101		Height(m.layout.main.Dy()).
102		PaddingBottom(1).
103		AlignVertical(lipgloss.Bottom).
104		Render(strings.Join(
105			[]string{
106				header,
107				path,
108				desc,
109				hint,
110				prompt,
111				buttons,
112			},
113			"\n\n",
114		))
115}