internal/ui/dialog/actions.go 🔗
@@ -83,6 +83,7 @@ type (
UserCode string
ExpiresIn int
VerificationURL string
+ Interval int
}
// ActionCompleteOAuth is sent when the device flow completes successfully.
Andrey Nering created
internal/ui/dialog/actions.go | 1
internal/ui/dialog/oauth.go | 2
internal/ui/dialog/oauth_copilot.go | 72 ++++++++++++++++++++++++++++++
internal/ui/model/ui.go | 27 ++++++++++-
4 files changed, 99 insertions(+), 3 deletions(-)
@@ -83,6 +83,7 @@ type (
UserCode string
ExpiresIn int
VerificationURL string
+ Interval int
}
// ActionCompleteOAuth is sent when the device flow completes successfully.
@@ -63,6 +63,7 @@ type OAuth struct {
userCode string
verificationURL string
expiresIn int
+ interval int
token *oauth.Token
cancelFunc context.CancelFunc
}
@@ -157,6 +158,7 @@ func (m *OAuth) HandleMsg(msg tea.Msg) Action {
m.userCode = msg.UserCode
m.expiresIn = msg.ExpiresIn
m.verificationURL = msg.VerificationURL
+ m.interval = msg.Interval
m.State = OAuthStateDisplay
return ActionCmd{m.oAuthProvider.startPolling(msg.DeviceCode, msg.ExpiresIn)}
@@ -0,0 +1,72 @@
+package dialog
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ tea "charm.land/bubbletea/v2"
+ "github.com/charmbracelet/catwalk/pkg/catwalk"
+ "github.com/charmbracelet/crush/internal/config"
+ "github.com/charmbracelet/crush/internal/oauth/copilot"
+ "github.com/charmbracelet/crush/internal/ui/common"
+)
+
+func NewOAuthCopilot(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*OAuth, error) {
+ return newOAuth(com, provider, model, modelType, &OAuthCopilot{})
+}
+
+type OAuthCopilot struct {
+ deviceCode *copilot.DeviceCode
+ cancelFunc func()
+}
+
+var _ OAuthProvider = (*OAuthCopilot)(nil)
+
+func (m *OAuthCopilot) name() string {
+ return "GitHub Copilot"
+}
+
+func (m *OAuthCopilot) initiateAuth() tea.Msg {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ deviceCode, err := copilot.RequestDeviceCode(ctx)
+ if err != nil {
+ return ActionOAuthErrored{Error: fmt.Errorf("failed to initiate device auth: %w", err)}
+ }
+
+ m.deviceCode = deviceCode
+
+ return ActionInitiateOAuth{
+ DeviceCode: deviceCode.DeviceCode,
+ UserCode: deviceCode.UserCode,
+ VerificationURL: deviceCode.VerificationURI,
+ ExpiresIn: deviceCode.ExpiresIn,
+ Interval: deviceCode.Interval,
+ }
+}
+
+func (m *OAuthCopilot) startPolling(deviceCode string, expiresIn int) tea.Cmd {
+ return func() tea.Msg {
+ ctx, cancel := context.WithCancel(context.Background())
+ m.cancelFunc = cancel
+
+ token, err := copilot.PollForToken(ctx, m.deviceCode)
+ if err != nil {
+ if ctx.Err() != nil {
+ return nil // cancelled, don't report error.
+ }
+ return ActionOAuthErrored{Error: err}
+ }
+
+ return ActionCompleteOAuth{Token: token}
+ }
+}
+
+func (m *OAuthCopilot) stopPolling() tea.Msg {
+ if m.cancelFunc != nil {
+ m.cancelFunc()
+ }
+ return nil
+}
@@ -931,8 +931,18 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
break
}
- _, isProviderConfigured := cfg.Providers.Get(msg.Model.Provider)
- if !isProviderConfigured {
+ var (
+ providerID = msg.Model.Provider
+ isCopilot = providerID == string(catwalk.InferenceProviderCopilot)
+ isConfigured = func() bool { _, ok := cfg.Providers.Get(providerID); return ok }
+ )
+
+ // Attempt to import GitHub Copilot tokens from VSCode if available.
+ if isCopilot && !isConfigured() {
+ config.Get().ImportCopilot()
+ }
+
+ if !isConfigured() {
m.dialog.CloseDialog(dialog.ModelsID)
if cmd := m.openAuthenticationDialog(msg.Provider, msg.Model, msg.ModelType); cmd != nil {
cmds = append(cmds, cmd)
@@ -1058,7 +1068,18 @@ func (m *UI) openOAuthHyperDialog(provider catwalk.Provider, model config.Select
}
func (m *UI) openOAuthCopilotDialog(provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) tea.Cmd {
- panic("TODO")
+ if m.dialog.ContainsDialog(dialog.OAuthID) {
+ m.dialog.BringToFront(dialog.OAuthID)
+ return nil
+ }
+
+ oAuthDialog, err := dialog.NewOAuthCopilot(m.com, provider, model, modelType)
+ if err != nil {
+ return uiutil.ReportError(err)
+ }
+ m.dialog.OpenDialog(oAuthDialog)
+
+ return oAuthDialog.Init()
}
func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {