Merge pull request #2603 from lightpanda-io/agent_repl_pimp

agent: improve banner
This commit is contained in:
Adrià Arrufat
2026-06-02 11:50:01 +02:00
committed by GitHub
3 changed files with 48 additions and 27 deletions

View File

@@ -158,6 +158,14 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
const remembered: ?settings.Remembered = if (resolve) settings.loadRemembered(allocator) else null;
defer if (remembered) |r| std.zon.parse.free(allocator, r);
// Print the banner before provider resolution so it appears before any
// interactive "Select a provider" prompt. On error paths (missing key /
// no key detected) resolveCredentials prints its own message and the
// banner is skipped.
if (will_repl and (!resolve or settings.wouldResolve(opts, remembered))) {
std.debug.print(Terminal.ansi.bold ++ "\n Lightpanda Agent" ++ Terminal.ansi.reset ++ " " ++ Terminal.ansi.dim ++ "({s})" ++ Terminal.ansi.reset ++ "\n", .{lp.build_config.version});
}
const resolved: ?settings.ResolvedProvider = if (resolve) try settings.resolveCredentials(opts, remembered, will_repl) else null;
const llm: ?Credentials = if (resolved) |r| r.credentials else null;
@@ -179,24 +187,19 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
try allocator.dupe(u8, "");
errdefer allocator.free(model);
if (resolved) |r| switch (r.source) {
.flag => {},
.remembered => std.debug.print(
"Resuming provider {s}, model {s} (remembered in ./.lp-agent.zon). Change with --provider/--model or /provider, /model.\n",
.{ @tagName(r.credentials.provider), model },
),
.detected => std.debug.print(
"Auto-selected provider {s}, model {s}. Set --provider/--model or use /provider, /model to change.\n",
.{ @tagName(r.credentials.provider), model },
),
.picked => {
if (resolved) |r| {
if (r.source == .picked) {
settings.saveRemembered(r.credentials.provider, model);
std.debug.print(
"Selected provider {s}, model {s} (saved to ./.lp-agent.zon). Change with /provider, /model.\n",
.{ @tagName(r.credentials.provider), model },
);
},
};
}
std.debug.print(Terminal.ansi.dim ++ " Provider: {s}, Model: {s} ", .{ @tagName(r.credentials.provider), model });
switch (r.source) {
.flag => {},
.remembered => std.debug.print("(from ./.lp-agent.zon) ", .{}),
.detected => std.debug.print("(auto-selected) ", .{}),
.picked => std.debug.print("(saved to /.lp-agent.zon) ", .{}),
}
std.debug.print("\n\n" ++ Terminal.ansi.reset, .{});
}
const notification: *lp.Notification = try .init(allocator);
errdefer notification.deinit();
@@ -431,21 +434,23 @@ fn runTurn(self: *Agent, input: TurnInput) bool {
}
fn runRepl(self: *Agent) void {
self.terminal.printDimmed("Lightpanda Agent (type '/quit' to exit)", .{});
self.terminal.printDimmed("Tab completes/cycles through commands; the dim grey ghost shows the first match.", .{});
self.terminal.printDimmed("Shift-Tab (or Ctrl-J) inserts a newline — use it inside '''…''' or \"\"\"\"\"\" blocks.", .{});
self.terminal.printDimmed("Type '!' on an empty prompt for JS mode (evaluates against the current page); Esc exits.", .{});
log.debug(.app, "tools loaded", .{ .count = globalTools().len });
if (self.ai_client) |ai_client| {
self.terminal.printDimmed("Provider: {s}, Model: {s}", .{ @tagName(std.meta.activeTag(ai_client)), self.model });
if (self.ai_client) |_| {
self.terminal.printItalic(" Use natural language or slash commands", .{});
} else {
self.terminal.printDimmed("Basic REPL (--no-llm) slash commands only.", .{});
self.terminal.printDimmed("To enable natural-language commands, " ++ llm_setup_hint ++ ".", .{});
self.terminal.printItalic(" Basic REPL (--no-llm) - slash commands only.", .{});
self.terminal.printDimmed(" To enable natural language, " ++ llm_setup_hint ++ ".", .{});
}
self.terminal.printDimmed(" /help to list slash commands\t\t\tTab completes/cycles through commands", .{});
self.terminal.printDimmed(" /quit to exit", .{});
self.terminal.printDimmed(" ! for JS mode (eval against the page)\t\tEsc exits JS mode", .{});
// self.terminal.printInfo("", .{});
log.debug(.app, "tools loaded", .{ .count = globalTools().len });
repl: while (true) {
std.debug.print("\n", .{});
const line = Terminal.readLine("") orelse break;
defer Terminal.freeLine(line);
std.debug.print("\n", .{});
// Slash commands and idle Ctrl-C set the cancel flag without
// clearing V8's terminate state; drain both before the next turn.

View File

@@ -43,6 +43,7 @@ pub const ansi = struct {
pub const reset = "\x1b[0m";
pub const bold = "\x1b[1m";
pub const dim = "\x1b[2m";
pub const italic = "\x1b[3m";
pub const cyan = "\x1b[36m";
pub const green = "\x1b[32m";
pub const yellow = "\x1b[33m";
@@ -1060,6 +1061,11 @@ pub fn printDimmed(self: *Terminal, comptime fmt: []const u8, args: anytype) voi
std.debug.print(ansi.dim ++ fmt ++ ansi.reset ++ "\n", args);
}
pub fn printItalic(self: *Terminal, comptime fmt: []const u8, args: anytype) void {
if (!self.isRepl() and !self.verbosity.atLeast(.medium)) return;
std.debug.print(ansi.italic ++ fmt ++ ansi.reset ++ "\n", args);
}
fn helpLessThan(_: void, a: SlashCommand.Help, b: SlashCommand.Help) bool {
return std.mem.lessThan(u8, a.name, b.name);
}

View File

@@ -39,6 +39,15 @@ pub const ResolvedProvider = struct {
source: enum { flag, remembered, detected, picked },
};
/// Returns true when resolveCredentials would succeed (no error, non-null).
/// Used by callers that need to print a banner before calling resolveCredentials.
pub fn wouldResolve(opts: Config.Agent, remembered: ?Remembered) bool {
if (opts.provider) |p| return zenai.provider.envApiKey(p) != null;
if (remembered) |r| if (zenai.provider.envApiKey(r.provider)) |_| return true;
var buf: [zenai.provider.default_candidates.len]Credentials = undefined;
return zenai.provider.detectKeys(&buf, zenai.provider.default_candidates).len > 0;
}
/// Precedence: `--provider` > remembered (if its key is still set) > first
/// detected. Null means no key at all (the reason is already printed).
pub fn resolveCredentials(opts: Config.Agent, remembered: ?Remembered, allow_pick: bool) !?ResolvedProvider {
@@ -75,7 +84,8 @@ pub fn resolveCredentials(opts: Config.Agent, remembered: ?Remembered, allow_pic
var names: [zenai.provider.default_candidates.len][]const u8 = undefined;
for (found, 0..) |cred, i| names[i] = @tagName(cred.provider);
const idx = Terminal.promptNumberedChoice("Select a provider:", names[0..found.len], 0) catch {
std.debug.print("\n", .{});
const idx = Terminal.promptNumberedChoice(" Select a provider:", names[0..found.len], 0) catch {
return .{ .credentials = found[0], .source = .detected };
};
return .{ .credentials = found[idx], .source = .picked };