V18 / 8d896b8a86e5
ChatCommandRegistry.cs
Lilith/Core/Commands/ChatCommandRegistry.cs
Unchanged from the preceding versionCS
using System.Text.RegularExpressions;
namespace Lilith.Core.Commands;
public sealed record ChatCommandContext(string Arguments, CancellationToken CancellationToken)
{
public void RequireNoArguments() { if (Arguments.Length > 0) throw new ArgumentException("This command takes no arguments."); }
}
public sealed record ChatCommandResult(bool Handled, string? Output = null, bool ExitRequested = false);
public sealed record ChatCommandInfo(string Name, string Description);
/// <summary>Host commands are dispatched locally, never sent to the model as chat.</summary>
public sealed class ChatCommandRegistry
{
private readonly object _sync = new();
private readonly Dictionary<string, (ChatCommandInfo Info, Func<ChatCommandContext, Task<ChatCommandResult>> Handler)> _commands = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyList<ChatCommandInfo> List() { lock (_sync) return _commands.Values.Select(c => c.Info).OrderBy(c => c.Name, StringComparer.Ordinal).ToArray(); }
public string Help() => string.Join("\n", List().Select(c => $"/{c.Name} - {c.Description}"));
public void Register(string name, string description, Func<ChatCommandContext, string?> handler)
{
ArgumentNullException.ThrowIfNull(handler);
RegisterAsync(name, description, context => Task.FromResult(new ChatCommandResult(true, handler(context))));
}
public void RegisterAsync(string name, string description, Func<ChatCommandContext, Task<ChatCommandResult>> handler)
{
ArgumentNullException.ThrowIfNull(handler);
name = name.TrimStart('/');
if (!Regex.IsMatch(name, "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$") || string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Supply a command name and description.");
lock (_sync)
if (!_commands.TryAdd(name, (new(name.ToLowerInvariant(), description), handler))) throw new ArgumentException("Command already registered: " + name);
}
public async Task<ChatCommandResult> ExecuteAsync(string input, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string text = input.TrimStart();
if (!text.StartsWith('/')) return new(false);
int end = 1;
while (end < text.Length && !char.IsWhiteSpace(text[end])) end++;
string name = text[1..end];
Func<ChatCommandContext, Task<ChatCommandResult>>? handler;
lock (_sync) handler = _commands.TryGetValue(name, out var command) ? command.Handler : null;
if (handler is null) return new(true, $"Unknown command /{name}. Use /help.");
var result = await handler(new(text[end..].Trim(), cancellationToken));
return result with { Handled = true };
}
}