on main help sign in
Merge remote-tracking branch 'origin/main' into feat/whitelist-commands
2128964Kujtim Hoxha
README.md+2−2
@@ -182,7 +182,7 @@ Crush includes a permission system to control which tools can be executed withou
182The `allowed_tools` array accepts:
182The `allowed_tools` array accepts:
183
183
184- Tool names (e.g., `"view"`) - allows all actions for that tool
184- Tool names (e.g., `"view"`) - allows all actions for that tool
185−- Tool:action combinations (e.g., `"bash:read"`) - allows only specific actions
185+- Tool:action combinations (e.g., `"edit:write"`) - allows only specific actions
186
186
187You can also skip all permission prompts entirely by running Crush with the `--yolo` flag.
187You can also skip all permission prompts entirely by running Crush with the `--yolo` flag.
188
188
@@ -199,7 +199,7 @@ Crush supports all OpenAI-compatible APIs. Here's an example configuration for D
199 "models": [
199 "models": [
200 {
200 {
201 "id": "deepseek-chat",
201 "id": "deepseek-chat",
202− "model": "Deepseek V3",
202+ "name": "Deepseek V3",
203 "cost_per_1m_in": 0.27,
203 "cost_per_1m_in": 0.27,
204 "cost_per_1m_out": 1.1,
204 "cost_per_1m_out": 1.1,
205 "cost_per_1m_in_cached": 0.07,
205 "cost_per_1m_in_cached": 0.07,
go.mod+1
@@ -13,6 +13,7 @@ require (
13 github.com/charlievieth/fastwalk v1.0.11
13 github.com/charlievieth/fastwalk v1.0.11
14 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
14 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
15 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac
15 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac
16+ github.com/charmbracelet/catwalk v0.3.1
16 github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674
17 github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674
17 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
18 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
18 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112
19 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112
go.sum+2−2
@@ -72,6 +72,8 @@ github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
72github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
72github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
73github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac h1:murtkvFYxZ/73vk4Z/tpE4biB+WDZcFmmBp8je/yV6M=
73github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac h1:murtkvFYxZ/73vk4Z/tpE4biB+WDZcFmmBp8je/yV6M=
74github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac/go.mod h1:m240IQxo1/eDQ7klblSzOCAUyc3LddHcV3Rc/YEGAgw=
74github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac/go.mod h1:m240IQxo1/eDQ7klblSzOCAUyc3LddHcV3Rc/YEGAgw=
75+github.com/charmbracelet/catwalk v0.3.1 h1:MkGWspcMyE659zDkqS+9wsaCMTKRFEDBFY2A2sap6+U=
76+github.com/charmbracelet/catwalk v0.3.1/go.mod h1:gUUCqqZ8bk4D7ZzGTu3I77k7cC2x4exRuJBN1H2u2pc=
75github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
77github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
76github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
78github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
77github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0=
79github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0=
@@ -82,8 +84,6 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112
82github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112/go.mod h1:BXY7j7rZgAprFwzNcO698++5KTd6GKI6lU83Pr4o0r0=
84github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112/go.mod h1:BXY7j7rZgAprFwzNcO698++5KTd6GKI6lU83Pr4o0r0=
83github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
85github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
84github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
86github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
85−github.com/charmbracelet/ultraviolet v0.0.0-20250721205647-f6ac6eda5d42 h1:Zqw2oP9Wo8VzMijVJbtIJcAaZviYyU07stvmCFCfn0Y=
86−github.com/charmbracelet/ultraviolet v0.0.0-20250721205647-f6ac6eda5d42/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
87github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1 h1:tsw1mOuIEIKlmm614bXctvJ3aavaFhyPG+y+wrKtuKQ=
87github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1 h1:tsw1mOuIEIKlmm614bXctvJ3aavaFhyPG+y+wrKtuKQ=
88github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
88github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc=
89github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
89github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
internal/app/app.go+8−4
@@ -161,16 +161,20 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
161
161
162 if result.Error != nil {
162 if result.Error != nil {
163 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
163 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
164− slog.Info("Agent processing cancelled", "session_id", sess.ID)
164+ slog.Info("Non-interactive: agent processing cancelled", "session_id", sess.ID)
165 return nil
165 return nil
166 }
166 }
167 return fmt.Errorf("agent processing failed: %w", result.Error)
167 return fmt.Errorf("agent processing failed: %w", result.Error)
168 }
168 }
169
169
170− part := result.Message.Content().String()[readBts:]
170+ msgContent := result.Message.Content().String()
171− fmt.Println(part)
171+ if len(msgContent) < readBts {
172+ slog.Error("Non-interactive: message content is shorter than read bytes", "message_length", len(msgContent), "read_bytes", readBts)
173+ return fmt.Errorf("message content is shorter than read bytes: %d < %d", len(msgContent), readBts)
174+ }
175+ fmt.Println(msgContent[readBts:])
172
176
173− slog.Info("Non-interactive run completed", "session_id", sess.ID)
177+ slog.Info("Non-interactive: run completed", "session_id", sess.ID)
174 return nil
178 return nil
175
179
176 case event := <-messageEvents:
180 case event := <-messageEvents:
internal/cmd/root.go+3−3
@@ -88,14 +88,14 @@ to assist developers in writing, debugging, and understanding code directly from
88
88
89 app, err := app.New(ctx, conn, cfg)
89 app, err := app.New(ctx, conn, cfg)
90 if err != nil {
90 if err != nil {
91− slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
91+ slog.Error("Failed to create app instance", "error", err)
92 return err
92 return err
93 }
93 }
94 defer app.Shutdown()
94 defer app.Shutdown()
95
95
96 prompt, err = maybePrependStdin(prompt)
96 prompt, err = maybePrependStdin(prompt)
97 if err != nil {
97 if err != nil {
98− slog.Error(fmt.Sprintf("Failed to read from stdin: %v", err))
98+ slog.Error("Failed to read from stdin", "error", err)
99 return err
99 return err
100 }
100 }
101
101
@@ -117,7 +117,7 @@ to assist developers in writing, debugging, and understanding code directly from
117 go app.Subscribe(program)
117 go app.Subscribe(program)
118
118
119 if _, err := program.Run(); err != nil {
119 if _, err := program.Run(); err != nil {
120− slog.Error(fmt.Sprintf("TUI run error: %v", err))
120+ slog.Error("TUI run error", "error", err)
121 return fmt.Errorf("TUI error: %v", err)
121 return fmt.Errorf("TUI error: %v", err)
122 }
122 }
123 return nil
123 return nil
internal/config/config.go+12−12
@@ -9,8 +9,8 @@ import (
9 "strings"
9 "strings"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/env"
13 "github.com/charmbracelet/crush/internal/env"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/tidwall/sjson"
14 "github.com/tidwall/sjson"
15 "golang.org/x/exp/slog"
15 "golang.org/x/exp/slog"
16)
16)
@@ -70,7 +70,7 @@ type ProviderConfig struct {
70 // The provider's API endpoint.
70 // The provider's API endpoint.
71 BaseURL string `json:"base_url,omitempty"`
71 BaseURL string `json:"base_url,omitempty"`
72 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
72 // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai.
73− Type provider.Type `json:"type,omitempty"`
73+ Type catwalk.Type `json:"type,omitempty"`
74 // The provider's API key.
74 // The provider's API key.
75 APIKey string `json:"api_key,omitempty"`
75 APIKey string `json:"api_key,omitempty"`
76 // Marks the provider as disabled.
76 // Marks the provider as disabled.
@@ -85,7 +85,7 @@ type ProviderConfig struct {
85 ExtraParams map[string]string `json:"-"`
85 ExtraParams map[string]string `json:"-"`
86
86
87 // The provider models
87 // The provider models
88− Models []provider.Model `json:"models,omitempty"`
88+ Models []catwalk.Model `json:"models,omitempty"`
89}
89}
90
90
91type MCPType string
91type MCPType string
@@ -256,8 +256,8 @@ type Config struct {
256 Agents map[string]Agent `json:"-"`
256 Agents map[string]Agent `json:"-"`
257 // TODO: find a better way to do this this should probably not be part of the config
257 // TODO: find a better way to do this this should probably not be part of the config
258 resolver VariableResolver
258 resolver VariableResolver
259− dataConfigDir string `json:"-"`
259+ dataConfigDir string `json:"-"`
260− knownProviders []provider.Provider `json:"-"`
260+ knownProviders []catwalk.Provider `json:"-"`
261}
261}
262
262
263func (c *Config) WorkingDir() string {
263func (c *Config) WorkingDir() string {
@@ -279,7 +279,7 @@ func (c *Config) IsConfigured() bool {
279 return len(c.EnabledProviders()) > 0
279 return len(c.EnabledProviders()) > 0
280}
280}
281
281
282−func (c *Config) GetModel(provider, model string) *provider.Model {
282+func (c *Config) GetModel(provider, model string) *catwalk.Model {
283 if providerConfig, ok := c.Providers[provider]; ok {
283 if providerConfig, ok := c.Providers[provider]; ok {
284 for _, m := range providerConfig.Models {
284 for _, m := range providerConfig.Models {
285 if m.ID == model {
285 if m.ID == model {
@@ -301,7 +301,7 @@ func (c *Config) GetProviderForModel(modelType SelectedModelType) *ProviderConfi
301 return nil
301 return nil
302}
302}
303
303
304−func (c *Config) GetModelByType(modelType SelectedModelType) *provider.Model {
304+func (c *Config) GetModelByType(modelType SelectedModelType) *catwalk.Model {
305 model, ok := c.Models[modelType]
305 model, ok := c.Models[modelType]
306 if !ok {
306 if !ok {
307 return nil
307 return nil
@@ -309,7 +309,7 @@ func (c *Config) GetModelByType(modelType SelectedModelType) *provider.Model {
309 return c.GetModel(model.Provider, model.Model)
309 return c.GetModel(model.Provider, model.Model)
310}
310}
311
311
312−func (c *Config) LargeModel() *provider.Model {
312+func (c *Config) LargeModel() *catwalk.Model {
313 model, ok := c.Models[SelectedModelTypeLarge]
313 model, ok := c.Models[SelectedModelTypeLarge]
314 if !ok {
314 if !ok {
315 return nil
315 return nil
@@ -317,7 +317,7 @@ func (c *Config) LargeModel() *provider.Model {
317 return c.GetModel(model.Provider, model.Model)
317 return c.GetModel(model.Provider, model.Model)
318}
318}
319
319
320−func (c *Config) SmallModel() *provider.Model {
320+func (c *Config) SmallModel() *catwalk.Model {
321 model, ok := c.Models[SelectedModelTypeSmall]
321 model, ok := c.Models[SelectedModelTypeSmall]
322 if !ok {
322 if !ok {
323 return nil
323 return nil
@@ -387,7 +387,7 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
387 return nil
387 return nil
388 }
388 }
389
389
390− var foundProvider *provider.Provider
390+ var foundProvider *catwalk.Provider
391 for _, p := range c.knownProviders {
391 for _, p := range c.knownProviders {
392 if string(p.ID) == providerID {
392 if string(p.ID) == providerID {
393 foundProvider = &p
393 foundProvider = &p
@@ -456,14 +456,14 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
456 headers := make(map[string]string)
456 headers := make(map[string]string)
457 apiKey, _ := resolver.ResolveValue(c.APIKey)
457 apiKey, _ := resolver.ResolveValue(c.APIKey)
458 switch c.Type {
458 switch c.Type {
459− case provider.TypeOpenAI:
459+ case catwalk.TypeOpenAI:
460 baseURL, _ := resolver.ResolveValue(c.BaseURL)
460 baseURL, _ := resolver.ResolveValue(c.BaseURL)
461 if baseURL == "" {
461 if baseURL == "" {
462 baseURL = "https://api.openai.com/v1"
462 baseURL = "https://api.openai.com/v1"
463 }
463 }
464 testURL = baseURL + "/models"
464 testURL = baseURL + "/models"
465 headers["Authorization"] = "Bearer " + apiKey
465 headers["Authorization"] = "Bearer " + apiKey
466− case provider.TypeAnthropic:
466+ case catwalk.TypeAnthropic:
467 baseURL, _ := resolver.ResolveValue(c.BaseURL)
467 baseURL, _ := resolver.ResolveValue(c.BaseURL)
468 if baseURL == "" {
468 if baseURL == "" {
469 baseURL = "https://api.anthropic.com/v1"
469 baseURL = "https://api.anthropic.com/v1"
internal/config/load.go+19−18
@@ -11,13 +11,14 @@ import (
11 "strings"
11 "strings"
12 "sync"
12 "sync"
13
13
14+ "github.com/charmbracelet/catwalk/pkg/catwalk"
14 "github.com/charmbracelet/crush/internal/env"
15 "github.com/charmbracelet/crush/internal/env"
15− "github.com/charmbracelet/crush/internal/fur/client"
16− "github.com/charmbracelet/crush/internal/fur/provider"
17 "github.com/charmbracelet/crush/internal/log"
16 "github.com/charmbracelet/crush/internal/log"
18 "golang.org/x/exp/slog"
17 "golang.org/x/exp/slog"
19)
18)
20
19
20+const catwalkURL = "https://catwalk.charm.sh"
21+
21// LoadReader config via io.Reader.
22// LoadReader config via io.Reader.
22func LoadReader(fd io.Reader) (*Config, error) {
23func LoadReader(fd io.Reader) (*Config, error) {
23 data, err := io.ReadAll(fd)
24 data, err := io.ReadAll(fd)
@@ -61,8 +62,8 @@ func Load(workingDir string, debug bool) (*Config, error) {
61 cfg.Options.Debug,
62 cfg.Options.Debug,
62 )
63 )
63
64
64− // Load known providers, this loads the config from fur
65+ // Load known providers, this loads the config from catwalk
65− providers, err := LoadProviders(client.New())
66+ providers, err := LoadProviders(catwalk.NewWithURL(catwalkURL))
66 if err != nil || len(providers) == 0 {
67 if err != nil || len(providers) == 0 {
67 return nil, fmt.Errorf("failed to load providers: %w", err)
68 return nil, fmt.Errorf("failed to load providers: %w", err)
68 }
69 }
@@ -81,7 +82,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
81 var wg sync.WaitGroup
82 var wg sync.WaitGroup
82
83
83 for _, p := range cfg.Providers {
84 for _, p := range cfg.Providers {
84− if p.Type == provider.TypeOpenAI || p.Type == provider.TypeAnthropic {
85+ if p.Type == catwalk.TypeOpenAI || p.Type == catwalk.TypeAnthropic {
85 wg.Add(1)
86 wg.Add(1)
86 go func(provider ProviderConfig) {
87 go func(provider ProviderConfig) {
87 defer wg.Done()
88 defer wg.Done()
@@ -117,7 +118,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
117 return cfg, nil
118 return cfg, nil
118}
119}
119
120
120−func (c *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []provider.Provider) error {
121+func (c *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []catwalk.Provider) error {
121 knownProviderNames := make(map[string]bool)
122 knownProviderNames := make(map[string]bool)
122 for _, p := range knownProviders {
123 for _, p := range knownProviders {
123 knownProviderNames[string(p.ID)] = true
124 knownProviderNames[string(p.ID)] = true
@@ -136,7 +137,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
136 p.APIKey = config.APIKey
137 p.APIKey = config.APIKey
137 }
138 }
138 if len(config.Models) > 0 {
139 if len(config.Models) > 0 {
139− models := []provider.Model{}
140+ models := []catwalk.Model{}
140 seen := make(map[string]bool)
141 seen := make(map[string]bool)
141
142
142 for _, model := range config.Models {
143 for _, model := range config.Models {
@@ -144,8 +145,8 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
144 continue
145 continue
145 }
146 }
146 seen[model.ID] = true
147 seen[model.ID] = true
147− if model.Model == "" {
148+ if model.Name == "" {
148− model.Model = model.ID
149+ model.Name = model.ID
149 }
150 }
150 models = append(models, model)
151 models = append(models, model)
151 }
152 }
@@ -154,8 +155,8 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
154 continue
155 continue
155 }
156 }
156 seen[model.ID] = true
157 seen[model.ID] = true
157− if model.Model == "" {
158+ if model.Name == "" {
158− model.Model = model.ID
159+ model.Name = model.ID
159 }
160 }
160 models = append(models, model)
161 models = append(models, model)
161 }
162 }
@@ -178,7 +179,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
178
179
179 switch p.ID {
180 switch p.ID {
180 // Handle specific providers that require additional configuration
181 // Handle specific providers that require additional configuration
181− case provider.InferenceProviderVertexAI:
182+ case catwalk.InferenceProviderVertexAI:
182 if !hasVertexCredentials(env) {
183 if !hasVertexCredentials(env) {
183 if configExists {
184 if configExists {
184 slog.Warn("Skipping Vertex AI provider due to missing credentials")
185 slog.Warn("Skipping Vertex AI provider due to missing credentials")
@@ -188,7 +189,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
188 }
189 }
189 prepared.ExtraParams["project"] = env.Get("GOOGLE_CLOUD_PROJECT")
190 prepared.ExtraParams["project"] = env.Get("GOOGLE_CLOUD_PROJECT")
190 prepared.ExtraParams["location"] = env.Get("GOOGLE_CLOUD_LOCATION")
191 prepared.ExtraParams["location"] = env.Get("GOOGLE_CLOUD_LOCATION")
191− case provider.InferenceProviderAzure:
192+ case catwalk.InferenceProviderAzure:
192 endpoint, err := resolver.ResolveValue(p.APIEndpoint)
193 endpoint, err := resolver.ResolveValue(p.APIEndpoint)
193 if err != nil || endpoint == "" {
194 if err != nil || endpoint == "" {
194 if configExists {
195 if configExists {
@@ -199,7 +200,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
199 }
200 }
200 prepared.BaseURL = endpoint
201 prepared.BaseURL = endpoint
201 prepared.ExtraParams["apiVersion"] = env.Get("AZURE_OPENAI_API_VERSION")
202 prepared.ExtraParams["apiVersion"] = env.Get("AZURE_OPENAI_API_VERSION")
202− case provider.InferenceProviderBedrock:
203+ case catwalk.InferenceProviderBedrock:
203 if !hasAWSCredentials(env) {
204 if !hasAWSCredentials(env) {
204 if configExists {
205 if configExists {
205 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
206 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
@@ -239,7 +240,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
239 }
240 }
240 // default to OpenAI if not set
241 // default to OpenAI if not set
241 if providerConfig.Type == "" {
242 if providerConfig.Type == "" {
242− providerConfig.Type = provider.TypeOpenAI
243+ providerConfig.Type = catwalk.TypeOpenAI
243 }
244 }
244
245
245 if providerConfig.Disable {
246 if providerConfig.Disable {
@@ -260,7 +261,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
260 delete(c.Providers, id)
261 delete(c.Providers, id)
261 continue
262 continue
262 }
263 }
263− if providerConfig.Type != provider.TypeOpenAI {
264+ if providerConfig.Type != catwalk.TypeOpenAI {
264 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
265 slog.Warn("Skipping custom provider because the provider type is not supported", "provider", id, "type", providerConfig.Type)
265 delete(c.Providers, id)
266 delete(c.Providers, id)
266 continue
267 continue
@@ -315,7 +316,7 @@ func (c *Config) setDefaults(workingDir string) {
315 c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
316 c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
316}
317}
317
318
318−func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
319+func (c *Config) defaultModelSelection(knownProviders []catwalk.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
319 if len(knownProviders) == 0 && len(c.Providers) == 0 {
320 if len(knownProviders) == 0 && len(c.Providers) == 0 {
320 err = fmt.Errorf("no providers configured, please configure at least one provider")
321 err = fmt.Errorf("no providers configured, please configure at least one provider")
321 return
322 return
@@ -384,7 +385,7 @@ func (c *Config) defaultModelSelection(knownProviders []provider.Provider) (larg
384 return
385 return
385}
386}
386
387
387−func (c *Config) configureSelectedModels(knownProviders []provider.Provider) error {
388+func (c *Config) configureSelectedModels(knownProviders []catwalk.Provider) error {
388 defaultLarge, defaultSmall, err := c.defaultModelSelection(knownProviders)
389 defaultLarge, defaultSmall, err := c.defaultModelSelection(knownProviders)
389 if err != nil {
390 if err != nil {
390 return fmt.Errorf("failed to select default models: %w", err)
391 return fmt.Errorf("failed to select default models: %w", err)
internal/config/load_test.go+80−80
@@ -8,8 +8,8 @@ import (
8 "strings"
8 "strings"
9 "testing"
9 "testing"
10
10
11+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/crush/internal/env"
12 "github.com/charmbracelet/crush/internal/env"
12− "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/assert"
14)
14)
15
15
@@ -54,12 +54,12 @@ func TestConfig_setDefaults(t *testing.T) {
54}
54}
55
55
56func TestConfig_configureProviders(t *testing.T) {
56func TestConfig_configureProviders(t *testing.T) {
57− knownProviders := []provider.Provider{
57+ knownProviders := []catwalk.Provider{
58 {
58 {
59 ID: "openai",
59 ID: "openai",
60 APIKey: "$OPENAI_API_KEY",
60 APIKey: "$OPENAI_API_KEY",
61 APIEndpoint: "https://api.openai.com/v1",
61 APIEndpoint: "https://api.openai.com/v1",
62− Models: []provider.Model{{
62+ Models: []catwalk.Model{{
63 ID: "test-model",
63 ID: "test-model",
64 }},
64 }},
65 },
65 },
@@ -80,12 +80,12 @@ func TestConfig_configureProviders(t *testing.T) {
80}
80}
81
81
82func TestConfig_configureProvidersWithOverride(t *testing.T) {
82func TestConfig_configureProvidersWithOverride(t *testing.T) {
83− knownProviders := []provider.Provider{
83+ knownProviders := []catwalk.Provider{
84 {
84 {
85 ID: "openai",
85 ID: "openai",
86 APIKey: "$OPENAI_API_KEY",
86 APIKey: "$OPENAI_API_KEY",
87 APIEndpoint: "https://api.openai.com/v1",
87 APIEndpoint: "https://api.openai.com/v1",
88− Models: []provider.Model{{
88+ Models: []catwalk.Model{{
89 ID: "test-model",
89 ID: "test-model",
90 }},
90 }},
91 },
91 },
@@ -96,10 +96,10 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
96 "openai": {
96 "openai": {
97 APIKey: "xyz",
97 APIKey: "xyz",
98 BaseURL: "https://api.openai.com/v2",
98 BaseURL: "https://api.openai.com/v2",
99− Models: []provider.Model{
99+ Models: []catwalk.Model{
100 {
100 {
101− ID: "test-model",
101+ ID: "test-model",
102− Model: "Updated",
102+ Name: "Updated",
103 },
103 },
104 {
104 {
105 ID: "another-model",
105 ID: "another-model",
@@ -122,16 +122,16 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
122 assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
122 assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
123 assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
123 assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
124 assert.Len(t, cfg.Providers["openai"].Models, 2)
124 assert.Len(t, cfg.Providers["openai"].Models, 2)
125− assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Model)
125+ assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Name)
126}
126}
127
127
128func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
128func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
129− knownProviders := []provider.Provider{
129+ knownProviders := []catwalk.Provider{
130 {
130 {
131 ID: "openai",
131 ID: "openai",
132 APIKey: "$OPENAI_API_KEY",
132 APIKey: "$OPENAI_API_KEY",
133 APIEndpoint: "https://api.openai.com/v1",
133 APIEndpoint: "https://api.openai.com/v1",
134− Models: []provider.Model{{
134+ Models: []catwalk.Model{{
135 ID: "test-model",
135 ID: "test-model",
136 }},
136 }},
137 },
137 },
@@ -142,7 +142,7 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
142 "custom": {
142 "custom": {
143 APIKey: "xyz",
143 APIKey: "xyz",
144 BaseURL: "https://api.someendpoint.com/v2",
144 BaseURL: "https://api.someendpoint.com/v2",
145− Models: []provider.Model{
145+ Models: []catwalk.Model{
146 {
146 {
147 ID: "test-model",
147 ID: "test-model",
148 },
148 },
@@ -172,12 +172,12 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
172}
172}
173
173
174func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
174func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
175− knownProviders := []provider.Provider{
175+ knownProviders := []catwalk.Provider{
176 {
176 {
177− ID: provider.InferenceProviderBedrock,
177+ ID: catwalk.InferenceProviderBedrock,
178 APIKey: "",
178 APIKey: "",
179 APIEndpoint: "",
179 APIEndpoint: "",
180− Models: []provider.Model{{
180+ Models: []catwalk.Model{{
181 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
181 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
182 }},
182 }},
183 },
183 },
@@ -201,12 +201,12 @@ func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
201}
201}
202
202
203func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
203func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
204− knownProviders := []provider.Provider{
204+ knownProviders := []catwalk.Provider{
205 {
205 {
206− ID: provider.InferenceProviderBedrock,
206+ ID: catwalk.InferenceProviderBedrock,
207 APIKey: "",
207 APIKey: "",
208 APIEndpoint: "",
208 APIEndpoint: "",
209− Models: []provider.Model{{
209+ Models: []catwalk.Model{{
210 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
210 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
211 }},
211 }},
212 },
212 },
@@ -223,12 +223,12 @@ func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
223}
223}
224
224
225func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
225func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
226− knownProviders := []provider.Provider{
226+ knownProviders := []catwalk.Provider{
227 {
227 {
228− ID: provider.InferenceProviderBedrock,
228+ ID: catwalk.InferenceProviderBedrock,
229 APIKey: "",
229 APIKey: "",
230 APIEndpoint: "",
230 APIEndpoint: "",
231− Models: []provider.Model{{
231+ Models: []catwalk.Model{{
232 ID: "some-random-model",
232 ID: "some-random-model",
233 }},
233 }},
234 },
234 },
@@ -246,12 +246,12 @@ func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
246}
246}
247
247
248func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
248func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
249− knownProviders := []provider.Provider{
249+ knownProviders := []catwalk.Provider{
250 {
250 {
251− ID: provider.InferenceProviderVertexAI,
251+ ID: catwalk.InferenceProviderVertexAI,
252 APIKey: "",
252 APIKey: "",
253 APIEndpoint: "",
253 APIEndpoint: "",
254− Models: []provider.Model{{
254+ Models: []catwalk.Model{{
255 ID: "gemini-pro",
255 ID: "gemini-pro",
256 }},
256 }},
257 },
257 },
@@ -278,12 +278,12 @@ func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
278}
278}
279
279
280func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
280func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
281− knownProviders := []provider.Provider{
281+ knownProviders := []catwalk.Provider{
282 {
282 {
283− ID: provider.InferenceProviderVertexAI,
283+ ID: catwalk.InferenceProviderVertexAI,
284 APIKey: "",
284 APIKey: "",
285 APIEndpoint: "",
285 APIEndpoint: "",
286− Models: []provider.Model{{
286+ Models: []catwalk.Model{{
287 ID: "gemini-pro",
287 ID: "gemini-pro",
288 }},
288 }},
289 },
289 },
@@ -304,12 +304,12 @@ func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
304}
304}
305
305
306func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
306func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
307− knownProviders := []provider.Provider{
307+ knownProviders := []catwalk.Provider{
308 {
308 {
309− ID: provider.InferenceProviderVertexAI,
309+ ID: catwalk.InferenceProviderVertexAI,
310 APIKey: "",
310 APIKey: "",
311 APIEndpoint: "",
311 APIEndpoint: "",
312− Models: []provider.Model{{
312+ Models: []catwalk.Model{{
313 ID: "gemini-pro",
313 ID: "gemini-pro",
314 }},
314 }},
315 },
315 },
@@ -329,12 +329,12 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
329}
329}
330
330
331func TestConfig_configureProvidersSetProviderID(t *testing.T) {
331func TestConfig_configureProvidersSetProviderID(t *testing.T) {
332− knownProviders := []provider.Provider{
332+ knownProviders := []catwalk.Provider{
333 {
333 {
334 ID: "openai",
334 ID: "openai",
335 APIKey: "$OPENAI_API_KEY",
335 APIKey: "$OPENAI_API_KEY",
336 APIEndpoint: "https://api.openai.com/v1",
336 APIEndpoint: "https://api.openai.com/v1",
337− Models: []provider.Model{{
337+ Models: []catwalk.Model{{
338 ID: "test-model",
338 ID: "test-model",
339 }},
339 }},
340 },
340 },
@@ -450,12 +450,12 @@ func TestConfig_IsConfigured(t *testing.T) {
450}
450}
451
451
452func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
452func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
453− knownProviders := []provider.Provider{
453+ knownProviders := []catwalk.Provider{
454 {
454 {
455 ID: "openai",
455 ID: "openai",
456 APIKey: "$OPENAI_API_KEY",
456 APIKey: "$OPENAI_API_KEY",
457 APIEndpoint: "https://api.openai.com/v1",
457 APIEndpoint: "https://api.openai.com/v1",
458− Models: []provider.Model{{
458+ Models: []catwalk.Model{{
459 ID: "test-model",
459 ID: "test-model",
460 }},
460 }},
461 },
461 },
@@ -489,7 +489,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
489 Providers: map[string]ProviderConfig{
489 Providers: map[string]ProviderConfig{
490 "custom": {
490 "custom": {
491 BaseURL: "https://api.custom.com/v1",
491 BaseURL: "https://api.custom.com/v1",
492− Models: []provider.Model{{
492+ Models: []catwalk.Model{{
493 ID: "test-model",
493 ID: "test-model",
494 }},
494 }},
495 },
495 },
@@ -502,7 +502,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
502
502
503 env := env.NewFromMap(map[string]string{})
503 env := env.NewFromMap(map[string]string{})
504 resolver := NewEnvironmentVariableResolver(env)
504 resolver := NewEnvironmentVariableResolver(env)
505− err := cfg.configureProviders(env, resolver, []provider.Provider{})
505+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
506 assert.NoError(t, err)
506 assert.NoError(t, err)
507
507
508 assert.Len(t, cfg.Providers, 1)
508 assert.Len(t, cfg.Providers, 1)
@@ -515,7 +515,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
515 Providers: map[string]ProviderConfig{
515 Providers: map[string]ProviderConfig{
516 "custom": {
516 "custom": {
517 APIKey: "test-key",
517 APIKey: "test-key",
518− Models: []provider.Model{{
518+ Models: []catwalk.Model{{
519 ID: "test-model",
519 ID: "test-model",
520 }},
520 }},
521 },
521 },
@@ -525,7 +525,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
525
525
526 env := env.NewFromMap(map[string]string{})
526 env := env.NewFromMap(map[string]string{})
527 resolver := NewEnvironmentVariableResolver(env)
527 resolver := NewEnvironmentVariableResolver(env)
528− err := cfg.configureProviders(env, resolver, []provider.Provider{})
528+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
529 assert.NoError(t, err)
529 assert.NoError(t, err)
530
530
531 assert.Len(t, cfg.Providers, 0)
531 assert.Len(t, cfg.Providers, 0)
@@ -539,7 +539,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
539 "custom": {
539 "custom": {
540 APIKey: "test-key",
540 APIKey: "test-key",
541 BaseURL: "https://api.custom.com/v1",
541 BaseURL: "https://api.custom.com/v1",
542− Models: []provider.Model{},
542+ Models: []catwalk.Model{},
543 },
543 },
544 },
544 },
545 }
545 }
@@ -547,7 +547,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
547
547
548 env := env.NewFromMap(map[string]string{})
548 env := env.NewFromMap(map[string]string{})
549 resolver := NewEnvironmentVariableResolver(env)
549 resolver := NewEnvironmentVariableResolver(env)
550− err := cfg.configureProviders(env, resolver, []provider.Provider{})
550+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
551 assert.NoError(t, err)
551 assert.NoError(t, err)
552
552
553 assert.Len(t, cfg.Providers, 0)
553 assert.Len(t, cfg.Providers, 0)
@@ -562,7 +562,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
562 APIKey: "test-key",
562 APIKey: "test-key",
563 BaseURL: "https://api.custom.com/v1",
563 BaseURL: "https://api.custom.com/v1",
564 Type: "unsupported",
564 Type: "unsupported",
565− Models: []provider.Model{{
565+ Models: []catwalk.Model{{
566 ID: "test-model",
566 ID: "test-model",
567 }},
567 }},
568 },
568 },
@@ -572,7 +572,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
572
572
573 env := env.NewFromMap(map[string]string{})
573 env := env.NewFromMap(map[string]string{})
574 resolver := NewEnvironmentVariableResolver(env)
574 resolver := NewEnvironmentVariableResolver(env)
575− err := cfg.configureProviders(env, resolver, []provider.Provider{})
575+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
576 assert.NoError(t, err)
576 assert.NoError(t, err)
577
577
578 assert.Len(t, cfg.Providers, 0)
578 assert.Len(t, cfg.Providers, 0)
@@ -586,8 +586,8 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
586 "custom": {
586 "custom": {
587 APIKey: "test-key",
587 APIKey: "test-key",
588 BaseURL: "https://api.custom.com/v1",
588 BaseURL: "https://api.custom.com/v1",
589− Type: provider.TypeOpenAI,
589+ Type: catwalk.TypeOpenAI,
590− Models: []provider.Model{{
590+ Models: []catwalk.Model{{
591 ID: "test-model",
591 ID: "test-model",
592 }},
592 }},
593 },
593 },
@@ -597,7 +597,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
597
597
598 env := env.NewFromMap(map[string]string{})
598 env := env.NewFromMap(map[string]string{})
599 resolver := NewEnvironmentVariableResolver(env)
599 resolver := NewEnvironmentVariableResolver(env)
600− err := cfg.configureProviders(env, resolver, []provider.Provider{})
600+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
601 assert.NoError(t, err)
601 assert.NoError(t, err)
602
602
603 assert.Len(t, cfg.Providers, 1)
603 assert.Len(t, cfg.Providers, 1)
@@ -614,9 +614,9 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
614 "custom": {
614 "custom": {
615 APIKey: "test-key",
615 APIKey: "test-key",
616 BaseURL: "https://api.custom.com/v1",
616 BaseURL: "https://api.custom.com/v1",
617− Type: provider.TypeOpenAI,
617+ Type: catwalk.TypeOpenAI,
618 Disable: true,
618 Disable: true,
619− Models: []provider.Model{{
619+ Models: []catwalk.Model{{
620 ID: "test-model",
620 ID: "test-model",
621 }},
621 }},
622 },
622 },
@@ -626,7 +626,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
626
626
627 env := env.NewFromMap(map[string]string{})
627 env := env.NewFromMap(map[string]string{})
628 resolver := NewEnvironmentVariableResolver(env)
628 resolver := NewEnvironmentVariableResolver(env)
629− err := cfg.configureProviders(env, resolver, []provider.Provider{})
629+ err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
630 assert.NoError(t, err)
630 assert.NoError(t, err)
631
631
632 assert.Len(t, cfg.Providers, 0)
632 assert.Len(t, cfg.Providers, 0)
@@ -637,12 +637,12 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
637
637
638func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
638func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
639 t.Run("VertexAI provider removed when credentials missing with existing config", func(t *testing.T) {
639 t.Run("VertexAI provider removed when credentials missing with existing config", func(t *testing.T) {
640− knownProviders := []provider.Provider{
640+ knownProviders := []catwalk.Provider{
641 {
641 {
642− ID: provider.InferenceProviderVertexAI,
642+ ID: catwalk.InferenceProviderVertexAI,
643 APIKey: "",
643 APIKey: "",
644 APIEndpoint: "",
644 APIEndpoint: "",
645− Models: []provider.Model{{
645+ Models: []catwalk.Model{{
646 ID: "gemini-pro",
646 ID: "gemini-pro",
647 }},
647 }},
648 },
648 },
@@ -670,12 +670,12 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
670 })
670 })
671
671
672 t.Run("Bedrock provider removed when AWS credentials missing with existing config", func(t *testing.T) {
672 t.Run("Bedrock provider removed when AWS credentials missing with existing config", func(t *testing.T) {
673− knownProviders := []provider.Provider{
673+ knownProviders := []catwalk.Provider{
674 {
674 {
675− ID: provider.InferenceProviderBedrock,
675+ ID: catwalk.InferenceProviderBedrock,
676 APIKey: "",
676 APIKey: "",
677 APIEndpoint: "",
677 APIEndpoint: "",
678− Models: []provider.Model{{
678+ Models: []catwalk.Model{{
679 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
679 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
680 }},
680 }},
681 },
681 },
@@ -701,12 +701,12 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
701 })
701 })
702
702
703 t.Run("provider removed when API key missing with existing config", func(t *testing.T) {
703 t.Run("provider removed when API key missing with existing config", func(t *testing.T) {
704− knownProviders := []provider.Provider{
704+ knownProviders := []catwalk.Provider{
705 {
705 {
706 ID: "openai",
706 ID: "openai",
707 APIKey: "$MISSING_API_KEY",
707 APIKey: "$MISSING_API_KEY",
708 APIEndpoint: "https://api.openai.com/v1",
708 APIEndpoint: "https://api.openai.com/v1",
709− Models: []provider.Model{{
709+ Models: []catwalk.Model{{
710 ID: "test-model",
710 ID: "test-model",
711 }},
711 }},
712 },
712 },
@@ -732,12 +732,12 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
732 })
732 })
733
733
734 t.Run("known provider should still be added if the endpoint is missing the client will use default endpoints", func(t *testing.T) {
734 t.Run("known provider should still be added if the endpoint is missing the client will use default endpoints", func(t *testing.T) {
735− knownProviders := []provider.Provider{
735+ knownProviders := []catwalk.Provider{
736 {
736 {
737 ID: "openai",
737 ID: "openai",
738 APIKey: "$OPENAI_API_KEY",
738 APIKey: "$OPENAI_API_KEY",
739 APIEndpoint: "$MISSING_ENDPOINT",
739 APIEndpoint: "$MISSING_ENDPOINT",
740− Models: []provider.Model{{
740+ Models: []catwalk.Model{{
741 ID: "test-model",
741 ID: "test-model",
742 }},
742 }},
743 },
743 },
@@ -767,13 +767,13 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
767
767
768func TestConfig_defaultModelSelection(t *testing.T) {
768func TestConfig_defaultModelSelection(t *testing.T) {
769 t.Run("default behavior uses the default models for given provider", func(t *testing.T) {
769 t.Run("default behavior uses the default models for given provider", func(t *testing.T) {
770− knownProviders := []provider.Provider{
770+ knownProviders := []catwalk.Provider{
771 {
771 {
772 ID: "openai",
772 ID: "openai",
773 APIKey: "abc",
773 APIKey: "abc",
774 DefaultLargeModelID: "large-model",
774 DefaultLargeModelID: "large-model",
775 DefaultSmallModelID: "small-model",
775 DefaultSmallModelID: "small-model",
776− Models: []provider.Model{
776+ Models: []catwalk.Model{
777 {
777 {
778 ID: "large-model",
778 ID: "large-model",
779 DefaultMaxTokens: 1000,
779 DefaultMaxTokens: 1000,
@@ -803,13 +803,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
803 assert.Equal(t, int64(500), small.MaxTokens)
803 assert.Equal(t, int64(500), small.MaxTokens)
804 })
804 })
805 t.Run("should error if no providers configured", func(t *testing.T) {
805 t.Run("should error if no providers configured", func(t *testing.T) {
806− knownProviders := []provider.Provider{
806+ knownProviders := []catwalk.Provider{
807 {
807 {
808 ID: "openai",
808 ID: "openai",
809 APIKey: "$MISSING_KEY",
809 APIKey: "$MISSING_KEY",
810 DefaultLargeModelID: "large-model",
810 DefaultLargeModelID: "large-model",
811 DefaultSmallModelID: "small-model",
811 DefaultSmallModelID: "small-model",
812− Models: []provider.Model{
812+ Models: []catwalk.Model{
813 {
813 {
814 ID: "large-model",
814 ID: "large-model",
815 DefaultMaxTokens: 1000,
815 DefaultMaxTokens: 1000,
@@ -833,13 +833,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
833 assert.Error(t, err)
833 assert.Error(t, err)
834 })
834 })
835 t.Run("should error if model is missing", func(t *testing.T) {
835 t.Run("should error if model is missing", func(t *testing.T) {
836− knownProviders := []provider.Provider{
836+ knownProviders := []catwalk.Provider{
837 {
837 {
838 ID: "openai",
838 ID: "openai",
839 APIKey: "abc",
839 APIKey: "abc",
840 DefaultLargeModelID: "large-model",
840 DefaultLargeModelID: "large-model",
841 DefaultSmallModelID: "small-model",
841 DefaultSmallModelID: "small-model",
842− Models: []provider.Model{
842+ Models: []catwalk.Model{
843 {
843 {
844 ID: "not-large-model",
844 ID: "not-large-model",
845 DefaultMaxTokens: 1000,
845 DefaultMaxTokens: 1000,
@@ -863,13 +863,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
863 })
863 })
864
864
865 t.Run("should configure the default models with a custom provider", func(t *testing.T) {
865 t.Run("should configure the default models with a custom provider", func(t *testing.T) {
866− knownProviders := []provider.Provider{
866+ knownProviders := []catwalk.Provider{
867 {
867 {
868 ID: "openai",
868 ID: "openai",
869 APIKey: "$MISSING", // will not be included in the config
869 APIKey: "$MISSING", // will not be included in the config
870 DefaultLargeModelID: "large-model",
870 DefaultLargeModelID: "large-model",
871 DefaultSmallModelID: "small-model",
871 DefaultSmallModelID: "small-model",
872− Models: []provider.Model{
872+ Models: []catwalk.Model{
873 {
873 {
874 ID: "not-large-model",
874 ID: "not-large-model",
875 DefaultMaxTokens: 1000,
875 DefaultMaxTokens: 1000,
@@ -887,7 +887,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
887 "custom": {
887 "custom": {
888 APIKey: "test-key",
888 APIKey: "test-key",
889 BaseURL: "https://api.custom.com/v1",
889 BaseURL: "https://api.custom.com/v1",
890− Models: []provider.Model{
890+ Models: []catwalk.Model{
891 {
891 {
892 ID: "model",
892 ID: "model",
893 DefaultMaxTokens: 600,
893 DefaultMaxTokens: 600,
@@ -912,13 +912,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
912 })
912 })
913
913
914 t.Run("should fail if no model configured", func(t *testing.T) {
914 t.Run("should fail if no model configured", func(t *testing.T) {
915− knownProviders := []provider.Provider{
915+ knownProviders := []catwalk.Provider{
916 {
916 {
917 ID: "openai",
917 ID: "openai",
918 APIKey: "$MISSING", // will not be included in the config
918 APIKey: "$MISSING", // will not be included in the config
919 DefaultLargeModelID: "large-model",
919 DefaultLargeModelID: "large-model",
920 DefaultSmallModelID: "small-model",
920 DefaultSmallModelID: "small-model",
921− Models: []provider.Model{
921+ Models: []catwalk.Model{
922 {
922 {
923 ID: "not-large-model",
923 ID: "not-large-model",
924 DefaultMaxTokens: 1000,
924 DefaultMaxTokens: 1000,
@@ -936,7 +936,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
936 "custom": {
936 "custom": {
937 APIKey: "test-key",
937 APIKey: "test-key",
938 BaseURL: "https://api.custom.com/v1",
938 BaseURL: "https://api.custom.com/v1",
939− Models: []provider.Model{},
939+ Models: []catwalk.Model{},
940 },
940 },
941 },
941 },
942 }
942 }
@@ -949,13 +949,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
949 assert.Error(t, err)
949 assert.Error(t, err)
950 })
950 })
951 t.Run("should use the default provider first", func(t *testing.T) {
951 t.Run("should use the default provider first", func(t *testing.T) {
952− knownProviders := []provider.Provider{
952+ knownProviders := []catwalk.Provider{
953 {
953 {
954 ID: "openai",
954 ID: "openai",
955 APIKey: "set",
955 APIKey: "set",
956 DefaultLargeModelID: "large-model",
956 DefaultLargeModelID: "large-model",
957 DefaultSmallModelID: "small-model",
957 DefaultSmallModelID: "small-model",
958− Models: []provider.Model{
958+ Models: []catwalk.Model{
959 {
959 {
960 ID: "large-model",
960 ID: "large-model",
961 DefaultMaxTokens: 1000,
961 DefaultMaxTokens: 1000,
@@ -973,7 +973,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
973 "custom": {
973 "custom": {
974 APIKey: "test-key",
974 APIKey: "test-key",
975 BaseURL: "https://api.custom.com/v1",
975 BaseURL: "https://api.custom.com/v1",
976− Models: []provider.Model{
976+ Models: []catwalk.Model{
977 {
977 {
978 ID: "large-model",
978 ID: "large-model",
979 DefaultMaxTokens: 1000,
979 DefaultMaxTokens: 1000,
@@ -1000,13 +1000,13 @@ func TestConfig_defaultModelSelection(t *testing.T) {
1000
1000
1001func TestConfig_configureSelectedModels(t *testing.T) {
1001func TestConfig_configureSelectedModels(t *testing.T) {
1002 t.Run("should override defaults", func(t *testing.T) {
1002 t.Run("should override defaults", func(t *testing.T) {
1003− knownProviders := []provider.Provider{
1003+ knownProviders := []catwalk.Provider{
1004 {
1004 {
1005 ID: "openai",
1005 ID: "openai",
1006 APIKey: "abc",
1006 APIKey: "abc",
1007 DefaultLargeModelID: "large-model",
1007 DefaultLargeModelID: "large-model",
1008 DefaultSmallModelID: "small-model",
1008 DefaultSmallModelID: "small-model",
1009− Models: []provider.Model{
1009+ Models: []catwalk.Model{
1010 {
1010 {
1011 ID: "larger-model",
1011 ID: "larger-model",
1012 DefaultMaxTokens: 2000,
1012 DefaultMaxTokens: 2000,
@@ -1048,13 +1048,13 @@ func TestConfig_configureSelectedModels(t *testing.T) {
1048 assert.Equal(t, int64(500), small.MaxTokens)
1048 assert.Equal(t, int64(500), small.MaxTokens)
1049 })
1049 })
1050 t.Run("should be possible to use multiple providers", func(t *testing.T) {
1050 t.Run("should be possible to use multiple providers", func(t *testing.T) {
1051− knownProviders := []provider.Provider{
1051+ knownProviders := []catwalk.Provider{
1052 {
1052 {
1053 ID: "openai",
1053 ID: "openai",
1054 APIKey: "abc",
1054 APIKey: "abc",
1055 DefaultLargeModelID: "large-model",
1055 DefaultLargeModelID: "large-model",
1056 DefaultSmallModelID: "small-model",
1056 DefaultSmallModelID: "small-model",
1057− Models: []provider.Model{
1057+ Models: []catwalk.Model{
1058 {
1058 {
1059 ID: "large-model",
1059 ID: "large-model",
1060 DefaultMaxTokens: 1000,
1060 DefaultMaxTokens: 1000,
@@ -1070,7 +1070,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
1070 APIKey: "abc",
1070 APIKey: "abc",
1071 DefaultLargeModelID: "a-large-model",
1071 DefaultLargeModelID: "a-large-model",
1072 DefaultSmallModelID: "a-small-model",
1072 DefaultSmallModelID: "a-small-model",
1073− Models: []provider.Model{
1073+ Models: []catwalk.Model{
1074 {
1074 {
1075 ID: "a-large-model",
1075 ID: "a-large-model",
1076 DefaultMaxTokens: 1000,
1076 DefaultMaxTokens: 1000,
@@ -1111,13 +1111,13 @@ func TestConfig_configureSelectedModels(t *testing.T) {
1111 })
1111 })
1112
1112
1113 t.Run("should override the max tokens only", func(t *testing.T) {
1113 t.Run("should override the max tokens only", func(t *testing.T) {
1114− knownProviders := []provider.Provider{
1114+ knownProviders := []catwalk.Provider{
1115 {
1115 {
1116 ID: "openai",
1116 ID: "openai",
1117 APIKey: "abc",
1117 APIKey: "abc",
1118 DefaultLargeModelID: "large-model",
1118 DefaultLargeModelID: "large-model",
1119 DefaultSmallModelID: "small-model",
1119 DefaultSmallModelID: "small-model",
1120− Models: []provider.Model{
1120+ Models: []catwalk.Model{
1121 {
1121 {
1122 ID: "large-model",
1122 ID: "large-model",
1123 DefaultMaxTokens: 1000,
1123 DefaultMaxTokens: 1000,
internal/config/provider.go+10−11
@@ -7,17 +7,16 @@ import (
7 "runtime"
7 "runtime"
8 "sync"
8 "sync"
9
9
10− "github.com/charmbracelet/crush/internal/fur/client"
10+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11− "github.com/charmbracelet/crush/internal/fur/provider"
12)
11)
13
12
14type ProviderClient interface {
13type ProviderClient interface {
15− GetProviders() ([]provider.Provider, error)
14+ GetProviders() ([]catwalk.Provider, error)
16}
15}
17
16
18var (
17var (
19 providerOnce sync.Once
18 providerOnce sync.Once
20− providerList []provider.Provider
19+ providerList []catwalk.Provider
21)
20)
22
21
23// file to cache provider data
22// file to cache provider data
@@ -41,7 +40,7 @@ func providerCacheFileData() string {
41 return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
40 return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
42}
41}
43
42
44−func saveProvidersInCache(path string, providers []provider.Provider) error {
43+func saveProvidersInCache(path string, providers []catwalk.Provider) error {
45 dir := filepath.Dir(path)
44 dir := filepath.Dir(path)
46 if err := os.MkdirAll(dir, 0o755); err != nil {
45 if err := os.MkdirAll(dir, 0o755); err != nil {
47 return err
46 return err
@@ -55,18 +54,18 @@ func saveProvidersInCache(path string, providers []provider.Provider) error {
55 return os.WriteFile(path, data, 0o644)
54 return os.WriteFile(path, data, 0o644)
56}
55}
57
56
58−func loadProvidersFromCache(path string) ([]provider.Provider, error) {
57+func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
59 data, err := os.ReadFile(path)
58 data, err := os.ReadFile(path)
60 if err != nil {
59 if err != nil {
61 return nil, err
60 return nil, err
62 }
61 }
63
62
64− var providers []provider.Provider
63+ var providers []catwalk.Provider
65 err = json.Unmarshal(data, &providers)
64 err = json.Unmarshal(data, &providers)
66 return providers, err
65 return providers, err
67}
66}
68
67
69−func loadProviders(path string, client ProviderClient) ([]provider.Provider, error) {
68+func loadProviders(path string, client ProviderClient) ([]catwalk.Provider, error) {
70 providers, err := client.GetProviders()
69 providers, err := client.GetProviders()
71 if err != nil {
70 if err != nil {
72 fallbackToCache, err := loadProvidersFromCache(path)
71 fallbackToCache, err := loadProvidersFromCache(path)
@@ -82,11 +81,11 @@ func loadProviders(path string, client ProviderClient) ([]provider.Provider, err
82 return providers, nil
81 return providers, nil
83}
82}
84
83
85−func Providers() ([]provider.Provider, error) {
84+func Providers() ([]catwalk.Provider, error) {
86− return LoadProviders(client.New())
85+ return LoadProviders(catwalk.NewWithURL(catwalkURL))
87}
86}
88
87
89−func LoadProviders(client ProviderClient) ([]provider.Provider, error) {
88+func LoadProviders(client ProviderClient) ([]catwalk.Provider, error) {
90 var err error
89 var err error
91 providerOnce.Do(func() {
90 providerOnce.Do(func() {
92 providerList, err = loadProviders(providerCacheFileData(), client)
91 providerList, err = loadProviders(providerCacheFileData(), client)
internal/config/provider_test.go+4−4
@@ -6,7 +6,7 @@ import (
6 "os"
6 "os"
7 "testing"
7 "testing"
8
8
9− "github.com/charmbracelet/crush/internal/fur/provider"
9+ "github.com/charmbracelet/catwalk/pkg/catwalk"
10 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/assert"
11)
11)
12
12
@@ -14,11 +14,11 @@ type mockProviderClient struct {
14 shouldFail bool
14 shouldFail bool
15}
15}
16
16
17−func (m *mockProviderClient) GetProviders() ([]provider.Provider, error) {
17+func (m *mockProviderClient) GetProviders() ([]catwalk.Provider, error) {
18 if m.shouldFail {
18 if m.shouldFail {
19 return nil, errors.New("failed to load providers")
19 return nil, errors.New("failed to load providers")
20 }
20 }
21− return []provider.Provider{
21+ return []catwalk.Provider{
22 {
22 {
23 Name: "Mock",
23 Name: "Mock",
24 },
24 },
@@ -43,7 +43,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
43 client := &mockProviderClient{shouldFail: true}
43 client := &mockProviderClient{shouldFail: true}
44 tmpPath := t.TempDir() + "/providers.json"
44 tmpPath := t.TempDir() + "/providers.json"
45 // store providers to a temporary file
45 // store providers to a temporary file
46− oldProviders := []provider.Provider{
46+ oldProviders := []catwalk.Provider{
47 {
47 {
48 Name: "OldProvider",
48 Name: "OldProvider",
49 },
49 },
internal/fur/client/client.godeleted−63
@@ -1,63 +0,0 @@
1−// Package client provides a client for interacting with the fur service.
2−package client
3−
4−import (
5− "encoding/json"
6− "fmt"
7− "net/http"
8− "os"
9−
10− "github.com/charmbracelet/crush/internal/fur/provider"
11−)
12−
13−const defaultURL = "https://fur.charm.sh"
14−
15−// Client represents a client for the fur service.
16−type Client struct {
17− baseURL string
18− httpClient *http.Client
19−}
20−
21−// New creates a new client instance
22−// Uses FUR_URL environment variable or falls back to localhost:8080.
23−func New() *Client {
24− baseURL := os.Getenv("FUR_URL")
25− if baseURL == "" {
26− baseURL = defaultURL
27− }
28−
29− return &Client{
30− baseURL: baseURL,
31− httpClient: &http.Client{},
32− }
33−}
34−
35−// NewWithURL creates a new client with a specific URL.
36−func NewWithURL(url string) *Client {
37− return &Client{
38− baseURL: url,
39− httpClient: &http.Client{},
40− }
41−}
42−
43−// GetProviders retrieves all available providers from the service.
44−func (c *Client) GetProviders() ([]provider.Provider, error) {
45− url := fmt.Sprintf("%s/providers", c.baseURL)
46−
47− resp, err := c.httpClient.Get(url) //nolint:noctx
48− if err != nil {
49− return nil, fmt.Errorf("failed to make request: %w", err)
50− }
51− defer resp.Body.Close() //nolint:errcheck
52−
53− if resp.StatusCode != http.StatusOK {
54− return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
55− }
56−
57− var providers []provider.Provider
58− if err := json.NewDecoder(resp.Body).Decode(&providers); err != nil {
59− return nil, fmt.Errorf("failed to decode response: %w", err)
60− }
61−
62− return providers, nil
63−}
internal/fur/provider/provider.godeleted−75
@@ -1,75 +0,0 @@
1−// Package provider provides types and constants for AI providers.
2−package provider
3−
4−// Type represents the type of AI provider.
5−type Type string
6−
7−// All the supported AI provider types.
8−const (
9− TypeOpenAI Type = "openai"
10− TypeAnthropic Type = "anthropic"
11− TypeGemini Type = "gemini"
12− TypeAzure Type = "azure"
13− TypeBedrock Type = "bedrock"
14− TypeVertexAI Type = "vertexai"
15− TypeXAI Type = "xai"
16−)
17−
18−// InferenceProvider represents the inference provider identifier.
19−type InferenceProvider string
20−
21−// All the inference providers supported by the system.
22−const (
23− InferenceProviderOpenAI InferenceProvider = "openai"
24− InferenceProviderAnthropic InferenceProvider = "anthropic"
25− InferenceProviderGemini InferenceProvider = "gemini"
26− InferenceProviderAzure InferenceProvider = "azure"
27− InferenceProviderBedrock InferenceProvider = "bedrock"
28− InferenceProviderVertexAI InferenceProvider = "vertexai"
29− InferenceProviderXAI InferenceProvider = "xai"
30− InferenceProviderGROQ InferenceProvider = "groq"
31− InferenceProviderOpenRouter InferenceProvider = "openrouter"
32−)
33−
34−// Provider represents an AI provider configuration.
35−type Provider struct {
36− Name string `json:"name"`
37− ID InferenceProvider `json:"id"`
38− APIKey string `json:"api_key,omitempty"`
39− APIEndpoint string `json:"api_endpoint,omitempty"`
40− Type Type `json:"type,omitempty"`
41− DefaultLargeModelID string `json:"default_large_model_id,omitempty"`
42− DefaultSmallModelID string `json:"default_small_model_id,omitempty"`
43− Models []Model `json:"models,omitempty"`
44−}
45−
46−// Model represents an AI model configuration.
47−type Model struct {
48− ID string `json:"id"`
49− Model string `json:"model"`
50− CostPer1MIn float64 `json:"cost_per_1m_in"`
51− CostPer1MOut float64 `json:"cost_per_1m_out"`
52− CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
53− CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
54− ContextWindow int64 `json:"context_window"`
55− DefaultMaxTokens int64 `json:"default_max_tokens"`
56− CanReason bool `json:"can_reason"`
57− HasReasoningEffort bool `json:"has_reasoning_efforts"`
58− DefaultReasoningEffort string `json:"default_reasoning_effort,omitempty"`
59− SupportsImages bool `json:"supports_attachments"`
60−}
61−
62−// KnownProviders returns all the known inference providers.
63−func KnownProviders() []InferenceProvider {
64− return []InferenceProvider{
65− InferenceProviderOpenAI,
66− InferenceProviderAnthropic,
67− InferenceProviderGemini,
68− InferenceProviderAzure,
69− InferenceProviderBedrock,
70− InferenceProviderVertexAI,
71− InferenceProviderXAI,
72− InferenceProviderGROQ,
73− InferenceProviderOpenRouter,
74− }
75−}
internal/llm/agent/agent.go+7−7
@@ -10,8 +10,8 @@ import (
10 "sync"
10 "sync"
11 "time"
11 "time"
12
12
13+ "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14− fur "github.com/charmbracelet/crush/internal/fur/provider"
15 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/llm/prompt"
16 "github.com/charmbracelet/crush/internal/llm/prompt"
17 "github.com/charmbracelet/crush/internal/llm/provider"
17 "github.com/charmbracelet/crush/internal/llm/provider"
@@ -52,7 +52,7 @@ type AgentEvent struct {
52
52
53type Service interface {
53type Service interface {
54 pubsub.Suscriber[AgentEvent]
54 pubsub.Suscriber[AgentEvent]
55− Model() fur.Model
55+ Model() catwalk.Model
56 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
56 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
57 Cancel(sessionID string)
57 Cancel(sessionID string)
58 CancelAll()
58 CancelAll()
@@ -219,7 +219,7 @@ func NewAgent(
219 return agent, nil
219 return agent, nil
220}
220}
221
221
222−func (a *agent) Model() fur.Model {
222+func (a *agent) Model() catwalk.Model {
223 return *config.Get().GetModelByType(a.agentCfg.Model)
223 return *config.Get().GetModelByType(a.agentCfg.Model)
224}
224}
225
225
@@ -227,7 +227,7 @@ func (a *agent) Cancel(sessionID string) {
227 // Cancel regular requests
227 // Cancel regular requests
228 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
228 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
229 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
229 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
230− slog.Info(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
230+ slog.Info("Request cancellation initiated", "session_id", sessionID)
231 cancel()
231 cancel()
232 }
232 }
233 }
233 }
@@ -235,7 +235,7 @@ func (a *agent) Cancel(sessionID string) {
235 // Also check for summarize requests
235 // Also check for summarize requests
236 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
236 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
237 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
237 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
238− slog.Info(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
238+ slog.Info("Summarize cancellation initiated", "session_id", sessionID)
239 cancel()
239 cancel()
240 }
240 }
241 }
241 }
@@ -365,7 +365,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
365 })
365 })
366 titleErr := a.generateTitle(context.Background(), sessionID, content)
366 titleErr := a.generateTitle(context.Background(), sessionID, content)
367 if titleErr != nil && !errors.Is(titleErr, context.Canceled) && !errors.Is(titleErr, context.DeadlineExceeded) {
367 if titleErr != nil && !errors.Is(titleErr, context.Canceled) && !errors.Is(titleErr, context.DeadlineExceeded) {
368− slog.Error(fmt.Sprintf("failed to generate title: %v", titleErr))
368+ slog.Error("failed to generate title", "error", titleErr)
369 }
369 }
370 }()
370 }()
371 }
371 }
@@ -638,7 +638,7 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg
638 return nil
638 return nil
639}
639}
640
640
641−func (a *agent) TrackUsage(ctx context.Context, sessionID string, model fur.Model, usage provider.TokenUsage) error {
641+func (a *agent) TrackUsage(ctx context.Context, sessionID string, model catwalk.Model, usage provider.TokenUsage) error {
642 sess, err := a.sessions.Get(ctx, sessionID)
642 sess, err := a.sessions.Get(ctx, sessionID)
643 if err != nil {
643 if err != nil {
644 return fmt.Errorf("failed to get session: %w", err)
644 return fmt.Errorf("failed to get session: %w", err)
internal/llm/prompt/coder.go+3−3
@@ -9,17 +9,17 @@ import (
9 "runtime"
9 "runtime"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/llm/tools"
14 "github.com/charmbracelet/crush/internal/llm/tools"
15)
15)
16
16
17func CoderPrompt(p string, contextFiles ...string) string {
17func CoderPrompt(p string, contextFiles ...string) string {
18 var basePrompt string
18 var basePrompt string
19 switch p {
19 switch p {
20− case string(provider.InferenceProviderOpenAI):
20+ case string(catwalk.InferenceProviderOpenAI):
21 basePrompt = baseOpenAICoderPrompt
21 basePrompt = baseOpenAICoderPrompt
22− case string(provider.InferenceProviderGemini), string(provider.InferenceProviderVertexAI):
22+ case string(catwalk.InferenceProviderGemini), string(catwalk.InferenceProviderVertexAI):
23 basePrompt = baseGeminiCoderPrompt
23 basePrompt = baseGeminiCoderPrompt
24 default:
24 default:
25 basePrompt = baseAnthropicCoderPrompt
25 basePrompt = baseAnthropicCoderPrompt
internal/llm/provider/anthropic.go+5−5
@@ -15,8 +15,8 @@ import (
15 "github.com/anthropics/anthropic-sdk-go"
15 "github.com/anthropics/anthropic-sdk-go"
16 "github.com/anthropics/anthropic-sdk-go/bedrock"
16 "github.com/anthropics/anthropic-sdk-go/bedrock"
17 "github.com/anthropics/anthropic-sdk-go/option"
17 "github.com/anthropics/anthropic-sdk-go/option"
18+ "github.com/charmbracelet/catwalk/pkg/catwalk"
18 "github.com/charmbracelet/crush/internal/config"
19 "github.com/charmbracelet/crush/internal/config"
19− "github.com/charmbracelet/crush/internal/fur/provider"
20 "github.com/charmbracelet/crush/internal/llm/tools"
20 "github.com/charmbracelet/crush/internal/llm/tools"
21 "github.com/charmbracelet/crush/internal/message"
21 "github.com/charmbracelet/crush/internal/message"
22)
22)
@@ -71,7 +71,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
71 var contentBlocks []anthropic.ContentBlockParamUnion
71 var contentBlocks []anthropic.ContentBlockParamUnion
72 contentBlocks = append(contentBlocks, content)
72 contentBlocks = append(contentBlocks, content)
73 for _, binaryContent := range msg.BinaryContent() {
73 for _, binaryContent := range msg.BinaryContent() {
74− base64Image := binaryContent.String(provider.InferenceProviderAnthropic)
74+ base64Image := binaryContent.String(catwalk.InferenceProviderAnthropic)
75 imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image)
75 imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image)
76 contentBlocks = append(contentBlocks, imageBlock)
76 contentBlocks = append(contentBlocks, imageBlock)
77 }
77 }
@@ -248,7 +248,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
248 return nil, retryErr
248 return nil, retryErr
249 }
249 }
250 if retry {
250 if retry {
251− slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
251+ slog.Warn("Retrying due to rate limit", "attempt", attempts, "max_retries", maxRetries)
252 select {
252 select {
253 case <-ctx.Done():
253 case <-ctx.Done():
254 return nil, ctx.Err()
254 return nil, ctx.Err()
@@ -401,7 +401,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
401 return
401 return
402 }
402 }
403 if retry {
403 if retry {
404− slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
404+ slog.Warn("Retrying due to rate limit", "attempt", attempts, "max_retries", maxRetries)
405 select {
405 select {
406 case <-ctx.Done():
406 case <-ctx.Done():
407 // context cancelled
407 // context cancelled
@@ -529,6 +529,6 @@ func (a *anthropicClient) usage(msg anthropic.Message) TokenUsage {
529 }
529 }
530}
530}
531
531
532−func (a *anthropicClient) Model() provider.Model {
532+func (a *anthropicClient) Model() catwalk.Model {
533 return a.providerOptions.model(a.providerOptions.modelType)
533 return a.providerOptions.model(a.providerOptions.modelType)
534}
534}
internal/llm/provider/bedrock.go+3−3
@@ -6,8 +6,8 @@ import (
6 "fmt"
6 "fmt"
7 "strings"
7 "strings"
8
8
9+ "github.com/charmbracelet/catwalk/pkg/catwalk"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
10− "github.com/charmbracelet/crush/internal/fur/provider"
11 "github.com/charmbracelet/crush/internal/llm/tools"
11 "github.com/charmbracelet/crush/internal/llm/tools"
12 "github.com/charmbracelet/crush/internal/message"
12 "github.com/charmbracelet/crush/internal/message"
13)
13)
@@ -32,7 +32,7 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
32 }
32 }
33 }
33 }
34
34
35− opts.model = func(modelType config.SelectedModelType) provider.Model {
35+ opts.model = func(modelType config.SelectedModelType) catwalk.Model {
36 model := config.Get().GetModelByType(modelType)
36 model := config.Get().GetModelByType(modelType)
37
37
38 // Prefix the model name with region
38 // Prefix the model name with region
@@ -88,6 +88,6 @@ func (b *bedrockClient) stream(ctx context.Context, messages []message.Message,
88 return b.childProvider.stream(ctx, messages, tools)
88 return b.childProvider.stream(ctx, messages, tools)
89}
89}
90
90
91−func (b *bedrockClient) Model() provider.Model {
91+func (b *bedrockClient) Model() catwalk.Model {
92 return b.providerOptions.model(b.providerOptions.modelType)
92 return b.providerOptions.model(b.providerOptions.modelType)
93}
93}
internal/llm/provider/gemini.go+4−4
@@ -10,8 +10,8 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/fur/provider"
15 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/llm/tools"
16 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/message"
17 "github.com/google/uuid"
17 "github.com/google/uuid"
@@ -210,7 +210,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
210 return nil, retryErr
210 return nil, retryErr
211 }
211 }
212 if retry {
212 if retry {
213− slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
213+ slog.Warn("Retrying due to rate limit", "attempt", attempts, "max_retries", maxRetries)
214 select {
214 select {
215 case <-ctx.Done():
215 case <-ctx.Done():
216 return nil, ctx.Err()
216 return nil, ctx.Err()
@@ -323,7 +323,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
323 return
323 return
324 }
324 }
325 if retry {
325 if retry {
326− slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
326+ slog.Warn("Retrying due to rate limit", "attempt", attempts, "max_retries", maxRetries)
327 select {
327 select {
328 case <-ctx.Done():
328 case <-ctx.Done():
329 if ctx.Err() != nil {
329 if ctx.Err() != nil {
@@ -463,7 +463,7 @@ func (g *geminiClient) usage(resp *genai.GenerateContentResponse) TokenUsage {
463 }
463 }
464}
464}
465
465
466−func (g *geminiClient) Model() provider.Model {
466+func (g *geminiClient) Model() catwalk.Model {
467 return g.providerOptions.model(g.providerOptions.modelType)
467 return g.providerOptions.model(g.providerOptions.modelType)
468}
468}
469
469
internal/llm/provider/openai.go+5−5
@@ -9,8 +9,8 @@ import (
9 "log/slog"
9 "log/slog"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/llm/tools"
14 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/message"
16 "github.com/openai/openai-go"
16 "github.com/openai/openai-go"
@@ -66,7 +66,7 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
66 textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
66 textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
67 content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
67 content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
68 for _, binaryContent := range msg.BinaryContent() {
68 for _, binaryContent := range msg.BinaryContent() {
69− imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(provider.InferenceProviderOpenAI)}
69+ imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(catwalk.InferenceProviderOpenAI)}
70 imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
70 imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
71
71
72 content = append(content, openai.ChatCompletionContentPartUnionParam{OfImageURL: &imageBlock})
72 content = append(content, openai.ChatCompletionContentPartUnionParam{OfImageURL: &imageBlock})
@@ -222,7 +222,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
222 return nil, retryErr
222 return nil, retryErr
223 }
223 }
224 if retry {
224 if retry {
225− slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
225+ slog.Warn("Retrying due to rate limit", "attempt", attempts, "max_retries", maxRetries)
226 select {
226 select {
227 case <-ctx.Done():
227 case <-ctx.Done():
228 return nil, ctx.Err()
228 return nil, ctx.Err()
@@ -395,7 +395,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
395 return
395 return
396 }
396 }
397 if retry {
397 if retry {
398− slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
398+ slog.Warn("Retrying due to rate limit", "attempt", attempts, "max_retries", maxRetries)
399 select {
399 select {
400 case <-ctx.Done():
400 case <-ctx.Done():
401 // context cancelled
401 // context cancelled
@@ -486,6 +486,6 @@ func (o *openaiClient) usage(completion openai.ChatCompletion) TokenUsage {
486 }
486 }
487}
487}
488
488
489−func (o *openaiClient) Model() provider.Model {
489+func (o *openaiClient) Model() catwalk.Model {
490 return o.providerOptions.model(o.providerOptions.modelType)
490 return o.providerOptions.model(o.providerOptions.modelType)
491}
491}
internal/llm/provider/openai_test.go+5−5
@@ -9,8 +9,8 @@ import (
9 "testing"
9 "testing"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/llm/tools"
14 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/message"
16 "github.com/openai/openai-go"
16 "github.com/openai/openai-go"
@@ -55,10 +55,10 @@ func TestOpenAIClientStreamChoices(t *testing.T) {
55 modelType: config.SelectedModelTypeLarge,
55 modelType: config.SelectedModelTypeLarge,
56 apiKey: "test-key",
56 apiKey: "test-key",
57 systemMessage: "test",
57 systemMessage: "test",
58− model: func(config.SelectedModelType) provider.Model {
58+ model: func(config.SelectedModelType) catwalk.Model {
59− return provider.Model{
59+ return catwalk.Model{
60− ID: "test-model",
60+ ID: "test-model",
61− Model: "test-model",
61+ Name: "test-model",
62 }
62 }
63 },
63 },
64 },
64 },
internal/llm/provider/provider.go+13−13
@@ -4,8 +4,8 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6
6
7+ "github.com/charmbracelet/catwalk/pkg/catwalk"
7 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/config"
8− "github.com/charmbracelet/crush/internal/fur/provider"
9 "github.com/charmbracelet/crush/internal/llm/tools"
9 "github.com/charmbracelet/crush/internal/llm/tools"
10 "github.com/charmbracelet/crush/internal/message"
10 "github.com/charmbracelet/crush/internal/message"
11)
11)
@@ -57,7 +57,7 @@ type Provider interface {
57
57
58 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
58 StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
59
59
60− Model() provider.Model
60+ Model() catwalk.Model
61}
61}
62
62
63type providerClientOptions struct {
63type providerClientOptions struct {
@@ -65,7 +65,7 @@ type providerClientOptions struct {
65 config config.ProviderConfig
65 config config.ProviderConfig
66 apiKey string
66 apiKey string
67 modelType config.SelectedModelType
67 modelType config.SelectedModelType
68− model func(config.SelectedModelType) provider.Model
68+ model func(config.SelectedModelType) catwalk.Model
69 disableCache bool
69 disableCache bool
70 systemMessage string
70 systemMessage string
71 maxTokens int64
71 maxTokens int64
@@ -80,7 +80,7 @@ type ProviderClient interface {
80 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
80 send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error)
81 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
81 stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent
82
82
83− Model() provider.Model
83+ Model() catwalk.Model
84}
84}
85
85
86type baseProvider[C ProviderClient] struct {
86type baseProvider[C ProviderClient] struct {
@@ -109,7 +109,7 @@ func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message
109 return p.client.stream(ctx, messages, tools)
109 return p.client.stream(ctx, messages, tools)
110}
110}
111
111
112−func (p *baseProvider[C]) Model() provider.Model {
112+func (p *baseProvider[C]) Model() catwalk.Model {
113 return p.client.Model()
113 return p.client.Model()
114}
114}
115
115
@@ -149,7 +149,7 @@ func NewProvider(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provi
149 apiKey: resolvedAPIKey,
149 apiKey: resolvedAPIKey,
150 extraHeaders: cfg.ExtraHeaders,
150 extraHeaders: cfg.ExtraHeaders,
151 extraBody: cfg.ExtraBody,
151 extraBody: cfg.ExtraBody,
152− model: func(tp config.SelectedModelType) provider.Model {
152+ model: func(tp config.SelectedModelType) catwalk.Model {
153 return *config.Get().GetModelByType(tp)
153 return *config.Get().GetModelByType(tp)
154 },
154 },
155 }
155 }
@@ -157,37 +157,37 @@ func NewProvider(cfg config.ProviderConfig, opts ...ProviderClientOption) (Provi
157 o(&clientOptions)
157 o(&clientOptions)
158 }
158 }
159 switch cfg.Type {
159 switch cfg.Type {
160− case provider.TypeAnthropic:
160+ case catwalk.TypeAnthropic:
161 return &baseProvider[AnthropicClient]{
161 return &baseProvider[AnthropicClient]{
162 options: clientOptions,
162 options: clientOptions,
163 client: newAnthropicClient(clientOptions, false),
163 client: newAnthropicClient(clientOptions, false),
164 }, nil
164 }, nil
165− case provider.TypeOpenAI:
165+ case catwalk.TypeOpenAI:
166 return &baseProvider[OpenAIClient]{
166 return &baseProvider[OpenAIClient]{
167 options: clientOptions,
167 options: clientOptions,
168 client: newOpenAIClient(clientOptions),
168 client: newOpenAIClient(clientOptions),
169 }, nil
169 }, nil
170− case provider.TypeGemini:
170+ case catwalk.TypeGemini:
171 return &baseProvider[GeminiClient]{
171 return &baseProvider[GeminiClient]{
172 options: clientOptions,
172 options: clientOptions,
173 client: newGeminiClient(clientOptions),
173 client: newGeminiClient(clientOptions),
174 }, nil
174 }, nil
175− case provider.TypeBedrock:
175+ case catwalk.TypeBedrock:
176 return &baseProvider[BedrockClient]{
176 return &baseProvider[BedrockClient]{
177 options: clientOptions,
177 options: clientOptions,
178 client: newBedrockClient(clientOptions),
178 client: newBedrockClient(clientOptions),
179 }, nil
179 }, nil
180− case provider.TypeAzure:
180+ case catwalk.TypeAzure:
181 return &baseProvider[AzureClient]{
181 return &baseProvider[AzureClient]{
182 options: clientOptions,
182 options: clientOptions,
183 client: newAzureClient(clientOptions),
183 client: newAzureClient(clientOptions),
184 }, nil
184 }, nil
185− case provider.TypeVertexAI:
185+ case catwalk.TypeVertexAI:
186 return &baseProvider[VertexAIClient]{
186 return &baseProvider[VertexAIClient]{
187 options: clientOptions,
187 options: clientOptions,
188 client: newVertexAIClient(clientOptions),
188 client: newVertexAIClient(clientOptions),
189 }, nil
189 }, nil
190− case provider.TypeXAI:
190+ case catwalk.TypeXAI:
191 clientOptions.baseURL = "https://api.x.ai/v1"
191 clientOptions.baseURL = "https://api.x.ai/v1"
192 return &baseProvider[OpenAIClient]{
192 return &baseProvider[OpenAIClient]{
193 options: clientOptions,
193 options: clientOptions,
internal/llm/tools/glob.go+1−1
@@ -146,7 +146,7 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
146 if err == nil {
146 if err == nil {
147 return matches, len(matches) >= limit && limit > 0, nil
147 return matches, len(matches) >= limit && limit > 0, nil
148 }
148 }
149− slog.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err))
149+ slog.Warn("Ripgrep execution failed, falling back to doublestar", "error", err)
150 }
150 }
151
151
152 return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
152 return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
internal/lsp/watcher/watcher.go+1−1
@@ -90,7 +90,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
90 slog.Debug("BaseURI", "baseURI", u)
90 slog.Debug("BaseURI", "baseURI", u)
91 }
91 }
92 default:
92 default:
93− slog.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
93+ slog.Debug("GlobPattern unknown type", "type", fmt.Sprintf("%T", v))
94 }
94 }
95
95
96 // Log WatchKind
96 // Log WatchKind
internal/message/content.go+3−3
@@ -5,7 +5,7 @@ import (
5 "slices"
5 "slices"
6 "time"
6 "time"
7
7
8− "github.com/charmbracelet/crush/internal/fur/provider"
8+ "github.com/charmbracelet/catwalk/pkg/catwalk"
9)
9)
10
10
11type MessageRole string
11type MessageRole string
@@ -74,9 +74,9 @@ type BinaryContent struct {
74 Data []byte
74 Data []byte
75}
75}
76
76
77−func (bc BinaryContent) String(p provider.InferenceProvider) string {
77+func (bc BinaryContent) String(p catwalk.InferenceProvider) string {
78 base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
78 base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
79− if p == provider.InferenceProviderOpenAI {
79+ if p == catwalk.InferenceProviderOpenAI {
80 return "data:" + bc.MIMEType + ";base64," + base64Encoded
80 return "data:" + bc.MIMEType + ";base64," + base64Encoded
81 }
81 }
82 return base64Encoded
82 return base64Encoded
internal/tui/components/chat/editor/editor.go+5−3
@@ -187,9 +187,11 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
187 value = value[:m.completionsStartIndex]
187 value = value[:m.completionsStartIndex]
188 value += item.Path
188 value += item.Path
189 m.textarea.SetValue(value)
189 m.textarea.SetValue(value)
190+ if !msg.Insert {
190− m.isCompletionsOpen = false
191+ m.isCompletionsOpen = false
191− m.currentQuery = ""
192+ m.currentQuery = ""
192− m.completionsStartIndex = 0
193+ m.completionsStartIndex = 0
194+ }
193 return m, nil
195 return m, nil
194 }
196 }
195 case openEditorMsg:
197 case openEditorMsg:
internal/tui/components/chat/messages/messages.go+4−4
@@ -8,11 +8,11 @@ import (
8
8
9 "github.com/charmbracelet/bubbles/v2/viewport"
9 "github.com/charmbracelet/bubbles/v2/viewport"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
11+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/x/ansi"
13 "github.com/charmbracelet/x/ansi"
13
14
14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/config"
15− "github.com/charmbracelet/crush/internal/fur/provider"
16 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/message"
17 "github.com/charmbracelet/crush/internal/tui/components/anim"
17 "github.com/charmbracelet/crush/internal/tui/components/anim"
18 "github.com/charmbracelet/crush/internal/tui/components/core"
18 "github.com/charmbracelet/crush/internal/tui/components/core"
@@ -369,11 +369,11 @@ func (m *assistantSectionModel) View() string {
369 model := config.Get().GetModel(m.message.Provider, m.message.Model)
369 model := config.Get().GetModel(m.message.Provider, m.message.Model)
370 if model == nil {
370 if model == nil {
371 // This means the model is not configured anymore
371 // This means the model is not configured anymore
372− model = &provider.Model{
372+ model = &catwalk.Model{
373− Model: "Unknown Model",
373+ Name: "Unknown Model",
374 }
374 }
375 }
375 }
376− modelFormatted := t.S().Muted.Render(model.Model)
376+ modelFormatted := t.S().Muted.Render(model.Name)
377 assistant := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
377 assistant := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
378 return t.S().Base.PaddingLeft(2).Render(
378 return t.S().Base.PaddingLeft(2).Render(
379 core.Section(assistant, m.width-2),
379 core.Section(assistant, m.width-2),
internal/tui/components/chat/sidebar/sidebar.go+4−4
@@ -9,10 +9,10 @@ import (
9 "sync"
9 "sync"
10
10
11 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
12+ "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/crush/internal/fur/provider"
16 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/lsp"
18 "github.com/charmbracelet/crush/internal/lsp/protocol"
18 "github.com/charmbracelet/crush/internal/lsp/protocol"
@@ -897,7 +897,7 @@ func (s *sidebarCmp) currentModelBlock() string {
897 t := styles.CurrentTheme()
897 t := styles.CurrentTheme()
898
898
899 modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
899 modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
900− modelName := t.S().Text.Render(model.Model)
900+ modelName := t.S().Text.Render(model.Name)
901 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
901 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
902 parts := []string{
902 parts := []string{
903 modelInfo,
903 modelInfo,
@@ -905,14 +905,14 @@ func (s *sidebarCmp) currentModelBlock() string {
905 if model.CanReason {
905 if model.CanReason {
906 reasoningInfoStyle := t.S().Subtle.PaddingLeft(2)
906 reasoningInfoStyle := t.S().Subtle.PaddingLeft(2)
907 switch modelProvider.Type {
907 switch modelProvider.Type {
908− case provider.TypeOpenAI:
908+ case catwalk.TypeOpenAI:
909 reasoningEffort := model.DefaultReasoningEffort
909 reasoningEffort := model.DefaultReasoningEffort
910 if selectedModel.ReasoningEffort != "" {
910 if selectedModel.ReasoningEffort != "" {
911 reasoningEffort = selectedModel.ReasoningEffort
911 reasoningEffort = selectedModel.ReasoningEffort
912 }
912 }
913 formatter := cases.Title(language.English, cases.NoLower)
913 formatter := cases.Title(language.English, cases.NoLower)
914 parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
914 parts = append(parts, reasoningInfoStyle.Render(formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))))
915− case provider.TypeAnthropic:
915+ case catwalk.TypeAnthropic:
916 formatter := cases.Title(language.English, cases.NoLower)
916 formatter := cases.Title(language.English, cases.NoLower)
917 if selectedModel.Think {
917 if selectedModel.Think {
918 parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking on")))
918 parts = append(parts, reasoningInfoStyle.Render(formatter.String("Thinking on")))
internal/tui/components/chat/splash/splash.go+3−3
@@ -10,8 +10,8 @@ import (
10 "github.com/charmbracelet/bubbles/v2/key"
10 "github.com/charmbracelet/bubbles/v2/key"
11 "github.com/charmbracelet/bubbles/v2/spinner"
11 "github.com/charmbracelet/bubbles/v2/spinner"
12 tea "github.com/charmbracelet/bubbletea/v2"
12 tea "github.com/charmbracelet/bubbletea/v2"
13+ "github.com/charmbracelet/catwalk/pkg/catwalk"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/fur/provider"
15 "github.com/charmbracelet/crush/internal/llm/prompt"
15 "github.com/charmbracelet/crush/internal/llm/prompt"
16 "github.com/charmbracelet/crush/internal/tui/components/chat"
16 "github.com/charmbracelet/crush/internal/tui/components/chat"
17 "github.com/charmbracelet/crush/internal/tui/components/completions"
17 "github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -109,7 +109,7 @@ func (s *splashCmp) SetOnboarding(onboarding bool) {
109 if err != nil {
109 if err != nil {
110 return
110 return
111 }
111 }
112− filteredProviders := []provider.Provider{}
112+ filteredProviders := []catwalk.Provider{}
113 simpleProviders := []string{
113 simpleProviders := []string{
114 "anthropic",
114 "anthropic",
115 "openai",
115 "openai",
@@ -407,7 +407,7 @@ func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
407 return nil
407 return nil
408}
408}
409
409
410−func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
410+func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
411 providers, err := config.Providers()
411 providers, err := config.Providers()
412 if err != nil {
412 if err != nil {
413 return nil, err
413 return nil, err
internal/tui/components/completions/completions.go+26−1
@@ -36,7 +36,8 @@ type CompletionsOpenedMsg struct{}
36type CloseCompletionsMsg struct{}
36type CloseCompletionsMsg struct{}
37
37
38type SelectCompletionMsg struct {
38type SelectCompletionMsg struct {
39− Value any // The value of the selected completion item
39+ Value any // The value of the selected completion item
40+ Insert bool
40}
41}
41
42
42type Completions interface {
43type Completions interface {
@@ -115,6 +116,30 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
115 d, cmd := c.list.Update(msg)
116 d, cmd := c.list.Update(msg)
116 c.list = d.(list.ListModel)
117 c.list = d.(list.ListModel)
117 return c, cmd
118 return c, cmd
119+ case key.Matches(msg, c.keyMap.UpInsert):
120+ selectedItemInx := c.list.SelectedIndex() - 1
121+ items := c.list.Items()
122+ if selectedItemInx == list.NoSelection || selectedItemInx < 0 {
123+ return c, nil // No item selected, do nothing
124+ }
125+ selectedItem := items[selectedItemInx].(CompletionItem).Value()
126+ c.list.SetSelected(selectedItemInx)
127+ return c, util.CmdHandler(SelectCompletionMsg{
128+ Value: selectedItem,
129+ Insert: true,
130+ })
131+ case key.Matches(msg, c.keyMap.DownInsert):
132+ selectedItemInx := c.list.SelectedIndex() + 1
133+ items := c.list.Items()
134+ if selectedItemInx == list.NoSelection || selectedItemInx >= len(items) {
135+ return c, nil // No item selected, do nothing
136+ }
137+ selectedItem := items[selectedItemInx].(CompletionItem).Value()
138+ c.list.SetSelected(selectedItemInx)
139+ return c, util.CmdHandler(SelectCompletionMsg{
140+ Value: selectedItem,
141+ Insert: true,
142+ })
118 case key.Matches(msg, c.keyMap.Select):
143 case key.Matches(msg, c.keyMap.Select):
119 selectedItemInx := c.list.SelectedIndex()
144 selectedItemInx := c.list.SelectedIndex()
120 if selectedItemInx == list.NoSelection {
145 if selectedItemInx == list.NoSelection {
internal/tui/components/completions/keys.go+10
@@ -9,6 +9,8 @@ type KeyMap struct {
9 Up,
9 Up,
10 Select,
10 Select,
11 Cancel key.Binding
11 Cancel key.Binding
12+ DownInsert,
13+ UpInsert key.Binding
12}
14}
13
15
14func DefaultKeyMap() KeyMap {
16func DefaultKeyMap() KeyMap {
@@ -29,6 +31,14 @@ func DefaultKeyMap() KeyMap {
29 key.WithKeys("esc"),
31 key.WithKeys("esc"),
30 key.WithHelp("esc", "cancel"),
32 key.WithHelp("esc", "cancel"),
31 ),
33 ),
34+ DownInsert: key.NewBinding(
35+ key.WithKeys("ctrl+n"),
36+ key.WithHelp("ctrl+n", "insert next"),
37+ ),
38+ UpInsert: key.NewBinding(
39+ key.WithKeys("ctrl+p"),
40+ key.WithHelp("ctrl+p", "insert previous"),
41+ ),
32 }
42 }
33}
43}
34
44
internal/tui/components/dialogs/commands/commands.go+2−2
@@ -4,10 +4,10 @@ import (
4 "github.com/charmbracelet/bubbles/v2/help"
4 "github.com/charmbracelet/bubbles/v2/help"
5 "github.com/charmbracelet/bubbles/v2/key"
5 "github.com/charmbracelet/bubbles/v2/key"
6 tea "github.com/charmbracelet/bubbletea/v2"
6 tea "github.com/charmbracelet/bubbletea/v2"
7+ "github.com/charmbracelet/catwalk/pkg/catwalk"
7 "github.com/charmbracelet/lipgloss/v2"
8 "github.com/charmbracelet/lipgloss/v2"
8
9
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
10− "github.com/charmbracelet/crush/internal/fur/provider"
11 "github.com/charmbracelet/crush/internal/llm/prompt"
11 "github.com/charmbracelet/crush/internal/llm/prompt"
12 "github.com/charmbracelet/crush/internal/tui/components/chat"
12 "github.com/charmbracelet/crush/internal/tui/components/chat"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
@@ -270,7 +270,7 @@ func (c *commandDialogCmp) defaultCommands() []Command {
270 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
270 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
271 model := cfg.GetModelByType(agentCfg.Model)
271 model := cfg.GetModelByType(agentCfg.Model)
272 if providerCfg != nil && model != nil &&
272 if providerCfg != nil && model != nil &&
273− providerCfg.Type == provider.TypeAnthropic && model.CanReason {
273+ providerCfg.Type == catwalk.TypeAnthropic && model.CanReason {
274 selectedModel := cfg.Models[agentCfg.Model]
274 selectedModel := cfg.Models[agentCfg.Model]
275 status := "Enable"
275 status := "Enable"
276 if selectedModel.Think {
276 if selectedModel.Think {
internal/tui/components/dialogs/models/list.go+11−11
@@ -5,8 +5,8 @@ import (
5 "slices"
5 "slices"
6
6
7 tea "github.com/charmbracelet/bubbletea/v2"
7 tea "github.com/charmbracelet/bubbletea/v2"
8+ "github.com/charmbracelet/catwalk/pkg/catwalk"
8 "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/config"
9− "github.com/charmbracelet/crush/internal/fur/provider"
10 "github.com/charmbracelet/crush/internal/tui/components/completions"
10 "github.com/charmbracelet/crush/internal/tui/components/completions"
11 "github.com/charmbracelet/crush/internal/tui/components/core/list"
11 "github.com/charmbracelet/crush/internal/tui/components/core/list"
12 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
12 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
@@ -18,7 +18,7 @@ import (
18type ModelListComponent struct {
18type ModelListComponent struct {
19 list list.ListModel
19 list list.ListModel
20 modelType int
20 modelType int
21− providers []provider.Provider
21+ providers []catwalk.Provider
22}
22}
23
23
24func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style, inputPlaceholder string) *ModelListComponent {
24func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style, inputPlaceholder string) *ModelListComponent {
@@ -109,19 +109,19 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
109 }
109 }
110
110
111 // Check if this provider is not in the known providers list
111 // Check if this provider is not in the known providers list
112− if !slices.ContainsFunc(knownProviders, func(p provider.Provider) bool { return p.ID == provider.InferenceProvider(providerID) }) {
112+ if !slices.ContainsFunc(knownProviders, func(p catwalk.Provider) bool { return p.ID == catwalk.InferenceProvider(providerID) }) {
113 // Convert config provider to provider.Provider format
113 // Convert config provider to provider.Provider format
114− configProvider := provider.Provider{
114+ configProvider := catwalk.Provider{
115 Name: providerConfig.Name,
115 Name: providerConfig.Name,
116− ID: provider.InferenceProvider(providerID),
116+ ID: catwalk.InferenceProvider(providerID),
117− Models: make([]provider.Model, len(providerConfig.Models)),
117+ Models: make([]catwalk.Model, len(providerConfig.Models)),
118 }
118 }
119
119
120 // Convert models
120 // Convert models
121 for i, model := range providerConfig.Models {
121 for i, model := range providerConfig.Models {
122− configProvider.Models[i] = provider.Model{
122+ configProvider.Models[i] = catwalk.Model{
123 ID: model.ID,
123 ID: model.ID,
124− Model: model.Model,
124+ Name: model.Name,
125 CostPer1MIn: model.CostPer1MIn,
125 CostPer1MIn: model.CostPer1MIn,
126 CostPer1MOut: model.CostPer1MOut,
126 CostPer1MOut: model.CostPer1MOut,
127 CostPer1MInCached: model.CostPer1MInCached,
127 CostPer1MInCached: model.CostPer1MInCached,
@@ -144,7 +144,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
144 section.SetInfo(configured)
144 section.SetInfo(configured)
145 modelItems = append(modelItems, section)
145 modelItems = append(modelItems, section)
146 for _, model := range configProvider.Models {
146 for _, model := range configProvider.Models {
147− modelItems = append(modelItems, completions.NewCompletionItem(model.Model, ModelOption{
147+ modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
148 Provider: configProvider,
148 Provider: configProvider,
149 Model: model,
149 Model: model,
150 }))
150 }))
@@ -179,7 +179,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
179 }
179 }
180 modelItems = append(modelItems, section)
180 modelItems = append(modelItems, section)
181 for _, model := range provider.Models {
181 for _, model := range provider.Models {
182− modelItems = append(modelItems, completions.NewCompletionItem(model.Model, ModelOption{
182+ modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{
183 Provider: provider,
183 Provider: provider,
184 Model: model,
184 Model: model,
185 }))
185 }))
@@ -201,6 +201,6 @@ func (m *ModelListComponent) SetInputPlaceholder(placeholder string) {
201 m.list.SetFilterPlaceholder(placeholder)
201 m.list.SetFilterPlaceholder(placeholder)
202}
202}
203
203
204−func (m *ModelListComponent) SetProviders(providers []provider.Provider) {
204+func (m *ModelListComponent) SetProviders(providers []catwalk.Provider) {
205 m.providers = providers
205 m.providers = providers
206}
206}
internal/tui/components/dialogs/models/models.go+4−4
@@ -8,8 +8,8 @@ import (
8 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/spinner"
9 "github.com/charmbracelet/bubbles/v2/spinner"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
11+ "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
13 "github.com/charmbracelet/crush/internal/tui/components/completions"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
15 "github.com/charmbracelet/crush/internal/tui/components/core/list"
15 "github.com/charmbracelet/crush/internal/tui/components/core/list"
@@ -48,8 +48,8 @@ type ModelDialog interface {
48}
48}
49
49
50type ModelOption struct {
50type ModelOption struct {
51− Provider provider.Provider
51+ Provider catwalk.Provider
52− Model provider.Model
52+ Model catwalk.Model
53}
53}
54
54
55type modelDialogCmp struct {
55type modelDialogCmp struct {
@@ -363,7 +363,7 @@ func (m *modelDialogCmp) isProviderConfigured(providerID string) bool {
363 return false
363 return false
364}
364}
365
365
366−func (m *modelDialogCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
366+func (m *modelDialogCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
367 providers, err := config.Providers()
367 providers, err := config.Providers()
368 if err != nil {
368 if err != nil {
369 return nil, err
369 return nil, err
internal/tui/page/chat/chat.go+1−1
@@ -279,7 +279,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
279 if model.SupportsImages {
279 if model.SupportsImages {
280 return p, util.CmdHandler(OpenFilePickerMsg{})
280 return p, util.CmdHandler(OpenFilePickerMsg{})
281 } else {
281 } else {
282− return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Model)
282+ return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
283 }
283 }
284 case key.Matches(msg, p.keyMap.Tab):
284 case key.Matches(msg, p.keyMap.Tab):
285 if p.session.ID == "" {
285 if p.session.ID == "" {
internal/tui/tui.go+41−23
@@ -3,6 +3,7 @@ package tui
3import (
3import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6+ "strings"
6
7
7 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
8 tea "github.com/charmbracelet/bubbletea/v2"
9 tea "github.com/charmbracelet/bubbletea/v2"
@@ -112,6 +113,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
112 }
113 }
113 return a, tea.Batch(cmds...)
114 return a, tea.Batch(cmds...)
114 case tea.WindowSizeMsg:
115 case tea.WindowSizeMsg:
116+ a.wWidth, a.wHeight = msg.Width, msg.Height
115 a.completions.Update(msg)
117 a.completions.Update(msg)
116 return a, a.handleWindowResize(msg.Width, msg.Height)
118 return a, a.handleWindowResize(msg.Width, msg.Height)
117
119
@@ -290,7 +292,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
290// handleWindowResize processes window resize events and updates all components.
292// handleWindowResize processes window resize events and updates all components.
291func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
293func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
292 var cmds []tea.Cmd
294 var cmds []tea.Cmd
293− a.wWidth, a.wHeight = width, height
294 if a.showingFullHelp {
295 if a.showingFullHelp {
295 height -= 5
296 height -= 5
296 } else {
297 } else {
@@ -319,26 +320,20 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
319
320
320// handleKeyPressMsg processes keyboard input and routes to appropriate handlers.
321// handleKeyPressMsg processes keyboard input and routes to appropriate handlers.
321func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
322func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
323+ if a.completions.Open() {
324+ // completions
325+ keyMap := a.completions.KeyMap()
326+ switch {
327+ case key.Matches(msg, keyMap.Up), key.Matches(msg, keyMap.Down),
328+ key.Matches(msg, keyMap.Select), key.Matches(msg, keyMap.Cancel),
329+ key.Matches(msg, keyMap.UpInsert), key.Matches(msg, keyMap.DownInsert):
330+ u, cmd := a.completions.Update(msg)
331+ a.completions = u.(completions.Completions)
332+ return cmd
333+ }
334+ }
322 switch {
335 switch {
323− // completions
324− case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Up):
325− u, cmd := a.completions.Update(msg)
326− a.completions = u.(completions.Completions)
327− return cmd
328−
329− case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Down):
330− u, cmd := a.completions.Update(msg)
331− a.completions = u.(completions.Completions)
332− return cmd
333− case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Select):
334− u, cmd := a.completions.Update(msg)
335− a.completions = u.(completions.Completions)
336− return cmd
337− case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Cancel):
338− u, cmd := a.completions.Update(msg)
339− a.completions = u.(completions.Completions)
340− return cmd
341− // help
336+ // help
342 case key.Matches(msg, a.keyMap.Help):
337 case key.Matches(msg, a.keyMap.Help):
343 a.status.ToggleFullHelp()
338 a.status.ToggleFullHelp()
344 a.showingFullHelp = !a.showingFullHelp
339 a.showingFullHelp = !a.showingFullHelp
@@ -429,6 +424,27 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
429
424
430// View renders the complete application interface including pages, dialogs, and overlays.
425// View renders the complete application interface including pages, dialogs, and overlays.
431func (a *appModel) View() tea.View {
426func (a *appModel) View() tea.View {
427+ var view tea.View
428+ t := styles.CurrentTheme()
429+ view.BackgroundColor = t.BgBase
430+ if a.wWidth < 25 || a.wHeight < 15 {
431+ view.Layer = lipgloss.NewCanvas(
432+ lipgloss.NewLayer(
433+ t.S().Base.Width(a.wWidth).Height(a.wHeight).
434+ Align(lipgloss.Center, lipgloss.Center).
435+ Render(
436+ t.S().Base.
437+ Padding(1, 4).
438+ Foreground(t.White).
439+ BorderStyle(lipgloss.RoundedBorder()).
440+ BorderForeground(t.Primary).
441+ Render("Window too small!"),
442+ ),
443+ ),
444+ )
445+ return view
446+ }
447+
432 page := a.pages[a.currentPage]
448 page := a.pages[a.currentPage]
433 if withHelp, ok := page.(core.KeyMapHelp); ok {
449 if withHelp, ok := page.(core.KeyMapHelp); ok {
434 a.status.SetKeyMap(withHelp.Help())
450 a.status.SetKeyMap(withHelp.Help())
@@ -453,6 +469,11 @@ func (a *appModel) View() tea.View {
453 var cursor *tea.Cursor
469 var cursor *tea.Cursor
454 if v, ok := page.(util.Cursor); ok {
470 if v, ok := page.(util.Cursor); ok {
455 cursor = v.Cursor()
471 cursor = v.Cursor()
472+ // Hide the cursor if it's positioned outside the textarea
473+ statusHeight := a.height - strings.Count(pageView, "\n") + 1
474+ if cursor != nil && cursor.Y+statusHeight+chat.EditorHeight-2 <= a.height { // 2 for the top and bottom app padding
475+ cursor = nil
476+ }
456 }
477 }
457 activeView := a.dialog.ActiveModel()
478 activeView := a.dialog.ActiveModel()
458 if activeView != nil {
479 if activeView != nil {
@@ -475,10 +496,7 @@ func (a *appModel) View() tea.View {
475 layers...,
496 layers...,
476 )
497 )
477
498
478− var view tea.View
479− t := styles.CurrentTheme()
480 view.Layer = canvas
499 view.Layer = canvas
481− view.BackgroundColor = t.BgBase
482 view.Cursor = cursor
500 view.Cursor = cursor
483 return view
501 return view
484}
502}
main.go+1−2
@@ -1,7 +1,6 @@
1package main
1package main
2
2
3import (
3import (
4− "fmt"
5 "log/slog"
4 "log/slog"
6 "net/http"
5 "net/http"
7 "os"
6 "os"
@@ -23,7 +22,7 @@ func main() {
23 go func() {
22 go func() {
24 slog.Info("Serving pprof at localhost:6060")
23 slog.Info("Serving pprof at localhost:6060")
25 if httpErr := http.ListenAndServe("localhost:6060", nil); httpErr != nil {
24 if httpErr := http.ListenAndServe("localhost:6060", nil); httpErr != nil {
26− slog.Error(fmt.Sprintf("Failed to pprof listen: %v", httpErr))
25+ slog.Error("Failed to pprof listen", "error", httpErr)
27 }
26 }
28 }()
27 }()
29 }
28 }