on main help sign in
feat: make hyper and copilot login work on onboarding
13f315cAndrey Nering
internal/tui/components/chat/splash/splash.go+176−46
@@ -12,12 +12,15 @@ import (
12 "github.com/atotto/clipboard"
12 "github.com/atotto/clipboard"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/crush/internal/agent"
14 "github.com/charmbracelet/crush/internal/agent"
15+ hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
15 "github.com/charmbracelet/crush/internal/config"
16 "github.com/charmbracelet/crush/internal/config"
16 "github.com/charmbracelet/crush/internal/home"
17 "github.com/charmbracelet/crush/internal/home"
17 "github.com/charmbracelet/crush/internal/tui/components/chat"
18 "github.com/charmbracelet/crush/internal/tui/components/chat"
18 "github.com/charmbracelet/crush/internal/tui/components/core"
19 "github.com/charmbracelet/crush/internal/tui/components/core"
19 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
20 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
20 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
21 "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"
21 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
24 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
22 "github.com/charmbracelet/crush/internal/tui/components/logo"
25 "github.com/charmbracelet/crush/internal/tui/components/logo"
23 lspcomponent "github.com/charmbracelet/crush/internal/tui/components/lsp"
26 lspcomponent "github.com/charmbracelet/crush/internal/tui/components/lsp"
@@ -55,6 +58,12 @@ type Splash interface {
55
58
56 // IsClaudeOAuthComplete returns whether Claude OAuth flow is complete
59 // IsClaudeOAuthComplete returns whether Claude OAuth flow is complete
57 IsClaudeOAuthComplete() bool
60 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
58}
67}
59
68
60const (
69const (
@@ -87,6 +96,14 @@ type splashCmp struct {
87 isAPIKeyValid bool
96 isAPIKeyValid bool
88 apiKeyValue string
97 apiKeyValue string
89
98
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+
90 // Claude state
107 // Claude state
91 claudeAuthMethodChooser *claude.AuthMethodChooser
108 claudeAuthMethodChooser *claude.AuthMethodChooser
92 claudeOAuth2 *claude.OAuth2
109 claudeOAuth2 *claude.OAuth2
@@ -186,6 +203,26 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
186 }
203 }
187
204
188 return s, tea.Batch(cmds...)
205 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)
189 case claude.AuthenticationCompleteMsg:
226 case claude.AuthenticationCompleteMsg:
190 s.showClaudeAuthMethodChooser = false
227 s.showClaudeAuthMethodChooser = false
191 s.showClaudeOAuth2 = false
228 s.showClaudeOAuth2 = false
@@ -205,6 +242,10 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
205 }
242 }
206 case tea.KeyPressMsg:
243 case tea.KeyPressMsg:
207 switch {
244 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()
208 case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL:
249 case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL:
209 return s, tea.Sequence(
250 return s, tea.Sequence(
210 tea.SetClipboard(s.claudeOAuth2.URL),
251 tea.SetClipboard(s.claudeOAuth2.URL),
@@ -223,21 +264,27 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
223 s.claudeOAuth2 = u.(*claude.OAuth2)
264 s.claudeOAuth2 = u.(*claude.OAuth2)
224 return s, cmd
265 return s, cmd
225 case key.Matches(msg, s.keyMap.Back):
266 case key.Matches(msg, s.keyMap.Back):
267+ switch {
226− if s.showClaudeAuthMethodChooser {
268+ case s.showClaudeAuthMethodChooser:
227 s.claudeAuthMethodChooser.SetDefaults()
269 s.claudeAuthMethodChooser.SetDefaults()
228 s.showClaudeAuthMethodChooser = false
270 s.showClaudeAuthMethodChooser = false
229 return s, nil
271 return s, nil
230− }
231− if s.showClaudeOAuth2 {
272+ case s.showClaudeOAuth2:
232 s.claudeOAuth2.SetDefaults()
273 s.claudeOAuth2.SetDefaults()
233 s.showClaudeOAuth2 = false
274 s.showClaudeOAuth2 = false
234 s.showClaudeAuthMethodChooser = true
275 s.showClaudeAuthMethodChooser = true
235 return s, nil
276 return s, nil
236− }
277+ case s.showHyperDeviceFlow:
237− if s.isAPIKeyValid {
278+ s.hyperDeviceFlow = nil
279+ s.showHyperDeviceFlow = false
238 return s, nil
280 return s, nil
239− }
281+ case s.showCopilotDeviceFlow:
282+ s.copilotDeviceFlow = nil
283+ s.showCopilotDeviceFlow = false
284+ return s, nil
285+ case s.isAPIKeyValid:
286+ return s, nil
240− if s.needsAPIKey {
287+ case s.needsAPIKey:
241 if s.selectedModel.Provider.ID == catwalk.InferenceProviderAnthropic {
288 if s.selectedModel.Provider.ID == catwalk.InferenceProviderAnthropic {
242 s.showClaudeAuthMethodChooser = true
289 s.showClaudeAuthMethodChooser = true
243 }
290 }
@@ -249,7 +296,8 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
249 return s, nil
296 return s, nil
250 }
297 }
251 case key.Matches(msg, s.keyMap.Select):
298 case key.Matches(msg, s.keyMap.Select):
299+ switch {
252− if s.showClaudeAuthMethodChooser {
300+ case s.showClaudeAuthMethodChooser:
253 selectedItem := s.modelList.SelectedModel()
301 selectedItem := s.modelList.SelectedModel()
254 if selectedItem == nil {
302 if selectedItem == nil {
255 return s, nil
303 return s, nil
@@ -267,16 +315,17 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
267 s.showClaudeOAuth2 = true
315 s.showClaudeOAuth2 = true
268 }
316 }
269 return s, nil
317 return s, nil
270− }
271− if s.showClaudeOAuth2 {
318+ case s.showClaudeOAuth2:
272 m2, cmd2 := s.claudeOAuth2.ValidationConfirm()
319 m2, cmd2 := s.claudeOAuth2.ValidationConfirm()
273 s.claudeOAuth2 = m2.(*claude.OAuth2)
320 s.claudeOAuth2 = m2.(*claude.OAuth2)
274 return s, cmd2
321 return s, cmd2
275− }
322+ case s.showHyperDeviceFlow:
323+ return s, s.hyperDeviceFlow.CopyCodeAndOpenURL()
324+ case s.showCopilotDeviceFlow:
325+ return s, s.copilotDeviceFlow.CopyCodeAndOpenURL()
276− if s.isAPIKeyValid {
326+ case s.isAPIKeyValid:
277 return s, s.saveAPIKeyAndContinue(s.apiKeyValue, true)
327 return s, s.saveAPIKeyAndContinue(s.apiKeyValue, true)
278− }
279− if s.isOnboarding && !s.needsAPIKey {
328+ case s.isOnboarding && !s.needsAPIKey:
280 selectedItem := s.modelList.SelectedModel()
329 selectedItem := s.modelList.SelectedModel()
281 if selectedItem == nil {
330 if selectedItem == nil {
282 return s, nil
331 return s, nil
@@ -286,9 +335,22 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
286 s.isOnboarding = false
335 s.isOnboarding = false
287 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
336 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
288 } else {
337 } else {
338+ switch selectedItem.Provider.ID {
289− if selectedItem.Provider.ID == catwalk.InferenceProviderAnthropic {
339+ case catwalk.InferenceProviderAnthropic:
290 s.showClaudeAuthMethodChooser = true
340 s.showClaudeAuthMethodChooser = true
291 return s, nil
341 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()
292 }
354 }
293 // Provider not configured, show API key input
355 // Provider not configured, show API key input
294 s.needsAPIKey = true
356 s.needsAPIKey = true
@@ -296,7 +358,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
296 s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
358 s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
297 return s, nil
359 return s, nil
298 }
360 }
299− } else if s.needsAPIKey {
361+ case s.needsAPIKey:
300 // Handle API key submission
362 // Handle API key submission
301 s.apiKeyValue = strings.TrimSpace(s.apiKeyInput.Value())
363 s.apiKeyValue = strings.TrimSpace(s.apiKeyInput.Value())
302 if s.apiKeyValue == "" {
364 if s.apiKeyValue == "" {
@@ -337,7 +399,7 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
337 }
399 }
338 },
400 },
339 )
401 )
340− } else if s.needsProjectInit {
402+ case s.needsProjectInit:
341 return s, s.initializeProject()
403 return s, s.initializeProject()
342 }
404 }
343 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
405 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) {
385 return s, s.initializeProject()
447 return s, s.initializeProject()
386 }
448 }
387 default:
449 default:
450+ switch {
388− if s.showClaudeAuthMethodChooser {
451+ case s.showClaudeAuthMethodChooser:
389 u, cmd := s.claudeAuthMethodChooser.Update(msg)
452 u, cmd := s.claudeAuthMethodChooser.Update(msg)
390 s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
453 s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
391 return s, cmd
454 return s, cmd
392− } else if s.showClaudeOAuth2 {
455+ case s.showClaudeOAuth2:
393 u, cmd := s.claudeOAuth2.Update(msg)
456 u, cmd := s.claudeOAuth2.Update(msg)
394 s.claudeOAuth2 = u.(*claude.OAuth2)
457 s.claudeOAuth2 = u.(*claude.OAuth2)
395 return s, cmd
458 return s, cmd
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
396− } else if s.needsAPIKey {
467+ case s.needsAPIKey:
397 u, cmd := s.apiKeyInput.Update(msg)
468 u, cmd := s.apiKeyInput.Update(msg)
398 s.apiKeyInput = u.(*models.APIKeyInput)
469 s.apiKeyInput = u.(*models.APIKeyInput)
399 return s, cmd
470 return s, cmd
400− } else if s.isOnboarding {
471+ case s.isOnboarding:
401 u, cmd := s.modelList.Update(msg)
472 u, cmd := s.modelList.Update(msg)
402 s.modelList = u
473 s.modelList = u
403 return s, cmd
474 return s, cmd
404 }
475 }
405 }
476 }
406 case tea.PasteMsg:
477 case tea.PasteMsg:
478+ switch {
407− if s.showClaudeOAuth2 {
479+ case s.showClaudeOAuth2:
408 u, cmd := s.claudeOAuth2.Update(msg)
480 u, cmd := s.claudeOAuth2.Update(msg)
409 s.claudeOAuth2 = u.(*claude.OAuth2)
481 s.claudeOAuth2 = u.(*claude.OAuth2)
410 return s, cmd
482 return s, cmd
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
411− } else if s.needsAPIKey {
491+ case s.needsAPIKey:
412 u, cmd := s.apiKeyInput.Update(msg)
492 u, cmd := s.apiKeyInput.Update(msg)
413 s.apiKeyInput = u.(*models.APIKeyInput)
493 s.apiKeyInput = u.(*models.APIKeyInput)
414 return s, cmd
494 return s, cmd
415− } else if s.isOnboarding {
495+ case s.isOnboarding:
416 var cmd tea.Cmd
496 var cmd tea.Cmd
417 s.modelList, cmd = s.modelList.Update(msg)
497 s.modelList, cmd = s.modelList.Update(msg)
418 return s, cmd
498 return s, cmd
419 }
499 }
420 case spinner.TickMsg:
500 case spinner.TickMsg:
501+ switch {
421− if s.showClaudeOAuth2 {
502+ case s.showClaudeOAuth2:
422 u, cmd := s.claudeOAuth2.Update(msg)
503 u, cmd := s.claudeOAuth2.Update(msg)
423 s.claudeOAuth2 = u.(*claude.OAuth2)
504 s.claudeOAuth2 = u.(*claude.OAuth2)
424 return s, cmd
505 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:
426 u, cmd := s.apiKeyInput.Update(msg)
515 u, cmd := s.apiKeyInput.Update(msg)
427 s.apiKeyInput = u.(*models.APIKeyInput)
516 s.apiKeyInput = u.(*models.APIKeyInput)
428 return s, cmd
517 return s, cmd
@@ -560,7 +649,9 @@ func (s *splashCmp) isProviderConfigured(providerID string) bool {
560func (s *splashCmp) View() string {
649func (s *splashCmp) View() string {
561 t := styles.CurrentTheme()
650 t := styles.CurrentTheme()
562 var content string
651 var content string
652+
653+ switch {
563− if s.showClaudeAuthMethodChooser {
654+ case s.showClaudeAuthMethodChooser:
564 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
655 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
565 chooserView := s.claudeAuthMethodChooser.View()
656 chooserView := s.claudeAuthMethodChooser.View()
566 authMethodSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
657 authMethodSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -576,7 +667,7 @@ func (s *splashCmp) View() string {
576 s.logoRendered,
667 s.logoRendered,
577 authMethodSelector,
668 authMethodSelector,
578 )
669 )
579− } else if s.showClaudeOAuth2 {
670+ case s.showClaudeOAuth2:
580 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
671 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
581 oauth2View := s.claudeOAuth2.View()
672 oauth2View := s.claudeOAuth2.View()
582 oauthSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
673 oauthSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -592,7 +683,39 @@ func (s *splashCmp) View() string {
592 s.logoRendered,
683 s.logoRendered,
593 oauthSelector,
684 oauthSelector,
594 )
685 )
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+ )
595− } else if s.needsAPIKey {
718+ case s.needsAPIKey:
596 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
719 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
597 apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View())
720 apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View())
598 apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
721 apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -606,7 +729,7 @@ func (s *splashCmp) View() string {
606 s.logoRendered,
729 s.logoRendered,
607 apiKeySelector,
730 apiKeySelector,
608 )
731 )
609− } else if s.isOnboarding {
732+ case s.isOnboarding:
610 modelListView := s.modelList.View()
733 modelListView := s.modelList.View()
611 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
734 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
612 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
735 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -622,7 +745,7 @@ func (s *splashCmp) View() string {
622 s.logoRendered,
745 s.logoRendered,
623 modelSelector,
746 modelSelector,
624 )
747 )
625− } else if s.needsProjectInit {
748+ case s.needsProjectInit:
626 titleStyle := t.S().Base.Foreground(t.FgBase)
749 titleStyle := t.S().Base.Foreground(t.FgBase)
627 pathStyle := t.S().Base.Foreground(t.Success).PaddingLeft(2)
750 pathStyle := t.S().Base.Foreground(t.Success).PaddingLeft(2)
628 bodyStyle := t.S().Base.Foreground(t.FgMuted)
751 bodyStyle := t.S().Base.Foreground(t.FgMuted)
@@ -673,7 +796,7 @@ func (s *splashCmp) View() string {
673 "",
796 "",
674 initContent,
797 initContent,
675 )
798 )
676− } else {
799+ default:
677 parts := []string{
800 parts := []string{
678 s.logoRendered,
801 s.logoRendered,
679 s.infoSection(),
802 s.infoSection(),
@@ -690,28 +813,25 @@ func (s *splashCmp) View() string {
690}
813}
691
814
692func (s *splashCmp) Cursor() *tea.Cursor {
815func (s *splashCmp) Cursor() *tea.Cursor {
816+ switch {
693− if s.showClaudeAuthMethodChooser {
817+ case s.showClaudeAuthMethodChooser:
694 return nil
818 return nil
695− }
696− if s.showClaudeOAuth2 {
819+ case s.showClaudeOAuth2:
697 if cursor := s.claudeOAuth2.CodeInput.Cursor(); cursor != nil {
820 if cursor := s.claudeOAuth2.CodeInput.Cursor(); cursor != nil {
698 cursor.Y += 2 // FIXME(@andreynering): Why do we need this?
821 cursor.Y += 2 // FIXME(@andreynering): Why do we need this?
699 return s.moveCursor(cursor)
822 return s.moveCursor(cursor)
700 }
823 }
701 return nil
824 return nil
702− }
703− if s.needsAPIKey {
825+ case s.needsAPIKey:
704 cursor := s.apiKeyInput.Cursor()
826 cursor := s.apiKeyInput.Cursor()
705 if cursor != nil {
827 if cursor != nil {
706 return s.moveCursor(cursor)
828 return s.moveCursor(cursor)
707 }
829 }
708− } else if s.isOnboarding {
830+ case s.isOnboarding:
709 cursor := s.modelList.Cursor()
831 cursor := s.modelList.Cursor()
710 if cursor != nil {
832 if cursor != nil {
711 return s.moveCursor(cursor)
833 return s.moveCursor(cursor)
712 }
834 }
713− } else {
714− return nil
715 }
835 }
716 return nil
836 return nil
717}
837}
@@ -803,13 +923,14 @@ func (s *splashCmp) logoGap() int {
803
923
804// Bindings implements SplashPage.
924// Bindings implements SplashPage.
805func (s *splashCmp) Bindings() []key.Binding {
925func (s *splashCmp) Bindings() []key.Binding {
926+ switch {
806− if s.showClaudeAuthMethodChooser {
927+ case s.showClaudeAuthMethodChooser:
807 return []key.Binding{
928 return []key.Binding{
808 s.keyMap.Select,
929 s.keyMap.Select,
809 s.keyMap.Tab,
930 s.keyMap.Tab,
810 s.keyMap.Back,
931 s.keyMap.Back,
811 }
932 }
812− } else if s.showClaudeOAuth2 {
933+ case s.showClaudeOAuth2:
813 bindings := []key.Binding{
934 bindings := []key.Binding{
814 s.keyMap.Select,
935 s.keyMap.Select,
815 }
936 }
@@ -817,18 +938,18 @@ func (s *splashCmp) Bindings() []key.Binding {
817 bindings = append(bindings, s.keyMap.Copy)
938 bindings = append(bindings, s.keyMap.Copy)
818 }
939 }
819 return bindings
940 return bindings
820− } else if s.needsAPIKey {
941+ case s.needsAPIKey:
821 return []key.Binding{
942 return []key.Binding{
822 s.keyMap.Select,
943 s.keyMap.Select,
823 s.keyMap.Back,
944 s.keyMap.Back,
824 }
945 }
825− } else if s.isOnboarding {
946+ case s.isOnboarding:
826 return []key.Binding{
947 return []key.Binding{
827 s.keyMap.Select,
948 s.keyMap.Select,
828 s.keyMap.Next,
949 s.keyMap.Next,
829 s.keyMap.Previous,
950 s.keyMap.Previous,
830 }
951 }
831− } else if s.needsProjectInit {
952+ case s.needsProjectInit:
832 return []key.Binding{
953 return []key.Binding{
833 s.keyMap.Select,
954 s.keyMap.Select,
834 s.keyMap.Yes,
955 s.keyMap.Yes,
@@ -836,8 +957,9 @@ func (s *splashCmp) Bindings() []key.Binding {
836 s.keyMap.Tab,
957 s.keyMap.Tab,
837 s.keyMap.LeftRight,
958 s.keyMap.LeftRight,
838 }
959 }
960+ default:
961+ return []key.Binding{}
839 }
962 }
840− return []key.Binding{}
841}
963}
842
964
843func (s *splashCmp) getMaxInfoWidth() int {
965func (s *splashCmp) getMaxInfoWidth() int {
@@ -938,3 +1060,11 @@ func (s *splashCmp) IsClaudeOAuthURLState() bool {
938func (s *splashCmp) IsClaudeOAuthComplete() bool {
1060func (s *splashCmp) IsClaudeOAuthComplete() bool {
939 return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateCode && s.claudeOAuth2.ValidationState == claude.OAuthValidationStateValid
1061 return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateCode && s.claudeOAuth2.ValidationState == claude.OAuthValidationStateValid
940}
1062}
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) {
174 case tea.KeyPressMsg:
174 case tea.KeyPressMsg:
175 switch {
175 switch {
176 // Handle Hyper device flow keys
176 // Handle Hyper device flow keys
177− case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && (m.showHyperDeviceFlow || m.showCopilotDeviceFlow):
177+ case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showHyperDeviceFlow:
178− if m.hyperDeviceFlow != nil {
178+ return m, m.hyperDeviceFlow.CopyCode()
179− return m, m.hyperDeviceFlow.CopyCode()
179+ case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showCopilotDeviceFlow:
180− }
181− if m.copilotDeviceFlow != nil {
182− return m, m.copilotDeviceFlow.CopyCode()
180+ return m, m.copilotDeviceFlow.CopyCode()
183− }
184 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL:
181 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL:
185 return m, tea.Sequence(
182 return m, tea.Sequence(
186 tea.SetClipboard(m.claudeOAuth2.URL),
183 tea.SetClipboard(m.claudeOAuth2.URL),
@@ -337,28 +334,26 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
337 return m, m.modelList.SetModelType(LargeModelType)
334 return m, m.modelList.SetModelType(LargeModelType)
338 }
335 }
339 case key.Matches(msg, m.keyMap.Close):
336 case key.Matches(msg, m.keyMap.Close):
337+ switch {
340− if m.showHyperDeviceFlow {
338+ case m.showHyperDeviceFlow:
341 if m.hyperDeviceFlow != nil {
339 if m.hyperDeviceFlow != nil {
342 m.hyperDeviceFlow.Cancel()
340 m.hyperDeviceFlow.Cancel()
343 }
341 }
344 m.showHyperDeviceFlow = false
342 m.showHyperDeviceFlow = false
345 m.selectedModel = nil
343 m.selectedModel = nil
346− }
347− if m.showCopilotDeviceFlow {
344+ case m.showCopilotDeviceFlow:
348 if m.copilotDeviceFlow != nil {
345 if m.copilotDeviceFlow != nil {
349 m.copilotDeviceFlow.Cancel()
346 m.copilotDeviceFlow.Cancel()
350 }
347 }
351 m.showCopilotDeviceFlow = false
348 m.showCopilotDeviceFlow = false
352 m.selectedModel = nil
349 m.selectedModel = nil
353− }
354− if m.showClaudeAuthMethodChooser {
350+ case m.showClaudeAuthMethodChooser:
355 m.claudeAuthMethodChooser.SetDefaults()
351 m.claudeAuthMethodChooser.SetDefaults()
356 m.showClaudeAuthMethodChooser = false
352 m.showClaudeAuthMethodChooser = false
357 m.keyMap.isClaudeAuthChoiceHelp = false
353 m.keyMap.isClaudeAuthChoiceHelp = false
358 m.keyMap.isClaudeOAuthHelp = false
354 m.keyMap.isClaudeOAuthHelp = false
359 return m, nil
355 return m, nil
360− }
361− if m.needsAPIKey {
356+ case m.needsAPIKey:
362 if m.isAPIKeyValid {
357 if m.isAPIKeyValid {
363 return m, nil
358 return m, nil
364 }
359 }
@@ -369,37 +364,40 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
369 m.apiKeyValue = ""
364 m.apiKeyValue = ""
370 m.apiKeyInput.Reset()
365 m.apiKeyInput.Reset()
371 return m, nil
366 return m, nil
367+ default:
368+ return m, util.CmdHandler(dialogs.CloseDialogMsg{})
372 }
369 }
373− return m, util.CmdHandler(dialogs.CloseDialogMsg{})
374 default:
370 default:
371+ switch {
375− if m.showClaudeAuthMethodChooser {
372+ case m.showClaudeAuthMethodChooser:
376 u, cmd := m.claudeAuthMethodChooser.Update(msg)
373 u, cmd := m.claudeAuthMethodChooser.Update(msg)
377 m.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
374 m.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
378 return m, cmd
375 return m, cmd
379− } else if m.showClaudeOAuth2 {
376+ case m.showClaudeOAuth2:
380 u, cmd := m.claudeOAuth2.Update(msg)
377 u, cmd := m.claudeOAuth2.Update(msg)
381 m.claudeOAuth2 = u.(*claude.OAuth2)
378 m.claudeOAuth2 = u.(*claude.OAuth2)
382 return m, cmd
379 return m, cmd
383− } else if m.needsAPIKey {
380+ case m.needsAPIKey:
384 u, cmd := m.apiKeyInput.Update(msg)
381 u, cmd := m.apiKeyInput.Update(msg)
385 m.apiKeyInput = u.(*APIKeyInput)
382 m.apiKeyInput = u.(*APIKeyInput)
386 return m, cmd
383 return m, cmd
387− } else {
384+ default:
388 u, cmd := m.modelList.Update(msg)
385 u, cmd := m.modelList.Update(msg)
389 m.modelList = u
386 m.modelList = u
390 return m, cmd
387 return m, cmd
391 }
388 }
392 }
389 }
393 case tea.PasteMsg:
390 case tea.PasteMsg:
391+ switch {
394− if m.showClaudeOAuth2 {
392+ case m.showClaudeOAuth2:
395 u, cmd := m.claudeOAuth2.Update(msg)
393 u, cmd := m.claudeOAuth2.Update(msg)
396 m.claudeOAuth2 = u.(*claude.OAuth2)
394 m.claudeOAuth2 = u.(*claude.OAuth2)
397 return m, cmd
395 return m, cmd
398− } else if m.needsAPIKey {
396+ case m.needsAPIKey:
399 u, cmd := m.apiKeyInput.Update(msg)
397 u, cmd := m.apiKeyInput.Update(msg)
400 m.apiKeyInput = u.(*APIKeyInput)
398 m.apiKeyInput = u.(*APIKeyInput)
401 return m, cmd
399 return m, cmd
402− } else {
400+ default:
403 var cmd tea.Cmd
401 var cmd tea.Cmd
404 m.modelList, cmd = m.modelList.Update(msg)
402 m.modelList, cmd = m.modelList.Update(msg)
405 return m, cmd
403 return m, cmd
internal/tui/page/chat/chat.go+25−4
@@ -31,7 +31,9 @@ import (
31 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
31 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
32 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
32 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
33 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
33 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
34+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
34 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
35 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
36+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
35 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
37 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
36 "github.com/charmbracelet/crush/internal/tui/components/dialogs/reasoning"
38 "github.com/charmbracelet/crush/internal/tui/components/dialogs/reasoning"
37 "github.com/charmbracelet/crush/internal/tui/page"
39 "github.com/charmbracelet/crush/internal/tui/page"
@@ -335,7 +337,14 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
335 cmds = append(cmds, cmd)
337 cmds = append(cmds, cmd)
336 return p, tea.Batch(cmds...)
338 return p, tea.Batch(cmds...)
337
339
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:
339 if p.focusedPane == PanelTypeSplash {
348 if p.focusedPane == PanelTypeSplash {
340 u, cmd := p.splash.Update(msg)
349 u, cmd := p.splash.Update(msg)
341 p.splash = u.(splash.Splash)
350 p.splash = u.(splash.Splash)
@@ -1050,7 +1059,8 @@ func (p *chatPage) Help() help.KeyMap {
1050 fullList = append(fullList, []key.Binding{v})
1059 fullList = append(fullList, []key.Binding{v})
1051 }
1060 }
1052 case p.isOnboarding && p.splash.IsShowingClaudeOAuth2():
1061 case p.isOnboarding && p.splash.IsShowingClaudeOAuth2():
1062+ switch {
1053− if p.splash.IsClaudeOAuthURLState() {
1063+ case p.splash.IsClaudeOAuthURLState():
1054 shortList = append(shortList,
1064 shortList = append(shortList,
1055 key.NewBinding(
1065 key.NewBinding(
1056 key.WithKeys("enter"),
1066 key.WithKeys("enter"),
@@ -1061,14 +1071,25 @@ func (p *chatPage) Help() help.KeyMap {
1061 key.WithHelp("c", "copy url"),
1071 key.WithHelp("c", "copy url"),
1062 ),
1072 ),
1063 )
1073 )
1064− } else if p.splash.IsClaudeOAuthComplete() {
1074+ case p.splash.IsClaudeOAuthComplete():
1065 shortList = append(shortList,
1075 shortList = append(shortList,
1066 key.NewBinding(
1076 key.NewBinding(
1067 key.WithKeys("enter"),
1077 key.WithKeys("enter"),
1068 key.WithHelp("enter", "continue"),
1078 key.WithHelp("enter", "continue"),
1069 ),
1079 ),
1070 )
1080 )
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:
1072 shortList = append(shortList,
1093 shortList = append(shortList,
1073 key.NewBinding(
1094 key.NewBinding(
1074 key.WithKeys("enter"),
1095 key.WithKeys("enter"),