From 7aef08f28b76848bf9d1312aabdfbf3ddc2463a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 30 Apr 2026 16:22:19 +0200 Subject: [PATCH] agent: replace QUIT command with /quit slash command --- docs/agent.md | 5 ++- src/agent/Agent.zig | 7 +---- src/agent/Command.zig | 18 ++--------- src/agent/CommandExecutor.zig | 2 +- src/agent/Terminal.zig | 59 ++++++++++++++++++++++------------- 5 files changed, 44 insertions(+), 47 deletions(-) diff --git a/docs/agent.md b/docs/agent.md index 1e9cffea..a9faf34d 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -66,13 +66,12 @@ recorded scripts round-trip through the parser. | `EXTRACT` | `EXTRACT ''` | Returns text content. | | `EVAL` | `EVAL ''` 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 diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 39ae67ee..6372520e 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -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 diff --git a/src/agent/Command.zig b/src/agent/Command.zig index e99cf6e7..a68c512d 100644 --- a/src/agent/Command.zig +++ b/src/agent/Command.zig @@ -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); } diff --git a/src/agent/CommandExecutor.zig b/src/agent/CommandExecutor.zig index 11ead679..b8fa52c7 100644 --- a/src/agent/CommandExecutor.zig +++ b/src/agent/CommandExecutor.zig @@ -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); diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index 106a9b88..94273b6b 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -34,9 +34,12 @@ const commands = [_]CommandInfo{ .{ .name = "EVAL", .hint = " '