@@ -9,7 +9,6 @@ import (
9 "charm.land/bubbles/v2/spinner"
9 "charm.land/bubbles/v2/spinner"
10 tea "charm.land/bubbletea/v2"
10 tea "charm.land/bubbletea/v2"
11 "charm.land/lipgloss/v2"
11 "charm.land/lipgloss/v2"
12− "github.com/atotto/clipboard"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/crush/internal/agent"
13 "github.com/charmbracelet/crush/internal/agent"
15 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
14 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
@@ -18,7 +17,6 @@ import (
18 "github.com/charmbracelet/crush/internal/tui/components/chat"
17 "github.com/charmbracelet/crush/internal/tui/components/chat"
19 "github.com/charmbracelet/crush/internal/tui/components/core"
18 "github.com/charmbracelet/crush/internal/tui/components/core"
20 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
19 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
21− "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
22 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
20 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
23 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
21 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
24 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
22 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
@@ -47,18 +45,6 @@ type Splash interface {
47 // IsAPIKeyValid returns whether the API key is valid
45 // IsAPIKeyValid returns whether the API key is valid
48 IsAPIKeyValid() bool
46 IsAPIKeyValid() bool
50− // IsShowingClaudeAuthMethodChooser returns whether showing Claude auth method chooser
51− IsShowingClaudeAuthMethodChooser() bool
53− // IsShowingClaudeOAuth2 returns whether showing Claude OAuth2 flow
54− IsShowingClaudeOAuth2() bool
56− // IsClaudeOAuthURLState returns whether in OAuth URL state
57− IsClaudeOAuthURLState() bool
59− // IsClaudeOAuthComplete returns whether Claude OAuth flow is complete
60− IsClaudeOAuthComplete() bool
62 // IsShowingClaudeOAuth2 returns whether showing Hyper OAuth2 flow
48 // IsShowingClaudeOAuth2 returns whether showing Hyper OAuth2 flow
63 IsShowingHyperOAuth2() bool
49 IsShowingHyperOAuth2() bool
@@ -103,12 +89,6 @@ type splashCmp struct {
103 // Copilot device flow state
89 // Copilot device flow state
104 copilotDeviceFlow *copilot.DeviceFlow
90 copilotDeviceFlow *copilot.DeviceFlow
105 showCopilotDeviceFlow bool
91 showCopilotDeviceFlow bool
108− claudeAuthMethodChooser *claude.AuthMethodChooser
109− claudeOAuth2 *claude.OAuth2
110− showClaudeAuthMethodChooser bool
111− showClaudeOAuth2 bool
114func New() Splash {
94func New() Splash {
@@ -134,9 +114,6 @@ func New() Splash {
134 modelList: modelList,
114 modelList: modelList,
135 apiKeyInput: apiKeyInput,
115 apiKeyInput: apiKeyInput,
136 selectedNo: false,
116 selectedNo: false,
138− claudeAuthMethodChooser: claude.NewAuthMethodChooser(),
139− claudeOAuth2: claude.NewOAuth2(),
@@ -158,8 +135,6 @@ func (s *splashCmp) Init() tea.Cmd {
158 return tea.Batch(
135 return tea.Batch(
159 s.modelList.Init(),
136 s.modelList.Init(),
160 s.apiKeyInput.Init(),
137 s.apiKeyInput.Init(),
161− s.claudeAuthMethodChooser.Init(),
162− s.claudeOAuth2.Init(),
@@ -176,7 +151,6 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
176 s.listHeight = s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) - s.logoGap() - 2
151 s.listHeight = s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) - s.logoGap() - 2
177 listWidth := min(60, width)
152 listWidth := min(60, width)
178 s.apiKeyInput.SetWidth(width - 2)
153 s.apiKeyInput.SetWidth(width - 2)
179− s.claudeAuthMethodChooser.SetWidth(min(width-2, 60))
180 return s.modelList.SetSize(listWidth, s.listHeight)
154 return s.modelList.SetSize(listWidth, s.listHeight)
@@ -185,24 +159,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
185 switch msg := msg.(type) {
159 switch msg := msg.(type) {
186 case tea.WindowSizeMsg:
160 case tea.WindowSizeMsg:
187 return s, s.SetSize(msg.Width, msg.Height)
161 return s, s.SetSize(msg.Width, msg.Height)
188− case claude.ValidationCompletedMsg:
190− u, cmd := s.claudeOAuth2.Update(msg)
191− s.claudeOAuth2 = u.(*claude.OAuth2)
192− cmds = append(cmds, cmd)
194− if msg.State == claude.OAuthValidationStateValid {
197− s.saveAPIKeyAndContinue(msg.Token, false),
199− time.Sleep(5 * time.Second)
200− return claude.AuthenticationCompleteMsg{}
205− return s, tea.Batch(cmds...)
206 case hyper.DeviceFlowCompletedMsg:
162 case hyper.DeviceFlowCompletedMsg:
207 s.showHyperDeviceFlow = false
163 s.showHyperDeviceFlow = false
208 return s, s.saveAPIKeyAndContinue(msg.Token, true)
164 return s, s.saveAPIKeyAndContinue(msg.Token, true)
@@ -223,10 +179,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
223 case copilot.DeviceFlowCompletedMsg:
179 case copilot.DeviceFlowCompletedMsg:
224 s.showCopilotDeviceFlow = false
180 s.showCopilotDeviceFlow = false
225 return s, s.saveAPIKeyAndContinue(msg.Token, true)
181 return s, s.saveAPIKeyAndContinue(msg.Token, true)
226− case claude.AuthenticationCompleteMsg:
227− s.showClaudeAuthMethodChooser = false
228− s.showClaudeOAuth2 = false
229− return s, util.CmdHandler(OnboardingCompleteMsg{})
230 case models.APIKeyStateChangeMsg:
182 case models.APIKeyStateChangeMsg:
231 u, cmd := s.apiKeyInput.Update(msg)
183 u, cmd := s.apiKeyInput.Update(msg)
232 s.apiKeyInput = u.(*models.APIKeyInput)
184 s.apiKeyInput = u.(*models.APIKeyInput)
@@ -246,34 +198,8 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
246 return s, s.hyperDeviceFlow.CopyCode()
198 return s, s.hyperDeviceFlow.CopyCode()
247 case key.Matches(msg, s.keyMap.Copy) && s.showCopilotDeviceFlow:
199 case key.Matches(msg, s.keyMap.Copy) && s.showCopilotDeviceFlow:
248 return s, s.copilotDeviceFlow.CopyCode()
200 return s, s.copilotDeviceFlow.CopyCode()
249− case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL:
250− return s, tea.Sequence(
251− tea.SetClipboard(s.claudeOAuth2.URL),
253− _ = clipboard.WriteAll(s.claudeOAuth2.URL)
256− util.ReportInfo("URL copied to clipboard"),
258− case key.Matches(msg, s.keyMap.Copy) && s.showClaudeAuthMethodChooser:
259− u, cmd := s.claudeAuthMethodChooser.Update(msg)
260− s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
262− case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2:
263− u, cmd := s.claudeOAuth2.Update(msg)
264− s.claudeOAuth2 = u.(*claude.OAuth2)
266 case key.Matches(msg, s.keyMap.Back):
201 case key.Matches(msg, s.keyMap.Back):
268− case s.showClaudeAuthMethodChooser:
269− s.claudeAuthMethodChooser.SetDefaults()
270− s.showClaudeAuthMethodChooser = false
272− case s.showClaudeOAuth2:
273− s.claudeOAuth2.SetDefaults()
274− s.showClaudeOAuth2 = false
275− s.showClaudeAuthMethodChooser = true
277 case s.showHyperDeviceFlow:
203 case s.showHyperDeviceFlow:
278 s.hyperDeviceFlow = nil
204 s.hyperDeviceFlow = nil
279 s.showHyperDeviceFlow = false
205 s.showHyperDeviceFlow = false
@@ -285,9 +211,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
285 case s.isAPIKeyValid:
211 case s.isAPIKeyValid:
286 return s, nil
212 return s, nil
287 case s.needsAPIKey:
213 case s.needsAPIKey:
288− if s.selectedModel.Provider.ID == catwalk.InferenceProviderAnthropic {
289− s.showClaudeAuthMethodChooser = true
291 s.needsAPIKey = false
214 s.needsAPIKey = false
292 s.selectedModel = nil
215 s.selectedModel = nil
293 s.isAPIKeyValid = false
216 s.isAPIKeyValid = false
@@ -297,28 +220,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
298 case key.Matches(msg, s.keyMap.Select):
221 case key.Matches(msg, s.keyMap.Select):
300− case s.showClaudeAuthMethodChooser:
301− selectedItem := s.modelList.SelectedModel()
302− if selectedItem == nil {
306− switch s.claudeAuthMethodChooser.State {
307− case claude.AuthMethodAPIKey:
308− s.showClaudeAuthMethodChooser = false
309− s.needsAPIKey = true
310− s.selectedModel = selectedItem
311− s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
312− case claude.AuthMethodOAuth2:
313− s.selectedModel = selectedItem
314− s.showClaudeAuthMethodChooser = false
315− s.showClaudeOAuth2 = true
318− case s.showClaudeOAuth2:
319− m2, cmd2 := s.claudeOAuth2.ValidationConfirm()
320− s.claudeOAuth2 = m2.(*claude.OAuth2)
322 case s.showHyperDeviceFlow:
223 case s.showHyperDeviceFlow:
323 return s, s.hyperDeviceFlow.CopyCodeAndOpenURL()
224 return s, s.hyperDeviceFlow.CopyCodeAndOpenURL()
324 case s.showCopilotDeviceFlow:
225 case s.showCopilotDeviceFlow:
@@ -336,9 +237,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
336 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
237 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
338 switch selectedItem.Provider.ID {
239 switch selectedItem.Provider.ID {
339− case catwalk.InferenceProviderAnthropic:
340− s.showClaudeAuthMethodChooser = true
342 case hyperp.Name:
240 case hyperp.Name:
343 s.selectedModel = selectedItem
241 s.selectedModel = selectedItem
344 s.showHyperDeviceFlow = true
242 s.showHyperDeviceFlow = true
@@ -407,10 +305,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
407 return s, s.initializeProject()
305 return s, s.initializeProject()
409 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
307 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
410− if s.showClaudeAuthMethodChooser {
411− s.claudeAuthMethodChooser.ToggleChoice()
414 if s.needsAPIKey {
308 if s.needsAPIKey {
415 u, cmd := s.apiKeyInput.Update(msg)
309 u, cmd := s.apiKeyInput.Update(msg)
416 s.apiKeyInput = u.(*models.APIKeyInput)
310 s.apiKeyInput = u.(*models.APIKeyInput)
@@ -452,14 +346,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
455− case s.showClaudeAuthMethodChooser:
456− u, cmd := s.claudeAuthMethodChooser.Update(msg)
457− s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
459− case s.showClaudeOAuth2:
460− u, cmd := s.claudeOAuth2.Update(msg)
461− s.claudeOAuth2 = u.(*claude.OAuth2)
463 case s.showHyperDeviceFlow:
349 case s.showHyperDeviceFlow:
464 u, cmd := s.hyperDeviceFlow.Update(msg)
350 u, cmd := s.hyperDeviceFlow.Update(msg)
465 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
351 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
@@ -480,10 +366,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
481 case tea.PasteMsg:
367 case tea.PasteMsg:
483− case s.showClaudeOAuth2:
484− u, cmd := s.claudeOAuth2.Update(msg)
485− s.claudeOAuth2 = u.(*claude.OAuth2)
487 case s.showHyperDeviceFlow:
369 case s.showHyperDeviceFlow:
488 u, cmd := s.hyperDeviceFlow.Update(msg)
370 u, cmd := s.hyperDeviceFlow.Update(msg)
489 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
371 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
@@ -503,10 +385,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
504 case spinner.TickMsg:
386 case spinner.TickMsg:
506− case s.showClaudeOAuth2:
507− u, cmd := s.claudeOAuth2.Update(msg)
508− s.claudeOAuth2 = u.(*claude.OAuth2)
510 case s.showHyperDeviceFlow:
388 case s.showHyperDeviceFlow:
511 u, cmd := s.hyperDeviceFlow.Update(msg)
389 u, cmd := s.hyperDeviceFlow.Update(msg)
512 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
390 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
@@ -655,38 +533,6 @@ func (s *splashCmp) View() string {
655 var content string
533 var content string
658− case s.showClaudeAuthMethodChooser:
659− remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
660− chooserView := s.claudeAuthMethodChooser.View()
661− authMethodSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
662− lipgloss.JoinVertical(
664− t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth Anthropic"),
669− content = lipgloss.JoinVertical(
674− case s.showClaudeOAuth2:
675− remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
676− oauth2View := s.claudeOAuth2.View()
677− oauthSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
678− lipgloss.JoinVertical(
680− t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth Anthropic"),
685− content = lipgloss.JoinVertical(
690 case s.showHyperDeviceFlow:
536 case s.showHyperDeviceFlow:
691 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
537 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
692 hyperView := s.hyperDeviceFlow.View()
538 hyperView := s.hyperDeviceFlow.View()
@@ -816,14 +662,6 @@ func (s *splashCmp) View() string {
817func (s *splashCmp) Cursor() *tea.Cursor {
663func (s *splashCmp) Cursor() *tea.Cursor {
819− case s.showClaudeAuthMethodChooser:
821− case s.showClaudeOAuth2:
822− if cursor := s.claudeOAuth2.CodeInput.Cursor(); cursor != nil {
823− cursor.Y += 2 // FIXME(@andreynering): Why do we need this?
824− return s.moveCursor(cursor)
827 case s.needsAPIKey:
665 case s.needsAPIKey:
828 cursor := s.apiKeyInput.Cursor()
666 cursor := s.apiKeyInput.Cursor()
829 if cursor != nil {
667 if cursor != nil {
@@ -894,16 +732,10 @@ func (s *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
895 // Calculate the correct Y offset based on current state
733 // Calculate the correct Y offset based on current state
896 logoHeight := lipgloss.Height(s.logoRendered)
734 logoHeight := lipgloss.Height(s.logoRendered)
897− if s.needsAPIKey || s.showClaudeOAuth2 {
735+ if s.needsAPIKey {
900− view = s.apiKeyInput.View()
902− view = s.claudeOAuth2.View()
904 infoSectionHeight := lipgloss.Height(s.infoSection())
736 infoSectionHeight := lipgloss.Height(s.infoSection())
905 baseOffset := logoHeight + SplashScreenPaddingY + infoSectionHeight
737 baseOffset := logoHeight + SplashScreenPaddingY + infoSectionHeight
906− remainingHeight := s.height - baseOffset - lipgloss.Height(view) - SplashScreenPaddingY
738+ remainingHeight := s.height - baseOffset - lipgloss.Height(s.apiKeyInput.View()) - SplashScreenPaddingY
907 offset := baseOffset + remainingHeight
739 offset := baseOffset + remainingHeight
908 cursor.Y += offset
740 cursor.Y += offset
909 cursor.X += 1
741 cursor.X += 1
@@ -926,20 +758,6 @@ func (s *splashCmp) logoGap() int {
926// Bindings implements SplashPage.
758// Bindings implements SplashPage.
927func (s *splashCmp) Bindings() []key.Binding {
759func (s *splashCmp) Bindings() []key.Binding {
929− case s.showClaudeAuthMethodChooser:
930− return []key.Binding{
935− case s.showClaudeOAuth2:
936− bindings := []key.Binding{
939− if s.claudeOAuth2.State == claude.OAuthStateURL {
940− bindings = append(bindings, s.keyMap.Copy)
943 case s.needsAPIKey:
761 case s.needsAPIKey:
944 return []key.Binding{
762 return []key.Binding{
945 s.keyMap.Select,
763 s.keyMap.Select,
@@ -1047,22 +865,6 @@ func (s *splashCmp) IsAPIKeyValid() bool {
1047 return s.isAPIKeyValid
865 return s.isAPIKeyValid
1050−func (s *splashCmp) IsShowingClaudeAuthMethodChooser() bool {
1051− return s.showClaudeAuthMethodChooser
1054−func (s *splashCmp) IsShowingClaudeOAuth2() bool {
1055− return s.showClaudeOAuth2
1058−func (s *splashCmp) IsClaudeOAuthURLState() bool {
1059− return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL
1062−func (s *splashCmp) IsClaudeOAuthComplete() bool {
1063− return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateCode && s.claudeOAuth2.ValidationState == claude.OAuthValidationStateValid
1066func (s *splashCmp) IsShowingHyperOAuth2() bool {
868func (s *splashCmp) IsShowingHyperOAuth2() bool {
1067 return s.showHyperDeviceFlow
869 return s.showHyperDeviceFlow