on main help sign in
feat: implement github copilot oauth flow in the new ui codebase
commitf2f44540f58e3cca3602f11d5bd7c002e3ff3109 authorAndrey Nering <andreynering@users.noreply.github.com> dateThu 15 Jan 2026, 14:23 (9 months ago) parentccb1a64
4 files changed+99−3
internal/ui/dialog/actions.go+1 internal/ui/dialog/oauth.go+2 internal/ui/dialog/oauth_copilot.go+72 internal/ui/model/ui.go+24−3
internal/ui/dialog/actions.go+1
@@ -83,6 +83,7 @@ type (
8383 UserCode string
8484 ExpiresIn int
8585 VerificationURL string
86+ Interval int
8687 }
8788
8889 // ActionCompleteOAuth is sent when the device flow completes successfully.
internal/ui/dialog/oauth.go+2
@@ -63,6 +63,7 @@ type OAuth struct {
6363 userCode string
6464 verificationURL string
6565 expiresIn int
66+ interval int
6667 token *oauth.Token
6768 cancelFunc context.CancelFunc
6869}
@@ -157,6 +158,7 @@ func (m *OAuth) HandleMsg(msg tea.Msg) Action {
157158 m.userCode = msg.UserCode
158159 m.expiresIn = msg.ExpiresIn
159160 m.verificationURL = msg.VerificationURL
161+ m.interval = msg.Interval
160162 m.State = OAuthStateDisplay
161163 return ActionCmd{m.oAuthProvider.startPolling(msg.DeviceCode, msg.ExpiresIn)}
162164
internal/ui/dialog/oauth_copilot.gonew file+72
@@ -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+}
internal/ui/model/ui.go+24−3
@@ -931,8 +931,18 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
931931 break
932932 }
933933
934− _, isProviderConfigured := cfg.Providers.Get(msg.Model.Provider)
935− if !isProviderConfigured {
934+ var (
935+ providerID = msg.Model.Provider
936+ isCopilot = providerID == string(catwalk.InferenceProviderCopilot)
937+ isConfigured = func() bool { _, ok := cfg.Providers.Get(providerID); return ok }
938+ )
939+
940+ // Attempt to import GitHub Copilot tokens from VSCode if available.
941+ if isCopilot && !isConfigured() {
942+ config.Get().ImportCopilot()
943+ }
944+
945+ if !isConfigured() {
936946 m.dialog.CloseDialog(dialog.ModelsID)
937947 if cmd := m.openAuthenticationDialog(msg.Provider, msg.Model, msg.ModelType); cmd != nil {
938948 cmds = append(cmds, cmd)
@@ -1058,7 +1068,18 @@ func (m *UI) openOAuthHyperDialog(provider catwalk.Provider, model config.Select
10581068}
10591069
10601070func (m *UI) openOAuthCopilotDialog(provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) tea.Cmd {
1061− panic("TODO")
1071+ if m.dialog.ContainsDialog(dialog.OAuthID) {
1072+ m.dialog.BringToFront(dialog.OAuthID)
1073+ return nil
1074+ }
1075+
1076+ oAuthDialog, err := dialog.NewOAuthCopilot(m.com, provider, model, modelType)
1077+ if err != nil {
1078+ return uiutil.ReportError(err)
1079+ }
1080+ m.dialog.OpenDialog(oAuthDialog)
1081+
1082+ return oAuthDialog.Init()
10621083}
10631084
10641085func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {