Compare commits

...

7 Commits

Author SHA1 Message Date
Jarred Sumner
1e147b3f7e Merge branch 'main' into claude/ai-agent-error-format 2025-10-09 19:52:39 -07:00
Claude Bot
cc65d86af4 Address CodeRabbit feedback and add runtime type info for AI mode
Changes:
1. Always print "... truncated" text in AI mode (even without colors)
   for consistent parser output
2. Fix "- -" prefix to "- " for no-position errors to match diff style
3. Use usize for caret indent calculation (type safety)
4. Add runtime type information display in AI mode when available
   (e.g., "value type: Undefined")

Runtime type info shows the actual JavaScript type of the value that
caused the error, helping AI agents understand type mismatches without
additional context requests.

Addresses CodeRabbit review feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:35:12 +00:00
Claude Bot
686627edc5 Refactor: Use sep_len constant for separator width
Introduces a single constant to track the separator width (2 for AI mode,
3 for normal mode) instead of using magic numbers. This prevents future
off-by-one regressions and makes the code more maintainable.

Changes:
- Added sep_len constant calculated once based on is_ai_agent
- Updated both caret indent calculations to use sep_len
- Consistent comment style explaining the calculation

Addresses CodeRabbit review feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:09:37 +00:00
Claude Bot
0dddeb9611 Fix caret indent calculation for AI agent mode (off-by-one error)
The format string produces "479- " (dash + space), so the caret indent
needs to account for 2 characters, not 1. Previously the caret was
positioned one column to the left of the actual error position.

Fixes CodeRabbit review feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:52:41 +00:00
Claude Bot
86f522fb49 Add explanatory comments to AI agent error formatting code
Clarifies:
- Purpose of is_ai_agent check and format difference
- Separate handling for lines before error vs error line itself
- Indent calculation for caret position with different separators

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:36:46 +00:00
Claude Bot
0a9e6844ee Add tests for AI agent error format with dash separator
Tests verify that error source lines use different separators based on
CLAUDECODE environment variable:
- Normal mode (CLAUDECODE=0): "479 |    return 42;"
- AI agent mode (CLAUDECODE=1): "479-    return 42;"

Added tests for:
- Error formatting in test failures
- Bun.inspect() error output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:25:57 +00:00
Claude Bot
c61c699045 Use dash separator for error source lines when in AI agent mode
When Output.isAIAgent() is true (CLAUDECODE=1 or AGENT=1), format error
source lines as "479-" instead of "479 |" for better AI parsing.

Before (normal mode):
479 |    return 42;
480 | }

After (AI agent mode):
479-    return 42;
480-}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 15:20:46 +00:00
3 changed files with 361 additions and 66 deletions

View File

