// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //! REPL-only meta slash commands (`/help`, `/quit`, `/verbosity`, `/effort`, //! `/stream`, `/usage`, `/model`, `/provider`). Not tool slash commands — handled by //! `Agent.handleMeta`, never reaching the recorder. Tool slash-command schema //! primitives live in `lp.Schema`; import that directly. const std = @import("std"); const lp = @import("lightpanda"); const Command = lp.Command; const Config = lp.Config; /// Row format for the `/help` listing — `name` carries no leading `/`. pub const Help = struct { name: []const u8, description: []const u8, }; pub const MetaCommand = struct { tag: Tag, name: [:0]const u8, /// Ghost-text fragment shown after the name + space. Empty when the command /// takes no args (`/help`, `/quit`). hint: []const u8, /// Tab-completion candidates for the first positional arg. values: []const []const u8, /// Terse one-liner for the `/help` listing; longer per-command detail is /// rendered by `Agent.printSlashHelp`. description: []const u8, /// Dispatched by `Agent.handleMeta` via an exhaustive switch, so a new meta /// command is a compile error until it's wired up there too. const Tag = enum { help, quit, verbosity, effort, stream, usage, clear, reset, save, load, model, provider }; }; const tagNames = Config.tagNames; const tagHint = Config.tagHint; pub const meta_commands = [_]MetaCommand{ .{ .tag = .help, .name = "help", .hint = "[command]", .values = &.{}, .description = "List commands, or show help for one" }, .{ .tag = .quit, .name = "quit", .hint = "", .values = &.{}, .description = "Exit the REPL" }, .{ .tag = .verbosity, .name = "verbosity", .hint = tagHint(Config.AgentVerbosity), .values = tagNames(Config.AgentVerbosity), .description = "Set agent verbosity" }, .{ .tag = .effort, .name = "effort", .hint = tagHint(Config.Effort), .values = tagNames(Config.Effort), .description = "Set per-turn reasoning effort" }, .{ .tag = .stream, .name = "stream", .hint = "[on|off]", .values = &.{ "on", "off" }, .description = "Toggle streaming of assistant text" }, .{ .tag = .usage, .name = "usage", .hint = "", .values = &.{}, .description = "Show token usage and cache stats for this session" }, .{ .tag = .clear, .name = "clear", .hint = "", .values = &.{}, .description = "Clear conversation history and usage (keeps page/cookies)" }, .{ .tag = .reset, .name = "reset", .hint = "", .values = &.{}, .description = "Reset conversation and browser session (drops page/cookies)" }, .{ .tag = .save, .name = "save", .hint = "[filename.js] [prompt]", .values = &.{}, .description = "Save this session to a file" }, .{ .tag = .load, .name = "load", .hint = "", .values = &.{}, .description = "Load and run a script from disk" }, .{ .tag = .model, .name = "model", .hint = "[name]", .values = &.{}, .description = "Change the model" }, .{ .tag = .provider, .name = "provider", .hint = "[name|null]", .values = &.{}, .description = "Change the provider, or 'null' to disable the LLM" }, }; /// Derived from `Command.LlmCommand` — name and description both come from the /// enum, so a new trigger there surfaces here automatically. pub const llm_commands = blk: { const values = std.enums.values(Command.LlmCommand); var rows: [values.len]Help = undefined; for (values, &rows) |lc, *row| row.* = .{ .name = @tagName(lc), .description = lc.description() }; break :blk rows; }; pub fn findMeta(name: []const u8) ?*const MetaCommand { for (&meta_commands) |*m| { if (std.ascii.eqlIgnoreCase(m.name, name)) return m; } return null; } const browser_tools = lp.tools; const llm_values = std.enums.values(Command.LlmCommand); /// Every slash-invocable name: browser tools, LLM triggers, meta commands. pub const all_names: [browser_tools.names.len + meta_commands.len + llm_values.len][]const u8 = blk: { var arr: [browser_tools.names.len + meta_commands.len + llm_values.len][]const u8 = undefined; var idx: usize = 0; for (browser_tools.names) |n| { arr[idx] = n; idx += 1; } for (llm_values) |lc| { arr[idx] = @tagName(lc); idx += 1; } for (meta_commands) |m| { arr[idx] = m.name; idx += 1; } break :blk arr; }; /// Closest command name within two edits, or null — for "did you mean?" on typos. pub fn closestCommand(name: []const u8) ?[]const u8 { var best: ?[]const u8 = null; var best_dist: usize = std.math.maxInt(usize); for (all_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. Returns `maxInt` for inputs longer /// than 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]; }