Skip to content

Commit 10fc93a

Browse files
authored
Merge pull request #1031 from jeanlaurent/better-fetch-output
Simplify API and Fetch tool output display
2 parents 07d5baf + 62018f7 commit 10fc93a

2 files changed

Lines changed: 183 additions & 0 deletions

File tree

pkg/tui/components/tool/factory.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/docker/cagent/pkg/tui/components/tool/shell"
1010
"github.com/docker/cagent/pkg/tui/components/tool/todotool"
1111
"github.com/docker/cagent/pkg/tui/components/tool/transfertask"
12+
"github.com/docker/cagent/pkg/tui/components/tool/webtool"
1213
"github.com/docker/cagent/pkg/tui/components/tool/writefile"
1314
"github.com/docker/cagent/pkg/tui/core/layout"
1415
"github.com/docker/cagent/pkg/tui/service"
@@ -31,10 +32,18 @@ func NewFactory(registry *Registry) *Factory {
3132
func (f *Factory) Create(msg *types.Message, sessionState *service.SessionState) layout.Model {
3233
toolName := msg.ToolCall.Function.Name
3334

35+
// First try to match by exact tool name
3436
if builder, ok := f.registry.Get(toolName); ok {
3537
return builder(msg, sessionState)
3638
}
3739

40+
// Then try to match by category
41+
if msg.ToolDefinition.Category != "" {
42+
if builder, ok := f.registry.Get("category:" + msg.ToolDefinition.Category); ok {
43+
return builder(msg, sessionState)
44+
}
45+
}
46+
3847
return defaulttool.New(msg, sessionState)
3948
}
4049

@@ -57,6 +66,10 @@ func newDefaultRegistry() *Registry {
5766
registry.Register(builtin.ToolNameListTodos, todotool.New)
5867
registry.Register(builtin.ToolNameShell, shell.New)
5968

69+
// Register category-based handlers
70+
registry.Register("category:api", webtool.New)
71+
registry.Register(builtin.ToolNameFetch, webtool.New)
72+
6073
return registry
6174
}
6275

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package webtool
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
tea "charm.land/bubbletea/v2"
8+
9+
"github.com/docker/cagent/pkg/tui/components/spinner"
10+
"github.com/docker/cagent/pkg/tui/components/toolcommon"
11+
"github.com/docker/cagent/pkg/tui/core/layout"
12+
"github.com/docker/cagent/pkg/tui/service"
13+
"github.com/docker/cagent/pkg/tui/styles"
14+
"github.com/docker/cagent/pkg/tui/types"
15+
)
16+
17+
type Component struct {
18+
message *types.Message
19+
spinner spinner.Spinner
20+
width int
21+
height int
22+
}
23+
24+
func New(
25+
msg *types.Message,
26+
_ *service.SessionState,
27+
) layout.Model {
28+
return &Component{
29+
message: msg,
30+
spinner: spinner.New(spinner.ModeSpinnerOnly),
31+
width: 80,
32+
height: 1,
33+
}
34+
}
35+
36+
func (c *Component) SetSize(width, height int) tea.Cmd {
37+
c.width = width
38+
c.height = height
39+
return nil
40+
}
41+
42+
func (c *Component) Init() tea.Cmd {
43+
if c.message.ToolStatus == types.ToolStatusPending || c.message.ToolStatus == types.ToolStatusRunning {
44+
return c.spinner.Init()
45+
}
46+
return nil
47+
}
48+
49+
func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
50+
if c.message.ToolStatus == types.ToolStatusPending || c.message.ToolStatus == types.ToolStatusRunning {
51+
var cmd tea.Cmd
52+
var model layout.Model
53+
model, cmd = c.spinner.Update(msg)
54+
c.spinner = model.(spinner.Spinner)
55+
return c, cmd
56+
}
57+
58+
return c, nil
59+
}
60+
61+
func (c *Component) View() string {
62+
msg := c.message
63+
64+
// Parse the arguments to extract info about the API call
65+
var args map[string]any
66+
var progressText string
67+
68+
if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil {
69+
// If we can't parse, show spinner while running
70+
if msg.ToolStatus == types.ToolStatusRunning {
71+
progressText = c.spinner.View()
72+
}
73+
return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), progressText, "", c.width)
74+
}
75+
76+
// Extract argument summary for the tool call display
77+
argsText := formatArgs(args)
78+
79+
// Build the display name with inline result
80+
displayName := msg.ToolDefinition.DisplayName()
81+
if argsText != "" {
82+
displayName = displayName + "(" + styles.MutedStyle.Render(argsText) + ")"
83+
}
84+
85+
// Add inline result/progress after the tool name
86+
switch msg.ToolStatus {
87+
case types.ToolStatusRunning:
88+
// While running, show what we're calling
89+
endpoint := extractEndpoint(args)
90+
if endpoint != "" {
91+
displayName += styles.MutedStyle.Render(": Calling " + endpoint)
92+
}
93+
case types.ToolStatusCompleted:
94+
// When completed, show a brief summary inline
95+
resultSummary := extractSummary(msg.Content)
96+
displayName += styles.MutedStyle.Render(": " + resultSummary)
97+
}
98+
99+
// Render everything on one line
100+
return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), displayName, "", "", c.width)
101+
}
102+
103+
// extractEndpoint tries to find the endpoint/URL being called
104+
func extractEndpoint(args map[string]any) string {
105+
if endpoint, ok := args["endpoint"].(string); ok {
106+
return endpoint
107+
}
108+
if url, ok := args["url"].(string); ok {
109+
return url
110+
}
111+
return ""
112+
}
113+
114+
// formatArgs creates a concise string representation of the arguments
115+
func formatArgs(args map[string]any) string {
116+
if len(args) == 0 {
117+
return ""
118+
}
119+
120+
// Check for URL or URLs field (common in fetch tools)
121+
if urlVal, ok := args["url"].(string); ok && urlVal != "" {
122+
return urlVal
123+
}
124+
if urlsVal, ok := args["urls"].([]any); ok && len(urlsVal) > 0 {
125+
// Extract just the URLs from the array
126+
var urls []string
127+
for _, u := range urlsVal {
128+
if urlStr, ok := u.(string); ok {
129+
urls = append(urls, urlStr)
130+
}
131+
}
132+
if len(urls) == 1 {
133+
return urls[0]
134+
} else if len(urls) > 1 {
135+
return fmt.Sprintf("%s (+%d more)", urls[0], len(urls)-1)
136+
}
137+
}
138+
139+
// Try to find common parameter names that might indicate what's being queried
140+
for _, key := range []string{"query", "q", "search", "message", "prompt", "text"} {
141+
if val, ok := args[key]; ok {
142+
if str, ok := val.(string); ok && str != "" {
143+
return str
144+
}
145+
}
146+
}
147+
148+
// Fallback: show JSON
149+
b, _ := json.Marshal(args)
150+
return string(b)
151+
}
152+
153+
// extractSummary tries to extract a meaningful summary from the API response
154+
func extractSummary(content string) string {
155+
size := len(content)
156+
157+
// Convert to KB if >= 1024 bytes
158+
if size >= 1024*1024 {
159+
// Show in MB
160+
mb := float64(size) / (1024 * 1024)
161+
return fmt.Sprintf("Received %.1f MB", mb)
162+
} else if size >= 1024 {
163+
// Show in KB
164+
kb := float64(size) / 1024
165+
return fmt.Sprintf("Received %.1f KB", kb)
166+
}
167+
168+
// Show in bytes for small responses
169+
return fmt.Sprintf("Received %d bytes", size)
170+
}

0 commit comments

Comments
 (0)