on main help sign in
feat: make hyper and copilot login work on onboarding
commit13f315cb30efb673178ec4059547a2306a0d7c50 authorAndrey Nering <andreynering@users.noreply.github.com> dateThu 18 Dec 2025, 14:42 (1 year ago) parentb945baa
3 files changed+221−72
internal/tui/components/chat/splash/splash.go+176−46 internal/tui/components/dialogs/models/models.go+20−22 internal/tui/page/chat/chat.go+25−4
internal/tui/components/chat/splash/splash.go+176−46
@@ -12,12 +12,15 @@ import (
1212 "github.com/atotto/clipboard"
1313 "github.com/charmbracelet/catwalk/pkg/catwalk"
1414 "github.com/charmbracelet/crush/internal/agent"
15+ hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
1516 "github.com/charmbracelet/crush/internal/config"
1617 "github.com/charmbracelet/crush/internal/home"
1718 "github.com/charmbracelet/crush/internal/tui/components/chat"
1819 "github.com/charmbracelet/crush/internal/tui/components/core"
1920 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
2021 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
22+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
23+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
2124 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
2225 "github.com/charmbracelet/crush/internal/tui/components/logo"
2326 lspcomponent "github.com/charmbracelet/crush/internal/tui/components/lsp"
@@ -55,6 +58,12 @@ type Splash interface {
5558
5659 // IsClaudeOAuthComplete returns whether Claude OAuth flow is complete
5760 IsClaudeOAuthComplete() bool
61+
62+ // IsShowingClaudeOAuth2 returns whether showing Hyper OAuth2 flow
63+ IsShowingHyperOAuth2() bool
64+
65+ // IsShowingClaudeOAuth2 returns whether showing GitHub Copilot OAuth2 flow
66+ IsShowingCopilotOAuth2() bool
5867}
5968
6069const (
@@ -87,6 +96,14 @@ type splashCmp struct {
8796 isAPIKeyValid bool
8897 apiKeyValue string
8998
99+ // Hyper device flow state
100+ hyperDeviceFlow *hyper.DeviceFlow
101+ showHyperDeviceFlow bool
102+
103+ // Copilot device flow state
104+ copilotDeviceFlow *copilot.DeviceFlow
105+ showCopilotDeviceFlow bool
106+
90107 // Claude state
91108 claudeAuthMethodChooser *claude.AuthMethodChooser
92109 claudeOAuth2 *claude.OAuth2
@@ -186,6 +203,26 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
186203 }
187204
188205 return s, tea.Batch(cmds...)
206+ case hyper.DeviceFlowCompletedMsg:
207+ s.showHyperDeviceFlow = false
208+ return s, s.saveAPIKeyAndContinue(msg.Token, true)
209+ case hyper.DeviceAuthInitiatedMsg, hyper.DeviceFlowErrorMsg:
210+ if s.hyperDeviceFlow != nil {
211+ u, cmd := s.hyperDeviceFlow.Update(msg)
212+ s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
213+ return s, cmd
214+ }
215+ return s, nil
216+ case copilot.DeviceAuthInitiatedMsg, copilot.DeviceFlowErrorMsg:
217+ if s.copilotDeviceFlow != nil {
218+ u, cmd := s.copilotDeviceFlow.Update(msg)
219+ s.copilotDeviceFlow = u.(*copilot.DeviceFlow)
220+ return s, cmd
221+ }
222+ return s, nil
223+ case copilot.DeviceFlowCompletedMsg:
224+ s.showCopilotDeviceFlow = false
225+ return s, s.saveAPIKeyAndContinue(msg.Token, true)
189226 case claude.AuthenticationCompleteMsg:
190227 s.showClaudeAuthMethodChooser = false
191228 s.showClaudeOAuth2 = false
@@ -205,6 +242,10 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
205242 }
206243 case tea.KeyPressMsg:
207244 switch {
245+ case key.Matches(msg, s.keyMap.Copy) && s.showHyperDeviceFlow:
246+ return s, s.hyperDeviceFlow.CopyCode()
247+ case key.Matches(msg, s.keyMap.Copy) && s.showCopilotDeviceFlow:
248+ return s, s.copilotDeviceFlow.CopyCode()
208249 case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL:
209250 return s, tea.Sequence(
210251 tea.SetClipboard(s.claudeOAuth2.URL),
@@ -223,21 +264,27 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
223264 s.claudeOAuth2 = u.(*claude.OAuth2)
224265 return s, cmd
225266 case key.Matches(msg, s.keyMap.Back):
226− if s.showClaudeAuthMethodChooser {
267+ switch {
268+ case s.showClaudeAuthMethodChooser:
227269 s.claudeAuthMethodChooser.SetDefaults()
228270 s.showClaudeAuthMethodChooser = false
229271 return s, nil
230− }
231− if s.showClaudeOAuth2 {
272+ case s.showClaudeOAuth2:
232273 s.claudeOAuth2.SetDefaults()
233274 s.showClaudeOAuth2 = false
234275 s.showClaudeAuthMethodChooser = true
235276 return s, nil
236− }
237− if s.isAPIKeyValid {
277+ case s.showHyperDeviceFlow:
278+ s.hyperDeviceFlow = nil
279+ s.showHyperDeviceFlow = false
238280 return s, nil
239− }
240− if s.needsAPIKey {
281+ case s.showCopilotDeviceFlow:
282+ s.copilotDeviceFlow = nil
283+ s.showCopilotDeviceFlow = false
284+ return s, nil
285+ case s.isAPIKeyValid:
286+ return s, nil
287+ case s.needsAPIKey:
241288 if s.selectedModel.Provider.ID == catwalk.InferenceProviderAnthropic {
242289 s.showClaudeAuthMethodChooser = true
243290 }
@@ -249,7 +296,8 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
249296 return s, nil
250297 }
251298 case key.Matches(msg, s.keyMap.Select):
252− if s.showClaudeAuthMethodChooser {
299+ switch {
300+ case s.showClaudeAuthMethodChooser:
253301 selectedItem := s.modelList.SelectedModel()
254302 if selectedItem == nil {
255303 return s, nil
@@ -267,16 +315,17 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
267315 s.showClaudeOAuth2 = true
268316 }
269317 return s, nil
270− }
271− if s.showClaudeOAuth2 {
318+ case s.showClaudeOAuth2:
272319 m2, cmd2 := s.claudeOAuth2.ValidationConfirm()
273320 s.claudeOAuth2 = m2.(*claude.OAuth2)
274321 return s, cmd2
275− }
276− if s.isAPIKeyValid {
322+ case s.showHyperDeviceFlow:
323+ return s, s.hyperDeviceFlow.CopyCodeAndOpenURL()
324+ case s.showCopilotDeviceFlow:
325+ return s, s.copilotDeviceFlow.CopyCodeAndOpenURL()
326+ case s.isAPIKeyValid:
277327 return s, s.saveAPIKeyAndContinue(s.apiKeyValue, true)
278− }
279− if s.isOnboarding && !s.needsAPIKey {
328+ case s.isOnboarding && !s.needsAPIKey:
280329 selectedItem := s.modelList.SelectedModel()
281330 if selectedItem == nil {
282331 return s, nil
@@ -286,9 +335,22 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
286335 s.isOnboarding = false
287336 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
288337 } else {
289− if selectedItem.Provider.ID == catwalk.InferenceProviderAnthropic {
338+ switch selectedItem.Provider.ID {
339+ case catwalk.InferenceProviderAnthropic:
290340 s.showClaudeAuthMethodChooser = true
291341 return s, nil
342+ case hyperp.Name:
343+ s.selectedModel = selectedItem
344+ s.showHyperDeviceFlow = true
345+ s.hyperDeviceFlow = hyper.NewDeviceFlow()
346+ s.hyperDeviceFlow.SetWidth(min(s.width-2, 60))
347+ return s, s.hyperDeviceFlow.Init()
348+ case catwalk.InferenceProviderCopilot:
349+ s.selectedModel = selectedItem
350+ s.showCopilotDeviceFlow = true
351+ s.copilotDeviceFlow = copilot.NewDeviceFlow()
352+ s.copilotDeviceFlow.SetWidth(min(s.width-2, 60))
353+ return s, s.copilotDeviceFlow.Init()
292354 }
293355 // Provider not configured, show API key input
294356 s.needsAPIKey = true
@@ -296,7 +358,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
296358 s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
297359 return s, nil
298360 }
299− } else if s.needsAPIKey {
361+ case s.needsAPIKey:
300362 // Handle API key submission
301363 s.apiKeyValue = strings.TrimSpace(s.apiKeyInput.Value())
302364 if s.apiKeyValue == "" {
@@ -337,7 +399,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
337399 }
338400 },
339401 )
340− } else if s.needsProjectInit {
402+ case s.needsProjectInit:
341403 return s, s.initializeProject()
342404 }
343405 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) {
385447 return s, s.initializeProject()
386448 }
387449 default:
388− if s.showClaudeAuthMethodChooser {
450+ switch {
451+ case s.showClaudeAuthMethodChooser:
389452 u, cmd := s.claudeAuthMethodChooser.Update(msg)
390453 s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
391454 return s, cmd
392− } else if s.showClaudeOAuth2 {
455+ case s.showClaudeOAuth2:
393456 u, cmd := s.claudeOAuth2.Update(msg)
394457 s.claudeOAuth2 = u.(*claude.OAuth2)
395458 return s, cmd
396− } else if s.needsAPIKey {
459+ case s.showHyperDeviceFlow:
460+ u, cmd := s.hyperDeviceFlow.Update(msg)
461+ s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
462+ return s, cmd
463+ case s.showCopilotDeviceFlow:
464+ u, cmd := s.copilotDeviceFlow.Update(msg)
465+ s.copilotDeviceFlow = u.(*copilot.DeviceFlow)
466+ return s, cmd
467+ case s.needsAPIKey:
397468 u, cmd := s.apiKeyInput.Update(msg)
398469 s.apiKeyInput = u.(*models.APIKeyInput)
399470 return s, cmd
400− } else if s.isOnboarding {
471+ case s.isOnboarding:
401472 u, cmd := s.modelList.Update(msg)
402473 s.modelList = u
403474 return s, cmd
404475 }
405476 }
406477 case tea.PasteMsg:
407− if s.showClaudeOAuth2 {
478+ switch {
479+ case s.showClaudeOAuth2:
408480 u, cmd := s.claudeOAuth2.Update(msg)
409481 s.claudeOAuth2 = u.(*claude.OAuth2)
410482 return s, cmd
411− } else if s.needsAPIKey {
483+ case s.showHyperDeviceFlow:
484+ u, cmd := s.hyperDeviceFlow.Update(msg)
485+ s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
486+ return s, cmd
487+ case s.showCopilotDeviceFlow:
488+ u, cmd := s.copilotDeviceFlow.Update(msg)
489+ s.copilotDeviceFlow = u.(*copilot.DeviceFlow)
490+ return s, cmd
491+ case s.needsAPIKey:
412492 u, cmd := s.apiKeyInput.Update(msg)
413493 s.apiKeyInput = u.(*models.APIKeyInput)
414494 return s, cmd
415− } else if s.isOnboarding {
495+ case s.isOnboarding:
416496 var cmd tea.Cmd
417497 s.modelList, cmd = s.modelList.Update(msg)
418498 return s, cmd
419499 }
420500 case spinner.TickMsg:
421− if s.showClaudeOAuth2 {
501+ switch {
502+ case s.showClaudeOAuth2:
422503 u, cmd := s.claudeOAuth2.Update(msg)
423504 s.claudeOAuth2 = u.(*claude.OAuth2)
424505 return s, cmd
425− } else {
506+ case s.showHyperDeviceFlow:
507+ u, cmd := s.hyperDeviceFlow.Update(msg)
508+ s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
509+ return s, cmd
510+ case s.showCopilotDeviceFlow:
511+ u, cmd := s.copilotDeviceFlow.Update(msg)
512+ s.copilotDeviceFlow = u.(*copilot.DeviceFlow)
513+ return s, cmd
514+ default:
426515 u, cmd := s.apiKeyInput.Update(msg)
427516 s.apiKeyInput = u.(*models.APIKeyInput)
428517 return s, cmd
@@ -560,7 +649,9 @@ func (s *splashCmp) isProviderConfigured(providerID string) bool {
560649func (s *splashCmp) View() string {
561650 t := styles.CurrentTheme()
562651 var content string
563− if s.showClaudeAuthMethodChooser {
652+
653+ switch {
654+ case s.showClaudeAuthMethodChooser:
564655 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
565656 chooserView := s.claudeAuthMethodChooser.View()
566657 authMethodSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -576,7 +667,7 @@ func (s *splashCmp) View() string {
576667 s.logoRendered,
577668 authMethodSelector,
578669 )
579− } else if s.showClaudeOAuth2 {
670+ case s.showClaudeOAuth2:
580671 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
581672 oauth2View := s.claudeOAuth2.View()
582673 oauthSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -592,7 +683,39 @@ func (s *splashCmp) View() string {
592683 s.logoRendered,
593684 oauthSelector,
594685 )
595− } else if s.needsAPIKey {
686+ case s.showHyperDeviceFlow:
687+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
688+ hyperView := s.hyperDeviceFlow.View()
689+ hyperSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
690+ lipgloss.JoinVertical(
691+ lipgloss.Left,
692+ t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth Hyper"),
693+ "",
694+ hyperView,
695+ ),
696+ )
697+ content = lipgloss.JoinVertical(
698+ lipgloss.Left,
699+ s.logoRendered,
700+ hyperSelector,
701+ )
702+ case s.showCopilotDeviceFlow:
703+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
704+ copilotView := s.copilotDeviceFlow.View()
705+ copilotSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
706+ lipgloss.JoinVertical(
707+ lipgloss.Left,
708+ t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth GitHub Copilot"),
709+ "",
710+ copilotView,
711+ ),
712+ )
713+ content = lipgloss.JoinVertical(
714+ lipgloss.Left,
715+ s.logoRendered,
716+ copilotSelector,
717+ )
718+ case s.needsAPIKey:
596719 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
597720 apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View())
598721 apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -606,7 +729,7 @@ func (s *splashCmp) View() string {
606729 s.logoRendered,
607730 apiKeySelector,
608731 )
609− } else if s.isOnboarding {
732+ case s.isOnboarding:
610733 modelListView := s.modelList.View()
611734 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
612735 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -622,7 +745,7 @@ func (s *splashCmp) View() string {
622745 s.logoRendered,
623746 modelSelector,
624747 )
625− } else if s.needsProjectInit {
748+ case s.needsProjectInit:
626749 titleStyle := t.S().Base.Foreground(t.FgBase)
627750 pathStyle := t.S().Base.Foreground(t.Success).PaddingLeft(2)
628751 bodyStyle := t.S().Base.Foreground(t.FgMuted)
@@ -673,7 +796,7 @@ func (s *splashCmp) View() string {
673796 "",
674797 initContent,
675798 )
676− } else {
799+ default:
677800 parts := []string{
678801 s.logoRendered,
679802 s.infoSection(),
@@ -690,28 +813,25 @@ func (s *splashCmp) View() string {
690813}
691814
692815func (s *splashCmp) Cursor() *tea.Cursor {
693− if s.showClaudeAuthMethodChooser {
816+ switch {
817+ case s.showClaudeAuthMethodChooser:
694818 return nil
695− }
696− if s.showClaudeOAuth2 {
819+ case s.showClaudeOAuth2:
697820 if cursor := s.claudeOAuth2.CodeInput.Cursor(); cursor != nil {
698821 cursor.Y += 2 // FIXME(@andreynering): Why do we need this?
699822 return s.moveCursor(cursor)
700823 }
701824 return nil
702− }
703− if s.needsAPIKey {
825+ case s.needsAPIKey:
704826 cursor := s.apiKeyInput.Cursor()
705827 if cursor != nil {
706828 return s.moveCursor(cursor)
707829 }
708− } else if s.isOnboarding {
830+ case s.isOnboarding:
709831 cursor := s.modelList.Cursor()
710832 if cursor != nil {
711833 return s.moveCursor(cursor)
712834 }
713− } else {
714− return nil
715835 }
716836 return nil
717837}
@@ -803,13 +923,14 @@ func (s *splashCmp) logoGap() int {
803923
804924// Bindings implements SplashPage.
805925func (s *splashCmp) Bindings() []key.Binding {
806− if s.showClaudeAuthMethodChooser {
926+ switch {
927+ case s.showClaudeAuthMethodChooser:
807928 return []key.Binding{
808929 s.keyMap.Select,
809930 s.keyMap.Tab,
810931 s.keyMap.Back,
811932 }
812− } else if s.showClaudeOAuth2 {
933+ case s.showClaudeOAuth2:
813934 bindings := []key.Binding{
814935 s.keyMap.Select,
815936 }
@@ -817,18 +938,18 @@ func (s *splashCmp) Bindings() []key.Binding {
817938 bindings = append(bindings, s.keyMap.Copy)
818939 }
819940 return bindings
820− } else if s.needsAPIKey {
941+ case s.needsAPIKey:
821942 return []key.Binding{
822943 s.keyMap.Select,
823944 s.keyMap.Back,
824945 }
825− } else if s.isOnboarding {
946+ case s.isOnboarding:
826947 return []key.Binding{
827948 s.keyMap.Select,
828949 s.keyMap.Next,
829950 s.keyMap.Previous,
830951 }
831− } else if s.needsProjectInit {
952+ case s.needsProjectInit:
832953 return []key.Binding{
833954 s.keyMap.Select,
834955 s.keyMap.Yes,
@@ -836,8 +957,9 @@ func (s *splashCmp) Bindings() []key.Binding {
836957 s.keyMap.Tab,
837958 s.keyMap.LeftRight,
838959 }
960+ default:
961+ return []key.Binding{}
839962 }
840− return []key.Binding{}
841963}
842964
843965func (s *splashCmp) getMaxInfoWidth() int {
@@ -938,3 +1060,11 @@ func (s *splashCmp) IsClaudeOAuthURLState() bool {
9381060func (s *splashCmp) IsClaudeOAuthComplete() bool {
9391061 return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateCode && s.claudeOAuth2.ValidationState == claude.OAuthValidationStateValid
9401062}
1063+
1064+func (s *splashCmp) IsShowingHyperOAuth2() bool {
1065+ return s.showHyperDeviceFlow
1066+}
1067+
1068+func (s *splashCmp) IsShowingCopilotOAuth2() bool {
1069+ return s.showCopilotDeviceFlow
1070+}
internal/tui/components/dialogs/models/models.go+20−22
@@ -174,13 +174,10 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
174174 case tea.KeyPressMsg:
175175 switch {
176176 // Handle Hyper device flow keys
177− case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && (m.showHyperDeviceFlow || m.showCopilotDeviceFlow):
178− if m.hyperDeviceFlow != nil {
179− return m, m.hyperDeviceFlow.CopyCode()
180− }
181− if m.copilotDeviceFlow != nil {
182− return m, m.copilotDeviceFlow.CopyCode()
183− }
177+ case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showHyperDeviceFlow:
178+ return m, m.hyperDeviceFlow.CopyCode()
179+ case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showCopilotDeviceFlow:
180+ return m, m.copilotDeviceFlow.CopyCode()
184181 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL:
185182 return m, tea.Sequence(
186183 tea.SetClipboard(m.claudeOAuth2.URL),
@@ -337,28 +334,26 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
337334 return m, m.modelList.SetModelType(LargeModelType)
338335 }
339336 case key.Matches(msg, m.keyMap.Close):
340− if m.showHyperDeviceFlow {
337+ switch {
338+ case m.showHyperDeviceFlow:
341339 if m.hyperDeviceFlow != nil {
342340 m.hyperDeviceFlow.Cancel()
343341 }
344342 m.showHyperDeviceFlow = false
345343 m.selectedModel = nil
346− }
347− if m.showCopilotDeviceFlow {
344+ case m.showCopilotDeviceFlow:
348345 if m.copilotDeviceFlow != nil {
349346 m.copilotDeviceFlow.Cancel()
350347 }
351348 m.showCopilotDeviceFlow = false
352349 m.selectedModel = nil
353− }
354− if m.showClaudeAuthMethodChooser {
350+ case m.showClaudeAuthMethodChooser:
355351 m.claudeAuthMethodChooser.SetDefaults()
356352 m.showClaudeAuthMethodChooser = false
357353 m.keyMap.isClaudeAuthChoiceHelp = false
358354 m.keyMap.isClaudeOAuthHelp = false
359355 return m, nil
360− }
361− if m.needsAPIKey {
356+ case m.needsAPIKey:
362357 if m.isAPIKeyValid {
363358 return m, nil
364359 }
@@ -369,37 +364,40 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
369364 m.apiKeyValue = ""
370365 m.apiKeyInput.Reset()
371366 return m, nil
367+ default:
368+ return m, util.CmdHandler(dialogs.CloseDialogMsg{})
372369 }
373− return m, util.CmdHandler(dialogs.CloseDialogMsg{})
374370 default:
375− if m.showClaudeAuthMethodChooser {
371+ switch {
372+ case m.showClaudeAuthMethodChooser:
376373 u, cmd := m.claudeAuthMethodChooser.Update(msg)
377374 m.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
378375 return m, cmd
379− } else if m.showClaudeOAuth2 {
376+ case m.showClaudeOAuth2:
380377 u, cmd := m.claudeOAuth2.Update(msg)
381378 m.claudeOAuth2 = u.(*claude.OAuth2)
382379 return m, cmd
383− } else if m.needsAPIKey {
380+ case m.needsAPIKey:
384381 u, cmd := m.apiKeyInput.Update(msg)
385382 m.apiKeyInput = u.(*APIKeyInput)
386383 return m, cmd
387− } else {
384+ default:
388385 u, cmd := m.modelList.Update(msg)
389386 m.modelList = u
390387 return m, cmd
391388 }
392389 }
393390 case tea.PasteMsg:
394− if m.showClaudeOAuth2 {
391+ switch {
392+ case m.showClaudeOAuth2:
395393 u, cmd := m.claudeOAuth2.Update(msg)
396394 m.claudeOAuth2 = u.(*claude.OAuth2)
397395 return m, cmd
398− } else if m.needsAPIKey {
396+ case m.needsAPIKey:
399397 u, cmd := m.apiKeyInput.Update(msg)
400398 m.apiKeyInput = u.(*APIKeyInput)
401399 return m, cmd
402− } else {
400+ default:
403401 var cmd tea.Cmd
404402 m.modelList, cmd = m.modelList.Update(msg)
405403 return m, cmd
internal/tui/page/chat/chat.go+25−4
@@ -31,7 +31,9 @@ import (
3131 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
3232 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
3333 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
34+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
3435 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
36+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
3537 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
3638 "github.com/charmbracelet/crush/internal/tui/components/dialogs/reasoning"
3739 "github.com/charmbracelet/crush/internal/tui/page"
@@ -335,7 +337,14 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
335337 cmds = append(cmds, cmd)
336338 return p, tea.Batch(cmds...)
337339
338− case claude.ValidationCompletedMsg, claude.AuthenticationCompleteMsg:
340+ case claude.ValidationCompletedMsg,
341+ claude.AuthenticationCompleteMsg,
342+ hyper.DeviceFlowCompletedMsg,
343+ hyper.DeviceAuthInitiatedMsg,
344+ hyper.DeviceFlowErrorMsg,
345+ copilot.DeviceAuthInitiatedMsg,
346+ copilot.DeviceFlowErrorMsg,
347+ copilot.DeviceFlowCompletedMsg:
339348 if p.focusedPane == PanelTypeSplash {
340349 u, cmd := p.splash.Update(msg)
341350 p.splash = u.(splash.Splash)
@@ -1050,7 +1059,8 @@ func (p *chatPage) Help() help.KeyMap {
10501059 fullList = append(fullList, []key.Binding{v})
10511060 }
10521061 case p.isOnboarding && p.splash.IsShowingClaudeOAuth2():
1053− if p.splash.IsClaudeOAuthURLState() {
1062+ switch {
1063+ case p.splash.IsClaudeOAuthURLState():
10541064 shortList = append(shortList,
10551065 key.NewBinding(
10561066 key.WithKeys("enter"),
@@ -1061,14 +1071,25 @@ func (p *chatPage) Help() help.KeyMap {
10611071 key.WithHelp("c", "copy url"),
10621072 ),
10631073 )
1064− } else if p.splash.IsClaudeOAuthComplete() {
1074+ case p.splash.IsClaudeOAuthComplete():
10651075 shortList = append(shortList,
10661076 key.NewBinding(
10671077 key.WithKeys("enter"),
10681078 key.WithHelp("enter", "continue"),
10691079 ),
10701080 )
1071− } else {
1081+ case p.splash.IsShowingHyperOAuth2() || p.splash.IsShowingCopilotOAuth2():
1082+ shortList = append(shortList,
1083+ key.NewBinding(
1084+ key.WithKeys("enter"),
1085+ key.WithHelp("enter", "copy url & open signup"),
1086+ ),
1087+ key.NewBinding(
1088+ key.WithKeys("c"),
1089+ key.WithHelp("c", "copy url"),
1090+ ),
1091+ )
1092+ default:
10721093 shortList = append(shortList,
10731094 key.NewBinding(
10741095 key.WithKeys("enter"),