Skip to content

Commit 9bff880

Browse files
authored
Merge pull request #525 from dgageot/improve-tool-calls-rendering
Improve tool calls rendering
2 parents 8cb5322 + 664e80b commit 9bff880

7 files changed

Lines changed: 128 additions & 329 deletions

File tree

pkg/tools/builtin/filesystem.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,10 @@ func (t *FilesystemTool) handleEditFile(ctx context.Context, toolCall tools.Tool
531531
return &tools.ToolCallResult{Output: fmt.Sprintf("File edited successfully but post-edit command failed: %s", err)}, nil
532532
}
533533

534+
if len(changes) == 1 {
535+
return &tools.ToolCallResult{Output: fmt.Sprintf("File edited successfully. %s", strings.TrimPrefix(changes[0], "Edit 1: "))}, nil
536+
}
537+
534538
return &tools.ToolCallResult{Output: fmt.Sprintf("File edited successfully. Changes:\n%s", strings.Join(changes, "\n"))}, nil
535539
}
536540

pkg/tools/builtin/todo.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type CreateTodoArgs struct {
3333
}
3434

3535
type CreateTodosArgs struct {
36-
Todos []CreateTodoItem `json:"todos" jsonschema:"List of todo items"`
36+
Descriptions []string `json:"descriptions" jsonschema:"Descriptions of the todo items"`
3737
}
3838

3939
type UpdateTodoArgs struct {
@@ -90,7 +90,7 @@ func (h *todoHandler) createTodo(_ context.Context, toolCall tools.ToolCall) (*t
9090
}
9191

9292
return &tools.ToolCallResult{
93-
Output: fmt.Sprintf("Created todo %s: %s", id, params.Description),
93+
Output: fmt.Sprintf("Created todo [%s]: %s", id, params.Description),
9494
}, nil
9595
}
9696

@@ -100,20 +100,28 @@ func (h *todoHandler) createTodos(_ context.Context, toolCall tools.ToolCall) (*
100100
return nil, fmt.Errorf("invalid arguments: %w", err)
101101
}
102102

103-
ids := make([]string, len(params.Todos))
103+
ids := make([]string, len(params.Descriptions))
104104
start := len(h.todos)
105-
for i, todo := range params.Todos {
105+
for i, desc := range params.Descriptions {
106106
id := fmt.Sprintf("todo_%d", start+i+1)
107107
h.todos[id] = Todo{
108108
ID: id,
109-
Description: todo.Description,
109+
Description: desc,
110110
Status: "pending",
111111
}
112112
ids[i] = id
113113
}
114114

115+
output := fmt.Sprintf("Created %d todos: ", len(params.Descriptions))
116+
for i, id := range ids {
117+
if i > 0 {
118+
output += ", "
119+
}
120+
output += fmt.Sprintf("[%s]", id)
121+
}
122+
115123
return &tools.ToolCallResult{
116-
Output: fmt.Sprintf("Created %d todos:\n%s", len(params.Todos), strings.Join(ids, "\n")),
124+
Output: output,
117125
}, nil
118126
}
119127

@@ -125,14 +133,14 @@ func (h *todoHandler) updateTodo(_ context.Context, toolCall tools.ToolCall) (*t
125133

126134
todo, exists := h.todos[params.ID]
127135
if !exists {
128-
return nil, fmt.Errorf("todo %s not found", params.ID)
136+
return nil, fmt.Errorf("todo [%s] not found", params.ID)
129137
}
130138

131139
todo.Status = params.Status
132140
h.todos[params.ID] = todo
133141

134142
return &tools.ToolCallResult{
135-
Output: fmt.Sprintf("Updated todo %s status to: %s", params.ID, params.Status),
143+
Output: fmt.Sprintf("Updated todo [%s] to status: [%s]", params.ID, params.Status),
136144
}, nil
137145
}
138146

