agent: record raw REPL input

- Add `recordRaw` to record raw JS lines in the REPL.
- Only record commands if they succeed without error.
- Fix kitty terminal cursor keys by forcing legacy arrow encoding.
This commit is contained in:
Adrià Arrufat
2026-06-02 12:54:37 +02:00
parent 0d86c5a22d
commit 3bbe20735f
3 changed files with 54 additions and 2 deletions

View File

@@ -483,6 +483,8 @@ fn runRepl(self: *Agent) void {
self.terminal.printError("{s}", .{result.text});
} else {
self.printData(result.text);
if (self.recorder) |*r| r.recordRaw(line);
self.recordSaveRaw(line);
}
continue :repl;
}
@@ -530,8 +532,10 @@ fn runRepl(self: *Agent) void {
const result = self.runCommand(aa, cmd);
self.terminal.endTool();
self.printCommandResult(cmd, result);
if (self.recorder) |*r| r.record(cmd);
self.recordSaveCommand(cmd);
if (!result.is_error) {
if (self.recorder) |*r| r.record(cmd);
self.recordSaveCommand(cmd);
}
self.recordSlashToolCall(trimmed, tc.name(), tc.args, result) catch |err| {
self.terminal.printWarning("LLM conversation out of sync (/{s}: {s}); next prompt may not see this action", .{ tc.name(), @errorName(err) });
};
@@ -784,6 +788,12 @@ fn recordSaveComment(self: *Agent, comment: []const u8) void {
};
}
fn recordSaveRaw(self: *Agent, line: []const u8) void {
self.save_buffer.recordRaw(line) catch |err| {
self.terminal.printError("save buffer disabled: {s}", .{@errorName(err)});
};
}
fn printSlashHelp(self: *Agent, arena: std.mem.Allocator, target: []const u8) void {
if (target.len == 0) {
const all = Schema.all();

View File

@@ -859,10 +859,15 @@ const RawTerminal = struct {
raw.cc[@intFromEnum(std.c.V.MIN)] = 0;
raw.cc[@intFromEnum(std.c.V.TIME)] = 1;
try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, raw);
// Under the REPL's kitty "disambiguate" flag, cursor keys arrive as
// CSI-u the byte reader can't parse; push flag 0 to force legacy arrow
// encoding. restore() pops back to the REPL's flag.
_ = std.posix.write(std.posix.STDOUT_FILENO, "\x1b[>0u") catch {};
return .{ .original = original };
}
fn restore(self: *const RawTerminal) void {
_ = std.posix.write(std.posix.STDOUT_FILENO, "\x1b[<u") catch {};
std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, self.original) catch {};
}
};

View File

@@ -95,6 +95,18 @@ pub fn recordComment(self: *Recorder, comment: []const u8) void {
self.tryRecordComment(comment) catch |err| self.disable(err);
}
pub fn recordRaw(self: *Recorder, line: []const u8) void {
if (self.file == null) return;
self.tryRecordRaw(line) catch |err| self.disable(err);
}
fn tryRecordRaw(self: *Recorder, line: []const u8) !void {
self.buf.clearRetainingCapacity();
try self.buf.writer.writeAll(line);
try self.buf.writer.writeByte('\n');
try self.writeScrubbed();
}
fn tryRecordComment(self: *Recorder, comment: []const u8) !void {
self.buf.clearRetainingCapacity();
try writeCommentLines(&self.buf.writer, comment);
@@ -191,6 +203,13 @@ pub const Memory = struct {
try self.appendScrubbed();
}
pub fn recordRaw(self: *Memory, line: []const u8) !void {
self.buf.clearRetainingCapacity();
try self.buf.writer.writeAll(line);
try self.buf.writer.writeByte('\n');
try self.appendScrubbed();
}
fn appendScrubbed(self: *Memory) !void {
_ = self.arena.reset(.retain_capacity);
const scrubbed = try lp.tools.reverseSubstituteEnvVars(self.arena.allocator(), self.buf.written());
@@ -248,6 +267,24 @@ test "record writes state-mutating commands" {
try std.testing.expect(std.mem.indexOf(u8, content, "markdown(") == null);
}
test "recordRaw writes the JS line verbatim" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var recorder = try Recorder.init(std.testing.allocator, tmp.dir, "raw.js");
defer recorder.deinit();
recorder.recordRaw("document.title");
recorder.recordRaw("window.scrollTo(0, 100)");
const file = tmp.dir.openFile("raw.js", .{}) catch unreachable;
defer file.close();
var buf: [256]u8 = undefined;
const n = file.readAll(&buf) catch unreachable;
try std.testing.expectEqualStrings("document.title\nwindow.scrollTo(0, 100)\n", buf[0..n]);
}
test "record skips empty and comment lines" {
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();