From c5f0e4da2baa31e865effd3da90abf7a222bb98f Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Tue, 27 Jan 2026 17:46:51 +0100 Subject: [PATCH] Handle unknown tool calls in the TUI (#2001) --- internal/ui/chat/generic.go | 98 +++++++++++++++++++++++++++++++++++++ internal/ui/chat/tools.go | 11 +---- internal/ui/model/ui.go | 5 -- 3 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 internal/ui/chat/generic.go diff --git a/internal/ui/chat/generic.go b/internal/ui/chat/generic.go new file mode 100644 index 0000000000000000000000000000000000000000..6b0ac433028daf7a06c57f85c7799250e9652f6f --- /dev/null +++ b/internal/ui/chat/generic.go @@ -0,0 +1,98 @@ +package chat + +import ( + "encoding/json" + "strings" + + "github.com/charmbracelet/crush/internal/message" + "github.com/charmbracelet/crush/internal/stringext" + "github.com/charmbracelet/crush/internal/ui/styles" +) + +// GenericToolMessageItem is a message item that represents an unknown tool call. +type GenericToolMessageItem struct { + *baseToolMessageItem +} + +var _ ToolMessageItem = (*GenericToolMessageItem)(nil) + +// NewGenericToolMessageItem creates a new [GenericToolMessageItem]. +func NewGenericToolMessageItem( + sty *styles.Styles, + toolCall message.ToolCall, + result *message.ToolResult, + canceled bool, +) ToolMessageItem { + return newBaseToolMessageItem(sty, toolCall, result, &GenericToolRenderContext{}, canceled) +} + +// GenericToolRenderContext renders unknown/generic tool messages. +type GenericToolRenderContext struct{} + +// RenderTool implements the [ToolRenderer] interface. +func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) + name := genericPrettyName(opts.ToolCall.Name) + + if opts.IsPending() { + return pendingTool(sty, name, opts.Anim) + } + + var params map[string]any + if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) + } + + var toolParams []string + if len(params) > 0 { + parsed, _ := json.Marshal(params) + toolParams = append(toolParams, string(parsed)) + } + + header := toolHeader(sty, opts.Status, name, cappedWidth, opts.Compact, toolParams...) + if opts.Compact { + return header + } + + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { + return joinToolParts(header, earlyState) + } + + if !opts.HasResult() || opts.Result.Content == "" { + return header + } + + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal + + // Handle image data. + if opts.Result.Data != "" && strings.HasPrefix(opts.Result.MIMEType, "image/") { + body := sty.Tool.Body.Render(toolOutputImageContent(sty, opts.Result.Data, opts.Result.MIMEType)) + return joinToolParts(header, body) + } + + // Try to parse result as JSON for pretty display. + var result json.RawMessage + var body string + if err := json.Unmarshal([]byte(opts.Result.Content), &result); err == nil { + prettyResult, err := json.MarshalIndent(result, "", " ") + if err == nil { + body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.json", string(prettyResult), 0, bodyWidth, opts.ExpandedContent)) + } else { + body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) + } + } else if looksLikeMarkdown(opts.Result.Content) { + body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", opts.Result.Content, 0, bodyWidth, opts.ExpandedContent)) + } else { + body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) + } + + return joinToolParts(header, body) +} + +// genericPrettyName converts a snake_case or kebab-case tool name to a +// human-readable title case name. +func genericPrettyName(name string) string { + name = strings.ReplaceAll(name, "_", " ") + name = strings.ReplaceAll(name, "-", " ") + return stringext.Capitalize(name) +} diff --git a/internal/ui/chat/tools.go b/internal/ui/chat/tools.go index e10d28e061e17c636dc9e1a6cfe364ca6f220d0e..8aac1c1401fe299b24bd2cda81e18113bfd6176d 100644 --- a/internal/ui/chat/tools.go +++ b/internal/ui/chat/tools.go @@ -255,14 +255,7 @@ func NewToolMessageItem( if strings.HasPrefix(toolCall.Name, "mcp_") { item = NewMCPToolMessageItem(sty, toolCall, result, canceled) } else { - // TODO: Implement other tool items - item = newBaseToolMessageItem( - sty, - toolCall, - result, - &DefaultToolRenderContext{}, - canceled, - ) + item = NewGenericToolMessageItem(sty, toolCall, result, canceled) } } item.SetMessageID(messageID) @@ -1399,6 +1392,6 @@ func prettifyToolName(name string) string { case tools.WriteToolName: return "Write" default: - return name + return genericPrettyName(name) } } diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index e6e54af525cb72c61a942e408dcbda9203de6cf2..121446524cb95fe8f443f551e14273e5327dd1a1 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -1124,11 +1124,6 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd { cmds = append(cmds, m.toggleCompactMode()) m.dialog.CloseDialog(dialog.CommandsID) case dialog.ActionToggleThinking: - if m.isAgentBusy() { - cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait...")) - break - } - cmds = append(cmds, func() tea.Msg { cfg := m.com.Config() if cfg == nil {