pkg/tools/builtin/todo_test.go

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,14 @@ func TestTodoTool_Tools(t *testing.T) {
6868
assert.JSONEq(t, `{
6969
"type": "object",
7070
"required": [
71-
"todos"
71+
"descriptions"
7272
],
7373
"properties": {
74-
"todos": {
74+
"descriptions": {
7575
"type": "array",
76-
"description": "List of todo items",
76+
"description": "Descriptions of the todo items",
7777
"items": {
78-
"type": "object",
79-
"required": [
80-
"description"
81-
],
82-
"properties": {
83-
"description": {
84-
"type": "string",
85-
"description": "Description of the todo item"
86-
}
87-
},
88-
"additionalProperties": false
78+
"type": "string"
8979
}
9080
}
9181
},
@@ -159,7 +149,7 @@ func TestTodoTool_CreateTodo(t *testing.T) {
159149

160150
// Verify
161151
require.NoError(t, err)
162-
assert.Contains(t, result.Output, "Created todo todo_1: Test todo item")
152+
assert.Contains(t, result.Output, "Created todo [todo_1]: Test todo item")
163153

164154
// Verify todo was added to the handler's todos map
165155
assert.Len(t, tool.handler.todos, 1)
@@ -181,10 +171,10 @@ func TestTodoTool_CreateTodos(t *testing.T) {
181171

182172
// Create multiple todos
183173
args := CreateTodosArgs{
184-
Todos: []CreateTodoItem{
185-
{Description: "First todo item"},
186-
{Description: "Second todo item"},
187-
{Description: "Third todo item"},
174+
Descriptions: []string{
175+
"First todo item",
176+
"Second todo item",
177+
"Third todo item",
188178
},
189179
}
190180
argsBytes, err := json.Marshal(args)
@@ -212,8 +202,8 @@ func TestTodoTool_CreateTodos(t *testing.T) {
212202

213203
// Create multiple todos
214204
args = CreateTodosArgs{
215-
Todos: []CreateTodoItem{
216-
{Description: "Last todo item"},
205+
Descriptions: []string{
206+
"Last todo item",
217207
},
218208
}
219209
argsBytes, err = json.Marshal(args)
@@ -283,7 +273,7 @@ func TestTodoTool_UpdateTodo(t *testing.T) {
283273

284274
// Verify
285275
require.NoError(t, err)
286-
assert.Contains(t, result.Output, "Updated todo todo_1 status to: completed")
276+
assert.Contains(t, result.Output, "Updated todo [todo_1] to status: [completed]")
287277

288278
// Verify todo status was updated
289279
todo, exists := tool.handler.todos["todo_1"]

pkg/tui/components/tool/builtins.go

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,14 @@ package tool
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"strings"
67

7-
"github.com/charmbracelet/glamour/v2"
8-
9-
"github.com/docker/cagent/pkg/codemode"
108
"github.com/docker/cagent/pkg/tools"
119
"github.com/docker/cagent/pkg/tools/builtin"
10+
"github.com/docker/cagent/pkg/tui/styles"
1211
)
1312

14-
func renderSearchFiles(toolCall tools.ToolCall) string {
15-
var args builtin.SearchFilesArgs
16-
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
17-
return ""
18-
}
19-
20-
// Search pattern
21-
output := args.Pattern
22-
23-
// Optional search path
24-
if path := args.Path; path != "" && path != "." {
25-
output += " in " + path
26-
}
27-
28-
// Optional exclude patterns
29-
if exclude := args.ExcludePatterns; len(exclude) > 0 {
30-
output += " excluding [" + strings.Join(exclude, ", ") + "]"
31-
}
32-
33-
return output
34-
}
35-
36-
func renderRunToolsWithJavascript(toolCall tools.ToolCall, renderer *glamour.TermRenderer) string {
37-
var args codemode.RunToolsWithJavascriptArgs
38-
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
39-
return ""
40-
}
41-
42-
md, err := renderer.Render("```javascript\n" + args.Script + "\n```")
43-
if err != nil {
44-
return args.Script
45-
}
46-
47-
return md
48-
}
49-
5013
func renderEditFile(toolCall tools.ToolCall, width int) (string, string) {
5114
var args builtin.EditFileArgs
5215
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
@@ -70,20 +33,67 @@ func renderEditFile(toolCall tools.ToolCall, width int) (string, string) {
7033
return output.String(), args.Path
7134
}
7235

73-
func renderShell(toolCall tools.ToolCall, renderer *glamour.TermRenderer) string {
74-
var args builtin.RunShellArgs
75-
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
36+
func renderToolArgs(toolCall tools.ToolCall, width int) string {
37+
decoder := json.NewDecoder(strings.NewReader(toolCall.Function.Arguments))
38+
39+
tok, err := decoder.Token()
40+
if err != nil {
41+
return ""
42+
}
43+
if delim, ok := tok.(json.Delim); !ok || delim != '{' {
7644
return ""
7745
}
7846

79-
md, err := renderer.Render("```sh\n" + args.Cmd + "\n```")
80-
if err != nil {
81-
md = args.Cmd
47+
type kv struct {
48+
Key string
49+
Value any
50+
}
51+
var kvs []kv
52+
53+
for decoder.More() {
54+
tok, err := decoder.Token()
55+
if err != nil {
56+
return ""
57+
}
58+
key, ok := tok.(string)
59+
if !ok {
60+
return ""
61+
}
62+
63+
var val any
64+
if err := decoder.Decode(&val); err != nil {
65+
return ""
66+
}
67+
68+
kvs = append(kvs, kv{Key: key, Value: val})
8269
}
70+
_, _ = decoder.Token()
71+
72+
style := styles.ToolCallArgs.Width(width)
73+
74+
var md strings.Builder
75+
for i, kv := range kvs {
76+
if i > 0 {
77+
md.WriteString("\n")
78+
}
8379

84-
if args.Cwd != "." {
85-
md += "\n In directory: " + args.Cwd + "\n"
80+
var content string
81+
if v, ok := kv.Value.(string); ok {
82+
content = v
83+
} else {
84+
buf, err := json.MarshalIndent(kv.Value, "", " ")
85+
if err != nil {
86+
content = fmt.Sprintf("%v", kv.Value)
87+
} else {
88+
content = string(buf)
89+
}
90+
}
91+
92+
fmt.Fprintf(&md, "%s:\n%s", styles.ToolCallArgKey.Render(kv.Key), content)
93+
if !strings.HasSuffix(content, "\n") {
94+
md.WriteString("\n")
95+
}
8696
}
8797

88-
return md
98+
return "\n" + style.Render(strings.TrimSuffix(md.String(), "\n"))
8999
}

0 commit comments

Comments
 (0)