mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-12 01:56:19 -04:00
Unifies PandaScript syntax by replacing uppercase keywords with slash commands (e.g., `/goto`, `/click`, `/fill`). Refactors the `Command` representation to use a generic tool call structure backed by schemas. BREAKING CHANGE: Uppercase PandaScript keywords (GOTO, CLICK, TYPE, etc.) are no longer supported. All scripts must use slash commands.
90 lines
3.8 KiB
Zig
90 lines
3.8 KiB
Zig
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
const lp = @import("lightpanda");
|
|
const browser_tools = lp.tools;
|
|
const Command = lp.script.Command;
|
|
const ToolExecutor = @import("ToolExecutor.zig");
|
|
const Terminal = @import("Terminal.zig");
|
|
|
|
const CommandRunner = @This();
|
|
|
|
tool_executor: *ToolExecutor,
|
|
terminal: *Terminal,
|
|
|
|
pub fn init(tool_executor: *ToolExecutor, terminal: *Terminal) CommandRunner {
|
|
return .{
|
|
.tool_executor = tool_executor,
|
|
.terminal = terminal,
|
|
};
|
|
}
|
|
|
|
/// Caller contract: `cmd` must be `.tool_call` — `.comment`, `.login`, and
|
|
/// `.accept_cookies` are filtered upstream (see `Agent.runRepl`) because they
|
|
/// have no tool mapping.
|
|
pub fn executeWithResult(self: *CommandRunner, arena: std.mem.Allocator, cmd: Command) browser_tools.ToolResult {
|
|
const tc = switch (cmd) {
|
|
.tool_call => |t| t,
|
|
else => return .{ .text = "internal: command has no tool mapping", .is_error = true },
|
|
};
|
|
const substituted = substituteStringArgs(arena, tc.name, tc.args) catch
|
|
return .{ .text = "out of memory", .is_error = true };
|
|
return self.tool_executor.callValue(arena, tc.name, substituted) catch |err| .{
|
|
.text = std.fmt.allocPrint(arena, "{s} failed: {s}", .{ tc.name, @errorName(err) }) catch "tool failed",
|
|
.is_error = true,
|
|
};
|
|
}
|
|
|
|
/// Resolve `$LP_*` placeholders in string args before the tool runs. `fill`'s
|
|
/// `value` is excluded — the tool resolves it internally and rewrites the
|
|
/// result text so the credential never appears in the echoed confirmation.
|
|
fn substituteStringArgs(arena: std.mem.Allocator, tool_name: []const u8, args: ?std.json.Value) error{OutOfMemory}!?std.json.Value {
|
|
const v = args orelse return null;
|
|
if (v != .object) return v;
|
|
var changed = false;
|
|
var new_obj: std.json.ObjectMap = .init(arena);
|
|
try new_obj.ensureTotalCapacity(v.object.count());
|
|
var it = v.object.iterator();
|
|
while (it.next()) |entry| {
|
|
const key = entry.key_ptr.*;
|
|
const val = entry.value_ptr.*;
|
|
const exclude = std.mem.eql(u8, tool_name, "fill") and std.mem.eql(u8, key, "value");
|
|
if (!exclude and val == .string) {
|
|
const resolved = try browser_tools.substituteEnvVars(arena, val.string);
|
|
if (resolved.ptr != val.string.ptr) changed = true;
|
|
try new_obj.put(key, .{ .string = resolved });
|
|
continue;
|
|
}
|
|
try new_obj.put(key, val);
|
|
}
|
|
// Only allocate a new value if substitution actually changed something —
|
|
// the original args object can stay aliased into the caller's arena.
|
|
return if (changed) .{ .object = new_obj } else v;
|
|
}
|
|
|
|
/// Data output (extract/eval/markdown/tree/…) → stdout on success; everything
|
|
/// else, including failures from those same commands, → stderr.
|
|
pub fn printResult(self: *CommandRunner, cmd: Command, result: browser_tools.ToolResult) void {
|
|
if (cmd.producesData() and !result.is_error) {
|
|
self.terminal.printAssistant(result.text);
|
|
} else {
|
|
self.terminal.printActionResult(result.text);
|
|
}
|
|
}
|