Skip to content
24 changes: 18 additions & 6 deletions pkg/tui/components/tool/api/apitool.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, render)
return toolcommon.NewBaseWithCollapsed(msg, sessionState, render, renderCollapsed)
}

func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionStateReader, width, _ int) string {
Expand All @@ -24,11 +24,7 @@ func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionS
return toolcommon.RenderTool(msg, s, "", "", width, sessionState.HideToolResults())
}

// Extract argument summary for the tool call display
var params string
if argsText := formatArgs(args); argsText != "" {
params = "(" + argsText + ")"
}
params := formatParams(args)

// Add inline result/progress after the tool name
switch msg.ToolStatus {
Expand All @@ -45,6 +41,22 @@ func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionS
return toolcommon.RenderTool(msg, s, params, "", width, sessionState.HideToolResults())
}

func renderCollapsed(msg *types.Message, s spinner.Spinner, sessionState service.SessionStateReader, width, _ int) string {
var args map[string]any
if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil {
return toolcommon.RenderTool(msg, s, "", "", width, sessionState.HideToolResults())
}

return toolcommon.RenderTool(msg, s, formatParams(args), "", width, sessionState.HideToolResults())
}

func formatParams(args map[string]any) string {
if argsText := formatArgs(args); argsText != "" {
return "(" + argsText + ")"
}
return ""
}

// extractEndpoint tries to find the endpoint/URL being called.
func extractEndpoint(args map[string]any) string {
if endpoint, ok := args["endpoint"].(string); ok {
Expand Down
10 changes: 9 additions & 1 deletion pkg/tui/components/tool/defaulttool/defaulttool.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
// New creates a new default tool component.
// It provides a standard visualization with tool name, arguments, and results.
func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, render)
return toolcommon.NewBaseWithCollapsed(msg, sessionState, render, renderCollapsed)
}

func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionStateReader, width, _ int) string {
Expand All @@ -31,3 +31,11 @@ func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionS

return toolcommon.RenderTool(msg, s, argsContent, resultContent, width, sessionState.HideToolResults())
}

func renderCollapsed(msg *types.Message, s spinner.Spinner, sessionState service.SessionStateReader, width, _ int) string {
var argsContent string
if msg.ToolCall.Function.Arguments != "" {
argsContent = renderToolArgsSummary(msg.ToolCall, width-4-len(msg.ToolDefinition.DisplayName()))
}
return toolcommon.RenderTool(msg, s, argsContent, "", width, sessionState.HideToolResults())
}
48 changes: 35 additions & 13 deletions pkg/tui/components/tool/defaulttool/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"charm.land/lipgloss/v2"

"github.com/docker/docker-agent/pkg/tools"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/styles"
)

Expand All @@ -17,19 +18,7 @@ type kv struct {
}

