on main help sign in
feat: remove claude code support (#1783)
5590161Andrey Nering
internal/agent/agent.go−18
@@ -833,10 +833,6 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
833 modelConfig.CostPer1MIn/1e6*float64(resp.TotalUsage.InputTokens) +
833 modelConfig.CostPer1MIn/1e6*float64(resp.TotalUsage.InputTokens) +
834 modelConfig.CostPer1MOut/1e6*float64(resp.TotalUsage.OutputTokens)
834 modelConfig.CostPer1MOut/1e6*float64(resp.TotalUsage.OutputTokens)
835
835
836− if a.isClaudeCode() {
837− cost = 0
838− }
839−
840 // Use override cost if available (e.g., from OpenRouter).
836 // Use override cost if available (e.g., from OpenRouter).
841 if openrouterCost != nil {
837 if openrouterCost != nil {
842 cost = *openrouterCost
838 cost = *openrouterCost
@@ -874,10 +870,6 @@ func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session,
874 modelConfig.CostPer1MIn/1e6*float64(usage.InputTokens) +
870 modelConfig.CostPer1MIn/1e6*float64(usage.InputTokens) +
875 modelConfig.CostPer1MOut/1e6*float64(usage.OutputTokens)
871 modelConfig.CostPer1MOut/1e6*float64(usage.OutputTokens)
876
872
877− if a.isClaudeCode() {
878− cost = 0
879− }
880−
881 a.eventTokensUsed(session.ID, model, usage, cost)
873 a.eventTokensUsed(session.ID, model, usage, cost)
882
874
883 if overrideCost != nil {
875 if overrideCost != nil {
@@ -985,19 +977,9 @@ func (a *sessionAgent) Model() Model {
985}
977}
986
978
987func (a *sessionAgent) promptPrefix() string {
979func (a *sessionAgent) promptPrefix() string {
988− if a.isClaudeCode() {
989− return "You are Claude Code, Anthropic's official CLI for Claude."
990− }
991 return a.systemPromptPrefix
980 return a.systemPromptPrefix
992}
981}
993
982
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−
1001// convertToToolResult converts a fantasy tool result to a message tool result.
983// convertToToolResult converts a fantasy tool result to a message tool result.
1002func (a *sessionAgent) convertToToolResult(result fantasy.ToolResultContent) message.ToolResult {
984func (a *sessionAgent) convertToToolResult(result fantasy.ToolResultContent) message.ToolResult {
1003 baseResult := message.ToolResult{
985 baseResult := message.ToolResult{
internal/agent/coordinator.go+4−4
@@ -518,13 +518,13 @@ func (c *coordinator) buildAgentModels(ctx context.Context, isSubAgent bool) (Mo
518 }, nil
518 }, nil
519}
519}
520
520
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) {
522 var opts []anthropic.Option
522 var opts []anthropic.Option
523
523
524− if isOauth {
524+ if strings.HasPrefix(apiKey, "Bearer ") {
525 // NOTE: Prevent the SDK from picking up the API key from env.
525 // NOTE: Prevent the SDK from picking up the API key from env.
526 os.Setenv("ANTHROPIC_API_KEY", "")
526 os.Setenv("ANTHROPIC_API_KEY", "")
527− headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
527+ headers["Authorization"] = apiKey
528 } else if apiKey != "" {
528 } else if apiKey != "" {
529 // X-Api-Key header
529 // X-Api-Key header
530 opts = append(opts, anthropic.WithAPIKey(apiKey))
530 opts = append(opts, anthropic.WithAPIKey(apiKey))
@@ -731,7 +731,7 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
731 case openai.Name:
731 case openai.Name:
732 return c.buildOpenaiProvider(baseURL, apiKey, headers)
732 return c.buildOpenaiProvider(baseURL, apiKey, headers)
733 case anthropic.Name:
733 case anthropic.Name:
734− return c.buildAnthropicProvider(baseURL, apiKey, headers, providerCfg.OAuthToken != nil)
734+ return c.buildAnthropicProvider(baseURL, apiKey, headers)
735 case openrouter.Name:
735 case openrouter.Name:
736 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
736 return c.buildOpenrouterProvider(baseURL, apiKey, headers)
737 case azure.Name:
737 case azure.Name:
internal/cmd/login.go+1−64
@@ -6,14 +6,12 @@ import (
6 "fmt"
6 "fmt"
7 "os"
7 "os"
8 "os/signal"
8 "os/signal"
9− "strings"
10
9
11 "charm.land/lipgloss/v2"
10 "charm.land/lipgloss/v2"
12 "github.com/atotto/clipboard"
11 "github.com/atotto/clipboard"
13 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
12 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
14 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/oauth"
14 "github.com/charmbracelet/crush/internal/oauth"
16− "github.com/charmbracelet/crush/internal/oauth/claude"
17 "github.com/charmbracelet/crush/internal/oauth/copilot"
15 "github.com/charmbracelet/crush/internal/oauth/copilot"
18 "github.com/charmbracelet/crush/internal/oauth/hyper"
16 "github.com/charmbracelet/crush/internal/oauth/hyper"
19 "github.com/pkg/browser"
17 "github.com/pkg/browser"
@@ -26,21 +24,16 @@ var loginCmd = &cobra.Command{
26 Short: "Login Crush to a platform",
24 Short: "Login Crush to a platform",
27 Long: `Login Crush to a specified platform.
25 Long: `Login Crush to a specified platform.
28The platform should be provided as an argument.
26The platform should be provided as an argument.
29−Available platforms are: hyper, claude, copilot.`,
27+Available platforms are: hyper, copilot.`,
30 Example: `
28 Example: `
31# Authenticate with Charm Hyper
29# Authenticate with Charm Hyper
32crush login
30crush login
33
31
34−# Authenticate with Claude Code Max
35−crush login claude
36−
37# Authenticate with GitHub Copilot
32# Authenticate with GitHub Copilot
38crush login copilot
33crush login copilot
39 `,
34 `,
40 ValidArgs: []cobra.Completion{
35 ValidArgs: []cobra.Completion{
41 "hyper",
36 "hyper",
42− "claude",
43− "anthropic",
44 "copilot",
37 "copilot",
45 "github",
38 "github",
46 "github-copilot",
39 "github-copilot",
@@ -60,8 +53,6 @@ crush login copilot
60 switch provider {
53 switch provider {
61 case "hyper":
54 case "hyper":
62 return loginHyper()
55 return loginHyper()
63− case "anthropic", "claude":
64− return loginClaude()
65 case "copilot", "github", "github-copilot":
56 case "copilot", "github", "github-copilot":
66 return loginCopilot()
57 return loginCopilot()
67 default:
58 default:
@@ -133,60 +124,6 @@ func loginHyper() error {
133 return nil
124 return nil
134}
125}
135
126
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−
190func loginCopilot() error {
127func loginCopilot() error {
191 ctx := getLoginContext()
128 ctx := getLoginContext()
192
129
internal/config/config.go+19−22
@@ -19,7 +19,6 @@ import (
19 "github.com/charmbracelet/crush/internal/csync"
19 "github.com/charmbracelet/crush/internal/csync"
20 "github.com/charmbracelet/crush/internal/env"
20 "github.com/charmbracelet/crush/internal/env"
21 "github.com/charmbracelet/crush/internal/oauth"
21 "github.com/charmbracelet/crush/internal/oauth"
22− "github.com/charmbracelet/crush/internal/oauth/claude"
23 "github.com/charmbracelet/crush/internal/oauth/copilot"
22 "github.com/charmbracelet/crush/internal/oauth/copilot"
24 "github.com/charmbracelet/crush/internal/oauth/hyper"
23 "github.com/charmbracelet/crush/internal/oauth/hyper"
25 "github.com/invopop/jsonschema"
24 "github.com/invopop/jsonschema"
@@ -155,21 +154,6 @@ func (pc *ProviderConfig) ToProvider() catwalk.Provider {
155 return provider
154 return provider
156}
155}
157
156
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−
173func (pc *ProviderConfig) SetupGitHubCopilot() {
157func (pc *ProviderConfig) SetupGitHubCopilot() {
174 maps.Copy(pc.ExtraHeaders, copilot.Headers())
158 maps.Copy(pc.ExtraHeaders, copilot.Headers())
175}
159}
@@ -522,6 +506,25 @@ func (c *Config) SetConfigField(key string, value any) error {
522 return nil
506 return nil
523}
507}
524
508
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+
525// RefreshOAuthToken refreshes the OAuth token for the given provider.
528// RefreshOAuthToken refreshes the OAuth token for the given provider.
526func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error {
529func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error {
527 providerConfig, exists := c.Providers.Get(providerID)
530 providerConfig, exists := c.Providers.Get(providerID)
@@ -536,8 +539,6 @@ func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error
536 var newToken *oauth.Token
539 var newToken *oauth.Token
537 var refreshErr error
540 var refreshErr error
538 switch providerID {
541 switch providerID {
539− case string(catwalk.InferenceProviderAnthropic):
540− newToken, refreshErr = claude.RefreshToken(ctx, providerConfig.OAuthToken.RefreshToken)
541 case string(catwalk.InferenceProviderCopilot):
542 case string(catwalk.InferenceProviderCopilot):
542 newToken, refreshErr = copilot.RefreshToken(ctx, providerConfig.OAuthToken.RefreshToken)
543 newToken, refreshErr = copilot.RefreshToken(ctx, providerConfig.OAuthToken.RefreshToken)
543 case hyperp.Name:
544 case hyperp.Name:
@@ -554,8 +555,6 @@ func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error
554 providerConfig.APIKey = newToken.AccessToken
555 providerConfig.APIKey = newToken.AccessToken
555
556
556 switch providerID {
557 switch providerID {
557− case string(catwalk.InferenceProviderAnthropic):
558− providerConfig.SetupClaudeCode()
559 case string(catwalk.InferenceProviderCopilot):
558 case string(catwalk.InferenceProviderCopilot):
560 providerConfig.SetupGitHubCopilot()
559 providerConfig.SetupGitHubCopilot()
561 }
560 }
@@ -594,8 +593,6 @@ func (c *Config) SetProviderAPIKey(providerID string, apiKey any) error {
594 providerConfig.APIKey = v.AccessToken
593 providerConfig.APIKey = v.AccessToken
595 providerConfig.OAuthToken = v
594 providerConfig.OAuthToken = v
596 switch providerID {
595 switch providerID {
597− case string(catwalk.InferenceProviderAnthropic):
598− providerConfig.SetupClaudeCode()
599 case string(catwalk.InferenceProviderCopilot):
596 case string(catwalk.InferenceProviderCopilot):
600 providerConfig.SetupGitHubCopilot()
597 providerConfig.SetupGitHubCopilot()
601 }
598 }
internal/config/load.go+6−5
@@ -202,11 +202,12 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
202
202
203 switch {
203 switch {
204 case p.ID == catwalk.InferenceProviderAnthropic && config.OAuthToken != nil:
204 case p.ID == catwalk.InferenceProviderAnthropic && config.OAuthToken != nil:
205− prepared.SetupClaudeCode()
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
206− case p.ID == catwalk.InferenceProviderCopilot:
209+ case p.ID == catwalk.InferenceProviderCopilot && config.OAuthToken != nil:
207− if config.OAuthToken != nil {
208− prepared.SetupGitHubCopilot()
210+ prepared.SetupGitHubCopilot()
209− }
210 }
211 }
211
212
212 switch p.ID {
213 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 (
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
49
47
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−
62 // IsShowingClaudeOAuth2 returns whether showing Hyper OAuth2 flow
48 // IsShowingClaudeOAuth2 returns whether showing Hyper OAuth2 flow
63 IsShowingHyperOAuth2() bool
49 IsShowingHyperOAuth2() bool
64
50
@@ -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
106−
107− // Claude state
108− claudeAuthMethodChooser *claude.AuthMethodChooser
109− claudeOAuth2 *claude.OAuth2
110− showClaudeAuthMethodChooser bool
111− showClaudeOAuth2 bool
112}
92}
113
93
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,
137−
138− claudeAuthMethodChooser: claude.NewAuthMethodChooser(),
139− claudeOAuth2: claude.NewOAuth2(),
140 }
117 }
141}
118}
142
119
@@ -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(),
163 )
138 )
164}
139}
165
140
@@ -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)
181}
155}
182
156
@@ -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:
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...)
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),
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
266 case key.Matches(msg, s.keyMap.Back):
201 case key.Matches(msg, s.keyMap.Back):
267 switch {
202 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
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
290− }
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) {
297 }
220 }
298 case key.Matches(msg, s.keyMap.Select):
221 case key.Matches(msg, s.keyMap.Select):
299 switch {
222 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
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{}))
337 } else {
238 } else {
338 switch selectedItem.Provider.ID {
239 switch selectedItem.Provider.ID {
339− case catwalk.InferenceProviderAnthropic:
340− s.showClaudeAuthMethodChooser = true
341− return s, nil
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()
408 }
306 }
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()
412− return s, nil
413− }
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) {
452 }
346 }
453 default:
347 default:
454 switch {
348 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
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) {
480 }
366 }
481 case tea.PasteMsg:
367 case tea.PasteMsg:
482 switch {
368 switch {
483− case s.showClaudeOAuth2:
484− u, cmd := s.claudeOAuth2.Update(msg)
485− s.claudeOAuth2 = u.(*claude.OAuth2)
486− return s, cmd
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) {
503 }
385 }
504 case spinner.TickMsg:
386 case spinner.TickMsg:
505 switch {
387 switch {
506− case s.showClaudeOAuth2:
507− u, cmd := s.claudeOAuth2.Update(msg)
508− s.claudeOAuth2 = u.(*claude.OAuth2)
509− return s, cmd
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
656
534
657 switch {
535 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− )
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 {
816
662
817func (s *splashCmp) Cursor() *tea.Cursor {
663func (s *splashCmp) Cursor() *tea.Cursor {
818 switch {
664 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
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 {
894 }
732 }
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 {
898− var view string
899− if s.needsAPIKey {
900− view = s.apiKeyInput.View()
901− } else {
902− view = s.claudeOAuth2.View()
903− }
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 {
928 switch {
760 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
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
1048}
866}
1049
867
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−
1066func (s *splashCmp) IsShowingHyperOAuth2() bool {
868func (s *splashCmp) IsShowingHyperOAuth2() bool {
1067 return s.showHyperDeviceFlow
869 return s.showHyperDeviceFlow
1068}
870}
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 {
18 isHyperDeviceFlow bool
18 isHyperDeviceFlow bool
19 isCopilotDeviceFlow bool
19 isCopilotDeviceFlow bool
20 isCopilotUnavailable bool
20 isCopilotUnavailable bool
21−
22− isClaudeAuthChoiceHelp bool
23− isClaudeOAuthHelp bool
24− isClaudeOAuthURLState bool
25− isClaudeOAuthHelpComplete bool
26}
21}
27
22
28func DefaultKeyMap() KeyMap {
23func DefaultKeyMap() KeyMap {
@@ -100,58 +95,6 @@ func (k KeyMap) ShortHelp() []key.Binding {
100 k.Close,
95 k.Close,
101 }
96 }
102 }
97 }
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− }
155 if k.isAPIKeyHelp && !k.isAPIKeyValid {
98 if k.isAPIKeyHelp && !k.isAPIKeyValid {
156 return []key.Binding{
99 return []key.Binding{
157 key.NewBinding(
100 key.NewBinding(
internal/tui/components/dialogs/models/models.go−122
@@ -10,13 +10,11 @@ import (
10 "charm.land/bubbles/v2/spinner"
10 "charm.land/bubbles/v2/spinner"
11 tea "charm.land/bubbletea/v2"
11 tea "charm.land/bubbletea/v2"
12 "charm.land/lipgloss/v2"
12 "charm.land/lipgloss/v2"
13− "github.com/atotto/clipboard"
14 "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/catwalk/pkg/catwalk"
15 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
14 hyperp "github.com/charmbracelet/crush/internal/agent/hyper"
16 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/tui/components/core"
16 "github.com/charmbracelet/crush/internal/tui/components/core"
18 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
17 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
19− "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
20 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
18 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
21 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
19 "github.com/charmbracelet/crush/internal/tui/components/dialogs/hyper"
22 "github.com/charmbracelet/crush/internal/tui/exp/list"
20 "github.com/charmbracelet/crush/internal/tui/exp/list"
@@ -81,12 +79,6 @@ type modelDialogCmp struct {
81 // Copilot device flow state
79 // Copilot device flow state
82 copilotDeviceFlow *copilot.DeviceFlow
80 copilotDeviceFlow *copilot.DeviceFlow
83 showCopilotDeviceFlow bool
81 showCopilotDeviceFlow bool
84−
85− // Claude state
86− claudeAuthMethodChooser *claude.AuthMethodChooser
87− claudeOAuth2 *claude.OAuth2
88− showClaudeAuthMethodChooser bool
89− showClaudeOAuth2 bool
90}
82}
91
83
92func NewModelDialogCmp() ModelDialog {
84func NewModelDialogCmp() ModelDialog {
@@ -111,9 +103,6 @@ func NewModelDialogCmp() ModelDialog {
111 width: defaultWidth,
103 width: defaultWidth,
112 keyMap: DefaultKeyMap(),
104 keyMap: DefaultKeyMap(),
113 help: help,
105 help: help,
114−
115− claudeAuthMethodChooser: claude.NewAuthMethodChooser(),
116− claudeOAuth2: claude.NewOAuth2(),
117 }
106 }
118}
107}
119
108
@@ -121,8 +110,6 @@ func (m *modelDialogCmp) Init() tea.Cmd {
121 return tea.Batch(
110 return tea.Batch(
122 m.modelList.Init(),
111 m.modelList.Init(),
123 m.apiKeyInput.Init(),
112 m.apiKeyInput.Init(),
124− m.claudeAuthMethodChooser.Init(),
125− m.claudeOAuth2.Init(),
126 )
113 )
127}
114}
128
115
@@ -133,7 +120,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
133 m.wHeight = msg.Height
120 m.wHeight = msg.Height
134 m.apiKeyInput.SetWidth(m.width - 2)
121 m.apiKeyInput.SetWidth(m.width - 2)
135 m.help.SetWidth(m.width - 2)
122 m.help.SetWidth(m.width - 2)
136− m.claudeAuthMethodChooser.SetWidth(m.width - 2)
137 return m, m.modelList.SetSize(m.listWidth(), m.listHeight())
123 return m, m.modelList.SetSize(m.listWidth(), m.listHeight())
138 case APIKeyStateChangeMsg:
124 case APIKeyStateChangeMsg:
139 u, cmd := m.apiKeyInput.Update(msg)
125 u, cmd := m.apiKeyInput.Update(msg)
@@ -157,20 +143,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
157 return m, nil
143 return m, nil
158 case copilot.DeviceFlowCompletedMsg:
144 case copilot.DeviceFlowCompletedMsg:
159 return m, m.saveOauthTokenAndContinue(msg.Token, true)
145 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{})
174 case tea.KeyPressMsg:
146 case tea.KeyPressMsg:
175 switch {
147 switch {
176 // Handle Hyper device flow keys
148 // Handle Hyper device flow keys
@@ -178,18 +150,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
178 return m, m.hyperDeviceFlow.CopyCode()
150 return m, m.hyperDeviceFlow.CopyCode()
179 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showCopilotDeviceFlow:
151 case key.Matches(msg, key.NewBinding(key.WithKeys("c", "C"))) && m.showCopilotDeviceFlow:
180 return m, m.copilotDeviceFlow.CopyCode()
152 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
193 case key.Matches(msg, m.keyMap.Select):
153 case key.Matches(msg, m.keyMap.Select):
194 // If showing device flow, enter copies code and opens URL
154 // If showing device flow, enter copies code and opens URL
195 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
155 if m.showHyperDeviceFlow && m.hyperDeviceFlow != nil {
@@ -209,37 +169,15 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
209 }
169 }
210
170
211 askForApiKey := func() {
171 askForApiKey := func() {
212− m.keyMap.isClaudeAuthChoiceHelp = false
213− m.keyMap.isClaudeOAuthHelp = false
214 m.keyMap.isAPIKeyHelp = true
172 m.keyMap.isAPIKeyHelp = true
215 m.showHyperDeviceFlow = false
173 m.showHyperDeviceFlow = false
216 m.showCopilotDeviceFlow = false
174 m.showCopilotDeviceFlow = false
217− m.showClaudeAuthMethodChooser = false
218 m.needsAPIKey = true
175 m.needsAPIKey = true
219 m.selectedModel = selectedItem
176 m.selectedModel = selectedItem
220 m.selectedModelType = modelType
177 m.selectedModelType = modelType
221 m.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
178 m.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
222 }
179 }
223
180
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− }
243 if m.isAPIKeyValid {
181 if m.isAPIKeyValid {
244 return m, m.saveOauthTokenAndContinue(m.apiKeyValue, true)
182 return m, m.saveOauthTokenAndContinue(m.apiKeyValue, true)
245 }
183 }
@@ -298,10 +236,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
298 )
236 )
299 }
237 }
300 switch selectedItem.Provider.ID {
238 switch selectedItem.Provider.ID {
301− case catwalk.InferenceProviderAnthropic:
302− m.showClaudeAuthMethodChooser = true
303− m.keyMap.isClaudeAuthChoiceHelp = true
304− return m, nil
305 case hyperp.Name:
239 case hyperp.Name:
306 m.showHyperDeviceFlow = true
240 m.showHyperDeviceFlow = true
307 m.selectedModel = selectedItem
241 m.selectedModel = selectedItem
@@ -327,9 +261,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
327 return m, nil
261 return m, nil
328 case key.Matches(msg, m.keyMap.Tab):
262 case key.Matches(msg, m.keyMap.Tab):
329 switch {
263 switch {
330− case m.showClaudeAuthMethodChooser:
331− m.claudeAuthMethodChooser.ToggleChoice()
332− return m, nil
333 case m.needsAPIKey:
264 case m.needsAPIKey:
334 u, cmd := m.apiKeyInput.Update(msg)
265 u, cmd := m.apiKeyInput.Update(msg)
335 m.apiKeyInput = u.(*APIKeyInput)
266 m.apiKeyInput = u.(*APIKeyInput)
@@ -355,12 +286,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
355 }
286 }
356 m.showCopilotDeviceFlow = false
287 m.showCopilotDeviceFlow = false
357 m.selectedModel = nil
288 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
364 case m.needsAPIKey:
289 case m.needsAPIKey:
365 if m.isAPIKeyValid {
290 if m.isAPIKeyValid {
366 return m, nil
291 return m, nil
@@ -377,14 +302,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
377 }
302 }
378 default:
303 default:
379 switch {
304 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
388 case m.needsAPIKey:
305 case m.needsAPIKey:
389 u, cmd := m.apiKeyInput.Update(msg)
306 u, cmd := m.apiKeyInput.Update(msg)
390 m.apiKeyInput = u.(*APIKeyInput)
307 m.apiKeyInput = u.(*APIKeyInput)
@@ -397,10 +314,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
397 }
314 }
398 case tea.PasteMsg:
315 case tea.PasteMsg:
399 switch {
316 switch {
400− case m.showClaudeOAuth2:
401− u, cmd := m.claudeOAuth2.Update(msg)
402− m.claudeOAuth2 = u.(*claude.OAuth2)
403− return m, cmd
404 case m.needsAPIKey:
317 case m.needsAPIKey:
405 u, cmd := m.apiKeyInput.Update(msg)
318 u, cmd := m.apiKeyInput.Update(msg)
406 m.apiKeyInput = u.(*APIKeyInput)
319 m.apiKeyInput = u.(*APIKeyInput)
@@ -433,10 +346,6 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
433 u, cmd := m.copilotDeviceFlow.Update(msg)
346 u, cmd := m.copilotDeviceFlow.Update(msg)
434 m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
347 m.copilotDeviceFlow = u.(*copilot.DeviceFlow)
435 return m, cmd
348 return m, cmd
436− case m.showClaudeOAuth2:
437− u, cmd := m.claudeOAuth2.Update(msg)
438− m.claudeOAuth2 = u.(*claude.OAuth2)
439− return m, cmd
440 default:
349 default:
441 u, cmd := m.apiKeyInput.Update(msg)
350 u, cmd := m.apiKeyInput.Update(msg)
442 m.apiKeyInput = u.(*APIKeyInput)
351 m.apiKeyInput = u.(*APIKeyInput)
@@ -483,27 +392,6 @@ func (m *modelDialogCmp) View() string {
483 m.keyMap.isCopilotUnavailable = false
392 m.keyMap.isCopilotUnavailable = false
484
393
485 switch {
394 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)
507 case m.needsAPIKey:
395 case m.needsAPIKey:
508 // Show API key input
396 // Show API key input
509 m.keyMap.isAPIKeyHelp = true
397 m.keyMap.isAPIKeyHelp = true
@@ -540,16 +428,6 @@ func (m *modelDialogCmp) Cursor() *tea.Cursor {
540 if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
428 if m.showCopilotDeviceFlow && m.copilotDeviceFlow != nil {
541 return m.copilotDeviceFlow.Cursor()
429 return m.copilotDeviceFlow.Cursor()
542 }
430 }
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− }
553 if m.needsAPIKey {
431 if m.needsAPIKey {
554 cursor := m.apiKeyInput.Cursor()
432 cursor := m.apiKeyInput.Cursor()
555 if cursor != nil {
433 if cursor != nil {
internal/tui/page/chat/chat.go+2−50
@@ -29,7 +29,6 @@ import (
29 "github.com/charmbracelet/crush/internal/tui/components/core"
29 "github.com/charmbracelet/crush/internal/tui/components/core"
30 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
30 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
31 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
31 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
32− "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
33 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
32 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
34 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
33 "github.com/charmbracelet/crush/internal/tui/components/dialogs/copilot"
35 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
34 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
@@ -337,9 +336,7 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
337 cmds = append(cmds, cmd)
336 cmds = append(cmds, cmd)
338 return p, tea.Batch(cmds...)
337 return p, tea.Batch(cmds...)
339
338
340− case claude.ValidationCompletedMsg,
341− claude.AuthenticationCompleteMsg,
342− hyper.DeviceFlowCompletedMsg,
339+ case hyper.DeviceFlowCompletedMsg,
343 hyper.DeviceAuthInitiatedMsg,
340 hyper.DeviceAuthInitiatedMsg,
344 hyper.DeviceFlowErrorMsg,
341 hyper.DeviceFlowErrorMsg,
345 copilot.DeviceAuthInitiatedMsg,
342 copilot.DeviceAuthInitiatedMsg,
@@ -1037,53 +1034,8 @@ func (p *chatPage) Help() help.KeyMap {
1037 var shortList []key.Binding
1034 var shortList []key.Binding
1038 var fullList [][]key.Binding
1035 var fullList [][]key.Binding
1039 switch {
1036 switch {
1040− case p.isOnboarding && p.splash.IsShowingClaudeAuthMethodChooser():
1037+ case p.isOnboarding:
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():
1068 switch {
1038 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− )
1087 case p.splash.IsShowingHyperOAuth2() || p.splash.IsShowingCopilotOAuth2():
1039 case p.splash.IsShowingHyperOAuth2() || p.splash.IsShowingCopilotOAuth2():
1088 shortList = append(shortList,
1040 shortList = append(shortList,
1089 key.NewBinding(
1041 key.NewBinding(