Merge branch 'main' into ui

Ayman Bagabas created

Change summary

.goreleaser.yml              |   5 -
go.mod                       |   5 +
go.sum                       |   6 ++
internal/cmd/login.go        | 108 ++++++++++++++++++++++++++++++++++++++
internal/cmd/root.go         |   8 +-
internal/config/config.go    |   2 
internal/stringext/string.go |  11 +++
internal/tui/tui.go          |  18 +++++-
schema.json                  |   1 
9 files changed, 149 insertions(+), 15 deletions(-)

Detailed changes

.goreleaser.yml 🔗

@@ -235,11 +235,10 @@ nfpms:
 
 signs:
   - cmd: cosign
-    certificate: "${artifact}.pem"
+    signature: "${artifact}.sigstore.json"
     args:
       - sign-blob
-      - "--output-certificate=${certificate}"
-      - "--output-signature=${signature}"
+      - "--bundle=${signature}"
       - "${artifact}"
       - "--yes"
     artifacts: checksum

go.mod 🔗

@@ -4,7 +4,7 @@ go 1.25.0
 
 require (
 	charm.land/bubbles/v2 v2.0.0-rc.1
-	charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251126220703-2a0096c500a7
+	charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251202162339-5fa38b798f16
 	charm.land/fantasy v0.3.2
 	charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca
 	charm.land/x/vcr v0.1.1
@@ -21,7 +21,7 @@ require (
 	github.com/charmbracelet/fang v0.4.4
 	github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930
 	github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0
-	github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38
+	github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b
 	github.com/charmbracelet/x/ansi v0.11.2
 	github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
 	github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
@@ -54,6 +54,7 @@ require (
 	github.com/stretchr/testify v1.11.1
 	github.com/tidwall/sjson v1.2.5
 	github.com/zeebo/xxh3 v1.0.2
+	golang.org/x/mod v0.30.0
 	golang.org/x/sync v0.18.0
 	golang.org/x/text v0.31.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1

go.sum 🔗

@@ -2,6 +2,8 @@ charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM
 charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
 charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251126220703-2a0096c500a7 h1:3qsObfEm0WuACFhe3MSTPX8QByjVcjWkZDO4o2VWFpc=
 charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251126220703-2a0096c500a7/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
+charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251202162339-5fa38b798f16 h1:9iVAss7WF8Ax5QBzmZE77aA08JbOMuDpbEhl/Uvc3Eo=
+charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251202162339-5fa38b798f16/go.mod h1:Vsh7/MLC7LQ2Ab8H63SXm6yD/L6o4HDvhdD/IrIRXrU=
 charm.land/fantasy v0.3.2 h1:yHTsSZ25LcICMRw3xzdz3OkaZtDQch+B5ljJo17HxgU=
 charm.land/fantasy v0.3.2/go.mod h1:sV8Ns/JTJHOaYOHPgVRDugMheAyxsW/nmdpVGrycYEk=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca h1:6bVc8OFotCS4sS7HKqxTudP7yn8Y0ODR6df2pdlY/+s=
@@ -100,6 +102,8 @@ github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0 h1:lxHzxsHd4P
 github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0/go.mod h1:Q7oMtlboDPnnrYiJDXNwdWmJblOmuOnycPKczlVju6I=
 github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 h1:7Rs87fbKJoIIxsQS8YKJYGYa0tlsDwwb0twQjV1KB+g=
 github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38/go.mod h1:6lfcr3MNP+kZR25sF1nQwJFuQnNYBlFy3PGX5rvslXc=
+github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b h1:jY1J0PcfetoB1uJ+w8rd86gUFSpKpJJI35gnfpKF5hg=
+github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
 github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
 github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50=
@@ -378,6 +382,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
+golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=

internal/cmd/login.go 🔗

@@ -0,0 +1,108 @@
+package cmd
+
+import (
+	"cmp"
+	"context"
+	"fmt"
+	"os"
+	"os/signal"
+	"strings"
+
+	"charm.land/lipgloss/v2"
+	"github.com/charmbracelet/crush/internal/config"
+	"github.com/charmbracelet/crush/internal/oauth/claude"
+	"github.com/spf13/cobra"
+)
+
+var loginCmd = &cobra.Command{
+	Aliases: []string{"auth"},
+	Use:     "login [platform]",
+	Short:   "Login Crush to a platform",
+	Long: `Login Crush to a specified platform.
+The platform should be provided as an argument.
+Available platforms are: claude.`,
+	Example: `
+# Authenticate with Claude Code Max
+crush login claude
+  `,
+	ValidArgs: []cobra.Completion{
+		"claude",
+		"anthropic",
+	},
+	Args: cobra.ExactArgs(1),
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) > 1 {
+			return fmt.Errorf("wrong number of arguments")
+		}
+		if len(args) == 0 || args[0] == "" {
+			return cmd.Help()
+		}
+
+		app, err := setupAppWithProgressBar(cmd)
+		if err != nil {
+			return err
+		}
+		defer app.Shutdown()
+
+		switch args[0] {
+		case "anthropic", "claude":
+			return loginClaude()
+		default:
+			return fmt.Errorf("unknown platform: %s", args[0])
+		}
+	},
+}
+
+func loginClaude() error {
+	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
+	go func() {
+		<-ctx.Done()
+		cancel()
+		os.Exit(1)
+	}()
+
+	verifier, challenge, err := claude.GetChallenge()
+	if err != nil {
+		return err
+	}
+	url, err := claude.AuthorizeURL(verifier, challenge)
+	if err != nil {
+		return err
+	}
+	fmt.Println("Open the following URL and follow the instructions to authenticate with Claude Code Max:")
+	fmt.Println()
+	fmt.Println(lipgloss.NewStyle().Hyperlink(url, "id=claude").Render(url))
+	fmt.Println()
+	fmt.Println("Press enter to continue...")
+	if _, err := fmt.Scanln(); err != nil {
+		return err
+	}
+
+	fmt.Println("Now paste and code from Anthropic and press enter...")
+	fmt.Println()
+	fmt.Print("> ")
+	var code string
+	for code == "" {
+		_, _ = fmt.Scanln(&code)
+		code = strings.TrimSpace(code)
+	}
+
+	fmt.Println()
+	fmt.Println("Exchanging authorization code...")
+	token, err := claude.ExchangeToken(ctx, code, verifier)
+	if err != nil {
+		return err
+	}
+
+	cfg := config.Get()
+	if err := cmp.Or(
+		cfg.SetConfigField("providers.anthropic.api_key", token.AccessToken),
+		cfg.SetConfigField("providers.anthropic.oauth", token),
+	); err != nil {
+		return err
+	}
+
+	fmt.Println()
+	fmt.Println("You're now authenticated with Claude Code Max!")
+	return nil
+}

internal/cmd/root.go 🔗

@@ -19,6 +19,7 @@ import (
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/db"
 	"github.com/charmbracelet/crush/internal/event"
+	"github.com/charmbracelet/crush/internal/stringext"
 	termutil "github.com/charmbracelet/crush/internal/term"
 	"github.com/charmbracelet/crush/internal/tui"
 	"github.com/charmbracelet/crush/internal/ui/common"
@@ -45,6 +46,7 @@ func init() {
 		updateProvidersCmd,
 		logsCmd,
 		schemaCmd,
+		loginCmd,
 	)
 }
 
@@ -276,9 +278,5 @@ func shouldQueryTerminalVersion(env uv.Environ) bool {
 	return (!okTermProg && !okSSHTTY) ||
 		(!strings.Contains(termProg, "Apple") && !okSSHTTY) ||
 		// Terminals that do support XTVERSION.
-		strings.Contains(termType, "ghostty") ||
-		strings.Contains(termType, "wezterm") ||
-		strings.Contains(termType, "alacritty") ||
-		strings.Contains(termType, "kitty") ||
-		strings.Contains(termType, "rio")
+		stringext.ContainsAny(termType, "alacritty", "ghostty", "kitty", "rio", "wezterm")
 }

internal/config/config.go 🔗

@@ -73,7 +73,7 @@ type SelectedModel struct {
 	Think bool `json:"think,omitempty" jsonschema:"description=Enable thinking mode for Anthropic models that support reasoning"`
 
 	// Overrides the default model configuration.
-	MaxTokens        int64    `json:"max_tokens,omitempty" jsonschema:"description=Maximum number of tokens for model responses,minimum=1,maximum=200000,example=4096"`
+	MaxTokens        int64    `json:"max_tokens,omitempty" jsonschema:"description=Maximum number of tokens for model responses,maximum=200000,example=4096"`
 	Temperature      *float64 `json:"temperature,omitempty" jsonschema:"description=Sampling temperature,minimum=0,maximum=1,example=0.7"`
 	TopP             *float64 `json:"top_p,omitempty" jsonschema:"description=Top-p (nucleus) sampling parameter,minimum=0,maximum=1,example=0.9"`
 	TopK             *int64   `json:"top_k,omitempty" jsonschema:"description=Top-k sampling parameter"`

internal/stringext/string.go 🔗

@@ -1,6 +1,8 @@
 package stringext
 
 import (
+	"strings"
+
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 )
@@ -8,3 +10,12 @@ import (
 func Capitalize(text string) string {
 	return cases.Title(language.English, cases.Compact).String(text)
 }
+
+func ContainsAny(str string, args ...string) bool {
+	for _, arg := range args {
+		if strings.Contains(str, arg) {
+			return true
+		}
+	}
+	return false
+}

internal/tui/tui.go 🔗

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"math/rand"
+	"regexp"
 	"slices"
 	"strings"
 	"time"
@@ -17,6 +18,7 @@ import (
 	"github.com/charmbracelet/crush/internal/event"
 	"github.com/charmbracelet/crush/internal/permission"
 	"github.com/charmbracelet/crush/internal/pubsub"
+	"github.com/charmbracelet/crush/internal/stringext"
 	cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
 	"github.com/charmbracelet/crush/internal/tui/components/chat/splash"
 	"github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -34,6 +36,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/page/chat"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
+	"golang.org/x/mod/semver"
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 )
@@ -120,10 +123,19 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			a.sendProgressBar = slices.Contains(msg, "WT_SESSION")
 		}
 	case tea.TerminalVersionMsg:
+		if a.sendProgressBar {
+			return a, nil
+		}
 		termVersion := strings.ToLower(msg.Name)
-		// Only enable progress bar for the following terminals.
-		if !a.sendProgressBar {
-			a.sendProgressBar = strings.Contains(termVersion, "ghostty")
+		switch {
+		case stringext.ContainsAny(termVersion, "ghostty", "rio"):
+			a.sendProgressBar = true
+		case strings.Contains(termVersion, "iterm2"):
+			// iTerm2 supports progress bars from version v3.6.6
+			matches := regexp.MustCompile(`^iterm2 (\d+\.\d+\.\d+)$`).FindStringSubmatch(termVersion)
+			if len(matches) == 2 && semver.Compare("v"+matches[1], "v3.6.6") >= 0 {
+				a.sendProgressBar = true
+			}
 		}
 		return a, nil
 	case tea.KeyboardEnhancementsMsg:

schema.json 🔗

@@ -555,7 +555,6 @@
         "max_tokens": {
           "type": "integer",
           "maximum": 200000,
-          "minimum": 1,
           "description": "Maximum number of tokens for model responses",
           "examples": [
             4096