mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
agent: suggest closest slash command on typo
Implements Levenshtein distance-based suggestions for unknown slash commands. If a typo is within two edits of a valid command, the terminal suggests it with "Did you mean ...?".
This commit is contained in:
@@ -813,8 +813,7 @@ fn printSlashHelp(self: *Agent, arena: std.mem.Allocator, target: []const u8) vo
|
||||
printHelpSection(&self.terminal, "\nMeta commands:", meta);
|
||||
return;
|
||||
}
|
||||
const lookup = if (target[0] == '/') target[1..] else target;
|
||||
if (SlashCommand.findMeta(lookup)) |meta| {
|
||||
if (SlashCommand.findMeta(target)) |meta| {
|
||||
switch (meta.tag) {
|
||||
.help => self.terminal.printInfo("/help [name] — show help for a command, or list all when [name] is omitted", .{}),
|
||||
.quit => self.terminal.printInfo("/quit — exit the REPL", .{}),
|
||||
@@ -837,8 +836,12 @@ fn printSlashHelp(self: *Agent, arena: std.mem.Allocator, target: []const u8) vo
|
||||
}
|
||||
return;
|
||||
}
|
||||
const tool_schema = Schema.findByName(lookup) orelse {
|
||||
self.terminal.printError("unknown command: {s}", .{lookup});
|
||||
const tool_schema = Schema.findByName(target) orelse {
|
||||
if (Terminal.closestCommand(target)) |near| {
|
||||
self.terminal.printError("unknown command: {s}. Did you mean " ++ Terminal.highlightCmd("/help {s}") ++ "?", .{ target, near });
|
||||
} else {
|
||||
self.terminal.printError("unknown command: {s}", .{target});
|
||||
}
|
||||
return;
|
||||
};
|
||||
self.terminal.printInfo("/{s} — {s}", .{ tool_schema.tool_name, tool_schema.description });
|
||||
@@ -856,7 +859,12 @@ fn printSlashParseError(self: *Agent, err: Schema.ParseError, name: []const u8,
|
||||
};
|
||||
}
|
||||
const reason: []const u8 = switch (err) {
|
||||
error.UnknownTool => return self.terminal.printError("{s}: unknown command. Try /help.", .{name}),
|
||||
error.UnknownTool => {
|
||||
if (Terminal.closestCommand(name)) |near| {
|
||||
return self.terminal.printError("{s}: unknown command. Did you mean " ++ Terminal.highlightCmd("/{s}") ++ "? Try /help.", .{ name, near });
|
||||
}
|
||||
return self.terminal.printError("{s}: unknown command. Try /help.", .{name});
|
||||
},
|
||||
error.MissingName => return self.terminal.printError("missing command name. Try /help.", .{}),
|
||||
error.MissingRequired => "missing required argument",
|
||||
error.MalformedKv => "malformed key=value. Use key=value or {json}",
|
||||
|
||||
@@ -49,6 +49,12 @@ pub const ansi = struct {
|
||||
pub const clear_eol = "\x1b[K";
|
||||
};
|
||||
|
||||
/// Wraps a comptime format fragment in the bold-cyan style used for command
|
||||
/// names, so highlighted commands match the look of the `/help` listing.
|
||||
pub fn highlightCmd(comptime fragment: []const u8) []const u8 {
|
||||
return ansi.bold ++ ansi.cyan ++ fragment ++ ansi.reset;
|
||||
}
|
||||
|
||||
const Verbosity = Config.AgentVerbosity;
|
||||
|
||||
fn atLeast(level: Verbosity, min: Verbosity) bool {
|
||||
@@ -575,6 +581,39 @@ fn slashHasPrefix(name: []const u8) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Closest command name to `name` within two edits, for "did you mean?"
|
||||
/// suggestions on a typo'd command. Null when nothing is near enough to propose.
|
||||
pub fn closestCommand(name: []const u8) ?[]const u8 {
|
||||
var best: ?[]const u8 = null;
|
||||
var best_dist: usize = std.math.maxInt(usize);
|
||||
for (all_slash_names) |cand| {
|
||||
const dist = editDistance(name, cand);
|
||||
if (dist < best_dist) {
|
||||
best_dist = dist;
|
||||
best = cand;
|
||||
}
|
||||
}
|
||||
return if (best_dist <= 2) best else null;
|
||||
}
|
||||
|
||||
/// Case-insensitive Levenshtein distance via a dynamic-programming table.
|
||||
/// `dp[i][j]` is the edit distance between `a[0..i]` and `b[0..j]`. Returns
|
||||
/// `maxInt` for inputs exceeding the table (no slash command is that long).
|
||||
fn editDistance(a: []const u8, b: []const u8) usize {
|
||||
const max = 32;
|
||||
if (a.len >= max or b.len >= max) return std.math.maxInt(usize);
|
||||
var dp: [max][max]usize = undefined;
|
||||
for (0..a.len + 1) |i| dp[i][0] = i;
|
||||
for (0..b.len + 1) |j| dp[0][j] = j;
|
||||
for (a, 1..) |ca, i| {
|
||||
for (b, 1..) |cb, j| {
|
||||
const cost: usize = if (std.ascii.toLower(ca) == std.ascii.toLower(cb)) 0 else 1;
|
||||
dp[i][j] = @min(@min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + cost);
|
||||
}
|
||||
}
|
||||
return dp[a.len][b.len];
|
||||
}
|
||||
|
||||
fn slashHasParams(name: []const u8) bool {
|
||||
if (Schema.findByName(name)) |s| return s.hints.len > 0;
|
||||
if (SlashCommand.findMeta(name)) |m| return m.hint.len > 0;
|
||||
|
||||
Reference in New Issue
Block a user