@@ -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,41 +242,49 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
205242 }
206243 case tea.KeyPressMsg:
207244 switch {
208− case key.Matches(msg, s.keyMap.Copy):
209− if s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL {
210− return s, tea.Sequence(
211− tea.SetClipboard(s.claudeOAuth2.URL),
212− func() tea.Msg {
213− _ = clipboard.WriteAll(s.claudeOAuth2.URL)
214− return nil
215− },
216− util.ReportInfo("URL copied to clipboard"),
217− )
218− } else if s.showClaudeAuthMethodChooser {
219− u, cmd := s.claudeAuthMethodChooser.Update(msg)
220− s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
221− return s, cmd
222− } else if s.showClaudeOAuth2 {
223− u, cmd := s.claudeOAuth2.Update(msg)
224− s.claudeOAuth2 = u.(*claude.OAuth2)
225− return s, cmd
226− }
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()
249+ case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL:
250+ return s, tea.Sequence(
251+ tea.SetClipboard(s.claudeOAuth2.URL),
252+ func() tea.Msg {
253+ _ = clipboard.WriteAll(s.claudeOAuth2.URL)
254+ return nil
255+ },
256+ util.ReportInfo("URL copied to clipboard"),
257+ )
258+ case key.Matches(msg, s.keyMap.Copy) && s.showClaudeAuthMethodChooser:
259+ u, cmd := s.claudeAuthMethodChooser.Update(msg)
260+ s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
261+ return s, cmd
262+ case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2:
263+ u, cmd := s.claudeOAuth2.Update(msg)
264+ s.claudeOAuth2 = u.(*claude.OAuth2)
265+ return s, cmd
227266 case key.Matches(msg, s.keyMap.Back):
228− if s.showClaudeAuthMethodChooser {
267+ switch {
268+ case s.showClaudeAuthMethodChooser:
229269 s.claudeAuthMethodChooser.SetDefaults()
230270 s.showClaudeAuthMethodChooser = false
231271 return s, nil
232− }
233− if s.showClaudeOAuth2 {
272+ case s.showClaudeOAuth2:
234273 s.claudeOAuth2.SetDefaults()
235274 s.showClaudeOAuth2 = false
236275 s.showClaudeAuthMethodChooser = true
237276 return s, nil
238− }
239− if s.isAPIKeyValid {
277+ case s.showHyperDeviceFlow:
278+ s.hyperDeviceFlow = nil
279+ s.showHyperDeviceFlow = false
240280 return s, nil
241− }
242− 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:
243288 if s.selectedModel.Provider.ID == catwalk.InferenceProviderAnthropic {
244289 s.showClaudeAuthMethodChooser = true
245290 }
@@ -251,7 +296,8 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
251296 return s, nil
252297 }
253298 case key.Matches(msg, s.keyMap.Select):
254− if s.showClaudeAuthMethodChooser {
299+ switch {
300+ case s.showClaudeAuthMethodChooser:
255301 selectedItem := s.modelList.SelectedModel()
256302 if selectedItem == nil {
257303 return s, nil
@@ -269,16 +315,17 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
269315 s.showClaudeOAuth2 = true
270316 }
271317 return s, nil
272− }
273− if s.showClaudeOAuth2 {
318+ case s.showClaudeOAuth2:
274319 m2, cmd2 := s.claudeOAuth2.ValidationConfirm()
275320 s.claudeOAuth2 = m2.(*claude.OAuth2)
276321 return s, cmd2
277− }
278− 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:
279327 return s, s.saveAPIKeyAndContinue(s.apiKeyValue, true)
280− }
281− if s.isOnboarding && !s.needsAPIKey {
328+ case s.isOnboarding && !s.needsAPIKey:
282329 selectedItem := s.modelList.SelectedModel()
283330 if selectedItem == nil {
284331 return s, nil
@@ -288,9 +335,26 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
288335 s.isOnboarding = false
289336 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
290337 } else {
291− if selectedItem.Provider.ID == catwalk.InferenceProviderAnthropic {
338+ switch selectedItem.Provider.ID {
339+ case catwalk.InferenceProviderAnthropic:
292340 s.showClaudeAuthMethodChooser = true
293341 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+ if token, ok := config.Get().ImportCopilot(); ok {
350+ s.selectedModel = selectedItem
351+ return s, s.saveAPIKeyAndContinue(token, true)
352+ }
353+ s.selectedModel = selectedItem
354+ s.showCopilotDeviceFlow = true
355+ s.copilotDeviceFlow = copilot.NewDeviceFlow()
356+ s.copilotDeviceFlow.SetWidth(min(s.width-2, 60))
357+ return s, s.copilotDeviceFlow.Init()
294358 }
295359 // Provider not configured, show API key input
296360 s.needsAPIKey = true
@@ -298,7 +362,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
298362 s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
299363 return s, nil
300364 }
301− } else if s.needsAPIKey {
365+ case s.needsAPIKey:
302366 // Handle API key submission
303367 s.apiKeyValue = strings.TrimSpace(s.apiKeyInput.Value())
304368 if s.apiKeyValue == "" {
@@ -339,7 +403,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
339403 }
340404 },
341405 )
342− } else if s.needsProjectInit {
406+ case s.needsProjectInit:
343407 return s, s.initializeProject()
344408 }
345409 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
@@ -387,44 +451,71 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
387451 return s, s.initializeProject()
388452 }
389453 default:
390− if s.showClaudeAuthMethodChooser {
454+ switch {
455+ case s.showClaudeAuthMethodChooser:
391456 u, cmd := s.claudeAuthMethodChooser.Update(msg)
392457 s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
393458 return s, cmd
394− } else if s.showClaudeOAuth2 {
459+ case s.showClaudeOAuth2:
395460 u, cmd := s.claudeOAuth2.Update(msg)
396461 s.claudeOAuth2 = u.(*claude.OAuth2)
397462 return s, cmd
398− } else if s.needsAPIKey {
463+ case s.showHyperDeviceFlow:
464+ u, cmd := s.hyperDeviceFlow.Update(msg)
465+ s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
466+ return s, cmd
467+ case s.showCopilotDeviceFlow:
468+ u, cmd := s.copilotDeviceFlow.Update(msg)
469+ s.copilotDeviceFlow = u.(*copilot.DeviceFlow)
470+ return s, cmd
471+ case s.needsAPIKey:
399472 u, cmd := s.apiKeyInput.Update(msg)
400473 s.apiKeyInput = u.(*models.APIKeyInput)
401474 return s, cmd
402− } else if s.isOnboarding {
475+ case s.isOnboarding:
403476 u, cmd := s.modelList.Update(msg)
404477 s.modelList = u
405478 return s, cmd
406479 }
407480 }
408481 case tea.PasteMsg:
409− if s.showClaudeOAuth2 {
482+ switch {
483+ case s.showClaudeOAuth2:
410484 u, cmd := s.claudeOAuth2.Update(msg)
411485 s.claudeOAuth2 = u.(*claude.OAuth2)
412486 return s, cmd
413− } else if s.needsAPIKey {
487+ case s.showHyperDeviceFlow:
488+ u, cmd := s.hyperDeviceFlow.Update(msg)
489+ s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
490+ return s, cmd
491+ case s.showCopilotDeviceFlow:
492+ u, cmd := s.copilotDeviceFlow.Update(msg)
493+ s.copilotDeviceFlow = u.(*copilot.DeviceFlow)
494+ return s, cmd
495+ case s.needsAPIKey:
414496 u, cmd := s.apiKeyInput.Update(msg)
415497 s.apiKeyInput = u.(*models.APIKeyInput)
416498 return s, cmd
417− } else if s.isOnboarding {
499+ case s.isOnboarding:
418500 var cmd tea.Cmd
419501 s.modelList, cmd = s.modelList.Update(msg)
420502 return s, cmd
421503 }
422504 case spinner.TickMsg:
423− if s.showClaudeOAuth2 {
505+ switch {
506+ case s.showClaudeOAuth2:
424507 u, cmd := s.claudeOAuth2.Update(msg)
425508 s.claudeOAuth2 = u.(*claude.OAuth2)
426509 return s, cmd
427− } else {
510+ case s.showHyperDeviceFlow:
511+ u, cmd := s.hyperDeviceFlow.Update(msg)
512+ s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
513+ return s, cmd
514+ case s.showCopilotDeviceFlow:
515+ u, cmd := s.copilotDeviceFlow.Update(msg)
516+ s.copilotDeviceFlow = u.(*copilot.DeviceFlow)
517+ return s, cmd
518+ default:
428519 u, cmd := s.apiKeyInput.Update(msg)
429520 s.apiKeyInput = u.(*models.APIKeyInput)
430521 return s, cmd
@@ -562,8 +653,10 @@ func (s *splashCmp) isProviderConfigured(providerID string) bool {
562653func (s *splashCmp) View() string {
563654 t := styles.CurrentTheme()
564655 var content string
565− if s.showClaudeAuthMethodChooser {
566− remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
656+
657+ switch {
658+ case s.showClaudeAuthMethodChooser:
659+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
567660 chooserView := s.claudeAuthMethodChooser.View()
568661 authMethodSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
569662 lipgloss.JoinVertical(
@@ -578,8 +671,8 @@ func (s *splashCmp) View() string {
578671 s.logoRendered,
579672 authMethodSelector,
580673 )
581− } else if s.showClaudeOAuth2 {
582− remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
674+ case s.showClaudeOAuth2:
675+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
583676 oauth2View := s.claudeOAuth2.View()
584677 oauthSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
585678 lipgloss.JoinVertical(
@@ -594,8 +687,38 @@ func (s *splashCmp) View() string {
594687 s.logoRendered,
595688 oauthSelector,
596689 )
597− } else if s.needsAPIKey {
598− remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
690+ case s.showHyperDeviceFlow:
691+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
692+ hyperView := s.hyperDeviceFlow.View()
693+ hyperSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
694+ lipgloss.JoinVertical(
695+ lipgloss.Left,
696+ t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth Hyper"),
697+ hyperView,
698+ ),
699+ )
700+ content = lipgloss.JoinVertical(
701+ lipgloss.Left,
702+ s.logoRendered,
703+ hyperSelector,
704+ )
705+ case s.showCopilotDeviceFlow:
706+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
707+ copilotView := s.copilotDeviceFlow.View()
708+ copilotSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
709+ lipgloss.JoinVertical(
710+ lipgloss.Left,
711+ t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth GitHub Copilot"),
712+ copilotView,
713+ ),
714+ )
715+ content = lipgloss.JoinVertical(
716+ lipgloss.Left,
717+ s.logoRendered,
718+ copilotSelector,
719+ )
720+ case s.needsAPIKey:
721+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
599722 apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View())
600723 apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
601724 lipgloss.JoinVertical(
@@ -608,9 +731,9 @@ func (s *splashCmp) View() string {
608731 s.logoRendered,
609732 apiKeySelector,
610733 )
611− } else if s.isOnboarding {
734+ case s.isOnboarding:
612735 modelListView := s.modelList.View()
613− remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
736+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
614737 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
615738 lipgloss.JoinVertical(
616739 lipgloss.Left,
@@ -624,7 +747,7 @@ func (s *splashCmp) View() string {
624747 s.logoRendered,
625748 modelSelector,
626749 )
627− } else if s.needsProjectInit {
750+ case s.needsProjectInit:
628751 titleStyle := t.S().Base.Foreground(t.FgBase)
629752 pathStyle := t.S().Base.Foreground(t.Success).PaddingLeft(2)
630753 bodyStyle := t.S().Base.Foreground(t.FgMuted)
@@ -675,7 +798,7 @@ func (s *splashCmp) View() string {
675798 "",
676799 initContent,
677800 )
678− } else {
801+ default:
679802 parts := []string{
680803 s.logoRendered,
681804 s.infoSection(),
@@ -692,28 +815,25 @@ func (s *splashCmp) View() string {
692815}
693816
694817func (s *splashCmp) Cursor() *tea.Cursor {
695− if s.showClaudeAuthMethodChooser {
818+ switch {
819+ case s.showClaudeAuthMethodChooser:
696820 return nil
697− }
698− if s.showClaudeOAuth2 {
821+ case s.showClaudeOAuth2:
699822 if cursor := s.claudeOAuth2.CodeInput.Cursor(); cursor != nil {
700823 cursor.Y += 2 // FIXME(@andreynering): Why do we need this?
701824 return s.moveCursor(cursor)
702825 }
703826 return nil
704− }
705− if s.needsAPIKey {
827+ case s.needsAPIKey:
706828 cursor := s.apiKeyInput.Cursor()
707829 if cursor != nil {
708830 return s.moveCursor(cursor)
709831 }
710− } else if s.isOnboarding {
832+ case s.isOnboarding:
711833 cursor := s.modelList.Cursor()
712834 if cursor != nil {
713835 return s.moveCursor(cursor)
714836 }
715− } else {
716− return nil
717837 }
718838 return nil
719839}
@@ -805,13 +925,14 @@ func (s *splashCmp) logoGap() int {
805925
806926// Bindings implements SplashPage.
807927func (s *splashCmp) Bindings() []key.Binding {
808− if s.showClaudeAuthMethodChooser {
928+ switch {
929+ case s.showClaudeAuthMethodChooser:
809930 return []key.Binding{
810931 s.keyMap.Select,
811932 s.keyMap.Tab,
812933 s.keyMap.Back,
813934 }
814− } else if s.showClaudeOAuth2 {
935+ case s.showClaudeOAuth2:
815936 bindings := []key.Binding{
816937 s.keyMap.Select,
817938 }
@@ -819,18 +940,18 @@ func (s *splashCmp) Bindings() []key.Binding {
819940 bindings = append(bindings, s.keyMap.Copy)
820941 }
821942 return bindings
822− } else if s.needsAPIKey {
943+ case s.needsAPIKey:
823944 return []key.Binding{
824945 s.keyMap.Select,
825946 s.keyMap.Back,
826947 }
827− } else if s.isOnboarding {
948+ case s.isOnboarding:
828949 return []key.Binding{
829950 s.keyMap.Select,
830951 s.keyMap.Next,
831952 s.keyMap.Previous,
832953 }
833− } else if s.needsProjectInit {
954+ case s.needsProjectInit:
834955 return []key.Binding{
835956 s.keyMap.Select,
836957 s.keyMap.Yes,
@@ -838,8 +959,9 @@ func (s *splashCmp) Bindings() []key.Binding {
838959 s.keyMap.Tab,
839960 s.keyMap.LeftRight,
840961 }
962+ default:
963+ return []key.Binding{}
841964 }
842− return []key.Binding{}
843965}
844966
845967func (s *splashCmp) getMaxInfoWidth() int {
@@ -940,3 +1062,11 @@ func (s *splashCmp) IsClaudeOAuthURLState() bool {
9401062func (s *splashCmp) IsClaudeOAuthComplete() bool {
9411063 return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateCode && s.claudeOAuth2.ValidationState == claude.OAuthValidationStateValid
9421064}
1065+
1066+func (s *splashCmp) IsShowingHyperOAuth2() bool {
1067+ return s.showHyperDeviceFlow
1068+}
1069+
1070+func (s *splashCmp) IsShowingCopilotOAuth2() bool {
1071+ return s.showCopilotDeviceFlow
1072+}