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