@@ -17,6 +17,7 @@ import (
1717 "github.com/charmbracelet/crush/internal/tui/components/core"
1818 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
1919 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
20+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
2021 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
2122 "github.com/charmbracelet/crush/internal/tui/exp/list"
2223 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -77,6 +78,10 @@ type modelDialogCmp struct {
7778 hyperDeviceFlow *hyper.DeviceFlow
7879 showHyperDeviceFlow bool
7980
81+ // Copilot device flow state
82+ copilotDeviceFlow *copilot.DeviceFlow
83+ showCopilotDeviceFlow bool
84+
8085 // Claude state
8186 claudeAuthMethodChooser *claude.AuthMethodChooser
8287 claudeOAuth2 *claude.OAuth2
@@ -143,6 +148,15 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
143148 return m, cmd
144149 }
145150 return m, nil
151+ case copilot.DeviceAuthInitiatedMsg, copilot.DeviceFlowErrorMsg:
152+ if m.copilotDeviceFlow != nil {
153+ u, cmd := m.copilotDeviceFlow.Update(msg)
154+ m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
155+ return m, cmd
156+ }
157+ return m, nil
158+ case copilot.DeviceFlowCompletedMsg:
159+ return m, m.saveOauthTokenAndContinue(msg.Token, true)
146160 case claude.ValidationCompletedMsg:
147161 var cmds []tea.Cmd
148162 u, cmd := m.claudeOAuth2.Update(msg)
@@ -160,10 +174,13 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
160174 case tea.KeyPressMsg:
161175 switch {
162176 // Handle Hyper device flow keys
163− case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showHyperDeviceFlow:
177+ case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && (m.showHyperDeviceFlow || m.showCopilotDeviceFlow):
164178 if m.hyperDeviceFlow != nil {
165179 return m, m.hyperDeviceFlow.CopyCode()
166180 }
181+ if m.copilotDeviceFlow != nil {
182+ return m, m.copilotDeviceFlow.CopyCode()
183+ }
167184 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL:
168185 return m, tea.Sequence(
169186 tea.SetClipboard(m.claudeOAuth2.URL),
@@ -181,6 +198,9 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
181198 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
182199 return m, m.hyperDeviceFlow.CopyCodeAndOpenURL()
183200 }
201+ if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
202+ return m, m.copilotDeviceFlow.CopyCodeAndOpenURL()
203+ }
184204 selectedItem := m.modelList.SelectedModel()
185205
186206 modelType := config.SelectedModelTypeLarge
@@ -193,6 +213,7 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
193213 m.keyMap.isClaudeOAuthHelp = false
194214 m.keyMap.isAPIKeyHelp = true
195215 m.showHyperDeviceFlow = false
216+ m.showCopilotDeviceFlow = false
196217 m.showClaudeAuthMethodChooser = false
197218 m.needsAPIKey = true
198219 m.selectedModel = selectedItem
@@ -288,6 +309,13 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
288309 m.hyperDeviceFlow = hyper.NewDeviceFlow()
289310 m.hyperDeviceFlow.SetWidth(m.width - 2)
290311 return m, m.hyperDeviceFlow.Init()
312+ case catwalk.InferenceProviderCopilot:
313+ m.showCopilotDeviceFlow = true
314+ m.selectedModel = selectedItem
315+ m.selectedModelType = modelType
316+ m.copilotDeviceFlow = copilot.NewDeviceFlow()
317+ m.copilotDeviceFlow.SetWidth(m.width - 2)
318+ return m, m.copilotDeviceFlow.Init()
291319 }
292320 // For other providers, show API key input
293321 askForApiKey()
@@ -310,13 +338,19 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
310338 }
311339 case key.Matches(msg, m.keyMap.Close):
312340 if m.showHyperDeviceFlow {
313− // Cancel device flow and go back to model selection
314341 if m.hyperDeviceFlow != nil {
315342 m.hyperDeviceFlow.Cancel()
316343 }
317344 m.showHyperDeviceFlow = false
318345 m.selectedModel = nil
319346 }
347+ if m.showCopilotDeviceFlow {
348+ if m.copilotDeviceFlow != nil {
349+ m.copilotDeviceFlow.Cancel()
350+ }
351+ m.showCopilotDeviceFlow = false
352+ m.selectedModel = nil
353+ }
320354 if m.showClaudeAuthMethodChooser {
321355 m.claudeAuthMethodChooser.SetDefaults()
322356 m.showClaudeAuthMethodChooser = false
@@ -377,18 +411,27 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
377411 u, cmd = m.hyperDeviceFlow.Update(msg)
378412 m.hyperDeviceFlow = u.(*hyper.DeviceFlow)
379413 }
414+ if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
415+ u, cmd = m.copilotDeviceFlow.Update(msg)
416+ m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
417+ }
380418 return m, cmd
381419 default:
382420 // Pass all other messages to the device flow for spinner animation
383− if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
421+ switch {
422+ case m.showHyperDeviceFlow && m.hyperDeviceFlow != nil:
384423 u, cmd := m.hyperDeviceFlow.Update(msg)
385424 m.hyperDeviceFlow = u.(*hyper.DeviceFlow)
386425 return m, cmd
387− } else if m.showClaudeOAuth2 {
426+ case m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil:
427+ u, cmd := m.copilotDeviceFlow.Update(msg)
428+ m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
429+ return m, cmd
430+ case m.showClaudeOAuth2:
388431 u, cmd := m.claudeOAuth2.Update(msg)
389432 m.claudeOAuth2 = u.(*claude.OAuth2)
390433 return m, cmd
391− } else {
434+ default:
392435 u, cmd := m.apiKeyInput.Update(msg)
393436 m.apiKeyInput = u.(*APIKeyInput)
394437 return m, cmd
@@ -413,9 +456,25 @@ func (m *modelDialogCmp) View() string {
413456 )
414457 return m.style().Render(content)
415458 }
459+ if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
460+ // Show Hyper device flow
461+ m.keyMap.isCopilotDeviceFlow = m.copilotDeviceFlow.State != copilot.DeviceFlowStateUnavailable
462+ m.keyMap.isCopilotUnavailable = m.copilotDeviceFlow.State == copilot.DeviceFlowStateUnavailable
463+ deviceFlowView := m.copilotDeviceFlow.View()
464+ content := lipgloss.JoinVertical(
465+ lipgloss.Left,
466+ t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Authenticate with GitHub Copilot", m.width-4)),
467+ deviceFlowView,
468+ "",
469+ t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
470+ )
471+ return m.style().Render(content)
472+ }
416473
417474 // Reset the flags when not showing device flow
418475 m.keyMap.isHyperDeviceFlow = false
476+ m.keyMap.isCopilotDeviceFlow = false
477+ m.keyMap.isCopilotUnavailable = false
419478
420479 switch {
421480 case m.showClaudeAuthMethodChooser:
@@ -472,6 +531,9 @@ func (m *modelDialogCmp) Cursor() *tea.Cursor {
472531 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
473532 return m.hyperDeviceFlow.Cursor()
474533 }
534+ if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
535+ return m.copilotDeviceFlow.Cursor()
536+ }
475537 if m.showClaudeAuthMethodChooser {
476538 return nil
477539 }