@@ -1,9 +1,16 @@
1package splash
1package splash
4 "github.com/charmbracelet/bubbles/v2/key"
7 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/fur/provider"
11+ "github.com/charmbracelet/crush/internal/tui/components/chat"
12+ "github.com/charmbracelet/crush/internal/tui/components/completions"
13+ "github.com/charmbracelet/crush/internal/tui/components/core"
7 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
14 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
8 "github.com/charmbracelet/crush/internal/tui/components/core/list"
15 "github.com/charmbracelet/crush/internal/tui/components/core/list"
9 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
16 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
@@ -43,6 +50,7 @@ type splashCmp struct {
43 state SplashScreenState
50 state SplashScreenState
44 modelList *models.ModelListComponent
51 modelList *models.ModelListComponent
45 cursorRow, cursorCol int
52 cursorRow, cursorCol int
53+ selectedNo bool // true if "No" button is selected in initialize state
48func New() Splash {
56func New() Splash {
@@ -67,6 +75,7 @@ func New() Splash {
67 state: SplashScreenStateOnboarding,
75 state: SplashScreenStateOnboarding,
68 logoRendered: "",
76 logoRendered: "",
69 modelList: modelList,
77 modelList: modelList,
@@ -83,8 +92,28 @@ func (s *splashCmp) Init() tea.Cmd {
84 s.state = SplashScreenStateReady
93 s.state = SplashScreenStateReady
96+ providers, err := config.Providers()
98+ return util.ReportError(err)
100+ filteredProviders := []provider.Provider{}
101+ simpleProviders := []string{
108+ for _, p := range providers {
109+ if slices.Contains(simpleProviders, string(p.ID)) {
110+ filteredProviders = append(filteredProviders, p)
113+ s.modelList.SetProviders(filteredProviders)
114+ return s.modelList.Init()
87− return s.modelList.Init()
116+ return nil
90// SetSize implements SplashPage.
119// SetSize implements SplashPage.
@@ -107,15 +136,157 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107 return s, s.SetSize(msg.Width, msg.Height)
136 return s, s.SetSize(msg.Width, msg.Height)
108 case tea.KeyPressMsg:
137 case tea.KeyPressMsg:
139+ case key.Matches(msg, s.keyMap.Select):
140+ if s.state == SplashScreenStateOnboarding {
141+ modelInx := s.modelList.SelectedIndex()
142+ items := s.modelList.Items()
143+ selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
144+ if s.isProviderConfigured(string(selectedItem.Provider.ID)) {
145+ cmd := s.setPreferredModel(selectedItem)
146+ s.state = SplashScreenStateReady
147+ if b, err := config.ProjectNeedsInitialization(); err != nil {
148+ return s, tea.Batch(cmd, util.ReportError(err))
150+ s.state = SplashScreenStateInitialize
153+ s.state = SplashScreenStateReady
154+ return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
157+ } else if s.state == SplashScreenStateInitialize {
158+ return s, s.initializeProject()
160+ case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
161+ if s.state == SplashScreenStateInitialize {
162+ s.selectedNo = !s.selectedNo
165+ case key.Matches(msg, s.keyMap.Yes):
166+ if s.state == SplashScreenStateInitialize {
167+ return s, s.initializeProject()
169+ case key.Matches(msg, s.keyMap.No):
170+ if s.state == SplashScreenStateInitialize {
171+ s.state = SplashScreenStateReady
172+ return s, util.CmdHandler(OnboardingCompleteMsg{})
175+ if s.state == SplashScreenStateOnboarding {
111− u, cmd := s.modelList.Update(msg)
176+ u, cmd := s.modelList.Update(msg)
112− s.modelList = u
177+ s.modelList = u
113− return s, cmd
178+ return s, cmd
116 return s, nil
182 return s, nil
185+func (s *splashCmp) initializeProject() tea.Cmd {
186+ s.state = SplashScreenStateReady
187+ prompt := `Please analyze this codebase and create a CRUSH.md file containing:
188+1. Build/lint/test commands - especially for running a single test
189+2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
191+The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
192+If there's already a CRUSH.md, improve it.
193+If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
194+Add the .crush directory to the .gitignore file if it's not already there.`
196+ // Mark the project as initialized
197+ if err := config.MarkProjectInitialized(); err != nil {
198+ return util.ReportError(err)
202+ cmds = append(cmds, util.CmdHandler(OnboardingCompleteMsg{}))
205+ util.CmdHandler(chat.SessionClearedMsg{}),
206+ util.CmdHandler(chat.SendMsg{
211+ return tea.Sequence(cmds...)
214+func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
216+ model := cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
218+ return util.ReportError(fmt.Errorf("model %s not found for provider %s", selectedItem.Model.ID, selectedItem.Provider.ID))
221+ selectedModel := config.SelectedModel{
222+ Model: selectedItem.Model.ID,
223+ Provider: string(selectedItem.Provider.ID),
224+ ReasoningEffort: model.DefaultReasoningEffort,
225+ MaxTokens: model.DefaultMaxTokens,
228+ err := cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
230+ return util.ReportError(err)
233+ // Now lets automatically setup the small model
234+ knownProvider, err := s.getProvider(selectedItem.Provider.ID)
236+ return util.ReportError(err)
238+ if knownProvider == nil {
239+ // for local provider we just use the same model
240+ err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
242+ return util.ReportError(err)
245+ smallModel := knownProvider.DefaultSmallModelID
246+ model := cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
247+ // should never happen
249+ err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
251+ return util.ReportError(err)
255+ smallSelectedModel := config.SelectedModel{
257+ Provider: string(selectedItem.Provider.ID),
258+ ReasoningEffort: model.DefaultReasoningEffort,
259+ MaxTokens: model.DefaultMaxTokens,
261+ err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
263+ return util.ReportError(err)
269+func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
270+ providers, err := config.Providers()
274+ for _, p := range providers {
275+ if p.ID == providerID {
282+func (s *splashCmp) isProviderConfigured(providerID string) bool {
284+ if _, ok := cfg.Providers[providerID]; ok {
119// View implements SplashPage.
290// View implements SplashPage.
120func (s *splashCmp) View() tea.View {
291func (s *splashCmp) View() tea.View {
121 t := styles.CurrentTheme()
292 t := styles.CurrentTheme()
@@ -141,6 +312,56 @@ func (s *splashCmp) View() tea.View {
141 s.logoRendered,
312 s.logoRendered,
142 modelSelector,
313 modelSelector,
315+ case SplashScreenStateInitialize:
316+ t := styles.CurrentTheme()
318+ titleStyle := t.S().Base.Foreground(t.FgBase)
319+ bodyStyle := t.S().Base.Foreground(t.FgMuted)
320+ shortcutStyle := t.S().Base.Foreground(t.Success)
322+ initText := lipgloss.JoinVertical(
324+ titleStyle.Render("Would you like to initialize this project?"),
326+ bodyStyle.Render("When I initialize your codebase I examine the project and put the"),
327+ bodyStyle.Render("result into a CRUSH.md file which serves as general context."),
329+ bodyStyle.Render("You can also initialize anytime via ")+shortcutStyle.Render("ctrl+p")+bodyStyle.Render("."),
331+ bodyStyle.Render("Would you like to initialize now?"),
334+ yesButton := core.SelectableButton(core.ButtonOpts{
337+ Selected: !s.selectedNo,
340+ noButton := core.SelectableButton(core.ButtonOpts{
343+ Selected: s.selectedNo,
346+ buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, " ", noButton)
348+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
350+ initContent := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
351+ lipgloss.JoinVertical(
359+ content = lipgloss.JoinVertical(
145 // Show just the logo for other states
366 // Show just the logo for other states
146 content = s.logoRendered
367 content = s.logoRendered
@@ -192,6 +413,14 @@ func (s *splashCmp) Bindings() []key.Binding {
192 s.keyMap.Next,
413 s.keyMap.Next,
193 s.keyMap.Previous,
414 s.keyMap.Previous,
416+ } else if s.state == SplashScreenStateInitialize {
417+ return []key.Binding{
196 return []key.Binding{}
425 return []key.Binding{}