@@ -0,0 +1,72 @@
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"
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{})
19+type OAuthCopilot struct {
20+ deviceCode *copilot.DeviceCode
24+var _ OAuthProvider = (*OAuthCopilot)(nil)
26+func (m *OAuthCopilot) name() string {
27+ return "GitHub Copilot"
30+func (m *OAuthCopilot) initiateAuth() tea.Msg {
31+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
34+ deviceCode, err := copilot.RequestDeviceCode(ctx)
36+ return ActionOAuthErrored{Error: fmt.Errorf("failed to initiate device auth: %w", err)}
39+ m.deviceCode = deviceCode
41+ return ActionInitiateOAuth{
42+ DeviceCode: deviceCode.DeviceCode,
43+ UserCode: deviceCode.UserCode,
44+ VerificationURL: deviceCode.VerificationURI,
45+ ExpiresIn: deviceCode.ExpiresIn,
46+ Interval: deviceCode.Interval,
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
55+ token, err := copilot.PollForToken(ctx, m.deviceCode)
57+ if ctx.Err() != nil {
58+ return nil // cancelled, don't report error.
60+ return ActionOAuthErrored{Error: err}
63+ return ActionCompleteOAuth{Token: token}
67+func (m *OAuthCopilot) stopPolling() tea.Msg {
68+ if m.cancelFunc != nil {