Detailed changes
@@ -0,0 +1,188 @@
+package messages
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "charm.land/lipgloss/v2"
+ "charm.land/lipgloss/v2/table"
+ "github.com/charmbracelet/crush/internal/tui/styles"
+
+ "github.com/charmbracelet/crush/internal/stringext"
+)
+
+type dockerMCPRenderer struct {
+ baseRenderer
+}
+
+// Render displays file content with optional limit and offset parameters
+func (dr dockerMCPRenderer) Render(v *toolCallCmp) string {
+ var params map[string]any
+ if err := dr.unmarshalParams(v.call.Input, ¶ms); err != nil {
+ return dr.renderError(v, "Invalid view parameters")
+ }
+
+ tool := strings.ReplaceAll(v.call.Name, "mcp_crush_docker_", "")
+
+ main := v.call.Input
+ extraArgs := map[string]string{}
+ switch tool {
+ case "mcp-find":
+ if query, ok := params["query"]; ok {
+ if qStr, ok := query.(string); ok {
+ main = qStr
+ }
+ }
+ for k, v := range params {
+ if k == "query" {
+ continue
+ }
+
+ data, _ := json.Marshal(v)
+ extraArgs[k] = string(data)
+ }
+ case "mcp-add":
+ if name, ok := params["name"]; ok {
+ if nStr, ok := name.(string); ok {
+ main = nStr
+ }
+ }
+ for k, v := range params {
+ if k == "name" {
+ continue
+ }
+
+ data, _ := json.Marshal(v)
+ extraArgs[k] = string(data)
+ }
+ case "mcp-remove":
+ if name, ok := params["name"]; ok {
+ if nStr, ok := name.(string); ok {
+ main = nStr
+ }
+ }
+ for k, v := range params {
+ if k == "name" {
+ continue
+ }
+
+ data, _ := json.Marshal(v)
+ extraArgs[k] = string(data)
+ }
+ }
+
+ args := newParamBuilder().
+ addMain(main)
+
+ for k, v := range extraArgs {
+ args.addKeyValue(k, v)
+ }
+
+ width := v.textWidth()
+ if v.isNested {
+ width -= 4 // Adjust for nested tool call indentation
+ }
+ header := dr.makeHeader(v, tool, width, args.build()...)
+ if v.isNested {
+ return v.style().Render(header)
+ }
+ if res, done := earlyState(header, v); done {
+ return res
+ }
+
+ if tool == "mcp-find" {
+ return joinHeaderBody(header, dr.renderMCPServers(v))
+ }
+ return joinHeaderBody(header, renderPlainContent(v, v.result.Content))
+}
+
+type FindMCPResponse struct {
+ Servers []struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ } `json:"servers"`
+}
+
+func (dr dockerMCPRenderer) renderMCPServers(v *toolCallCmp) string {
+ t := styles.CurrentTheme()
+ var result FindMCPResponse
+ if err := dr.unmarshalParams(v.result.Content, &result); err != nil {
+ return renderPlainContent(v, v.result.Content)
+ }
+
+ if len(result.Servers) == 0 {
+ return t.S().Muted.Render("No MCP servers found.")
+ }
+ width := min(120, v.textWidth())
+ rows := [][]string{}
+ moreServers := ""
+ for i, server := range result.Servers {
+ if i > 9 {
+ moreServers = t.S().Subtle.Render(fmt.Sprintf("... and %d mode", len(result.Servers)-10))
+ break
+ }
+ rows = append(rows, []string{t.S().Base.Render(server.Name), t.S().Muted.Render(server.Description)})
+
+ }
+ serverTable := table.New().
+ Wrap(false).
+ BorderTop(false).
+ BorderBottom(false).
+ BorderRight(false).
+ BorderLeft(false).
+ BorderColumn(false).
+ BorderRow(false).
+ StyleFunc(func(row, col int) lipgloss.Style {
+ if row == table.HeaderRow {
+ return lipgloss.NewStyle()
+ }
+ switch col {
+ case 0:
+ return lipgloss.NewStyle().PaddingRight(1)
+ }
+ return lipgloss.NewStyle()
+ }).Rows(rows...).Width(width)
+ if moreServers != "" {
+ return serverTable.Render() + "\n" + moreServers
+ }
+ return serverTable.Render()
+}
+
+func (dr dockerMCPRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
+ t := styles.CurrentTheme()
+ mainTool := "Docker MCP"
+ action := tool
+ actionStyle := t.S().Base.Foreground(t.BlueDark)
+ switch tool {
+ case "mcp-find":
+ action = "Find"
+ case "mcp-add":
+ action = "Add"
+ actionStyle = t.S().Base.Foreground(t.GreenLight)
+ case "mcp-remove":
+ action = "Remove"
+ actionStyle = t.S().Base.Foreground(t.RedLighter)
+ default:
+ action = strings.ReplaceAll(tool, "-", " ")
+ action = strings.ReplaceAll(tool, "_", " ")
+ action = stringext.Capitalize(tool)
+ }
+ if v.isNested {
+ return dr.makeNestedHeader(v, tool, width, params...)
+ }
+ icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
+ if v.result.ToolCallID != "" {
+ if v.result.IsError {
+ icon = t.S().Base.Foreground(t.RedDark).Render(styles.ToolError)
+ } else {
+ icon = t.S().Base.Foreground(t.Green).Render(styles.ToolSuccess)
+ }
+ } else if v.cancelled {
+ icon = t.S().Muted.Render(styles.ToolPending)
+ }
+ tool = t.S().Base.Foreground(t.Blue).Render(mainTool)
+ arrow := t.S().Base.Foreground(t.BlueDark).Render(styles.ArrowIcon)
+ prefix := fmt.Sprintf("%s %s %s %s ", icon, tool, arrow, actionStyle.Render(action))
+ return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...)
+}
@@ -13,6 +13,7 @@ import (
"github.com/charmbracelet/crush/internal/agent/tools"
"github.com/charmbracelet/crush/internal/ansiext"
"github.com/charmbracelet/crush/internal/fsext"
+ "github.com/charmbracelet/crush/internal/stringext"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/highlight"
"github.com/charmbracelet/crush/internal/tui/styles"
@@ -946,7 +947,7 @@ func renderParamList(nested bool, paramsWidth int, params ...string) string {
}
if len(params) == 1 {
- return t.S().Subtle.Render(mainParam)
+ return t.S().Muted.Render(mainParam)
}
otherParams := params[1:]
// create pairs of key/value
@@ -968,14 +969,16 @@ func renderParamList(nested bool, paramsWidth int, params ...string) string {
remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
if remainingWidth < 30 {
// No space for the params, just show the main
- return t.S().Subtle.Render(mainParam)
+ return t.S().Muted.Render(mainParam)
}
if len(parts) > 0 {
- mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
+ paramsStyles := t.S().Subtle.Render(fmt.Sprintf("(%s)", strings.Join(parts, ", ")))
+ mainParam = t.S().Muted.Render(mainParam)
+ mainParam = fmt.Sprintf("%s %s", mainParam, paramsStyles)
}
- return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "β¦"))
+ return ansi.Truncate(mainParam, paramsWidth, "β¦")
}
// earlyState returns immediatelyβrendered error/cancelled/ongoing states.
@@ -1159,6 +1162,25 @@ func truncateHeight(s string, h int) string {
return s
}
+func mcpToolName(name string) string {
+ if strings.HasPrefix(name, "mcp_crush_docker") {
+ name = strings.ReplaceAll(name, "mcp_crush_docker_", "")
+ if name == "mcp-find" {
+ name = "find"
+ }
+ if name == "mcp-add" {
+ name = "add"
+ }
+ if name == "mcp-remove" {
+ name = "remove"
+ }
+ name = strings.ReplaceAll(name, "_", " ")
+ name = strings.ReplaceAll(name, "-", " ")
+ return "Docker MCP: " + stringext.Capitalize(name)
+ }
+ return name
+}
+
func prettifyToolName(name string) string {
switch name {
case agent.AgentToolName:
@@ -1194,6 +1216,9 @@ func prettifyToolName(name string) string {
case tools.WriteToolName:
return "Write"
default:
+ if strings.HasPrefix(name, "mcp_") {
+ return mcpToolName(name)
+ }
return name
}
}
@@ -181,11 +181,13 @@ func (m *toolCallCmp) View() string {
return box.Render(m.renderPending())
}
- r := registry.lookup(m.call.Name)
-
- if m.isNested {
+ if strings.HasPrefix(m.call.Name, "mcp_crush_docker") {
+ r := dockerMCPRenderer{}
return box.Render(r.Render(m))
}
+
+ r := registry.lookup(m.call.Name)
+
return box.Render(r.Render(m))
}
@@ -85,12 +85,15 @@ func RenderMCPList(opts RenderOptions) []string {
} else if l.MCP.Disabled {
description = t.S().Subtle.Render("disabled")
}
-
+ name := l.Name
+ if l.Name == "crush_docker" {
+ name = "Docker MCP"
+ }
mcpList = append(mcpList,
core.Status(
core.StatusOpts{
Icon: icon.String(),
- Title: l.Name,
+ Title: name,
Description: description,
ExtraContent: strings.Join(extraContent, " "),
},
@@ -52,10 +52,11 @@ func NewCharmtoneTheme() *Theme {
GreenDark: charmtone.Guac,
GreenLight: charmtone.Bok,
- Red: charmtone.Coral,
- RedDark: charmtone.Sriracha,
- RedLight: charmtone.Salmon,
- Cherry: charmtone.Cherry,
+ Red: charmtone.Coral,
+ RedDark: charmtone.Sriracha,
+ RedLight: charmtone.Salmon,
+ RedLighter: charmtone.Tuna,
+ Cherry: charmtone.Cherry,
}
// Text selection.
@@ -10,6 +10,7 @@ const (
LoadingIcon string = "β³"
DocumentIcon string = "πΌ"
ModelIcon string = "β"
+ ArrowIcon = "β"
// Tool call icons
ToolPending string = "β"
@@ -71,10 +71,11 @@ type Theme struct {
GreenLight color.Color
// Reds
- Red color.Color
- RedDark color.Color
- RedLight color.Color
- Cherry color.Color
+ Red color.Color
+ RedDark color.Color
+ RedLight color.Color
+ RedLighter color.Color
+ Cherry color.Color
// Text selection.
TextSelection lipgloss.Style