oauth_copilot.go

 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}