mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
agent: replace QUIT command with /quit slash command
This commit is contained in:
@@ -66,13 +66,12 @@ recorded scripts round-trip through the parser.
|
||||
| `EXTRACT` | `EXTRACT '<selector>'` | Returns text content. |
|
||||
| `EVAL` | `EVAL '<js>'` or `EVAL '''…'''` | Triple-quote for multi-line JS. |
|
||||
| `TREE` | `TREE` | Print the semantic tree (not recorded). |
|
||||
| `MARKDOWN` / `MD`| `MARKDOWN` | Print page as markdown (not recorded). |
|
||||
| `MARKDOWN` | `MARKDOWN` | Print page as markdown (not recorded). |
|
||||
| `LOGIN` | `LOGIN` | LLM-driven: fill `$LP_USERNAME` / `$LP_PASSWORD`. |
|
||||
| `ACCEPT_COOKIES` | `ACCEPT_COOKIES` | LLM-driven: dismiss the consent banner. |
|
||||
| `EXIT` / `QUIT` | `EXIT` | REPL only. |
|
||||
|
||||
In the REPL, anything that does not parse as a Pandascript command is sent to
|
||||
the LLM as natural language.
|
||||
the LLM as natural language. To leave the REPL, use the `/quit` slash command.
|
||||
|
||||
### Example script
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ fn runOneShot(self: *Self, task: []const u8) bool {
|
||||
}
|
||||
|
||||
fn runRepl(self: *Self) void {
|
||||
self.terminal.printInfo("Lightpanda Agent (type 'quit' to exit)");
|
||||
self.terminal.printInfo("Lightpanda Agent (type '/quit' to exit)");
|
||||
self.terminal.printInfo("Tab completes/cycles throuch commands; the dim grey ghost shows the first match.");
|
||||
log.debug(.app, "tools loaded", .{ .count = self.tools.len });
|
||||
if (self.ai_client) |ai_client| {
|
||||
@@ -291,7 +291,6 @@ fn runRepl(self: *Self) void {
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
.quit => break :repl,
|
||||
.comment => continue :repl,
|
||||
.login => self.processUserMessage(login_prompt, line) catch |err| {
|
||||
self.terminal.printErrorFmt("LOGIN failed: {s}", .{@errorName(err)});
|
||||
@@ -452,10 +451,6 @@ fn runScript(self: *Self, path: []const u8) bool {
|
||||
|
||||
while (iter.next()) |entry| {
|
||||
switch (entry.command) {
|
||||
.quit => {
|
||||
self.terminal.printInfo("QUIT — stopping script.");
|
||||
break;
|
||||
},
|
||||
.comment => {
|
||||
// Track the most recent comment — recorded scripts
|
||||
// prefix LLM-generated commands with the natural
|
||||
|
||||
@@ -36,13 +36,12 @@ pub const Command = union(enum) {
|
||||
eval_js: []const u8,
|
||||
login: void,
|
||||
accept_cookies: void,
|
||||
quit: void,
|
||||
comment: void,
|
||||
natural_language: []const u8,
|
||||
|
||||
pub fn isRecorded(self: Command) bool {
|
||||
return switch (self) {
|
||||
.tree, .markdown, .comment, .quit => false,
|
||||
.tree, .markdown, .comment => false,
|
||||
.goto, .click, .type_cmd, .wait, .scroll, .hover, .select, .check, .extract, .eval_js, .login, .accept_cookies => true,
|
||||
.natural_language => |text| text.len > 0,
|
||||
};
|
||||
@@ -100,7 +99,6 @@ pub const Command = union(enum) {
|
||||
try writer.print("EVAL {f}", .{quote(script)}),
|
||||
.login => try writer.writeAll("LOGIN"),
|
||||
.accept_cookies => try writer.writeAll("ACCEPT_COOKIES"),
|
||||
.quit => try writer.writeAll("QUIT"),
|
||||
.comment => try writer.writeAll("#"),
|
||||
.natural_language => |text| try writer.writeAll(text),
|
||||
}
|
||||
@@ -215,10 +213,6 @@ pub fn parse(line: []const u8) Command {
|
||||
return .{ .accept_cookies = {} };
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(cmd_word, "QUIT")) {
|
||||
return .{ .quit = {} };
|
||||
}
|
||||
|
||||
return .{ .natural_language = trimmed };
|
||||
}
|
||||
|
||||
@@ -417,7 +411,7 @@ pub fn noSubstitute(_: std.mem.Allocator, input: []const u8) []const u8 {
|
||||
|
||||
/// Map a Command to its (tool_name, JSON args) representation. Returns
|
||||
/// null for variants without a 1:1 tool mapping (login, accept_cookies,
|
||||
/// natural_language, comment, quit, extract — extract is rendered as a
|
||||
/// natural_language, comment, extract — extract is rendered as a
|
||||
/// custom `eval` script by the caller).
|
||||
///
|
||||
/// `substitute` is applied to selector-like fields. The `value` field of
|
||||
@@ -447,7 +441,7 @@ pub fn toToolCall(arena: std.mem.Allocator, cmd: Command, substitute: Substitute
|
||||
.tree => .{ .name = @tagName(Action.tree), .args_json = "" },
|
||||
.markdown => .{ .name = @tagName(Action.markdown), .args_json = "" },
|
||||
.eval_js => |script| .{ .name = @tagName(Action.eval), .args_json = buildJson(arena, .{ .script = script }) },
|
||||
.extract, .quit, .natural_language, .comment, .login, .accept_cookies => null,
|
||||
.extract, .natural_language, .comment, .login, .accept_cookies => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -709,11 +703,6 @@ test "parse ACCEPT_COOKIES" {
|
||||
try std.testing.expect(parse("accept_cookies") == .accept_cookies);
|
||||
}
|
||||
|
||||
test "parse QUIT" {
|
||||
try std.testing.expect(parse("QUIT") == .quit);
|
||||
try std.testing.expect(parse("quit") == .quit);
|
||||
}
|
||||
|
||||
test "parse comment" {
|
||||
try std.testing.expect(parse("# this is a comment") == .comment);
|
||||
try std.testing.expect(parse("# INTENT: LOGIN") == .comment);
|
||||
@@ -1040,7 +1029,6 @@ test "toToolCall: variants without tool mapping return null" {
|
||||
try std.testing.expect(toToolCall(a, .{ .extract = ".x" }, noSubstitute) == null);
|
||||
try std.testing.expect(toToolCall(a, .login, noSubstitute) == null);
|
||||
try std.testing.expect(toToolCall(a, .accept_cookies, noSubstitute) == null);
|
||||
try std.testing.expect(toToolCall(a, .quit, noSubstitute) == null);
|
||||
try std.testing.expect(toToolCall(a, .comment, noSubstitute) == null);
|
||||
try std.testing.expect(toToolCall(a, .{ .natural_language = "hi" }, noSubstitute) == null);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn executeWithResult(self: *Self, a: std.mem.Allocator, cmd: Command.Command
|
||||
if (cmd == .extract) return self.execExtract(a, cmd.extract);
|
||||
|
||||
const tc = Command.toToolCall(a, cmd, browser_tools.substituteEnvVars) orelse switch (cmd) {
|
||||
.quit, .natural_language, .comment, .login, .accept_cookies => unreachable,
|
||||
.natural_language, .comment, .login, .accept_cookies => unreachable,
|
||||
else => return .{ .output = "command has no tool mapping", .failed = true },
|
||||
};
|
||||
return self.callTool(a, tc.name, tc.args_json);
|
||||
|
||||
@@ -34,9 +34,12 @@ const commands = [_]CommandInfo{
|
||||
.{ .name = "EVAL", .hint = " '<script>'" },
|
||||
.{ .name = "LOGIN", .hint = "" },
|
||||
.{ .name = "ACCEPT_COOKIES", .hint = "" },
|
||||
.{ .name = "QUIT", .hint = "" },
|
||||
};
|
||||
|
||||
// Meta slash commands handled directly by the agent (not by ToolExecutor).
|
||||
// Kept in sync with `handleSlash` in `Agent.zig`.
|
||||
const meta_slash_commands = [_][:0]const u8{ "help", "quit" };
|
||||
|
||||
pub fn init(history_path: ?[:0]const u8) Self {
|
||||
c.linenoiseSetMultiLine(1);
|
||||
c.linenoiseSetCompletionCallback(&completionCallback);
|
||||
@@ -47,6 +50,23 @@ pub fn init(history_path: ?[:0]const u8) Self {
|
||||
return .{ .history_path = history_path };
|
||||
}
|
||||
|
||||
fn addSlashCompletion(lc: [*c]c.linenoiseCompletions, name_buf: *[64:0]u8, name: []const u8, partial: []const u8) void {
|
||||
const total = 1 + name.len;
|
||||
if (total >= name_buf.len) return;
|
||||
if (name.len < partial.len) return;
|
||||
if (!std.ascii.eqlIgnoreCase(name[0..partial.len], partial)) return;
|
||||
name_buf[0] = '/';
|
||||
@memcpy(name_buf[1..total], name);
|
||||
name_buf[total] = 0;
|
||||
c.linenoiseAddCompletion(lc, name_buf);
|
||||
}
|
||||
|
||||
fn slashHint(name: []const u8, partial: []const u8) ?[]const u8 {
|
||||
if (name.len <= partial.len) return null;
|
||||
if (!std.ascii.eqlIgnoreCase(name[0..partial.len], partial)) return null;
|
||||
return name[partial.len..];
|
||||
}
|
||||
|
||||
fn completionCallback(buf: [*c]const u8, lc: [*c]c.linenoiseCompletions) callconv(.c) void {
|
||||
const input = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(buf)), 0);
|
||||
if (input.len == 0) return;
|
||||
@@ -55,18 +75,10 @@ fn completionCallback(buf: [*c]const u8, lc: [*c]c.linenoiseCompletions) callcon
|
||||
if (input[0] == '/') {
|
||||
const partial = input[1..];
|
||||
// linenoise strdup's the string, so a stack buffer reused per match
|
||||
// is fine. 64 covers every tool name comfortably.
|
||||
// is fine. 64 covers every name comfortably.
|
||||
var name_buf: [64:0]u8 = undefined;
|
||||
for (browser_tools.tool_defs) |td| {
|
||||
const total = 1 + td.name.len;
|
||||
if (total >= name_buf.len) continue;
|
||||
if (td.name.len < partial.len) continue;
|
||||
if (!std.ascii.eqlIgnoreCase(td.name[0..partial.len], partial)) continue;
|
||||
name_buf[0] = '/';
|
||||
@memcpy(name_buf[1..total], td.name);
|
||||
name_buf[total] = 0;
|
||||
c.linenoiseAddCompletion(lc, &name_buf);
|
||||
}
|
||||
for (browser_tools.tool_defs) |td| addSlashCompletion(lc, &name_buf, td.name, partial);
|
||||
for (meta_slash_commands) |name| addSlashCompletion(lc, &name_buf, name, partial);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,16 +104,19 @@ fn hintsCallback(buf: [*c]const u8, color: [*c]c_int, bold: [*c]c_int) callconv(
|
||||
|
||||
if (input[0] == '/') {
|
||||
const partial = input[1..];
|
||||
for (browser_tools.tool_defs) |td| {
|
||||
if (td.name.len <= partial.len) continue;
|
||||
if (!std.ascii.eqlIgnoreCase(td.name[0..partial.len], partial)) continue;
|
||||
const suffix = td.name[partial.len..];
|
||||
if (suffix.len + 1 > hint_buf.len) return null;
|
||||
@memcpy(hint_buf[0..suffix.len], suffix);
|
||||
hint_buf[suffix.len] = 0;
|
||||
return @ptrCast(&hint_buf);
|
||||
}
|
||||
return null;
|
||||
const suffix = blk: {
|
||||
for (browser_tools.tool_defs) |td| {
|
||||
if (slashHint(td.name, partial)) |s| break :blk s;
|
||||
}
|
||||
for (meta_slash_commands) |name| {
|
||||
if (slashHint(name, partial)) |s| break :blk s;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
if (suffix.len + 1 > hint_buf.len) return null;
|
||||
@memcpy(hint_buf[0..suffix.len], suffix);
|
||||
hint_buf[suffix.len] = 0;
|
||||
return @ptrCast(&hint_buf);
|
||||
}
|
||||
|
||||
for (commands) |cmd| {
|
||||
|
||||
Reference in New Issue
Block a user