devel/crush: new port, crush 0.82.0
This commit is contained in:
244
devel/crush/patches/patch-internal_ui_dialog_oauth_claude_go
Normal file
244
devel/crush/patches/patch-internal_ui_dialog_oauth_claude_go
Normal file
@@ -0,0 +1,244 @@
|
||||
--- /dev/null
|
||||
+++ internal/ui/dialog/oauth_claude.go
|
||||
@@ -0,0 +1,241 @@
|
||||
+package dialog
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "fmt"
|
||||
+ "strings"
|
||||
+
|
||||
+ "charm.land/bubbles/v2/help"
|
||||
+ "charm.land/bubbles/v2/key"
|
||||
+ "charm.land/bubbles/v2/spinner"
|
||||
+ "charm.land/bubbles/v2/textinput"
|
||||
+ tea "charm.land/bubbletea/v2"
|
||||
+ "charm.land/catwalk/pkg/catwalk"
|
||||
+ "github.com/charmbracelet/crush/internal/config"
|
||||
+ "github.com/charmbracelet/crush/internal/oauth"
|
||||
+ "github.com/charmbracelet/crush/internal/oauth/claude"
|
||||
+ "github.com/charmbracelet/crush/internal/ui/common"
|
||||
+ "github.com/charmbracelet/crush/internal/ui/util"
|
||||
+ uv "github.com/charmbracelet/ultraviolet"
|
||||
+ "github.com/pkg/browser"
|
||||
+)
|
||||
+
|
||||
+type oauthClaudeState int
|
||||
+
|
||||
+const (
|
||||
+ oauthClaudeStateInput oauthClaudeState = iota
|
||||
+ oauthClaudeStateExchanging
|
||||
+ oauthClaudeStateError
|
||||
+)
|
||||
+
|
||||
+type oauthClaudeComplete struct{ token *oauth.Token }
|
||||
+type oauthClaudeError struct{ err error }
|
||||
+
|
||||
+// OAuthClaudeID is the identifier for the Claude OAuth dialog.
|
||||
+const OAuthClaudeID = "oauth_claude"
|
||||
+
|
||||
+// OAuthClaude is a dialog for Claude Code Max PKCE authentication.
|
||||
+type OAuthClaude struct {
|
||||
+ com *common.Common
|
||||
+ isOnboarding bool
|
||||
+ provider catwalk.Provider
|
||||
+ model config.SelectedModel
|
||||
+ modelType config.SelectedModelType
|
||||
+
|
||||
+ width int
|
||||
+ state oauthClaudeState
|
||||
+ verifier string
|
||||
+ verificationURL string
|
||||
+
|
||||
+ keyMap struct {
|
||||
+ Submit key.Binding
|
||||
+ Open key.Binding
|
||||
+ Close key.Binding
|
||||
+ }
|
||||
+ input textinput.Model
|
||||
+ spinner spinner.Model
|
||||
+ help help.Model
|
||||
+}
|
||||
+
|
||||
+var _ Dialog = (*OAuthClaude)(nil)
|
||||
+
|
||||
+// NewOAuthClaude creates a new Claude PKCE authentication dialog.
|
||||
+func NewOAuthClaude(
|
||||
+ com *common.Common,
|
||||
+ isOnboarding bool,
|
||||
+ provider catwalk.Provider,
|
||||
+ model config.SelectedModel,
|
||||
+ modelType config.SelectedModelType,
|
||||
+) (*OAuthClaude, tea.Cmd) {
|
||||
+ verifier, challenge, err := claude.GetChallenge()
|
||||
+ if err != nil {
|
||||
+ m := &OAuthClaude{com: com, isOnboarding: isOnboarding, provider: provider, model: model, modelType: modelType, width: 60, state: oauthClaudeStateError}
|
||||
+ return m, util.ReportError(fmt.Errorf("failed to generate PKCE challenge: %w", err))
|
||||
+ }
|
||||
+ authURL, err := claude.AuthorizeURL(verifier, challenge)
|
||||
+ if err != nil {
|
||||
+ m := &OAuthClaude{com: com, isOnboarding: isOnboarding, provider: provider, model: model, modelType: modelType, width: 60, state: oauthClaudeStateError}
|
||||
+ return m, util.ReportError(fmt.Errorf("failed to build auth URL: %w", err))
|
||||
+ }
|
||||
+
|
||||
+ t := com.Styles
|
||||
+ m := &OAuthClaude{
|
||||
+ com: com, isOnboarding: isOnboarding, provider: provider,
|
||||
+ model: model, modelType: modelType, width: 60,
|
||||
+ state: oauthClaudeStateInput, verifier: verifier, verificationURL: authURL,
|
||||
+ }
|
||||
+
|
||||
+ innerWidth := m.width - t.Dialog.View.GetHorizontalFrameSize() - 2
|
||||
+ m.input = textinput.New()
|
||||
+ m.input.SetVirtualCursor(false)
|
||||
+ m.input.Placeholder = "Paste authorization code..."
|
||||
+ m.input.SetStyles(com.Styles.TextInput)
|
||||
+ m.input.Focus()
|
||||
+ m.input.SetWidth(max(0, innerWidth-t.Dialog.InputPrompt.GetHorizontalFrameSize()-1))
|
||||
+ m.input.Prompt = "> "
|
||||
+
|
||||
+ m.spinner = spinner.New(
|
||||
+ spinner.WithSpinner(spinner.Dot),
|
||||
+ spinner.WithStyle(t.Dialog.APIKey.Spinner),
|
||||
+ )
|
||||
+ m.help = help.New()
|
||||
+ m.help.Styles = t.DialogHelpStyles()
|
||||
+
|
||||
+ m.keyMap.Submit = key.NewBinding(key.WithKeys("enter", "ctrl+y"), key.WithHelp("enter", "submit"))
|
||||
+ m.keyMap.Open = key.NewBinding(key.WithKeys("o"), key.WithHelp("o", "open browser"))
|
||||
+ m.keyMap.Close = CloseKey
|
||||
+
|
||||
+ return m, nil
|
||||
+}
|
||||
+
|
||||
+func (m *OAuthClaude) ID() string { return OAuthClaudeID }
|
||||
+
|
||||
+func (m *OAuthClaude) HandleMsg(msg tea.Msg) Action {
|
||||
+ switch msg := msg.(type) {
|
||||
+ case oauthClaudeComplete:
|
||||
+ err := m.com.Workspace.SetProviderAPIKey(config.ScopeGlobal, string(m.provider.ID), msg.token)
|
||||
+ if err != nil {
|
||||
+ return ActionCmd{util.ReportError(fmt.Errorf("failed to save token: %w", err))}
|
||||
+ }
|
||||
+ return ActionSelectModel{Provider: m.provider, Model: m.model, ModelType: m.modelType}
|
||||
+
|
||||
+ case oauthClaudeError:
|
||||
+ m.state = oauthClaudeStateError
|
||||
+ return ActionCmd{util.ReportError(msg.err)}
|
||||
+
|
||||
+ case spinner.TickMsg:
|
||||
+ if m.state == oauthClaudeStateExchanging {
|
||||
+ var cmd tea.Cmd
|
||||
+ m.spinner, cmd = m.spinner.Update(msg)
|
||||
+ if cmd != nil {
|
||||
+ return ActionCmd{cmd}
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ case tea.KeyPressMsg:
|
||||
+ switch {
|
||||
+ case m.state == oauthClaudeStateExchanging:
|
||||
+ // absorb input during exchange
|
||||
+ case key.Matches(msg, m.keyMap.Close):
|
||||
+ return ActionClose{}
|
||||
+ case key.Matches(msg, m.keyMap.Open) && m.state == oauthClaudeStateInput:
|
||||
+ _ = browser.OpenURL(m.verificationURL)
|
||||
+ case key.Matches(msg, m.keyMap.Submit) && m.state == oauthClaudeStateInput:
|
||||
+ code := strings.TrimSpace(m.input.Value())
|
||||
+ if code == "" {
|
||||
+ return nil
|
||||
+ }
|
||||
+ m.state = oauthClaudeStateExchanging
|
||||
+ verifier := m.verifier
|
||||
+ return ActionCmd{tea.Batch(m.spinner.Tick, func() tea.Msg {
|
||||
+ token, err := claude.ExchangeToken(context.Background(), code, verifier)
|
||||
+ if err != nil {
|
||||
+ return oauthClaudeError{err}
|
||||
+ }
|
||||
+ return oauthClaudeComplete{token}
|
||||
+ })}
|
||||
+ default:
|
||||
+ if m.state == oauthClaudeStateInput {
|
||||
+ var cmd tea.Cmd
|
||||
+ m.input, cmd = m.input.Update(msg)
|
||||
+ if cmd != nil {
|
||||
+ return ActionCmd{cmd}
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ case tea.PasteMsg:
|
||||
+ if m.state == oauthClaudeStateInput {
|
||||
+ var cmd tea.Cmd
|
||||
+ m.input, cmd = m.input.Update(msg)
|
||||
+ if cmd != nil {
|
||||
+ return ActionCmd{cmd}
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+func (m *OAuthClaude) Cursor() *tea.Cursor {
|
||||
+ return InputCursor(m.com.Styles, m.input.Cursor())
|
||||
+}
|
||||
+
|
||||
+func (m *OAuthClaude) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
|
||||
+ t := m.com.Styles
|
||||
+ dialogStyle := t.Dialog.View.Width(m.width)
|
||||
+ helpStyle := t.Dialog.HelpView.Width(m.width - dialogStyle.GetHorizontalFrameSize())
|
||||
+ textStyle := t.Dialog.SecondaryText
|
||||
+ inputStyle := t.Dialog.InputPrompt
|
||||
+ linkStyle := t.Dialog.OAuth.Link
|
||||
+
|
||||
+ if m.state == oauthClaudeStateExchanging {
|
||||
+ m.input.Prompt = m.spinner.View()
|
||||
+ } else {
|
||||
+ m.input.Prompt = "> "
|
||||
+ }
|
||||
+
|
||||
+ content := strings.Join([]string{
|
||||
+ m.headerView(),
|
||||
+ textStyle.Render("Visit the URL and paste the authorization code:"),
|
||||
+ "",
|
||||
+ textStyle.Render(linkStyle.Hyperlink(m.verificationURL, "id=oauth-claude").Render(m.verificationURL)),
|
||||
+ "",
|
||||
+ inputStyle.Render(m.input.View()),
|
||||
+ "",
|
||||
+ helpStyle.Render(m.help.View(m)),
|
||||
+ }, "\n")
|
||||
+
|
||||
+ cur := m.Cursor()
|
||||
+ if m.isOnboarding {
|
||||
+ cur = adjustOnboardingInputCursor(t, cur)
|
||||
+ DrawOnboardingCursor(scr, area, content, cur)
|
||||
+ } else {
|
||||
+ view := dialogStyle.Render(content)
|
||||
+ DrawCenterCursor(scr, area, view, cur)
|
||||
+ }
|
||||
+ return cur
|
||||
+}
|
||||
+
|
||||
+func (m *OAuthClaude) headerView() string {
|
||||
+ t := m.com.Styles
|
||||
+ titleStyle := t.Dialog.Title
|
||||
+ textStyle := t.Dialog.PrimaryText
|
||||
+ dialogStyle := t.Dialog.View.Width(m.width)
|
||||
+ title := fmt.Sprintf("Authenticate with %s", m.provider.Name)
|
||||
+ if m.isOnboarding {
|
||||
+ return textStyle.Render(title)
|
||||
+ }
|
||||
+ headerOffset := titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
|
||||
+ return common.DialogTitle(t, titleStyle.Render(title), m.width-headerOffset, t.Dialog.TitleGradFromColor, t.Dialog.TitleGradToColor)
|
||||
+}
|
||||
+
|
||||
+func (m *OAuthClaude) ShortHelp() []key.Binding {
|
||||
+ if m.state == oauthClaudeStateInput {
|
||||
+ return []key.Binding{m.keyMap.Open, m.keyMap.Submit, m.keyMap.Close}
|
||||
+ }
|
||||
+ return []key.Binding{m.keyMap.Close}
|
||||
+}
|
||||
+
|
||||
+func (m *OAuthClaude) FullHelp() [][]key.Binding {
|
||||
+ return [][]key.Binding{m.ShortHelp()}
|
||||
+}
|
||||
Reference in New Issue
Block a user