diff --git a/internal/ui/chat/search.go b/internal/ui/chat/search.go new file mode 100644 index 0000000000000000000000000000000000000000..6fa907d2c1f37dc17b62a925d720a52048f1f342 --- /dev/null +++ b/internal/ui/chat/search.go @@ -0,0 +1,194 @@ +package chat + +import ( + "encoding/json" + + "github.com/charmbracelet/crush/internal/agent/tools" + "github.com/charmbracelet/crush/internal/fsext" + "github.com/charmbracelet/crush/internal/message" + "github.com/charmbracelet/crush/internal/ui/styles" +) + +// ----------------------------------------------------------------------------- +// Glob Tool +// ----------------------------------------------------------------------------- + +// GlobToolMessageItem is a message item that represents a glob tool call. +type GlobToolMessageItem struct { + *baseToolMessageItem +} + +var _ ToolMessageItem = (*GlobToolMessageItem)(nil) + +// NewGlobToolMessageItem creates a new [GlobToolMessageItem]. +func NewGlobToolMessageItem( + sty *styles.Styles, + toolCall message.ToolCall, + result *message.ToolResult, + canceled bool, +) ToolMessageItem { + return newBaseToolMessageItem(sty, toolCall, result, &GlobToolRenderContext{}, canceled) +} + +// GlobToolRenderContext renders glob tool messages. +type GlobToolRenderContext struct{} + +// RenderTool implements the [ToolRenderer] interface. +func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) + if !opts.ToolCall.Finished && !opts.Canceled { + return pendingTool(sty, "Glob", opts.Anim) + } + + var params tools.GlobParams + if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) + } + + toolParams := []string{params.Pattern} + if params.Path != "" { + toolParams = append(toolParams, "path", params.Path) + } + + header := toolHeader(sty, opts.Status(), "Glob", cappedWidth, opts.Nested, toolParams...) + if opts.Nested { + return header + } + + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { + return joinToolParts(header, earlyState) + } + + if opts.Result == nil || opts.Result.Content == "" { + return header + } + + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal + body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded)) + return joinToolParts(header, body) +} + +// ----------------------------------------------------------------------------- +// Grep Tool +// ----------------------------------------------------------------------------- + +// GrepToolMessageItem is a message item that represents a grep tool call. +type GrepToolMessageItem struct { + *baseToolMessageItem +} + +var _ ToolMessageItem = (*GrepToolMessageItem)(nil) + +// NewGrepToolMessageItem creates a new [GrepToolMessageItem]. +func NewGrepToolMessageItem( + sty *styles.Styles, + toolCall message.ToolCall, + result *message.ToolResult, + canceled bool, +) ToolMessageItem { + return newBaseToolMessageItem(sty, toolCall, result, &GrepToolRenderContext{}, canceled) +} + +// GrepToolRenderContext renders grep tool messages. +type GrepToolRenderContext struct{} + +// RenderTool implements the [ToolRenderer] interface. +func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) + if !opts.ToolCall.Finished && !opts.Canceled { + return pendingTool(sty, "Grep", opts.Anim) + } + + var params tools.GrepParams + if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) + } + + toolParams := []string{params.Pattern} + if params.Path != "" { + toolParams = append(toolParams, "path", params.Path) + } + if params.Include != "" { + toolParams = append(toolParams, "include", params.Include) + } + if params.LiteralText { + toolParams = append(toolParams, "literal", "true") + } + + header := toolHeader(sty, opts.Status(), "Grep", cappedWidth, opts.Nested, toolParams...) + if opts.Nested { + return header + } + + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { + return joinToolParts(header, earlyState) + } + + if opts.Result == nil || opts.Result.Content == "" { + return header + } + + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal + body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded)) + return joinToolParts(header, body) +} + +// ----------------------------------------------------------------------------- +// LS Tool +// ----------------------------------------------------------------------------- + +// LSToolMessageItem is a message item that represents an ls tool call. +type LSToolMessageItem struct { + *baseToolMessageItem +} + +var _ ToolMessageItem = (*LSToolMessageItem)(nil) + +// NewLSToolMessageItem creates a new [LSToolMessageItem]. +func NewLSToolMessageItem( + sty *styles.Styles, + toolCall message.ToolCall, + result *message.ToolResult, + canceled bool, +) ToolMessageItem { + return newBaseToolMessageItem(sty, toolCall, result, &LSToolRenderContext{}, canceled) +} + +// LSToolRenderContext renders ls tool messages. +type LSToolRenderContext struct{} + +// RenderTool implements the [ToolRenderer] interface. +func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) + if !opts.ToolCall.Finished && !opts.Canceled { + return pendingTool(sty, "List", opts.Anim) + } + + var params tools.LSParams + if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) + } + + path := params.Path + if path == "" { + path = "." + } + path = fsext.PrettyPath(path) + + header := toolHeader(sty, opts.Status(), "List", cappedWidth, opts.Nested, path) + if opts.Nested { + return header + } + + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { + return joinToolParts(header, earlyState) + } + + if opts.Result == nil || opts.Result.Content == "" { + return header + } + + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal + body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded)) + return joinToolParts(header, body) +} diff --git a/internal/ui/chat/tools.go b/internal/ui/chat/tools.go index ef6a9d66b8aeacdcdaef252157bf6be3bca44ecd..40a3d0307be2080d4dd7991939303a409731bd64 100644 --- a/internal/ui/chat/tools.go +++ b/internal/ui/chat/tools.go @@ -171,6 +171,12 @@ func NewToolMessageItem( return NewEditToolMessageItem(sty, toolCall, result, canceled) case tools.MultiEditToolName: return NewMultiEditToolMessageItem(sty, toolCall, result, canceled) + case tools.GlobToolName: + return NewGlobToolMessageItem(sty, toolCall, result, canceled) + case tools.GrepToolName: + return NewGrepToolMessageItem(sty, toolCall, result, canceled) + case tools.LSToolName: + return NewLSToolMessageItem(sty, toolCall, result, canceled) default: // TODO: Implement other tool items return newBaseToolMessageItem(