diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 1357e729d..d1f8dc2e3 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -990,32 +990,55 @@ func buildArgsSnippet(toolName string, args map[string]interface{}, workspace st // entries under this avoids line-wrapping that causes height jitter. const maxEntryLineWidth = 36 +// isFileToolEntry returns true if the entry name contains a file-operation tool. +func isFileToolEntry(name string) bool { + for _, t := range []string{"read_file", "write_file", "edit_file", "append_file", "list_dir"} { + if strings.Contains(name, t) { + return true + } + } + return false +} + // formatCompactEntry formats a finished tool log entry as a fixed single line. // The result marker (✓/✗) is always shown at the end regardless of truncation. +// File tools omit duration (always near-instant); paths truncate from the +// start so the filename is always visible. func formatCompactEntry(entry toolLogEntry) string { - // result is e.g. "✓ 1.2s" or "✗ 3.0s" — always 6-8 chars result := entry.Result if result == "" { result = "\u23F3" // ⏳ } - prefix := entry.Name - if entry.ArgsSnip != "" { - prefix += " " + entry.ArgsSnip + // File tools: strip duration, keep only marker (✓/✗/⏳) + isFile := isFileToolEntry(entry.Name) + if isFile { + if r := []rune(result); len(r) > 0 { + result = string(r[0:1]) // just the symbol + } } - // Budget: total ≤ maxEntryLineWidth, need space for " " + result - budget := maxEntryLineWidth - 1 - utf8.RuneCountInString(result) - if budget < 4 { - budget = 4 + // Budget for ArgsSnip: total - name - " " - " " - result + nameLen := utf8.RuneCountInString(entry.Name) + resultLen := utf8.RuneCountInString(result) + argsBudget := maxEntryLineWidth - nameLen - 1 - 1 - resultLen + + args := entry.ArgsSnip + if args != "" && argsBudget > 3 { + argsRunes := []rune(args) + if len(argsRunes) > argsBudget { + // Paths: truncate from the start, keeping the filename visible + if strings.Contains(args, "/") { + args = "\u2026" + string(argsRunes[len(argsRunes)-argsBudget+1:]) + } else { + args = string(argsRunes[:argsBudget-1]) + "\u2026" + } + } + return entry.Name + " " + args + " " + result } - prefixRunes := []rune(prefix) - if len(prefixRunes) > budget { - prefix = string(prefixRunes[:budget-1]) + "\u2026" // … - } - - return prefix + " " + result + // No room for args or args empty + return entry.Name + " " + result } // Display layout constants. diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index d8fe41a72..2263e6bd1 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -1484,23 +1484,32 @@ func TestFormatCompactEntry(t *testing.T) { entry toolLogEntry wantSub string // must be a substring wantMark string // result marker must appear + noTime bool // if true, duration should NOT appear }{ { - name: "short entry", - entry: toolLogEntry{Name: "exec", ArgsSnip: "ls", Result: "✓ 1.0s"}, + name: "exec short entry", + entry: toolLogEntry{Name: "[1] exec", ArgsSnip: "ls", Result: "✓ 1.0s"}, wantSub: "exec ls", - wantMark: "✓", + wantMark: "✓ 1.0s", // exec keeps duration }, { - name: "long entry is truncated with marker preserved", - entry: toolLogEntry{Name: "exec", ArgsSnip: "pytest tests/integration/test_very_long_name_that_exceeds_width.py", Result: "✗ 3.0s"}, + name: "exec long entry truncated from end", + entry: toolLogEntry{Name: "[2] exec", ArgsSnip: "pytest tests/integration/test_very_long_name.py", Result: "✗ 3.0s"}, wantMark: "✗", }, { - name: "no args", - entry: toolLogEntry{Name: "list_dir", Result: "✓ 0.1s"}, - wantSub: "list_dir", + name: "file tool omits duration, shows filename", + entry: toolLogEntry{Name: "[3] edit_file", ArgsSnip: "projects/terra/src/deep/nested/backend.py", Result: "✓ 0.0s"}, + wantSub: "backend.py", wantMark: "✓", + noTime: true, + }, + { + name: "file tool path truncates from start", + entry: toolLogEntry{Name: "[4] read_file", ArgsSnip: "projects/terra-py-form/src/terra_py_form/hot/state/backend.py", Result: "✓ 0.1s"}, + wantSub: "backend.py", + wantMark: "✓", + noTime: true, }, } for _, tt := range tests { @@ -1512,6 +1521,9 @@ func TestFormatCompactEntry(t *testing.T) { if !strings.Contains(got, tt.wantMark) { t.Errorf("result marker %q missing from: %q", tt.wantMark, got) } + if tt.noTime && strings.Contains(got, "0s") { + t.Errorf("file tool should omit duration, got: %q", got) + } // Must not exceed maxEntryLineWidth if runeLen := len([]rune(got)); runeLen > maxEntryLineWidth { t.Errorf("entry too wide: %d runes (max %d): %q", runeLen, maxEntryLineWidth, got)