Files
kit/internal/core/ls.go
T
Ed Zynda 424847f0db feat: enable parallel tool execution with multi-tool status display
- Mark read-only core tools as parallel-safe (read, grep, find, ls)
- Mark spawn_subagent as parallel-safe for concurrent task delegation
- Update UI to track multiple active tools during parallel execution
- Display 'Running: tool1, tool2, ...' in spinner for concurrent tools
- Add test for parallel tool execution scenarios

Fantasy already supports parallel execution via ToolInfo.Parallel field.
Tools marked parallel run concurrently (up to 5 at a time).
2026-03-14 17:24:20 +03:00

104 lines
2.6 KiB
Go

package core
import (
"context"
"fmt"
"os"
"sort"
"strings"
"charm.land/fantasy"
)
type lsArgs struct {
Path string `json:"path,omitempty"`
Limit int `json:"limit,omitempty"`
}
// NewLsTool creates the ls core tool.
func NewLsTool(opts ...ToolOption) fantasy.AgentTool {
cfg := ApplyOptions(opts)
return &coreTool{
info: fantasy.ToolInfo{
Name: "ls",
Description: "List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to 500 entries or 50KB.",
Parameters: map[string]any{
"path": map[string]any{
"type": "string",
"description": "Directory to list (default: current directory)",
},
"limit": map[string]any{
"type": "number",
"description": "Maximum number of entries to return (default: 500)",
},
},
Required: []string{},
Parallel: true,
},
handler: func(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
return executeLs(ctx, call, cfg.WorkDir)
},
}
}
func executeLs(ctx context.Context, call fantasy.ToolCall, workDir string) (fantasy.ToolResponse, error) {
var args lsArgs
_ = parseArgs(call.Input, &args) // optional args
limit := 500
if args.Limit > 0 {
limit = args.Limit
}
dirPath := "."
if args.Path != "" {
resolved, err := resolvePathWithWorkDir(args.Path, workDir)
if err != nil {
return fantasy.NewTextErrorResponse(fmt.Sprintf("invalid path: %v", err)), nil
}
dirPath = resolved
} else if workDir != "" {
dirPath = workDir
}
info, err := os.Stat(dirPath)
if err != nil {
return fantasy.NewTextErrorResponse(fmt.Sprintf("cannot access '%s': %v", args.Path, err)), nil
}
if !info.IsDir() {
return fantasy.NewTextErrorResponse(fmt.Sprintf("'%s' is not a directory", args.Path)), nil
}
entries, err := os.ReadDir(dirPath)
if err != nil {
return fantasy.NewTextErrorResponse(fmt.Sprintf("failed to read directory: %v", err)), nil
}
// Sort alphabetically (case-insensitive)
sort.Slice(entries, func(i, j int) bool {
return strings.ToLower(entries[i].Name()) < strings.ToLower(entries[j].Name())
})
var result strings.Builder
count := 0
for _, entry := range entries {
if count >= limit {
fmt.Fprintf(&result, "\n[truncated: showing %d of %d entries]", limit, len(entries))
break
}
name := entry.Name()
if entry.IsDir() {
name += "/"
}
result.WriteString(name + "\n")
count++
}
output := result.String()
if output == "" {
return fantasy.NewTextResponse("(empty directory)"), nil
}
return fantasy.NewTextResponse(strings.TrimRight(output, "\n")), nil
}