From d92a3ebf5caf5da33e3b46d52497248f1795ddd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:37:53 +0000 Subject: [PATCH 1/7] Initial plan From da7bcf49f730be1f41903602a55c9241d60225fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:47:45 +0000 Subject: [PATCH 2/7] Add read_dir tool for reading directories Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- pkg/aiusechat/tools.go | 1 + pkg/aiusechat/tools_readdir.go | 189 +++++++++++++++++++++++++ pkg/aiusechat/tools_readdir_test.go | 211 ++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+) create mode 100644 pkg/aiusechat/tools_readdir.go create mode 100644 pkg/aiusechat/tools_readdir_test.go diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 02105a9155..8b9ba3f82b 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -127,6 +127,7 @@ func GenerateTabStateAndTools(ctx context.Context, tabid string, widgetAccess bo if widgetAccess { tools = append(tools, GetCaptureScreenshotToolDefinition(tabid)) tools = append(tools, GetReadTextFileToolDefinition()) + tools = append(tools, GetReadDirToolDefinition()) viewTypes := make(map[string]bool) for _, block := range blocks { if block.Meta == nil { diff --git a/pkg/aiusechat/tools_readdir.go b/pkg/aiusechat/tools_readdir.go new file mode 100644 index 0000000000..ad43517e19 --- /dev/null +++ b/pkg/aiusechat/tools_readdir.go @@ -0,0 +1,189 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package aiusechat + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" + "github.com/wavetermdev/waveterm/pkg/util/utilfn" + "github.com/wavetermdev/waveterm/pkg/wavebase" +) + +const ReadDirDefaultMaxEntries = 1000 + +type readDirParams struct { + Path string `json:"path"` + MaxEntries *int `json:"max_entries"` +} + +func parseReadDirInput(input any) (*readDirParams, error) { + result := &readDirParams{} + + if input == nil { + return nil, fmt.Errorf("input is required") + } + + if err := utilfn.ReUnmarshal(result, input); err != nil { + return nil, fmt.Errorf("invalid input format: %w", err) + } + + if result.Path == "" { + return nil, fmt.Errorf("missing path parameter") + } + + if result.MaxEntries == nil { + maxEntries := ReadDirDefaultMaxEntries + result.MaxEntries = &maxEntries + } + + if *result.MaxEntries < 1 { + return nil, fmt.Errorf("max_entries must be at least 1, got %d", *result.MaxEntries) + } + + return result, nil +} + +func readDirCallback(input any) (any, error) { + params, err := parseReadDirInput(input) + if err != nil { + return nil, err + } + + expandedPath, err := wavebase.ExpandHomeDir(params.Path) + if err != nil { + return nil, fmt.Errorf("failed to expand path: %w", err) + } + + fileInfo, err := os.Stat(expandedPath) + if err != nil { + return nil, fmt.Errorf("failed to stat path: %w", err) + } + + if !fileInfo.IsDir() { + return nil, fmt.Errorf("path is not a directory, cannot be read with the read_dir tool. use the read_text_file tool to read files") + } + + entries, err := os.ReadDir(expandedPath) + if err != nil { + return nil, fmt.Errorf("failed to read directory: %w", err) + } + + maxEntries := *params.MaxEntries + var truncated bool + if len(entries) > maxEntries { + entries = entries[:maxEntries] + truncated = true + } + + // Sort entries: directories first, then files, alphabetically within each group + sort.Slice(entries, func(i, j int) bool { + if entries[i].IsDir() != entries[j].IsDir() { + return entries[i].IsDir() + } + return entries[i].Name() < entries[j].Name() + }) + + var entryList []map[string]any + for _, entry := range entries { + info, err := entry.Info() + if err != nil { + // Skip entries we can't stat + continue + } + + entryData := map[string]any{ + "name": entry.Name(), + "is_dir": entry.IsDir(), + "mode": info.Mode().String(), + "modified": utilfn.FormatRelativeTime(info.ModTime()), + } + + if !entry.IsDir() { + entryData["size"] = info.Size() + } + + entryList = append(entryList, entryData) + } + + // Create a formatted directory listing + var listing strings.Builder + for _, entry := range entryList { + name := entry["name"].(string) + isDir := entry["is_dir"].(bool) + mode := entry["mode"].(string) + modified := entry["modified"].(string) + + if isDir { + listing.WriteString(fmt.Sprintf("[DIR] %-40s %s %s\n", name, mode, modified)) + } else { + size := entry["size"].(int64) + listing.WriteString(fmt.Sprintf("[FILE] %-40s %10d %s %s\n", name, size, mode, modified)) + } + } + + result := map[string]any{ + "path": params.Path, + "absolute_path": expandedPath, + "entry_count": len(entryList), + "total_entries": len(entries), + "entries": entryList, + "listing": strings.TrimSuffix(listing.String(), "\n"), + } + + if truncated { + result["truncated"] = true + result["truncated_message"] = fmt.Sprintf("Directory listing truncated to %d entries (out of %d+ total). Increase max_entries to see more.", maxEntries, maxEntries) + } + + // Get absolute path of parent directory for context + parentDir := filepath.Dir(expandedPath) + if parentDir != expandedPath { + result["parent_dir"] = parentDir + } + + return result, nil +} + +func GetReadDirToolDefinition() uctypes.ToolDefinition { + return uctypes.ToolDefinition{ + Name: "read_dir", + DisplayName: "Read Directory", + Description: "Read a directory from the filesystem and list its contents. Returns information about files and subdirectories including names, types, sizes, permissions, and modification times. Requires user approval.", + ToolLogName: "gen:readdir", + Strict: false, + InputSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "path": map[string]any{ + "type": "string", + "description": "Path to the directory to read", + }, + "max_entries": map[string]any{ + "type": "integer", + "minimum": 1, + "default": 1000, + "description": "Maximum number of entries to return. Defaults to 1000.", + }, + }, + "required": []string{"path"}, + "additionalProperties": false, + }, + ToolInputDesc: func(input any) string { + parsed, err := parseReadDirInput(input) + if err != nil { + return fmt.Sprintf("error parsing input: %v", err) + } + return fmt.Sprintf("reading directory %q", parsed.Path) + }, + ToolAnyCallback: readDirCallback, + ToolApproval: func(input any) string { + return uctypes.ApprovalNeedsApproval + }, + } +} diff --git a/pkg/aiusechat/tools_readdir_test.go b/pkg/aiusechat/tools_readdir_test.go new file mode 100644 index 0000000000..e06575f494 --- /dev/null +++ b/pkg/aiusechat/tools_readdir_test.go @@ -0,0 +1,211 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package aiusechat + +import ( + "os" + "path/filepath" + "testing" +) + +func TestReadDirCallback(t *testing.T) { + // Create a temporary test directory + tmpDir, err := os.MkdirTemp("", "readdir_test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test files and directories + testFile1 := filepath.Join(tmpDir, "file1.txt") + testFile2 := filepath.Join(tmpDir, "file2.log") + testSubDir := filepath.Join(tmpDir, "subdir") + + if err := os.WriteFile(testFile1, []byte("test content 1"), 0644); err != nil { + t.Fatalf("Failed to create test file 1: %v", err) + } + if err := os.WriteFile(testFile2, []byte("test content 2"), 0644); err != nil { + t.Fatalf("Failed to create test file 2: %v", err) + } + if err := os.Mkdir(testSubDir, 0755); err != nil { + t.Fatalf("Failed to create test subdir: %v", err) + } + + // Test reading the directory + input := map[string]any{ + "path": tmpDir, + } + + result, err := readDirCallback(input) + if err != nil { + t.Fatalf("readDirCallback failed: %v", err) + } + + resultMap, ok := result.(map[string]any) + if !ok { + t.Fatalf("Result is not a map") + } + + // Verify the result contains expected fields + if resultMap["path"] != tmpDir { + t.Errorf("Expected path %q, got %q", tmpDir, resultMap["path"]) + } + + entryCount, ok := resultMap["entry_count"].(int) + if !ok { + t.Fatalf("entry_count is not an int") + } + if entryCount != 3 { + t.Errorf("Expected 3 entries, got %d", entryCount) + } + + entries, ok := resultMap["entries"].([]map[string]any) + if !ok { + t.Fatalf("entries is not a slice of maps") + } + + // Check that we have the expected entries + foundFiles := 0 + foundDirs := 0 + for _, entry := range entries { + if entry["is_dir"].(bool) { + foundDirs++ + } else { + foundFiles++ + } + } + + if foundFiles != 2 { + t.Errorf("Expected 2 files, got %d", foundFiles) + } + if foundDirs != 1 { + t.Errorf("Expected 1 directory, got %d", foundDirs) + } +} + +func TestReadDirOnFile(t *testing.T) { + // Create a temporary test file + tmpFile, err := os.CreateTemp("", "readdir_test_file") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + tmpFile.Close() + + // Test reading a file (should fail) + input := map[string]any{ + "path": tmpFile.Name(), + } + + _, err = readDirCallback(input) + if err == nil { + t.Fatalf("Expected error when reading a file with read_dir, got nil") + } + + expectedErrSubstr := "path is not a directory" + if err.Error()[:len(expectedErrSubstr)] != expectedErrSubstr { + t.Errorf("Expected error containing %q, got %q", expectedErrSubstr, err.Error()) + } +} + +func TestReadDirMaxEntries(t *testing.T) { + // Create a temporary test directory with many files + tmpDir, err := os.MkdirTemp("", "readdir_test_max") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create 10 test files + for i := 0; i < 10; i++ { + testFile := filepath.Join(tmpDir, filepath.Base(tmpDir)+string(rune('a'+i))+".txt") + if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + } + + // Test reading with max_entries=5 + maxEntries := 5 + input := map[string]any{ + "path": tmpDir, + "max_entries": maxEntries, + } + + result, err := readDirCallback(input) + if err != nil { + t.Fatalf("readDirCallback failed: %v", err) + } + + resultMap := result.(map[string]any) + entryCount := resultMap["entry_count"].(int) + + if entryCount != maxEntries { + t.Errorf("Expected %d entries, got %d", maxEntries, entryCount) + } + + if _, ok := resultMap["truncated"]; !ok { + t.Error("Expected truncated field to be present") + } +} + +func TestParseReadDirInput(t *testing.T) { + // Test valid input + input := map[string]any{ + "path": "/tmp/test", + } + + params, err := parseReadDirInput(input) + if err != nil { + t.Fatalf("parseReadDirInput failed on valid input: %v", err) + } + + if params.Path != "/tmp/test" { + t.Errorf("Expected path '/tmp/test', got %q", params.Path) + } + + if *params.MaxEntries != ReadDirDefaultMaxEntries { + t.Errorf("Expected default max_entries %d, got %d", ReadDirDefaultMaxEntries, *params.MaxEntries) + } + + // Test missing path + input = map[string]any{} + _, err = parseReadDirInput(input) + if err == nil { + t.Error("Expected error for missing path, got nil") + } + + // Test invalid max_entries + input = map[string]any{ + "path": "/tmp/test", + "max_entries": 0, + } + _, err = parseReadDirInput(input) + if err == nil { + t.Error("Expected error for max_entries < 1, got nil") + } +} + +func TestGetReadDirToolDefinition(t *testing.T) { + toolDef := GetReadDirToolDefinition() + + if toolDef.Name != "read_dir" { + t.Errorf("Expected tool name 'read_dir', got %q", toolDef.Name) + } + + if toolDef.ToolLogName != "gen:readdir" { + t.Errorf("Expected tool log name 'gen:readdir', got %q", toolDef.ToolLogName) + } + + if toolDef.ToolAnyCallback == nil { + t.Error("ToolAnyCallback should not be nil") + } + + if toolDef.ToolApproval == nil { + t.Error("ToolApproval should not be nil") + } + + if toolDef.ToolInputDesc == nil { + t.Error("ToolInputDesc should not be nil") + } +} From 7d1b5e35479afaff4ada139ab5fd2462de24b113 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:51:03 +0000 Subject: [PATCH 3/7] Implementation complete - all tests passing Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- tsunami/demo/cpuchart/go.mod | 7 +------ tsunami/demo/cpuchart/go.sum | 12 ------------ tsunami/demo/githubaction/go.mod | 7 ------- tsunami/demo/githubaction/go.sum | 4 ---- tsunami/demo/pomodoro/go.mod | 7 ------- tsunami/demo/pomodoro/go.sum | 4 ---- tsunami/demo/recharts/go.mod | 7 ------- tsunami/demo/recharts/go.sum | 4 ---- tsunami/demo/tabletest/go.mod | 7 ------- tsunami/demo/tabletest/go.sum | 4 ---- tsunami/demo/todo/go.mod | 7 ------- tsunami/demo/todo/go.sum | 4 ---- tsunami/demo/tsunamiconfig/go.mod | 7 ------- tsunami/demo/tsunamiconfig/go.sum | 4 ---- 14 files changed, 1 insertion(+), 84 deletions(-) diff --git a/tsunami/demo/cpuchart/go.mod b/tsunami/demo/cpuchart/go.mod index 0140d12180..864522e4e6 100644 --- a/tsunami/demo/cpuchart/go.mod +++ b/tsunami/demo/cpuchart/go.mod @@ -2,17 +2,12 @@ module tsunami/app/cpuchart go 1.24.6 -require ( - github.com/shirou/gopsutil/v4 v4.25.8 - github.com/wavetermdev/waveterm/tsunami v0.0.0 -) +require github.com/shirou/gopsutil/v4 v4.25.8 require ( github.com/ebitengine/purego v0.8.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/outrigdev/goid v0.3.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect diff --git a/tsunami/demo/cpuchart/go.sum b/tsunami/demo/cpuchart/go.sum index 4d3c872cfc..965d0ed1e9 100644 --- a/tsunami/demo/cpuchart/go.sum +++ b/tsunami/demo/cpuchart/go.sum @@ -1,26 +1,16 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= -github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970= github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= @@ -32,5 +22,3 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tsunami/demo/githubaction/go.mod b/tsunami/demo/githubaction/go.mod index cf51505f61..7ba875a5ab 100644 --- a/tsunami/demo/githubaction/go.mod +++ b/tsunami/demo/githubaction/go.mod @@ -2,11 +2,4 @@ module tsunami/app/githubaction go 1.24.6 -require github.com/wavetermdev/waveterm/tsunami v0.0.0 - -require ( - github.com/google/uuid v1.6.0 // indirect - github.com/outrigdev/goid v0.3.0 // indirect -) - replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/githubaction/go.sum b/tsunami/demo/githubaction/go.sum index 4c44991dfc..e69de29bb2 100644 --- a/tsunami/demo/githubaction/go.sum +++ b/tsunami/demo/githubaction/go.sum @@ -1,4 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= -github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/pomodoro/go.mod b/tsunami/demo/pomodoro/go.mod index 9e5e8362d4..bf8b7c0096 100644 --- a/tsunami/demo/pomodoro/go.mod +++ b/tsunami/demo/pomodoro/go.mod @@ -2,11 +2,4 @@ module tsunami/app/pomodoro go 1.24.6 -require github.com/wavetermdev/waveterm/tsunami v0.0.0 - -require ( - github.com/google/uuid v1.6.0 // indirect - github.com/outrigdev/goid v0.3.0 // indirect -) - replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/pomodoro/go.sum b/tsunami/demo/pomodoro/go.sum index 4c44991dfc..e69de29bb2 100644 --- a/tsunami/demo/pomodoro/go.sum +++ b/tsunami/demo/pomodoro/go.sum @@ -1,4 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= -github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/recharts/go.mod b/tsunami/demo/recharts/go.mod index dfe7fef813..9d2536376d 100644 --- a/tsunami/demo/recharts/go.mod +++ b/tsunami/demo/recharts/go.mod @@ -2,11 +2,4 @@ module tsunami/app/recharts go 1.24.6 -require github.com/wavetermdev/waveterm/tsunami v0.0.0 - -require ( - github.com/google/uuid v1.6.0 // indirect - github.com/outrigdev/goid v0.3.0 // indirect -) - replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/recharts/go.sum b/tsunami/demo/recharts/go.sum index 4c44991dfc..e69de29bb2 100644 --- a/tsunami/demo/recharts/go.sum +++ b/tsunami/demo/recharts/go.sum @@ -1,4 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= -github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/tabletest/go.mod b/tsunami/demo/tabletest/go.mod index 86b7209a5a..6f2d6aad21 100644 --- a/tsunami/demo/tabletest/go.mod +++ b/tsunami/demo/tabletest/go.mod @@ -2,11 +2,4 @@ module tsunami/app/tabletest go 1.24.6 -require github.com/wavetermdev/waveterm/tsunami v0.0.0 - -require ( - github.com/google/uuid v1.6.0 // indirect - github.com/outrigdev/goid v0.3.0 // indirect -) - replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/tabletest/go.sum b/tsunami/demo/tabletest/go.sum index 4c44991dfc..e69de29bb2 100644 --- a/tsunami/demo/tabletest/go.sum +++ b/tsunami/demo/tabletest/go.sum @@ -1,4 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= -github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/todo/go.mod b/tsunami/demo/todo/go.mod index f82b52b1cc..f5b6f93578 100644 --- a/tsunami/demo/todo/go.mod +++ b/tsunami/demo/todo/go.mod @@ -2,11 +2,4 @@ module tsunami/app/todo go 1.24.6 -require github.com/wavetermdev/waveterm/tsunami v0.0.0 - -require ( - github.com/google/uuid v1.6.0 // indirect - github.com/outrigdev/goid v0.3.0 // indirect -) - replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/todo/go.sum b/tsunami/demo/todo/go.sum index 4c44991dfc..e69de29bb2 100644 --- a/tsunami/demo/todo/go.sum +++ b/tsunami/demo/todo/go.sum @@ -1,4 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= -github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/tsunamiconfig/go.mod b/tsunami/demo/tsunamiconfig/go.mod index 165622f27a..6c3f08b497 100644 --- a/tsunami/demo/tsunamiconfig/go.mod +++ b/tsunami/demo/tsunamiconfig/go.mod @@ -2,11 +2,4 @@ module tsunami/app/tsunamiconfig go 1.24.6 -require github.com/wavetermdev/waveterm/tsunami v0.0.0 - -require ( - github.com/google/uuid v1.6.0 // indirect - github.com/outrigdev/goid v0.3.0 // indirect -) - replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/tsunamiconfig/go.sum b/tsunami/demo/tsunamiconfig/go.sum index 4c44991dfc..e69de29bb2 100644 --- a/tsunami/demo/tsunamiconfig/go.sum +++ b/tsunami/demo/tsunamiconfig/go.sum @@ -1,4 +0,0 @@ -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= -github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= From b723292efc869665f8a6d10530e0b3e714fd0d98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:57:59 +0000 Subject: [PATCH 4/7] Revert tsunami demo go.mod and go.sum changes Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- tsunami/demo/cpuchart/go.mod | 7 ++++++- tsunami/demo/cpuchart/go.sum | 12 ++++++++++++ tsunami/demo/githubaction/go.mod | 7 +++++++ tsunami/demo/githubaction/go.sum | 4 ++++ tsunami/demo/pomodoro/go.mod | 7 +++++++ tsunami/demo/pomodoro/go.sum | 4 ++++ tsunami/demo/recharts/go.mod | 7 +++++++ tsunami/demo/recharts/go.sum | 4 ++++ tsunami/demo/tabletest/go.mod | 7 +++++++ tsunami/demo/tabletest/go.sum | 4 ++++ tsunami/demo/todo/go.mod | 7 +++++++ tsunami/demo/todo/go.sum | 4 ++++ tsunami/demo/tsunamiconfig/go.mod | 7 +++++++ tsunami/demo/tsunamiconfig/go.sum | 4 ++++ 14 files changed, 84 insertions(+), 1 deletion(-) diff --git a/tsunami/demo/cpuchart/go.mod b/tsunami/demo/cpuchart/go.mod index 864522e4e6..0140d12180 100644 --- a/tsunami/demo/cpuchart/go.mod +++ b/tsunami/demo/cpuchart/go.mod @@ -2,12 +2,17 @@ module tsunami/app/cpuchart go 1.24.6 -require github.com/shirou/gopsutil/v4 v4.25.8 +require ( + github.com/shirou/gopsutil/v4 v4.25.8 + github.com/wavetermdev/waveterm/tsunami v0.0.0 +) require ( github.com/ebitengine/purego v0.8.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/outrigdev/goid v0.3.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect diff --git a/tsunami/demo/cpuchart/go.sum b/tsunami/demo/cpuchart/go.sum index 965d0ed1e9..4d3c872cfc 100644 --- a/tsunami/demo/cpuchart/go.sum +++ b/tsunami/demo/cpuchart/go.sum @@ -1,16 +1,26 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970= github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= @@ -22,3 +32,5 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tsunami/demo/githubaction/go.mod b/tsunami/demo/githubaction/go.mod index 7ba875a5ab..cf51505f61 100644 --- a/tsunami/demo/githubaction/go.mod +++ b/tsunami/demo/githubaction/go.mod @@ -2,4 +2,11 @@ module tsunami/app/githubaction go 1.24.6 +require github.com/wavetermdev/waveterm/tsunami v0.0.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/outrigdev/goid v0.3.0 // indirect +) + replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/githubaction/go.sum b/tsunami/demo/githubaction/go.sum index e69de29bb2..4c44991dfc 100644 --- a/tsunami/demo/githubaction/go.sum +++ b/tsunami/demo/githubaction/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/pomodoro/go.mod b/tsunami/demo/pomodoro/go.mod index bf8b7c0096..9e5e8362d4 100644 --- a/tsunami/demo/pomodoro/go.mod +++ b/tsunami/demo/pomodoro/go.mod @@ -2,4 +2,11 @@ module tsunami/app/pomodoro go 1.24.6 +require github.com/wavetermdev/waveterm/tsunami v0.0.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/outrigdev/goid v0.3.0 // indirect +) + replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/pomodoro/go.sum b/tsunami/demo/pomodoro/go.sum index e69de29bb2..4c44991dfc 100644 --- a/tsunami/demo/pomodoro/go.sum +++ b/tsunami/demo/pomodoro/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/recharts/go.mod b/tsunami/demo/recharts/go.mod index 9d2536376d..dfe7fef813 100644 --- a/tsunami/demo/recharts/go.mod +++ b/tsunami/demo/recharts/go.mod @@ -2,4 +2,11 @@ module tsunami/app/recharts go 1.24.6 +require github.com/wavetermdev/waveterm/tsunami v0.0.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/outrigdev/goid v0.3.0 // indirect +) + replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/recharts/go.sum b/tsunami/demo/recharts/go.sum index e69de29bb2..4c44991dfc 100644 --- a/tsunami/demo/recharts/go.sum +++ b/tsunami/demo/recharts/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/tabletest/go.mod b/tsunami/demo/tabletest/go.mod index 6f2d6aad21..86b7209a5a 100644 --- a/tsunami/demo/tabletest/go.mod +++ b/tsunami/demo/tabletest/go.mod @@ -2,4 +2,11 @@ module tsunami/app/tabletest go 1.24.6 +require github.com/wavetermdev/waveterm/tsunami v0.0.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/outrigdev/goid v0.3.0 // indirect +) + replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/tabletest/go.sum b/tsunami/demo/tabletest/go.sum index e69de29bb2..4c44991dfc 100644 --- a/tsunami/demo/tabletest/go.sum +++ b/tsunami/demo/tabletest/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/todo/go.mod b/tsunami/demo/todo/go.mod index f5b6f93578..f82b52b1cc 100644 --- a/tsunami/demo/todo/go.mod +++ b/tsunami/demo/todo/go.mod @@ -2,4 +2,11 @@ module tsunami/app/todo go 1.24.6 +require github.com/wavetermdev/waveterm/tsunami v0.0.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/outrigdev/goid v0.3.0 // indirect +) + replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/todo/go.sum b/tsunami/demo/todo/go.sum index e69de29bb2..4c44991dfc 100644 --- a/tsunami/demo/todo/go.sum +++ b/tsunami/demo/todo/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= diff --git a/tsunami/demo/tsunamiconfig/go.mod b/tsunami/demo/tsunamiconfig/go.mod index 6c3f08b497..165622f27a 100644 --- a/tsunami/demo/tsunamiconfig/go.mod +++ b/tsunami/demo/tsunamiconfig/go.mod @@ -2,4 +2,11 @@ module tsunami/app/tsunamiconfig go 1.24.6 +require github.com/wavetermdev/waveterm/tsunami v0.0.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/outrigdev/goid v0.3.0 // indirect +) + replace github.com/wavetermdev/waveterm/tsunami => /Users/mike/work/waveterm/tsunami diff --git a/tsunami/demo/tsunamiconfig/go.sum b/tsunami/demo/tsunamiconfig/go.sum index e69de29bb2..4c44991dfc 100644 --- a/tsunami/demo/tsunamiconfig/go.sum +++ b/tsunami/demo/tsunamiconfig/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/outrigdev/goid v0.3.0 h1:t/otQD3EXc45cLtQVPUnNgEyRaTQA4cPeu3qVcrsIws= +github.com/outrigdev/goid v0.3.0/go.mod h1:hEH7f27ypN/GHWt/7gvkRoFYR0LZizfUBIAbak4neVE= From 8562ef2a9a30d02b77ffece6bc0456fbaf167e1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 00:26:16 +0000 Subject: [PATCH 5/7] Sort entries before truncation and preserve real total count Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- pkg/aiusechat/tools_readdir.go | 20 +++++--- pkg/aiusechat/tools_readdir_test.go | 80 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/pkg/aiusechat/tools_readdir.go b/pkg/aiusechat/tools_readdir.go index ad43517e19..9579c78ad0 100644 --- a/pkg/aiusechat/tools_readdir.go +++ b/pkg/aiusechat/tools_readdir.go @@ -74,12 +74,8 @@ func readDirCallback(input any) (any, error) { return nil, fmt.Errorf("failed to read directory: %w", err) } - maxEntries := *params.MaxEntries - var truncated bool - if len(entries) > maxEntries { - entries = entries[:maxEntries] - truncated = true - } + // Keep track of the original total before truncation + totalEntries := len(entries) // Sort entries: directories first, then files, alphabetically within each group sort.Slice(entries, func(i, j int) bool { @@ -89,6 +85,14 @@ func readDirCallback(input any) (any, error) { return entries[i].Name() < entries[j].Name() }) + // Truncate after sorting to ensure directories come first + maxEntries := *params.MaxEntries + var truncated bool + if len(entries) > maxEntries { + entries = entries[:maxEntries] + truncated = true + } + var entryList []map[string]any for _, entry := range entries { info, err := entry.Info() @@ -131,14 +135,14 @@ func readDirCallback(input any) (any, error) { "path": params.Path, "absolute_path": expandedPath, "entry_count": len(entryList), - "total_entries": len(entries), + "total_entries": totalEntries, "entries": entryList, "listing": strings.TrimSuffix(listing.String(), "\n"), } if truncated { result["truncated"] = true - result["truncated_message"] = fmt.Sprintf("Directory listing truncated to %d entries (out of %d+ total). Increase max_entries to see more.", maxEntries, maxEntries) + result["truncated_message"] = fmt.Sprintf("Directory listing truncated to %d entries (out of %d total). Increase max_entries to see more.", len(entryList), totalEntries) } // Get absolute path of parent directory for context diff --git a/pkg/aiusechat/tools_readdir_test.go b/pkg/aiusechat/tools_readdir_test.go index e06575f494..305c0bfcbd 100644 --- a/pkg/aiusechat/tools_readdir_test.go +++ b/pkg/aiusechat/tools_readdir_test.go @@ -4,8 +4,10 @@ package aiusechat import ( + "fmt" "os" "path/filepath" + "strings" "testing" ) @@ -139,14 +141,92 @@ func TestReadDirMaxEntries(t *testing.T) { resultMap := result.(map[string]any) entryCount := resultMap["entry_count"].(int) + totalEntries := resultMap["total_entries"].(int) if entryCount != maxEntries { t.Errorf("Expected %d entries, got %d", maxEntries, entryCount) } + // Verify total_entries reports the original count, not the truncated count + if totalEntries != 10 { + t.Errorf("Expected total_entries to be 10, got %d", totalEntries) + } + if _, ok := resultMap["truncated"]; !ok { t.Error("Expected truncated field to be present") } + + // Verify the truncation message includes the correct total + truncMsg, ok := resultMap["truncated_message"].(string) + if !ok { + t.Error("Expected truncated_message to be present") + } + expectedMsg := fmt.Sprintf("Directory listing truncated to %d entries (out of %d total)", maxEntries, 10) + if !strings.Contains(truncMsg, expectedMsg[:len(expectedMsg)-1]) { + t.Errorf("Expected truncated_message to contain %q, got %q", expectedMsg, truncMsg) + } +} + +func TestReadDirSortBeforeTruncate(t *testing.T) { + // Create a temporary test directory + tmpDir, err := os.MkdirTemp("", "readdir_test_sort") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create files with names that would sort alphabetically before directories + // but we want directories to appear first + for i := 0; i < 5; i++ { + testFile := filepath.Join(tmpDir, fmt.Sprintf("a_file_%d.txt", i)) + if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + } + + // Create directories with names that sort alphabetically after the files + for i := 0; i < 3; i++ { + testDir := filepath.Join(tmpDir, fmt.Sprintf("z_dir_%d", i)) + if err := os.Mkdir(testDir, 0755); err != nil { + t.Fatalf("Failed to create test dir: %v", err) + } + } + + // Test with max_entries=5 (less than total of 8) + // All 3 directories should still appear because they're sorted first + maxEntries := 5 + input := map[string]any{ + "path": tmpDir, + "max_entries": maxEntries, + } + + result, err := readDirCallback(input) + if err != nil { + t.Fatalf("readDirCallback failed: %v", err) + } + + resultMap := result.(map[string]any) + entries := resultMap["entries"].([]map[string]any) + + // Count directories in the result + dirCount := 0 + for _, entry := range entries { + if entry["is_dir"].(bool) { + dirCount++ + } + } + + // All 3 directories should be present because sorting happens before truncation + if dirCount != 3 { + t.Errorf("Expected 3 directories in truncated results, got %d", dirCount) + } + + // First 3 entries should be directories + for i := 0; i < 3; i++ { + if !entries[i]["is_dir"].(bool) { + t.Errorf("Expected entry %d to be a directory, but it was a file", i) + } + } } func TestParseReadDirInput(t *testing.T) { From f72e327393ae784c930e917e29e5188a9e7abb19 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Oct 2025 20:30:15 -0700 Subject: [PATCH 6/7] trigger CLA check From ab351e329f64c3773719e10481884fb393f6a98b Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Oct 2025 21:38:10 -0700 Subject: [PATCH 7/7] updates to readdir for symlinks, drop listings, modified_time, max_entries cap --- pkg/aiusechat/tools_readdir.go | 95 +++++++++++++++++++++------------ pkg/aiusechat/tools_readfile.go | 3 +- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/pkg/aiusechat/tools_readdir.go b/pkg/aiusechat/tools_readdir.go index 9579c78ad0..a0689904e4 100644 --- a/pkg/aiusechat/tools_readdir.go +++ b/pkg/aiusechat/tools_readdir.go @@ -5,23 +5,35 @@ package aiusechat import ( "fmt" + "io/fs" "os" "path/filepath" "sort" - "strings" + "time" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/wavebase" ) -const ReadDirDefaultMaxEntries = 1000 +const ReadDirDefaultMaxEntries = 500 +const ReadDirHardMaxEntries = 10000 type readDirParams struct { Path string `json:"path"` MaxEntries *int `json:"max_entries"` } +type DirEntryOut struct { + Name string `json:"name"` + Dir bool `json:"dir,omitempty"` + Symlink bool `json:"symlink,omitempty"` + Size int64 `json:"size,omitempty"` + Mode string `json:"mode"` + Modified string `json:"modified"` + ModifiedTime string `json:"modified_time"` +} + func parseReadDirInput(input any) (*readDirParams, error) { result := &readDirParams{} @@ -46,6 +58,10 @@ func parseReadDirInput(input any) (*readDirParams, error) { return nil, fmt.Errorf("max_entries must be at least 1, got %d", *result.MaxEntries) } + if *result.MaxEntries > ReadDirHardMaxEntries { + return nil, fmt.Errorf("max_entries cannot exceed %d, got %d", ReadDirHardMaxEntries, *result.MaxEntries) + } + return result, nil } @@ -77,10 +93,34 @@ func readDirCallback(input any) (any, error) { // Keep track of the original total before truncation totalEntries := len(entries) + // Build a map of actual directory status, checking symlink targets + isDirMap := make(map[string]bool) + symlinkCount := 0 + for _, entry := range entries { + name := entry.Name() + if entry.Type()&fs.ModeSymlink != 0 { + if symlinkCount < 1000 { + symlinkCount++ + fullPath := filepath.Join(expandedPath, name) + if info, err := os.Stat(fullPath); err == nil { + isDirMap[name] = info.IsDir() + } else { + isDirMap[name] = entry.IsDir() + } + } else { + isDirMap[name] = entry.IsDir() + } + } else { + isDirMap[name] = entry.IsDir() + } + } + // Sort entries: directories first, then files, alphabetically within each group sort.Slice(entries, func(i, j int) bool { - if entries[i].IsDir() != entries[j].IsDir() { - return entries[i].IsDir() + iIsDir := isDirMap[entries[i].Name()] + jIsDir := isDirMap[entries[j].Name()] + if iIsDir != jIsDir { + return iIsDir } return entries[i].Name() < entries[j].Name() }) @@ -93,51 +133,38 @@ func readDirCallback(input any) (any, error) { truncated = true } - var entryList []map[string]any + var entryList []DirEntryOut for _, entry := range entries { info, err := entry.Info() if err != nil { - // Skip entries we can't stat continue } - entryData := map[string]any{ - "name": entry.Name(), - "is_dir": entry.IsDir(), - "mode": info.Mode().String(), - "modified": utilfn.FormatRelativeTime(info.ModTime()), + isDir := isDirMap[entry.Name()] + isSymlink := entry.Type()&fs.ModeSymlink != 0 + + entryData := DirEntryOut{ + Name: entry.Name(), + Dir: isDir, + Symlink: isSymlink, + Mode: info.Mode().String(), + Modified: utilfn.FormatRelativeTime(info.ModTime()), + ModifiedTime: info.ModTime().UTC().Format(time.RFC3339), } - if !entry.IsDir() { - entryData["size"] = info.Size() + if !isDir { + entryData.Size = info.Size() } entryList = append(entryList, entryData) } - // Create a formatted directory listing - var listing strings.Builder - for _, entry := range entryList { - name := entry["name"].(string) - isDir := entry["is_dir"].(bool) - mode := entry["mode"].(string) - modified := entry["modified"].(string) - - if isDir { - listing.WriteString(fmt.Sprintf("[DIR] %-40s %s %s\n", name, mode, modified)) - } else { - size := entry["size"].(int64) - listing.WriteString(fmt.Sprintf("[FILE] %-40s %10d %s %s\n", name, size, mode, modified)) - } - } - result := map[string]any{ "path": params.Path, "absolute_path": expandedPath, "entry_count": len(entryList), "total_entries": totalEntries, "entries": entryList, - "listing": strings.TrimSuffix(listing.String(), "\n"), } if truncated { @@ -145,7 +172,6 @@ func readDirCallback(input any) (any, error) { result["truncated_message"] = fmt.Sprintf("Directory listing truncated to %d entries (out of %d total). Increase max_entries to see more.", len(entryList), totalEntries) } - // Get absolute path of parent directory for context parentDir := filepath.Dir(expandedPath) if parentDir != expandedPath { result["parent_dir"] = parentDir @@ -171,8 +197,9 @@ func GetReadDirToolDefinition() uctypes.ToolDefinition { "max_entries": map[string]any{ "type": "integer", "minimum": 1, - "default": 1000, - "description": "Maximum number of entries to return. Defaults to 1000.", + "maximum": 10000, + "default": 500, + "description": "Maximum number of entries to return. Defaults to 500, max 10000.", }, }, "required": []string{"path"}, @@ -183,7 +210,7 @@ func GetReadDirToolDefinition() uctypes.ToolDefinition { if err != nil { return fmt.Sprintf("error parsing input: %v", err) } - return fmt.Sprintf("reading directory %q", parsed.Path) + return fmt.Sprintf("reading directory %q (max_entries: %d)", parsed.Path, *parsed.MaxEntries) }, ToolAnyCallback: readDirCallback, ToolApproval: func(input any) string { diff --git a/pkg/aiusechat/tools_readfile.go b/pkg/aiusechat/tools_readfile.go index 9f5e38b047..3aab51f730 100644 --- a/pkg/aiusechat/tools_readfile.go +++ b/pkg/aiusechat/tools_readfile.go @@ -8,6 +8,7 @@ import ( "io" "os" "strings" + "time" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/util/readutil" @@ -179,7 +180,7 @@ func readTextFileCallback(input any) (any, error) { "total_size": totalSize, "data": data, "modified": utilfn.FormatRelativeTime(modTime), - "modified_time": modTime.UTC().Format("2006-01-02 15:04:05 UTC"), + "modified_time": modTime.UTC().Format(time.RFC3339), "mode": fileInfo.Mode().String(), } if stopReason != "" {