1package dialog
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 tea "charm.land/bubbletea/v2"
9 "charm.land/catwalk/pkg/catwalk"
10 "github.com/charmbracelet/crush/internal/config"
11 "github.com/charmbracelet/crush/internal/oauth/copilot"
12 "github.com/charmbracelet/crush/internal/ui/common"
13)
14
15func NewOAuthCopilot(
16 com *common.Common,
17 isOnboarding bool,
18 provider catwalk.Provider,
19 model config.SelectedModel,
20 modelType config.SelectedModelType,
21) (*OAuth, tea.Cmd) {
22 return newOAuth(com, isOnboarding, provider, model, modelType, &OAuthCopilot{})
23}
24
25type OAuthCopilot struct {
26 deviceCode *copilot.DeviceCode
27 cancelFunc func()
28}
29
30var _ OAuthProvider = (*OAuthCopilot)(nil)
31
32func (m *OAuthCopilot) name() string {
33 return "GitHub Copilot"
34}
35
36func (m *OAuthCopilot) initiateAuth() tea.Msg {
37 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
38 defer cancel()
39
40 deviceCode, err := copilot.RequestDeviceCode(ctx)
41 if err != nil {
42 return ActionOAuthErrored{Error: fmt.Errorf("failed to initiate device auth: %w", err)}
43 }
44
45 m.deviceCode = deviceCode
46
47 return ActionInitiateOAuth{
48 DeviceCode: deviceCode.DeviceCode,
49 UserCode: deviceCode.UserCode,
50 VerificationURL: deviceCode.VerificationURI,
51 ExpiresIn: deviceCode.ExpiresIn,
52 Interval: deviceCode.Interval,
53 }
54}
55
56func (m *OAuthCopilot) startPolling(deviceCode string, expiresIn int) tea.Cmd {
57 return func() tea.Msg {
58 ctx, cancel := context.WithCancel(context.Background())
59 m.cancelFunc = cancel
60
61 token, err := copilot.PollForToken(ctx, m.deviceCode)
62 if err != nil {
63 if ctx.Err() != nil {
64 return nil // cancelled, don't report error.
65 }
66 return ActionOAuthErrored{Error: err}
67 }
68
69 return ActionCompleteOAuth{Token: token}
70 }
71}
72
73func (m *OAuthCopilot) stopPolling() tea.Msg {
74 if m.cancelFunc != nil {
75 m.cancelFunc()
76 }
77 return nil
78}