diff --git a/.github/cla-signatures.json b/.github/cla-signatures.json
index dc7d5873d3110320f09b8457b696a47d307ef41a..61a8b03447ae07a5dc775ca59a5eef7aacfe9c2b 100644
--- a/.github/cla-signatures.json
+++ b/.github/cla-signatures.json
@@ -703,6 +703,14 @@
"created_at": "2025-10-06T19:31:50Z",
"repoId": 987670088,
"pullRequestNo": 1200
+ },
+ {
+ "name": "daps94",
+ "id": 35882689,
+ "comment_id": 3395964275,
+ "created_at": "2025-10-13T05:56:20Z",
+ "repoId": 987670088,
+ "pullRequestNo": 1223
}
]
}
\ No newline at end of file
diff --git a/.github/workflows/schema-update.yml b/.github/workflows/schema-update.yml
index bc1a69c68273c007a764c268958858be3b62bcd2..466c3a25fb3698a183ed84436d5dca9813b2dcb6 100644
--- a/.github/workflows/schema-update.yml
+++ b/.github/workflows/schema-update.yml
@@ -17,7 +17,7 @@ jobs:
with:
go-version-file: go.mod
- run: go run . schema > ./schema.json
- - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v5
+ - uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v5
with:
commit_message: "chore: auto-update generated files"
branch: main
diff --git a/Taskfile.yaml b/Taskfile.yaml
index 1c4225158fc21508e8dccac8d6f47610f7d81faf..92b162dfbb847356e09eb17ea5996e6093a305b2 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -99,9 +99,9 @@ tasks:
cmds:
- task: fetch-tags
- git commit --allow-empty -m "{{.NEXT}}"
- - git tag --annotate --sign {{.NEXT}} {{.CLI_ARGS}}
+ - git tag --annotate -m "{{.NEXT}}" {{.NEXT}} {{.CLI_ARGS}}
- echo "Pushing {{.NEXT}}..."
- - git push origin --tags
+ - git push origin main --follow-tags
fetch-tags:
cmds:
diff --git a/go.mod b/go.mod
index 843e7f231f729e86d4e299349fa1293005ad3971..e0b92a9380af54233306de80c826a5191878298a 100644
--- a/go.mod
+++ b/go.mod
@@ -13,8 +13,8 @@ require (
github.com/bmatcuk/doublestar/v4 v4.9.1
github.com/charlievieth/fastwalk v1.0.14
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2
- github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250930175933-4cafc092c5e7
- github.com/charmbracelet/catwalk v0.6.3
+ github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619
+ github.com/charmbracelet/catwalk v0.6.4
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
@@ -22,6 +22,7 @@ require (
github.com/charmbracelet/x/ansi v0.10.2
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
+ github.com/charmbracelet/x/exp/ordered v0.1.0
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
github.com/google/uuid v1.6.0
github.com/invopop/jsonschema v0.13.0
@@ -117,7 +118,7 @@ require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/posthog/posthog-go v1.6.10
+ github.com/posthog/posthog-go v1.6.11
github.com/rivo/uniseg v0.4.7
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
@@ -149,10 +150,10 @@ require (
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
- golang.org/x/text v0.29.0
+ 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.28.0
+ google.golang.org/genai v1.30.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 563016cca9ffcec4a7be40aeed80822a105d1769..d1c0349a66d5e8c0e9bf6968d849cb0cbf6d26c5 100644
--- a/go.sum
+++ b/go.sum
@@ -78,10 +78,10 @@ github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICg
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
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.20250930175933-4cafc092c5e7 h1:wH4F+UvxcZSDOxy8j45tghiRo8amrYHejbE9+1C6xv0=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250930175933-4cafc092c5e7/go.mod h1:5IzIGXU1n0foRc8bRAherC8ZuQCQURPlwx3ANLq1138=
-github.com/charmbracelet/catwalk v0.6.3 h1:RyL8Yqd4QsV3VyvBEsePScv1z2vKaZxPfQQ0XB5L5AA=
-github.com/charmbracelet/catwalk v0.6.3/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY=
+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/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=
@@ -102,6 +102,8 @@ github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
+github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/slice v0.0.0-20250829135019-44e44e21330d h1:H2oh4WlSsXy8qwLd7I3eAvPd/X3S40aM9l+h47WF1eA=
github.com/charmbracelet/x/exp/slice v0.0.0-20250829135019-44e44e21330d/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
github.com/charmbracelet/x/powernap v0.0.0-20250919153222-1038f7e6fef4 h1:ZhDGU688EHQXslD9KphRpXwK0pKP03egUoZAATUDlV0=
@@ -237,8 +239,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v1.6.10 h1:OA6bkiUg89rI7f5cSXbcrH5+wLinyS6hHplnD92Pu/M=
-github.com/posthog/posthog-go v1.6.10/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
+github.com/posthog/posthog-go v1.6.11 h1:5G8Y3pxnOpc3S4+PK1z1dCmZRuldiWxBsqqvvSfC2+w=
+github.com/posthog/posthog-go v1.6.11/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/qjebbs/go-jsons v1.0.0-alpha.4 h1:Qsb4ohRUHQODIUAsJKdKJ/SIDbsO7oGOzsfy+h1yQZs=
@@ -410,8 +412,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
-golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -420,13 +422,13 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
-golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
+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.28.0 h1:6qpUWFH3PkHPhxNnu3wjaCVJ6Jri1EIR7ks07f9IpIk=
-google.golang.org/genai v1.28.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
+google.golang.org/genai v1.30.0 h1:7021aneIvl24nEBLbtQFEWleHsMbjzpcQvkT4WcJ1dc=
+google.golang.org/genai v1.30.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/app/app.go b/internal/app/app.go
index 29631c1be84e96617adfeb705b2e35e0b68725e5..8f305f765f3391e1a6afce294e3c42525ec65668 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -107,10 +107,6 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
ctx, cancel := context.WithCancel(ctx)
defer cancel()
- // Start progress bar and spinner
- fmt.Printf(ansi.SetIndeterminateProgressBar)
- defer fmt.Printf(ansi.ResetProgressBar)
-
var spinner *format.Spinner
if !quiet {
spinner = format.NewSpinner(ctx, cancel, "Generating")
@@ -154,7 +150,11 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
messageEvents := app.Messages.Subscribe(ctx)
messageReadBytes := make(map[string]int)
+ defer fmt.Printf(ansi.ResetProgressBar)
for {
+ // HACK: add it again on every iteration so it doesn't get hidden by
+ // the terminal due to inactivity.
+ fmt.Printf(ansi.SetIndeterminateProgressBar)
select {
case result := <-done:
stopSpinner()
diff --git a/internal/config/config.go b/internal/config/config.go
index b37b98cad717e789ad16237b3ca250a2f1555ba9..ff948b874ea1613ca126053547dcf9b7d4cc3297 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -143,7 +143,7 @@ type Completions struct {
}
func (c Completions) Limits() (depth, items int) {
- return ptrValOr(c.MaxDepth, -1), ptrValOr(c.MaxItems, -1)
+ return ptrValOr(c.MaxDepth, 0), ptrValOr(c.MaxItems, 0)
}
type Permissions struct {
@@ -269,7 +269,7 @@ type ToolLs struct {
}
func (t ToolLs) Limits() (depth, items int) {
- return ptrValOr(t.MaxDepth, -1), ptrValOr(t.MaxItems, -1)
+ return ptrValOr(t.MaxDepth, 0), ptrValOr(t.MaxItems, 0)
}
// Config holds the configuration for crush.
diff --git a/internal/config/load.go b/internal/config/load.go
index 9fb45028d6936a652f2657f51707b6cde73f4084..c63a9663613bdfdea6a9c9ccef9f53d375e35c74 100644
--- a/internal/config/load.go
+++ b/internal/config/load.go
@@ -605,6 +605,11 @@ func hasAWSCredentials(env env.Env) bool {
env.Get("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
return true
}
+
+ if _, err := os.Stat(filepath.Join(home.Dir(), ".aws/credentials")); err == nil {
+ return true
+ }
+
return false
}
diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go
index 67f0b39ccfb6eb8aad3abd337e7545a59766d872..038cd43f4469953779799b70850355ef5dcda45f 100644
--- a/internal/llm/agent/mcp-tools.go
+++ b/internal/llm/agent/mcp-tools.go
@@ -98,9 +98,26 @@ func (b *McpTool) Name() string {
}
func (b *McpTool) Info() tools.ToolInfo {
- input := b.tool.InputSchema.(map[string]any)
- required, _ := input["required"].([]string)
- parameters, _ := input["properties"].(map[string]any)
+ var parameters map[string]any
+ var required []string
+
+ if input, ok := b.tool.InputSchema.(map[string]any); ok {
+ if props, ok := input["properties"].(map[string]any); ok {
+ parameters = props
+ }
+ if req, ok := input["required"].([]any); ok {
+ // Convert []any -> []string when elements are strings
+ for _, v := range req {
+ if s, ok := v.(string); ok {
+ required = append(required, s)
+ }
+ }
+ } else if reqStr, ok := input["required"].([]string); ok {
+ // Handle case where it's already []string
+ required = reqStr
+ }
+ }
+
return tools.ToolInfo{
Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
Description: b.tool.Description,
diff --git a/internal/llm/provider/vertexai.go b/internal/llm/provider/vertexai.go
index 871ff092b058af70833ba615260efcdbc09f2514..e7591af70c24a528d48895c11e653f023ba86c89 100644
--- a/internal/llm/provider/vertexai.go
+++ b/internal/llm/provider/vertexai.go
@@ -30,7 +30,7 @@ func newVertexAIClient(opts providerClientOptions) VertexAIClient {
}
model := opts.model(opts.modelType)
- if strings.Contains(model.ID, "anthropic") || strings.Contains(model.ID, "claude-sonnet") {
+ if strings.Contains(model.ID, "anthropic") || strings.Contains(model.ID, "claude") || strings.Contains(model.ID, "sonnet") {
return newAnthropicClient(opts, AnthropicClientTypeVertex)
}
return &geminiClient{
diff --git a/internal/llm/tools/grep.go b/internal/llm/tools/grep.go
index cbf50360b9355c05797690678a99d1310b19556f..237d4e18dab0bc518b9d4b6e2c73ef5035d2b348 100644
--- a/internal/llm/tools/grep.go
+++ b/internal/llm/tools/grep.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "net/http"
"os"
"os/exec"
"path/filepath"
@@ -390,8 +391,8 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
}
func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, string, error) {
- // Quick binary file detection
- if isBinaryFile(filePath) {
+ // Only search text files.
+ if !isTextFile(filePath) {
return false, 0, "", nil
}
@@ -414,45 +415,30 @@ func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, st
return false, 0, "", scanner.Err()
}
-var binaryExts = map[string]struct{}{
- ".exe": {}, ".dll": {}, ".so": {}, ".dylib": {},
- ".bin": {}, ".obj": {}, ".o": {}, ".a": {},
- ".zip": {}, ".tar": {}, ".gz": {}, ".bz2": {},
- ".jpg": {}, ".jpeg": {}, ".png": {}, ".gif": {},
- ".pdf": {}, ".doc": {}, ".docx": {}, ".xls": {},
- ".mp3": {}, ".mp4": {}, ".avi": {}, ".mov": {},
-}
-
-// isBinaryFile performs a quick check to determine if a file is binary
-func isBinaryFile(filePath string) bool {
- // Check file extension first (fastest)
- ext := strings.ToLower(filepath.Ext(filePath))
- if _, isBinary := binaryExts[ext]; isBinary {
- return true
- }
-
- // Quick content check for files without clear extensions
+// isTextFile checks if a file is a text file by examining its MIME type.
+func isTextFile(filePath string) bool {
file, err := os.Open(filePath)
if err != nil {
- return false // If we can't open it, let the caller handle the error
+ return false
}
defer file.Close()
- // Read first 512 bytes to check for null bytes
+ // Read first 512 bytes for MIME type detection.
buffer := make([]byte, 512)
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
return false
}
- // Check for null bytes (common in binary files)
- for i := range n {
- if buffer[i] == 0 {
- return true
- }
- }
+ // Detect content type.
+ contentType := http.DetectContentType(buffer[:n])
- return false
+ // Check if it's a text MIME type.
+ return strings.HasPrefix(contentType, "text/") ||
+ contentType == "application/json" ||
+ contentType == "application/xml" ||
+ contentType == "application/javascript" ||
+ contentType == "application/x-sh"
}
func globToRegex(glob string) string {
diff --git a/internal/llm/tools/grep_test.go b/internal/llm/tools/grep_test.go
index 53c96b22df444adfba59c6b13995a104411a57be..435b3045b93a8e1297ff2aaeff9ee8977b974b56 100644
--- a/internal/llm/tools/grep_test.go
+++ b/internal/llm/tools/grep_test.go
@@ -198,3 +198,195 @@ func BenchmarkRegexCacheVsCompile(b *testing.B) {
}
})
}
+
+func TestIsTextFile(t *testing.T) {
+ t.Parallel()
+ tempDir := t.TempDir()
+
+ tests := []struct {
+ name string
+ filename string
+ content []byte
+ wantText bool
+ }{
+ {
+ name: "go file",
+ filename: "test.go",
+ content: []byte("package main\n\nfunc main() {}\n"),
+ wantText: true,
+ },
+ {
+ name: "yaml file",
+ filename: "config.yaml",
+ content: []byte("key: value\nlist:\n - item1\n - item2\n"),
+ wantText: true,
+ },
+ {
+ name: "yml file",
+ filename: "config.yml",
+ content: []byte("key: value\n"),
+ wantText: true,
+ },
+ {
+ name: "json file",
+ filename: "data.json",
+ content: []byte(`{"key": "value"}`),
+ wantText: true,
+ },
+ {
+ name: "javascript file",
+ filename: "script.js",
+ content: []byte("console.log('hello');\n"),
+ wantText: true,
+ },
+ {
+ name: "typescript file",
+ filename: "script.ts",
+ content: []byte("const x: string = 'hello';\n"),
+ wantText: true,
+ },
+ {
+ name: "markdown file",
+ filename: "README.md",
+ content: []byte("# Title\n\nSome content\n"),
+ wantText: true,
+ },
+ {
+ name: "shell script",
+ filename: "script.sh",
+ content: []byte("#!/bin/bash\necho 'hello'\n"),
+ wantText: true,
+ },
+ {
+ name: "python file",
+ filename: "script.py",
+ content: []byte("print('hello')\n"),
+ wantText: true,
+ },
+ {
+ name: "xml file",
+ filename: "data.xml",
+ content: []byte("\n\n"),
+ wantText: true,
+ },
+ {
+ name: "plain text",
+ filename: "file.txt",
+ content: []byte("plain text content\n"),
+ wantText: true,
+ },
+ {
+ name: "css file",
+ filename: "style.css",
+ content: []byte("body { color: red; }\n"),
+ wantText: true,
+ },
+ {
+ name: "scss file",
+ filename: "style.scss",
+ content: []byte("$primary: blue;\nbody { color: $primary; }\n"),
+ wantText: true,
+ },
+ {
+ name: "sass file",
+ filename: "style.sass",
+ content: []byte("$primary: blue\nbody\n color: $primary\n"),
+ wantText: true,
+ },
+ {
+ name: "rust file",
+ filename: "main.rs",
+ content: []byte("fn main() {\n println!(\"Hello, world!\");\n}\n"),
+ wantText: true,
+ },
+ {
+ name: "zig file",
+ filename: "main.zig",
+ content: []byte("const std = @import(\"std\");\npub fn main() void {}\n"),
+ wantText: true,
+ },
+ {
+ name: "java file",
+ filename: "Main.java",
+ content: []byte("public class Main {\n public static void main(String[] args) {}\n}\n"),
+ wantText: true,
+ },
+ {
+ name: "c file",
+ filename: "main.c",
+ content: []byte("#include \nint main() { return 0; }\n"),
+ wantText: true,
+ },
+ {
+ name: "cpp file",
+ filename: "main.cpp",
+ content: []byte("#include \nint main() { return 0; }\n"),
+ wantText: true,
+ },
+ {
+ name: "fish shell",
+ filename: "script.fish",
+ content: []byte("#!/usr/bin/env fish\necho 'hello'\n"),
+ wantText: true,
+ },
+ {
+ name: "powershell file",
+ filename: "script.ps1",
+ content: []byte("Write-Host 'Hello, World!'\n"),
+ wantText: true,
+ },
+ {
+ name: "cmd batch file",
+ filename: "script.bat",
+ content: []byte("@echo off\necho Hello, World!\n"),
+ wantText: true,
+ },
+ {
+ name: "cmd file",
+ filename: "script.cmd",
+ content: []byte("@echo off\necho Hello, World!\n"),
+ wantText: true,
+ },
+ {
+ name: "binary exe",
+ filename: "binary.exe",
+ content: []byte{0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00},
+ wantText: false,
+ },
+ {
+ name: "png image",
+ filename: "image.png",
+ content: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
+ wantText: false,
+ },
+ {
+ name: "jpeg image",
+ filename: "image.jpg",
+ content: []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46},
+ wantText: false,
+ },
+ {
+ name: "zip archive",
+ filename: "archive.zip",
+ content: []byte{0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00},
+ wantText: false,
+ },
+ {
+ name: "pdf file",
+ filename: "document.pdf",
+ content: []byte("%PDF-1.4\n%âãÏÓ\n"),
+ wantText: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ filePath := filepath.Join(tempDir, tt.filename)
+ require.NoError(t, os.WriteFile(filePath, tt.content, 0o644))
+
+ got := isTextFile(filePath)
+ require.Equal(t, tt.wantText, got, "isTextFile(%s) = %v, want %v", tt.filename, got, tt.wantText)
+ })
+ }
+}
diff --git a/internal/llm/tools/ls.go b/internal/llm/tools/ls.go
index 305f7f10249594ff06ac008a8bf81145d7d834de..af25259dd8c69ff8d52d467e20532612681b51b1 100644
--- a/internal/llm/tools/ls.go
+++ b/internal/llm/tools/ls.go
@@ -157,7 +157,7 @@ func ListDirectoryTree(searchPath string, params LSParams) (string, LSResponseMe
ls := config.Get().Tools.Ls
depth, limit := ls.Limits()
- maxFiles := min(limit, maxLSFiles)
+ maxFiles := cmp.Or(limit, maxLSFiles)
files, truncated, err := fsext.ListDirectory(
searchPath,
params.Ignore,
diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go
index 296b02478a7d0738fef2f60ae6b2211d44424a2f..d931ba7e179255d6639db78ebea5e82b57af1504 100644
--- a/internal/tui/components/chat/messages/messages.go
+++ b/internal/tui/components/chat/messages/messages.go
@@ -12,6 +12,7 @@ import (
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
+ "github.com/charmbracelet/x/exp/ordered"
"github.com/google/uuid"
"github.com/atotto/clipboard"
@@ -271,7 +272,7 @@ func (m *messageCmp) renderThinkingContent() string {
}
}
fullContent := content.String()
- height := util.Clamp(lipgloss.Height(fullContent), 1, 10)
+ height := ordered.Clamp(lipgloss.Height(fullContent), 1, 10)
m.thinkingViewport.SetHeight(height)
m.thinkingViewport.SetWidth(m.textWidth())
m.thinkingViewport.SetContent(fullContent)
@@ -344,7 +345,7 @@ func (m *messageCmp) GetSize() (int, int) {
// SetSize updates the width of the message component for text wrapping
func (m *messageCmp) SetSize(width int, height int) tea.Cmd {
- m.width = util.Clamp(width, 1, 120)
+ m.width = ordered.Clamp(width, 1, 120)
m.thinkingViewport.SetWidth(m.width - 4)
return nil
}
diff --git a/internal/tui/components/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go
index 0f46895a24ea5340d8496f00c9cf988f39d73a91..9cb1141de92529b1f0d2abd8db32a002b55db631 100644
--- a/internal/tui/components/dialogs/commands/arguments.go
+++ b/internal/tui/components/dialogs/commands/arguments.go
@@ -119,8 +119,6 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case tea.KeyPressMsg:
switch {
- case key.Matches(msg, c.keys.Cancel):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
case key.Matches(msg, c.keys.Confirm):
if c.focused == len(c.inputs)-1 {
args := make(map[string]string)
@@ -147,12 +145,19 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
c.inputs[c.focused].Blur()
c.focused = (c.focused - 1 + len(c.inputs)) % len(c.inputs)
c.inputs[c.focused].Focus()
-
+ case key.Matches(msg, c.keys.Paste):
+ return c, textinput.Paste
+ case key.Matches(msg, c.keys.Close):
+ return c, util.CmdHandler(dialogs.CloseDialogMsg{})
default:
var cmd tea.Cmd
c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
return c, cmd
}
+ case tea.PasteMsg:
+ var cmd tea.Cmd
+ c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
+ return c, cmd
}
return c, nil
}
diff --git a/internal/tui/components/dialogs/commands/keys.go b/internal/tui/components/dialogs/commands/keys.go
index 32d34d3c26360f040ea8ecd7e9c1fa3c4d2d7a5a..b704f227fe6f183a430bd25d3af62f4ef50b3365 100644
--- a/internal/tui/components/dialogs/commands/keys.go
+++ b/internal/tui/components/dialogs/commands/keys.go
@@ -76,7 +76,8 @@ type ArgumentsDialogKeyMap struct {
Confirm key.Binding
Next key.Binding
Previous key.Binding
- Cancel key.Binding
+ Paste key.Binding
+ Close key.Binding
}
func DefaultArgumentsDialogKeyMap() ArgumentsDialogKeyMap {
@@ -94,8 +95,12 @@ func DefaultArgumentsDialogKeyMap() ArgumentsDialogKeyMap {
key.WithKeys("shift+tab", "up"),
key.WithHelp("shift+tab/↑", "previous"),
),
- Cancel: key.NewBinding(
- key.WithKeys("esc"),
+ Paste: key.NewBinding(
+ key.WithKeys("ctrl+v"),
+ key.WithHelp("ctrl+v", "paste"),
+ ),
+ Close: key.NewBinding(
+ key.WithKeys("esc", "alt+esc"),
key.WithHelp("esc", "cancel"),
),
}
@@ -107,6 +112,8 @@ func (k ArgumentsDialogKeyMap) KeyBindings() []key.Binding {
k.Confirm,
k.Next,
k.Previous,
+ k.Paste,
+ k.Close,
}
}
@@ -127,5 +134,7 @@ func (k ArgumentsDialogKeyMap) ShortHelp() []key.Binding {
k.Confirm,
k.Next,
k.Previous,
+ k.Paste,
+ k.Close,
}
}
diff --git a/internal/tui/exp/list/list.go b/internal/tui/exp/list/list.go
index fd789f90b89b016abb9b9fb5c79227da7ef30fd9..e18b88348959c59190f1741698f76c33f04571db 100644
--- a/internal/tui/exp/list/list.go
+++ b/internal/tui/exp/list/list.go
@@ -15,6 +15,7 @@ import (
"github.com/charmbracelet/lipgloss/v2"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/ansi"
+ "github.com/charmbracelet/x/exp/ordered"
"github.com/rivo/uniseg"
)
@@ -1283,14 +1284,14 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
newItem, ok := l.renderedItems.Get(item.ID())
if ok {
newLines := newItem.height - oldItem.height
- l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
+ l.offset = ordered.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
}
}
} else if hasOldItem && l.offset > oldItem.start {
newItem, ok := l.renderedItems.Get(item.ID())
if ok {
newLines := newItem.height - oldItem.height
- l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
+ l.offset = ordered.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
}
}
}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 5fd430fe7c7ba4ac55ae4a716fc98ade9b60ec38..2a532b7ae7e317cc7e06583b19126c81903af71c 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -615,10 +615,9 @@ func (a *appModel) View() tea.View {
view.Layer = canvas
view.Cursor = cursor
- view.ProgressBar = tea.NewProgressBar(tea.ProgressBarNone, 0)
- if a.app.CoderAgent.IsBusy() {
- // use a random percentage to prevent the ghostty from hiding it after
- // a timeout.
+ 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.
view.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, rand.Intn(100))
}
return view
diff --git a/internal/tui/util/util.go b/internal/tui/util/util.go
index 1f4ea30c49c8fb0517a5068d3b7f05970638743a..eb19ad89544b281af2e836f667ac63aaa6414e01 100644
--- a/internal/tui/util/util.go
+++ b/internal/tui/util/util.go
@@ -60,10 +60,3 @@ type (
}
ClearStatusMsg struct{}
)
-
-func Clamp(v, low, high int) int {
- if high < low {
- low, high = high, low
- }
- return min(high, max(low, v))
-}