From 13f315cb30efb673178ec4059547a2306a0d7c50 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 18 Dec 2025 14:42:17 -0300 Subject: [PATCH] feat: make hyper and copilot login work on onboarding --- internal/tui/components/chat/splash/splash.go | 222 ++++++++++++++---- .../tui/components/dialogs/models/models.go | 42 ++-- internal/tui/page/chat/chat.go | 29 ++- 3 files changed, 221 insertions(+), 72 deletions(-) diff --git a/internal/tui/components/chat/splash/splash.go b/internal/tui/components/chat/splash/splash.go index e8e372f6ea20c52731ec6539ec058770df27aab2..32f30b5df7a32ff27dc011331ec4857ce36cddc8 100644 --- a/internal/tui/components/chat/splash/splash.go +++ b/internal/tui/components/chat/splash/splash.go @@ -12,12 +12,15 @@ import ( "github.com/atotto/clipboard" "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/agent" + hyperp "github.com/charmbracelet/crush/internal/agent/hyper" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/home" "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper" "github.com/charmbracelet/crush/internal/tui/components/dialogs/models" "github.com/charmbracelet/crush/internal/tui/components/logo" lspcomponent "github.com/charmbracelet/crush/internal/tui/components/lsp" @@ -55,6 +58,12 @@ type Splash interface { // IsClaudeOAuthComplete returns whether Claude OAuth flow is complete IsClaudeOAuthComplete() bool + + // IsShowingClaudeOAuth2 returns whether showing Hyper OAuth2 flow + IsShowingHyperOAuth2() bool + + // IsShowingClaudeOAuth2 returns whether showing GitHub Copilot OAuth2 flow + IsShowingCopilotOAuth2() bool } const ( @@ -87,6 +96,14 @@ type splashCmp struct { isAPIKeyValid bool apiKeyValue string + // Hyper device flow state + hyperDeviceFlow *hyper.DeviceFlow + showHyperDeviceFlow bool + + // Copilot device flow state + copilotDeviceFlow *copilot.DeviceFlow + showCopilotDeviceFlow bool + // Claude state claudeAuthMethodChooser *claude.AuthMethodChooser claudeOAuth2 *claude.OAuth2 @@ -186,6 +203,26 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { } return s, tea.Batch(cmds...) + case hyper.DeviceFlowCompletedMsg: + s.showHyperDeviceFlow = false + return s, s.saveAPIKeyAndContinue(msg.Token, true) + case hyper.DeviceAuthInitiatedMsg, hyper.DeviceFlowErrorMsg: + if s.hyperDeviceFlow != nil { + u, cmd := s.hyperDeviceFlow.Update(msg) + s.hyperDeviceFlow = u.(*hyper.DeviceFlow) + return s, cmd + } + return s, nil + case copilot.DeviceAuthInitiatedMsg, copilot.DeviceFlowErrorMsg: + if s.copilotDeviceFlow != nil { + u, cmd := s.copilotDeviceFlow.Update(msg) + s.copilotDeviceFlow = u.(*copilot.DeviceFlow) + return s, cmd + } + return s, nil + case copilot.DeviceFlowCompletedMsg: + s.showCopilotDeviceFlow = false + return s, s.saveAPIKeyAndContinue(msg.Token, true) case claude.AuthenticationCompleteMsg: s.showClaudeAuthMethodChooser = false s.showClaudeOAuth2 = false @@ -205,6 +242,10 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { } case tea.KeyPressMsg: switch { + case key.Matches(msg, s.keyMap.Copy) && s.showHyperDeviceFlow: + return s, s.hyperDeviceFlow.CopyCode() + case key.Matches(msg, s.keyMap.Copy) && s.showCopilotDeviceFlow: + return s, s.copilotDeviceFlow.CopyCode() case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL: return s, tea.Sequence( tea.SetClipboard(s.claudeOAuth2.URL), @@ -223,21 +264,27 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { s.claudeOAuth2 = u.(*claude.OAuth2) return s, cmd case key.Matches(msg, s.keyMap.Back): - if s.showClaudeAuthMethodChooser { + switch { + case s.showClaudeAuthMethodChooser: s.claudeAuthMethodChooser.SetDefaults() s.showClaudeAuthMethodChooser = false return s, nil - } - if s.showClaudeOAuth2 { + case s.showClaudeOAuth2: s.claudeOAuth2.SetDefaults() s.showClaudeOAuth2 = false s.showClaudeAuthMethodChooser = true return s, nil - } - if s.isAPIKeyValid { + case s.showHyperDeviceFlow: + s.hyperDeviceFlow = nil + s.showHyperDeviceFlow = false return s, nil - } - if s.needsAPIKey { + case s.showCopilotDeviceFlow: + s.copilotDeviceFlow = nil + s.showCopilotDeviceFlow = false + return s, nil + case s.isAPIKeyValid: + return s, nil + case s.needsAPIKey: if s.selectedModel.Provider.ID == catwalk.InferenceProviderAnthropic { s.showClaudeAuthMethodChooser = true } @@ -249,7 +296,8 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { return s, nil } case key.Matches(msg, s.keyMap.Select): - if s.showClaudeAuthMethodChooser { + switch { + case s.showClaudeAuthMethodChooser: selectedItem := s.modelList.SelectedModel() if selectedItem == nil { return s, nil @@ -267,16 +315,17 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { s.showClaudeOAuth2 = true } return s, nil - } - if s.showClaudeOAuth2 { + case s.showClaudeOAuth2: m2, cmd2 := s.claudeOAuth2.ValidationConfirm() s.claudeOAuth2 = m2.(*claude.OAuth2) return s, cmd2 - } - if s.isAPIKeyValid { + case s.showHyperDeviceFlow: + return s, s.hyperDeviceFlow.CopyCodeAndOpenURL() + case s.showCopilotDeviceFlow: + return s, s.copilotDeviceFlow.CopyCodeAndOpenURL() + case s.isAPIKeyValid: return s, s.saveAPIKeyAndContinue(s.apiKeyValue, true) - } - if s.isOnboarding && !s.needsAPIKey { + case s.isOnboarding && !s.needsAPIKey: selectedItem := s.modelList.SelectedModel() if selectedItem == nil { return s, nil @@ -286,9 +335,22 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { s.isOnboarding = false return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{})) } else { - if selectedItem.Provider.ID == catwalk.InferenceProviderAnthropic { + switch selectedItem.Provider.ID { + case catwalk.InferenceProviderAnthropic: s.showClaudeAuthMethodChooser = true return s, nil + case hyperp.Name: + s.selectedModel = selectedItem + s.showHyperDeviceFlow = true + s.hyperDeviceFlow = hyper.NewDeviceFlow() + s.hyperDeviceFlow.SetWidth(min(s.width-2, 60)) + return s, s.hyperDeviceFlow.Init() + case catwalk.InferenceProviderCopilot: + s.selectedModel = selectedItem + s.showCopilotDeviceFlow = true + s.copilotDeviceFlow = copilot.NewDeviceFlow() + s.copilotDeviceFlow.SetWidth(min(s.width-2, 60)) + return s, s.copilotDeviceFlow.Init() } // Provider not configured, show API key input s.needsAPIKey = true @@ -296,7 +358,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { s.apiKeyInput.SetProviderName(selectedItem.Provider.Name) return s, nil } - } else if s.needsAPIKey { + case s.needsAPIKey: // Handle API key submission s.apiKeyValue = strings.TrimSpace(s.apiKeyInput.Value()) if s.apiKeyValue == "" { @@ -337,7 +399,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { } }, ) - } else if s.needsProjectInit { + case s.needsProjectInit: return s, s.initializeProject() } case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight): @@ -385,44 +447,71 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { return s, s.initializeProject() } default: - if s.showClaudeAuthMethodChooser { + switch { + case s.showClaudeAuthMethodChooser: u, cmd := s.claudeAuthMethodChooser.Update(msg) s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser) return s, cmd - } else if s.showClaudeOAuth2 { + case s.showClaudeOAuth2: u, cmd := s.claudeOAuth2.Update(msg) s.claudeOAuth2 = u.(*claude.OAuth2) return s, cmd - } else if s.needsAPIKey { + case s.showHyperDeviceFlow: + u, cmd := s.hyperDeviceFlow.Update(msg) + s.hyperDeviceFlow = u.(*hyper.DeviceFlow) + return s, cmd + case s.showCopilotDeviceFlow: + u, cmd := s.copilotDeviceFlow.Update(msg) + s.copilotDeviceFlow = u.(*copilot.DeviceFlow) + return s, cmd + case s.needsAPIKey: u, cmd := s.apiKeyInput.Update(msg) s.apiKeyInput = u.(*models.APIKeyInput) return s, cmd - } else if s.isOnboarding { + case s.isOnboarding: u, cmd := s.modelList.Update(msg) s.modelList = u return s, cmd } } case tea.PasteMsg: - if s.showClaudeOAuth2 { + switch { + case s.showClaudeOAuth2: u, cmd := s.claudeOAuth2.Update(msg) s.claudeOAuth2 = u.(*claude.OAuth2) return s, cmd - } else if s.needsAPIKey { + case s.showHyperDeviceFlow: + u, cmd := s.hyperDeviceFlow.Update(msg) + s.hyperDeviceFlow = u.(*hyper.DeviceFlow) + return s, cmd + case s.showCopilotDeviceFlow: + u, cmd := s.copilotDeviceFlow.Update(msg) + s.copilotDeviceFlow = u.(*copilot.DeviceFlow) + return s, cmd + case s.needsAPIKey: u, cmd := s.apiKeyInput.Update(msg) s.apiKeyInput = u.(*models.APIKeyInput) return s, cmd - } else if s.isOnboarding { + case s.isOnboarding: var cmd tea.Cmd s.modelList, cmd = s.modelList.Update(msg) return s, cmd } case spinner.TickMsg: - if s.showClaudeOAuth2 { + switch { + case s.showClaudeOAuth2: u, cmd := s.claudeOAuth2.Update(msg) s.claudeOAuth2 = u.(*claude.OAuth2) return s, cmd - } else { + case s.showHyperDeviceFlow: + u, cmd := s.hyperDeviceFlow.Update(msg) + s.hyperDeviceFlow = u.(*hyper.DeviceFlow) + return s, cmd + case s.showCopilotDeviceFlow: + u, cmd := s.copilotDeviceFlow.Update(msg) + s.copilotDeviceFlow = u.(*copilot.DeviceFlow) + return s, cmd + default: u, cmd := s.apiKeyInput.Update(msg) s.apiKeyInput = u.(*models.APIKeyInput) return s, cmd @@ -560,7 +649,9 @@ func (s *splashCmp) isProviderConfigured(providerID string) bool { func (s *splashCmp) View() string { t := styles.CurrentTheme() var content string - if s.showClaudeAuthMethodChooser { + + switch { + case s.showClaudeAuthMethodChooser: remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) chooserView := s.claudeAuthMethodChooser.View() authMethodSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render( @@ -576,7 +667,7 @@ func (s *splashCmp) View() string { s.logoRendered, authMethodSelector, ) - } else if s.showClaudeOAuth2 { + case s.showClaudeOAuth2: remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) oauth2View := s.claudeOAuth2.View() oauthSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render( @@ -592,7 +683,39 @@ func (s *splashCmp) View() string { s.logoRendered, oauthSelector, ) - } else if s.needsAPIKey { + case s.showHyperDeviceFlow: + remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) + hyperView := s.hyperDeviceFlow.View() + hyperSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render( + lipgloss.JoinVertical( + lipgloss.Left, + t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth Hyper"), + "", + hyperView, + ), + ) + content = lipgloss.JoinVertical( + lipgloss.Left, + s.logoRendered, + hyperSelector, + ) + case s.showCopilotDeviceFlow: + remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) + copilotView := s.copilotDeviceFlow.View() + copilotSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render( + lipgloss.JoinVertical( + lipgloss.Left, + t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth GitHub Copilot"), + "", + copilotView, + ), + ) + content = lipgloss.JoinVertical( + lipgloss.Left, + s.logoRendered, + copilotSelector, + ) + case s.needsAPIKey: remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View()) apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render( @@ -606,7 +729,7 @@ func (s *splashCmp) View() string { s.logoRendered, apiKeySelector, ) - } else if s.isOnboarding { + case s.isOnboarding: modelListView := s.modelList.View() remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render( @@ -622,7 +745,7 @@ func (s *splashCmp) View() string { s.logoRendered, modelSelector, ) - } else if s.needsProjectInit { + case s.needsProjectInit: titleStyle := t.S().Base.Foreground(t.FgBase) pathStyle := t.S().Base.Foreground(t.Success).PaddingLeft(2) bodyStyle := t.S().Base.Foreground(t.FgMuted) @@ -673,7 +796,7 @@ func (s *splashCmp) View() string { "", initContent, ) - } else { + default: parts := []string{ s.logoRendered, s.infoSection(), @@ -690,28 +813,25 @@ func (s *splashCmp) View() string { } func (s *splashCmp) Cursor() *tea.Cursor { - if s.showClaudeAuthMethodChooser { + switch { + case s.showClaudeAuthMethodChooser: return nil - } - if s.showClaudeOAuth2 { + case s.showClaudeOAuth2: if cursor := s.claudeOAuth2.CodeInput.Cursor(); cursor != nil { cursor.Y += 2 // FIXME(@andreynering): Why do we need this? return s.moveCursor(cursor) } return nil - } - if s.needsAPIKey { + case s.needsAPIKey: cursor := s.apiKeyInput.Cursor() if cursor != nil { return s.moveCursor(cursor) } - } else if s.isOnboarding { + case s.isOnboarding: cursor := s.modelList.Cursor() if cursor != nil { return s.moveCursor(cursor) } - } else { - return nil } return nil } @@ -803,13 +923,14 @@ func (s *splashCmp) logoGap() int { // Bindings implements SplashPage. func (s *splashCmp) Bindings() []key.Binding { - if s.showClaudeAuthMethodChooser { + switch { + case s.showClaudeAuthMethodChooser: return []key.Binding{ s.keyMap.Select, s.keyMap.Tab, s.keyMap.Back, } - } else if s.showClaudeOAuth2 { + case s.showClaudeOAuth2: bindings := []key.Binding{ s.keyMap.Select, } @@ -817,18 +938,18 @@ func (s *splashCmp) Bindings() []key.Binding { bindings = append(bindings, s.keyMap.Copy) } return bindings - } else if s.needsAPIKey { + case s.needsAPIKey: return []key.Binding{ s.keyMap.Select, s.keyMap.Back, } - } else if s.isOnboarding { + case s.isOnboarding: return []key.Binding{ s.keyMap.Select, s.keyMap.Next, s.keyMap.Previous, } - } else if s.needsProjectInit { + case s.needsProjectInit: return []key.Binding{ s.keyMap.Select, s.keyMap.Yes, @@ -836,8 +957,9 @@ func (s *splashCmp) Bindings() []key.Binding { s.keyMap.Tab, s.keyMap.LeftRight, } + default: + return []key.Binding{} } - return []key.Binding{} } func (s *splashCmp) getMaxInfoWidth() int { @@ -938,3 +1060,11 @@ func (s *splashCmp) IsClaudeOAuthURLState() bool { func (s *splashCmp) IsClaudeOAuthComplete() bool { return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateCode && s.claudeOAuth2.ValidationState == claude.OAuthValidationStateValid } + +func (s *splashCmp) IsShowingHyperOAuth2() bool { + return s.showHyperDeviceFlow +} + +func (s *splashCmp) IsShowingCopilotOAuth2() bool { + return s.showCopilotDeviceFlow +} diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index 8ed2ffbf0bf0ddd4641fbbda6f0e2b20a1967e07..ff7bd4f0409693454222b45567c23a151f8de077 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -174,13 +174,10 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { case tea.KeyPressMsg: switch { // Handle Hyper device flow keys - case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && (m.showHyperDeviceFlow || m.showCopilotDeviceFlow): - if m.hyperDeviceFlow != nil { - return m, m.hyperDeviceFlow.CopyCode() - } - if m.copilotDeviceFlow != nil { - return m, m.copilotDeviceFlow.CopyCode() - } + case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showHyperDeviceFlow: + return m, m.hyperDeviceFlow.CopyCode() + case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showCopilotDeviceFlow: + return m, m.copilotDeviceFlow.CopyCode() case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL: return m, tea.Sequence( tea.SetClipboard(m.claudeOAuth2.URL), @@ -337,28 +334,26 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { return m, m.modelList.SetModelType(LargeModelType) } case key.Matches(msg, m.keyMap.Close): - if m.showHyperDeviceFlow { + switch { + case m.showHyperDeviceFlow: if m.hyperDeviceFlow != nil { m.hyperDeviceFlow.Cancel() } m.showHyperDeviceFlow = false m.selectedModel = nil - } - if m.showCopilotDeviceFlow { + case m.showCopilotDeviceFlow: if m.copilotDeviceFlow != nil { m.copilotDeviceFlow.Cancel() } m.showCopilotDeviceFlow = false m.selectedModel = nil - } - if m.showClaudeAuthMethodChooser { + case m.showClaudeAuthMethodChooser: m.claudeAuthMethodChooser.SetDefaults() m.showClaudeAuthMethodChooser = false m.keyMap.isClaudeAuthChoiceHelp = false m.keyMap.isClaudeOAuthHelp = false return m, nil - } - if m.needsAPIKey { + case m.needsAPIKey: if m.isAPIKeyValid { return m, nil } @@ -369,37 +364,40 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { m.apiKeyValue = "" m.apiKeyInput.Reset() return m, nil + default: + return m, util.CmdHandler(dialogs.CloseDialogMsg{}) } - return m, util.CmdHandler(dialogs.CloseDialogMsg{}) default: - if m.showClaudeAuthMethodChooser { + switch { + case m.showClaudeAuthMethodChooser: u, cmd := m.claudeAuthMethodChooser.Update(msg) m.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser) return m, cmd - } else if m.showClaudeOAuth2 { + case m.showClaudeOAuth2: u, cmd := m.claudeOAuth2.Update(msg) m.claudeOAuth2 = u.(*claude.OAuth2) return m, cmd - } else if m.needsAPIKey { + case m.needsAPIKey: u, cmd := m.apiKeyInput.Update(msg) m.apiKeyInput = u.(*APIKeyInput) return m, cmd - } else { + default: u, cmd := m.modelList.Update(msg) m.modelList = u return m, cmd } } case tea.PasteMsg: - if m.showClaudeOAuth2 { + switch { + case m.showClaudeOAuth2: u, cmd := m.claudeOAuth2.Update(msg) m.claudeOAuth2 = u.(*claude.OAuth2) return m, cmd - } else if m.needsAPIKey { + case m.needsAPIKey: u, cmd := m.apiKeyInput.Update(msg) m.apiKeyInput = u.(*APIKeyInput) return m, cmd - } else { + default: var cmd tea.Cmd m.modelList, cmd = m.modelList.Update(msg) return m, cmd diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index 8ba323e1b2320445cdc2d5a60f5f270a2ee5ce96..33a71f9e3b9d184c3fb18423a91dd0baf8c2a0b9 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -31,7 +31,9 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/dialogs" "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude" "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot" "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper" "github.com/charmbracelet/crush/internal/tui/components/dialogs/models" "github.com/charmbracelet/crush/internal/tui/components/dialogs/reasoning" "github.com/charmbracelet/crush/internal/tui/page" @@ -335,7 +337,14 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { cmds = append(cmds, cmd) return p, tea.Batch(cmds...) - case claude.ValidationCompletedMsg, claude.AuthenticationCompleteMsg: + case claude.ValidationCompletedMsg, + claude.AuthenticationCompleteMsg, + hyper.DeviceFlowCompletedMsg, + hyper.DeviceAuthInitiatedMsg, + hyper.DeviceFlowErrorMsg, + copilot.DeviceAuthInitiatedMsg, + copilot.DeviceFlowErrorMsg, + copilot.DeviceFlowCompletedMsg: if p.focusedPane == PanelTypeSplash { u, cmd := p.splash.Update(msg) p.splash = u.(splash.Splash) @@ -1050,7 +1059,8 @@ func (p *chatPage) Help() help.KeyMap { fullList = append(fullList, []key.Binding{v}) } case p.isOnboarding && p.splash.IsShowingClaudeOAuth2(): - if p.splash.IsClaudeOAuthURLState() { + switch { + case p.splash.IsClaudeOAuthURLState(): shortList = append(shortList, key.NewBinding( key.WithKeys("enter"), @@ -1061,14 +1071,25 @@ func (p *chatPage) Help() help.KeyMap { key.WithHelp("c", "copy url"), ), ) - } else if p.splash.IsClaudeOAuthComplete() { + case p.splash.IsClaudeOAuthComplete(): shortList = append(shortList, key.NewBinding( key.WithKeys("enter"), key.WithHelp("enter", "continue"), ), ) - } else { + case p.splash.IsShowingHyperOAuth2() || p.splash.IsShowingCopilotOAuth2(): + shortList = append(shortList, + key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "copy url & open signup"), + ), + key.NewBinding( + key.WithKeys("c"), + key.WithHelp("c", "copy url"), + ), + ) + default: shortList = append(shortList, key.NewBinding( key.WithKeys("enter"),