@@ -2940,6 +2940,16 @@ fn printErrorInstance(
for (line_numbers) |line| max_line = @max(max_line, line);
const max_line_number_pad = std.fmt.count("{d}", .{max_line + 1});
// Use dash separator for AI agents (CLAUDECODE=1 or AGENT=1) to make error output
// more familiar and easier to parse, similar to diff/patch format.
// Normal: "479 | return 42;"
// AI: "479- return 42;"
const is_ai_agent = Output.isAIAgent();
// Width from line number to code start, depending on separator:
// AI: "- " (2 chars), Normal: " | " (3 chars)
const sep_len: u64 = if (is_ai_agent) 2 else 3;
var source_lines = exception.stack.sourceLineIterator();
var last_pad: u64 = 0;
while (source_lines.untilLast()) |source| {
@@ -2954,23 +2964,51 @@ fn printErrorInstance(
const trimmed = std.mem.trimRight(u8, std.mem.trim(u8, source.text.slice(), "\n"), "\t ");
const clamped = trimmed[0..@min(trimmed.len, max_line_length)];
// Print source lines before the error line
if (clamped.len != trimmed.len) {
const fmt = if (comptime allow_ansi_color) "<r><d> | ... truncated <r>\n" else "\n";
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}" ++ fmt,
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
// Line was truncated
if (is_ai_agent) {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d}-<r> {}",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
if (comptime allow_ansi_color) {
try writer.writeAll(Output.prettyFmt("<d> ... truncated<r>\n", true));
} else {
try writer.writeAll(" ... truncated\n");
}
} else {
const fmt = if (comptime allow_ansi_color) "<r><d> | ... truncated <r>\n" else "\n";
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}" ++ fmt,
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
}
} else {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}\n",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
// Full line fits
if (is_ai_agent) {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d}-<r> {}\n",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
} else {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}\n",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
}
}
}
@@ -3012,6 +3050,7 @@ fn printErrorInstance(
}
}
// Print the error line itself (no valid position, so we use "-" as line number)
if (top_frame == null or top_frame.?.position.isInvalid()) {
defer did_print_name = true;
defer source.text.deinit();
@@ -3020,26 +3059,52 @@ fn printErrorInstance(
const text = trimmed[0..@min(trimmed.len, max_line_length)];
if (text.len != trimmed.len) {
const fmt = if (comptime allow_ansi_color) "<r><d> | ... truncated <r>\n" else "\n";
try writer.print(
comptime Output.prettyFmt(
"<r><b>- |<r> {}" ++ fmt,
allow_ansi_color,
),
.{bun.fmt.fmtJavaScript(text, .{ .enable_colors = allow_ansi_color })},
);
if (is_ai_agent) {
try writer.print(
comptime Output.prettyFmt(
"<r><b>-<r> {}",
allow_ansi_color,
),
.{bun.fmt.fmtJavaScript(text, .{ .enable_colors = allow_ansi_color })},
);
if (comptime allow_ansi_color) {
try writer.writeAll(Output.prettyFmt("<d> ... truncated<r>\n", true));
} else {
try writer.writeAll(" ... truncated\n");
}
} else {
const fmt = if (comptime allow_ansi_color) "<r><d> | ... truncated <r>\n" else "\n";
try writer.print(
comptime Output.prettyFmt(
"<r><b>- |<r> {}" ++ fmt,
allow_ansi_color,
),
.{bun.fmt.fmtJavaScript(text, .{ .enable_colors = allow_ansi_color })},
);
}
} else {
try writer.print(
comptime Output.prettyFmt(
"<r><d>- |<r> {}\n",
allow_ansi_color,
),
.{bun.fmt.fmtJavaScript(text, .{ .enable_colors = allow_ansi_color })},
);
if (is_ai_agent) {
try writer.print(
comptime Output.prettyFmt(
"<r><d>-<r> {}\n",
allow_ansi_color,
),
.{bun.fmt.fmtJavaScript(text, .{ .enable_colors = allow_ansi_color })},
);
} else {
try writer.print(
comptime Output.prettyFmt(
"<r><d>- |<r> {}\n",
allow_ansi_color,
),
.{bun.fmt.fmtJavaScript(text, .{ .enable_colors = allow_ansi_color })},
);
}
}
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, Writer, writer, allow_ansi_color, formatter.error_display_level);
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, exception.runtime_type, Writer, writer, allow_ansi_color, formatter.error_display_level);
} else if (top_frame) |top| {
// Print the error line with a caret (^) pointing to the error position
defer did_print_name = true;
const display_line = source.line + 1;
const int_size = std.fmt.count("{d}", .{display_line});
@@ -3053,42 +3118,81 @@ fn printErrorInstance(
const clamped = trimmed[0..@min(trimmed.len, max_line_length)];
if (clamped.len != trimmed.len) {
const fmt = if (comptime allow_ansi_color) "<r><d> | ... truncated <r>\n\n" else "\n\n";
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}" ++ fmt,
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
} else {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}\n",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
if (clamped.len < max_line_length_with_divot or top.position.column.zeroBased() > max_line_length_with_divot) {
const indent = max_line_number_pad + " | ".len + @as(u64, @intCast(top.position.column.zeroBased()));
try writer.writeByteNTimes(' ', indent);
try writer.print(comptime Output.prettyFmt(
"<red><b>^<r>\n",
allow_ansi_color,
), .{});
if (is_ai_agent) {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d}-<r> {}",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
if (comptime allow_ansi_color) {
try writer.writeAll(Output.prettyFmt("<d> ... truncated<r>\n\n", true));
} else {
try writer.writeAll(" ... truncated\n\n");
}
} else {
try writer.writeAll("\n");
const fmt = if (comptime allow_ansi_color) "<r><d> | ... truncated <r>\n\n" else "\n\n";
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}" ++ fmt,
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
}
} else {
if (is_ai_agent) {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d}-<r> {}\n",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
if (clamped.len < max_line_length_with_divot or top.position.column.zeroBased() > max_line_length_with_divot) {
// Calculate indent for caret: line number padding + separator width + column
const indent: usize = @intCast(max_line_number_pad + sep_len + @as(u64, @intCast(top.position.column.zeroBased())));
try writer.writeByteNTimes(' ', indent);
try writer.print(comptime Output.prettyFmt(
"<red><b>^<r>\n",
allow_ansi_color,
), .{});
} else {
try writer.writeAll("\n");
}
} else {
try writer.print(
comptime Output.prettyFmt(
"<r><b>{d} |<r> {}\n",
allow_ansi_color,
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
if (clamped.len < max_line_length_with_divot or top.position.column.zeroBased() > max_line_length_with_divot) {
// Calculate indent for caret: line number padding + separator width + column
const indent: usize = @intCast(max_line_number_pad + sep_len + @as(u64, @intCast(top.position.column.zeroBased())));
try writer.writeByteNTimes(' ', indent);
try writer.print(comptime Output.prettyFmt(
"<red><b>^<r>\n",
allow_ansi_color,
), .{});
} else {
try writer.writeAll("\n");
}
}
}
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, Writer, writer, allow_ansi_color, formatter.error_display_level);
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, exception.runtime_type, Writer, writer, allow_ansi_color, formatter.error_display_level);
}
}
if (!did_print_name) {
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, Writer, writer, allow_ansi_color, formatter.error_display_level);
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, exception.runtime_type, Writer, writer, allow_ansi_color, formatter.error_display_level);
}
// This is usually unsafe to do, but we are protecting them each time first
@@ -3279,11 +3383,35 @@ fn printErrorNameAndMessage(
message: String,
is_browser_error: bool,
optional_code: ?[]const u8,
runtime_type: jsc.JSRuntimeType,
comptime Writer: type,
writer: Writer,
comptime allow_ansi_color: bool,
error_display_level: ConsoleObject.FormatOptions.ErrorDisplayLevel,
) !void {
// In AI mode, print runtime type information before the error message
if (Output.isAIAgent() and runtime_type != .Nothing) {
const type_name = switch (runtime_type) {
.Function => "Function",
.Undefined => "Undefined",
.Null => "Null",
.Boolean => "Boolean",
.AnyInt => "Integer",
.Number => "Number",
.String => "String",
.Object => "Object",
.Symbol => "Symbol",
.BigInt => "BigInt",
else => null,
};
if (type_name) |tn| {
if (comptime allow_ansi_color) {
try writer.print(comptime Output.prettyFmt(" <d>value type: <r><cyan>{s}<r>\n", true), .{tn});
} else {
try writer.print(" value type: {s}\n", .{tn});
}
}
}
if (is_browser_error) {
try writer.writeAll(Output.prettyFmt("<red>frontend<r> ", true));
}

View File

@@ -2,13 +2,13 @@
exports[`CLAUDECODE=1 shows quiet test output (only failures) 1`] = `
"test2.test.js:
4 | test("passing test", () => {
5 | expect(1).toBe(1);
6 | });
7 |
8 | test("failing test", () => {
9 | expect(1).toBe(2);
^
4- test("passing test", () => {
5- expect(1).toBe(1);
6- });
7-
8- test("failing test", () => {
9- expect(1).toBe(2);
^
error: expect(received).toBe(expected)
Expected: 2
@@ -65,3 +65,78 @@ exports[`CLAUDECODE flag handles no test files found: no-tests-quiet 1`] = `
bun test <version> (<revision>)"
`;
exports[`CLAUDECODE=1 formats error source lines with dash separator: error-normal 1`] = `
"error.test.js:
3 |
4 | test("error formatting", () => {
5 | function foo() {
6 | const x = 1;
7 | const y = 2;
8 | throw new Error("Test error message");
^
error: Test error message
at foo (file:NN:NN)
at <anonymous> (file:NN:NN)
(fail) error formatting
0 pass
1 fail
Ran 1 test across 1 file.
bun test <version> (<revision>)"
`;
exports[`CLAUDECODE=1 formats error source lines with dash separator: error-ai-agent 1`] = `
"error.test.js:
3-
4- test("error formatting", () => {
5- function foo() {
6- const x = 1;
7- const y = 2;
8- throw new Error("Test error message");
^
error: Test error message
at foo (file:NN:NN)
at <anonymous> (file:NN:NN)
(fail) error formatting
0 pass
1 fail
Ran 1 test across 1 file.
bun test <version> (<revision>)"
`;
exports[`CLAUDECODE=1 error format in Bun.inspect: inspect-error-normal 1`] = `
"inspect.test.js:
(pass) inspect error format
1 pass
0 fail
Ran 1 test across 1 file.
bun test <version> (<revision>)
1 |
2 | import { test, expect } from "bun:test";
3 |
4 | test("inspect error format", () => {
5 | const err = new Error("Inspected error");
^
error: Inspected error
at <anonymous> (file:NN:NN)"
`;
exports[`CLAUDECODE=1 error format in Bun.inspect: inspect-error-ai-agent 1`] = `
"inspect.test.js:
1 pass
0 fail
Ran 1 test across 1 file.
bun test <version> (<revision>)
1-
2- import { test, expect } from "bun:test";
3-
4- test("inspect error format", () => {
5- const err = new Error("Inspected error");
^
error: Inspected error
at <anonymous> (file:NN:NN)"
`;

View File

@@ -144,3 +144,95 @@ test("CLAUDECODE flag handles no test files found", () => {
expect(normalizeBunSnapshot(normalOutput, dir)).toMatchSnapshot("no-tests-normal");
expect(normalizeBunSnapshot(quietOutput, dir)).toMatchSnapshot("no-tests-quiet");
});
test("CLAUDECODE=1 formats error source lines with dash separator", () => {
const dir = tempDirWithFiles("claudecode-error-format", {
"error.test.js": `
import { test, expect } from "bun:test";
test("error formatting", () => {
function foo() {
const x = 1;
const y = 2;
throw new Error("Test error message");
}
foo();
});
`,
});
// Run with CLAUDECODE=0 (normal output with pipe separator)
const result1 = spawnSync({
cmd: [bunExe(), "test", "error.test.js"],
env: { ...testEnv, CLAUDECODE: "0" },
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
// Run with CLAUDECODE=1 (AI agent output with dash separator)
const result2 = spawnSync({
cmd: [bunExe(), "test", "error.test.js"],
env: { ...testEnv, CLAUDECODE: "1" },
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
const normalOutput = result1.stderr.toString() + result1.stdout.toString();
const aiAgentOutput = result2.stderr.toString() + result2.stdout.toString();
// Normal output should use pipe separator: "6 |"
expect(normalOutput).toMatch(/\d+ \|/);
// AI agent output should use dash separator: "6-"
expect(aiAgentOutput).toMatch(/\d+-/);
expect(aiAgentOutput).not.toMatch(/\d+ \|/);
expect(normalizeBunSnapshot(normalOutput, dir)).toMatchSnapshot("error-normal");
expect(normalizeBunSnapshot(aiAgentOutput, dir)).toMatchSnapshot("error-ai-agent");
});
test("CLAUDECODE=1 error format in Bun.inspect", () => {
const dir = tempDirWithFiles("claudecode-inspect-error", {
"inspect.test.js": `
import { test, expect } from "bun:test";
test("inspect error format", () => {
const err = new Error("Inspected error");
console.log(Bun.inspect(err));
});
`,
});
// Run with CLAUDECODE=0 (normal output with pipe separator)
const result1 = spawnSync({
cmd: [bunExe(), "test", "inspect.test.js"],
env: { ...testEnv, CLAUDECODE: "0" },
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
// Run with CLAUDECODE=1 (AI agent output with dash separator)
const result2 = spawnSync({
cmd: [bunExe(), "test", "inspect.test.js"],
env: { ...testEnv, CLAUDECODE: "1" },
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
const normalOutput = result1.stderr.toString() + result1.stdout.toString();
const aiAgentOutput = result2.stderr.toString() + result2.stdout.toString();
// Normal output should use pipe separator: "6 |"
expect(normalOutput).toMatch(/\d+ \|/);
// AI agent output should use dash separator: "6-"
expect(aiAgentOutput).toMatch(/\d+-/);
expect(aiAgentOutput).not.toMatch(/\d+ \|/);
expect(normalizeBunSnapshot(normalOutput, dir)).toMatchSnapshot("inspect-error-normal");
expect(normalizeBunSnapshot(aiAgentOutput, dir)).toMatchSnapshot("inspect-error-ai-agent");
});