func renderToolArgs(toolCall tools.ToolCall, shortWidth, width int) string {
args, err := decodeArguments(toolCall.Function.Arguments)
if err != nil {
return ""
}

// Filter out the friendly description parameter
filteredArgs := make([]kv, 0, len(args))
for _, arg := range args {
if arg.Key != tools.DescriptionParam {
filteredArgs = append(filteredArgs, arg)
}
}

filteredArgs := filteredToolArgs(toolCall)
if len(filteredArgs) == 0 {
return ""
}
Expand Down Expand Up @@ -58,6 +47,39 @@ func renderToolArgs(toolCall tools.ToolCall, shortWidth, width int) string {
return "\n" + styles.ToolCallArgs.Width(width).Render(strings.TrimSuffix(md.String(), "\n"))
}

func renderToolArgsSummary(toolCall tools.ToolCall, width int) string {
filteredArgs := filteredToolArgs(toolCall)
if len(filteredArgs) == 0 {
return ""
}

arg := filteredArgs[0]
summary := fmt.Sprintf("%s=%s", arg.Key, formatValueInline(arg.Value))
if len(filteredArgs) > 1 {
summary += fmt.Sprintf(" (+%d more)", len(filteredArgs)-1)
}
return toolcommon.TruncateText(summary, max(width, 1))
}

func filteredToolArgs(toolCall tools.ToolCall) []kv {
args, err := decodeArguments(toolCall.Function.Arguments)
if err != nil {
return nil
}

filteredArgs := make([]kv, 0, len(args))
for _, arg := range args {
if arg.Key != tools.DescriptionParam {
filteredArgs = append(filteredArgs, arg)
}
}
return filteredArgs
}

func formatValueInline(value any) string {
return strings.Join(strings.Fields(formatValue(value)), " ")
}

// formatValue formats a value for display.
// Single-element arrays are kept on one line, while larger arrays are indented.
func formatValue(value any) string {
Expand Down
23 changes: 19 additions & 4 deletions pkg/tui/components/tool/directorytree/directorytree.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,32 @@ import (

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/spinner"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core/layout"
"github.com/docker/docker-agent/pkg/tui/service"
"github.com/docker/docker-agent/pkg/tui/styles"
"github.com/docker/docker-agent/pkg/tui/types"
)

func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, toolcommon.SimpleRendererWithResult(
toolcommon.ExtractField(func(a filesystem.DirectoryTreeArgs) string { return pathx.ShortenHome(a.Path) }),
extractResult,
))
extractPath := toolcommon.ExtractField(func(a filesystem.DirectoryTreeArgs) string { return pathx.ShortenHome(a.Path) })
renderPath := toolcommon.SimpleRenderer(extractPath)
return toolcommon.NewBaseWithCollapsed(
msg,
sessionState,
render,
toolcommon.CollapsedRenderer(renderPath),
)
}

func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionStateReader, width, _ int) string {
path := toolcommon.ExtractField(func(a filesystem.DirectoryTreeArgs) string { return pathx.ShortenHome(a.Path) })(msg.ToolCall.Function.Arguments)
header := toolcommon.RenderTool(msg, s, path, extractResult(msg), width, sessionState.HideToolResults())
if sessionState.HideToolResults() || msg.Content == "" {
return header
}
return header + "\n" + styles.ToolCallResult.Render(toolcommon.FormatToolResult(msg.Content, width))
}

func extractResult(msg *types.Message) string {
Expand Down
55 changes: 51 additions & 4 deletions pkg/tui/components/tool/listdirectory/listdirectory.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,39 @@ import (

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/spinner"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core/layout"
"github.com/docker/docker-agent/pkg/tui/service"
"github.com/docker/docker-agent/pkg/tui/styles"
"github.com/docker/docker-agent/pkg/tui/types"
)

const maxVisibleEntries = 10

func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, toolcommon.SimpleRendererWithResult(
toolcommon.ExtractField(func(a filesystem.ListDirectoryArgs) string { return pathx.ShortenHome(a.Path) }),
extractResult,
))
extractPath := toolcommon.ExtractField(func(a filesystem.ListDirectoryArgs) string { return pathx.ShortenHome(a.Path) })
renderPath := toolcommon.SimpleRenderer(extractPath)
return toolcommon.NewBaseWithCollapsed(
msg,
sessionState,
render,
toolcommon.CollapsedRenderer(renderPath),
)
}

func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionStateReader, width, _ int) string {
path := toolcommon.ExtractField(func(a filesystem.ListDirectoryArgs) string { return pathx.ShortenHome(a.Path) })(msg.ToolCall.Function.Arguments)
header := toolcommon.RenderTool(msg, s, path, extractResult(msg), width, sessionState.HideToolResults())
if sessionState.HideToolResults() {
return header
}

details := renderEntries(msg)
if details == "" {
return header
}
return header + "\n" + styles.ToolCallResult.Render(details)
}

func extractResult(msg *types.Message) string {
Expand Down Expand Up @@ -47,3 +69,28 @@ func extractResult(msg *types.Message) string {
}
return result
}

