on main help sign in
Merge pull request #1679 from charmbracelet/chat-search-tools

refactor(chat): add search tools

commitee7137e0e87d8b1792c0e7c38a8f91c57d0d09bb authorAyman Bagabas <ayman.bagabas@gmail.com> dateFri 19 Dec 2025, 12:01 (1 year ago) parent78544f75699a91
2 files changed+200
internal/ui/chat/search.go+194 internal/ui/chat/tools.go+6
internal/ui/chat/search.gonew file+194
@@ -0,0 +1,194 @@
1+package chat
2+
3+import (
4+ "encoding/json"
5+
6+ "github.com/charmbracelet/crush/internal/agent/tools"
7+ "github.com/charmbracelet/crush/internal/fsext"
8+ "github.com/charmbracelet/crush/internal/message"
9+ "github.com/charmbracelet/crush/internal/ui/styles"
10+)
11+
12+// -----------------------------------------------------------------------------
13+// Glob Tool
14+// -----------------------------------------------------------------------------
15+
16+// GlobToolMessageItem is a message item that represents a glob tool call.
17+type GlobToolMessageItem struct {
18+ *baseToolMessageItem
19+}
20+
21+var _ ToolMessageItem = (*GlobToolMessageItem)(nil)
22+
23+// NewGlobToolMessageItem creates a new [GlobToolMessageItem].
24+func NewGlobToolMessageItem(
25+ sty *styles.Styles,
26+ toolCall message.ToolCall,
27+ result *message.ToolResult,
28+ canceled bool,
29+) ToolMessageItem {
30+ return newBaseToolMessageItem(sty, toolCall, result, &GlobToolRenderContext{}, canceled)
31+}
32+
33+// GlobToolRenderContext renders glob tool messages.
34+type GlobToolRenderContext struct{}
35+
36+// RenderTool implements the [ToolRenderer] interface.
37+func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
38+ cappedWidth := cappedMessageWidth(width)
39+ if !opts.ToolCall.Finished && !opts.Canceled {
40+ return pendingTool(sty, "Glob", opts.Anim)
41+ }
42+
43+ var params tools.GlobParams
44+ if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
45+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
46+ }
47+
48+ toolParams := []string{params.Pattern}
49+ if params.Path != "" {
50+ toolParams = append(toolParams, "path", params.Path)
51+ }
52+
53+ header := toolHeader(sty, opts.Status(), "Glob", cappedWidth, opts.Nested, toolParams...)
54+ if opts.Nested {
55+ return header
56+ }
57+
58+ if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
59+ return joinToolParts(header, earlyState)
60+ }
61+
62+ if opts.Result == nil || opts.Result.Content == "" {
63+ return header
64+ }
65+
66+ bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
67+ body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded))
68+ return joinToolParts(header, body)
69+}
70+
71+// -----------------------------------------------------------------------------
72+// Grep Tool
73+// -----------------------------------------------------------------------------
74+
75+// GrepToolMessageItem is a message item that represents a grep tool call.
76+type GrepToolMessageItem struct {
77+ *baseToolMessageItem
78+}
79+
80+var _ ToolMessageItem = (*GrepToolMessageItem)(nil)
81+
82+// NewGrepToolMessageItem creates a new [GrepToolMessageItem].
83+func NewGrepToolMessageItem(
84+ sty *styles.Styles,
85+ toolCall message.ToolCall,
86+ result *message.ToolResult,
87+ canceled bool,
88+) ToolMessageItem {
89+ return newBaseToolMessageItem(sty, toolCall, result, &GrepToolRenderContext{}, canceled)
90+}
91+
92+// GrepToolRenderContext renders grep tool messages.
93+type GrepToolRenderContext struct{}
94+
95+// RenderTool implements the [ToolRenderer] interface.
96+func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
97+ cappedWidth := cappedMessageWidth(width)
98+ if !opts.ToolCall.Finished && !opts.Canceled {
99+ return pendingTool(sty, "Grep", opts.Anim)
100+ }
101+
102+ var params tools.GrepParams
103+ if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
104+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
105+ }
106+
107+ toolParams := []string{params.Pattern}
108+ if params.Path != "" {
109+ toolParams = append(toolParams, "path", params.Path)
110+ }
111+ if params.Include != "" {
112+ toolParams = append(toolParams, "include", params.Include)
113+ }
114+ if params.LiteralText {
115+ toolParams = append(toolParams, "literal", "true")
116+ }
117+
118+ header := toolHeader(sty, opts.Status(), "Grep", cappedWidth, opts.Nested, toolParams...)
119+ if opts.Nested {
120+ return header
121+ }
122+
123+ if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
124+ return joinToolParts(header, earlyState)
125+ }
126+
127+ if opts.Result == nil || opts.Result.Content == "" {
128+ return header
129+ }
130+
131+ bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
132+ body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded))
133+ return joinToolParts(header, body)
134+}
135+
136+// -----------------------------------------------------------------------------
137+// LS Tool
138+// -----------------------------------------------------------------------------
139+
140+// LSToolMessageItem is a message item that represents an ls tool call.
141+type LSToolMessageItem struct {
142+ *baseToolMessageItem
143+}
144+
145+var _ ToolMessageItem = (*LSToolMessageItem)(nil)
146+
147+// NewLSToolMessageItem creates a new [LSToolMessageItem].
148+func NewLSToolMessageItem(
149+ sty *styles.Styles,
150+ toolCall message.ToolCall,
151+ result *message.ToolResult,
152+ canceled bool,
153+) ToolMessageItem {
154+ return newBaseToolMessageItem(sty, toolCall, result, &LSToolRenderContext{}, canceled)
155+}
156+
157+// LSToolRenderContext renders ls tool messages.
158+type LSToolRenderContext struct{}
159+
160+// RenderTool implements the [ToolRenderer] interface.
161+func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
162+ cappedWidth := cappedMessageWidth(width)
163+ if !opts.ToolCall.Finished && !opts.Canceled {
164+ return pendingTool(sty, "List", opts.Anim)
165+ }
166+
167+ var params tools.LSParams
168+ if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
169+ return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
170+ }
171+
172+ path := params.Path
173+ if path == "" {
174+ path = "."
175+ }
176+ path = fsext.PrettyPath(path)
177+
178+ header := toolHeader(sty, opts.Status(), "List", cappedWidth, opts.Nested, path)
179+ if opts.Nested {
180+ return header
181+ }
182+
183+ if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
184+ return joinToolParts(header, earlyState)
185+ }
186+
187+ if opts.Result == nil || opts.Result.Content == "" {
188+ return header
189+ }
190+
191+ bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
192+ body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded))
193+ return joinToolParts(header, body)
194+}
internal/ui/chat/tools.go+6
@@ -171,6 +171,12 @@ func NewToolMessageItem(
171171 return NewEditToolMessageItem(sty, toolCall, result, canceled)
172172 case tools.MultiEditToolName:
173173 return NewMultiEditToolMessageItem(sty, toolCall, result, canceled)
174+ case tools.GlobToolName:
175+ return NewGlobToolMessageItem(sty, toolCall, result, canceled)
176+ case tools.GrepToolName:
177+ return NewGrepToolMessageItem(sty, toolCall, result, canceled)
178+ case tools.LSToolName:
179+ return NewLSToolMessageItem(sty, toolCall, result, canceled)
174180 default:
175181 // TODO: Implement other tool items
176182 return newBaseToolMessageItem(