@@ -17,6 +17,7 @@ import (
17 "github.com/charmbracelet/crush/internal/tui/components/core"
17 "github.com/charmbracelet/crush/internal/tui/components/core"
18 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
18 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
19 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
19 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
20+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
20 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
21 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
21 "github.com/charmbracelet/crush/internal/tui/exp/list"
22 "github.com/charmbracelet/crush/internal/tui/exp/list"
22 "github.com/charmbracelet/crush/internal/tui/styles"
23 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -77,6 +78,10 @@ type modelDialogCmp struct {
77 hyperDeviceFlow *hyper.DeviceFlow
78 hyperDeviceFlow *hyper.DeviceFlow
78 showHyperDeviceFlow bool
79 showHyperDeviceFlow bool
81+ // Copilot device flow state
82+ copilotDeviceFlow *copilot.DeviceFlow
83+ showCopilotDeviceFlow bool
80 // Claude state
85 // Claude state
81 claudeAuthMethodChooser *claude.AuthMethodChooser
86 claudeAuthMethodChooser *claude.AuthMethodChooser
82 claudeOAuth2 *claude.OAuth2
87 claudeOAuth2 *claude.OAuth2
@@ -143,6 +148,15 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
143 return m, cmd
148 return m, cmd
145 return m, nil
150 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)
158+ case copilot.DeviceFlowCompletedMsg:
159+ return m, m.saveOauthTokenAndContinue(msg.Token, true)
146 case claude.ValidationCompletedMsg:
160 case claude.ValidationCompletedMsg:
147 var cmds []tea.Cmd
161 var cmds []tea.Cmd
148 u, cmd := m.claudeOAuth2.Update(msg)
162 u, cmd := m.claudeOAuth2.Update(msg)
@@ -160,10 +174,13 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
160 case tea.KeyPressMsg:
174 case tea.KeyPressMsg:
162 // Handle Hyper device flow keys
176 // 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):
164 if m.hyperDeviceFlow != nil {
178 if m.hyperDeviceFlow != nil {
165 return m, m.hyperDeviceFlow.CopyCode()
179 return m, m.hyperDeviceFlow.CopyCode()
181+ if m.copilotDeviceFlow != nil {
182+ return m, m.copilotDeviceFlow.CopyCode()
167 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL:
184 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL:
168 return m, tea.Sequence(
185 return m, tea.Sequence(
169 tea.SetClipboard(m.claudeOAuth2.URL),
186 tea.SetClipboard(m.claudeOAuth2.URL),
@@ -181,6 +198,9 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
181 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
198 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
182 return m, m.hyperDeviceFlow.CopyCodeAndOpenURL()
199 return m, m.hyperDeviceFlow.CopyCodeAndOpenURL()
201+ if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
202+ return m, m.copilotDeviceFlow.CopyCodeAndOpenURL()
184 selectedItem := m.modelList.SelectedModel()
204 selectedItem := m.modelList.SelectedModel()
186 modelType := config.SelectedModelTypeLarge
206 modelType := config.SelectedModelTypeLarge
@@ -193,6 +213,7 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
193 m.keyMap.isClaudeOAuthHelp = false
213 m.keyMap.isClaudeOAuthHelp = false
194 m.keyMap.isAPIKeyHelp = true
214 m.keyMap.isAPIKeyHelp = true
195 m.showHyperDeviceFlow = false
215 m.showHyperDeviceFlow = false
216+ m.showCopilotDeviceFlow = false
196 m.showClaudeAuthMethodChooser = false
217 m.showClaudeAuthMethodChooser = false
197 m.needsAPIKey = true
218 m.needsAPIKey = true
198 m.selectedModel = selectedItem
219 m.selectedModel = selectedItem
@@ -288,6 +309,13 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
288 m.hyperDeviceFlow = hyper.NewDeviceFlow()
309 m.hyperDeviceFlow = hyper.NewDeviceFlow()
289 m.hyperDeviceFlow.SetWidth(m.width - 2)
310 m.hyperDeviceFlow.SetWidth(m.width - 2)
290 return m, m.hyperDeviceFlow.Init()
311 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()
292 // For other providers, show API key input
320 // For other providers, show API key input
293 askForApiKey()
321 askForApiKey()
@@ -310,13 +338,19 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
311 case key.Matches(msg, m.keyMap.Close):
339 case key.Matches(msg, m.keyMap.Close):
312 if m.showHyperDeviceFlow {
340 if m.showHyperDeviceFlow {
313− // Cancel device flow and go back to model selection
314 if m.hyperDeviceFlow != nil {
341 if m.hyperDeviceFlow != nil {
315 m.hyperDeviceFlow.Cancel()
342 m.hyperDeviceFlow.Cancel()
317 m.showHyperDeviceFlow = false
344 m.showHyperDeviceFlow = false
318 m.selectedModel = nil
345 m.selectedModel = nil
347+ if m.showCopilotDeviceFlow {
348+ if m.copilotDeviceFlow != nil {
349+ m.copilotDeviceFlow.Cancel()
351+ m.showCopilotDeviceFlow = false
352+ m.selectedModel = nil
320 if m.showClaudeAuthMethodChooser {
354 if m.showClaudeAuthMethodChooser {
321 m.claudeAuthMethodChooser.SetDefaults()
355 m.claudeAuthMethodChooser.SetDefaults()
322 m.showClaudeAuthMethodChooser = false
356 m.showClaudeAuthMethodChooser = false
@@ -377,18 +411,27 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
377 u, cmd = m.hyperDeviceFlow.Update(msg)
411 u, cmd = m.hyperDeviceFlow.Update(msg)
378 m.hyperDeviceFlow = u.(*hyper.DeviceFlow)
412 m.hyperDeviceFlow = u.(*hyper.DeviceFlow)
414+ if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
415+ u, cmd = m.copilotDeviceFlow.Update(msg)
416+ m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
380 return m, cmd
418 return m, cmd
382 // Pass all other messages to the device flow for spinner animation
420 // Pass all other messages to the device flow for spinner animation
383− if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
422+ case m.showHyperDeviceFlow && m.hyperDeviceFlow != nil:
384 u, cmd := m.hyperDeviceFlow.Update(msg)
423 u, cmd := m.hyperDeviceFlow.Update(msg)
385 m.hyperDeviceFlow = u.(*hyper.DeviceFlow)
424 m.hyperDeviceFlow = u.(*hyper.DeviceFlow)
386 return m, cmd
425 return m, cmd
426+ case m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil:
427+ u, cmd := m.copilotDeviceFlow.Update(msg)
428+ m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
387− } else if m.showClaudeOAuth2 {
430+ case m.showClaudeOAuth2:
388 u, cmd := m.claudeOAuth2.Update(msg)
431 u, cmd := m.claudeOAuth2.Update(msg)
389 m.claudeOAuth2 = u.(*claude.OAuth2)
432 m.claudeOAuth2 = u.(*claude.OAuth2)
390 return m, cmd
433 return m, cmd
391− } else {
434+ default:
392 u, cmd := m.apiKeyInput.Update(msg)
435 u, cmd := m.apiKeyInput.Update(msg)
393 m.apiKeyInput = u.(*APIKeyInput)
436 m.apiKeyInput = u.(*APIKeyInput)
394 return m, cmd
437 return m, cmd
@@ -413,9 +456,25 @@ func (m *modelDialogCmp) View() string {
414 return m.style().Render(content)
457 return m.style().Render(content)
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(
466+ t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Authenticate with GitHub Copilot", m.width-4)),
469+ t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
471+ return m.style().Render(content)
417 // Reset the flags when not showing device flow
474 // Reset the flags when not showing device flow
418 m.keyMap.isHyperDeviceFlow = false
475 m.keyMap.isHyperDeviceFlow = false
476+ m.keyMap.isCopilotDeviceFlow = false
477+ m.keyMap.isCopilotUnavailable = false
421 case m.showClaudeAuthMethodChooser:
480 case m.showClaudeAuthMethodChooser:
@@ -472,6 +531,9 @@ func (m *modelDialogCmp) Cursor() *tea.Cursor {
472 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
531 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
473 return m.hyperDeviceFlow.Cursor()
532 return m.hyperDeviceFlow.Cursor()
534+ if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
535+ return m.copilotDeviceFlow.Cursor()
475 if m.showClaudeAuthMethodChooser {
537 if m.showClaudeAuthMethodChooser {
476 return nil
538 return nil