on main help sign in
perf: reduce memory usage (#1812)
d1382bbCarlos Alexandro Becker
Taskfile.yaml+2−1
@@ -44,7 +44,8 @@ tasks:
44 run:
44 run:
45 desc: Run build
45 desc: Run build
46 cmds:
46 cmds:
47+ - go build -o crush .
47− - go run . {{.CLI_ARGS}}
48+ - ./crush {{.CLI_ARGS}}
48
49
49 test:
50 test:
50 desc: Run tests
51 desc: Run tests
go.mod+7−1
@@ -25,7 +25,7 @@ require (
25 github.com/charmbracelet/x/ansi v0.11.3
25 github.com/charmbracelet/x/ansi v0.11.3
26 github.com/charmbracelet/x/editor v0.2.0
26 github.com/charmbracelet/x/editor v0.2.0
27 github.com/charmbracelet/x/etag v0.2.0
27 github.com/charmbracelet/x/etag v0.2.0
28− github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
28+ github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
29 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
29 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
30 github.com/charmbracelet/x/exp/ordered v0.1.0
30 github.com/charmbracelet/x/exp/ordered v0.1.0
31 github.com/charmbracelet/x/exp/slice v0.0.0-20251201173703-9f73bfd934ff
31 github.com/charmbracelet/x/exp/slice v0.0.0-20251201173703-9f73bfd934ff
@@ -63,6 +63,7 @@ require (
63 golang.org/x/text v0.32.0
63 golang.org/x/text v0.32.0
64 gopkg.in/natefinch/lumberjack.v2 v2.2.1
64 gopkg.in/natefinch/lumberjack.v2 v2.2.1
65 gopkg.in/yaml.v3 v3.0.1
65 gopkg.in/yaml.v3 v3.0.1
66+ modernc.org/sqlite v1.43.0
66 mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5
67 mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5
67 mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
68 mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
68)
69)
@@ -141,9 +142,11 @@ require (
141 github.com/muesli/mango-cobra v1.2.0 // indirect
142 github.com/muesli/mango-cobra v1.2.0 // indirect
142 github.com/muesli/mango-pflag v0.1.0 // indirect
143 github.com/muesli/mango-pflag v0.1.0 // indirect
143 github.com/muesli/roff v0.1.0 // indirect
144 github.com/muesli/roff v0.1.0 // indirect
145+ github.com/ncruces/go-strftime v0.1.9 // indirect
144 github.com/ncruces/julianday v1.0.0 // indirect
146 github.com/ncruces/julianday v1.0.0 // indirect
145 github.com/pierrec/lz4/v4 v4.1.22 // indirect
147 github.com/pierrec/lz4/v4 v4.1.22 // indirect
146 github.com/pmezard/go-difflib v1.0.0 // indirect
148 github.com/pmezard/go-difflib v1.0.0 // indirect
149+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
147 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
150 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
148 github.com/sethvargo/go-retry v0.3.0 // indirect
151 github.com/sethvargo/go-retry v0.3.0 // indirect
149 github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect
152 github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect
@@ -180,4 +183,7 @@ require (
180 google.golang.org/protobuf v1.36.10 // indirect
183 google.golang.org/protobuf v1.36.10 // indirect
181 gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect
184 gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect
182 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
185 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
186+ modernc.org/libc v1.66.10 // indirect
187+ modernc.org/mathutil v1.7.1 // indirect
188+ modernc.org/memory v1.11.0 // indirect
183)
189)
go.sum+26−6
@@ -108,8 +108,8 @@ github.com/charmbracelet/x/editor v0.2.0 h1:7XLUKtaRaB8jN7bWU2p2UChiySyaAuIfYiIR
108github.com/charmbracelet/x/editor v0.2.0/go.mod h1:p3oQ28TSL3YPd+GKJ1fHWcp+7bVGpedHpXmo0D6t1dY=
108github.com/charmbracelet/x/editor v0.2.0/go.mod h1:p3oQ28TSL3YPd+GKJ1fHWcp+7bVGpedHpXmo0D6t1dY=
109github.com/charmbracelet/x/etag v0.2.0 h1:Euj1VkheoHfTYA9y+TCwkeXF/hN8Fb9l4LqZl79pt04=
109github.com/charmbracelet/x/etag v0.2.0 h1:Euj1VkheoHfTYA9y+TCwkeXF/hN8Fb9l4LqZl79pt04=
110github.com/charmbracelet/x/etag v0.2.0/go.mod h1:C1B7/bsgvzzxpfu0Rabbd+rTHJa5TmC/qgTseCf6DF0=
110github.com/charmbracelet/x/etag v0.2.0/go.mod h1:C1B7/bsgvzzxpfu0Rabbd+rTHJa5TmC/qgTseCf6DF0=
111−github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50=
111+github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f h1:OKFNbG2sSmgpQW9EC3gYNG+QrcQ4+wWYjzfmJvWkkDo=
112−github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
112+github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY=
113github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
113github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
114github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
114github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
115github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
115github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
@@ -184,6 +184,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
184github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
184github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
185github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
185github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
186github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
186github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
187+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
188+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
187github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
189github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
188github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
190github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
189github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
191github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -501,14 +503,32 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
501gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
503gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
502gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
504gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
503gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
505gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
504−modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
506+modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
505−modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
507+modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
508+modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
509+modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
510+modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
511+modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
512+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
513+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
514+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
515+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
516+modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
517+modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
506modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
518modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
507modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
519modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
508modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
520modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
509modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
521modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
510−modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
522+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
511−modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
523+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
524+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
525+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
526+modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
527+modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
528+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
529+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
530+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
531+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
512mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5 h1:mO2lyKtGwu4mGQ+Qqjx0+fd5UU5BXhX/rslFmxd5aco=
532mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5 h1:mO2lyKtGwu4mGQ+Qqjx0+fd5UU5BXhX/rslFmxd5aco=
513mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5/go.mod h1:Of9PCedbLDYT8b3EyiYG64rNnx5nOp27OLCVdDrjJyo=
533mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5/go.mod h1:Of9PCedbLDYT8b3EyiYG64rNnx5nOp27OLCVdDrjJyo=
514mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5 h1:e7Z/Lgw/zMijvQBVrfh/vUDZ+9FpuSLrJDVGBuoJtuo=
534mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5 h1:e7Z/Lgw/zMijvQBVrfh/vUDZ+9FpuSLrJDVGBuoJtuo=
internal/agent/common_test.go+4
@@ -182,6 +182,10 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel
182 // would be included in prompt and break VCR cassette matching.
182 // would be included in prompt and break VCR cassette matching.
183 cfg.Options.SkillsPaths = []string{}
183 cfg.Options.SkillsPaths = []string{}
184
184
185+ // Clear LSP config to ensure test reproducibility - user's LSP config
186+ // would be included in prompt and break VCR cassette matching.
187+ cfg.LSP = nil
188+
185 systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), *cfg)
189 systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), *cfg)
186 if err != nil {
190 if err != nil {
187 return nil, err
191 return nil, err
internal/agent/tools/fetch_helpers.go+3−2
@@ -19,6 +19,8 @@ import (
19// BrowserUserAgent is a realistic browser User-Agent for better compatibility.
19// BrowserUserAgent is a realistic browser User-Agent for better compatibility.
20const BrowserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
20const BrowserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
21
21
22+var multipleNewlinesRe = regexp.MustCompile(`\n{3,}`)
23+
22// FetchURLAndConvert fetches a URL and converts HTML content to markdown.
24// FetchURLAndConvert fetches a URL and converts HTML content to markdown.
23func FetchURLAndConvert(ctx context.Context, client *http.Client, url string) (string, error) {
25func FetchURLAndConvert(ctx context.Context, client *http.Client, url string) (string, error) {
24 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
26 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
@@ -128,8 +130,7 @@ func removeNoisyElements(htmlContent string) string {
128// cleanupMarkdown removes excessive whitespace and blank lines from markdown.
130// cleanupMarkdown removes excessive whitespace and blank lines from markdown.
129func cleanupMarkdown(content string) string {
131func cleanupMarkdown(content string) string {
130 // Collapse multiple blank lines into at most two.
132 // Collapse multiple blank lines into at most two.
131− multipleNewlines := regexp.MustCompile(`\n{3,}`)
132− content = multipleNewlines.ReplaceAllString(content, "\n\n")
133+ content = multipleNewlinesRe.ReplaceAllString(content, "\n\n")
133
134
134 // Remove trailing whitespace from each line.
135 // Remove trailing whitespace from each line.
135 lines := strings.Split(content, "\n")
136 lines := strings.Split(content, "\n")
internal/agent/tools/mcp/init.go+1−2
@@ -9,7 +9,6 @@ import (
9 "fmt"
9 "fmt"
10 "io"
10 "io"
11 "log/slog"
11 "log/slog"
12− "maps"
13 "net/http"
12 "net/http"
14 "os"
13 "os"
15 "os/exec"
14 "os/exec"
@@ -98,7 +97,7 @@ func SubscribeEvents(ctx context.Context) <-chan pubsub.Event[Event] {
98
97
99// GetStates returns the current state of all MCP clients
98// GetStates returns the current state of all MCP clients
100func GetStates() map[string]ClientInfo {
99func GetStates() map[string]ClientInfo {
101− return maps.Collect(states.Seq2())
100+ return states.Copy()
102}
101}
103
102
104// GetState returns the state of a specific MCP client
103// GetState returns the state of a specific MCP client
internal/app/lsp_events.go+1−2
@@ -2,7 +2,6 @@ package app
2
2
3import (
3import (
4 "context"
4 "context"
5− "maps"
6 "time"
5 "time"
7
6
8 "github.com/charmbracelet/crush/internal/csync"
7 "github.com/charmbracelet/crush/internal/csync"
@@ -49,7 +48,7 @@ func SubscribeLSPEvents(ctx context.Context) <-chan pubsub.Event[LSPEvent] {
49
48
50// GetLSPStates returns the current state of all LSP clients
49// GetLSPStates returns the current state of all LSP clients
51func GetLSPStates() map[string]LSPClientInfo {
50func GetLSPStates() map[string]LSPClientInfo {
52− return maps.Collect(lspStates.Seq2())
51+ return lspStates.Copy()
53}
52}
54
53
55// GetLSPState returns the state of a specific LSP client
54// GetLSPState returns the state of a specific LSP client
internal/csync/maps.go+8−4
@@ -96,12 +96,16 @@ func (m *Map[K, V]) Take(key K) (V, bool) {
96 return v, ok
96 return v, ok
97}
97}
98
98
99+// Copy returns a copy of the inner map.
100+func (m *Map[K, V]) Copy() map[K]V {
101+ m.mu.RLock()
102+ defer m.mu.RUnlock()
103+ return maps.Clone(m.inner)
104+}
105+
99// Seq2 returns an iter.Seq2 that yields key-value pairs from the map.
106// Seq2 returns an iter.Seq2 that yields key-value pairs from the map.
100func (m *Map[K, V]) Seq2() iter.Seq2[K, V] {
107func (m *Map[K, V]) Seq2() iter.Seq2[K, V] {
101− dst := make(map[K]V)
108+ dst := m.Copy()
102− m.mu.RLock()
103− maps.Copy(dst, m.inner)
104− m.mu.RUnlock()
105 return func(yield func(K, V) bool) {
109 return func(yield func(K, V) bool) {
106 for k, v := range dst {
110 for k, v := range dst {
107 if !yield(k, v) {
111 if !yield(k, v) {
internal/csync/versionedmap.go+5
@@ -40,6 +40,11 @@ func (m *VersionedMap[K, V]) Seq2() iter.Seq2[K, V] {
40 return m.m.Seq2()
40 return m.m.Seq2()
41}
41}
42
42
43+// Copy returns a copy of the inner map.
44+func (m *VersionedMap[K, V]) Copy() map[K]V {
45+ return m.m.Copy()
46+}
47+
43// Len returns the number of items in the map.
48// Len returns the number of items in the map.
44func (m *VersionedMap[K, V]) Len() int {
49func (m *VersionedMap[K, V]) Len() int {
45 return m.m.Len()
50 return m.m.Len()
internal/db/connect.go+3−24
@@ -7,42 +7,21 @@ import (
7 "log/slog"
7 "log/slog"
8 "path/filepath"
8 "path/filepath"
9
9
10− "github.com/ncruces/go-sqlite3"
11− "github.com/ncruces/go-sqlite3/driver"
12− _ "github.com/ncruces/go-sqlite3/embed"
13−
14 "github.com/pressly/goose/v3"
10 "github.com/pressly/goose/v3"
15)
11)
16
12
13+// Connect opens a SQLite database connection and runs migrations.
17func Connect(ctx context.Context, dataDir string) (*sql.DB, error) {
14func Connect(ctx context.Context, dataDir string) (*sql.DB, error) {
18 if dataDir == "" {
15 if dataDir == "" {
19 return nil, fmt.Errorf("data.dir is not set")
16 return nil, fmt.Errorf("data.dir is not set")
20 }
17 }
21 dbPath := filepath.Join(dataDir, "crush.db")
18 dbPath := filepath.Join(dataDir, "crush.db")
22
19
23− // Set pragmas for better performance
20+ db, err := openDB(dbPath)
24− pragmas := []string{
25− "PRAGMA foreign_keys = ON;",
26− "PRAGMA journal_mode = WAL;",
27− "PRAGMA page_size = 4096;",
28− "PRAGMA cache_size = -8000;",
29− "PRAGMA synchronous = NORMAL;",
30− "PRAGMA secure_delete = ON;",
31− }
32−
33− db, err := driver.Open(dbPath, func(c *sqlite3.Conn) error {
34− for _, pragma := range pragmas {
35− if err := c.Exec(pragma); err != nil {
36− return fmt.Errorf("failed to set pragma `%s`: %w", pragma, err)
37− }
38− }
39− return nil
40− })
41 if err != nil {
21 if err != nil {
42− return nil, fmt.Errorf("failed to open database: %w", err)
22+ return nil, err
43 }
23 }
44
24
45− // Verify connection
46 if err = db.PingContext(ctx); err != nil {
25 if err = db.PingContext(ctx); err != nil {
47 db.Close()
26 db.Close()
48 return nil, fmt.Errorf("failed to connect to database: %w", err)
27 return nil, fmt.Errorf("failed to connect to database: %w", err)
internal/db/connect_modernc.gonew file+30
@@ -0,0 +1,30 @@
1+//go:build (darwin && (amd64 || arm64)) || (freebsd && (amd64 || arm64)) || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)) || (windows && (386 || amd64 || arm64))
2+
3+package db
4+
5+import (
6+ "database/sql"
7+ "fmt"
8+ "net/url"
9+
10+ _ "modernc.org/sqlite"
11+)
12+
13+func openDB(dbPath string) (*sql.DB, error) {
14+ // Set pragmas for better performance via _pragma query params.
15+ // Format: _pragma=name(value)
16+ params := url.Values{}
17+ params.Add("_pragma", "foreign_keys(on)")
18+ params.Add("_pragma", "journal_mode(WAL)")
19+ params.Add("_pragma", "page_size(4096)")
20+ params.Add("_pragma", "cache_size(-8000)")
21+ params.Add("_pragma", "synchronous(NORMAL)")
22+ params.Add("_pragma", "secure_delete(on)")
23+
24+ dsn := fmt.Sprintf("file:%s?%s", dbPath, params.Encode())
25+ db, err := sql.Open("sqlite", dsn)
26+ if err != nil {
27+ return nil, fmt.Errorf("failed to open database: %w", err)
28+ }
29+ return db, nil
30+}
internal/db/connect_ncruces.gonew file+37
@@ -0,0 +1,37 @@
1+//go:build !((darwin && (amd64 || arm64)) || (freebsd && (amd64 || arm64)) || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)) || (windows && (386 || amd64 || arm64)))
2+
3+package db
4+
5+import (
6+ "database/sql"
7+ "fmt"
8+
9+ "github.com/ncruces/go-sqlite3"
10+ "github.com/ncruces/go-sqlite3/driver"
11+ _ "github.com/ncruces/go-sqlite3/embed"
12+)
13+
14+func openDB(dbPath string) (*sql.DB, error) {
15+ // Set pragmas for better performance.
16+ pragmas := []string{
17+ "PRAGMA foreign_keys = ON;",
18+ "PRAGMA journal_mode = WAL;",
19+ "PRAGMA page_size = 4096;",
20+ "PRAGMA cache_size = -8000;",
21+ "PRAGMA synchronous = NORMAL;",
22+ "PRAGMA secure_delete = ON;",
23+ }
24+
25+ db, err := driver.Open(dbPath, func(c *sqlite3.Conn) error {
26+ for _, pragma := range pragmas {
27+ if err := c.Exec(pragma); err != nil {
28+ return fmt.Errorf("failed to set pragma %q: %w", pragma, err)
29+ }
30+ }
31+ return nil
32+ })
33+ if err != nil {
34+ return nil, fmt.Errorf("failed to open database: %w", err)
35+ }
36+ return db, nil
37+}
internal/lsp/client.go+49−1
@@ -9,6 +9,7 @@ import (
9 "os"
9 "os"
10 "path/filepath"
10 "path/filepath"
11 "strings"
11 "strings"
12+ "sync"
12 "sync/atomic"
13 "sync/atomic"
13 "time"
14 "time"
14
15
@@ -21,6 +22,14 @@ import (
21 "github.com/charmbracelet/x/powernap/pkg/transport"
22 "github.com/charmbracelet/x/powernap/pkg/transport"
22)
23)
23
24
25+// DiagnosticCounts holds the count of diagnostics by severity.
26+type DiagnosticCounts struct {
27+ Error int
28+ Warning int
29+ Information int
30+ Hint int
31+}
32+
24type Client struct {
33type Client struct {
25 client *powernap.Client
34 client *powernap.Client
26 name string
35 name string
@@ -37,6 +46,11 @@ type Client struct {
37 // Diagnostic cache
46 // Diagnostic cache
38 diagnostics *csync.VersionedMap[protocol.DocumentURI, []protocol.Diagnostic]
47 diagnostics *csync.VersionedMap[protocol.DocumentURI, []protocol.Diagnostic]
39
48
49+ // Cached diagnostic counts to avoid map copy on every UI render.
50+ diagCountsCache DiagnosticCounts
51+ diagCountsVersion uint64
52+ diagCountsMu sync.Mutex
53+
40 // Files are currently opened by the LSP
54 // Files are currently opened by the LSP
41 openFiles *csync.Map[string, *OpenFileInfo]
55 openFiles *csync.Map[string, *OpenFileInfo]
42
56
@@ -350,7 +364,41 @@ func (c *Client) GetFileDiagnostics(uri protocol.DocumentURI) []protocol.Diagnos
350
364
351// GetDiagnostics returns all diagnostics for all files.
365// GetDiagnostics returns all diagnostics for all files.
352func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic {
366func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic {
367+ return c.diagnostics.Copy()
368+}
369+
370+// GetDiagnosticCounts returns cached diagnostic counts by severity.
371+// Uses the VersionedMap version to avoid recomputing on every call.
372+func (c *Client) GetDiagnosticCounts() DiagnosticCounts {
373+ currentVersion := c.diagnostics.Version()
374+
375+ c.diagCountsMu.Lock()
376+ defer c.diagCountsMu.Unlock()
377+
378+ if currentVersion == c.diagCountsVersion {
379+ return c.diagCountsCache
380+ }
381+
382+ // Recompute counts.
383+ counts := DiagnosticCounts{}
353− return maps.Collect(c.diagnostics.Seq2())
384+ for _, diags := range c.diagnostics.Seq2() {
385+ for _, diag := range diags {
386+ switch diag.Severity {
387+ case protocol.SeverityError:
388+ counts.Error++
389+ case protocol.SeverityWarning:
390+ counts.Warning++
391+ case protocol.SeverityInformation:
392+ counts.Information++
393+ case protocol.SeverityHint:
394+ counts.Hint++
395+ }
396+ }
397+ }
398+
399+ c.diagCountsCache = counts
400+ c.diagCountsVersion = currentVersion
401+ return counts
354}
402}
355
403
356// OpenFileOnDemand opens a file only if it's not already open.
404// OpenFileOnDemand opens a file only if it's not already open.
internal/oauth/copilot/client.go+2−1
@@ -12,6 +12,8 @@ import (
12 "github.com/charmbracelet/crush/internal/log"
12 "github.com/charmbracelet/crush/internal/log"
13)
13)
14
14
15+var assistantRolePattern = regexp.MustCompile(`"role"\s*:\s*"assistant"`)
16+
15// NewClient creates a new HTTP client with a custom transport that adds the
17// NewClient creates a new HTTP client with a custom transport that adds the
16// X-Initiator header based on message history in the request body.
18// X-Initiator header based on message history in the request body.
17func NewClient(isSubAgent, debug bool) *http.Client {
19func NewClient(isSubAgent, debug bool) *http.Client {
@@ -58,7 +60,6 @@ func (t *initiatorTransport) RoundTrip(req *http.Request) (*http.Response, error
58 // Check for assistant messages using regex to handle whitespace
60 // Check for assistant messages using regex to handle whitespace
59 // variations in the JSON while avoiding full unmarshalling overhead.
61 // variations in the JSON while avoiding full unmarshalling overhead.
60 initiator := userInitiator
62 initiator := userInitiator
61− assistantRolePattern := regexp.MustCompile(`"role"\s*:\s*"assistant"`)
62 if assistantRolePattern.Match(bodyBytes) || t.isSubAgent {
63 if assistantRolePattern.Match(bodyBytes) || t.isSubAgent {
63 slog.Debug("Setting X-Initiator header to agent (found assistant messages in history)")
64 slog.Debug("Setting X-Initiator header to agent (found assistant messages in history)")
64 initiator = agentInitiator
65 initiator = agentInitiator
internal/tui/components/chat/header/header.go+1−8
@@ -15,7 +15,6 @@ import (
15 "github.com/charmbracelet/crush/internal/tui/styles"
15 "github.com/charmbracelet/crush/internal/tui/styles"
16 "github.com/charmbracelet/crush/internal/tui/util"
16 "github.com/charmbracelet/crush/internal/tui/util"
17 "github.com/charmbracelet/x/ansi"
17 "github.com/charmbracelet/x/ansi"
18− "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
19)
18)
20
19
21type Header interface {
20type Header interface {
@@ -106,13 +105,7 @@ func (h *header) details(availWidth int) string {
106
105
107 errorCount := 0
106 errorCount := 0
108 for l := range h.lspClients.Seq() {
107 for l := range h.lspClients.Seq() {
109− for _, diagnostics := range l.GetDiagnostics() {
108+ errorCount += l.GetDiagnosticCounts().Error
110− for _, diagnostic := range diagnostics {
111− if diagnostic.Severity == protocol.SeverityError {
112− errorCount++
113− }
114− }
115− }
116 }
109 }
117
110
118 if errorCount > 0 {
111 if errorCount > 0 {
internal/tui/components/lsp/lsp.go+14−28
@@ -11,7 +11,6 @@ import (
11 "github.com/charmbracelet/crush/internal/lsp"
11 "github.com/charmbracelet/crush/internal/lsp"
12 "github.com/charmbracelet/crush/internal/tui/components/core"
12 "github.com/charmbracelet/crush/internal/tui/components/core"
13 "github.com/charmbracelet/crush/internal/tui/styles"
13 "github.com/charmbracelet/crush/internal/tui/styles"
14− "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
15)
14)
16
15
17// RenderOptions contains options for rendering LSP lists.
16// RenderOptions contains options for rendering LSP lists.
@@ -61,36 +60,23 @@ func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOption
61 // Calculate diagnostic counts if we have LSP clients
60 // Calculate diagnostic counts if we have LSP clients
62 var extraContent string
61 var extraContent string
63 if lspClients != nil {
62 if lspClients != nil {
64− lspErrs := map[protocol.DiagnosticSeverity]int{
65− protocol.SeverityError: 0,
66− protocol.SeverityWarning: 0,
67− protocol.SeverityHint: 0,
68− protocol.SeverityInformation: 0,
69− }
70 if client, ok := lspClients.Get(l.Name); ok {
63 if client, ok := lspClients.Get(l.Name); ok {
71− for _, diagnostics := range client.GetDiagnostics() {
64+ counts := client.GetDiagnosticCounts()
72− for _, diagnostic := range diagnostics {
65+ errs := []string{}
73− if severity, ok := lspErrs[diagnostic.Severity]; ok {
66+ if counts.Error > 0 {
74− lspErrs[diagnostic.Severity] = severity + 1
67+ errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, counts.Error)))
75− }
76− }
77 }
68 }
69+ if counts.Warning > 0 {
70+ errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, counts.Warning)))
71+ }
72+ if counts.Hint > 0 {
73+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.HintIcon, counts.Hint)))
74+ }
75+ if counts.Information > 0 {
76+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.InfoIcon, counts.Information)))
77+ }
78+ extraContent = strings.Join(errs, " ")
78 }
79 }
79−
80− errs := []string{}
81− if lspErrs[protocol.SeverityError] > 0 {
82− errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, lspErrs[protocol.SeverityError])))
83− }
84− if lspErrs[protocol.SeverityWarning] > 0 {
85− errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, lspErrs[protocol.SeverityWarning])))
86− }
87− if lspErrs[protocol.SeverityHint] > 0 {
88− errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.HintIcon, lspErrs[protocol.SeverityHint])))
89− }
90− if lspErrs[protocol.SeverityInformation] > 0 {
91− errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s %d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
92− }
93− extraContent = strings.Join(errs, " ")
94 }
80 }
95
81
96 lspList = append(lspList,
82 lspList = append(lspList,