mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
agent: unify interactive prompts and tool execution
This commit is contained in:
@@ -1026,39 +1026,21 @@ fn envVarName(p: Config.AiProvider) []const u8 {
|
||||
}
|
||||
|
||||
fn promptForProvider(found: []const Config.AiProvider) !Config.AiProvider {
|
||||
const stdin_tty = std.posix.isatty(std.posix.STDIN_FILENO);
|
||||
const stderr_tty = std.posix.isatty(std.posix.STDERR_FILENO);
|
||||
if (!stdin_tty or !stderr_tty) {
|
||||
if (!interactiveTty()) {
|
||||
log.fatal(.app, "multiple API keys detected", .{
|
||||
.hint = "Pass --provider explicitly when running non-interactively",
|
||||
});
|
||||
return error.AmbiguousProvider;
|
||||
}
|
||||
|
||||
var stdin_buf: [16]u8 = undefined;
|
||||
var stdin = std.fs.File.stdin().reader(&stdin_buf);
|
||||
var labels_buf: [@typeInfo(Config.AiProvider).@"enum".fields.len][]const u8 = undefined;
|
||||
for (found, 0..) |p, i| labels_buf[i] = @tagName(p);
|
||||
|
||||
var attempt: u8 = 0;
|
||||
while (attempt < 3) : (attempt += 1) {
|
||||
std.debug.print("Multiple API keys detected. Pick provider:\n", .{});
|
||||
for (found, 0..) |p, idx| {
|
||||
std.debug.print(" {d}) {s}\n", .{ idx + 1, @tagName(p) });
|
||||
}
|
||||
std.debug.print("> ", .{});
|
||||
|
||||
const line = stdin.interface.takeDelimiterExclusive('\n') catch |err| switch (err) {
|
||||
error.EndOfStream, error.StreamTooLong, error.ReadFailed => return error.UserCancelled,
|
||||
};
|
||||
const trimmed = std.mem.trim(u8, line, " \t\r\n");
|
||||
const choice = std.fmt.parseInt(usize, trimmed, 10) catch {
|
||||
std.debug.print("Invalid input — type a number.\n", .{});
|
||||
continue;
|
||||
};
|
||||
if (choice >= 1 and choice <= found.len) return found[choice - 1];
|
||||
std.debug.print("Out of range.\n", .{});
|
||||
}
|
||||
log.fatal(.app, "could not pick provider", .{ .hint = "Pass --provider explicitly" });
|
||||
return error.AmbiguousProvider;
|
||||
const idx = (promptNumberedChoice("Multiple API keys detected. Pick provider:", labels_buf[0..found.len], false, null) catch {
|
||||
log.fatal(.app, "could not pick provider", .{ .hint = "Pass --provider explicitly" });
|
||||
return error.AmbiguousProvider;
|
||||
}) orelse unreachable;
|
||||
return found[idx];
|
||||
}
|
||||
|
||||
/// Fetch the provider's chat-capable model list and prompt the user to pick
|
||||
@@ -1071,9 +1053,7 @@ fn pickModel(
|
||||
api_key: [:0]const u8,
|
||||
base_url: ?[:0]const u8,
|
||||
) ![]u8 {
|
||||
const stdin_tty = std.posix.isatty(std.posix.STDIN_FILENO);
|
||||
const stderr_tty = std.posix.isatty(std.posix.STDERR_FILENO);
|
||||
if (!stdin_tty or !stderr_tty) {
|
||||
if (!interactiveTty()) {
|
||||
log.fatal(.app, "pick-model needs a TTY", .{
|
||||
.hint = "rerun in a terminal or pass --model explicitly",
|
||||
});
|
||||
@@ -1095,16 +1075,44 @@ fn pickModel(
|
||||
}
|
||||
|
||||
const default_model = zenai.provider.defaultModel(provider);
|
||||
var default_idx: ?usize = null;
|
||||
for (ids, 0..) |id, i| if (std.mem.eql(u8, id, default_model)) {
|
||||
default_idx = i;
|
||||
break;
|
||||
};
|
||||
|
||||
var header_buf: [128]u8 = undefined;
|
||||
const header = std.fmt.bufPrint(&header_buf, "Pick model for {s} (Enter for default):", .{@tagName(provider)}) catch
|
||||
"Pick model (Enter for default):";
|
||||
|
||||
const result = promptNumberedChoice(header, ids, true, default_idx) catch {
|
||||
log.fatal(.app, "could not pick model", .{ .hint = "Pass --model explicitly" });
|
||||
return error.NoChoice;
|
||||
};
|
||||
if (result) |idx| return try allocator.dupe(u8, ids[idx]);
|
||||
// Honor the baked-in default even when it isn't in the listed ids.
|
||||
std.debug.print("Using default: {s}\n", .{default_model});
|
||||
return try allocator.dupe(u8, default_model);
|
||||
}
|
||||
|
||||
fn interactiveTty() bool {
|
||||
return std.posix.isatty(std.posix.STDIN_FILENO) and std.posix.isatty(std.posix.STDERR_FILENO);
|
||||
}
|
||||
|
||||
/// Numbered TTY picker. With `allow_default`, empty input returns null so
|
||||
/// the caller can substitute its own default; `default_marker_idx` (if set)
|
||||
/// just renders `(default)` next to that row. Errors with NoChoice after
|
||||
/// 3 invalid attempts.
|
||||
fn promptNumberedChoice(header: []const u8, items: []const []const u8, allow_default: bool, default_marker_idx: ?usize) !?usize {
|
||||
var stdin_buf: [128]u8 = undefined;
|
||||
var stdin = std.fs.File.stdin().reader(&stdin_buf);
|
||||
|
||||
var attempt: u8 = 0;
|
||||
while (attempt < 3) : (attempt += 1) {
|
||||
std.debug.print("Pick model for {s} (Enter for default):\n", .{@tagName(provider)});
|
||||
for (ids, 0..) |id, idx| {
|
||||
const marker: []const u8 = if (std.mem.eql(u8, id, default_model)) " (default)" else "";
|
||||
std.debug.print(" {d:>3}) {s}{s}\n", .{ idx + 1, id, marker });
|
||||
std.debug.print("{s}\n", .{header});
|
||||
for (items, 0..) |item, idx| {
|
||||
const marker: []const u8 = if (default_marker_idx) |d| (if (d == idx) " (default)" else "") else "";
|
||||
std.debug.print(" {d:>3}) {s}{s}\n", .{ idx + 1, item, marker });
|
||||
}
|
||||
std.debug.print("> ", .{});
|
||||
|
||||
@@ -1113,19 +1121,18 @@ fn pickModel(
|
||||
};
|
||||
const trimmed = std.mem.trim(u8, line, " \t\r\n");
|
||||
if (trimmed.len == 0) {
|
||||
std.debug.print("Using default: {s}\n", .{default_model});
|
||||
return try allocator.dupe(u8, default_model);
|
||||
if (allow_default) return null;
|
||||
std.debug.print("Invalid input — type a number.\n", .{});
|
||||
continue;
|
||||
}
|
||||
const choice = std.fmt.parseInt(usize, trimmed, 10) catch {
|
||||
std.debug.print("Invalid input — type a number (or press Enter for default).\n", .{});
|
||||
const hint: []const u8 = if (allow_default) " (or press Enter for default)" else "";
|
||||
std.debug.print("Invalid input — type a number{s}.\n", .{hint});
|
||||
continue;
|
||||
};
|
||||
if (choice >= 1 and choice <= ids.len) {
|
||||
return try allocator.dupe(u8, ids[choice - 1]);
|
||||
}
|
||||
if (choice >= 1 and choice <= items.len) return choice - 1;
|
||||
std.debug.print("Out of range.\n", .{});
|
||||
}
|
||||
log.fatal(.app, "could not pick model", .{ .hint = "Pass --model explicitly" });
|
||||
return error.NoChoice;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn executeWithResult(self: *Self, arena: std.mem.Allocator, cmd: Command.Com
|
||||
.natural_language, .comment, .login, .accept_cookies => unreachable,
|
||||
else => return .{ .output = "command has no tool mapping", .failed = true },
|
||||
};
|
||||
if (browser_tools.call(arena, self.tool_executor.session, &self.tool_executor.node_registry, tcv.name, tcv.args)) |output|
|
||||
if (self.tool_executor.callValue(arena, tcv.name, tcv.args)) |output|
|
||||
return .{ .output = output, .failed = false }
|
||||
else |err|
|
||||
return .{ .output = std.fmt.allocPrint(arena, "{s} failed: {s}", .{ tcv.name, @errorName(err) }) catch "tool failed", .failed = true };
|
||||
@@ -60,6 +60,6 @@ pub fn printResult(self: *Self, cmd: Command.Command, result: ExecResult) void {
|
||||
|
||||
fn execExtract(self: *Self, arena: std.mem.Allocator, raw_selector: []const u8) ExecResult {
|
||||
const selector = browser_tools.substituteEnvVars(arena, raw_selector);
|
||||
const result = browser_tools.extractText(arena, self.tool_executor.session, &self.tool_executor.node_registry, selector);
|
||||
const result = self.tool_executor.extractText(arena, selector);
|
||||
return .{ .output = result.text, .failed = result.is_error };
|
||||
}
|
||||
|
||||
@@ -89,5 +89,16 @@ pub fn call(self: *Self, arena: std.mem.Allocator, tool_name: []const u8, argume
|
||||
else
|
||||
null;
|
||||
|
||||
return self.callValue(arena, tool_name, arguments);
|
||||
}
|
||||
|
||||
/// Like `call` but takes an already-parsed JSON value. Skips the
|
||||
/// stringify+reparse for callers (e.g. PandaScript replay) that already
|
||||
/// have a `std.json.Value`.
|
||||
pub fn callValue(self: *Self, arena: std.mem.Allocator, tool_name: []const u8, arguments: ?std.json.Value) browser_tools.ToolError![]const u8 {
|
||||
return browser_tools.call(arena, self.session, &self.node_registry, tool_name, arguments);
|
||||
}
|
||||
|
||||
pub fn extractText(self: *Self, arena: std.mem.Allocator, selector: []const u8) browser_tools.EvalResult {
|
||||
return browser_tools.extractText(arena, self.session, &self.node_registry, selector);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user