From 6da9497fdaf2ada7df2471a4f2a5740da7a517c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Mon, 11 May 2026 14:21:50 +0200 Subject: [PATCH] repl: add syntax highlighting and completions --- src/agent/Agent.zig | 12 +++ src/agent/Command.zig | 34 +++++++ src/agent/Terminal.zig | 207 +++++++++++++++++++++++++++++++++++++++-- src/browser/tools.zig | 18 ++++ 4 files changed, 264 insertions(+), 7 deletions(-) diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index e71bde4c..c447019e 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -312,6 +312,18 @@ fn runRepl(self: *Self) void { const cmd = Command.parse(line); + // Distinguish "you typed `TYPE` but forgot the args" from "this is + // natural language for the LLM". Both fall through to + // `.natural_language` in Command.parse, but the first should never + // hit the LLM-needed error path — it's a syntax mistake on a + // PandaScript command. + if (std.meta.activeTag(cmd) == .natural_language) { + if (Command.keywordSyntax(line)) |kc| { + self.terminal.printErrorFmt("Usage: {s} {s}", .{ kc.name, kc.args }); + continue; + } + } + if (cmd.needsLlm() and self.ai_client == null) { self.terminal.printError("This command needs an LLM. Set an API key or pass --provider (and drop --no-llm if you set it). PandaScript commands (GOTO, CLICK, EXTRACT, ...) work without one."); continue; diff --git a/src/agent/Command.zig b/src/agent/Command.zig index 6449c184..6ca26895 100644 --- a/src/agent/Command.zig +++ b/src/agent/Command.zig @@ -217,6 +217,40 @@ pub fn parse(line: []const u8) Command { return .{ .natural_language = trimmed }; } +pub const KeywordSyntax = struct { + name: []const u8, + args: []const u8, +}; + +/// If the first word of `line` matches a recognized PandaScript keyword that +/// takes arguments, returns its expected shape. Lets the REPL distinguish +/// "you mistyped args for a known command" from "this is natural language" — +/// the latter goes to the LLM, the former gets a syntax error. Argless +/// commands (TREE, MARKDOWN, LOGIN, ACCEPT_COOKIES) are intentionally absent +/// because they always parse successfully when typed alone, so they never +/// fall through to natural_language. +pub fn keywordSyntax(line: []const u8) ?KeywordSyntax { + const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); + const end = std.mem.indexOfAny(u8, trimmed, &std.ascii.whitespace) orelse trimmed.len; + const word = trimmed[0..end]; + const table = [_]KeywordSyntax{ + .{ .name = "GOTO", .args = "" }, + .{ .name = "CLICK", .args = "''" }, + .{ .name = "TYPE", .args = "'' ''" }, + .{ .name = "WAIT", .args = "''" }, + .{ .name = "SCROLL", .args = "[x] [y]" }, + .{ .name = "HOVER", .args = "''" }, + .{ .name = "SELECT", .args = "'' ''" }, + .{ .name = "CHECK", .args = "'' [true|false]" }, + .{ .name = "EXTRACT", .args = "''" }, + .{ .name = "EVAL", .args = "'