Files
paseo/packages/app/src/plugins/client-slash-commands/model.ts
T
Mohamed Boudra e34aea206e feat(plugins): add live timeline rows and client slash commands
Plugins can transform live timeline source items at render time, append durable plugin rows from daemon handlers, and register client-only slash commands in the composer.

Related to #3940 and #3952.
2026-09-02 13:12:09 +02:00

71 lines
2.4 KiB
TypeScript

export interface SlashCommandDescriptor {
name: string;
aliases?: readonly string[];
}
export type AvailableSlashCommand<
BuiltIn extends SlashCommandDescriptor,
Plugin extends SlashCommandDescriptor,
Provider extends SlashCommandDescriptor,
> =
| { source: "built-in"; command: BuiltIn }
| { source: "plugin"; command: Plugin }
| { source: "provider"; command: Provider };
export function mergeSlashCommandSources<
BuiltIn extends SlashCommandDescriptor,
Plugin extends SlashCommandDescriptor,
Provider extends SlashCommandDescriptor,
>(input: {
builtIn: readonly BuiltIn[];
plugins: readonly Plugin[];
provider: readonly Provider[];
onPluginCollision(command: Plugin, winner: "built-in" | "plugin"): void;
}): Array<AvailableSlashCommand<BuiltIn, Plugin, Provider>> {
const commands: Array<AvailableSlashCommand<BuiltIn, Plugin, Provider>> = input.builtIn.map(
(command) => ({ source: "built-in", command }),
);
const claimed = new Map<string, "built-in" | "plugin">();
for (const command of input.builtIn) {
claimed.set(command.name, "built-in");
for (const alias of command.aliases ?? []) claimed.set(alias, "built-in");
}
for (const command of input.plugins) {
const winner = claimed.get(command.name);
if (winner) {
input.onPluginCollision(command, winner);
continue;
}
claimed.set(command.name, "plugin");
commands.push({ source: "plugin", command });
}
for (const command of input.provider) {
if (!claimed.has(command.name)) commands.push({ source: "provider", command });
}
return commands;
}
export function resolvePluginClientSlashCommand<Command extends SlashCommandDescriptor>(input: {
text: string;
hasAttachments: boolean;
commands: readonly Command[];
}): { command: Command; args: string } | null {
if (input.hasAttachments) return null;
const match = /^\/([^\s]+)(?:\s+([\s\S]*))?$/.exec(input.text.trim());
if (!match) return null;
const command = input.commands.find((candidate) => candidate.name === match[1]);
return command ? { command, args: (match[2] ?? "").trim() } : null;
}
export function executePluginClientSlashCommand(input: {
command: { run(args: string): Promise<void> };
args: string;
onError(error: unknown): void;
}): void {
try {
void input.command.run(input.args).catch(input.onError);
} catch (error) {
input.onError(error);
}
}