From 28e0ff355198c3858f45fe87fea3d389f9a47b8e Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Tue, 20 Jan 2026 18:05:11 -0300 Subject: [PATCH] feat: implement onboarding flow on the new ui codebase --- internal/app/app.go | 5 ++- internal/ui/common/common.go | 10 +++++ internal/ui/dialog/api_key_input.go | 53 +++++++++++++++++------- internal/ui/dialog/common.go | 11 ++++- internal/ui/dialog/dialog.go | 18 ++++++++- internal/ui/dialog/models.go | 58 ++++++++++++++++++++++----- internal/ui/dialog/oauth.go | 29 +++++++++++--- internal/ui/dialog/oauth_copilot.go | 10 ++++- internal/ui/dialog/oauth_hyper.go | 10 ++++- internal/ui/model/sidebar.go | 5 ++- internal/ui/model/status.go | 20 +++++++--- internal/ui/model/ui.go | 62 +++++++++++++++++++++++------ 12 files changed, 233 insertions(+), 58 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 816f7a8b25cc945436a148110ce7d774750bb493..b6cb9c8dfb95d79eccec07145f1246e6b8910713 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -351,15 +351,16 @@ func (app *App) overrideModelsForNonInteractive(ctx context.Context, largeModel, case largeModel != "": // No small model specified, but large model was - use provider's default. - smallCfg := app.getDefaultSmallModel(largeProviderID) + smallCfg := app.GetDefaultSmallModel(largeProviderID) app.config.Models[config.SelectedModelTypeSmall] = smallCfg } return app.AgentCoordinator.UpdateModels(ctx) } +// GetDefaultSmallModel returns the default small model for the given // provider. Falls back to the large model if no default is found. -func (app *App) getDefaultSmallModel(providerID string) config.SelectedModel { +func (app *App) GetDefaultSmallModel(providerID string) config.SelectedModel { cfg := app.config largeModelCfg := cfg.Models[config.SelectedModelTypeLarge] diff --git a/internal/ui/common/common.go b/internal/ui/common/common.go index 0150b6e4b84085637178009ec7859ae2f28aaf93..0c811b0384b0b1bf24b227b6500e8c9a21726d21 100644 --- a/internal/ui/common/common.go +++ b/internal/ui/common/common.go @@ -52,6 +52,16 @@ func CenterRect(area uv.Rectangle, width, height int) uv.Rectangle { return image.Rect(minX, minY, maxX, maxY) } +// BottomLeftRect returns a new [Rectangle] positioned at the bottom-left within the given area with the +// specified width and height. +func BottomLeftRect(area uv.Rectangle, width, height int) uv.Rectangle { + minX := area.Min.X + maxX := minX + width + maxY := area.Max.Y + minY := maxY - height + return image.Rect(minX, minY, maxX, maxY) +} + // IsFileTooBig checks if the file at the given path exceeds the specified size // limit. func IsFileTooBig(filePath string, sizeLimit int64) (bool, error) { diff --git a/internal/ui/dialog/api_key_input.go b/internal/ui/dialog/api_key_input.go index 72ff9739a76394c529ee954392e8d9990c70a8a5..01d5e41a1d9d7e4a3fa25db91caa12fb12daea1f 100644 --- a/internal/ui/dialog/api_key_input.go +++ b/internal/ui/dialog/api_key_input.go @@ -33,7 +33,8 @@ const APIKeyInputID = "api_key_input" // APIKeyInput represents a model selection dialog. type APIKeyInput struct { - com *common.Common + com *common.Common + isOnboarding bool provider catwalk.Provider model config.SelectedModel @@ -54,11 +55,18 @@ type APIKeyInput struct { var _ Dialog = (*APIKeyInput)(nil) // NewAPIKeyInput creates a new Models dialog. -func NewAPIKeyInput(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*APIKeyInput, tea.Cmd) { +func NewAPIKeyInput( + com *common.Common, + isOnboarding bool, + provider catwalk.Provider, + model config.SelectedModel, + modelType config.SelectedModelType, +) (*APIKeyInput, tea.Cmd) { t := com.Styles m := APIKeyInput{} m.com = com + m.isOnboarding = isOnboarding m.provider = provider m.model = model m.modelType = modelType @@ -170,28 +178,45 @@ func (m *APIKeyInput) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { helpStyle.Render(m.help.View(m)), }, "\n") - view := dialogStyle.Render(content) - cur := m.Cursor() - DrawCenterCursor(scr, area, view, cur) + + if m.isOnboarding { + view := content + DrawOnboardingCursor(scr, area, view, cur) + + // FIXME(@andreynering): Figure it out how to properly fix this + if cur != nil { + cur.Y -= 1 + cur.X -= 1 + } + } else { + view := dialogStyle.Render(content) + DrawCenterCursor(scr, area, view, cur) + } return cur } func (m *APIKeyInput) headerView() string { - t := m.com.Styles - titleStyle := t.Dialog.Title - dialogStyle := t.Dialog.View.Width(m.width) - + var ( + t = m.com.Styles + titleStyle = t.Dialog.Title + textStyle = t.Dialog.PrimaryText + dialogStyle = t.Dialog.View.Width(m.width) + ) + if m.isOnboarding { + return textStyle.Render(m.dialogTitle()) + } headerOffset := titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize() return common.DialogTitle(t, titleStyle.Render(m.dialogTitle()), m.width-headerOffset) } func (m *APIKeyInput) dialogTitle() string { - t := m.com.Styles - textStyle := t.Dialog.TitleText - errorStyle := t.Dialog.TitleError - accentStyle := t.Dialog.TitleAccent - + var ( + t = m.com.Styles + textStyle = t.Dialog.TitleText + errorStyle = t.Dialog.TitleError + accentStyle = t.Dialog.TitleAccent + ) switch m.state { case APIKeyInputStateInitial: return textStyle.Render("Enter your ") + accentStyle.Render(fmt.Sprintf("%s Key", m.provider.Name)) + textStyle.Render(".") diff --git a/internal/ui/dialog/common.go b/internal/ui/dialog/common.go index 76b75064670935715f03e0d732b9df5070b9e9da..fe54a7e60649a222eabe80e2ecc02546036bac17 100644 --- a/internal/ui/dialog/common.go +++ b/internal/ui/dialog/common.go @@ -59,6 +59,10 @@ type RenderContext struct { // Help is the help view content. This will be appended to the content parts // slice using the default dialog help style. Help string + // IsOnboarding indicates whether to render the dialog as part of the + // onboarding flow. This means that the content will be rendered at the + // bottom left of the screen. + IsOnboarding bool } // NewRenderContext creates a new RenderContext with the provided styles and width. @@ -82,7 +86,8 @@ func (rc *RenderContext) Render() string { titleStyle := rc.Styles.Dialog.Title dialogStyle := rc.Styles.Dialog.View.Width(rc.Width) - parts := []string{} + var parts []string + if len(rc.Title) > 0 { var titleInfoWidth int if len(rc.TitleInfo) > 0 { @@ -125,6 +130,8 @@ func (rc *RenderContext) Render() string { } content := strings.Join(parts, "\n") - + if rc.IsOnboarding { + return content + } return dialogStyle.Render(content) } diff --git a/internal/ui/dialog/dialog.go b/internal/ui/dialog/dialog.go index 7a3db40128fb1e5543a94a93faa4ae9aeec5f947..990b4ed68174bee20d627dec5f7176d9466b77d8 100644 --- a/internal/ui/dialog/dialog.go +++ b/internal/ui/dialog/dialog.go @@ -170,7 +170,6 @@ func DrawCenterCursor(scr uv.Screen, area uv.Rectangle, view string, cur *tea.Cu cur.X += center.Min.X cur.Y += center.Min.Y } - uv.NewStyledString(view).Draw(scr, center) } @@ -179,6 +178,23 @@ func DrawCenter(scr uv.Screen, area uv.Rectangle, view string) { DrawCenterCursor(scr, area, view, nil) } +// DrawOnboarding draws the given string view centered in the screen area. +func DrawOnboarding(scr uv.Screen, area uv.Rectangle, view string) { + DrawOnboardingCursor(scr, area, view, nil) +} + +// DrawOnboardingCursor draws the given string view positioned at the bottom +// left area of the screen. +func DrawOnboardingCursor(scr uv.Screen, area uv.Rectangle, view string, cur *tea.Cursor) { + width, height := lipgloss.Size(view) + bottomLeft := common.BottomLeftRect(area, width, height) + if cur != nil { + cur.X += bottomLeft.Min.X + cur.Y += bottomLeft.Min.Y + } + uv.NewStyledString(view).Draw(scr, bottomLeft) +} + // Draw renders the overlay and its dialogs. func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { var cur *tea.Cursor diff --git a/internal/ui/dialog/models.go b/internal/ui/dialog/models.go index 532fad84acd816d67e88e71eb8160964e70e35b0..450ee8b99b75f13c1c9885281a1dfd1a0a3d9867 100644 --- a/internal/ui/dialog/models.go +++ b/internal/ui/dialog/models.go @@ -62,8 +62,9 @@ func (mt ModelType) Placeholder() string { } const ( - largeModelInputPlaceholder = "Choose a model for large, complex tasks" - smallModelInputPlaceholder = "Choose a model for small, simple tasks" + onboardingModelInputPlaceholder = "Find your fave" + largeModelInputPlaceholder = "Choose a model for large, complex tasks" + smallModelInputPlaceholder = "Choose a model for small, simple tasks" ) // ModelsID is the identifier for the model selection dialog. @@ -73,7 +74,8 @@ const defaultModelsDialogMaxWidth = 70 // Models represents a model selection dialog. type Models struct { - com *common.Common + com *common.Common + isOnboarding bool modelType ModelType providers []catwalk.Provider @@ -94,10 +96,12 @@ type Models struct { var _ Dialog = (*Models)(nil) // NewModels creates a new Models dialog. -func NewModels(com *common.Common) (*Models, error) { +func NewModels(com *common.Common, isOnboarding bool) (*Models, error) { t := com.Styles m := &Models{} m.com = com + m.isOnboarding = isOnboarding + help := help.New() help.Styles = t.DialogHelpStyles() @@ -108,7 +112,7 @@ func NewModels(com *common.Common) (*Models, error) { m.input = textinput.New() m.input.SetVirtualCursor(false) - m.input.Placeholder = largeModelInputPlaceholder + m.input.Placeholder = onboardingModelInputPlaceholder m.input.SetStyles(com.Styles.TextInput) m.input.Focus() @@ -194,6 +198,9 @@ func (m *Models) HandleMsg(msg tea.Msg) Action { ModelType: modelItem.SelectedModelType(), } case key.Matches(msg, m.keyMap.Tab): + if m.isOnboarding { + break + } if m.modelType == ModelTypeLarge { m.modelType = ModelTypeSmall } else { @@ -251,6 +258,7 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight + t.Dialog.HelpView.GetVerticalFrameSize() + t.Dialog.View.GetVerticalFrameSize() + m.input.SetWidth(max(0, innerWidth-t.Dialog.InputPrompt.GetHorizontalFrameSize()-1)) // (1) cursor padding m.list.SetSize(innerWidth, height-heightOffset) m.help.SetWidth(innerWidth) @@ -258,21 +266,49 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { rc := NewRenderContext(t, width) rc.Title = "Switch Model" rc.TitleInfo = m.modelTypeRadioView() + + if m.isOnboarding { + titleText := t.Dialog.PrimaryText.Render("To start, let's choose a provider and model.") + rc.AddPart(titleText) + } + inputView := t.Dialog.InputPrompt.Render(m.input.View()) rc.AddPart(inputView) + listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render()) rc.AddPart(listView) - rc.Help = m.help.View(m) - view := rc.Render() + rc.Help = m.help.View(m) cur := m.Cursor() - DrawCenterCursor(scr, area, view, cur) + + if m.isOnboarding { + rc.Title = "" + rc.TitleInfo = "" + rc.IsOnboarding = true + view := rc.Render() + DrawOnboardingCursor(scr, area, view, cur) + + // FIXME(@andreynering): Figure it out how to properly fix this + if cur != nil { + cur.Y -= 1 + cur.X -= 1 + } + } else { + view := rc.Render() + DrawCenterCursor(scr, area, view, cur) + } return cur } // ShortHelp returns the short help view. func (m *Models) ShortHelp() []key.Binding { + if m.isOnboarding { + return []key.Binding{ + m.keyMap.UpDown, + m.keyMap.Select, + } + } return []key.Binding{ m.keyMap.UpDown, m.keyMap.Tab, @@ -459,10 +495,12 @@ func (m *Models) setProviderItems() error { // Set model groups in the list. m.list.SetGroups(groups...) m.list.SetSelectedItem(selectedItemID) - m.list.ScrollToSelected() + m.list.ScrollToTop() // Update placeholder based on model type - m.input.Placeholder = m.modelType.Placeholder() + if !m.isOnboarding { + m.input.Placeholder = m.modelType.Placeholder() + } return nil } diff --git a/internal/ui/dialog/oauth.go b/internal/ui/dialog/oauth.go index ae5a2ab25a1ec596ba50ea6b3a0d03f560f1b10d..e4c4ec664a893846f0b37fe2d4bfc323ac1773da 100644 --- a/internal/ui/dialog/oauth.go +++ b/internal/ui/dialog/oauth.go @@ -41,7 +41,8 @@ const OAuthID = "oauth" // OAuth handles the OAuth flow authentication. type OAuth struct { - com *common.Common + com *common.Common + isOnboarding bool provider catwalk.Provider model config.SelectedModel @@ -71,11 +72,19 @@ type OAuth struct { var _ Dialog = (*OAuth)(nil) // newOAuth creates a new device flow component. -func newOAuth(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType, oAuthProvider OAuthProvider) (*OAuth, tea.Cmd) { +func newOAuth( + com *common.Common, + isOnboarding bool, + provider catwalk.Provider, + model config.SelectedModel, + modelType config.SelectedModelType, + oAuthProvider OAuthProvider, +) (*OAuth, tea.Cmd) { t := com.Styles m := OAuth{} m.com = com + m.isOnboarding = isOnboarding m.provider = provider m.model = model m.modelType = modelType @@ -175,9 +184,14 @@ func (m *OAuth) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { var ( t = m.com.Styles dialogStyle = t.Dialog.View.Width(m.width) - view = dialogStyle.Render(m.dialogContent()) ) - DrawCenterCursor(scr, area, view, nil) + if m.isOnboarding { + view := m.dialogContent() + DrawOnboarding(scr, area, view) + } else { + view := dialogStyle.Render(m.dialogContent()) + DrawCenter(scr, area, view) + } return nil } @@ -205,10 +219,15 @@ func (m *OAuth) headerContent() string { var ( t = m.com.Styles titleStyle = t.Dialog.Title + textStyle = t.Dialog.PrimaryText dialogStyle = t.Dialog.View.Width(m.width) headerOffset = titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize() + dialogTitle = fmt.Sprintf("Authenticate with %s", m.oAuthProvider.name()) ) - return common.DialogTitle(t, titleStyle.Render("Authenticate with "+m.oAuthProvider.name()), m.width-headerOffset) + if m.isOnboarding { + return textStyle.Render(dialogTitle) + } + return common.DialogTitle(t, titleStyle.Render(dialogTitle), m.width-headerOffset) } func (m *OAuth) innerDialogContent() string { diff --git a/internal/ui/dialog/oauth_copilot.go b/internal/ui/dialog/oauth_copilot.go index 19e389b38a965c4c22ba1b2080b029975aaedc19..4b671852d476578f94653393796056d630ba23a5 100644 --- a/internal/ui/dialog/oauth_copilot.go +++ b/internal/ui/dialog/oauth_copilot.go @@ -12,8 +12,14 @@ import ( "github.com/charmbracelet/crush/internal/ui/common" ) -func NewOAuthCopilot(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, tea.Cmd) { - return newOAuth(com, provider, model, modelType, &OAuthCopilot{}) +func NewOAuthCopilot( + com *common.Common, + isOnboarding bool, + provider catwalk.Provider, + model config.SelectedModel, + modelType config.SelectedModelType, +) (*OAuth, tea.Cmd) { + return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthCopilot{}) } type OAuthCopilot struct { diff --git a/internal/ui/dialog/oauth_hyper.go b/internal/ui/dialog/oauth_hyper.go index 478960b0df10f62d88b65450de360f4db6d6cd0c..bddf4d78ef2c920855f21e056e7ee48f985b0b68 100644 --- a/internal/ui/dialog/oauth_hyper.go +++ b/internal/ui/dialog/oauth_hyper.go @@ -12,8 +12,14 @@ import ( "github.com/charmbracelet/crush/internal/ui/common" ) -func NewOAuthHyper(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, tea.Cmd) { - return newOAuth(com, provider, model, modelType, &OAuthHyper{}) +func NewOAuthHyper( + com *common.Common, + isOnboarding bool, + provider catwalk.Provider, + model config.SelectedModel, + modelType config.SelectedModelType, +) (*OAuth, tea.Cmd) { + return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthHyper{}) } type OAuthHyper struct { diff --git a/internal/ui/model/sidebar.go b/internal/ui/model/sidebar.go index 2437fab9177b9186cfcd4c185c45c48204cea7d9..a0623a1262863da749bbaebdfb2f42eccef7cf50 100644 --- a/internal/ui/model/sidebar.go +++ b/internal/ui/model/sidebar.go @@ -45,14 +45,15 @@ func (m *UI) modelInfo(width int) string { } var modelContext *common.ModelContextInfo - if m.session != nil { + if model != nil && m.session != nil { modelContext = &common.ModelContextInfo{ ContextUsed: m.session.CompletionTokens + m.session.PromptTokens, Cost: m.session.Cost, ModelContext: model.CatwalkCfg.ContextWindow, } + return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width) } - return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width) + return "" } // getDynamicHeightLimits will give us the num of items to show in each section based on the hight diff --git a/internal/ui/model/status.go b/internal/ui/model/status.go index a3371d27d2f19f3236734ea8a31602fa5d518e62..2e1b9396e32b970c663cb755e2dc74f6c9f5eca0 100644 --- a/internal/ui/model/status.go +++ b/internal/ui/model/status.go @@ -17,10 +17,11 @@ const DefaultStatusTTL = 5 * time.Second // Status is the status bar and help model. type Status struct { - com *common.Common - help help.Model - helpKm help.KeyMap - msg uiutil.InfoMsg + com *common.Common + hideHelp bool + help help.Model + helpKm help.KeyMap + msg uiutil.InfoMsg } // NewStatus creates a new status bar and help model. @@ -58,10 +59,17 @@ func (s *Status) ToggleHelp() { s.help.ShowAll = !s.help.ShowAll } +// SetHideHelp sets whether the app is on the onboarding flow. +func (s *Status) SetHideHelp(hideHelp bool) { + s.hideHelp = hideHelp +} + // Draw draws the status bar onto the screen. func (s *Status) Draw(scr uv.Screen, area uv.Rectangle) { - helpView := s.com.Styles.Status.Help.Render(s.help.View(s.helpKm)) - uv.NewStyledString(helpView).Draw(scr, area) + if !s.hideHelp { + helpView := s.com.Styles.Status.Help.Render(s.help.View(s.helpKm)) + uv.NewStyledString(helpView).Draw(scr, area) + } // Render notifications if s.msg.IsEmpty() { diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index a224179689594cc6c5fd390deded65d93e57f0fe..ec17627a43fc70c945dd22cf5eee5082c5d5eac2 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -299,6 +299,11 @@ func (m *UI) Init() tea.Cmd { if m.QueryCapabilities { cmds = append(cmds, tea.RequestTerminalVersion) } + if m.state == uiOnboarding { + if cmd := m.openModelsDialog(); cmd != nil { + cmds = append(cmds, cmd) + } + } // load the user commands async cmds = append(cmds, m.loadCustomCommands()) return tea.Batch(cmds...) @@ -1028,10 +1033,23 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd { return tea.Batch(cmds...) } + isOnboarding := m.state == uiOnboarding + switch msg := action.(type) { // Generic dialog messages case dialog.ActionClose: + if isOnboarding && m.dialog.ContainsDialog(dialog.ModelsID) { + break + } + m.dialog.CloseFrontDialog() + + if isOnboarding { + if cmd := m.openModelsDialog(); cmd != nil { + cmds = append(cmds, cmd) + } + } + if m.focus == uiFocusEditor { cmds = append(cmds, m.textarea.Focus()) } @@ -1164,10 +1182,18 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd { if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil { cmds = append(cmds, uiutil.ReportError(err)) + } else if _, ok := cfg.Models[config.SelectedModelTypeSmall]; !ok { + // Ensure small model is set is unset. + smallModel := m.com.App.GetDefaultSmallModel(providerID) + if err := cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallModel); err != nil { + cmds = append(cmds, uiutil.ReportError(err)) + } } cmds = append(cmds, func() tea.Msg { - m.com.App.UpdateAgentModel(context.TODO()) + if err := m.com.App.UpdateAgentModel(context.TODO()); err != nil { + return uiutil.ReportError(err) + } modelMsg := fmt.Sprintf("%s model changed to %s", msg.ModelType, msg.Model.Model) @@ -1177,6 +1203,16 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd { m.dialog.CloseDialog(dialog.APIKeyInputID) m.dialog.CloseDialog(dialog.OAuthID) m.dialog.CloseDialog(dialog.ModelsID) + + if isOnboarding { + m.state = uiLanding + m.focus = uiFocusEditor + + m.com.Config().SetupAgents() + if err := m.com.App.InitCoderAgent(context.TODO()); err != nil { + cmds = append(cmds, uiutil.ReportError(err)) + } + } case dialog.ActionSelectReasoningEffort: if m.isAgentBusy() { cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait...")) @@ -1284,15 +1320,17 @@ func (m *UI) openAuthenticationDialog(provider catwalk.Provider, model config.Se var ( dlg dialog.Dialog cmd tea.Cmd + + isOnboarding = m.state == uiOnboarding ) switch provider.ID { case "hyper": - dlg, cmd = dialog.NewOAuthHyper(m.com, provider, model, modelType) + dlg, cmd = dialog.NewOAuthHyper(m.com, isOnboarding, provider, model, modelType) case catwalk.InferenceProviderCopilot: - dlg, cmd = dialog.NewOAuthCopilot(m.com, provider, model, modelType) + dlg, cmd = dialog.NewOAuthCopilot(m.com, isOnboarding, provider, model, modelType) default: - dlg, cmd = dialog.NewAPIKeyInput(m.com, provider, model, modelType) + dlg, cmd = dialog.NewAPIKeyInput(m.com, isOnboarding, provider, model, modelType) } if m.dialog.ContainsDialog(dlg.ID()) { @@ -1648,12 +1686,8 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { header := uv.NewStyledString(m.header) header.Draw(scr, layout.header) - mainView := lipgloss.NewStyle().Width(layout.main.Dx()). - Height(layout.main.Dy()). - Background(lipgloss.ANSIColor(rand.Intn(256))). - Render(" Onboarding ") - main := uv.NewStyledString(mainView) - main.Draw(scr, layout.main) + // NOTE: Onboarding flow will be rendered as dialogs below, but + // positioned at the bottom left of the screen. case uiInitialize: header := uv.NewStyledString(m.header) @@ -1697,11 +1731,14 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor { } } + isOnboarding := m.state == uiOnboarding + // Add status and help layer + m.status.SetHideHelp(isOnboarding) m.status.Draw(scr, layout.status) // Draw completions popup if open - if m.completionsOpen && m.completions.HasItems() { + if !isOnboarding && m.completionsOpen && m.completions.HasItems() { w, h := m.completions.Size() x := m.completionsPositionStart.X y := m.completionsPositionStart.Y - h @@ -2606,7 +2643,8 @@ func (m *UI) openModelsDialog() tea.Cmd { return nil } - modelsDialog, err := dialog.NewModels(m.com) + isOnboarding := m.state == uiOnboarding + modelsDialog, err := dialog.NewModels(m.com, isOnboarding) if err != nil { return uiutil.ReportError(err) }