diff --git a/claudetool/bash.go b/claudetool/bash.go index f50d4cbdadda0e165c2700adf0fb664ea8b3a66d..78d3653ddf0c8a0c86cd61e8a88de1c62f059fd0 100644 --- a/claudetool/bash.go +++ b/claudetool/bash.go @@ -231,6 +231,7 @@ const ( largeOutputThreshold = 50 * 1024 // 50KB - threshold for saving to file firstLinesCount = 2 lastLinesCount = 5 + maxLineLength = 200 // truncate displayed lines to this length ) func (b *BashTool) makeBashCommand(ctx context.Context, command string, out io.Writer) *exec.Cmd { @@ -342,19 +343,27 @@ func formatForegroundBashOutput(out string) (string, error) { result.WriteString("First lines:\n") firstN := min(firstLinesCount, len(lines)) for i := 0; i < firstN; i++ { - result.WriteString(fmt.Sprintf("%5d: %s\n", i+1, lines[i])) + result.WriteString(fmt.Sprintf("%5d: %s\n", i+1, truncateLine(lines[i]))) } // Last N lines result.WriteString("\n...\n\nLast lines:\n") startIdx := max(0, len(lines)-lastLinesCount) for i := startIdx; i < len(lines); i++ { - result.WriteString(fmt.Sprintf("%5d: %s\n", i+1, lines[i])) + result.WriteString(fmt.Sprintf("%5d: %s\n", i+1, truncateLine(lines[i]))) } return result.String(), nil } +// truncateLine truncates a line to maxLineLength characters, appending "..." if truncated. +func truncateLine(line string) string { + if len(line) <= maxLineLength { + return line + } + return line[:maxLineLength] + "..." +} + func humanizeBytes(bytes int) string { switch { case bytes < 4*1024: diff --git a/claudetool/bash_test.go b/claudetool/bash_test.go index deb5d36c5850879ec9a89d333d6c7c519dbfe490..4346a1b5b990a0201ca95702b165ecd5c3c0b36a 100644 --- a/claudetool/bash_test.go +++ b/claudetool/bash_test.go @@ -645,6 +645,39 @@ func TestFormatForegroundBashOutput(t *testing.T) { t.Logf("Large binary-like output result:\n%s", result) }) + + // Test large output with very long lines (e.g., minified JS) + t.Run("Large Output With Long Lines", func(t *testing.T) { + // Generate output > 50KB with few very long lines + longLine := strings.Repeat("abcdefghij", 1000) // 10KB per line + lines := []string{longLine, longLine, longLine, longLine, longLine, longLine} + largeOutput := strings.Join(lines, "\n") + if len(largeOutput) < largeOutputThreshold { + t.Fatalf("Test setup error: output is only %d bytes, need > %d", len(largeOutput), largeOutputThreshold) + } + + result, err := formatForegroundBashOutput(largeOutput) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Result should be reasonable size (not blow up context) + if len(result) > 4096 { + t.Errorf("Expected truncated result < 4KB, got %d bytes:\n%s", len(result), result) + } + + // Should mention the file + if !strings.Contains(result, "saved to:") { + t.Errorf("Expected result to mention saved file, got:\n%s", result) + } + + // Lines should be truncated + if !strings.Contains(result, "...") { + t.Errorf("Expected truncated lines with '...', got:\n%s", result) + } + + t.Logf("Large output with long lines result:\n%s", result) + }) } // waitForFile waits for a file to exist and be non-empty or times out