on main help sign in
wip: oboarding splashscreen
commit4941a7e490d4ffba405dc6891483337405158549 authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 4 Jul 2025, 13:26 (1 year ago) parent1526d1a
5 files changed+303−157
internal/tui/components/chat/splash/keys.go+14−4 internal/tui/components/chat/splash/splash.go+107−13 internal/tui/components/dialogs/models/list.go+159 internal/tui/components/dialogs/models/models.go+11−129 internal/tui/page/chat/chat.go+12−11
internal/tui/components/chat/splash/keys.go+14−4
@@ -5,14 +5,24 @@ import (
55)
66
77type KeyMap struct {
8− Cancel key.Binding
8+ Select,
9+ Next,
10+ Previous key.Binding
911}
1012
1113func DefaultKeyMap() KeyMap {
1214 return KeyMap{
13− Cancel: key.NewBinding(
14− key.WithKeys("esc"),
15− key.WithHelp("esc", "cancel"),
15+ Select: key.NewBinding(
16+ key.WithKeys("enter", "ctrl+y"),
17+ key.WithHelp("enter", "confirm"),
18+ ),
19+ Next: key.NewBinding(
20+ key.WithKeys("down", "ctrl+n"),
21+ key.WithHelp("↓", "next item"),
22+ ),
23+ Previous: key.NewBinding(
24+ key.WithKeys("up", "ctrl+p"),
25+ key.WithHelp("↑", "previous item"),
1626 ),
1727 }
1828}
internal/tui/components/chat/splash/splash.go+107−13
@@ -3,7 +3,10 @@ package splash
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
6+ "github.com/charmbracelet/crush/internal/config"
67 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
8+ "github.com/charmbracelet/crush/internal/tui/components/core/list"
9+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
710 "github.com/charmbracelet/crush/internal/tui/components/logo"
811 "github.com/charmbracelet/crush/internal/tui/styles"
912 "github.com/charmbracelet/crush/internal/tui/util"
@@ -22,18 +25,48 @@ const (
2225 SplashScreenPaddingY = 1 // Padding Y for the splash screen
2326)
2427
28+type SplashScreenState string
29+
30+const (
31+ SplashScreenStateOnboarding SplashScreenState = "onboarding"
32+ SplashScreenStateInitialize SplashScreenState = "initialize"
33+ SplashScreenStateReady SplashScreenState = "ready"
34+)
35+
36+// OnboardingCompleteMsg is sent when onboarding is complete
37+type OnboardingCompleteMsg struct{}
38+
2539type splashCmp struct {
26− width, height int
27− keyMap KeyMap
28− logoRendered string
40+ width, height int
41+ keyMap KeyMap
42+ logoRendered string
43+ state SplashScreenState
44+ modelList *models.ModelListComponent
45+ cursorRow, cursorCol int
2946}
3047
3148func New() Splash {
49+ keyMap := DefaultKeyMap()
50+ listKeyMap := list.DefaultKeyMap()
51+ listKeyMap.Down.SetEnabled(false)
52+ listKeyMap.Up.SetEnabled(false)
53+ listKeyMap.HalfPageDown.SetEnabled(false)
54+ listKeyMap.HalfPageUp.SetEnabled(false)
55+ listKeyMap.Home.SetEnabled(false)
56+ listKeyMap.End.SetEnabled(false)
57+ listKeyMap.DownOneItem = keyMap.Next
58+ listKeyMap.UpOneItem = keyMap.Previous
59+
60+ t := styles.CurrentTheme()
61+ inputStyle := t.S().Base.Padding(0, 1, 0, 1)
62+ modelList := models.NewModelListComponent(listKeyMap, inputStyle)
3263 return &splashCmp{
3364 width: 0,
3465 height: 0,
35− keyMap: DefaultKeyMap(),
66+ keyMap: keyMap,
67+ state: SplashScreenStateOnboarding,
3668 logoRendered: "",
69+ modelList: modelList,
3770 }
3871}
3972
@@ -44,7 +77,14 @@ func (s *splashCmp) GetSize() (int, int) {
4477
4578// Init implements SplashPage.
4679func (s *splashCmp) Init() tea.Cmd {
47− return nil
80+ if config.HasInitialDataConfig() {
81+ if b, _ := config.ProjectNeedsInitialization(); b {
82+ s.state = SplashScreenStateInitialize
83+ } else {
84+ s.state = SplashScreenStateReady
85+ }
86+ }
87+ return s.modelList.Init()
4888}
4989
5090// SetSize implements SplashPage.
@@ -52,7 +92,12 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
5292 s.width = width
5393 s.height = height
5494 s.logoRendered = s.logoBlock()
55− return nil
95+ listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
96+ listWidth := min(60, width-(SplashScreenPaddingX*2))
97+
98+ // Calculate the cursor position based on the height and logo size
99+ s.cursorRow = height - listHeigh
100+ return s.modelList.SetSize(listWidth, listHeigh)
56101}
57102
58103// Update implements SplashPage.
@@ -60,6 +105,13 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
60105 switch msg := msg.(type) {
61106 case tea.WindowSizeMsg:
62107 return s, s.SetSize(msg.Width, msg.Height)
108+ case tea.KeyPressMsg:
109+ switch {
110+ default:
111+ u, cmd := s.modelList.Update(msg)
112+ s.modelList = u
113+ return s, cmd
114+ }
63115 }
64116 return s, nil
65117}
@@ -67,8 +119,34 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
67119// View implements SplashPage.
68120func (s *splashCmp) View() tea.View {
69121 t := styles.CurrentTheme()
70− content := lipgloss.JoinVertical(lipgloss.Left, s.logoRendered)
71− return tea.NewView(
122+ var cursor *tea.Cursor
123+
124+ var content string
125+ switch s.state {
126+ case SplashScreenStateOnboarding:
127+ // Show logo and model selector
128+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
129+ modelListView := s.modelList.View()
130+ cursor = s.moveCursor(modelListView.Cursor())
131+ modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
132+ lipgloss.JoinVertical(
133+ lipgloss.Left,
134+ t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Choose a Model"),
135+ "",
136+ modelListView.String(),
137+ ),
138+ )
139+ content = lipgloss.JoinVertical(
140+ lipgloss.Left,
141+ s.logoRendered,
142+ modelSelector,
143+ )
144+ default:
145+ // Show just the logo for other states
146+ content = s.logoRendered
147+ }
148+
149+ view := tea.NewView(
72150 t.S().Base.
73151 Width(s.width).
74152 Height(s.height).
@@ -76,10 +154,11 @@ func (s *splashCmp) View() tea.View {
76154 PaddingLeft(SplashScreenPaddingX).
77155 PaddingRight(SplashScreenPaddingX).
78156 PaddingBottom(SplashScreenPaddingY).
79− Render(
80− content,
81− ),
157+ Render(content),
82158 )
159+
160+ view.SetCursor(cursor)
161+ return view
83162}
84163
85164func (s *splashCmp) logoBlock() string {
@@ -95,9 +174,24 @@ func (s *splashCmp) logoBlock() string {
95174 })
96175}
97176
177+func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
178+ if cursor == nil {
179+ return nil
180+ }
181+ offset := m.cursorRow
182+ cursor.Y += offset
183+ cursor.X = cursor.X + 3 // 3 for padding
184+ return cursor
185+}
186+
98187// Bindings implements SplashPage.
99188func (s *splashCmp) Bindings() []key.Binding {
100− return []key.Binding{
101− s.keyMap.Cancel,
189+ if s.state == SplashScreenStateOnboarding {
190+ return []key.Binding{
191+ s.keyMap.Select,
192+ s.keyMap.Next,
193+ s.keyMap.Previous,
194+ }
102195 }
196+ return []key.Binding{}
103197}
internal/tui/components/dialogs/models/list.gonew file+159
@@ -0,0 +1,159 @@
1+package models
2+
3+import (
4+ "slices"
5+
6+ tea "github.com/charmbracelet/bubbletea/v2"
7+ "github.com/charmbracelet/crush/internal/config"
8+ "github.com/charmbracelet/crush/internal/fur/provider"
9+ "github.com/charmbracelet/crush/internal/tui/components/completions"
10+ "github.com/charmbracelet/crush/internal/tui/components/core/list"
11+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
12+ "github.com/charmbracelet/crush/internal/tui/util"
13+ "github.com/charmbracelet/lipgloss/v2"
14+)
15+
16+type ModelListComponent struct {
17+ list list.ListModel
18+ modelType int
19+}
20+
21+func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style) *ModelListComponent {
22+ modelList := list.New(
23+ list.WithFilterable(true),
24+ list.WithKeyMap(keyMap),
25+ list.WithInputStyle(inputStyle),
26+ list.WithWrapNavigation(true),
27+ )
28+
29+ return &ModelListComponent{
30+ list: modelList,
31+ modelType: LargeModelType,
32+ }
33+}
34+
35+func (m *ModelListComponent) Init() tea.Cmd {
36+ return tea.Batch(m.list.Init(), m.SetModelType(m.modelType))
37+}
38+
39+func (m *ModelListComponent) Update(msg tea.Msg) (*ModelListComponent, tea.Cmd) {
40+ u, cmd := m.list.Update(msg)
41+ m.list = u.(list.ListModel)
42+ return m, cmd
43+}
44+
45+func (m *ModelListComponent) View() tea.View {
46+ return m.list.View()
47+}
48+
49+func (m *ModelListComponent) SetSize(width, height int) tea.Cmd {
50+ return m.list.SetSize(width, height)
51+}
52+
53+func (m *ModelListComponent) Items() []util.Model {
54+ return m.list.Items()
55+}
56+
57+func (m *ModelListComponent) SelectedIndex() int {
58+ return m.list.SelectedIndex()
59+}
60+
61+func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
62+ m.modelType = modelType
63+
64+ providers := config.Providers()
65+ modelItems := []util.Model{}
66+ selectIndex := 0
67+
68+ cfg := config.Get()
69+ var currentModel config.PreferredModel
70+ if m.modelType == LargeModelType {
71+ currentModel = cfg.Models.Large
72+ } else {
73+ currentModel = cfg.Models.Small
74+ }
75+
76+ addedProviders := make(map[provider.InferenceProvider]bool)
77+
78+ knownProviders := provider.KnownProviders()
79+ for providerID, providerConfig := range cfg.Providers {
80+ if providerConfig.Disabled {
81+ continue
82+ }
83+
84+ // Check if this provider is not in the known providers list
85+ if !slices.Contains(knownProviders, providerID) {
86+ configProvider := provider.Provider{
87+ Name: string(providerID),
88+ ID: providerID,
89+ Models: make([]provider.Model, len(providerConfig.Models)),
90+ }
91+
92+ for i, model := range providerConfig.Models {
93+ configProvider.Models[i] = provider.Model{
94+ ID: model.ID,
95+ Name: model.Name,
96+ CostPer1MIn: model.CostPer1MIn,
97+ CostPer1MOut: model.CostPer1MOut,
98+ CostPer1MInCached: model.CostPer1MInCached,
99+ CostPer1MOutCached: model.CostPer1MOutCached,
100+ ContextWindow: model.ContextWindow,
101+ DefaultMaxTokens: model.DefaultMaxTokens,
102+ CanReason: model.CanReason,
103+ HasReasoningEffort: model.HasReasoningEffort,
104+ DefaultReasoningEffort: model.ReasoningEffort,
105+ SupportsImages: model.SupportsImages,
106+ }
107+ }
108+
109+ name := configProvider.Name
110+ if name == "" {
111+ name = string(configProvider.ID)
112+ }
113+ modelItems = append(modelItems, commands.NewItemSection(name))
114+ for _, model := range configProvider.Models {
115+ modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
116+ Provider: configProvider,
117+ Model: model,
118+ }))
119+ if model.ID == currentModel.ModelID && configProvider.ID == currentModel.Provider {
120+ selectIndex = len(modelItems) - 1
121+ }
122+ }
123+ addedProviders[providerID] = true
124+ }
125+ }
126+
127+ for _, provider := range providers {
128+ if addedProviders[provider.ID] {
129+ continue
130+ }
131+
132+ if providerConfig, exists := cfg.Providers[provider.ID]; exists && providerConfig.Disabled {
133+ continue
134+ }
135+
136+ name := provider.Name
137+ if name == "" {
138+ name = string(provider.ID)
139+ }
140+ modelItems = append(modelItems, commands.NewItemSection(name))
141+ for _, model := range provider.Models {
142+ modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
143+ Provider: provider,
144+ Model: model,
145+ }))
146+ if model.ID == currentModel.ModelID && provider.ID == currentModel.Provider {
147+ selectIndex = len(modelItems) - 1
148+ }
149+ }
150+ }
151+
152+ return tea.Sequence(m.list.SetItems(modelItems), m.list.SetSelected(selectIndex))
153+}
154+
155+// GetModelType returns the current model type
156+func (m *ModelListComponent) GetModelType() int {
157+ return m.modelType
158+}
159+
internal/tui/components/dialogs/models/models.go+11−129
@@ -1,8 +1,6 @@
11package models
22
33import (
4− "slices"
5−
64 "github.com/charmbracelet/bubbles/v2/help"
75 "github.com/charmbracelet/bubbles/v2/key"
86 tea "github.com/charmbracelet/bubbletea/v2"
@@ -12,7 +10,6 @@ import (
1210 "github.com/charmbracelet/crush/internal/tui/components/core"
1311 "github.com/charmbracelet/crush/internal/tui/components/core/list"
1412 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
15− "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
1613 "github.com/charmbracelet/crush/internal/tui/styles"
1714 "github.com/charmbracelet/crush/internal/tui/util"
1815 "github.com/charmbracelet/lipgloss/v2"
@@ -53,10 +50,9 @@ type modelDialogCmp struct {
5350 wWidth int
5451 wHeight int
5552
56− modelList list.ListModel
53+ modelList *ModelListComponent
5754 keyMap KeyMap
5855 help help.Model
59− modelType int
6056}
6157
6258func NewModelDialogCmp() ModelDialog {
@@ -75,12 +71,7 @@ func NewModelDialogCmp() ModelDialog {
7571
7672 t := styles.CurrentTheme()
7773 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
78− modelList := list.New(
79− list.WithFilterable(true),
80− list.WithKeyMap(listKeyMap),
81− list.WithInputStyle(inputStyle),
82− list.WithWrapNavigation(true),
83− )
74+ modelList := NewModelListComponent(listKeyMap, inputStyle)
8475 help := help.New()
8576 help.Styles = t.S().Help
8677
@@ -89,12 +80,10 @@ func NewModelDialogCmp() ModelDialog {
8980 width: defaultWidth,
9081 keyMap: DefaultKeyMap(),
9182 help: help,
92− modelType: LargeModelType,
9383 }
9484}
9585
9686func (m *modelDialogCmp) Init() tea.Cmd {
97− m.SetModelType(m.modelType)
9887 return m.modelList.Init()
9988}
10089
@@ -103,7 +92,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
10392 case tea.WindowSizeMsg:
10493 m.wWidth = msg.Width
10594 m.wHeight = msg.Height
106− m.SetModelType(m.modelType)
10795 return m, m.modelList.SetSize(m.listWidth(), m.listHeight())
10896 case tea.KeyPressMsg:
10997 switch {
@@ -116,8 +104,8 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116104 selectedItem := items[selectedItemInx].(completions.CompletionItem).Value().(ModelOption)
117105
118106 var modelType config.SelectedModelType
119− if m.modelType == LargeModelType {
120− modelType = config.SelectedModelTypeLarge
107+ if m.modelList.GetModelType() == LargeModelType {
108+ modelType = config.LargeModel
121109 } else {
122110 modelType = config.SelectedModelTypeSmall
123111 }
@@ -133,16 +121,16 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
133121 }),
134122 )
135123 case key.Matches(msg, m.keyMap.Tab):
136− if m.modelType == LargeModelType {
137− return m, m.SetModelType(SmallModelType)
124+ if m.modelList.GetModelType() == LargeModelType {
125+ return m, m.modelList.SetModelType(SmallModelType)
138126 } else {
139− return m, m.SetModelType(LargeModelType)
127+ return m, m.modelList.SetModelType(LargeModelType)
140128 }
141129 case key.Matches(msg, m.keyMap.Close):
142130 return m, util.CmdHandler(dialogs.CloseDialogMsg{})
143131 default:
144132 u, cmd := m.modelList.Update(msg)
145− m.modelList = u.(list.ListModel)
133+ m.modelList = u
146134 return m, cmd
147135 }
148136 }
@@ -181,7 +169,8 @@ func (m *modelDialogCmp) listWidth() int {
181169}
182170
183171func (m *modelDialogCmp) listHeight() int {
184− listHeigh := len(m.modelList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections
172+ items := m.modelList.Items()
173+ listHeigh := len(items) + 2 + 4
185174 return min(listHeigh, m.wHeight/2)
186175}
187176
@@ -209,115 +198,8 @@ func (m *modelDialogCmp) modelTypeRadio() string {
209198 choices := []string{"Large Task", "Small Task"}
210199 iconSelected := "◉"
211200 iconUnselected := "○"
212− if m.modelType == LargeModelType {
201+ if m.modelList.GetModelType() == LargeModelType {
213202 return t.S().Base.Foreground(t.FgHalfMuted).Render(iconSelected + " " + choices[0] + " " + iconUnselected + " " + choices[1])
214203 }
215204 return t.S().Base.Foreground(t.FgHalfMuted).Render(iconUnselected + " " + choices[0] + " " + iconSelected + " " + choices[1])
216205}
217−
218−func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd {
219− m.modelType = modelType
220−
221− providers, err := config.Providers()
222− if err != nil {
223− return util.ReportError(err)
224− }
225−
226− modelItems := []util.Model{}
227− selectIndex := 0
228−
229− cfg := config.Get()
230− var currentModel config.SelectedModel
231− if m.modelType == LargeModelType {
232− currentModel = cfg.Models[config.SelectedModelTypeLarge]
233− } else {
234− currentModel = cfg.Models[config.SelectedModelTypeSmall]
235− }
236−
237− // Create a map to track which providers we've already added
238− addedProviders := make(map[string]bool)
239−
240− // First, add any configured providers that are not in the known providers list
241− // These should appear at the top of the list
242− knownProviders := provider.KnownProviders()
243− for providerID, providerConfig := range cfg.Providers {
244− if providerConfig.Disable {
245− continue
246− }
247−
248− // Check if this provider is not in the known providers list
249− if !slices.Contains(knownProviders, provider.InferenceProvider(providerID)) {
250− // Convert config provider to provider.Provider format
251− configProvider := provider.Provider{
252− Name: string(providerID), // Use provider ID as name for unknown providers
253− ID: provider.InferenceProvider(providerID),
254− Models: make([]provider.Model, len(providerConfig.Models)),
255− }
256−
257− // Convert models
258− for i, model := range providerConfig.Models {
259− configProvider.Models[i] = provider.Model{
260− ID: model.ID,
261− Name: model.Name,
262− CostPer1MIn: model.CostPer1MIn,
263− CostPer1MOut: model.CostPer1MOut,
264− CostPer1MInCached: model.CostPer1MInCached,
265− CostPer1MOutCached: model.CostPer1MOutCached,
266− ContextWindow: model.ContextWindow,
267− DefaultMaxTokens: model.DefaultMaxTokens,
268− CanReason: model.CanReason,
269− HasReasoningEffort: model.HasReasoningEffort,
270− DefaultReasoningEffort: model.DefaultReasoningEffort,
271− SupportsImages: model.SupportsImages,
272− }
273− }
274−
275− // Add this unknown provider to the list
276− name := configProvider.Name
277− if name == "" {
278− name = string(configProvider.ID)
279− }
280− modelItems = append(modelItems, commands.NewItemSection(name))
281− for _, model := range configProvider.Models {
282− modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
283− Provider: configProvider,
284− Model: model,
285− }))
286− if model.ID == currentModel.Model && string(configProvider.ID) == currentModel.Provider {
287− selectIndex = len(modelItems) - 1 // Set the selected index to the current model
288− }
289− }
290− addedProviders[providerID] = true
291− }
292− }
293−
294− // Then add the known providers from the predefined list
295− for _, provider := range providers {
296− // Skip if we already added this provider as an unknown provider
297− if addedProviders[string(provider.ID)] {
298− continue
299− }
300−
301− // Check if this provider is configured and not disabled
302− if providerConfig, exists := cfg.Providers[string(provider.ID)]; exists && providerConfig.Disable {
303− continue
304− }
305−
306− name := provider.Name
307− if name == "" {
308− name = string(provider.ID)
309− }
310− modelItems = append(modelItems, commands.NewItemSection(name))
311− for _, model := range provider.Models {
312− modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
313− Provider: provider,
314− Model: model,
315− }))
316− if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
317− selectIndex = len(modelItems) - 1 // Set the selected index to the current model
318− }
319− }
320− }
321−
322− return tea.Sequence(m.modelList.SetItems(modelItems), m.modelList.SetSelected(selectIndex))
323−}
internal/tui/page/chat/chat.go+12−11
@@ -109,17 +109,18 @@ func New(app *app.App) ChatPage {
109109
110110 keyMap: DefaultKeyMap(),
111111
112− header: header.New(app.LSPClients),
113− sidebar: sidebar.New(app.History, app.LSPClients, false),
114− chat: chat.New(app),
115− editor: editor.New(app),
116− splash: splash.New(),
112+ header: header.New(app.LSPClients),
113+ sidebar: sidebar.New(app.History, app.LSPClients, false),
114+ chat: chat.New(app),
115+ editor: editor.New(app),
116+ splash: splash.New(),
117+ focusedPane: PanelTypeSplash,
117118 }
118119}
119120
120121func (p *chatPage) Init() tea.Cmd {
121122 cfg := config.Get()
122− if cfg.IsReady() {
123+ if config.HasInitialDataConfig() {
123124 if b, _ := config.ProjectNeedsInitialization(); b {
124125 p.state = ChatStateInitProject
125126 } else {
@@ -248,6 +249,10 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
248249 u, cmd := p.editor.Update(msg)
249250 p.editor = u.(editor.Editor)
250251 cmds = append(cmds, cmd)
252+ case PanelTypeSplash:
253+ u, cmd := p.splash.Update(msg)
254+ p.splash = u.(splash.Splash)
255+ cmds = append(cmds, cmd)
251256 }
252257 }
253258 return p, tea.Batch(cmds...)
@@ -258,11 +263,7 @@ func (p *chatPage) View() tea.View {
258263 t := styles.CurrentTheme()
259264 switch p.state {
260265 case ChatStateOnboarding, ChatStateInitProject:
261− chatView = tea.NewView(
262− t.S().Base.Render(
263− p.splash.View().String(),
264− ),
265− )
266+ chatView = p.splash.View()
266267 case ChatStateNewMessage:
267268 editorView := p.editor.View()
268269 chatView = tea.NewView(