on main help sign in
feat: remove claude code support (#1783)
commit5590161f86806f00bc8e4702b0c67d2abcaa9c62 authorAndrey Nering <andreynering@users.noreply.github.com> dateWed 7 Jan 2026, 13:18 (9 months ago) parent43b0e0b
13 files changed+34−1078
internal/agent/agent.go−18 internal/agent/coordinator.go+4−4 internal/cmd/login.go+1−64 internal/config/config.go+19−22 internal/config/load.go+6−5 internal/oauth/claude/challenge.go−28 internal/oauth/claude/oauth.go−126 internal/tui/components/chat/splash/splash.go+2−200 internal/tui/components/dialogs/claude/method.go−115 internal/tui/components/dialogs/claude/oauth.go−267 internal/tui/components/dialogs/models/keys.go−57 internal/tui/components/dialogs/models/models.go−122 internal/tui/page/chat/chat.go+2−50
internal/agent/agent.go−18
@@ -833,10 +833,6 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
833833 modelConfig.CostPer1MIn/1e6*float64(resp.TotalUsage.InputTokens) +
834834 modelConfig.CostPer1MOut/1e6*float64(resp.TotalUsage.OutputTokens)
835835
836− if a.isClaudeCode() {
837− cost = 0
838− }
839−
840836 // Use override cost if available (e.g., from OpenRouter).
841837 if openrouterCost != nil {
842838 cost = *openrouterCost
@@ -874,10 +870,6 @@ func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session,
874870 modelConfig.CostPer1MIn/1e6*float64(usage.InputTokens) +
875871 modelConfig.CostPer1MOut/1e6*float64(usage.OutputTokens)
876872
877− if a.isClaudeCode() {
878− cost = 0
879− }
880−
881873 a.eventTokensUsed(session.ID, model, usage, cost)
882874
883875 if overrideCost != nil {
@@ -985,19 +977,9 @@ func (a *sessionAgent) Model() Model {
985977}
986978
987979func (a *sessionAgent) promptPrefix() string {
988− if a.isClaudeCode() {
989− return "You are Claude Code, Anthropic's official CLI for Claude."
990− }
991980 return a.systemPromptPrefix
992981}
993982
994−// XXX: this should be generalized to cover other subscription plans, like Copilot.
995−func (a *sessionAgent) isClaudeCode() bool {
996− cfg := config.Get()
997− pc, ok := cfg.Providers.Get(a.largeModel.ModelCfg.Provider)
998− return ok && pc.ID == string(catwalk.InferenceProviderAnthropic) && pc.OAuthToken != nil
999−}
1000−
1001983// convertToToolResult converts a fantasy tool result to a message tool result.
1002984func (a *sessionAgent) convertToToolResult(result fantasy.ToolResultContent) message.ToolResult {
1003985 baseResult := message.ToolResult{
internal/agent/coordinator.go+4−4
@@ -518,13 +518,13 @@ func (c *coordinator) buildAgentModels(ctx context.Context, isSubAgent bool) (Mo
518518 }, nil
519519}
520520
521−func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string, isOauth bool) (fantasy.Provider, error) {
521+func (c *coordinator) buildAnthropicProvider(baseURL, apiKey string, headers map[string]string) (fantasy.Provider, error) {
522522 var opts []anthropic.Option
523523
524− if isOauth {
524+ if strings.HasPrefix(apiKey, "Bearer ") {
525525 // NOTE: Prevent the SDK from picking up the API key from env.
526526 os.Setenv("ANTHROPIC_API_KEY", "")
527− headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
527+ headers["Authorization"] = apiKey
528528 } else if apiKey != "" {
529529 // X-Api-Key header
530530 opts = append(opts, anthropic.WithAPIKey(apiKey))
@@ -731,7 +731,7 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
731731 case openai.Name:
732732 return c.buildOpenaiProvider(baseURL, apiKey, headers)
733733 case anthropic.Name:
734− return c.buildAnthropicProvider(baseURL, apiKey, headers, providerCfg.OAuthToken != nil)
734+ return c.buildAnthropicProvider(baseURL, apiKey, headers)
735735 case openrouter.Name:
736736 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
737737 case azure.Name:
internal/cmd/login.go+1−64
@@ -6,14 +6,12 @@ import (
66 "fmt"
77 "os"
88 "os/signal"
9− "strings"
109
1110 "charm.land/lipgloss/v2"
1211 "github.com/atotto/clipboard"
1312 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
1413 "github.com/charmbracelet/crush/internal/config"
1514 "github.com/charmbracelet/crush/internal/oauth"
16− "github.com/charmbracelet/crush/internal/oauth/claude"
1715 "github.com/charmbracelet/crush/internal/oauth/copilot"
1816 "github.com/charmbracelet/crush/internal/oauth/hyper"
1917 "github.com/pkg/browser"
@@ -26,21 +24,16 @@ var loginCmd = &cobra.Command{
2624 Short: "Login Crush to a platform",
2725 Long: `Login Crush to a specified platform.
2826The platform should be provided as an argument.
29−Available platforms are: hyper, claude, copilot.`,
27+Available platforms are: hyper, copilot.`,
3028 Example: `
3129# Authenticate with Charm Hyper
3230crush login
3331
34−# Authenticate with Claude Code Max
35−crush login claude
36−
3732# Authenticate with GitHub Copilot
3833crush login copilot
3934 `,
4035 ValidArgs: []cobra.Completion{
4136 "hyper",
42− "claude",
43− "anthropic",
4437 "copilot",
4538 "github",
4639 "github-copilot",
@@ -60,8 +53,6 @@ crush login copilot
6053 switch provider {
6154 case "hyper":
6255 return loginHyper()
63− case "anthropic", "claude":
64− return loginClaude()
6556 case "copilot", "github", "github-copilot":
6657 return loginCopilot()
6758 default:
@@ -133,60 +124,6 @@ func loginHyper() error {
133124 return nil
134125}
135126
136−func loginClaude() error {
137− ctx := getLoginContext()
138−
139− cfg := config.Get()
140− if cfg.HasConfigField("providers.anthropic.oauth") {
141− fmt.Println("You are already logged in to Claude.")
142− return nil
143− }
144−
145− verifier, challenge, err := claude.GetChallenge()
146− if err != nil {
147− return err
148− }
149− url, err := claude.AuthorizeURL(verifier, challenge)
150− if err != nil {
151− return err
152− }
153− fmt.Println("Open the following URL and follow the instructions to authenticate with Claude Code Max:")
154− fmt.Println()
155− fmt.Println(lipgloss.NewStyle().Hyperlink(url, "id=claude").Render(url))
156− fmt.Println()
157− fmt.Println("Press enter to continue...")
158− if _, err := fmt.Scanln(); err != nil {
159− return err
160− }
161−
162− fmt.Println("Now paste and code from Anthropic and press enter...")
163− fmt.Println()
164− fmt.Print("> ")
165− var code string
166− for code == "" {
167− _, _ = fmt.Scanln(&code)
168− code = strings.TrimSpace(code)
169− }
170−
171− fmt.Println()
172− fmt.Println("Exchanging authorization code...")
173− token, err := claude.ExchangeToken(ctx, code, verifier)
174− if err != nil {
175− return err
176− }
177−
178− if err := cmp.Or(
179− cfg.SetConfigField("providers.anthropic.api_key", token.AccessToken),
180− cfg.SetConfigField("providers.anthropic.oauth", token),
181− ); err != nil {
182− return err
183− }
184−
185− fmt.Println()
186− fmt.Println("You're now authenticated with Claude Code Max!")
187− return nil
188−}
189−
190127func loginCopilot() error {
191128 ctx := getLoginContext()
192129
internal/config/config.go+19−22
@@ -19,7 +19,6 @@ import (
1919 "github.com/charmbracelet/crush/internal/csync"
2020 "github.com/charmbracelet/crush/internal/env"
2121 "github.com/charmbracelet/crush/internal/oauth"
22− "github.com/charmbracelet/crush/internal/oauth/claude"
2322 "github.com/charmbracelet/crush/internal/oauth/copilot"
2423 "github.com/charmbracelet/crush/internal/oauth/hyper"
2524 "github.com/invopop/jsonschema"
@@ -155,21 +154,6 @@ func (pc *ProviderConfig) ToProvider() catwalk.Provider {
155154 return provider
156155}
157156
158−func (pc *ProviderConfig) SetupClaudeCode() {
159− pc.SystemPromptPrefix = "You are Claude Code, Anthropic's official CLI for Claude."
160− pc.ExtraHeaders["anthropic-version"] = "2023-06-01"
161−
162− value := pc.ExtraHeaders["anthropic-beta"]
163− const want = "oauth-2025-04-20"
164− if !strings.Contains(value, want) {
165− if value != "" {
166− value += ","
167− }
168− value += want
169− }
170− pc.ExtraHeaders["anthropic-beta"] = value
171−}
172−
173157func (pc *ProviderConfig) SetupGitHubCopilot() {
174158 maps.Copy(pc.ExtraHeaders, copilot.Headers())
175159}
@@ -522,6 +506,25 @@ func (c *Config) SetConfigField(key string, value any) error {
522506 return nil
523507}
524508
509+func (c *Config) RemoveConfigField(key string) error {
510+ data, err := os.ReadFile(c.dataConfigDir)
511+ if err != nil {
512+ return fmt.Errorf("failed to read config file: %w", err)
513+ }
514+
515+ newValue, err := sjson.Delete(string(data), key)
516+ if err != nil {
517+ return fmt.Errorf("failed to delete config field %s: %w", key, err)
518+ }
519+ if err := os.MkdirAll(filepath.Dir(c.dataConfigDir), 0o755); err != nil {
520+ return fmt.Errorf("failed to create config directory %q: %w", c.dataConfigDir, err)
521+ }
522+ if err := os.WriteFile(c.dataConfigDir, []byte(newValue), 0o600); err != nil {
523+ return fmt.Errorf("failed to write config file: %w", err)
524+ }
525+ return nil
526+}
527+
525528// RefreshOAuthToken refreshes the OAuth token for the given provider.
526529func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error {
527530 providerConfig, exists := c.Providers.Get(providerID)
@@ -536,8 +539,6 @@ func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error
536539 var newToken *oauth.Token
537540 var refreshErr error
538541 switch providerID {
539− case string(catwalk.InferenceProviderAnthropic):
540− newToken, refreshErr = claude.RefreshToken(ctx, providerConfig.OAuthToken.RefreshToken)
541542 case string(catwalk.InferenceProviderCopilot):
542543 newToken, refreshErr = copilot.RefreshToken(ctx, providerConfig.OAuthToken.RefreshToken)
543544 case hyperp.Name:
@@ -554,8 +555,6 @@ func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error
554555 providerConfig.APIKey = newToken.AccessToken
555556
556557 switch providerID {
557− case string(catwalk.InferenceProviderAnthropic):
558− providerConfig.SetupClaudeCode()
559558 case string(catwalk.InferenceProviderCopilot):
560559 providerConfig.SetupGitHubCopilot()
561560 }
@@ -594,8 +593,6 @@ func (c *Config) SetProviderAPIKey(providerID string, apiKey any) error {
594593 providerConfig.APIKey = v.AccessToken
595594 providerConfig.OAuthToken = v
596595 switch providerID {
597− case string(catwalk.InferenceProviderAnthropic):
598− providerConfig.SetupClaudeCode()
599596 case string(catwalk.InferenceProviderCopilot):
600597 providerConfig.SetupGitHubCopilot()
601598 }
internal/config/load.go+6−5
@@ -202,11 +202,12 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
202202
203203 switch {
204204 case p.ID == catwalk.InferenceProviderAnthropic && config.OAuthToken != nil:
205− prepared.SetupClaudeCode()
206− case p.ID == catwalk.InferenceProviderCopilot:
207− if config.OAuthToken != nil {
208− prepared.SetupGitHubCopilot()
209− }
205+ // Claude Code subscription is not supported anymore. Remove to show onboarding.
206+ c.RemoveConfigField("providers.anthropic")
207+ c.Providers.Del(string(p.ID))
208+ continue
209+ case p.ID == catwalk.InferenceProviderCopilot && config.OAuthToken != nil:
210+ prepared.SetupGitHubCopilot()
210211 }
211212
212213 switch p.ID {
internal/oauth/claude/challenge.godeleted−28
@@ -1,28 +0,0 @@
1−package claude
2−
3−import (
4− "crypto/rand"
5− "crypto/sha256"
6− "encoding/base64"
7− "strings"
8−)
9−
10−// GetChallenge generates a PKCE verifier and its corresponding challenge.
11−func GetChallenge() (verifier string, challenge string, err error) {
12− bytes := make([]byte, 32)
13− if _, err := rand.Read(bytes); err != nil {
14− return "", "", err
15− }
16− verifier = encodeBase64(bytes)
17− hash := sha256.Sum256([]byte(verifier))
18− challenge = encodeBase64(hash[:])
19− return verifier, challenge, nil
20−}
21−
22−func encodeBase64(input []byte) (encoded string) {
23− encoded = base64.StdEncoding.EncodeToString(input)
24− encoded = strings.ReplaceAll(encoded, "=", "")
25− encoded = strings.ReplaceAll(encoded, "+", "-")
26− encoded = strings.ReplaceAll(encoded, "/", "_")
27− return encoded
28−}
internal/oauth/claude/oauth.godeleted−126
@@ -1,126 +0,0 @@
1−package claude
2−
3−import (
4− "bytes"
5− "context"
6− "encoding/json"
7− "fmt"
8− "io"
9− "net/http"
10− "net/url"
11− "strings"
12− "time"
13−
14− "github.com/charmbracelet/crush/internal/oauth"
15−)
16−
17−const clientId = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
18−
19−// AuthorizeURL returns the Claude Code Max OAuth2 authorization URL.
20−func AuthorizeURL(verifier, challenge string) (string, error) {
21− u, err := url.Parse("https://claude.ai/oauth/authorize")
22− if err != nil {
23− return "", err
24− }
25− q := u.Query()
26− q.Set("response_type", "code")
27− q.Set("client_id", clientId)
28− q.Set("redirect_uri", "https://console.anthropic.com/oauth/code/callback")
29− q.Set("scope", "org:create_api_key user:profile user:inference")
30− q.Set("code_challenge", challenge)
31− q.Set("code_challenge_method", "S256")
32− q.Set("state", verifier)
33− u.RawQuery = q.Encode()
34− return u.String(), nil
35−}
36−
37−// ExchangeToken exchanges the authorization code for an OAuth2 token.
38−func ExchangeToken(ctx context.Context, code, verifier string) (*oauth.Token, error) {
39− code = strings.TrimSpace(code)
40− parts := strings.SplitN(code, "#", 2)
41− pure := parts[0]
42− state := ""
43− if len(parts) > 1 {
44− state = parts[1]
45− }
46−
47− reqBody := map[string]string{
48− "code": pure,
49− "state": state,
50− "grant_type": "authorization_code",
51− "client_id": clientId,
52− "redirect_uri": "https://console.anthropic.com/oauth/code/callback",
53− "code_verifier": verifier,
54− }
55−
56− resp, err := request(ctx, "POST", "https://console.anthropic.com/v1/oauth/token", reqBody)
57− if err != nil {
58− return nil, err
59− }
60− defer resp.Body.Close()
61−
62− body, err := io.ReadAll(resp.Body)
63− if err != nil {
64− return nil, err
65− }
66−
67− if resp.StatusCode != http.StatusOK {
68− return nil, fmt.Errorf("claude code max: failed to exchange token: status %d body %q", resp.StatusCode, string(body))
69− }
70−
71− var token oauth.Token
72− if err := json.Unmarshal(body, &token); err != nil {
73− return nil, err
74− }
75− token.SetExpiresAt()
76− return &token, nil
77−}
78−
79−// RefreshToken refreshes the OAuth2 token using the provided refresh token.
80−func RefreshToken(ctx context.Context, refreshToken string) (*oauth.Token, error) {
81− reqBody := map[string]string{
82− "grant_type": "refresh_token",
83− "refresh_token": refreshToken,
84− "client_id": clientId,
85− }
86−
87− resp, err := request(ctx, "POST", "https://console.anthropic.com/v1/oauth/token", reqBody)
88− if err != nil {
89− return nil, err
90− }
91− defer resp.Body.Close()
92−
93− body, err := io.ReadAll(resp.Body)
94− if err != nil {
95− return nil, err
96− }
97−
98− if resp.StatusCode != http.StatusOK {
99− return nil, fmt.Errorf("claude code max: failed to refresh token: status %d body %q", resp.StatusCode, string(body))
100− }
101−
102− var token oauth.Token
103− if err := json.Unmarshal(body, &token); err != nil {
104− return nil, err
105− }
106− token.SetExpiresAt()
107− return &token, nil
108−}
109−
110−func request(ctx context.Context, method, url string, body any) (*http.Response, error) {
111− date, err := json.Marshal(body)
112− if err != nil {
113− return nil, err
114− }
115−
116− req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(date))
117− if err != nil {
118− return nil, err
119− }
120−
121− req.Header.Set("Content-Type", "application/json")
122− req.Header.Set("User-Agent", "anthropic")
123−
124− client := &http.Client{Timeout: 30 * time.Second}
125− return client.Do(req)
126−}
internal/tui/components/chat/splash/splash.go+2−200
@@ -9,7 +9,6 @@ import (
99 "charm.land/bubbles/v2/spinner"
1010 tea "charm.land/bubbletea/v2"
1111 "charm.land/lipgloss/v2"
12− "github.com/atotto/clipboard"
1312 "github.com/charmbracelet/catwalk/pkg/catwalk"
1413 "github.com/charmbracelet/crush/internal/agent"
1514 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
@@ -18,7 +17,6 @@ import (
1817 "github.com/charmbracelet/crush/internal/tui/components/chat"
1918 "github.com/charmbracelet/crush/internal/tui/components/core"
2019 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
21− "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
2220 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
2321 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
2422 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
@@ -47,18 +45,6 @@ type Splash interface {
4745 // IsAPIKeyValid returns whether the API key is valid
4846 IsAPIKeyValid() bool
4947
50− // IsShowingClaudeAuthMethodChooser returns whether showing Claude auth method chooser
51− IsShowingClaudeAuthMethodChooser() bool
52−
53− // IsShowingClaudeOAuth2 returns whether showing Claude OAuth2 flow
54− IsShowingClaudeOAuth2() bool
55−
56− // IsClaudeOAuthURLState returns whether in OAuth URL state
57− IsClaudeOAuthURLState() bool
58−
59− // IsClaudeOAuthComplete returns whether Claude OAuth flow is complete
60− IsClaudeOAuthComplete() bool
61−
6248 // IsShowingClaudeOAuth2 returns whether showing Hyper OAuth2 flow
6349 IsShowingHyperOAuth2() bool
6450
@@ -103,12 +89,6 @@ type splashCmp struct {
10389 // Copilot device flow state
10490 copilotDeviceFlow *copilot.DeviceFlow
10591 showCopilotDeviceFlow bool
106−
107− // Claude state
108− claudeAuthMethodChooser *claude.AuthMethodChooser
109− claudeOAuth2 *claude.OAuth2
110− showClaudeAuthMethodChooser bool
111− showClaudeOAuth2 bool
11292}
11393
11494func New() Splash {
@@ -134,9 +114,6 @@ func New() Splash {
134114 modelList: modelList,
135115 apiKeyInput: apiKeyInput,
136116 selectedNo: false,
137−
138− claudeAuthMethodChooser: claude.NewAuthMethodChooser(),
139− claudeOAuth2: claude.NewOAuth2(),
140117 }
141118}
142119
@@ -158,8 +135,6 @@ func (s *splashCmp) Init() tea.Cmd {
158135 return tea.Batch(
159136 s.modelList.Init(),
160137 s.apiKeyInput.Init(),
161− s.claudeAuthMethodChooser.Init(),
162− s.claudeOAuth2.Init(),
163138 )
164139}
165140
@@ -176,7 +151,6 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
176151 s.listHeight = s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) - s.logoGap() - 2
177152 listWidth := min(60, width)
178153 s.apiKeyInput.SetWidth(width - 2)
179− s.claudeAuthMethodChooser.SetWidth(min(width-2, 60))
180154 return s.modelList.SetSize(listWidth, s.listHeight)
181155}
182156
@@ -185,24 +159,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
185159 switch msg := msg.(type) {
186160 case tea.WindowSizeMsg:
187161 return s, s.SetSize(msg.Width, msg.Height)
188− case claude.ValidationCompletedMsg:
189− var cmds []tea.Cmd
190− u, cmd := s.claudeOAuth2.Update(msg)
191− s.claudeOAuth2 = u.(*claude.OAuth2)
192− cmds = append(cmds, cmd)
193−
194− if msg.State == claude.OAuthValidationStateValid {
195− cmds = append(
196− cmds,
197− s.saveAPIKeyAndContinue(msg.Token, false),
198− func() tea.Msg {
199− time.Sleep(5 * time.Second)
200− return claude.AuthenticationCompleteMsg{}
201− },
202− )
203− }
204−
205− return s, tea.Batch(cmds...)
206162 case hyper.DeviceFlowCompletedMsg:
207163 s.showHyperDeviceFlow = false
208164 return s, s.saveAPIKeyAndContinue(msg.Token, true)
@@ -223,10 +179,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
223179 case copilot.DeviceFlowCompletedMsg:
224180 s.showCopilotDeviceFlow = false
225181 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{})
230182 case models.APIKeyStateChangeMsg:
231183 u, cmd := s.apiKeyInput.Update(msg)
232184 s.apiKeyInput = u.(*models.APIKeyInput)
@@ -246,34 +198,8 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
246198 return s, s.hyperDeviceFlow.CopyCode()
247199 case key.Matches(msg, s.keyMap.Copy) && s.showCopilotDeviceFlow:
248200 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),
252− func() tea.Msg {
253− _ = clipboard.WriteAll(s.claudeOAuth2.URL)
254− return nil
255− },
256− util.ReportInfo("URL copied to clipboard"),
257− )
258− case key.Matches(msg, s.keyMap.Copy) && s.showClaudeAuthMethodChooser:
259− u, cmd := s.claudeAuthMethodChooser.Update(msg)
260− s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
261− return s, cmd
262− case key.Matches(msg, s.keyMap.Copy) && s.showClaudeOAuth2:
263− u, cmd := s.claudeOAuth2.Update(msg)
264− s.claudeOAuth2 = u.(*claude.OAuth2)
265− return s, cmd
266201 case key.Matches(msg, s.keyMap.Back):
267202 switch {
268− case s.showClaudeAuthMethodChooser:
269− s.claudeAuthMethodChooser.SetDefaults()
270− s.showClaudeAuthMethodChooser = false
271− return s, nil
272− case s.showClaudeOAuth2:
273− s.claudeOAuth2.SetDefaults()
274− s.showClaudeOAuth2 = false
275− s.showClaudeAuthMethodChooser = true
276− return s, nil
277203 case s.showHyperDeviceFlow:
278204 s.hyperDeviceFlow = nil
279205 s.showHyperDeviceFlow = false
@@ -285,9 +211,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
285211 case s.isAPIKeyValid:
286212 return s, nil
287213 case s.needsAPIKey:
288− if s.selectedModel.Provider.ID == catwalk.InferenceProviderAnthropic {
289− s.showClaudeAuthMethodChooser = true
290− }
291214 s.needsAPIKey = false
292215 s.selectedModel = nil
293216 s.isAPIKeyValid = false
@@ -297,28 +220,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
297220 }
298221 case key.Matches(msg, s.keyMap.Select):
299222 switch {
300− case s.showClaudeAuthMethodChooser:
301− selectedItem := s.modelList.SelectedModel()
302− if selectedItem == nil {
303− return s, nil
304− }
305−
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
316− }
317− return s, nil
318− case s.showClaudeOAuth2:
319− m2, cmd2 := s.claudeOAuth2.ValidationConfirm()
320− s.claudeOAuth2 = m2.(*claude.OAuth2)
321− return s, cmd2
322223 case s.showHyperDeviceFlow:
323224 return s, s.hyperDeviceFlow.CopyCodeAndOpenURL()
324225 case s.showCopilotDeviceFlow:
@@ -336,9 +237,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
336237 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
337238 } else {
338239 switch selectedItem.Provider.ID {
339− case catwalk.InferenceProviderAnthropic:
340− s.showClaudeAuthMethodChooser = true
341− return s, nil
342240 case hyperp.Name:
343241 s.selectedModel = selectedItem
344242 s.showHyperDeviceFlow = true
@@ -407,10 +305,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
407305 return s, s.initializeProject()
408306 }
409307 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
410− if s.showClaudeAuthMethodChooser {
411− s.claudeAuthMethodChooser.ToggleChoice()
412− return s, nil
413− }
414308 if s.needsAPIKey {
415309 u, cmd := s.apiKeyInput.Update(msg)
416310 s.apiKeyInput = u.(*models.APIKeyInput)
@@ -452,14 +346,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
452346 }
453347 default:
454348 switch {
455− case s.showClaudeAuthMethodChooser:
456− u, cmd := s.claudeAuthMethodChooser.Update(msg)
457− s.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
458− return s, cmd
459− case s.showClaudeOAuth2:
460− u, cmd := s.claudeOAuth2.Update(msg)
461− s.claudeOAuth2 = u.(*claude.OAuth2)
462− return s, cmd
463349 case s.showHyperDeviceFlow:
464350 u, cmd := s.hyperDeviceFlow.Update(msg)
465351 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
@@ -480,10 +366,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
480366 }
481367 case tea.PasteMsg:
482368 switch {
483− case s.showClaudeOAuth2:
484− u, cmd := s.claudeOAuth2.Update(msg)
485− s.claudeOAuth2 = u.(*claude.OAuth2)
486− return s, cmd
487369 case s.showHyperDeviceFlow:
488370 u, cmd := s.hyperDeviceFlow.Update(msg)
489371 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
@@ -503,10 +385,6 @@ func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
503385 }
504386 case spinner.TickMsg:
505387 switch {
506− case s.showClaudeOAuth2:
507− u, cmd := s.claudeOAuth2.Update(msg)
508− s.claudeOAuth2 = u.(*claude.OAuth2)
509− return s, cmd
510388 case s.showHyperDeviceFlow:
511389 u, cmd := s.hyperDeviceFlow.Update(msg)
512390 s.hyperDeviceFlow = u.(*hyper.DeviceFlow)
@@ -655,38 +533,6 @@ func (s *splashCmp) View() string {
655533 var content string
656534
657535 switch {
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(
663− lipgloss.Left,
664− t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth Anthropic"),
665− "",
666− chooserView,
667− ),
668− )
669− content = lipgloss.JoinVertical(
670− lipgloss.Left,
671− s.logoRendered,
672− authMethodSelector,
673− )
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(
679− lipgloss.Left,
680− t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Let's Auth Anthropic"),
681− "",
682− oauth2View,
683− ),
684− )
685− content = lipgloss.JoinVertical(
686− lipgloss.Left,
687− s.logoRendered,
688− oauthSelector,
689− )
690536 case s.showHyperDeviceFlow:
691537 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - SplashScreenPaddingY
692538 hyperView := s.hyperDeviceFlow.View()
@@ -816,14 +662,6 @@ func (s *splashCmp) View() string {
816662
817663func (s *splashCmp) Cursor() *tea.Cursor {
818664 switch {
819− case s.showClaudeAuthMethodChooser:
820− return nil
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)
825− }
826− return nil
827665 case s.needsAPIKey:
828666 cursor := s.apiKeyInput.Cursor()
829667 if cursor != nil {
@@ -894,16 +732,10 @@ func (s *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
894732 }
895733 // Calculate the correct Y offset based on current state
896734 logoHeight := lipgloss.Height(s.logoRendered)
897− if s.needsAPIKey || s.showClaudeOAuth2 {
898− var view string
899− if s.needsAPIKey {
900− view = s.apiKeyInput.View()
901− } else {
902− view = s.claudeOAuth2.View()
903− }
735+ if s.needsAPIKey {
904736 infoSectionHeight := lipgloss.Height(s.infoSection())
905737 baseOffset := logoHeight + SplashScreenPaddingY + infoSectionHeight
906− remainingHeight := s.height - baseOffset - lipgloss.Height(view) - SplashScreenPaddingY
738+ remainingHeight := s.height - baseOffset - lipgloss.Height(s.apiKeyInput.View()) - SplashScreenPaddingY
907739 offset := baseOffset + remainingHeight
908740 cursor.Y += offset
909741 cursor.X += 1
@@ -926,20 +758,6 @@ func (s *splashCmp) logoGap() int {
926758// Bindings implements SplashPage.
927759func (s *splashCmp) Bindings() []key.Binding {
928760 switch {
929− case s.showClaudeAuthMethodChooser:
930− return []key.Binding{
931− s.keyMap.Select,
932− s.keyMap.Tab,
933− s.keyMap.Back,
934− }
935− case s.showClaudeOAuth2:
936− bindings := []key.Binding{
937− s.keyMap.Select,
938− }
939− if s.claudeOAuth2.State == claude.OAuthStateURL {
940− bindings = append(bindings, s.keyMap.Copy)
941− }
942− return bindings
943761 case s.needsAPIKey:
944762 return []key.Binding{
945763 s.keyMap.Select,
@@ -1047,22 +865,6 @@ func (s *splashCmp) IsAPIKeyValid() bool {
1047865 return s.isAPIKeyValid
1048866}
1049867
1050−func (s *splashCmp) IsShowingClaudeAuthMethodChooser() bool {
1051− return s.showClaudeAuthMethodChooser
1052−}
1053−
1054−func (s *splashCmp) IsShowingClaudeOAuth2() bool {
1055− return s.showClaudeOAuth2
1056−}
1057−
1058−func (s *splashCmp) IsClaudeOAuthURLState() bool {
1059− return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateURL
1060−}
1061−
1062−func (s *splashCmp) IsClaudeOAuthComplete() bool {
1063− return s.showClaudeOAuth2 && s.claudeOAuth2.State == claude.OAuthStateCode && s.claudeOAuth2.ValidationState == claude.OAuthValidationStateValid
1064−}
1065−
1066868func (s *splashCmp) IsShowingHyperOAuth2() bool {
1067869 return s.showHyperDeviceFlow
1068870}
internal/tui/components/dialogs/claude/method.godeleted−115
@@ -1,115 +0,0 @@
1−package claude
2−
3−import (
4− tea "charm.land/bubbletea/v2"
5− "charm.land/lipgloss/v2"
6− "github.com/charmbracelet/crush/internal/tui/styles"
7− "github.com/charmbracelet/crush/internal/tui/util"
8−)
9−
10−type AuthMethod int
11−
12−const (
13− AuthMethodAPIKey AuthMethod = iota
14− AuthMethodOAuth2
15−)
16−
17−type AuthMethodChooser struct {
18− State AuthMethod
19− width int
20− isOnboarding bool
21−}
22−
23−func NewAuthMethodChooser() *AuthMethodChooser {
24− return &AuthMethodChooser{
25− State: AuthMethodOAuth2,
26− }
27−}
28−
29−func (a *AuthMethodChooser) Init() tea.Cmd {
30− return nil
31−}
32−
33−func (a *AuthMethodChooser) Update(msg tea.Msg) (util.Model, tea.Cmd) {
34− return a, nil
35−}
36−
37−func (a *AuthMethodChooser) View() string {
38− t := styles.CurrentTheme()
39−
40− white := lipgloss.NewStyle().Foreground(t.White)
41− primary := lipgloss.NewStyle().Foreground(t.Primary)
42− success := lipgloss.NewStyle().Foreground(t.Success)
43−
44− titleStyle := white
45− if a.isOnboarding {
46− titleStyle = primary
47− }
48−
49− question := lipgloss.
50− NewStyle().
51− Margin(0, 1).
52− Render(titleStyle.Render("How would you like to authenticate with ") + success.Render("Anthropic") + titleStyle.Render("?"))
53−
54− squareWidth := (a.width - 2) / 2
55− squareHeight := squareWidth / 3
56− if isOdd(squareHeight) {
57− squareHeight++
58− }
59−
60− square := lipgloss.NewStyle().
61− Width(squareWidth).
62− Height(squareHeight).
63− Margin(0, 0).
64− Border(lipgloss.RoundedBorder())
65−
66− squareText := lipgloss.NewStyle().
67− Width(squareWidth - 2).
68− Height(squareHeight).
69− Align(lipgloss.Center).
70− AlignVertical(lipgloss.Center)
71−
72− oauthBorder := t.AuthBorderSelected
73− oauthText := t.AuthTextSelected
74− apiKeyBorder := t.AuthBorderUnselected
75− apiKeyText := t.AuthTextUnselected
76−
77− if a.State == AuthMethodAPIKey {
78− oauthBorder, apiKeyBorder = apiKeyBorder, oauthBorder
79− oauthText, apiKeyText = apiKeyText, oauthText
80− }
81−
82− return lipgloss.JoinVertical(
83− lipgloss.Left,
84− question,
85− "",
86− lipgloss.JoinHorizontal(
87− lipgloss.Center,
88− square.MarginLeft(1).
89− Inherit(oauthBorder).Render(squareText.Inherit(oauthText).Render("Claude Account\nwith Subscription")),
90− square.MarginRight(1).
91− Inherit(apiKeyBorder).Render(squareText.Inherit(apiKeyText).Render("API Key")),
92− ),
93− )
94−}
95−
96−func (a *AuthMethodChooser) SetDefaults() {
97− a.State = AuthMethodOAuth2
98−}
99−
100−func (a *AuthMethodChooser) SetWidth(w int) {
101− a.width = w
102−}
103−
104−func (a *AuthMethodChooser) ToggleChoice() {
105− switch a.State {
106− case AuthMethodAPIKey:
107− a.State = AuthMethodOAuth2
108− case AuthMethodOAuth2:
109− a.State = AuthMethodAPIKey
110− }
111−}
112−
113−func isOdd(n int) bool {
114− return n%2 != 0
115−}
internal/tui/components/dialogs/claude/oauth.godeleted−267
@@ -1,267 +0,0 @@
1−package claude
2−
3−import (
4− "context"
5− "fmt"
6− "net/url"
7−
8− "charm.land/bubbles/v2/spinner"
9− "charm.land/bubbles/v2/textinput"
10− tea "charm.land/bubbletea/v2"
11− "charm.land/lipgloss/v2"
12− "github.com/charmbracelet/crush/internal/oauth"
13− "github.com/charmbracelet/crush/internal/oauth/claude"
14− "github.com/charmbracelet/crush/internal/tui/styles"
15− "github.com/charmbracelet/crush/internal/tui/util"
16− "github.com/pkg/browser"
17− "github.com/zeebo/xxh3"
18−)
19−
20−type OAuthState int
21−
22−const (
23− OAuthStateURL OAuthState = iota
24− OAuthStateCode
25−)
26−
27−type OAuthValidationState int
28−
29−const (
30− OAuthValidationStateNone OAuthValidationState = iota
31− OAuthValidationStateVerifying
32− OAuthValidationStateValid
33− OAuthValidationStateError
34−)
35−
36−type ValidationCompletedMsg struct {
37− State OAuthValidationState
38− Token *oauth.Token
39−}
40−
41−type AuthenticationCompleteMsg struct{}
42−
43−type OAuth2 struct {
44− State OAuthState
45− ValidationState OAuthValidationState
46− width int
47− isOnboarding bool
48−
49− // URL page
50− err error
51− verifier string
52− challenge string
53− URL string
54− urlId string
55− token *oauth.Token
56−
57− // Code input page
58− CodeInput textinput.Model
59− spinner spinner.Model
60−}
61−
62−func NewOAuth2() *OAuth2 {
63− return &OAuth2{
64− State: OAuthStateURL,
65− }
66−}
67−
68−func (o *OAuth2) Init() tea.Cmd {
69− t := styles.CurrentTheme()
70−
71− verifier, challenge, err := claude.GetChallenge()
72− if err != nil {
73− o.err = err
74− return nil
75− }
76−
77− url, err := claude.AuthorizeURL(verifier, challenge)
78− if err != nil {
79− o.err = err
80− return nil
81− }
82−
83− o.verifier = verifier
84− o.challenge = challenge
85− o.URL = url
86−
87− h := xxh3.New()
88− _, _ = h.WriteString(o.URL)
89− o.urlId = fmt.Sprintf("id=%x", h.Sum(nil))
90−
91− o.CodeInput = textinput.New()
92− o.CodeInput.Placeholder = "Paste or type"
93− o.CodeInput.SetVirtualCursor(false)
94− o.CodeInput.Prompt = "> "
95− o.CodeInput.SetStyles(t.S().TextInput)
96− o.CodeInput.SetWidth(50)
97−
98− o.spinner = spinner.New(
99− spinner.WithSpinner(spinner.Dot),
100− spinner.WithStyle(t.S().Base.Foreground(t.Green)),
101− )
102−
103− return nil
104−}
105−
106−func (o *OAuth2) Update(msg tea.Msg) (util.Model, tea.Cmd) {
107− var cmds []tea.Cmd
108−
109− switch msg := msg.(type) {
110− case ValidationCompletedMsg:
111− o.ValidationState = msg.State
112− o.token = msg.Token
113− switch o.ValidationState {
114− case OAuthValidationStateError:
115− o.CodeInput.Focus()
116− }
117− o.updatePrompt()
118− }
119−
120− if o.ValidationState == OAuthValidationStateVerifying {
121− var cmd tea.Cmd
122− o.spinner, cmd = o.spinner.Update(msg)
123− cmds = append(cmds, cmd)
124− o.updatePrompt()
125− }
126− {
127− var cmd tea.Cmd
128− o.CodeInput, cmd = o.CodeInput.Update(msg)
129− cmds = append(cmds, cmd)
130− }
131−
132− return o, tea.Batch(cmds...)
133−}
134−
135−func (o *OAuth2) ValidationConfirm() (util.Model, tea.Cmd) {
136− var cmds []tea.Cmd
137−
138− switch {
139− case o.State == OAuthStateURL:
140− _ = browser.OpenURL(o.URL)
141− o.State = OAuthStateCode
142− cmds = append(cmds, o.CodeInput.Focus())
143− case o.ValidationState == OAuthValidationStateNone || o.ValidationState == OAuthValidationStateError:
144− o.CodeInput.Blur()
145− o.ValidationState = OAuthValidationStateVerifying
146− cmds = append(cmds, o.spinner.Tick, o.validateCode)
147− case o.ValidationState == OAuthValidationStateValid:
148− cmds = append(cmds, func() tea.Msg { return AuthenticationCompleteMsg{} })
149− }
150−
151− o.updatePrompt()
152− return o, tea.Batch(cmds...)
153−}
154−
155−func (o *OAuth2) View() string {
156− t := styles.CurrentTheme()
157−
158− whiteStyle := lipgloss.NewStyle().Foreground(t.White)
159− primaryStyle := lipgloss.NewStyle().Foreground(t.Primary)
160− successStyle := lipgloss.NewStyle().Foreground(t.Success)
161− errorStyle := lipgloss.NewStyle().Foreground(t.Error)
162−
163− titleStyle := whiteStyle
164− if o.isOnboarding {
165− titleStyle = primaryStyle
166− }
167−
168− switch {
169− case o.err != nil:
170− return lipgloss.NewStyle().
171− Margin(0, 1).
172− Foreground(t.Error).
173− Render(o.err.Error())
174− case o.State == OAuthStateURL:
175− heading := lipgloss.
176− NewStyle().
177− Margin(0, 1).
178− Render(titleStyle.Render("Press enter key to open the following ") + successStyle.Render("URL") + titleStyle.Render(":"))
179−
180− return lipgloss.JoinVertical(
181− lipgloss.Left,
182− heading,
183− "",
184− lipgloss.NewStyle().
185− Margin(0, 1).
186− Foreground(t.FgMuted).
187− Hyperlink(o.URL, o.urlId).
188− Render(o.displayUrl()),
189− )
190− case o.State == OAuthStateCode:
191− var heading string
192−
193− switch o.ValidationState {
194− case OAuthValidationStateNone:
195− st := lipgloss.NewStyle().Margin(0, 1)
196− heading = st.Render(titleStyle.Render("Enter the ") + successStyle.Render("code") + titleStyle.Render(" you received."))
197− case OAuthValidationStateVerifying:
198− heading = titleStyle.Margin(0, 1).Render("Verifying...")
199− case OAuthValidationStateValid:
200− heading = successStyle.Margin(0, 1).Render("Validated.")
201− case OAuthValidationStateError:
202− heading = errorStyle.Margin(0, 1).Render("Invalid. Try again?")
203− }
204−
205− return lipgloss.JoinVertical(
206− lipgloss.Left,
207− heading,
208− "",
209− " "+o.CodeInput.View(),
210− )
211− default:
212− panic("claude oauth2: invalid state")
213− }
214−}
215−
216−func (o *OAuth2) SetDefaults() {
217− o.State = OAuthStateURL
218− o.ValidationState = OAuthValidationStateNone
219− o.CodeInput.SetValue("")
220− o.err = nil
221−}
222−
223−func (o *OAuth2) SetWidth(w int) {
224− o.width = w
225− o.CodeInput.SetWidth(w - 4)
226−}
227−
228−func (o *OAuth2) SetError(err error) {
229− o.err = err
230−}
231−
232−func (o *OAuth2) validateCode() tea.Msg {
233− token, err := claude.ExchangeToken(context.Background(), o.CodeInput.Value(), o.verifier)
234− if err != nil || token == nil {
235− return ValidationCompletedMsg{State: OAuthValidationStateError}
236− }
237− return ValidationCompletedMsg{State: OAuthValidationStateValid, Token: token}
238−}
239−
240−func (o *OAuth2) updatePrompt() {
241− switch o.ValidationState {
242− case OAuthValidationStateNone:
243− o.CodeInput.Prompt = "> "
244− case OAuthValidationStateVerifying:
245− o.CodeInput.Prompt = o.spinner.View() + " "
246− case OAuthValidationStateValid:
247− o.CodeInput.Prompt = styles.CheckIcon + " "
248− case OAuthValidationStateError:
249− o.CodeInput.Prompt = styles.ErrorIcon + " "
250− }
251−}
252−
253−// Remove query params for display
254−// e.g., "https://claude.ai/oauth/authorize?..." -> "https://claude.ai/oauth/authorize..."
255−func (o *OAuth2) displayUrl() string {
256− parsed, err := url.Parse(o.URL)
257− if err != nil {
258− return o.URL
259− }
260−
261− if parsed.RawQuery != "" {
262− parsed.RawQuery = ""
263− return parsed.String() + "..."
264− }
265−
266− return o.URL
267−}
internal/tui/components/dialogs/models/keys.go−57
@@ -18,11 +18,6 @@ type KeyMap struct {
1818 isHyperDeviceFlow bool
1919 isCopilotDeviceFlow bool
2020 isCopilotUnavailable bool
21−
22− isClaudeAuthChoiceHelp bool
23− isClaudeOAuthHelp bool
24− isClaudeOAuthURLState bool
25− isClaudeOAuthHelpComplete bool
2621}
2722
2823func DefaultKeyMap() KeyMap {
@@ -100,58 +95,6 @@ func (k KeyMap) ShortHelp() []key.Binding {
10095 k.Close,
10196 }
10297 }
103− if k.isClaudeAuthChoiceHelp {
104− return []key.Binding{
105− key.NewBinding(
106− key.WithKeys("left", "right", "h", "l"),
107− key.WithHelp("←→", "choose"),
108− ),
109− key.NewBinding(
110− key.WithKeys("enter"),
111− key.WithHelp("enter", "accept"),
112− ),
113− key.NewBinding(
114− key.WithKeys("esc"),
115− key.WithHelp("esc", "back"),
116− ),
117− }
118− }
119− if k.isClaudeOAuthHelp {
120− if k.isClaudeOAuthHelpComplete {
121− return []key.Binding{
122− key.NewBinding(
123− key.WithKeys("enter"),
124− key.WithHelp("enter", "close"),
125− ),
126− }
127− }
128−
129− enterHelp := "submit"
130− if k.isClaudeOAuthURLState {
131− enterHelp = "open"
132− }
133−
134− bindings := []key.Binding{
135− key.NewBinding(
136− key.WithKeys("enter"),
137− key.WithHelp("enter", enterHelp),
138− ),
139− }
140−
141− if k.isClaudeOAuthURLState {
142− bindings = append(bindings, key.NewBinding(
143− key.WithKeys("c"),
144− key.WithHelp("c", "copy url"),
145− ))
146− }
147−
148− bindings = append(bindings, key.NewBinding(
149− key.WithKeys("esc"),
150− key.WithHelp("esc", "back"),
151− ))
152−
153− return bindings
154− }
15598 if k.isAPIKeyHelp && !k.isAPIKeyValid {
15699 return []key.Binding{
157100 key.NewBinding(
internal/tui/components/dialogs/models/models.go−122
@@ -10,13 +10,11 @@ import (
1010 "charm.land/bubbles/v2/spinner"
1111 tea "charm.land/bubbletea/v2"
1212 "charm.land/lipgloss/v2"
13− "github.com/atotto/clipboard"
1413 "github.com/charmbracelet/catwalk/pkg/catwalk"
1514 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
1615 "github.com/charmbracelet/crush/internal/config"
1716 "github.com/charmbracelet/crush/internal/tui/components/core"
1817 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
19− "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
2018 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
2119 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
2220 "github.com/charmbracelet/crush/internal/tui/exp/list"
@@ -81,12 +79,6 @@ type modelDialogCmp struct {
8179 // Copilot device flow state
8280 copilotDeviceFlow *copilot.DeviceFlow
8381 showCopilotDeviceFlow bool
84−
85− // Claude state
86− claudeAuthMethodChooser *claude.AuthMethodChooser
87− claudeOAuth2 *claude.OAuth2
88− showClaudeAuthMethodChooser bool
89− showClaudeOAuth2 bool
9082}
9183
9284func NewModelDialogCmp() ModelDialog {
@@ -111,9 +103,6 @@ func NewModelDialogCmp() ModelDialog {
111103 width: defaultWidth,
112104 keyMap: DefaultKeyMap(),
113105 help: help,
114−
115− claudeAuthMethodChooser: claude.NewAuthMethodChooser(),
116− claudeOAuth2: claude.NewOAuth2(),
117106 }
118107}
119108
@@ -121,8 +110,6 @@ func (m *modelDialogCmp) Init() tea.Cmd {
121110 return tea.Batch(
122111 m.modelList.Init(),
123112 m.apiKeyInput.Init(),
124− m.claudeAuthMethodChooser.Init(),
125− m.claudeOAuth2.Init(),
126113 )
127114}
128115
@@ -133,7 +120,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
133120 m.wHeight = msg.Height
134121 m.apiKeyInput.SetWidth(m.width - 2)
135122 m.help.SetWidth(m.width - 2)
136− m.claudeAuthMethodChooser.SetWidth(m.width - 2)
137123 return m, m.modelList.SetSize(m.listWidth(), m.listHeight())
138124 case APIKeyStateChangeMsg:
139125 u, cmd := m.apiKeyInput.Update(msg)
@@ -157,20 +143,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
157143 return m, nil
158144 case copilot.DeviceFlowCompletedMsg:
159145 return m, m.saveOauthTokenAndContinue(msg.Token, true)
160− case claude.ValidationCompletedMsg:
161− var cmds []tea.Cmd
162− u, cmd := m.claudeOAuth2.Update(msg)
163− m.claudeOAuth2 = u.(*claude.OAuth2)
164− cmds = append(cmds, cmd)
165−
166− if msg.State == claude.OAuthValidationStateValid {
167− cmds = append(cmds, m.saveOauthTokenAndContinue(msg.Token, false))
168− m.keyMap.isClaudeOAuthHelpComplete = true
169− }
170−
171− return m, tea.Batch(cmds...)
172− case claude.AuthenticationCompleteMsg:
173− return m, util.CmdHandler(dialogs.CloseDialogMsg{})
174146 case tea.KeyPressMsg:
175147 switch {
176148 // Handle Hyper device flow keys
@@ -178,18 +150,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
178150 return m, m.hyperDeviceFlow.CopyCode()
179151 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showCopilotDeviceFlow:
180152 return m, m.copilotDeviceFlow.CopyCode()
181− case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showClaudeOAuth2 && m.claudeOAuth2.State == claude.OAuthStateURL:
182− return m, tea.Sequence(
183− tea.SetClipboard(m.claudeOAuth2.URL),
184− func() tea.Msg {
185− _ = clipboard.WriteAll(m.claudeOAuth2.URL)
186− return nil
187− },
188− util.ReportInfo("URL copied to clipboard"),
189− )
190− case key.Matches(msg, m.keyMap.Choose) && m.showClaudeAuthMethodChooser:
191− m.claudeAuthMethodChooser.ToggleChoice()
192− return m, nil
193153 case key.Matches(msg, m.keyMap.Select):
194154 // If showing device flow, enter copies code and opens URL
195155 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
@@ -209,37 +169,15 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
209169 }
210170
211171 askForApiKey := func() {
212− m.keyMap.isClaudeAuthChoiceHelp = false
213− m.keyMap.isClaudeOAuthHelp = false
214172 m.keyMap.isAPIKeyHelp = true
215173 m.showHyperDeviceFlow = false
216174 m.showCopilotDeviceFlow = false
217− m.showClaudeAuthMethodChooser = false
218175 m.needsAPIKey = true
219176 m.selectedModel = selectedItem
220177 m.selectedModelType = modelType
221178 m.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
222179 }
223180
224− if m.showClaudeAuthMethodChooser {
225− switch m.claudeAuthMethodChooser.State {
226− case claude.AuthMethodAPIKey:
227− askForApiKey()
228− case claude.AuthMethodOAuth2:
229− m.selectedModel = selectedItem
230− m.selectedModelType = modelType
231− m.showClaudeAuthMethodChooser = false
232− m.showClaudeOAuth2 = true
233− m.keyMap.isClaudeAuthChoiceHelp = false
234− m.keyMap.isClaudeOAuthHelp = true
235− }
236− return m, nil
237− }
238− if m.showClaudeOAuth2 {
239− m2, cmd2 := m.claudeOAuth2.ValidationConfirm()
240− m.claudeOAuth2 = m2.(*claude.OAuth2)
241− return m, cmd2
242− }
243181 if m.isAPIKeyValid {
244182 return m, m.saveOauthTokenAndContinue(m.apiKeyValue, true)
245183 }
@@ -298,10 +236,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
298236 )
299237 }
300238 switch selectedItem.Provider.ID {
301− case catwalk.InferenceProviderAnthropic:
302− m.showClaudeAuthMethodChooser = true
303− m.keyMap.isClaudeAuthChoiceHelp = true
304− return m, nil
305239 case hyperp.Name:
306240 m.showHyperDeviceFlow = true
307241 m.selectedModel = selectedItem
@@ -327,9 +261,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
327261 return m, nil
328262 case key.Matches(msg, m.keyMap.Tab):
329263 switch {
330− case m.showClaudeAuthMethodChooser:
331− m.claudeAuthMethodChooser.ToggleChoice()
332− return m, nil
333264 case m.needsAPIKey:
334265 u, cmd := m.apiKeyInput.Update(msg)
335266 m.apiKeyInput = u.(*APIKeyInput)
@@ -355,12 +286,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
355286 }
356287 m.showCopilotDeviceFlow = false
357288 m.selectedModel = nil
358− case m.showClaudeAuthMethodChooser:
359− m.claudeAuthMethodChooser.SetDefaults()
360− m.showClaudeAuthMethodChooser = false
361− m.keyMap.isClaudeAuthChoiceHelp = false
362− m.keyMap.isClaudeOAuthHelp = false
363− return m, nil
364289 case m.needsAPIKey:
365290 if m.isAPIKeyValid {
366291 return m, nil
@@ -377,14 +302,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
377302 }
378303 default:
379304 switch {
380− case m.showClaudeAuthMethodChooser:
381− u, cmd := m.claudeAuthMethodChooser.Update(msg)
382− m.claudeAuthMethodChooser = u.(*claude.AuthMethodChooser)
383− return m, cmd
384− case m.showClaudeOAuth2:
385− u, cmd := m.claudeOAuth2.Update(msg)
386− m.claudeOAuth2 = u.(*claude.OAuth2)
387− return m, cmd
388305 case m.needsAPIKey:
389306 u, cmd := m.apiKeyInput.Update(msg)
390307 m.apiKeyInput = u.(*APIKeyInput)
@@ -397,10 +314,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
397314 }
398315 case tea.PasteMsg:
399316 switch {
400− case m.showClaudeOAuth2:
401− u, cmd := m.claudeOAuth2.Update(msg)
402− m.claudeOAuth2 = u.(*claude.OAuth2)
403− return m, cmd
404317 case m.needsAPIKey:
405318 u, cmd := m.apiKeyInput.Update(msg)
406319 m.apiKeyInput = u.(*APIKeyInput)
@@ -433,10 +346,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
433346 u, cmd := m.copilotDeviceFlow.Update(msg)
434347 m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
435348 return m, cmd
436− case m.showClaudeOAuth2:
437− u, cmd := m.claudeOAuth2.Update(msg)
438− m.claudeOAuth2 = u.(*claude.OAuth2)
439− return m, cmd
440349 default:
441350 u, cmd := m.apiKeyInput.Update(msg)
442351 m.apiKeyInput = u.(*APIKeyInput)
@@ -483,27 +392,6 @@ func (m *modelDialogCmp) View() string {
483392 m.keyMap.isCopilotUnavailable = false
484393
485394 switch {
486− case m.showClaudeAuthMethodChooser:
487− chooserView := m.claudeAuthMethodChooser.View()
488− content := lipgloss.JoinVertical(
489− lipgloss.Left,
490− t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Let's Auth Anthropic", m.width-4)),
491− chooserView,
492− "",
493− t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
494− )
495− return m.style().Render(content)
496− case m.showClaudeOAuth2:
497− m.keyMap.isClaudeOAuthURLState = m.claudeOAuth2.State == claude.OAuthStateURL
498− oauth2View := m.claudeOAuth2.View()
499− content := lipgloss.JoinVertical(
500− lipgloss.Left,
501− t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Let's Auth Anthropic", m.width-4)),
502− oauth2View,
503− "",
504− t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
505− )
506− return m.style().Render(content)
507395 case m.needsAPIKey:
508396 // Show API key input
509397 m.keyMap.isAPIKeyHelp = true
@@ -540,16 +428,6 @@ func (m *modelDialogCmp) Cursor() *tea.Cursor {
540428 if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
541429 return m.copilotDeviceFlow.Cursor()
542430 }
543− if m.showClaudeAuthMethodChooser {
544− return nil
545− }
546− if m.showClaudeOAuth2 {
547− if cursor := m.claudeOAuth2.CodeInput.Cursor(); cursor != nil {
548− cursor.Y += 2 // FIXME(@andreynering): Why do we need this?
549− return m.moveCursor(cursor)
550− }
551− return nil
552− }
553431 if m.needsAPIKey {
554432 cursor := m.apiKeyInput.Cursor()
555433 if cursor != nil {
internal/tui/page/chat/chat.go+2−50
@@ -29,7 +29,6 @@ import (
2929 "github.com/charmbracelet/crush/internal/tui/components/core"
3030 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
3131 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
32− "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
3332 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
3433 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
3534 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
@@ -337,9 +336,7 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
337336 cmds = append(cmds, cmd)
338337 return p, tea.Batch(cmds...)
339338
340− case claude.ValidationCompletedMsg,
341− claude.AuthenticationCompleteMsg,
342− hyper.DeviceFlowCompletedMsg,
339+ case hyper.DeviceFlowCompletedMsg,
343340 hyper.DeviceAuthInitiatedMsg,
344341 hyper.DeviceFlowErrorMsg,
345342 copilot.DeviceAuthInitiatedMsg,
@@ -1037,53 +1034,8 @@ func (p *chatPage) Help() help.KeyMap {
10371034 var shortList []key.Binding
10381035 var fullList [][]key.Binding
10391036 switch {
1040− case p.isOnboarding && p.splash.IsShowingClaudeAuthMethodChooser():
1041− shortList = append(shortList,
1042− // Choose auth method
1043− key.NewBinding(
1044− key.WithKeys("left", "right", "tab"),
1045− key.WithHelp("←→/tab", "choose"),
1046− ),
1047− // Accept selection
1048− key.NewBinding(
1049− key.WithKeys("enter"),
1050− key.WithHelp("enter", "accept"),
1051− ),
1052− // Go back
1053− key.NewBinding(
1054− key.WithKeys("esc", "alt+esc"),
1055− key.WithHelp("esc", "back"),
1056− ),
1057− // Quit
1058− key.NewBinding(
1059− key.WithKeys("ctrl+c"),
1060− key.WithHelp("ctrl+c", "quit"),
1061− ),
1062− )
1063− // keep them the same
1064− for _, v := range shortList {
1065− fullList = append(fullList, []key.Binding{v})
1066− }
1067− case p.isOnboarding && p.splash.IsShowingClaudeOAuth2():
1037+ case p.isOnboarding:
10681038 switch {
1069− case p.splash.IsClaudeOAuthURLState():
1070− shortList = append(shortList,
1071− key.NewBinding(
1072− key.WithKeys("enter"),
1073− key.WithHelp("enter", "open"),
1074− ),
1075− key.NewBinding(
1076− key.WithKeys("c"),
1077− key.WithHelp("c", "copy url"),
1078− ),
1079− )
1080− case p.splash.IsClaudeOAuthComplete():
1081− shortList = append(shortList,
1082− key.NewBinding(
1083− key.WithKeys("enter"),
1084− key.WithHelp("enter", "continue"),
1085− ),
1086− )
10871039 case p.splash.IsShowingHyperOAuth2() || p.splash.IsShowingCopilotOAuth2():
10881040 shortList = append(shortList,
10891041 key.NewBinding(