diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 0b19c825..261a8742 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -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. diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index a9ab0034..ee8141cd 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -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); } diff --git a/src/agent/settings.zig b/src/agent/settings.zig index f5904124..e324bf5d 100644 --- a/src/agent/settings.zig +++ b/src/agent/settings.zig @@ -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 };