on main help sign in
feat: implement onboarding flow on the new ui codebase
28e0ff3Andrey Nering
internal/app/app.go+3−2
@@ -351,15 +351,16 @@ func (app *App) overrideModelsForNonInteractive(ctx context.Context, largeModel,
351
351
352 case largeModel != "":
352 case largeModel != "":
353 // No small model specified, but large model was - use provider's default.
353 // No small model specified, but large model was - use provider's default.
354− smallCfg := app.getDefaultSmallModel(largeProviderID)
354+ smallCfg := app.GetDefaultSmallModel(largeProviderID)
355 app.config.Models[config.SelectedModelTypeSmall] = smallCfg
355 app.config.Models[config.SelectedModelTypeSmall] = smallCfg
356 }
356 }
357
357
358 return app.AgentCoordinator.UpdateModels(ctx)
358 return app.AgentCoordinator.UpdateModels(ctx)
359}
359}
360
360
361+// GetDefaultSmallModel returns the default small model for the given
361// provider. Falls back to the large model if no default is found.
362// provider. Falls back to the large model if no default is found.
362−func (app *App) getDefaultSmallModel(providerID string) config.SelectedModel {
363+func (app *App) GetDefaultSmallModel(providerID string) config.SelectedModel {
363 cfg := app.config
364 cfg := app.config
364 largeModelCfg := cfg.Models[config.SelectedModelTypeLarge]
365 largeModelCfg := cfg.Models[config.SelectedModelTypeLarge]
365
366
internal/ui/common/common.go+10
@@ -52,6 +52,16 @@ func CenterRect(area uv.Rectangle, width, height int) uv.Rectangle {
52 return image.Rect(minX, minY, maxX, maxY)
52 return image.Rect(minX, minY, maxX, maxY)
53}
53}
54
54
55+// BottomLeftRect returns a new [Rectangle] positioned at the bottom-left within the given area with the
56+// specified width and height.
57+func BottomLeftRect(area uv.Rectangle, width, height int) uv.Rectangle {
58+ minX := area.Min.X
59+ maxX := minX + width
60+ maxY := area.Max.Y
61+ minY := maxY - height
62+ return image.Rect(minX, minY, maxX, maxY)
63+}
64+
55// IsFileTooBig checks if the file at the given path exceeds the specified size
65// IsFileTooBig checks if the file at the given path exceeds the specified size
56// limit.
66// limit.
57func IsFileTooBig(filePath string, sizeLimit int64) (bool, error) {
67func IsFileTooBig(filePath string, sizeLimit int64) (bool, error) {
internal/ui/dialog/api_key_input.go+39−14
@@ -33,7 +33,8 @@ const APIKeyInputID = "api_key_input"
33
33
34// APIKeyInput represents a model selection dialog.
34// APIKeyInput represents a model selection dialog.
35type APIKeyInput struct {
35type APIKeyInput struct {
36− com *common.Common
36+ com *common.Common
37+ isOnboarding bool
37
38
38 provider catwalk.Provider
39 provider catwalk.Provider
39 model config.SelectedModel
40 model config.SelectedModel
@@ -54,11 +55,18 @@ type APIKeyInput struct {
54var _ Dialog = (*APIKeyInput)(nil)
55var _ Dialog = (*APIKeyInput)(nil)
55
56
56// NewAPIKeyInput creates a new Models dialog.
57// NewAPIKeyInput creates a new Models dialog.
57−func NewAPIKeyInput(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*APIKeyInput, tea.Cmd) {
58+func NewAPIKeyInput(
59+ com *common.Common,
60+ isOnboarding bool,
61+ provider catwalk.Provider,
62+ model config.SelectedModel,
63+ modelType config.SelectedModelType,
64+) (*APIKeyInput, tea.Cmd) {
58 t := com.Styles
65 t := com.Styles
59
66
60 m := APIKeyInput{}
67 m := APIKeyInput{}
61 m.com = com
68 m.com = com
69+ m.isOnboarding = isOnboarding
62 m.provider = provider
70 m.provider = provider
63 m.model = model
71 m.model = model
64 m.modelType = modelType
72 m.modelType = modelType
@@ -170,28 +178,45 @@ func (m *APIKeyInput) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
170 helpStyle.Render(m.help.View(m)),
178 helpStyle.Render(m.help.View(m)),
171 }, "\n")
179 }, "\n")
172
180
173− view := dialogStyle.Render(content)
174−
175 cur := m.Cursor()
181 cur := m.Cursor()
182+
183+ if m.isOnboarding {
184+ view := content
185+ DrawOnboardingCursor(scr, area, view, cur)
186+
187+ // FIXME(@andreynering): Figure it out how to properly fix this
188+ if cur != nil {
189+ cur.Y -= 1
190+ cur.X -= 1
191+ }
192+ } else {
193+ view := dialogStyle.Render(content)
176− DrawCenterCursor(scr, area, view, cur)
194+ DrawCenterCursor(scr, area, view, cur)
195+ }
177 return cur
196 return cur
178}
197}
179
198
180func (m *APIKeyInput) headerView() string {
199func (m *APIKeyInput) headerView() string {
200+ var (
181− t := m.com.Styles
201+ t = m.com.Styles
182− titleStyle := t.Dialog.Title
202+ titleStyle = t.Dialog.Title
203+ textStyle = t.Dialog.PrimaryText
183− dialogStyle := t.Dialog.View.Width(m.width)
204+ dialogStyle = t.Dialog.View.Width(m.width)
184−
205+ )
206+ if m.isOnboarding {
207+ return textStyle.Render(m.dialogTitle())
208+ }
185 headerOffset := titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
209 headerOffset := titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
186 return common.DialogTitle(t, titleStyle.Render(m.dialogTitle()), m.width-headerOffset)
210 return common.DialogTitle(t, titleStyle.Render(m.dialogTitle()), m.width-headerOffset)
187}
211}
188
212
189func (m *APIKeyInput) dialogTitle() string {
213func (m *APIKeyInput) dialogTitle() string {
214+ var (
190− t := m.com.Styles
215+ t = m.com.Styles
191− textStyle := t.Dialog.TitleText
216+ textStyle = t.Dialog.TitleText
192− errorStyle := t.Dialog.TitleError
217+ errorStyle = t.Dialog.TitleError
193− accentStyle := t.Dialog.TitleAccent
218+ accentStyle = t.Dialog.TitleAccent
194−
219+ )
195 switch m.state {
220 switch m.state {
196 case APIKeyInputStateInitial:
221 case APIKeyInputStateInitial:
197 return textStyle.Render("Enter your ") + accentStyle.Render(fmt.Sprintf("%s Key", m.provider.Name)) + textStyle.Render(".")
222 return textStyle.Render("Enter your ") + accentStyle.Render(fmt.Sprintf("%s Key", m.provider.Name)) + textStyle.Render(".")
internal/ui/dialog/common.go+9−2
@@ -59,6 +59,10 @@ type RenderContext struct {
59 // Help is the help view content. This will be appended to the content parts
59 // Help is the help view content. This will be appended to the content parts
60 // slice using the default dialog help style.
60 // slice using the default dialog help style.
61 Help string
61 Help string
62+ // IsOnboarding indicates whether to render the dialog as part of the
63+ // onboarding flow. This means that the content will be rendered at the
64+ // bottom left of the screen.
65+ IsOnboarding bool
62}
66}
63
67
64// NewRenderContext creates a new RenderContext with the provided styles and width.
68// NewRenderContext creates a new RenderContext with the provided styles and width.
@@ -82,7 +86,8 @@ func (rc *RenderContext) Render() string {
82 titleStyle := rc.Styles.Dialog.Title
86 titleStyle := rc.Styles.Dialog.Title
83 dialogStyle := rc.Styles.Dialog.View.Width(rc.Width)
87 dialogStyle := rc.Styles.Dialog.View.Width(rc.Width)
84
88
85− parts := []string{}
89+ var parts []string
90+
86 if len(rc.Title) > 0 {
91 if len(rc.Title) > 0 {
87 var titleInfoWidth int
92 var titleInfoWidth int
88 if len(rc.TitleInfo) > 0 {
93 if len(rc.TitleInfo) > 0 {
@@ -125,6 +130,8 @@ func (rc *RenderContext) Render() string {
125 }
130 }
126
131
127 content := strings.Join(parts, "\n")
132 content := strings.Join(parts, "\n")
128−
133+ if rc.IsOnboarding {
134+ return content
135+ }
129 return dialogStyle.Render(content)
136 return dialogStyle.Render(content)
130}
137}
internal/ui/dialog/dialog.go+17−1
@@ -170,7 +170,6 @@ func DrawCenterCursor(scr uv.Screen, area uv.Rectangle, view string, cur *tea.Cu
170 cur.X += center.Min.X
170 cur.X += center.Min.X
171 cur.Y += center.Min.Y
171 cur.Y += center.Min.Y
172 }
172 }
173−
174 uv.NewStyledString(view).Draw(scr, center)
173 uv.NewStyledString(view).Draw(scr, center)
175}
174}
176
175
@@ -179,6 +178,23 @@ func DrawCenter(scr uv.Screen, area uv.Rectangle, view string) {
179 DrawCenterCursor(scr, area, view, nil)
178 DrawCenterCursor(scr, area, view, nil)
180}
179}
181
180
181+// DrawOnboarding draws the given string view centered in the screen area.
182+func DrawOnboarding(scr uv.Screen, area uv.Rectangle, view string) {
183+ DrawOnboardingCursor(scr, area, view, nil)
184+}
185+
186+// DrawOnboardingCursor draws the given string view positioned at the bottom
187+// left area of the screen.
188+func DrawOnboardingCursor(scr uv.Screen, area uv.Rectangle, view string, cur *tea.Cursor) {
189+ width, height := lipgloss.Size(view)
190+ bottomLeft := common.BottomLeftRect(area, width, height)
191+ if cur != nil {
192+ cur.X += bottomLeft.Min.X
193+ cur.Y += bottomLeft.Min.Y
194+ }
195+ uv.NewStyledString(view).Draw(scr, bottomLeft)
196+}
197+
182// Draw renders the overlay and its dialogs.
198// Draw renders the overlay and its dialogs.
183func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
199func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
184 var cur *tea.Cursor
200 var cur *tea.Cursor
internal/ui/dialog/models.go+48−10
@@ -62,8 +62,9 @@ func (mt ModelType) Placeholder() string {
62}
62}
63
63
64const (
64const (
65+ onboardingModelInputPlaceholder = "Find your fave"
65− largeModelInputPlaceholder = "Choose a model for large, complex tasks"
66+ largeModelInputPlaceholder = "Choose a model for large, complex tasks"
66− smallModelInputPlaceholder = "Choose a model for small, simple tasks"
67+ smallModelInputPlaceholder = "Choose a model for small, simple tasks"
67)
68)
68
69
69// ModelsID is the identifier for the model selection dialog.
70// ModelsID is the identifier for the model selection dialog.
@@ -73,7 +74,8 @@ const defaultModelsDialogMaxWidth = 70
73
74
74// Models represents a model selection dialog.
75// Models represents a model selection dialog.
75type Models struct {
76type Models struct {
76− com *common.Common
77+ com *common.Common
78+ isOnboarding bool
77
79
78 modelType ModelType
80 modelType ModelType
79 providers []catwalk.Provider
81 providers []catwalk.Provider
@@ -94,10 +96,12 @@ type Models struct {
94var _ Dialog = (*Models)(nil)
96var _ Dialog = (*Models)(nil)
95
97
96// NewModels creates a new Models dialog.
98// NewModels creates a new Models dialog.
97−func NewModels(com *common.Common) (*Models, error) {
99+func NewModels(com *common.Common, isOnboarding bool) (*Models, error) {
98 t := com.Styles
100 t := com.Styles
99 m := &Models{}
101 m := &Models{}
100 m.com = com
102 m.com = com
103+ m.isOnboarding = isOnboarding
104+
101 help := help.New()
105 help := help.New()
102 help.Styles = t.DialogHelpStyles()
106 help.Styles = t.DialogHelpStyles()
103
107
@@ -108,7 +112,7 @@ func NewModels(com *common.Common) (*Models, error) {
108
112
109 m.input = textinput.New()
113 m.input = textinput.New()
110 m.input.SetVirtualCursor(false)
114 m.input.SetVirtualCursor(false)
111− m.input.Placeholder = largeModelInputPlaceholder
115+ m.input.Placeholder = onboardingModelInputPlaceholder
112 m.input.SetStyles(com.Styles.TextInput)
116 m.input.SetStyles(com.Styles.TextInput)
113 m.input.Focus()
117 m.input.Focus()
114
118
@@ -194,6 +198,9 @@ func (m *Models) HandleMsg(msg tea.Msg) Action {
194 ModelType: modelItem.SelectedModelType(),
198 ModelType: modelItem.SelectedModelType(),
195 }
199 }
196 case key.Matches(msg, m.keyMap.Tab):
200 case key.Matches(msg, m.keyMap.Tab):
201+ if m.isOnboarding {
202+ break
203+ }
197 if m.modelType == ModelTypeLarge {
204 if m.modelType == ModelTypeLarge {
198 m.modelType = ModelTypeSmall
205 m.modelType = ModelTypeSmall
199 } else {
206 } else {
@@ -251,6 +258,7 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
251 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
258 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
252 t.Dialog.HelpView.GetVerticalFrameSize() +
259 t.Dialog.HelpView.GetVerticalFrameSize() +
253 t.Dialog.View.GetVerticalFrameSize()
260 t.Dialog.View.GetVerticalFrameSize()
261+
254 m.input.SetWidth(max(0, innerWidth-t.Dialog.InputPrompt.GetHorizontalFrameSize()-1)) // (1) cursor padding
262 m.input.SetWidth(max(0, innerWidth-t.Dialog.InputPrompt.GetHorizontalFrameSize()-1)) // (1) cursor padding
255 m.list.SetSize(innerWidth, height-heightOffset)
263 m.list.SetSize(innerWidth, height-heightOffset)
256 m.help.SetWidth(innerWidth)
264 m.help.SetWidth(innerWidth)
@@ -258,21 +266,49 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
258 rc := NewRenderContext(t, width)
266 rc := NewRenderContext(t, width)
259 rc.Title = "Switch Model"
267 rc.Title = "Switch Model"
260 rc.TitleInfo = m.modelTypeRadioView()
268 rc.TitleInfo = m.modelTypeRadioView()
269+
270+ if m.isOnboarding {
271+ titleText := t.Dialog.PrimaryText.Render("To start, let's choose a provider and model.")
272+ rc.AddPart(titleText)
273+ }
274+
261 inputView := t.Dialog.InputPrompt.Render(m.input.View())
275 inputView := t.Dialog.InputPrompt.Render(m.input.View())
262 rc.AddPart(inputView)
276 rc.AddPart(inputView)
277+
263 listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render())
278 listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render())
264 rc.AddPart(listView)
279 rc.AddPart(listView)
265− rc.Help = m.help.View(m)
266
280
267− view := rc.Render()
281+ rc.Help = m.help.View(m)
268
282
269 cur := m.Cursor()
283 cur := m.Cursor()
284+
285+ if m.isOnboarding {
286+ rc.Title = ""
287+ rc.TitleInfo = ""
288+ rc.IsOnboarding = true
289+ view := rc.Render()
290+ DrawOnboardingCursor(scr, area, view, cur)
291+
292+ // FIXME(@andreynering): Figure it out how to properly fix this
293+ if cur != nil {
294+ cur.Y -= 1
295+ cur.X -= 1
296+ }
297+ } else {
298+ view := rc.Render()
270− DrawCenterCursor(scr, area, view, cur)
299+ DrawCenterCursor(scr, area, view, cur)
300+ }
271 return cur
301 return cur
272}
302}
273
303
274// ShortHelp returns the short help view.
304// ShortHelp returns the short help view.
275func (m *Models) ShortHelp() []key.Binding {
305func (m *Models) ShortHelp() []key.Binding {
306+ if m.isOnboarding {
307+ return []key.Binding{
308+ m.keyMap.UpDown,
309+ m.keyMap.Select,
310+ }
311+ }
276 return []key.Binding{
312 return []key.Binding{
277 m.keyMap.UpDown,
313 m.keyMap.UpDown,
278 m.keyMap.Tab,
314 m.keyMap.Tab,
@@ -459,10 +495,12 @@ func (m *Models) setProviderItems() error {
459 // Set model groups in the list.
495 // Set model groups in the list.
460 m.list.SetGroups(groups...)
496 m.list.SetGroups(groups...)
461 m.list.SetSelectedItem(selectedItemID)
497 m.list.SetSelectedItem(selectedItemID)
462− m.list.ScrollToSelected()
498+ m.list.ScrollToTop()
463
499
464 // Update placeholder based on model type
500 // Update placeholder based on model type
501+ if !m.isOnboarding {
465− m.input.Placeholder = m.modelType.Placeholder()
502+ m.input.Placeholder = m.modelType.Placeholder()
503+ }
466
504
467 return nil
505 return nil
468}
506}
internal/ui/dialog/oauth.go+24−5
@@ -41,7 +41,8 @@ const OAuthID = "oauth"
41
41
42// OAuth handles the OAuth flow authentication.
42// OAuth handles the OAuth flow authentication.
43type OAuth struct {
43type OAuth struct {
44− com *common.Common
44+ com *common.Common
45+ isOnboarding bool
45
46
46 provider catwalk.Provider
47 provider catwalk.Provider
47 model config.SelectedModel
48 model config.SelectedModel
@@ -71,11 +72,19 @@ type OAuth struct {
71var _ Dialog = (*OAuth)(nil)
72var _ Dialog = (*OAuth)(nil)
72
73
73// newOAuth creates a new device flow component.
74// newOAuth creates a new device flow component.
74−func newOAuth(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType, oAuthProvider OAuthProvider) (*OAuth, tea.Cmd) {
75+func newOAuth(
76+ com *common.Common,
77+ isOnboarding bool,
78+ provider catwalk.Provider,
79+ model config.SelectedModel,
80+ modelType config.SelectedModelType,
81+ oAuthProvider OAuthProvider,
82+) (*OAuth, tea.Cmd) {
75 t := com.Styles
83 t := com.Styles
76
84
77 m := OAuth{}
85 m := OAuth{}
78 m.com = com
86 m.com = com
87+ m.isOnboarding = isOnboarding
79 m.provider = provider
88 m.provider = provider
80 m.model = model
89 m.model = model
81 m.modelType = modelType
90 m.modelType = modelType
@@ -175,9 +184,14 @@ func (m *OAuth) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
175 var (
184 var (
176 t = m.com.Styles
185 t = m.com.Styles
177 dialogStyle = t.Dialog.View.Width(m.width)
186 dialogStyle = t.Dialog.View.Width(m.width)
178− view = dialogStyle.Render(m.dialogContent())
179 )
187 )
188+ if m.isOnboarding {
189+ view := m.dialogContent()
180− DrawCenterCursor(scr, area, view, nil)
190+ DrawOnboarding(scr, area, view)
191+ } else {
192+ view := dialogStyle.Render(m.dialogContent())
193+ DrawCenter(scr, area, view)
194+ }
181 return nil
195 return nil
182}
196}
183
197
@@ -205,10 +219,15 @@ func (m *OAuth) headerContent() string {
205 var (
219 var (
206 t = m.com.Styles
220 t = m.com.Styles
207 titleStyle = t.Dialog.Title
221 titleStyle = t.Dialog.Title
222+ textStyle = t.Dialog.PrimaryText
208 dialogStyle = t.Dialog.View.Width(m.width)
223 dialogStyle = t.Dialog.View.Width(m.width)
209 headerOffset = titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
224 headerOffset = titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
225+ dialogTitle = fmt.Sprintf("Authenticate with %s", m.oAuthProvider.name())
210 )
226 )
227+ if m.isOnboarding {
228+ return textStyle.Render(dialogTitle)
229+ }
211− return common.DialogTitle(t, titleStyle.Render("Authenticate with "+m.oAuthProvider.name()), m.width-headerOffset)
230+ return common.DialogTitle(t, titleStyle.Render(dialogTitle), m.width-headerOffset)
212}
231}
213
232
214func (m *OAuth) innerDialogContent() string {
233func (m *OAuth) innerDialogContent() string {
internal/ui/dialog/oauth_copilot.go+8−2
@@ -12,8 +12,14 @@ import (
12 "github.com/charmbracelet/crush/internal/ui/common"
12 "github.com/charmbracelet/crush/internal/ui/common"
13)
13)
14
14
15+func NewOAuthCopilot(
16+ com *common.Common,
17+ isOnboarding bool,
18+ provider catwalk.Provider,
19+ model config.SelectedModel,
15−func NewOAuthCopilot(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, tea.Cmd) {
20+ modelType config.SelectedModelType,
21+) (*OAuth, tea.Cmd) {
16− return newOAuth(com, provider, model, modelType, &OAuthCopilot{})
22+ return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthCopilot{})
17}
23}
18
24
19type OAuthCopilot struct {
25type OAuthCopilot struct {
internal/ui/dialog/oauth_hyper.go+8−2
@@ -12,8 +12,14 @@ import (
12 "github.com/charmbracelet/crush/internal/ui/common"
12 "github.com/charmbracelet/crush/internal/ui/common"
13)
13)
14
14
15+func NewOAuthHyper(
16+ com *common.Common,
17+ isOnboarding bool,
18+ provider catwalk.Provider,
19+ model config.SelectedModel,
15−func NewOAuthHyper(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, tea.Cmd) {
20+ modelType config.SelectedModelType,
21+) (*OAuth, tea.Cmd) {
16− return newOAuth(com, provider, model, modelType, &OAuthHyper{})
22+ return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthHyper{})
17}
23}
18
24
19type OAuthHyper struct {
25type OAuthHyper struct {
internal/ui/model/sidebar.go+3−2
@@ -45,14 +45,15 @@ func (m *UI) modelInfo(width int) string {
45 }
45 }
46
46
47 var modelContext *common.ModelContextInfo
47 var modelContext *common.ModelContextInfo
48− if m.session != nil {
48+ if model != nil && m.session != nil {
49 modelContext = &common.ModelContextInfo{
49 modelContext = &common.ModelContextInfo{
50 ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
50 ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
51 Cost: m.session.Cost,
51 Cost: m.session.Cost,
52 ModelContext: model.CatwalkCfg.ContextWindow,
52 ModelContext: model.CatwalkCfg.ContextWindow,
53 }
53 }
54+ return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
54 }
55 }
55− return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
56+ return ""
56}
57}
57
58
58// getDynamicHeightLimits will give us the num of items to show in each section based on the hight
59// getDynamicHeightLimits will give us the num of items to show in each section based on the hight
internal/ui/model/status.go+14−6
@@ -17,10 +17,11 @@ const DefaultStatusTTL = 5 * time.Second
17
17
18// Status is the status bar and help model.
18// Status is the status bar and help model.
19type Status struct {
19type Status struct {
20− com *common.Common
20+ com *common.Common
21+ hideHelp bool
21− help help.Model
22+ help help.Model
22− helpKm help.KeyMap
23+ helpKm help.KeyMap
23− msg uiutil.InfoMsg
24+ msg uiutil.InfoMsg
24}
25}
25
26
26// NewStatus creates a new status bar and help model.
27// NewStatus creates a new status bar and help model.
@@ -58,10 +59,17 @@ func (s *Status) ToggleHelp() {
58 s.help.ShowAll = !s.help.ShowAll
59 s.help.ShowAll = !s.help.ShowAll
59}
60}
60
61
62+// SetHideHelp sets whether the app is on the onboarding flow.
63+func (s *Status) SetHideHelp(hideHelp bool) {
64+ s.hideHelp = hideHelp
65+}
66+
61// Draw draws the status bar onto the screen.
67// Draw draws the status bar onto the screen.
62func (s *Status) Draw(scr uv.Screen, area uv.Rectangle) {
68func (s *Status) Draw(scr uv.Screen, area uv.Rectangle) {
69+ if !s.hideHelp {
63− helpView := s.com.Styles.Status.Help.Render(s.help.View(s.helpKm))
70+ helpView := s.com.Styles.Status.Help.Render(s.help.View(s.helpKm))
64− uv.NewStyledString(helpView).Draw(scr, area)
71+ uv.NewStyledString(helpView).Draw(scr, area)
72+ }
65
73
66 // Render notifications
74 // Render notifications
67 if s.msg.IsEmpty() {
75 if s.msg.IsEmpty() {
internal/ui/model/ui.go+50−12
@@ -299,6 +299,11 @@ func (m *UI) Init() tea.Cmd {
299 if m.QueryCapabilities {
299 if m.QueryCapabilities {
300 cmds = append(cmds, tea.RequestTerminalVersion)
300 cmds = append(cmds, tea.RequestTerminalVersion)
301 }
301 }
302+ if m.state == uiOnboarding {
303+ if cmd := m.openModelsDialog(); cmd != nil {
304+ cmds = append(cmds, cmd)
305+ }
306+ }
302 // load the user commands async
307 // load the user commands async
303 cmds = append(cmds, m.loadCustomCommands())
308 cmds = append(cmds, m.loadCustomCommands())
304 return tea.Batch(cmds...)
309 return tea.Batch(cmds...)
@@ -1028,10 +1033,23 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1028 return tea.Batch(cmds...)
1033 return tea.Batch(cmds...)
1029 }
1034 }
1030
1035
1036+ isOnboarding := m.state == uiOnboarding
1037+
1031 switch msg := action.(type) {
1038 switch msg := action.(type) {
1032 // Generic dialog messages
1039 // Generic dialog messages
1033 case dialog.ActionClose:
1040 case dialog.ActionClose:
1041+ if isOnboarding && m.dialog.ContainsDialog(dialog.ModelsID) {
1042+ break
1043+ }
1044+
1034 m.dialog.CloseFrontDialog()
1045 m.dialog.CloseFrontDialog()
1046+
1047+ if isOnboarding {
1048+ if cmd := m.openModelsDialog(); cmd != nil {
1049+ cmds = append(cmds, cmd)
1050+ }
1051+ }
1052+
1035 if m.focus == uiFocusEditor {
1053 if m.focus == uiFocusEditor {
1036 cmds = append(cmds, m.textarea.Focus())
1054 cmds = append(cmds, m.textarea.Focus())
1037 }
1055 }
@@ -1164,10 +1182,18 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1164
1182
1165 if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil {
1183 if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil {
1166 cmds = append(cmds, uiutil.ReportError(err))
1184 cmds = append(cmds, uiutil.ReportError(err))
1185+ } else if _, ok := cfg.Models[config.SelectedModelTypeSmall]; !ok {
1186+ // Ensure small model is set is unset.
1187+ smallModel := m.com.App.GetDefaultSmallModel(providerID)
1188+ if err := cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallModel); err != nil {
1189+ cmds = append(cmds, uiutil.ReportError(err))
1190+ }
1167 }
1191 }
1168
1192
1169 cmds = append(cmds, func() tea.Msg {
1193 cmds = append(cmds, func() tea.Msg {
1170− m.com.App.UpdateAgentModel(context.TODO())
1194+ if err := m.com.App.UpdateAgentModel(context.TODO()); err != nil {
1195+ return uiutil.ReportError(err)
1196+ }
1171
1197
1172 modelMsg := fmt.Sprintf("%s model changed to %s", msg.ModelType, msg.Model.Model)
1198 modelMsg := fmt.Sprintf("%s model changed to %s", msg.ModelType, msg.Model.Model)
1173
1199
@@ -1177,6 +1203,16 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1177 m.dialog.CloseDialog(dialog.APIKeyInputID)
1203 m.dialog.CloseDialog(dialog.APIKeyInputID)
1178 m.dialog.CloseDialog(dialog.OAuthID)
1204 m.dialog.CloseDialog(dialog.OAuthID)
1179 m.dialog.CloseDialog(dialog.ModelsID)
1205 m.dialog.CloseDialog(dialog.ModelsID)
1206+
1207+ if isOnboarding {
1208+ m.state = uiLanding
1209+ m.focus = uiFocusEditor
1210+
1211+ m.com.Config().SetupAgents()
1212+ if err := m.com.App.InitCoderAgent(context.TODO()); err != nil {
1213+ cmds = append(cmds, uiutil.ReportError(err))
1214+ }
1215+ }
1180 case dialog.ActionSelectReasoningEffort:
1216 case dialog.ActionSelectReasoningEffort:
1181 if m.isAgentBusy() {
1217 if m.isAgentBusy() {
1182 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
1218 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
@@ -1284,15 +1320,17 @@ func (m *UI) openAuthenticationDialog(provider catwalk.Provider, model config.Se
1284 var (
1320 var (
1285 dlg dialog.Dialog
1321 dlg dialog.Dialog
1286 cmd tea.Cmd
1322 cmd tea.Cmd
1323+
1324+ isOnboarding = m.state == uiOnboarding
1287 )
1325 )
1288
1326
1289 switch provider.ID {
1327 switch provider.ID {
1290 case "hyper":
1328 case "hyper":
1291− dlg, cmd = dialog.NewOAuthHyper(m.com, provider, model, modelType)
1329+ dlg, cmd = dialog.NewOAuthHyper(m.com, isOnboarding, provider, model, modelType)
1292 case catwalk.InferenceProviderCopilot:
1330 case catwalk.InferenceProviderCopilot:
1293− dlg, cmd = dialog.NewOAuthCopilot(m.com, provider, model, modelType)
1331+ dlg, cmd = dialog.NewOAuthCopilot(m.com, isOnboarding, provider, model, modelType)
1294 default:
1332 default:
1295− dlg, cmd = dialog.NewAPIKeyInput(m.com, provider, model, modelType)
1333+ dlg, cmd = dialog.NewAPIKeyInput(m.com, isOnboarding, provider, model, modelType)
1296 }
1334 }
1297
1335
1298 if m.dialog.ContainsDialog(dlg.ID()) {
1336 if m.dialog.ContainsDialog(dlg.ID()) {
@@ -1648,12 +1686,8 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1648 header := uv.NewStyledString(m.header)
1686 header := uv.NewStyledString(m.header)
1649 header.Draw(scr, layout.header)
1687 header.Draw(scr, layout.header)
1650
1688
1651− mainView := lipgloss.NewStyle().Width(layout.main.Dx()).
1689+ // NOTE: Onboarding flow will be rendered as dialogs below, but
1652− Height(layout.main.Dy()).
1690+ // positioned at the bottom left of the screen.
1653− Background(lipgloss.ANSIColor(rand.Intn(256))).
1654− Render(" Onboarding ")
1655− main := uv.NewStyledString(mainView)
1656− main.Draw(scr, layout.main)
1657
1691
1658 case uiInitialize:
1692 case uiInitialize:
1659 header := uv.NewStyledString(m.header)
1693 header := uv.NewStyledString(m.header)
@@ -1697,11 +1731,14 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1697 }
1731 }
1698 }
1732 }
1699
1733
1734+ isOnboarding := m.state == uiOnboarding
1735+
1700 // Add status and help layer
1736 // Add status and help layer
1737+ m.status.SetHideHelp(isOnboarding)
1701 m.status.Draw(scr, layout.status)
1738 m.status.Draw(scr, layout.status)
1702
1739
1703 // Draw completions popup if open
1740 // Draw completions popup if open
1704− if m.completionsOpen && m.completions.HasItems() {
1741+ if !isOnboarding && m.completionsOpen && m.completions.HasItems() {
1705 w, h := m.completions.Size()
1742 w, h := m.completions.Size()
1706 x := m.completionsPositionStart.X
1743 x := m.completionsPositionStart.X
1707 y := m.completionsPositionStart.Y - h
1744 y := m.completionsPositionStart.Y - h
@@ -2606,7 +2643,8 @@ func (m *UI) openModelsDialog() tea.Cmd {
2606 return nil
2643 return nil
2607 }
2644 }
2608
2645
2646+ isOnboarding := m.state == uiOnboarding
2609− modelsDialog, err := dialog.NewModels(m.com)
2647+ modelsDialog, err := dialog.NewModels(m.com, isOnboarding)
2610 if err != nil {
2648 if err != nil {
2611 return uiutil.ReportError(err)
2649 return uiutil.ReportError(err)
2612 }
2650 }