diff --git a/README.md b/README.md index 32d7ebd50a8dc7762a52539831948be9e19e46ed..e25c99a5cb84372414d68a63a511eba824ac9b76 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,7 @@ using `$(echo $VAR)` syntax. "args": ["/path/to/mcp-server.js"], "timeout": 120, "disabled": false, + "disabled_tools": ["some-tool-name"], "env": { "NODE_ENV": "production" } @@ -284,6 +285,7 @@ using `$(echo $VAR)` syntax. "url": "https://api.githubcopilot.com/mcp/", "timeout": 120, "disabled": false, + "disabled_tools": ["create_issue", "create_pull_request"], "headers": { "Authorization": "Bearer $GH_PAT" } @@ -335,6 +337,26 @@ permissions. Use this with care. You can also skip all permission prompts entirely by running Crush with the `--yolo` flag. Be very, very careful with this feature. +### Disabling Built-In Tools + +If you'd like to prevent Crush from using certain built-in tools entirely, you +can disable them via the `options.disabled_tools` list. Disabled tools are +completely hidden from the agent. + +```json +{ + "$schema": "https://charm.land/crush.json", + "options": { + "disabled_tools": [ + "bash", + "sourcegraph" + ] + } +} +``` + +To disable tools from MCP servers, see the [MCP config section](#mcps). + ### Initialization When you initialize a project, Crush analyzes your codebase and creates diff --git a/go.mod b/go.mod index b419bebcbb13d92ead60b4e7b444cfd53845715a..3f49c42af9fc1bf6c5f12641f5323d4e210a248f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( charm.land/bubbles/v2 v2.0.0-rc.1 charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251202162339-5fa38b798f16 charm.land/fantasy v0.5.1 - charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca + charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251205162909-7869489d8971 charm.land/x/vcr v0.1.1 github.com/JohannesKaufmann/html-to-markdown v1.6.0 github.com/MakeNowJust/heredoc v1.0.0 @@ -21,7 +21,7 @@ require ( github.com/charmbracelet/fang v0.4.4 github.com/charmbracelet/glamour/v2 v2.0.0-20251106195642-800eb8175930 github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0 - github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b + github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 github.com/charmbracelet/x/ansi v0.11.2 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f diff --git a/go.sum b/go.sum index c406f84cbc6d629b765101bfaa27ba71f5ed7c30..53b05846cd65e9412d979ddcca0099513a13c0ee 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ charm.land/fantasy v0.5.1 h1:Svi/UpI4/DwVjTqNYceDXoJJYn6SVEM5dnLH92UBiEs= charm.land/fantasy v0.5.1/go.mod h1:SPOsnIlkBKnhw2Wnasv+wZ82EmCMIGesx0je3tgR6+M= charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca h1:6bVc8OFotCS4sS7HKqxTudP7yn8Y0ODR6df2pdlY/+s= charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca/go.mod h1:XSJjv7DaH4zd1Y27kZis295RkEj9OFR9zh2WffQQsKQ= +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251205162909-7869489d8971 h1:xZFcNsJMiIDbFtWRyDmkKNk1sjojfaom4Zoe0cyH/8c= +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251205162909-7869489d8971/go.mod h1:i61Y3FmdbcBNSKa+pKB3DaE4uVQmBLMs/xlvRyHcXAE= charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q= charm.land/x/vcr v0.1.1/go.mod h1:eByq2gqzWvcct/8XE2XO5KznoWEBiXH56+y2gphbltM= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= @@ -100,6 +102,8 @@ github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0 h1:lxHzxsHd4P github.com/charmbracelet/log/v2 v2.0.0-20251106192421-eb64aaa963a0/go.mod h1:Q7oMtlboDPnnrYiJDXNwdWmJblOmuOnycPKczlVju6I= github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b h1:jY1J0PcfetoB1uJ+w8rd86gUFSpKpJJI35gnfpKF5hg= github.com/charmbracelet/ultraviolet v0.0.0-20251202162030-ecc8c1ae4b2b/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs= github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg= github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50= diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 436aa27d95e4b86f83c20c3f46b2e1434986e89d..285fa2ff96b15e24f205fb764457854581a8aa75 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -388,6 +388,12 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan } for _, tool := range tools.GetMCPTools(c.permissions, c.cfg.WorkingDir()) { + // Check MCP-specific disabled tools. + if mcpCfg, ok := c.cfg.MCP[tool.MCP()]; ok { + if slices.Contains(mcpCfg.DisabledTools, tool.MCPToolName()) { + continue + } + } if agent.AllowedMCP == nil { // No MCP restrictions filteredTools = append(filteredTools, tool) diff --git a/internal/config/config.go b/internal/config/config.go index 4c9dc7bafe83ff0b75b0a0238fcd71ba9e63a3bf..b8f1fcd0dbbef7e5d5d70e2c99515db6d1f6d7b5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -144,13 +144,14 @@ const ( ) type MCPConfig struct { - Command string `json:"command,omitempty" jsonschema:"description=Command to execute for stdio MCP servers,example=npx"` - Env map[string]string `json:"env,omitempty" jsonschema:"description=Environment variables to set for the MCP server"` - Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the MCP server command"` - Type MCPType `json:"type" jsonschema:"required,description=Type of MCP connection,enum=stdio,enum=sse,enum=http,default=stdio"` - URL string `json:"url,omitempty" jsonschema:"description=URL for HTTP or SSE MCP servers,format=uri,example=http://localhost:3000/mcp"` - Disabled bool `json:"disabled,omitempty" jsonschema:"description=Whether this MCP server is disabled,default=false"` - Timeout int `json:"timeout,omitempty" jsonschema:"description=Timeout in seconds for MCP server connections,default=15,example=30,example=60,example=120"` + Command string `json:"command,omitempty" jsonschema:"description=Command to execute for stdio MCP servers,example=npx"` + Env map[string]string `json:"env,omitempty" jsonschema:"description=Environment variables to set for the MCP server"` + Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the MCP server command"` + Type MCPType `json:"type" jsonschema:"required,description=Type of MCP connection,enum=stdio,enum=sse,enum=http,default=stdio"` + URL string `json:"url,omitempty" jsonschema:"description=URL for HTTP or SSE MCP servers,format=uri,example=http://localhost:3000/mcp"` + Disabled bool `json:"disabled,omitempty" jsonschema:"description=Whether this MCP server is disabled,default=false"` + DisabledTools []string `json:"disabled_tools,omitempty" jsonschema:"description=List of tools from this MCP server to disable,example=get-library-doc"` + Timeout int `json:"timeout,omitempty" jsonschema:"description=Timeout in seconds for MCP server connections,default=15,example=30,example=60,example=120"` // TODO: maybe make it possible to get the value from the env Headers map[string]string `json:"headers,omitempty" jsonschema:"description=HTTP headers for HTTP/SSE MCP servers"` @@ -221,7 +222,7 @@ type Options struct { DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"` DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"` DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd - DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"` + DisabledTools []string `json:"disabled_tools,omitempty" jsonschema:"description=List of built-in tools to disable and hide from the agent,example=bash,example=sourcegraph"` DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"` Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"` DisableMetrics bool `json:"disable_metrics,omitempty" jsonschema:"description=Disable sending metrics,default=false"` diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 8ae8ed3d7dd7b5f277b8d0076b97859b5c6aa73f..c3f4ff06d631c3df757d6a7a1d12428e66a3ae58 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -41,6 +41,7 @@ type Editor interface { SetSession(session session.Session) tea.Cmd IsCompletionsOpen() bool HasAttachments() bool + IsEmpty() bool Cursor() *tea.Cursor } @@ -261,7 +262,7 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { curIdx := m.textarea.Width()*cur.Y + cur.X switch { // Open command palette when "/" is pressed on empty prompt - case msg.String() == "/" && len(strings.TrimSpace(m.textarea.Value())) == 0: + case msg.String() == "/" && m.IsEmpty(): return m, util.CmdHandler(dialogs.OpenDialogMsg{ Model: commands.NewCommandDialog(m.session.ID), }) @@ -542,6 +543,10 @@ func (c *editorCmp) HasAttachments() bool { return len(c.attachments) > 0 } +func (c *editorCmp) IsEmpty() bool { + return strings.TrimSpace(c.textarea.Value()) == "" +} + func normalPromptFunc(info textarea.PromptInfo) string { t := styles.CurrentTheme() if info.LineNumber == 0 { diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index a5cb757d7d4403b8b99c82f5941065037bf86c71..0166dc6d9d9d5ede72f41235850db309f3b9f31d 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -525,9 +525,7 @@ func (p *chatPage) View() string { ) layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1)) } - canvas := lipgloss.NewCanvas( - layers..., - ) + canvas := lipgloss.NewCompositor(layers...) return canvas.Render() } @@ -1024,6 +1022,9 @@ func (p *chatPage) Help() help.KeyMap { key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "commands"), ) + if p.focusedPane == PanelTypeEditor && p.editor.IsEmpty() { + commandsBinding.SetHelp("/ or ctrl+p", "commands") + } modelsBinding := key.NewBinding( key.WithKeys("ctrl+m", "ctrl+l"), key.WithHelp("ctrl+l", "models"), diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 45f0ae5ec410e85b3d30d620b4db5c499cff09c3..e91fae5592b8d51963e524d0662d868cbfed6869 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -593,22 +593,15 @@ func (a *appModel) View() tea.View { view.MouseMode = tea.MouseModeCellMotion view.BackgroundColor = t.BgBase if a.wWidth < 25 || a.wHeight < 15 { - view.SetContent( - lipgloss.NewCanvas( - lipgloss.NewLayer( - t.S().Base.Width(a.wWidth).Height(a.wHeight). - Align(lipgloss.Center, lipgloss.Center). - Render( - t.S().Base. - Padding(1, 4). - Foreground(t.White). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(t.Primary). - Render("Window too small!"), - ), - ), - ).Render(), - ) + view.Content = t.S().Base.Width(a.wWidth).Height(a.wHeight). + Align(lipgloss.Center, lipgloss.Center). + Render(t.S().Base. + Padding(1, 4). + Foreground(t.White). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(t.Primary). + Render("Window too small!"), + ) return view } @@ -659,11 +652,8 @@ func (a *appModel) View() tea.View { ) } - canvas := lipgloss.NewCanvas( - layers..., - ) - - view.Content = canvas.Render() + comp := lipgloss.NewCompositor(layers...) + view.Content = comp.Render() view.Cursor = cursor if a.sendProgressBar && a.app != nil && a.app.AgentCoordinator != nil && a.app.AgentCoordinator.IsBusy() { diff --git a/schema.json b/schema.json index 47740b9c18c8d2807c74557ffd9e21b5b6658ceb..974200854d28bb300c94613328485ea5a3e2165d 100644 --- a/schema.json +++ b/schema.json @@ -229,6 +229,16 @@ "description": "Whether this MCP server is disabled", "default": false }, + "disabled_tools": { + "items": { + "type": "string", + "examples": [ + "get-library-doc" + ] + }, + "type": "array", + "description": "List of tools from this MCP server to disable" + }, "timeout": { "type": "integer", "description": "Timeout in seconds for MCP server connections", @@ -386,10 +396,14 @@ }, "disabled_tools": { "items": { - "type": "string" + "type": "string", + "examples": [ + "bash", + "sourcegraph" + ] }, "type": "array", - "description": "Tools to disable" + "description": "List of built-in tools to disable and hide from the agent" }, "disable_provider_auto_update": { "type": "boolean", @@ -418,10 +432,7 @@ } }, "additionalProperties": false, - "type": "object", - "required": [ - "disabled_tools" - ] + "type": "object" }, "Permissions": { "properties": {