on main help sign in
feat: implement onboarding flow on the new ui codebase
commit28e0ff355198c3858f45fe87fea3d389f9a47b8e authorAndrey Nering <andreynering@users.noreply.github.com> dateTue 20 Jan 2026, 18:05 (9 months ago) parent1e691e0
12 files changed+233−58
internal/app/app.go+3−2 internal/ui/common/common.go+10 internal/ui/dialog/api_key_input.go+39−14 internal/ui/dialog/common.go+9−2 internal/ui/dialog/dialog.go+17−1 internal/ui/dialog/models.go+48−10 internal/ui/dialog/oauth.go+24−5 internal/ui/dialog/oauth_copilot.go+8−2 internal/ui/dialog/oauth_hyper.go+8−2 internal/ui/model/sidebar.go+3−2 internal/ui/model/status.go+14−6 internal/ui/model/ui.go+50−12
internal/app/app.go+3−2
@@ -351,15 +351,16 @@ func (app *App) overrideModelsForNonInteractive(ctx context.Context, largeModel,
351351
352352 case largeModel != "":
353353 // No small model specified, but large model was - use provider's default.
354− smallCfg := app.getDefaultSmallModel(largeProviderID)
354+ smallCfg := app.GetDefaultSmallModel(largeProviderID)
355355 app.config.Models[config.SelectedModelTypeSmall] = smallCfg
356356 }
357357
358358 return app.AgentCoordinator.UpdateModels(ctx)
359359}
360360
361+// GetDefaultSmallModel returns the default small model for the given
361362// 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 {
363364 cfg := app.config
364365 largeModelCfg := cfg.Models[config.SelectedModelTypeLarge]
365366
internal/ui/common/common.go+10
@@ -52,6 +52,16 @@ func CenterRect(area uv.Rectangle, width, height int) uv.Rectangle {
5252 return image.Rect(minX, minY, maxX, maxY)
5353}
5454
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+
5565// IsFileTooBig checks if the file at the given path exceeds the specified size
5666// limit.
5767func 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"
3333
3434// APIKeyInput represents a model selection dialog.
3535type APIKeyInput struct {
36− com *common.Common
36+ com *common.Common
37+ isOnboarding bool
3738
3839 provider catwalk.Provider
3940 model config.SelectedModel
@@ -54,11 +55,18 @@ type APIKeyInput struct {
5455var _ Dialog = (*APIKeyInput)(nil)
5556
5657// 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) {
5865 t := com.Styles
5966
6067 m := APIKeyInput{}
6168 m.com = com
69+ m.isOnboarding = isOnboarding
6270 m.provider = provider
6371 m.model = model
6472 m.modelType = modelType
@@ -170,28 +178,45 @@ func (m *APIKeyInput) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
170178 helpStyle.Render(m.help.View(m)),
171179 }, "\n")
172180
173− view := dialogStyle.Render(content)
174−
175181 cur := m.Cursor()
176− DrawCenterCursor(scr, area, view, cur)
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)
194+ DrawCenterCursor(scr, area, view, cur)
195+ }
177196 return cur
178197}
179198
180199func (m *APIKeyInput) headerView() string {
181− t := m.com.Styles
182− titleStyle := t.Dialog.Title
183− dialogStyle := t.Dialog.View.Width(m.width)
184−
200+ var (
201+ t = m.com.Styles
202+ titleStyle = t.Dialog.Title
203+ textStyle = t.Dialog.PrimaryText
204+ dialogStyle = t.Dialog.View.Width(m.width)
205+ )
206+ if m.isOnboarding {
207+ return textStyle.Render(m.dialogTitle())
208+ }
185209 headerOffset := titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
186210 return common.DialogTitle(t, titleStyle.Render(m.dialogTitle()), m.width-headerOffset)
187211}
188212
189213func (m *APIKeyInput) dialogTitle() string {
190− t := m.com.Styles
191− textStyle := t.Dialog.TitleText
192− errorStyle := t.Dialog.TitleError
193− accentStyle := t.Dialog.TitleAccent
194−
214+ var (
215+ t = m.com.Styles
216+ textStyle = t.Dialog.TitleText
217+ errorStyle = t.Dialog.TitleError
218+ accentStyle = t.Dialog.TitleAccent
219+ )
195220 switch m.state {
196221 case APIKeyInputStateInitial:
197222 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 {
5959 // Help is the help view content. This will be appended to the content parts
6060 // slice using the default dialog help style.
6161 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
6266}
6367
6468// NewRenderContext creates a new RenderContext with the provided styles and width.
@@ -82,7 +86,8 @@ func (rc *RenderContext) Render() string {
8286 titleStyle := rc.Styles.Dialog.Title
8387 dialogStyle := rc.Styles.Dialog.View.Width(rc.Width)
8488
85− parts := []string{}
89+ var parts []string
90+
8691 if len(rc.Title) > 0 {
8792 var titleInfoWidth int
8893 if len(rc.TitleInfo) > 0 {
@@ -125,6 +130,8 @@ func (rc *RenderContext) Render() string {
125130 }
126131
127132 content := strings.Join(parts, "\n")
128−
133+ if rc.IsOnboarding {
134+ return content
135+ }
129136 return dialogStyle.Render(content)
130137}
internal/ui/dialog/dialog.go+17−1
@@ -170,7 +170,6 @@ func DrawCenterCursor(scr uv.Screen, area uv.Rectangle, view string, cur *tea.Cu
170170 cur.X += center.Min.X
171171 cur.Y += center.Min.Y
172172 }
173−
174173 uv.NewStyledString(view).Draw(scr, center)
175174}
176175
@@ -179,6 +178,23 @@ func DrawCenter(scr uv.Screen, area uv.Rectangle, view string) {
179178 DrawCenterCursor(scr, area, view, nil)
180179}
181180
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+
182198// Draw renders the overlay and its dialogs.
183199func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
184200 var cur *tea.Cursor
internal/ui/dialog/models.go+48−10
@@ -62,8 +62,9 @@ func (mt ModelType) Placeholder() string {
6262}
6363
6464const (
65− largeModelInputPlaceholder = "Choose a model for large, complex tasks"
66− smallModelInputPlaceholder = "Choose a model for small, simple tasks"
65+ onboardingModelInputPlaceholder = "Find your fave"
66+ largeModelInputPlaceholder = "Choose a model for large, complex tasks"
67+ smallModelInputPlaceholder = "Choose a model for small, simple tasks"
6768)
6869
6970// ModelsID is the identifier for the model selection dialog.
@@ -73,7 +74,8 @@ const defaultModelsDialogMaxWidth = 70
7374
7475// Models represents a model selection dialog.
7576type Models struct {
76− com *common.Common
77+ com *common.Common
78+ isOnboarding bool
7779
7880 modelType ModelType
7981 providers []catwalk.Provider
@@ -94,10 +96,12 @@ type Models struct {
9496var _ Dialog = (*Models)(nil)
9597
9698// NewModels creates a new Models dialog.
97−func NewModels(com *common.Common) (*Models, error) {
99+func NewModels(com *common.Common, isOnboarding bool) (*Models, error) {
98100 t := com.Styles
99101 m := &Models{}
100102 m.com = com
103+ m.isOnboarding = isOnboarding
104+
101105 help := help.New()
102106 help.Styles = t.DialogHelpStyles()
103107
@@ -108,7 +112,7 @@ func NewModels(com *common.Common) (*Models, error) {
108112
109113 m.input = textinput.New()
110114 m.input.SetVirtualCursor(false)
111− m.input.Placeholder = largeModelInputPlaceholder
115+ m.input.Placeholder = onboardingModelInputPlaceholder
112116 m.input.SetStyles(com.Styles.TextInput)
113117 m.input.Focus()
114118
@@ -194,6 +198,9 @@ func (m *Models) HandleMsg(msg tea.Msg) Action {
194198 ModelType: modelItem.SelectedModelType(),
195199 }
196200 case key.Matches(msg, m.keyMap.Tab):
201+ if m.isOnboarding {
202+ break
203+ }
197204 if m.modelType == ModelTypeLarge {
198205 m.modelType = ModelTypeSmall
199206 } else {
@@ -251,6 +258,7 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
251258 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
252259 t.Dialog.HelpView.GetVerticalFrameSize() +
253260 t.Dialog.View.GetVerticalFrameSize()
261+
254262 m.input.SetWidth(max(0, innerWidth-t.Dialog.InputPrompt.GetHorizontalFrameSize()-1)) // (1) cursor padding
255263 m.list.SetSize(innerWidth, height-heightOffset)
256264 m.help.SetWidth(innerWidth)
@@ -258,21 +266,49 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
258266 rc := NewRenderContext(t, width)
259267 rc.Title = "Switch Model"
260268 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+
261275 inputView := t.Dialog.InputPrompt.Render(m.input.View())
262276 rc.AddPart(inputView)
277+
263278 listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render())
264279 rc.AddPart(listView)
265− rc.Help = m.help.View(m)
266280
267− view := rc.Render()
281+ rc.Help = m.help.View(m)
268282
269283 cur := m.Cursor()
270− DrawCenterCursor(scr, area, view, cur)
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()
299+ DrawCenterCursor(scr, area, view, cur)
300+ }
271301 return cur
272302}
273303
274304// ShortHelp returns the short help view.
275305func (m *Models) ShortHelp() []key.Binding {
306+ if m.isOnboarding {
307+ return []key.Binding{
308+ m.keyMap.UpDown,
309+ m.keyMap.Select,
310+ }
311+ }
276312 return []key.Binding{
277313 m.keyMap.UpDown,
278314 m.keyMap.Tab,
@@ -459,10 +495,12 @@ func (m *Models) setProviderItems() error {
459495 // Set model groups in the list.
460496 m.list.SetGroups(groups...)
461497 m.list.SetSelectedItem(selectedItemID)
462− m.list.ScrollToSelected()
498+ m.list.ScrollToTop()
463499
464500 // Update placeholder based on model type
465− m.input.Placeholder = m.modelType.Placeholder()
501+ if !m.isOnboarding {
502+ m.input.Placeholder = m.modelType.Placeholder()
503+ }
466504
467505 return nil
468506}
internal/ui/dialog/oauth.go+24−5
@@ -41,7 +41,8 @@ const OAuthID = "oauth"
4141
4242// OAuth handles the OAuth flow authentication.
4343type OAuth struct {
44− com *common.Common
44+ com *common.Common
45+ isOnboarding bool
4546
4647 provider catwalk.Provider
4748 model config.SelectedModel
@@ -71,11 +72,19 @@ type OAuth struct {
7172var _ Dialog = (*OAuth)(nil)
7273
7374// 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) {
7583 t := com.Styles
7684
7785 m := OAuth{}
7886 m.com = com
87+ m.isOnboarding = isOnboarding
7988 m.provider = provider
8089 m.model = model
8190 m.modelType = modelType
@@ -175,9 +184,14 @@ func (m *OAuth) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
175184 var (
176185 t = m.com.Styles
177186 dialogStyle = t.Dialog.View.Width(m.width)
178− view = dialogStyle.Render(m.dialogContent())
179187 )
180− DrawCenterCursor(scr, area, view, nil)
188+ if m.isOnboarding {
189+ view := m.dialogContent()
190+ DrawOnboarding(scr, area, view)
191+ } else {
192+ view := dialogStyle.Render(m.dialogContent())
193+ DrawCenter(scr, area, view)
194+ }
181195 return nil
182196}
183197
@@ -205,10 +219,15 @@ func (m *OAuth) headerContent() string {
205219 var (
206220 t = m.com.Styles
207221 titleStyle = t.Dialog.Title
222+ textStyle = t.Dialog.PrimaryText
208223 dialogStyle = t.Dialog.View.Width(m.width)
209224 headerOffset = titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
225+ dialogTitle = fmt.Sprintf("Authenticate with %s", m.oAuthProvider.name())
210226 )
211− return common.DialogTitle(t, titleStyle.Render("Authenticate with "+m.oAuthProvider.name()), m.width-headerOffset)
227+ if m.isOnboarding {
228+ return textStyle.Render(dialogTitle)
229+ }
230+ return common.DialogTitle(t, titleStyle.Render(dialogTitle), m.width-headerOffset)
212231}
213232
214233func (m *OAuth) innerDialogContent() string {
internal/ui/dialog/oauth_copilot.go+8−2
@@ -12,8 +12,14 @@ import (
1212 "github.com/charmbracelet/crush/internal/ui/common"
1313)
1414
15−func NewOAuthCopilot(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, tea.Cmd) {
16− return newOAuth(com, provider, model, modelType, &OAuthCopilot{})
15+func NewOAuthCopilot(
16+ com *common.Common,
17+ isOnboarding bool,
18+ provider catwalk.Provider,
19+ model config.SelectedModel,
20+ modelType config.SelectedModelType,
21+) (*OAuth, tea.Cmd) {
22+ return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthCopilot{})
1723}
1824
1925type OAuthCopilot struct {
internal/ui/dialog/oauth_hyper.go+8−2
@@ -12,8 +12,14 @@ import (
1212 "github.com/charmbracelet/crush/internal/ui/common"
1313)
1414
15−func NewOAuthHyper(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, tea.Cmd) {
16− return newOAuth(com, provider, model, modelType, &OAuthHyper{})
15+func NewOAuthHyper(
16+ com *common.Common,
17+ isOnboarding bool,
18+ provider catwalk.Provider,
19+ model config.SelectedModel,
20+ modelType config.SelectedModelType,
21+) (*OAuth, tea.Cmd) {
22+ return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthHyper{})
1723}
1824
1925type OAuthHyper struct {
internal/ui/model/sidebar.go+3−2
@@ -45,14 +45,15 @@ func (m *UI) modelInfo(width int) string {
4545 }
4646
4747 var modelContext *common.ModelContextInfo
48− if m.session != nil {
48+ if model != nil && m.session != nil {
4949 modelContext = &common.ModelContextInfo{
5050 ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
5151 Cost: m.session.Cost,
5252 ModelContext: model.CatwalkCfg.ContextWindow,
5353 }
54+ return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
5455 }
55− return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
56+ return ""
5657}
5758
5859// 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
1717
1818// Status is the status bar and help model.
1919type Status struct {
20− com *common.Common
21− help help.Model
22− helpKm help.KeyMap
23− msg uiutil.InfoMsg
20+ com *common.Common
21+ hideHelp bool
22+ help help.Model
23+ helpKm help.KeyMap
24+ msg uiutil.InfoMsg
2425}
2526
2627// NewStatus creates a new status bar and help model.
@@ -58,10 +59,17 @@ func (s *Status) ToggleHelp() {
5859 s.help.ShowAll = !s.help.ShowAll
5960}
6061
62+// SetHideHelp sets whether the app is on the onboarding flow.
63+func (s *Status) SetHideHelp(hideHelp bool) {
64+ s.hideHelp = hideHelp
65+}
66+
6167// Draw draws the status bar onto the screen.
6268func (s *Status) Draw(scr uv.Screen, area uv.Rectangle) {
63− helpView := s.com.Styles.Status.Help.Render(s.help.View(s.helpKm))
64− uv.NewStyledString(helpView).Draw(scr, area)
69+ if !s.hideHelp {
70+ helpView := s.com.Styles.Status.Help.Render(s.help.View(s.helpKm))
71+ uv.NewStyledString(helpView).Draw(scr, area)
72+ }
6573
6674 // Render notifications
6775 if s.msg.IsEmpty() {
internal/ui/model/ui.go+50−12
@@ -299,6 +299,11 @@ func (m *UI) Init() tea.Cmd {
299299 if m.QueryCapabilities {
300300 cmds = append(cmds, tea.RequestTerminalVersion)
301301 }
302+ if m.state == uiOnboarding {
303+ if cmd := m.openModelsDialog(); cmd != nil {
304+ cmds = append(cmds, cmd)
305+ }
306+ }
302307 // load the user commands async
303308 cmds = append(cmds, m.loadCustomCommands())
304309 return tea.Batch(cmds...)
@@ -1028,10 +1033,23 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
10281033 return tea.Batch(cmds...)
10291034 }
10301035
1036+ isOnboarding := m.state == uiOnboarding
1037+
10311038 switch msg := action.(type) {
10321039 // Generic dialog messages
10331040 case dialog.ActionClose:
1041+ if isOnboarding && m.dialog.ContainsDialog(dialog.ModelsID) {
1042+ break
1043+ }
1044+
10341045 m.dialog.CloseFrontDialog()
1046+
1047+ if isOnboarding {
1048+ if cmd := m.openModelsDialog(); cmd != nil {
1049+ cmds = append(cmds, cmd)
1050+ }
1051+ }
1052+
10351053 if m.focus == uiFocusEditor {
10361054 cmds = append(cmds, m.textarea.Focus())
10371055 }
@@ -1164,10 +1182,18 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
11641182
11651183 if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil {
11661184 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+ }
11671191 }
11681192
11691193 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+ }
11711197
11721198 modelMsg := fmt.Sprintf("%s model changed to %s", msg.ModelType, msg.Model.Model)
11731199
@@ -1177,6 +1203,16 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
11771203 m.dialog.CloseDialog(dialog.APIKeyInputID)
11781204 m.dialog.CloseDialog(dialog.OAuthID)
11791205 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+ }
11801216 case dialog.ActionSelectReasoningEffort:
11811217 if m.isAgentBusy() {
11821218 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
@@ -1284,15 +1320,17 @@ func (m *UI) openAuthenticationDialog(provider catwalk.Provider, model config.Se
12841320 var (
12851321 dlg dialog.Dialog
12861322 cmd tea.Cmd
1323+
1324+ isOnboarding = m.state == uiOnboarding
12871325 )
12881326
12891327 switch provider.ID {
12901328 case "hyper":
1291− dlg, cmd = dialog.NewOAuthHyper(m.com, provider, model, modelType)
1329+ dlg, cmd = dialog.NewOAuthHyper(m.com, isOnboarding, provider, model, modelType)
12921330 case catwalk.InferenceProviderCopilot:
1293− dlg, cmd = dialog.NewOAuthCopilot(m.com, provider, model, modelType)
1331+ dlg, cmd = dialog.NewOAuthCopilot(m.com, isOnboarding, provider, model, modelType)
12941332 default:
1295− dlg, cmd = dialog.NewAPIKeyInput(m.com, provider, model, modelType)
1333+ dlg, cmd = dialog.NewAPIKeyInput(m.com, isOnboarding, provider, model, modelType)
12961334 }
12971335
12981336 if m.dialog.ContainsDialog(dlg.ID()) {
@@ -1648,12 +1686,8 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
16481686 header := uv.NewStyledString(m.header)
16491687 header.Draw(scr, layout.header)
16501688
1651− mainView := lipgloss.NewStyle().Width(layout.main.Dx()).
1652− Height(layout.main.Dy()).
1653− Background(lipgloss.ANSIColor(rand.Intn(256))).
1654− Render(" Onboarding ")
1655− main := uv.NewStyledString(mainView)
1656− main.Draw(scr, layout.main)
1689+ // NOTE: Onboarding flow will be rendered as dialogs below, but
1690+ // positioned at the bottom left of the screen.
16571691
16581692 case uiInitialize:
16591693 header := uv.NewStyledString(m.header)
@@ -1697,11 +1731,14 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
16971731 }
16981732 }
16991733
1734+ isOnboarding := m.state == uiOnboarding
1735+
17001736 // Add status and help layer
1737+ m.status.SetHideHelp(isOnboarding)
17011738 m.status.Draw(scr, layout.status)
17021739
17031740 // Draw completions popup if open
1704− if m.completionsOpen && m.completions.HasItems() {
1741+ if !isOnboarding && m.completionsOpen && m.completions.HasItems() {
17051742 w, h := m.completions.Size()
17061743 x := m.completionsPositionStart.X
17071744 y := m.completionsPositionStart.Y - h
@@ -2606,7 +2643,8 @@ func (m *UI) openModelsDialog() tea.Cmd {
26062643 return nil
26072644 }
26082645
2609− modelsDialog, err := dialog.NewModels(m.com)
2646+ isOnboarding := m.state == uiOnboarding
2647+ modelsDialog, err := dialog.NewModels(m.com, isOnboarding)
26102648 if err != nil {
26112649 return uiutil.ReportError(err)
26122650 }