← All source files

V8 / 34b774036cd1

ChatCommandRegistry.cs

Lilith/Core/Commands/ChatCommandRegistry.cs

Unchanged from the preceding version
CS
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 };
    }
}