on main help sign in
docs: add targeted documentation for state management
862baebKujtim Hoxha
internal/tui/components/chat/splash/splash.go+49−62
@@ -25,6 +25,10 @@ type Splash interface {
25 util.Model
25 util.Model
26 layout.Sizeable
26 layout.Sizeable
27 layout.Help
27 layout.Help
28+ // SetOnboarding controls whether the splash shows model selection UI
29+ SetOnboarding(bool)
30+ // SetProjectInit controls whether the splash shows project initialization prompt
31+ SetProjectInit(bool)
28}
32}
29
33
30const (
34const (
@@ -32,14 +36,6 @@ const (
32 SplashScreenPaddingY = 1 // Padding Y for the splash screen
36 SplashScreenPaddingY = 1 // Padding Y for the splash screen
33)
37)
34
38
35−type SplashScreenState string
36−
37−const (
38− SplashScreenStateOnboarding SplashScreenState = "onboarding"
39− SplashScreenStateInitialize SplashScreenState = "initialize"
40− SplashScreenStateReady SplashScreenState = "ready"
41−)
42−
43// OnboardingCompleteMsg is sent when onboarding is complete
39// OnboardingCompleteMsg is sent when onboarding is complete
44type OnboardingCompleteMsg struct{}
40type OnboardingCompleteMsg struct{}
45
41
@@ -47,10 +43,14 @@ type splashCmp struct {
47 width, height int
43 width, height int
48 keyMap KeyMap
44 keyMap KeyMap
49 logoRendered string
45 logoRendered string
50− state SplashScreenState
46+
47+ // State
48+ isOnboarding bool
49+ needsProjectInit bool
50+ selectedNo bool
51+
51 modelList *models.ModelListComponent
52 modelList *models.ModelListComponent
52 cursorRow, cursorCol int
53 cursorRow, cursorCol int
53− selectedNo bool // true if "No" button is selected in initialize state
54}
54}
55
55
56func New() Splash {
56func New() Splash {
@@ -69,33 +69,21 @@ func New() Splash {
69 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
69 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
70 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
70 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
71 return &splashCmp{
71 return &splashCmp{
72− width: 0,
72+ width: 0,
73− height: 0,
73+ height: 0,
74− keyMap: keyMap,
74+ keyMap: keyMap,
75− state: SplashScreenStateOnboarding,
76− logoRendered: "",
75+ logoRendered: "",
77− modelList: modelList,
76+ modelList: modelList,
78− selectedNo: false,
77+ selectedNo: false,
79 }
78 }
80}
79}
81
80
82−// GetSize implements SplashPage.
83−func (s *splashCmp) GetSize() (int, int) {
81+func (s *splashCmp) SetOnboarding(onboarding bool) {
84− return s.width, s.height
82+ s.isOnboarding = onboarding
85−}
83+ if onboarding {
86−
87−// Init implements SplashPage.
88−func (s *splashCmp) Init() tea.Cmd {
89− if config.HasInitialDataConfig() {
90− if b, _ := config.ProjectNeedsInitialization(); b {
91− s.state = SplashScreenStateInitialize
92− } else {
93− s.state = SplashScreenStateReady
94− }
95− } else {
96 providers, err := config.Providers()
84 providers, err := config.Providers()
97 if err != nil {
85 if err != nil {
98− return util.ReportError(err)
86+ return
99 }
87 }
100 filteredProviders := []provider.Provider{}
88 filteredProviders := []provider.Provider{}
101 simpleProviders := []string{
89 simpleProviders := []string{
@@ -111,9 +99,21 @@ func (s *splashCmp) Init() tea.Cmd {
111 }
99 }
112 }
100 }
113 s.modelList.SetProviders(filteredProviders)
101 s.modelList.SetProviders(filteredProviders)
114− return s.modelList.Init()
115 }
102 }
103+}
104+
105+func (s *splashCmp) SetProjectInit(needsInit bool) {
106+ s.needsProjectInit = needsInit
107+}
108+
109+// GetSize implements SplashPage.
110+func (s *splashCmp) GetSize() (int, int) {
116− return nil
111+ return s.width, s.height
112+}
113+
114+// Init implements SplashPage.
115+func (s *splashCmp) Init() tea.Cmd {
116+ return s.modelList.Init()
117}
117}
118
118
119// SetSize implements SplashPage.
119// SetSize implements SplashPage.
@@ -137,42 +137,34 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
137 case tea.KeyPressMsg:
137 case tea.KeyPressMsg:
138 switch {
138 switch {
139 case key.Matches(msg, s.keyMap.Select):
139 case key.Matches(msg, s.keyMap.Select):
140− if s.state == SplashScreenStateOnboarding {
140+ if s.isOnboarding {
141 modelInx := s.modelList.SelectedIndex()
141 modelInx := s.modelList.SelectedIndex()
142 items := s.modelList.Items()
142 items := s.modelList.Items()
143 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
143 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
144 if s.isProviderConfigured(string(selectedItem.Provider.ID)) {
144 if s.isProviderConfigured(string(selectedItem.Provider.ID)) {
145 cmd := s.setPreferredModel(selectedItem)
145 cmd := s.setPreferredModel(selectedItem)
146− s.state = SplashScreenStateReady
146+ s.isOnboarding = false
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{}))
147+ return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
155− }
156 }
148 }
157− } else if s.state == SplashScreenStateInitialize {
149+ } else if s.needsProjectInit {
158 return s, s.initializeProject()
150 return s, s.initializeProject()
159 }
151 }
160 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
152 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
161− if s.state == SplashScreenStateInitialize {
153+ if s.needsProjectInit {
162 s.selectedNo = !s.selectedNo
154 s.selectedNo = !s.selectedNo
163 return s, nil
155 return s, nil
164 }
156 }
165 case key.Matches(msg, s.keyMap.Yes):
157 case key.Matches(msg, s.keyMap.Yes):
166− if s.state == SplashScreenStateInitialize {
158+ if s.needsProjectInit {
167 return s, s.initializeProject()
159 return s, s.initializeProject()
168 }
160 }
169 case key.Matches(msg, s.keyMap.No):
161 case key.Matches(msg, s.keyMap.No):
170− if s.state == SplashScreenStateInitialize {
162+ if s.needsProjectInit {
171− s.state = SplashScreenStateReady
163+ s.needsProjectInit = false
172 return s, util.CmdHandler(OnboardingCompleteMsg{})
164 return s, util.CmdHandler(OnboardingCompleteMsg{})
173 }
165 }
174 default:
166 default:
175− if s.state == SplashScreenStateOnboarding {
167+ if s.isOnboarding {
176 u, cmd := s.modelList.Update(msg)
168 u, cmd := s.modelList.Update(msg)
177 s.modelList = u
169 s.modelList = u
178 return s, cmd
170 return s, cmd
@@ -183,7 +175,7 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
183}
175}
184
176
185func (s *splashCmp) initializeProject() tea.Cmd {
177func (s *splashCmp) initializeProject() tea.Cmd {
186− s.state = SplashScreenStateReady
178+ s.needsProjectInit = false
187 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
179 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
1881. Build/lint/test commands - especially for running a single test
1801. Build/lint/test commands - especially for running a single test
1892. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
1812. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
@@ -193,7 +185,6 @@ If there's already a CRUSH.md, improve it.
193If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
185If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
194Add the .crush directory to the .gitignore file if it's not already there.`
186Add the .crush directory to the .gitignore file if it's not already there.`
195
187
196− // Mark the project as initialized
197 if err := config.MarkProjectInitialized(); err != nil {
188 if err := config.MarkProjectInitialized(); err != nil {
198 return util.ReportError(err)
189 return util.ReportError(err)
199 }
190 }
@@ -293,9 +284,7 @@ func (s *splashCmp) View() tea.View {
293 var cursor *tea.Cursor
284 var cursor *tea.Cursor
294
285
295 var content string
286 var content string
296− switch s.state {
287+ if s.isOnboarding {
297− case SplashScreenStateOnboarding:
298− // Show logo and model selector
299 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
288 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
300 modelListView := s.modelList.View()
289 modelListView := s.modelList.View()
301 cursor = s.moveCursor(modelListView.Cursor())
290 cursor = s.moveCursor(modelListView.Cursor())
@@ -312,7 +301,7 @@ func (s *splashCmp) View() tea.View {
312 s.logoRendered,
301 s.logoRendered,
313 modelSelector,
302 modelSelector,
314 )
303 )
315− case SplashScreenStateInitialize:
304+ } else if s.needsProjectInit {
316 t := styles.CurrentTheme()
305 t := styles.CurrentTheme()
317
306
318 titleStyle := t.S().Base.Foreground(t.FgBase)
307 titleStyle := t.S().Base.Foreground(t.FgBase)
@@ -361,9 +350,7 @@ func (s *splashCmp) View() tea.View {
361 s.logoRendered,
350 s.logoRendered,
362 initContent,
351 initContent,
363 )
352 )
364−
353+ } else {
365− default:
366− // Show just the logo for other states
367 content = s.logoRendered
354 content = s.logoRendered
368 }
355 }
369
356
@@ -407,13 +394,13 @@ func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
407
394
408// Bindings implements SplashPage.
395// Bindings implements SplashPage.
409func (s *splashCmp) Bindings() []key.Binding {
396func (s *splashCmp) Bindings() []key.Binding {
410− if s.state == SplashScreenStateOnboarding {
397+ if s.isOnboarding {
411 return []key.Binding{
398 return []key.Binding{
412 s.keyMap.Select,
399 s.keyMap.Select,
413 s.keyMap.Next,
400 s.keyMap.Next,
414 s.keyMap.Previous,
401 s.keyMap.Previous,
415 }
402 }
416− } else if s.state == SplashScreenStateInitialize {
403+ } else if s.needsProjectInit {
417 return []key.Binding{
404 return []key.Binding{
418 s.keyMap.Select,
405 s.keyMap.Select,
419 s.keyMap.Yes,
406 s.keyMap.Yes,