diff --git a/.github/cla-signatures.json b/.github/cla-signatures.json index 12b86fb4c2936e871a1a022a150385b3744b23cf..4ba10c80f7a179932218e7d730709c22d2630212 100644 --- a/.github/cla-signatures.json +++ b/.github/cla-signatures.json @@ -719,6 +719,38 @@ "created_at": "2025-10-16T17:30:07Z", "repoId": 987670088, "pullRequestNo": 1245 + }, + { + "name": "dpolishuk", + "id": 466424, + "comment_id": 3418756045, + "created_at": "2025-10-18T19:24:00Z", + "repoId": 987670088, + "pullRequestNo": 1254 + }, + { + "name": "Jesssullivan", + "id": 37297218, + "comment_id": 3439361465, + "created_at": "2025-10-23T21:50:17Z", + "repoId": 987670088, + "pullRequestNo": 1292 + }, + { + "name": "mmangkad", + "id": 176301910, + "comment_id": 3440286180, + "created_at": "2025-10-24T01:32:47Z", + "repoId": 987670088, + "pullRequestNo": 1294 + }, + { + "name": "blouflab", + "id": 227565774, + "comment_id": 3444483981, + "created_at": "2025-10-24T18:59:01Z", + "repoId": 987670088, + "pullRequestNo": 1296 } ] } \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index aabf2f7606462ebb540fd6ebe9efb302a6855e5f..28539bc1681353065ea542a1e4de711a2d425585 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -303,6 +303,7 @@ changelog: - "^docs: update$" - "^test:" - "^test\\(" + - "^v\\d.*" - "merge conflict" - "merge conflict" - Merge branch diff --git a/README.md b/README.md index 7f28c5c049cdb6c45bc83ec59f94f4310c13b7c5..435c999d334a71187464670373b00effd23a8e1a 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,8 @@ That said, you can also set environment variables for preferred providers. | `AWS_ACCESS_KEY_ID` | AWS Bedrock (Claude) | | `AWS_SECRET_ACCESS_KEY` | AWS Bedrock (Claude) | | `AWS_REGION` | AWS Bedrock (Claude) | -| `AWS_PROFILE` | Custom AWS Profile | -| `AWS_REGION` | AWS Region | +| `AWS_PROFILE` | AWS Bedrock (Custom Profile) | +| `AWS_BEARER_TOKEN_BEDROCK` | AWS Bedrock | | `AZURE_OPENAI_API_ENDPOINT` | Azure OpenAI models | | `AZURE_OPENAI_API_KEY` | Azure OpenAI models (optional when using Entra ID) | | `AZURE_OPENAI_API_VERSION` | Azure OpenAI models | @@ -479,6 +479,7 @@ Crush currently supports running Anthropic models through Bedrock, with caching - A Bedrock provider will appear once you have AWS configured, i.e. `aws configure` - Crush also expects the `AWS_REGION` or `AWS_DEFAULT_REGION` to be set - To use a specific AWS profile set `AWS_PROFILE` in your environment, i.e. `AWS_PROFILE=myprofile crush` +- Alternatively to `aws configure`, you can also just set `AWS_BEARER_TOKEN_BEDROCK` ### Vertex AI Platform @@ -649,8 +650,8 @@ See the [contributing guide](https://github.com/charmbracelet/crush?tab=contribu We’d love to hear your thoughts on this project. Need help? We gotchu. You can find us on: - [Twitter](https://twitter.com/charmcli) -- [Discord][discord] - [Slack](https://charm.land/slack) +- [Discord][discord] - [The Fediverse](https://mastodon.social/@charmcli) - [Bluesky](https://bsky.app/profile/charm.land) diff --git a/Taskfile.yaml b/Taskfile.yaml index 65a7e2d42fe8dcb307bced7c1fa9a0326b35ccc1..9e0f214fb1f7081ffb90d53e7a62eab120950b0d 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -2,6 +2,10 @@ version: "3" +vars: + VERSION: + sh: git describe --long 2>/dev/null || echo "" + env: CGO_ENABLED: 0 GOEXPERIMENT: greenteagc @@ -30,8 +34,10 @@ tasks: build: desc: Run build + vars: + LDFLAGS: '{{if .VERSION}}-ldflags="-X github.com/charmbracelet/crush/internal/version.Version={{.VERSION}}"{{end}}' cmds: - - go build . + - go build {{.LDFLAGS}} . generates: - crush @@ -59,8 +65,10 @@ tasks: install: desc: Install the application + vars: + LDFLAGS: '{{if .VERSION}}-ldflags="-X github.com/charmbracelet/crush/internal/version.Version={{.VERSION}}"{{end}}' cmds: - - go install -v . + - go install {{.LDFLAGS}} -v . profile:cpu: desc: 10s CPU profile diff --git a/go.mod b/go.mod index c0bc32fe29ac100f98c589edf7697f104aa854a5..27ca8d5dbfd8a6f4511390234f38bab625581db1 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,14 @@ require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/PuerkitoBio/goquery v1.10.3 github.com/alecthomas/chroma/v2 v2.20.0 - github.com/anthropics/anthropic-sdk-go v1.13.0 github.com/atotto/clipboard v0.1.4 github.com/aymanbagabas/go-udiff v0.3.1 github.com/bmatcuk/doublestar/v4 v4.9.1 github.com/charlievieth/fastwalk v1.0.14 + github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 - github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619 - github.com/charmbracelet/catwalk v0.6.4 + github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5 + github.com/charmbracelet/catwalk v0.7.0 github.com/charmbracelet/fang v0.4.3 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea @@ -74,7 +74,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/colorprofile v0.3.2 - github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef + github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250829135019-44e44e21330d github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4 @@ -148,12 +148,12 @@ require ( golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.37.0 // indirect golang.org/x/term v0.35.0 // indirect golang.org/x/text v0.30.0 golang.org/x/time v0.8.0 // indirect google.golang.org/api v0.211.0 // indirect - google.golang.org/genai v1.30.0 + google.golang.org/genai v1.31.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.8 // indirect diff --git a/go.sum b/go.sum index 0fa4e9f695cf5d60a60be753aaee9a0b2e14c192..f390f506f9b0c12d7597c3ee84efaff2ce986b02 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,6 @@ github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW5 github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/anthropics/anthropic-sdk-go v1.13.0 h1:Bhbe8sRoDPtipttg8bQYrMCKe2b79+q6rFW1vOKEUKI= -github.com/anthropics/anthropic-sdk-go v1.13.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= @@ -76,12 +74,14 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg= github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY= +github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 h1:rwLdEpG9wE6kL69KkEKDiWprO8pQOZHZXeod6+9K+mw= +github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 h1:973OHYuq2Jx9deyuPwe/6lsuQrDCatOsjP8uCd02URE= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619 h1:hjOhtqsxa+LVuCAkzhfA43wtusOaUPyQdSTg/wbRscw= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619/go.mod h1:5IzIGXU1n0foRc8bRAherC8ZuQCQURPlwx3ANLq1138= -github.com/charmbracelet/catwalk v0.6.4 h1:zFHtuP94mSDE48nST3DS3a37wfsQqNcVnsFkS3v6N6E= -github.com/charmbracelet/catwalk v0.6.4/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5 h1:oAChAeh730gtLKK/BpaTeJHzmj3KFuEfQ7AZgf2VGHM= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5/go.mod h1:SUTLq+/pGQ5qntHgt0JswfVJFfgJgWDqyvyiSLVlmbo= +github.com/charmbracelet/catwalk v0.7.0 h1:qhLv56aeel5Q+2G/YFh9k5FhTqsozsn4HYViuAQ/Rio= +github.com/charmbracelet/catwalk v0.7.0/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY= @@ -92,8 +92,8 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM= -github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M= -github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef/go.mod h1:AThRsQH1t+dfyOKIwXRoJBniYFQUkUpQq4paheHMc2o= +github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 h1:Lr+igmzKpLPdb8yUZBP9noYWwCZP042z2nWPrJZTc+8= +github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731/go.mod h1:KfWwUa0Oe//D72YlhbOq/g40L7UiGtATrvsGI3cciG8= github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a h1:zYSNtEJM9jwHbJts2k+Hroj+xQwsW1yxc4Wopdv7KaI= @@ -388,8 +388,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -427,8 +427,8 @@ golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg= google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0= -google.golang.org/genai v1.30.0 h1:7021aneIvl24nEBLbtQFEWleHsMbjzpcQvkT4WcJ1dc= -google.golang.org/genai v1.30.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg= +google.golang.org/genai v1.31.0 h1:R7xDt/Dosz11vcXbZ4IgisGnzUGGau2PZOIOAnXsYjw= +google.golang.org/genai v1.31.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index e7160f4a1307406be20f1fe00a59e93de5232d67..4372083189701e1410c83690c18fbd371f778169 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -10,8 +10,10 @@ import ( "slices" "time" + "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/log/v2" + "github.com/charmbracelet/x/term" "github.com/nxadm/tail" "github.com/spf13/cobra" ) @@ -45,6 +47,9 @@ var logsCmd = &cobra.Command{ log.SetLevel(log.DebugLevel) log.SetOutput(os.Stdout) + if !term.IsTerminal(os.Stdout.Fd()) { + log.SetColorProfile(colorprofile.NoTTY) + } cfg, err := config.Load(cwd, dataDir, false) if err != nil { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index d6a26d818643a05704f554223a7b7960792970c5..005f2e86f7012b265fb619580c7cc2eec2e4de03 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -83,11 +83,8 @@ crush -y // Set up the TUI. program := tea.NewProgram( tui.New(app), - tea.WithAltScreen(), tea.WithContext(cmd.Context()), - tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding - tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state - ) + tea.WithFilter(tui.MouseEventFilter)) // Filter mouse events based on focus state go app.Subscribe(program) diff --git a/internal/config/load.go b/internal/config/load.go index c63a9663613bdfdea6a9c9ccef9f53d375e35c74..a219b7d1c848b3eea76809fca96e8e4049838365 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -589,6 +589,10 @@ func hasVertexCredentials(env env.Env) bool { } func hasAWSCredentials(env env.Env) bool { + if env.Get("AWS_BEARER_TOKEN_BEDROCK") != "" { + return true + } + if env.Get("AWS_ACCESS_KEY_ID") != "" && env.Get("AWS_SECRET_ACCESS_KEY") != "" { return true } diff --git a/internal/config/provider.go b/internal/config/provider.go index 671c348f71da3a79f65c14c624bdaf2adc011411..108d6a667794e4f8f1beebe6997848a65d8fd6e6 100644 --- a/internal/config/provider.go +++ b/internal/config/provider.go @@ -126,7 +126,7 @@ func Providers(cfg *Config) ([]catwalk.Provider, error) { } func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) ([]catwalk.Provider, error) { - cacheIsStale, cacheExists := isCacheStale(path) + _, cacheExists := isCacheStale(path) catwalkGetAndSave := func() ([]catwalk.Provider, error) { providers, err := client.GetProviders() @@ -142,25 +142,6 @@ func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) return providers, nil } - backgroundCacheUpdate := func() { - go func() { - slog.Info("Updating providers cache in background", "path", path) - - providers, err := client.GetProviders() - if err != nil { - slog.Error("Failed to fetch providers in background from Catwalk", "error", err) - return - } - if len(providers) == 0 { - slog.Error("Empty providers list from Catwalk") - return - } - if err := saveProvidersInCache(path, providers); err != nil { - slog.Error("Failed to update providers.json in background", "error", err) - } - }() - } - switch { case autoUpdateDisabled: slog.Warn("Providers auto-update is disabled") @@ -177,19 +158,6 @@ func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) } return providers, nil - case cacheExists && !cacheIsStale: - slog.Info("Recent providers cache is available.", "path", path) - - providers, err := loadProvidersFromCache(path) - if err != nil { - return nil, err - } - if len(providers) == 0 { - return catwalkGetAndSave() - } - backgroundCacheUpdate() - return providers, nil - default: slog.Info("Cache is not available or is stale. Fetching providers from Catwalk.", "path", path) diff --git a/internal/config/provider_test.go b/internal/config/provider_test.go index 8b499919bca666915a89d38c1e5014a911f4d2d1..1262b60ef42050b9061c9f7c6be4dc431efe3548 100644 --- a/internal/config/provider_test.go +++ b/internal/config/provider_test.go @@ -57,7 +57,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) { if err != nil { t.Fatalf("Failed to write old providers to file: %v", err) } - providers, err := loadProviders(false, client, tmpPath) + providers, err := loadProviders(true, client, tmpPath) require.NoError(t, err) require.NotNil(t, providers) require.Len(t, providers, 1) diff --git a/internal/format/spinner.go b/internal/format/spinner.go index ebaa47b6e175356052b4206f80a8bc11988df65d..ddbea302b2fae4089b48b73436da131798645637 100644 --- a/internal/format/spinner.go +++ b/internal/format/spinner.go @@ -23,8 +23,8 @@ type model struct { anim anim.Spinner } -func (m model) Init() tea.Cmd { return m.anim.Init() } -func (m model) View() string { return m.anim.View() } +func (m model) Init() tea.Cmd { return m.anim.Init() } +func (m model) View() tea.View { return tea.NewView(m.anim.View()) } // Update implements tea.Model. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index b2b222db1a481b1eb4c7e945467bd5c74506d5ab..6825da22ac13dd107731abb1a506b49bec8a5271 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -535,6 +535,10 @@ func (a *agent) getAllTools() ([]tools.BaseTool, error) { } allTools = append(allTools, agentTool) } + + slices.SortFunc(allTools, func(a, b tools.BaseTool) int { + return strings.Compare(a.Name(), b.Name()) + }) return allTools, nil } diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index 981ff4590fd7db92288ff11b3d8f607e594cb0fd..1f1965fde3fd04dad759f368ce2e232543f86c8e 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -13,10 +13,10 @@ import ( "strings" "time" - "github.com/anthropics/anthropic-sdk-go" - "github.com/anthropics/anthropic-sdk-go/bedrock" - "github.com/anthropics/anthropic-sdk-go/option" - "github.com/anthropics/anthropic-sdk-go/vertex" + "github.com/charmbracelet/anthropic-sdk-go" + "github.com/charmbracelet/anthropic-sdk-go/bedrock" + "github.com/charmbracelet/anthropic-sdk-go/option" + "github.com/charmbracelet/anthropic-sdk-go/vertex" "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/llm/tools" diff --git a/internal/llm/tools/diagnostics.go b/internal/llm/tools/diagnostics.go index c2625e9495963f1de467656b2d74e71e0b3c78fa..3418a08e5b13d3a7a795fe0a5872d4c62899ff7f 100644 --- a/internal/llm/tools/diagnostics.go +++ b/internal/llm/tools/diagnostics.go @@ -122,7 +122,7 @@ func getDiagnostics(filePath string, lsps *csync.Map[string, *lsp.Client]) strin } out := output.String() - slog.Info("Diagnostics", "output", fmt.Sprintf("%q", out)) + slog.Info("Diagnostics", "output", out) return out } diff --git a/internal/log/http.go b/internal/log/http.go index 46c4b42af599f1809478a5c3f083c6249a3e13d0..a4564ffdc50335e3944c44ccf0a9a562e2f6454a 100644 --- a/internal/log/http.go +++ b/internal/log/http.go @@ -39,12 +39,14 @@ func (h *HTTPRoundTripLogger) RoundTrip(req *http.Request) (*http.Response, erro return nil, err } - slog.Debug( - "HTTP Request", - "method", req.Method, - "url", req.URL, - "body", bodyToString(save), - ) + if slog.Default().Enabled(req.Context(), slog.LevelDebug) { + slog.Debug( + "HTTP Request", + "method", req.Method, + "url", req.URL, + "body", bodyToString(save), + ) + } start := time.Now() resp, err := h.Transport.RoundTrip(req) @@ -61,16 +63,18 @@ func (h *HTTPRoundTripLogger) RoundTrip(req *http.Request) (*http.Response, erro } save, resp.Body, err = drainBody(resp.Body) - slog.Debug( - "HTTP Response", - "status_code", resp.StatusCode, - "status", resp.Status, - "headers", formatHeaders(resp.Header), - "body", bodyToString(save), - "content_length", resp.ContentLength, - "duration_ms", duration.Milliseconds(), - "error", err, - ) + if slog.Default().Enabled(req.Context(), slog.LevelDebug) { + slog.Debug( + "HTTP Response", + "status_code", resp.StatusCode, + "status", resp.Status, + "headers", formatHeaders(resp.Header), + "body", bodyToString(save), + "content_length", resp.ContentLength, + "duration_ms", duration.Milliseconds(), + "error", err, + ) + } return resp, err } @@ -84,7 +88,7 @@ func bodyToString(body io.ReadCloser) string { return "" } var b bytes.Buffer - if json.Compact(&b, bytes.TrimSpace(src)) != nil { + if json.Indent(&b, bytes.TrimSpace(src), "", " ") != nil { // not json probably return string(src) } diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 8688f7e24c94290c74ae4344499acff61b43ac39..aaaf683494a8dd1608d9ebae4f07dae6037def26 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -101,7 +101,7 @@ func (m *messageListCmp) Init() tea.Cmd { } // Update handles incoming messages and updates the component state. -func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd if m.session.ID != "" && m.app.CoderAgent != nil { queueSize := m.app.CoderAgent.QueuedPrompts(m.session.ID) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index f70a0a3dbe63a9473f552efa233e03bd4efc0ee1..92c6bea70c3e43af1b92f03c30ba3e15af0f5e4d 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -86,6 +86,7 @@ var DeleteKeyMaps = DeleteAttachmentKeyMaps{ const ( maxAttachments = 5 + maxFileResults = 25 ) type OpenEditorMsg struct { @@ -171,7 +172,7 @@ func (m *editorCmp) repositionCompletions() tea.Msg { return completions.RepositionCompletionsMsg{X: x, Y: y} } -func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { @@ -500,6 +501,7 @@ func (m *editorCmp) startCompletions() tea.Msg { Completions: completionItems, X: x, Y: y, + MaxResults: maxFileResults, } } diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index 21861a4a2eda1340f6e01c0748f24cb713f15398..6bea86690b4ffe813799ad6e1ba01359562ed791 100644 --- a/internal/tui/components/chat/header/header.go +++ b/internal/tui/components/chat/header/header.go @@ -44,7 +44,7 @@ func (h *header) Init() tea.Cmd { return nil } -func (h *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (h *header) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case pubsub.Event[session.Session]: if msg.Type == pubsub.UpdatedEvent { diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index 0e579db33402d249773e2295cca19e1e688c29c3..d7379e47f5dd7a85e2db30a0a14b91deaed7ae8c 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -35,7 +35,7 @@ var ClearSelectionKey = key.NewBinding(key.WithKeys("esc", "alt+esc"), key.WithH // MessageCmp defines the interface for message components in the chat interface. // It combines standard UI model interfaces with message-specific functionality. type MessageCmp interface { - util.Model // Basic Bubble Tea model interface + util.Model // Basic Bubble util.Model interface layout.Sizeable // Width/height management layout.Focusable // Focus state management GetMessage() message.Message // Access to underlying message data @@ -95,7 +95,7 @@ func (m *messageCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. // Manages animation updates for spinning messages and stops animation when appropriate. -func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *messageCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case anim.StepMsg: m.spinning = m.shouldSpin() @@ -385,7 +385,7 @@ func (m *assistantSectionModel) Init() tea.Cmd { return nil } -func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (m *assistantSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) { return m, nil } diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index cb7e648efc01b27b3ca0805859960d91050a4a4c..8e03071d9b003231853fb18030ea0a2614b7f35d 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -27,7 +27,7 @@ import ( // ToolCallCmp defines the interface for tool call components in the chat interface. // It manages the display of tool execution including pending states, results, and errors. type ToolCallCmp interface { - util.Model // Basic Bubble Tea model interface + util.Model // Basic Bubble util.Model interface layout.Sizeable // Width/height management layout.Focusable // Focus state management GetToolCall() message.ToolCall // Access to tool call data @@ -149,7 +149,7 @@ func (m *toolCallCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. // Manages animation updates for pending tool calls. -func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *toolCallCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case anim.StepMsg: var cmds []tea.Cmd diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index b50a78c7f8697e4f4db19649a01794cfe7a23bac..28808e0a8e57df881263d2fb90d25dfe8d02b83b 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -88,7 +88,7 @@ func (m *sidebarCmp) Init() tea.Cmd { return nil } -func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *sidebarCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case SessionFilesMsg: m.files = csync.NewMap[string, SessionFile]() diff --git a/internal/tui/components/chat/splash/splash.go b/internal/tui/components/chat/splash/splash.go index 187fc35e6ec47a858b99f35e135a8cef3500fbf1..e6446a981754665ba32beca48dc4a395addc5b93 100644 --- a/internal/tui/components/chat/splash/splash.go +++ b/internal/tui/components/chat/splash/splash.go @@ -135,7 +135,7 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd { } // Update implements SplashPage. -func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: return s, s.SetSize(msg.Width, msg.Height) diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index ae3c233e4f21b089f59b7effb88ddc3300277d16..93c1b6498f418c23a17ef0738d5748e25d04a685 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -22,6 +22,7 @@ type OpenCompletionsMsg struct { Completions []Completion X int // X position for the completions popup Y int // Y position for the completions popup + MaxResults int // Maximum number of results to render, 0 for no limit } type FilterCompletionsMsg struct { @@ -111,7 +112,7 @@ func (c *completionsCmp) Init() tea.Cmd { } // Update implements Completions. -func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *completionsCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth, c.wHeight = msg.Width, msg.Height @@ -192,6 +193,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } c.width = width c.height = max(min(maxCompletionsHeight, len(items)), 1) // Ensure at least 1 item height + c.list.SetResultsSize(msg.MaxResults) return c, tea.Batch( c.list.SetItems(items), c.list.SetSize(c.width, c.height), diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index b01873a22b18f87d798757bb5a6ba799ae0e7a81..effbaac9d48c8600c2b9b0e7dce94b9bbf5b429b 100644 --- a/internal/tui/components/core/status/status.go +++ b/internal/tui/components/core/status/status.go @@ -36,7 +36,7 @@ func (m *statusCmp) Init() tea.Cmd { return nil } -func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *statusCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width diff --git a/internal/tui/components/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go index 72677bc934864970c2cbded87b31853ad702a6ed..66ad3f7ba06ae41fa2a4d0e033906ceda5298c22 100644 --- a/internal/tui/components/dialogs/commands/arguments.go +++ b/internal/tui/components/dialogs/commands/arguments.go @@ -92,7 +92,7 @@ func (c *commandArgumentsDialogCmp) Init() tea.Cmd { } // Update implements CommandArgumentsDialog. -func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 664158fc392a87d8a7725bfa964748f7ef4f8e67..d05ec8fea44415ab83158849319cde62f96ef329 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -116,7 +116,7 @@ func (c *commandDialogCmp) Init() tea.Cmd { return c.SetCommandType(c.commandType) } -func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/compact/compact.go b/internal/tui/components/dialogs/compact/compact.go index ecde402fd8dfe1f31791834cd4e4bae13ec45e00..12cea72b55b4b3ad4f11c2f756ad7961ba3c8f87 100644 --- a/internal/tui/components/dialogs/compact/compact.go +++ b/internal/tui/components/dialogs/compact/compact.go @@ -61,7 +61,7 @@ func (c *compactDialogCmp) Init() tea.Cmd { return nil } -func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *compactDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/dialogs.go b/internal/tui/components/dialogs/dialogs.go index 99e14e51fdd271a9cee0c27528c7608ea28fa24e..d5ad83c160e0e618e637dabe2b5e297ff0c1cd65 100644 --- a/internal/tui/components/dialogs/dialogs.go +++ b/internal/tui/components/dialogs/dialogs.go @@ -32,7 +32,7 @@ type CloseDialogMsg struct{} // DialogCmp manages a stack of dialogs with keyboard navigation. type DialogCmp interface { - tea.Model + util.Model Dialogs() []DialogModel HasDialogs() bool @@ -62,7 +62,7 @@ func (d dialogCmp) Init() tea.Cmd { } // Update handles dialog lifecycle and forwards messages to the active dialog. -func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (d dialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: var cmds []tea.Cmd @@ -98,7 +98,11 @@ func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return d, nil } -func (d dialogCmp) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) { +func (d dialogCmp) View() string { + return "" +} + +func (d dialogCmp) handleOpen(msg OpenDialogMsg) (util.Model, tea.Cmd) { if d.HasDialogs() { dialog := d.dialogs[len(d.dialogs)-1] if dialog.ID() == msg.Model.ID() { diff --git a/internal/tui/components/dialogs/filepicker/filepicker.go b/internal/tui/components/dialogs/filepicker/filepicker.go index fcec2fc8b6e3e606e555c55949049f397a30f921..85a391ce5ceba7689148fbdcd016b73c1e100f54 100644 --- a/internal/tui/components/dialogs/filepicker/filepicker.go +++ b/internal/tui/components/dialogs/filepicker/filepicker.go @@ -88,7 +88,7 @@ func (m *model) Init() tea.Cmd { return m.filePicker.Init() } -func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *model) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/models/apikey.go b/internal/tui/components/dialogs/models/apikey.go index 0490335f9ad745839a94de0460a0fc5c1b6f125c..1c4ee0c14a77e2006d2bd43e40947b6852fa1736 100644 --- a/internal/tui/components/dialogs/models/apikey.go +++ b/internal/tui/components/dialogs/models/apikey.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/home" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" ) @@ -75,7 +76,7 @@ func (a *APIKeyInput) Init() tea.Cmd { return a.spinner.Tick } -func (a *APIKeyInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (a *APIKeyInput) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case spinner.TickMsg: if a.state == APIKeyInputStateVerifying { diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index 7c2863706c29180cffcfb88c385a012e39df464c..2e0b68cc3640c9ee5ed411eb10a07e9dc3bc0635 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -98,7 +98,7 @@ func (m *modelDialogCmp) Init() tea.Cmd { return tea.Batch(m.modelList.Init(), m.apiKeyInput.Init()) } -func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/permissions/keys.go b/internal/tui/components/dialogs/permissions/keys.go index 4b7660ceb2310595fc0ad7d1ce51dade83169035..fc1810fc582dc4c25cada280b00b3f9515e43008 100644 --- a/internal/tui/components/dialogs/permissions/keys.go +++ b/internal/tui/components/dialogs/permissions/keys.go @@ -42,7 +42,7 @@ func DefaultKeyMap() KeyMap { key.WithHelp("s", "allow session"), ), Deny: key.NewBinding( - key.WithKeys("d", "D", "ctrl+d", "esc"), + key.WithKeys("d", "D", "esc"), key.WithHelp("d", "deny"), ), Select: key.NewBinding( diff --git a/internal/tui/components/dialogs/permissions/permissions.go b/internal/tui/components/dialogs/permissions/permissions.go index 9e0a6b05d7385c354f8faba3110b1c0951f9a97d..7705edd394bd91466220326c474ea2b8ef55ffc7 100644 --- a/internal/tui/components/dialogs/permissions/permissions.go +++ b/internal/tui/components/dialogs/permissions/permissions.go @@ -95,7 +95,7 @@ func (p *permissionDialogCmp) supportsDiffView() bool { return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName || p.permission.ToolName == tools.MultiEditToolName } -func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (p *permissionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { diff --git a/internal/tui/components/dialogs/quit/quit.go b/internal/tui/components/dialogs/quit/quit.go index 763dc842d386a072176e1a26741d8b68c1e2993b..a8857104550886abc5f70956bf384ab2df6ec302 100644 --- a/internal/tui/components/dialogs/quit/quit.go +++ b/internal/tui/components/dialogs/quit/quit.go @@ -40,7 +40,7 @@ func (q *quitDialogCmp) Init() tea.Cmd { } // Update handles keyboard input for the quit dialog. -func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (q *quitDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: q.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/reasoning/reasoning.go b/internal/tui/components/dialogs/reasoning/reasoning.go index ba49abd8c58a0e7eb84235e7b68f5f5193a96b1b..81f521c4bd31daa25fad5ccfb127a80ea2f20eba 100644 --- a/internal/tui/components/dialogs/reasoning/reasoning.go +++ b/internal/tui/components/dialogs/reasoning/reasoning.go @@ -172,7 +172,7 @@ func (r *reasoningDialogCmp) populateEffortOptions() tea.Cmd { return nil } -func (r *reasoningDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (r *reasoningDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: r.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/sessions/sessions.go b/internal/tui/components/dialogs/sessions/sessions.go index 037eb5ebb727a24b8ab9bfda2e2c72943120e819..7f01f3ba4dacfe408fed0e8f5a2f34b39d8b2edd 100644 --- a/internal/tui/components/dialogs/sessions/sessions.go +++ b/internal/tui/components/dialogs/sessions/sessions.go @@ -81,7 +81,7 @@ func (s *sessionDialogCmp) Init() tea.Cmd { return tea.Sequence(cmds...) } -func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: var cmds []tea.Cmd diff --git a/internal/tui/exp/list/filterable.go b/internal/tui/exp/list/filterable.go index e639786db5777aaeda237e959dffe36d9c6a7583..b93f8cc3309f66fb957c40e0d6b25419ef51d4e7 100644 --- a/internal/tui/exp/list/filterable.go +++ b/internal/tui/exp/list/filterable.go @@ -3,14 +3,13 @@ package list import ( "regexp" "slices" - "sort" - "strings" "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/textinput" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/sahilm/fuzzy" ) @@ -28,7 +27,9 @@ type FilterableList[T FilterableItem] interface { Cursor() *tea.Cursor SetInputWidth(int) SetInputPlaceholder(string) + SetResultsSize(int) Filter(q string) tea.Cmd + fuzzy.Source } type HasMatchIndexes interface { @@ -47,10 +48,11 @@ type filterableList[T FilterableItem] struct { *filterableOptions width, height int // stores all available items - items []T - input textinput.Model - inputWidth int - query string + items []T + resultsSize int + input textinput.Model + inputWidth int + query string } type filterableListOption func(*filterableOptions) @@ -115,7 +117,7 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption return f } -func (f *filterableList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (f *filterableList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch { @@ -246,22 +248,18 @@ func (f *filterableList[T]) Filter(query string) tea.Cmd { return f.list.SetItems(f.items) } - words := make([]string, len(f.items)) - for i, item := range f.items { - words[i] = strings.ToLower(item.FilterValue()) - } - - matches := fuzzy.Find(query, words) - - sort.SliceStable(matches, func(i, j int) bool { - return matches[i].Score > matches[j].Score - }) + matches := fuzzy.FindFrom(query, f) var matchedItems []T - for _, match := range matches { + resultSize := len(matches) + if f.resultsSize > 0 && resultSize > f.resultsSize { + resultSize = f.resultsSize + } + for i := range resultSize { + match := matches[i] item := f.items[match.Index] - if i, ok := any(item).(HasMatchIndexes); ok { - i.MatchIndexes(match.MatchedIndexes) + if it, ok := any(item).(HasMatchIndexes); ok { + it.MatchIndexes(match.MatchedIndexes) } matchedItems = append(matchedItems, item) } @@ -307,3 +305,15 @@ func (f *filterableList[T]) SetInputWidth(w int) { func (f *filterableList[T]) SetInputPlaceholder(ph string) { f.placeholder = ph } + +func (f *filterableList[T]) SetResultsSize(size int) { + f.resultsSize = size +} + +func (f *filterableList[T]) String(i int) string { + return f.items[i].FilterValue() +} + +func (f *filterableList[T]) Len() int { + return len(f.items) +} diff --git a/internal/tui/exp/list/filterable_group.go b/internal/tui/exp/list/filterable_group.go index 6e9a5dc7eaad66d32ec34baf7e41d35ab3233048..15084cce28be5190367eba861a491231139af53f 100644 --- a/internal/tui/exp/list/filterable_group.go +++ b/internal/tui/exp/list/filterable_group.go @@ -11,6 +11,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/sahilm/fuzzy" ) @@ -65,7 +66,7 @@ func NewFilterableGroupedList[T FilterableItem](items []Group[T], opts ...filter return f } -func (f *filterableGroupList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (f *filterableGroupList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch { diff --git a/internal/tui/exp/list/grouped.go b/internal/tui/exp/list/grouped.go index cb54628a70e84cb80eeb162a0d9f836f14271641..43223602dbfbeaa0ae60d0368b95a4f455228f96 100644 --- a/internal/tui/exp/list/grouped.go +++ b/internal/tui/exp/list/grouped.go @@ -58,7 +58,7 @@ func (g *groupedList[T]) Init() tea.Cmd { return g.render() } -func (l *groupedList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (l *groupedList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { u, cmd := l.list.Update(msg) l.list = u.(*list[Item]) return l, cmd diff --git a/internal/tui/exp/list/items.go b/internal/tui/exp/list/items.go index 9e7259dc10a61c95e970d9f1fc93b0d61d7a65a8..143908d5416be744424cc30965b8d663ca2a2c68 100644 --- a/internal/tui/exp/list/items.go +++ b/internal/tui/exp/list/items.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/ansi" "github.com/google/uuid" @@ -97,7 +98,7 @@ func (c *completionItemCmp[T]) Init() tea.Cmd { } // Update implements CommandItem. -func (c *completionItemCmp[T]) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (c *completionItemCmp[T]) Update(tea.Msg) (util.Model, tea.Cmd) { return c, nil } @@ -348,7 +349,7 @@ func (m *itemSectionModel) Init() tea.Cmd { return nil } -func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (m *itemSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) { return m, nil } diff --git a/internal/tui/exp/list/list.go b/internal/tui/exp/list/list.go index e18b88348959c59190f1741698f76c33f04571db..ea04b0c7d640f7801ba320d26071e21eafbbd90c 100644 --- a/internal/tui/exp/list/list.go +++ b/internal/tui/exp/list/list.go @@ -217,7 +217,7 @@ func (l *list[T]) Init() tea.Cmd { } // Update implements List. -func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (l *list[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.MouseWheelMsg: if l.enableMouse { @@ -277,7 +277,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return l, nil } -func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) { +func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (util.Model, tea.Cmd) { var cmd tea.Cmd switch msg.Button { case tea.MouseWheelDown: diff --git a/internal/tui/exp/list/list_test.go b/internal/tui/exp/list/list_test.go index 63cfa599e8ce1c96aad1cae67243caa2b097ee0b..4e6d8e3110d8c585b26293b7ef1f1e80e06c8b50 100644 --- a/internal/tui/exp/list/list_test.go +++ b/internal/tui/exp/list/list_test.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/exp/golden" "github.com/google/uuid" @@ -602,7 +603,7 @@ func (s *simpleItem) Init() tea.Cmd { return nil } -func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *simpleItem) Update(msg tea.Msg) (util.Model, tea.Cmd) { return s, nil } @@ -644,7 +645,7 @@ func (s *selectableItem) IsFocused() bool { return s.focused } -func execCmd(m tea.Model, cmd tea.Cmd) { +func execCmd(m util.Model, cmd tea.Cmd) { for cmd != nil { msg := cmd() m, cmd = m.Update(msg) diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index 2918925068cb2f012bead47bbf44260c6255288c..1559d314d052d118019797f85eedd91a7e0f6d00 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -163,7 +163,7 @@ func (p *chatPage) Init() tea.Cmd { ) } -func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyboardEnhancementsMsg: diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 26d23f46ee62aafe07d1bb6209a4fedea929c6e1..efaf1dbb9431bc3a69fb08278b65f9b34ac281fa 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -91,8 +91,6 @@ func (a appModel) Init() tea.Cmd { cmd = a.status.Init() cmds = append(cmds, cmd) - cmds = append(cmds, tea.EnableMouseAllMotion) - return tea.Batch(cmds...) } @@ -106,9 +104,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyboardEnhancementsMsg: for id, page := range a.pages { m, pageCmd := page.Update(msg) - if model, ok := m.(util.Model); ok { - a.pages[id] = model - } + a.pages[id] = m if pageCmd != nil { cmds = append(cmds, pageCmd) @@ -232,9 +228,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Forward to view. updated, itemCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated return a, itemCmd case pubsub.Event[permission.PermissionRequest]: @@ -292,9 +286,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.isConfigured = config.HasInitialDataConfig() updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) return a, tea.Batch(cmds...) @@ -314,9 +306,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) } @@ -336,9 +326,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) } @@ -353,9 +341,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, cmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated if a.dialog.HasDialogs() { u, dialogCmd := a.dialog.Update(msg) @@ -391,9 +377,7 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd { // Update the current view. for p, page := range a.pages { updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height}) - if model, ok := updated.(util.Model); ok { - a.pages[p] = model - } + a.pages[p] = updated cmds = append(cmds, pageCmd) } @@ -496,9 +480,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { } updated, cmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated return cmd } } @@ -602,6 +584,8 @@ func (a *appModel) View() tea.View { view.Layer = canvas view.Cursor = cursor + view.MouseMode = tea.MouseModeCellMotion + view.AltScreen = true if a.app != nil && a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() { // HACK: use a random percentage to prevent ghostty from hiding it // after a timeout. diff --git a/internal/tui/util/util.go b/internal/tui/util/util.go index eb19ad89544b281af2e836f667ac63aaa6414e01..c3ce1dbf7ad94cc89def5e6a11da94b540f7b38e 100644 --- a/internal/tui/util/util.go +++ b/internal/tui/util/util.go @@ -12,8 +12,9 @@ type Cursor interface { } type Model interface { - tea.Model - tea.ViewModel + Init() tea.Cmd + Update(tea.Msg) (Model, tea.Cmd) + View() string } func CmdHandler(msg tea.Msg) tea.Cmd {