func renderEntries(msg *types.Message) string {
if msg.ToolResult == nil || msg.ToolResult.Meta == nil {
return ""
}
meta, ok := msg.ToolResult.Meta.(filesystem.ListDirectoryMeta)
if !ok {
return ""
}

entries := make([]string, 0, len(meta.Dirs)+len(meta.Files)+1)
for _, dir := range meta.Dirs {
entries = append(entries, "DIR "+dir)
}
for _, file := range meta.Files {
entries = append(entries, "FILE "+file)
}
if len(entries) == 0 {
return ""
}
if len(entries) > maxVisibleEntries {
entries = append(entries[:maxVisibleEntries], "…")
}
return strings.Join(entries, "\n")
}
54 changes: 50 additions & 4 deletions pkg/tui/components/tool/readfile/readfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,43 @@ package readfile

import (
"fmt"
"strings"

pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
"github.com/docker/docker-agent/pkg/tui/components/spinner"
"github.com/docker/docker-agent/pkg/tui/components/toolcommon"
"github.com/docker/docker-agent/pkg/tui/core/layout"
"github.com/docker/docker-agent/pkg/tui/service"
"github.com/docker/docker-agent/pkg/tui/styles"
"github.com/docker/docker-agent/pkg/tui/types"
)

const maxPreviewLines = 10

func New(msg *types.Message, sessionState service.SessionStateReader) layout.Model {
return toolcommon.NewBase(msg, sessionState, toolcommon.SimpleRendererWithResult(
toolcommon.ExtractField(func(a filesystem.ReadFileArgs) string { return pathx.ShortenHome(a.Path) }),
extractResult,
))
extractPath := toolcommon.ExtractField(func(a filesystem.ReadFileArgs) string { return pathx.ShortenHome(a.Path) })
renderPath := toolcommon.SimpleRenderer(extractPath)
return toolcommon.NewBaseWithCollapsed(
msg,
sessionState,
render,
toolcommon.CollapsedRenderer(renderPath),
)
}

func render(msg *types.Message, s spinner.Spinner, sessionState service.SessionStateReader, width, _ int) string {
path := toolcommon.ExtractField(func(a filesystem.ReadFileArgs) string { return pathx.ShortenHome(a.Path) })(msg.ToolCall.Function.Arguments)
header := toolcommon.RenderTool(msg, s, path, extractResult(msg), width, sessionState.HideToolResults())
if sessionState.HideToolResults() || msg.ToolStatus == types.ToolStatusError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] Read-file preview shown for in-progress / pending status — inconsistent with writefile

The new render function skips the preview only when HideToolResults() is true or the status is ToolStatusError:

if sessionState.HideToolResults() || msg.ToolStatus == types.ToolStatusError {
    return header
}
preview := formatLastLines(msg.Content, width)

If msg.Content is ever non-empty while the tool is still running (e.g. during streaming), a partial preview would be surfaced. The sibling writefile renderer guards against this explicitly:

if msg.ToolStatus == types.ToolStatusCompleted || msg.ToolStatus == types.ToolStatusError {
    result = msg.Content
}

If read_file is guaranteed to only produce content after completion this is fine as-is, but adding a ToolStatusCompleted guard would make the intent explicit and guard against future streaming changes:

if sessionState.HideToolResults() || msg.ToolStatus != types.ToolStatusCompleted {
    return header
}

return header
}

preview := formatLastLines(msg.Content, width)
if preview == "" {
return header
}
return header + "\n" + styles.ToolCallResult.Render(preview)
}

func extractResult(msg *types.Message) string {
Expand All @@ -31,3 +54,26 @@ func extractResult(msg *types.Message) string {
}
return fmt.Sprintf("%d lines", meta.LineCount)
}

func formatLastLines(content string, width int) string {
content = strings.TrimRight(content, "\n")
if content == "" {
return ""
}

lines := strings.Split(content, "\n")
truncated := len(lines) > maxPreviewLines
if truncated {
lines = lines[len(lines)-maxPreviewLines:]
}

availableWidth := max(width-styles.ToolCallResult.GetHorizontalFrameSize(), 10)
wrapped := make([]string, 0, len(lines)+1)
if truncated {
wrapped = append(wrapped, "…")
}
for _, line := range lines {
wrapped = append(wrapped, toolcommon.WrapLines(line, availableWidth)...)
}
return strings.Join(wrapped, "\n")
}
Loading
Loading