@@ -0,0 +1,72 @@
1+package dialog
2+
3+import (
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+
15+func NewOAuthCopilot(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, error) {
16+ return newOAuth(com, provider, model, modelType, &OAuthCopilot{})
17+}
18+
19+type OAuthCopilot struct {
20+ deviceCode *copilot.DeviceCode
21+ cancelFunc func()
22+}
23+
24+var _ OAuthProvider = (*OAuthCopilot)(nil)
25+
26+func (m *OAuthCopilot) name() string {
27+ return "GitHub Copilot"
28+}
29+
30+func (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+
50+func (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+
67+func (m *OAuthCopilot) stopPolling() tea.Msg {
68+ if m.cancelFunc != nil {
69+ m.cancelFunc()
70+ }
71+ return nil
72+}