mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
refactor: unify tool UI and browser action finalization
- Add `beginTool` and `endTool` to `Terminal` to encapsulate spinner logic. - Consolidate navigation awaiting and context tagging in browser tools via a new `finalizeAction` helper.
This commit is contained in:
@@ -361,12 +361,11 @@ fn runRepl(self: *Self) void {
|
||||
.natural_language => _ = self.runTurn(.{ .prompt = line, .record_comment = line }),
|
||||
else => {
|
||||
const split = SlashCommand.splitNameRest(line) orelse continue :repl;
|
||||
self.terminal.spinner.setTool(split.name, split.rest);
|
||||
self.terminal.beginTool(split.name, split.rest);
|
||||
var arena: std.heap.ArenaAllocator = .init(self.allocator);
|
||||
defer arena.deinit();
|
||||
const result = self.cmd_executor.executeWithResult(arena.allocator(), cmd);
|
||||
if (result.failed) self.terminal.spinner.markToolFailed();
|
||||
self.terminal.spinner.cancel();
|
||||
self.terminal.endTool(!result.failed);
|
||||
self.cmd_executor.printResult(cmd, result);
|
||||
self.recorder.record(cmd);
|
||||
},
|
||||
@@ -414,10 +413,9 @@ fn handleSlash(self: *Self, body: []const u8) bool {
|
||||
self.terminal.printError("eval requires a `script` argument.");
|
||||
return false;
|
||||
};
|
||||
self.terminal.spinner.setTool(schema.tool_name, rest);
|
||||
self.terminal.beginTool(schema.tool_name, rest);
|
||||
const result = self.tool_executor.callEval(aa, eval_script);
|
||||
if (result.is_error) self.terminal.spinner.markToolFailed();
|
||||
self.terminal.spinner.cancel();
|
||||
self.terminal.endTool(!result.is_error);
|
||||
if (result.is_error) {
|
||||
self.terminal.printErrorFmt("eval: {s}", .{result.text});
|
||||
} else {
|
||||
@@ -426,13 +424,12 @@ fn handleSlash(self: *Self, body: []const u8) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.terminal.spinner.setTool(schema.tool_name, rest);
|
||||
self.terminal.beginTool(schema.tool_name, rest);
|
||||
if (self.tool_executor.call(aa, schema.tool_name, args_json)) |result| {
|
||||
self.terminal.spinner.cancel();
|
||||
self.terminal.endTool(true);
|
||||
self.terminal.printToolResult(schema.tool_name, result);
|
||||
} else |err| {
|
||||
self.terminal.spinner.markToolFailed();
|
||||
self.terminal.spinner.cancel();
|
||||
self.terminal.endTool(false);
|
||||
self.terminal.printErrorFmt("{s}: {s}", .{ schema.tool_name, @errorName(err) });
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -112,6 +112,18 @@ pub fn deinit(self: *Self) void {
|
||||
|
||||
const bullet_line_fmt = "{s}●{s} {s}[tool: {s}]{s} {s}\n";
|
||||
|
||||
/// Mark the start of a manual REPL tool call. Pairs with `endTool`.
|
||||
pub fn beginTool(self: *Self, name: []const u8, args: []const u8) void {
|
||||
self.spinner.setTool(name, args);
|
||||
}
|
||||
|
||||
/// Mark the end of a manual REPL tool call. On failure, flashes the spinner
|
||||
/// label red before clearing it.
|
||||
pub fn endTool(self: *Self, ok: bool) void {
|
||||
if (!ok) self.spinner.markToolFailed();
|
||||
self.spinner.cancel();
|
||||
}
|
||||
|
||||
/// Called after the tool returns.
|
||||
///
|
||||
/// - Spinner mode (TTY REPL): the running label flashes red on failure
|
||||
|
||||
@@ -790,7 +790,12 @@ fn formatActionResult(
|
||||
return std.fmt.allocPrint(arena, "{s} ({s}){s}", .{ prefix, target, suffix }) catch ToolError.InternalError;
|
||||
}
|
||||
|
||||
fn appendPageContext(arena: std.mem.Allocator, body: []const u8, page: *lp.Frame) ToolError![]const u8 {
|
||||
/// Finish a state-changing action: drain any queued navigation triggered by
|
||||
/// the action, then tag `body` with the resulting page URL and title so the
|
||||
/// caller (LLM, MCP client) can see whether the action triggered navigation.
|
||||
fn finalizeAction(arena: std.mem.Allocator, session: *lp.Session, body: []const u8) ToolError![]const u8 {
|
||||
try awaitQueuedNavigation(session);
|
||||
const page = try requireFrame(session);
|
||||
const page_title = page.getTitle() catch null;
|
||||
return std.fmt.allocPrint(arena, "{s}. Page url: {s}, title: {s}", .{
|
||||
body, page.url, page_title orelse "(none)",
|
||||
@@ -807,11 +812,8 @@ fn execClick(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.
|
||||
|
||||
lp.actions.click(resolved.node, resolved.page) catch |err| return mapActionError(err);
|
||||
|
||||
try awaitQueuedNavigation(session);
|
||||
|
||||
const page = try requireFrame(session);
|
||||
const body = try formatActionResult(arena, "Clicked element", args.selector, args.backendNodeId, "");
|
||||
return appendPageContext(arena, body, page);
|
||||
return finalizeAction(arena, session, body);
|
||||
}
|
||||
|
||||
fn execFill(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
@@ -830,7 +832,8 @@ fn execFill(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.R
|
||||
|
||||
// Show the original reference (e.g. $LP_PASSWORD) in the result, not the resolved value
|
||||
const suffix = std.fmt.allocPrint(arena, " with \"{s}\"", .{raw_text}) catch return ToolError.InternalError;
|
||||
return formatActionResult(arena, "Filled element", args.selector, args.backendNodeId, suffix);
|
||||
const body = try formatActionResult(arena, "Filled element", args.selector, args.backendNodeId, suffix);
|
||||
return finalizeAction(arena, session, body);
|
||||
}
|
||||
|
||||
fn execScroll(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
@@ -881,7 +884,8 @@ fn execHover(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.
|
||||
|
||||
lp.actions.hover(resolved.node, resolved.page) catch |err| return mapActionError(err);
|
||||
|
||||
return formatActionResult(arena, "Hovered element", args.selector, args.backendNodeId, "");
|
||||
const body = try formatActionResult(arena, "Hovered element", args.selector, args.backendNodeId, "");
|
||||
return finalizeAction(arena, session, body);
|
||||
}
|
||||
|
||||
fn execPress(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
@@ -896,12 +900,10 @@ fn execPress(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.
|
||||
|
||||
lp.actions.press(target_node, args.key, page) catch |err| return mapActionError(err);
|
||||
|
||||
// Pressing Enter on a form input triggers implicit form submission.
|
||||
try awaitQueuedNavigation(session);
|
||||
|
||||
const current_page = try requireFrame(session);
|
||||
// Pressing Enter on a form input triggers implicit form submission;
|
||||
// `finalizeAction` drains the queued navigation before tagging the body.
|
||||
const body = std.fmt.allocPrint(arena, "Pressed key '{s}'", .{args.key}) catch return ToolError.InternalError;
|
||||
return appendPageContext(arena, body, current_page);
|
||||
return finalizeAction(arena, session, body);
|
||||
}
|
||||
|
||||
fn execSelectOption(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
@@ -916,7 +918,8 @@ fn execSelectOption(arena: std.mem.Allocator, session: *lp.Session, registry: *C
|
||||
lp.actions.selectOption(resolved.node, args.value, resolved.page) catch |err| return mapActionError(err);
|
||||
|
||||
const prefix = std.fmt.allocPrint(arena, "Selected option '{s}'", .{args.value}) catch return ToolError.InternalError;
|
||||
return formatActionResult(arena, prefix, args.selector, args.backendNodeId, "");
|
||||
const body = try formatActionResult(arena, prefix, args.selector, args.backendNodeId, "");
|
||||
return finalizeAction(arena, session, body);
|
||||
}
|
||||
|
||||
fn execSetChecked(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
@@ -932,7 +935,8 @@ fn execSetChecked(arena: std.mem.Allocator, session: *lp.Session, registry: *CDP
|
||||
|
||||
const state_str: []const u8 = if (args.checked) "checked" else "unchecked";
|
||||
const suffix = std.fmt.allocPrint(arena, " to {s}", .{state_str}) catch return ToolError.InternalError;
|
||||
return formatActionResult(arena, "Set element", args.selector, args.backendNodeId, suffix);
|
||||
const body = try formatActionResult(arena, "Set element", args.selector, args.backendNodeId, suffix);
|
||||
return finalizeAction(arena, session, body);
|
||||
}
|
||||
|
||||
fn execFindElement(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
|
||||
Reference in New Issue
Block a user