@@ -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+}