repl: categorize help and centralize LLM checks

- Group `/help` output into Browser, LLM, and Meta commands.
- Only show LLM commands in help if an LLM client is configured.
- Centralize the check for commands requiring an LLM in the REPL loop.
- Add descriptions to meta and LLM commands.
This commit is contained in:
Adrià Arrufat
2026-05-22 20:24:24 +02:00
parent 0049480f04
commit d4f4be704d
2 changed files with 50 additions and 19 deletions

View File

@@ -478,15 +478,16 @@ fn runRepl(self: *Agent) void {
},
};
if (cmd.needsLlm() and self.ai_client == null) {
self.terminal.printErrorFmt("/{s} requires an LLM. Drop --no-llm and set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY).", .{@tagName(std.meta.activeTag(cmd))});
continue :repl;
}
switch (cmd) {
.comment => continue :repl,
.login, .acceptCookies => {
if (self.ai_client == null) {
self.terminal.printError("/login and /acceptCookies require an LLM. Drop --no-llm and set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY).");
continue :repl;
}
const prompt = if (cmd == .login) login_prompt else accept_cookies_prompt;
const label: []const u8 = if (cmd == .login) "/login" else "/acceptCookies";
const prompt = if (cmd == .login) login_prompt else accept_cookies_prompt;
_ = self.runTurn(.{ .prompt = prompt, .record_comment = line, .label = label });
},
.tool_call => |tc| {
@@ -527,21 +528,33 @@ fn handleVerbosity(self: *Agent, rest: []const u8) void {
self.terminal.printInfoFmt("verbosity: {s}", .{@tagName(level)});
}
fn helpLessThan(_: void, a: SlashCommand.Help, b: SlashCommand.Help) bool {
return std.mem.lessThan(u8, a.name, b.name);
}
fn printHelpSection(term: *Terminal, header: []const u8, rows: []SlashCommand.Help) void {
if (rows.len == 0) return;
std.sort.pdq(SlashCommand.Help, rows, {}, helpLessThan);
term.printInfo(header);
for (rows) |r| term.printInfoFmt(" /{s} — {s}", .{ r.name, r.description });
}
fn printSlashHelp(self: *Agent, arena: std.mem.Allocator, target: []const u8) void {
if (target.len == 0) {
self.terminal.printInfo("Slash commands (no LLM, REPL only):");
const all = Schema.all();
const sorted = arena.alloc(*const Schema, all.len) catch {
for (all) |s| self.terminal.printInfoFmt(" /{s} — {s}", .{ s.tool_name, firstSentence(s.description) });
self.terminal.printInfo("Meta: /help [name], /quit, /verbosity <low|medium|high>");
return;
};
for (sorted, all) |*p, *s| p.* = s;
std.sort.pdq(*const Schema, sorted, {}, Schema.lessByName);
for (sorted) |s| {
self.terminal.printInfoFmt(" /{s} — {s}", .{ s.tool_name, firstSentence(s.description) });
const browser = arena.alloc(SlashCommand.Help, all.len) catch return;
for (all, browser) |*s, *e| e.* = .{ .name = s.tool_name, .description = firstSentence(s.description) };
printHelpSection(&self.terminal, "Browser commands:", browser);
if (self.ai_client != null) {
const llm = arena.alloc(SlashCommand.Help, SlashCommand.llm_commands.len) catch return;
@memcpy(llm, &SlashCommand.llm_commands);
printHelpSection(&self.terminal, "\nLLM commands:", llm);
}
self.terminal.printInfo("Meta: /help [name], /quit, /verbosity <low|medium|high>");
const meta = arena.alloc(SlashCommand.Help, SlashCommand.meta_commands.len) catch return;
for (SlashCommand.meta_commands, meta) |m, *e| e.* = .{ .name = m.name, .description = m.description };
printHelpSection(&self.terminal, "\nMeta commands:", meta);
return;
}
const lookup = if (target[0] == '/') target[1..] else target;

View File

@@ -23,6 +23,13 @@
const std = @import("std");
/// Shared row format for the `/help` listing — `name` is the slash name
/// (no `/`), `description` is a single-sentence summary.
pub const Help = struct {
name: []const u8,
description: []const u8,
};
pub const MetaCommand = struct {
tag: Tag,
name: [:0]const u8,
@@ -31,6 +38,9 @@ pub const MetaCommand = struct {
hint: []const u8,
/// Tab-completion candidates for the first positional arg.
values: []const [:0]const u8,
/// First-sentence summary for `/help`; longer detail is rendered by
/// `Agent.printSlashHelp` for the per-command lookup.
description: []const u8,
/// Dispatched by `Agent.handleMeta` via an exhaustive switch so adding
/// a new meta command is a compile error until it's wired up there too.
@@ -38,9 +48,17 @@ pub const MetaCommand = struct {
};
pub const meta_commands = [_]MetaCommand{
.{ .tag = .help, .name = "help", .hint = "", .values = &.{} },
.{ .tag = .quit, .name = "quit", .hint = "", .values = &.{} },
.{ .tag = .verbosity, .name = "verbosity", .hint = "<low|medium|high>", .values = &.{ "low", "medium", "high" } },
.{ .tag = .help, .name = "help", .hint = "", .values = &.{}, .description = "Show help for a slash command, or list all when no name is given" },
.{ .tag = .quit, .name = "quit", .hint = "", .values = &.{}, .description = "Exit the REPL" },
.{ .tag = .verbosity, .name = "verbosity", .hint = "<low|medium|high>", .values = &.{ "low", "medium", "high" }, .description = "Set REPL agent verbosity; bare /verbosity prints the current level" },
};
/// LLM-driven slash commands. Parsed via `script.Command.parse` (they're
/// variants of the `Command` union) — listed here only so the help
/// renderer and completer have a single source of names + descriptions.
pub const llm_commands = [_]Help{
.{ .name = "login", .description = "Log in to the current site using $LP_* env-var credentials" },
.{ .name = "acceptCookies", .description = "Find and dismiss the cookie consent banner" },
};
pub fn findMeta(name: []const u8) ?*const MetaCommand {