on main help sign in
wip: init screen and simple flow
commiteeeef60efe72afc5b37757f1cdd4b4799cba8017 authorKujtim Hoxha <kujtimii.h@gmail.com> dateTue 8 Jul 2025, 16:50 (1 year ago) parent2043151
10 files changed+339−349
internal/config/config.go+9−8 internal/config/load.go+34−27 internal/tui/components/chat/messages/messages.go+7 internal/tui/components/chat/splash/keys.go+21−1 internal/tui/components/chat/splash/splash.go+233−4 internal/tui/components/dialogs/init/init.go−214 internal/tui/components/dialogs/init/keys.go−69 internal/tui/components/dialogs/models/list.go+16−7 internal/tui/page/chat/chat.go+18 internal/tui/tui.go+1−19
internal/config/config.go+9−8
@@ -208,7 +208,8 @@ type Config struct {
208208 // TODO: most likely remove this concept when I come back to it
209209 Agents map[string]Agent `json:"-"`
210210 // TODO: find a better way to do this this should probably not be part of the config
211− resolver VariableResolver
211+ resolver VariableResolver
212+ dataConfigDir string `json:"-"`
212213}
213214
214215func (c *Config) WorkingDir() string {
@@ -291,17 +292,17 @@ func (c *Config) Resolve(key string) (string, error) {
291292 return c.resolver.ResolveValue(key)
292293}
293294
294−// TODO: maybe handle this better
295−func UpdatePreferredModel(modelType SelectedModelType, model SelectedModel) error {
296− cfg := Get()
297− cfg.Models[modelType] = model
295+func (c *Config) UpdatePreferredModel(modelType SelectedModelType, model SelectedModel) error {
296+ c.Models[modelType] = model
297+ if err := c.SetConfigField(fmt.Sprintf("models.%s", modelType), model); err != nil {
298+ return fmt.Errorf("failed to update preferred model: %w", err)
299+ }
298300 return nil
299301}
300302
301303func (c *Config) SetConfigField(key string, value any) error {
302− configPath := GlobalConfigData()
303304 // read the data
304− data, err := os.ReadFile(configPath)
305+ data, err := os.ReadFile(c.dataConfigDir)
305306 if err != nil {
306307 if os.IsNotExist(err) {
307308 data = []byte("{}")
@@ -314,7 +315,7 @@ func (c *Config) SetConfigField(key string, value any) error {
314315 if err != nil {
315316 return fmt.Errorf("failed to set config field %s: %w", key, err)
316317 }
317− if err := os.WriteFile(configPath, []byte(newValue), 0o644); err != nil {
318+ if err := os.WriteFile(c.dataConfigDir, []byte(newValue), 0o644); err != nil {
318319 return fmt.Errorf("failed to write config file: %w", err)
319320 }
320321 return nil
internal/config/load.go+34−27
@@ -46,6 +46,8 @@ func Load(workingDir string, debug bool) (*Config, error) {
4646 return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
4747 }
4848
49+ cfg.dataConfigDir = GlobalConfigData()
50+
4951 cfg.setDefaults(workingDir)
5052
5153 if debug {
@@ -367,10 +369,11 @@ func (cfg *Config) defaultModelSelection(knownProviders []provider.Provider) (la
367369}
368370
369371func (cfg *Config) configureSelectedModels(knownProviders []provider.Provider) error {
370− large, small, err := cfg.defaultModelSelection(knownProviders)
372+ defaultLarge, defaultSmall, err := cfg.defaultModelSelection(knownProviders)
371373 if err != nil {
372374 return fmt.Errorf("failed to select default models: %w", err)
373375 }
376+ large, small := defaultLarge, defaultSmall
374377
375378 largeModelSelected, largeModelConfigured := cfg.Models[SelectedModelTypeLarge]
376379 if largeModelConfigured {
@@ -381,18 +384,26 @@ func (cfg *Config) configureSelectedModels(knownProviders []provider.Provider) e
381384 large.Provider = largeModelSelected.Provider
382385 }
383386 model := cfg.GetModel(large.Provider, large.Model)
387+ slog.Info("Configuring selected large model", "provider", large.Provider, "model", large.Model)
388+ slog.Info("MOdel configured", "model", model)
384389 if model == nil {
385− return fmt.Errorf("large model %s not found for provider %s", large.Model, large.Provider)
386− }
387− if largeModelSelected.MaxTokens > 0 {
388− large.MaxTokens = largeModelSelected.MaxTokens
390+ large = defaultLarge
391+ // override the model type to large
392+ err := cfg.UpdatePreferredModel(SelectedModelTypeLarge, large)
393+ if err != nil {
394+ return fmt.Errorf("failed to update preferred large model: %w", err)
395+ }
389396 } else {
390− large.MaxTokens = model.DefaultMaxTokens
391− }
392− if largeModelSelected.ReasoningEffort != "" {
393− large.ReasoningEffort = largeModelSelected.ReasoningEffort
397+ if largeModelSelected.MaxTokens > 0 {
398+ large.MaxTokens = largeModelSelected.MaxTokens
399+ } else {
400+ large.MaxTokens = model.DefaultMaxTokens
401+ }
402+ if largeModelSelected.ReasoningEffort != "" {
403+ large.ReasoningEffort = largeModelSelected.ReasoningEffort
404+ }
405+ large.Think = largeModelSelected.Think
394406 }
395− large.Think = largeModelSelected.Think
396407 }
397408 smallModelSelected, smallModelConfigured := cfg.Models[SelectedModelTypeSmall]
398409 if smallModelConfigured {
@@ -405,25 +416,21 @@ func (cfg *Config) configureSelectedModels(knownProviders []provider.Provider) e
405416
406417 model := cfg.GetModel(small.Provider, small.Model)
407418 if model == nil {
408− return fmt.Errorf("large model %s not found for provider %s", large.Model, large.Provider)
409− }
410− if smallModelSelected.MaxTokens > 0 {
411− small.MaxTokens = smallModelSelected.MaxTokens
419+ small = defaultSmall
420+ // override the model type to small
421+ err := cfg.UpdatePreferredModel(SelectedModelTypeSmall, small)
422+ if err != nil {
423+ return fmt.Errorf("failed to update preferred small model: %w", err)
424+ }
412425 } else {
413− small.MaxTokens = model.DefaultMaxTokens
426+ if smallModelSelected.MaxTokens > 0 {
427+ small.MaxTokens = smallModelSelected.MaxTokens
428+ } else {
429+ small.MaxTokens = model.DefaultMaxTokens
430+ }
431+ small.ReasoningEffort = smallModelSelected.ReasoningEffort
432+ small.Think = smallModelSelected.Think
414433 }
415− small.ReasoningEffort = smallModelSelected.ReasoningEffort
416− small.Think = smallModelSelected.Think
417− }
418−
419− // validate the selected models
420− largeModel := cfg.GetModel(large.Provider, large.Model)
421− if largeModel == nil {
422− return fmt.Errorf("large model %s not found for provider %s", large.Model, large.Provider)
423− }
424− smallModel := cfg.GetModel(small.Provider, small.Model)
425− if smallModel == nil {
426− return fmt.Errorf("small model %s not found for provider %s", small.Model, small.Provider)
427434 }
428435 cfg.Models[SelectedModelTypeLarge] = large
429436 cfg.Models[SelectedModelTypeSmall] = small
internal/tui/components/chat/messages/messages.go+7
@@ -10,6 +10,7 @@ import (
1010 "github.com/charmbracelet/lipgloss/v2"
1111
1212 "github.com/charmbracelet/crush/internal/config"
13+ "github.com/charmbracelet/crush/internal/fur/provider"
1314 "github.com/charmbracelet/crush/internal/message"
1415 "github.com/charmbracelet/crush/internal/tui/components/anim"
1516 "github.com/charmbracelet/crush/internal/tui/components/core"
@@ -296,6 +297,12 @@ func (m *assistantSectionModel) View() tea.View {
296297 infoMsg := t.S().Subtle.Render(duration.String())
297298 icon := t.S().Subtle.Render(styles.ModelIcon)
298299 model := config.Get().GetModel(m.message.Provider, m.message.Model)
300+ if model == nil {
301+ // This means the model is not configured anymore
302+ model = &provider.Model{
303+ Name: "Unknown Model",
304+ }
305+ }
299306 modelFormatted := t.S().Muted.Render(model.Name)
300307 assistant := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
301308 return tea.NewView(
internal/tui/components/chat/splash/keys.go+21−1
@@ -7,7 +7,11 @@ import (
77type KeyMap struct {
88 Select,
99 Next,
10− Previous key.Binding
10+ Previous,
11+ Yes,
12+ No,
13+ Tab,
14+ LeftRight key.Binding
1115}
1216
1317func DefaultKeyMap() KeyMap {
@@ -24,5 +28,21 @@ func DefaultKeyMap() KeyMap {
2428 key.WithKeys("up", "ctrl+p"),
2529 key.WithHelp("↑", "previous item"),
2630 ),
31+ Yes: key.NewBinding(
32+ key.WithKeys("y"),
33+ key.WithHelp("y", "yes"),
34+ ),
35+ No: key.NewBinding(
36+ key.WithKeys("n"),
37+ key.WithHelp("n", "no"),
38+ ),
39+ Tab: key.NewBinding(
40+ key.WithKeys("tab"),
41+ key.WithHelp("tab", "switch"),
42+ ),
43+ LeftRight: key.NewBinding(
44+ key.WithKeys("left", "right"),
45+ key.WithHelp("←/→", "switch"),
46+ ),
2747 }
2848}
internal/tui/components/chat/splash/splash.go+233−4
@@ -1,9 +1,16 @@
11package splash
22
33import (
4+ "fmt"
5+ "slices"
6+
47 "github.com/charmbracelet/bubbles/v2/key"
58 tea "github.com/charmbracelet/bubbletea/v2"
69 "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"
714 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
815 "github.com/charmbracelet/crush/internal/tui/components/core/list"
916 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
@@ -43,6 +50,7 @@ type splashCmp struct {
4350 state SplashScreenState
4451 modelList *models.ModelListComponent
4552 cursorRow, cursorCol int
53+ selectedNo bool // true if "No" button is selected in initialize state
4654}
4755
4856func New() Splash {
@@ -67,6 +75,7 @@ func New() Splash {
6775 state: SplashScreenStateOnboarding,
6876 logoRendered: "",
6977 modelList: modelList,
78+ selectedNo: false,
7079 }
7180}
7281
@@ -83,8 +92,28 @@ func (s *splashCmp) Init() tea.Cmd {
8392 } else {
8493 s.state = SplashScreenStateReady
8594 }
95+ } else {
96+ providers, err := config.Providers()
97+ if err != nil {
98+ return util.ReportError(err)
99+ }
100+ filteredProviders := []provider.Provider{}
101+ simpleProviders := []string{
102+ "anthropic",
103+ "openai",
104+ "gemini",
105+ "xai",
106+ "openrouter",
107+ }
108+ for _, p := range providers {
109+ if slices.Contains(simpleProviders, string(p.ID)) {
110+ filteredProviders = append(filteredProviders, p)
111+ }
112+ }
113+ s.modelList.SetProviders(filteredProviders)
114+ return s.modelList.Init()
86115 }
87− return s.modelList.Init()
116+ return nil
88117}
89118
90119// SetSize implements SplashPage.
@@ -107,15 +136,157 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107136 return s, s.SetSize(msg.Width, msg.Height)
108137 case tea.KeyPressMsg:
109138 switch {
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))
149+ } else if b {
150+ s.state = SplashScreenStateInitialize
151+ return s, cmd
152+ } else {
153+ s.state = SplashScreenStateReady
154+ return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
155+ }
156+ }
157+ } else if s.state == SplashScreenStateInitialize {
158+ return s, s.initializeProject()
159+ }
160+ case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
161+ if s.state == SplashScreenStateInitialize {
162+ s.selectedNo = !s.selectedNo
163+ return s, nil
164+ }
165+ case key.Matches(msg, s.keyMap.Yes):
166+ if s.state == SplashScreenStateInitialize {
167+ return s, s.initializeProject()
168+ }
169+ case key.Matches(msg, s.keyMap.No):
170+ if s.state == SplashScreenStateInitialize {
171+ s.state = SplashScreenStateReady
172+ return s, util.CmdHandler(OnboardingCompleteMsg{})
173+ }
110174 default:
111− u, cmd := s.modelList.Update(msg)
112− s.modelList = u
113− return s, cmd
175+ if s.state == SplashScreenStateOnboarding {
176+ u, cmd := s.modelList.Update(msg)
177+ s.modelList = u
178+ return s, cmd
179+ }
114180 }
115181 }
116182 return s, nil
117183}
118184
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.
190+
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.`
195+
196+ // Mark the project as initialized
197+ if err := config.MarkProjectInitialized(); err != nil {
198+ return util.ReportError(err)
199+ }
200+ var cmds []tea.Cmd
201+
202+ cmds = append(cmds, util.CmdHandler(OnboardingCompleteMsg{}))
203+ if !s.selectedNo {
204+ cmds = append(cmds,
205+ util.CmdHandler(chat.SessionClearedMsg{}),
206+ util.CmdHandler(chat.SendMsg{
207+ Text: prompt,
208+ }),
209+ )
210+ }
211+ return tea.Sequence(cmds...)
212+}
213+
214+func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
215+ cfg := config.Get()
216+ model := cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
217+ if model == nil {
218+ return util.ReportError(fmt.Errorf("model %s not found for provider %s", selectedItem.Model.ID, selectedItem.Provider.ID))
219+ }
220+
221+ selectedModel := config.SelectedModel{
222+ Model: selectedItem.Model.ID,
223+ Provider: string(selectedItem.Provider.ID),
224+ ReasoningEffort: model.DefaultReasoningEffort,
225+ MaxTokens: model.DefaultMaxTokens,
226+ }
227+
228+ err := cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
229+ if err != nil {
230+ return util.ReportError(err)
231+ }
232+
233+ // Now lets automatically setup the small model
234+ knownProvider, err := s.getProvider(selectedItem.Provider.ID)
235+ if err != nil {
236+ return util.ReportError(err)
237+ }
238+ if knownProvider == nil {
239+ // for local provider we just use the same model
240+ err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
241+ if err != nil {
242+ return util.ReportError(err)
243+ }
244+ } else {
245+ smallModel := knownProvider.DefaultSmallModelID
246+ model := cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
247+ // should never happen
248+ if model == nil {
249+ err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
250+ if err != nil {
251+ return util.ReportError(err)
252+ }
253+ return nil
254+ }
255+ smallSelectedModel := config.SelectedModel{
256+ Model: smallModel,
257+ Provider: string(selectedItem.Provider.ID),
258+ ReasoningEffort: model.DefaultReasoningEffort,
259+ MaxTokens: model.DefaultMaxTokens,
260+ }
261+ err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
262+ if err != nil {
263+ return util.ReportError(err)
264+ }
265+ }
266+ return nil
267+}
268+
269+func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
270+ providers, err := config.Providers()
271+ if err != nil {
272+ return nil, err
273+ }
274+ for _, p := range providers {
275+ if p.ID == providerID {
276+ return &p, nil
277+ }
278+ }
279+ return nil, nil
280+}
281+
282+func (s *splashCmp) isProviderConfigured(providerID string) bool {
283+ cfg := config.Get()
284+ if _, ok := cfg.Providers[providerID]; ok {
285+ return true
286+ }
287+ return false
288+}
289+
119290// View implements SplashPage.
120291func (s *splashCmp) View() tea.View {
121292 t := styles.CurrentTheme()
@@ -141,6 +312,56 @@ func (s *splashCmp) View() tea.View {
141312 s.logoRendered,
142313 modelSelector,
143314 )
315+ case SplashScreenStateInitialize:
316+ t := styles.CurrentTheme()
317+
318+ titleStyle := t.S().Base.Foreground(t.FgBase)
319+ bodyStyle := t.S().Base.Foreground(t.FgMuted)
320+ shortcutStyle := t.S().Base.Foreground(t.Success)
321+
322+ initText := lipgloss.JoinVertical(
323+ lipgloss.Left,
324+ titleStyle.Render("Would you like to initialize this project?"),
325+ "",
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."),
328+ "",
329+ bodyStyle.Render("You can also initialize anytime via ")+shortcutStyle.Render("ctrl+p")+bodyStyle.Render("."),
330+ "",
331+ bodyStyle.Render("Would you like to initialize now?"),
332+ )
333+
334+ yesButton := core.SelectableButton(core.ButtonOpts{
335+ Text: "Yep!",
336+ UnderlineIndex: 0,
337+ Selected: !s.selectedNo,
338+ })
339+
340+ noButton := core.SelectableButton(core.ButtonOpts{
341+ Text: "Nope",
342+ UnderlineIndex: 0,
343+ Selected: s.selectedNo,
344+ })
345+
346+ buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, " ", noButton)
347+
348+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
349+
350+ initContent := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
351+ lipgloss.JoinVertical(
352+ lipgloss.Left,
353+ initText,
354+ "",
355+ buttons,
356+ ),
357+ )
358+
359+ content = lipgloss.JoinVertical(
360+ lipgloss.Left,
361+ s.logoRendered,
362+ initContent,
363+ )
364+
144365 default:
145366 // Show just the logo for other states
146367 content = s.logoRendered
@@ -192,6 +413,14 @@ func (s *splashCmp) Bindings() []key.Binding {
192413 s.keyMap.Next,
193414 s.keyMap.Previous,
194415 }
416+ } else if s.state == SplashScreenStateInitialize {
417+ return []key.Binding{
418+ s.keyMap.Select,
419+ s.keyMap.Yes,
420+ s.keyMap.No,
421+ s.keyMap.Tab,
422+ s.keyMap.LeftRight,
423+ }
195424 }
196425 return []key.Binding{}
197426}
internal/tui/components/dialogs/init/init.godeleted−214
@@ -1,214 +0,0 @@
1−package init
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5− tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/lipgloss/v2"
7−
8− "github.com/charmbracelet/crush/internal/config"
9− cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
10− "github.com/charmbracelet/crush/internal/tui/components/core"
11− "github.com/charmbracelet/crush/internal/tui/components/dialogs"
12− "github.com/charmbracelet/crush/internal/tui/styles"
13− "github.com/charmbracelet/crush/internal/tui/util"
14−)
15−
16−const InitDialogID dialogs.DialogID = "init"
17−
18−// InitDialogCmp is a component that asks the user if they want to initialize the project.
19−type InitDialogCmp interface {
20− dialogs.DialogModel
21−}
22−
23−type initDialogCmp struct {
24− wWidth, wHeight int
25− width, height int
26− selected int
27− keyMap KeyMap
28−}
29−
30−// NewInitDialogCmp creates a new InitDialogCmp.
31−func NewInitDialogCmp() InitDialogCmp {
32− return &initDialogCmp{
33− selected: 0,
34− keyMap: DefaultKeyMap(),
35− }
36−}
37−
38−// Init implements tea.Model.
39−func (m *initDialogCmp) Init() tea.Cmd {
40− return nil
41−}
42−
43−// Update implements tea.Model.
44−func (m *initDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
45− switch msg := msg.(type) {
46− case tea.WindowSizeMsg:
47− m.wWidth = msg.Width
48− m.wHeight = msg.Height
49− cmd := m.SetSize()
50− return m, cmd
51− case tea.KeyPressMsg:
52− switch {
53− case key.Matches(msg, m.keyMap.Close):
54− return m, tea.Batch(
55− util.CmdHandler(dialogs.CloseDialogMsg{}),
56− m.handleInitialization(false),
57− )
58− case key.Matches(msg, m.keyMap.ChangeSelection):
59− m.selected = (m.selected + 1) % 2
60− return m, nil
61− case key.Matches(msg, m.keyMap.Select):
62− return m, tea.Batch(
63− util.CmdHandler(dialogs.CloseDialogMsg{}),
64− m.handleInitialization(m.selected == 0),
65− )
66− case key.Matches(msg, m.keyMap.Y):
67− return m, tea.Batch(
68− util.CmdHandler(dialogs.CloseDialogMsg{}),
69− m.handleInitialization(true),
70− )
71− case key.Matches(msg, m.keyMap.N):
72− return m, tea.Batch(
73− util.CmdHandler(dialogs.CloseDialogMsg{}),
74− m.handleInitialization(false),
75− )
76− }
77− }
78− return m, nil
79−}
80−
81−func (m *initDialogCmp) renderButtons() string {
82− t := styles.CurrentTheme()
83− baseStyle := t.S().Base
84−
85− buttons := []core.ButtonOpts{
86− {
87− Text: "Yes",
88− UnderlineIndex: 0, // "Y"
89− Selected: m.selected == 0,
90− },
91− {
92− Text: "No",
93− UnderlineIndex: 0, // "N"
94− Selected: m.selected == 1,
95− },
96− }
97−
98− content := core.SelectableButtons(buttons, " ")
99−
100− return baseStyle.AlignHorizontal(lipgloss.Right).Width(m.width - 4).Render(content)
101−}
102−
103−func (m *initDialogCmp) renderContent() string {
104− t := styles.CurrentTheme()
105− baseStyle := t.S().Base
106−
107− explanation := t.S().Text.
108− Width(m.width - 4).
109− Render("Initialization generates a new CRUSH.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
110−
111− question := t.S().Text.
112− Width(m.width - 4).
113− Render("Would you like to initialize this project?")
114−
115− return baseStyle.Render(lipgloss.JoinVertical(
116− lipgloss.Left,
117− explanation,
118− "",
119− question,
120− ))
121−}
122−
123−func (m *initDialogCmp) render() string {
124− t := styles.CurrentTheme()
125− baseStyle := t.S().Base
126− title := core.Title("Initialize Project", m.width-4)
127−
128− content := m.renderContent()
129− buttons := m.renderButtons()
130−
131− dialogContent := lipgloss.JoinVertical(
132− lipgloss.Top,
133− title,
134− "",
135− content,
136− "",
137− buttons,
138− "",
139− )
140−
141− return baseStyle.
142− Padding(0, 1).
143− Border(lipgloss.RoundedBorder()).
144− BorderForeground(t.BorderFocus).
145− Width(m.width).
146− Render(dialogContent)
147−}
148−
149−// View implements tea.Model.
150−func (m *initDialogCmp) View() tea.View {
151− return tea.NewView(m.render())
152−}
153−
154−// SetSize sets the size of the component.
155−func (m *initDialogCmp) SetSize() tea.Cmd {
156− m.width = min(90, m.wWidth)
157− m.height = min(15, m.wHeight)
158− return nil
159−}
160−
161−// ID implements DialogModel.
162−func (m *initDialogCmp) ID() dialogs.DialogID {
163− return InitDialogID
164−}
165−
166−// Position implements DialogModel.
167−func (m *initDialogCmp) Position() (int, int) {
168− row := (m.wHeight / 2) - (m.height / 2)
169− col := (m.wWidth / 2) - (m.width / 2)
170− return row, col
171−}
172−
173−// handleInitialization handles the initialization logic when the dialog is closed.
174−func (m *initDialogCmp) handleInitialization(initialize bool) tea.Cmd {
175− if initialize {
176− // Run the initialization command
177− prompt := `Please analyze this codebase and create a CRUSH.md file containing:
178−1. Build/lint/test commands - especially for running a single test
179−2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
180−
181−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.
182−If there's already a CRUSH.md, improve it.
183−If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
184−Add the .crush directory to the .gitignore file if it's not already there.`
185−
186− // Mark the project as initialized
187− if err := config.MarkProjectInitialized(); err != nil {
188− return util.ReportError(err)
189− }
190−
191− return tea.Sequence(
192− util.CmdHandler(cmpChat.SessionClearedMsg{}),
193− util.CmdHandler(cmpChat.SendMsg{
194− Text: prompt,
195− }),
196− )
197− } else {
198− // Mark the project as initialized without running the command
199− if err := config.MarkProjectInitialized(); err != nil {
200− return util.ReportError(err)
201− }
202− }
203− return nil
204−}
205−
206−// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
207−type CloseInitDialogMsg struct {
208− Initialize bool
209−}
210−
211−// ShowInitDialogMsg is a message that is sent to show the init dialog.
212−type ShowInitDialogMsg struct {
213− Show bool
214−}
internal/tui/components/dialogs/init/keys.godeleted−69
@@ -1,69 +0,0 @@
1−package init
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5−)
6−
7−type KeyMap struct {
8− ChangeSelection,
9− Select,
10− Y,
11− N,
12− Close key.Binding
13−}
14−
15−func DefaultKeyMap() KeyMap {
16− return KeyMap{
17− ChangeSelection: key.NewBinding(
18− key.WithKeys("tab", "left", "right", "h", "l"),
19− key.WithHelp("tab/←/→", "toggle selection"),
20− ),
21− Select: key.NewBinding(
22− key.WithKeys("enter"),
23− key.WithHelp("enter", "confirm"),
24− ),
25− Y: key.NewBinding(
26− key.WithKeys("y"),
27− key.WithHelp("y", "yes"),
28− ),
29− N: key.NewBinding(
30− key.WithKeys("n"),
31− key.WithHelp("n", "no"),
32− ),
33− Close: key.NewBinding(
34− key.WithKeys("esc"),
35− key.WithHelp("esc", "cancel"),
36− ),
37− }
38−}
39−
40−// KeyBindings implements layout.KeyMapProvider
41−func (k KeyMap) KeyBindings() []key.Binding {
42− return []key.Binding{
43− k.ChangeSelection,
44− k.Select,
45− k.Y,
46− k.N,
47− k.Close,
48− }
49−}
50−
51−// FullHelp implements help.KeyMap.
52−func (k KeyMap) FullHelp() [][]key.Binding {
53− m := [][]key.Binding{}
54− slice := k.KeyBindings()
55− for i := 0; i < len(slice); i += 4 {
56− end := min(i+4, len(slice))
57− m = append(m, slice[i:end])
58− }
59− return m
60−}
61−
62−// ShortHelp implements help.KeyMap.
63−func (k KeyMap) ShortHelp() []key.Binding {
64− return []key.Binding{
65− k.ChangeSelection,
66− k.Select,
67− k.Close,
68− }
69−}
internal/tui/components/dialogs/models/list.go+16−7
@@ -18,6 +18,7 @@ import (
1818type ModelListComponent struct {
1919 list list.ListModel
2020 modelType int
21+ providers []provider.Provider
2122}
2223
2324func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style, inputPlaceholder string) *ModelListComponent {
@@ -36,7 +37,16 @@ func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style, inputP
3637}
3738
3839func (m *ModelListComponent) Init() tea.Cmd {
39− return tea.Batch(m.list.Init(), m.SetModelType(m.modelType))
40+ var cmds []tea.Cmd
41+ if len(m.providers) == 0 {
42+ providers, err := config.Providers()
43+ m.providers = providers
44+ if err != nil {
45+ cmds = append(cmds, util.ReportError(err))
46+ }
47+ }
48+ cmds = append(cmds, m.list.Init(), m.SetModelType(m.modelType))
49+ return tea.Batch(cmds...)
4050}
4151
4252func (m *ModelListComponent) Update(msg tea.Msg) (*ModelListComponent, tea.Cmd) {
@@ -65,11 +75,6 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
6575 t := styles.CurrentTheme()
6676 m.modelType = modelType
6777
68− providers, err := config.Providers()
69− if err != nil {
70− return util.ReportError(err)
71− }
72−
7378 modelItems := []util.Model{}
7479 selectIndex := 0
7580
@@ -144,7 +149,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
144149 }
145150
146151 // Then add the known providers from the predefined list
147− for _, provider := range providers {
152+ for _, provider := range m.providers {
148153 // Skip if we already added this provider as an unknown provider
149154 if addedProviders[string(provider.ID)] {
150155 continue
@@ -187,3 +192,7 @@ func (m *ModelListComponent) GetModelType() int {
187192func (m *ModelListComponent) SetInputPlaceholder(placeholder string) {
188193 m.list.SetFilterPlaceholder(placeholder)
189194}
195+
196+func (m *ModelListComponent) SetProviders(providers []provider.Provider) {
197+ m.providers = providers
198+}
internal/tui/page/chat/chat.go+18
@@ -220,6 +220,14 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
220220 if cmd != nil {
221221 return p, cmd
222222 }
223+ case splash.OnboardingCompleteMsg:
224+ p.state = ChatStateNewMessage
225+ err := p.app.InitCoderAgent()
226+ if err != nil {
227+ return p, util.ReportError(err)
228+ }
229+ p.focusedPane = PanelTypeEditor
230+ return p, p.SetSize(p.width, p.height)
223231 case tea.KeyPressMsg:
224232 switch {
225233 case key.Matches(msg, p.keyMap.NewSession):
@@ -233,6 +241,11 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
233241 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
234242 }
235243 case key.Matches(msg, p.keyMap.Tab):
244+ if p.state == ChatStateOnboarding || p.state == ChatStateInitProject {
245+ u, cmd := p.splash.Update(msg)
246+ p.splash = u.(splash.Splash)
247+ return p, cmd
248+ }
236249 p.changeFocus()
237250 return p, nil
238251 case key.Matches(msg, p.keyMap.Cancel):
@@ -416,6 +429,7 @@ func (p *chatPage) newSession() tea.Cmd {
416429 // Cannot start a new session if we are not in the session state
417430 return nil
418431 }
432+
419433 // blank session
420434 p.session = session.Session{}
421435 p.state = ChatStateNewMessage
@@ -456,8 +470,12 @@ func (p *chatPage) changeFocus() {
456470 switch p.focusedPane {
457471 case PanelTypeChat:
458472 p.focusedPane = PanelTypeEditor
473+ p.editor.Focus()
474+ p.chat.Blur()
459475 case PanelTypeEditor:
460476 p.focusedPane = PanelTypeChat
477+ p.chat.Focus()
478+ p.editor.Blur()
461479 }
462480}
463481
internal/tui/tui.go+1−19
@@ -19,7 +19,6 @@ import (
1919 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
2020 "github.com/charmbracelet/crush/internal/tui/components/dialogs/compact"
2121 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
22− initDialog "github.com/charmbracelet/crush/internal/tui/components/dialogs/init"
2322 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
2423 "github.com/charmbracelet/crush/internal/tui/components/dialogs/permissions"
2524 "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
@@ -65,23 +64,6 @@ func (a appModel) Init() tea.Cmd {
6564 cmd = a.status.Init()
6665 cmds = append(cmds, cmd)
6766
68− // Check if we should show the init dialog
69− cmds = append(cmds, func() tea.Msg {
70− shouldShow, err := config.ProjectNeedsInitialization()
71− if err != nil {
72− return util.InfoMsg{
73− Type: util.InfoTypeError,
74− Msg: "Failed to check init status: " + err.Error(),
75− }
76− }
77− if shouldShow {
78− return dialogs.OpenDialogMsg{
79− Model: initDialog.NewInitDialogCmp(),
80− }
81− }
82− return nil
83− })
84−
8567 return tea.Batch(cmds...)
8668}
8769
@@ -156,7 +138,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
156138
157139 // Model Switch
158140 case models.ModelSelectedMsg:
159− config.UpdatePreferredModel(msg.ModelType, msg.Model)
141+ config.Get().UpdatePreferredModel(msg.ModelType, msg.Model)
160142
161143 // Update the agent with the new model/provider configuration
162144 if err := a.app.UpdateAgentModel(); err != nil {