feat(tts): add descriptive inline menu with action descriptions

- Add value/label support for command arg choices
- TTS menu now shows descriptive title listing each action
- Capitalize button labels (On, Off, Status, etc.)
- Update Telegram, Discord, and Slack handlers to use labels

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Glucksberg 2026-01-25 21:11:08 +00:00 committed by Shadow
parent 145618d625
commit c120aa8a2e
No known key found for this signature in database
6 changed files with 65 additions and 28 deletions

View File

@ -186,9 +186,18 @@ function buildChatCommands(): ChatCommandDefinition[] {
args: [ args: [
{ {
name: "action", name: "action",
description: "on | off | status | provider | limit | summary | audio | help", description: "TTS action",
type: "string", type: "string",
choices: ["on", "off", "status", "provider", "limit", "summary", "audio", "help"], choices: [
{ value: "on", label: "On" },
{ value: "off", label: "Off" },
{ value: "status", label: "Status" },
{ value: "provider", label: "Provider" },
{ value: "limit", label: "Limit" },
{ value: "summary", label: "Summary" },
{ value: "audio", label: "Audio" },
{ value: "help", label: "Help" },
],
}, },
{ {
name: "value", name: "value",
@ -197,7 +206,19 @@ function buildChatCommands(): ChatCommandDefinition[] {
captureRemaining: true, captureRemaining: true,
}, },
], ],
argsMenu: "auto", argsMenu: {
arg: "action",
title:
"TTS Actions:\n" +
"• On Enable TTS for responses\n" +
"• Off Disable TTS\n" +
"• Status Show current settings\n" +
"• Provider Set voice provider (edge, elevenlabs, openai)\n" +
"• Limit Set max characters for TTS\n" +
"• Summary Toggle AI summary for long texts\n" +
"• Audio Generate TTS from custom text\n" +
"• Help Show usage guide",
},
}), }),
defineChatCommand({ defineChatCommand({
key: "whoami", key: "whoami",

View File

@ -255,33 +255,41 @@ function resolveDefaultCommandContext(cfg?: ClawdbotConfig): {
}; };
} }
export type ResolvedCommandArgChoice = { value: string; label: string };
export function resolveCommandArgChoices(params: { export function resolveCommandArgChoices(params: {
command: ChatCommandDefinition; command: ChatCommandDefinition;
arg: CommandArgDefinition; arg: CommandArgDefinition;
cfg?: ClawdbotConfig; cfg?: ClawdbotConfig;
provider?: string; provider?: string;
model?: string; model?: string;
}): string[] { }): ResolvedCommandArgChoice[] {
const { command, arg, cfg } = params; const { command, arg, cfg } = params;
if (!arg.choices) return []; if (!arg.choices) return [];
const provided = arg.choices; const provided = arg.choices;
if (Array.isArray(provided)) return provided; const raw = Array.isArray(provided)
const defaults = resolveDefaultCommandContext(cfg); ? provided
const context: CommandArgChoiceContext = { : (() => {
cfg, const defaults = resolveDefaultCommandContext(cfg);
provider: params.provider ?? defaults.provider, const context: CommandArgChoiceContext = {
model: params.model ?? defaults.model, cfg,
command, provider: params.provider ?? defaults.provider,
arg, model: params.model ?? defaults.model,
}; command,
return provided(context); arg,
};
return provided(context);
})();
return raw.map((choice) =>
typeof choice === "string" ? { value: choice, label: choice } : choice,
);
} }
export function resolveCommandArgMenu(params: { export function resolveCommandArgMenu(params: {
command: ChatCommandDefinition; command: ChatCommandDefinition;
args?: CommandArgs; args?: CommandArgs;
cfg?: ClawdbotConfig; cfg?: ClawdbotConfig;
}): { arg: CommandArgDefinition; choices: string[]; title?: string } | null { }): { arg: CommandArgDefinition; choices: ResolvedCommandArgChoice[]; title?: string } | null {
const { command, args, cfg } = params; const { command, args, cfg } = params;
if (!command.args || !command.argsMenu) return null; if (!command.args || !command.argsMenu) return null;
if (command.argsParsing === "none") return null; if (command.argsParsing === "none") return null;

View File

@ -12,14 +12,16 @@ export type CommandArgChoiceContext = {
arg: CommandArgDefinition; arg: CommandArgDefinition;
}; };
export type CommandArgChoicesProvider = (context: CommandArgChoiceContext) => string[]; export type CommandArgChoice = string | { value: string; label: string };
export type CommandArgChoicesProvider = (context: CommandArgChoiceContext) => CommandArgChoice[];
export type CommandArgDefinition = { export type CommandArgDefinition = {
name: string; name: string;
description: string; description: string;
type: CommandArgType; type: CommandArgType;
required?: boolean; required?: boolean;
choices?: string[] | CommandArgChoicesProvider; choices?: CommandArgChoice[] | CommandArgChoicesProvider;
captureRemaining?: boolean; captureRemaining?: boolean;
}; };

View File

@ -93,16 +93,18 @@ function buildDiscordCommandOptions(params: {
typeof focused?.value === "string" ? focused.value.trim().toLowerCase() : ""; typeof focused?.value === "string" ? focused.value.trim().toLowerCase() : "";
const choices = resolveCommandArgChoices({ command, arg, cfg }); const choices = resolveCommandArgChoices({ command, arg, cfg });
const filtered = focusValue const filtered = focusValue
? choices.filter((choice) => choice.toLowerCase().includes(focusValue)) ? choices.filter((choice) => choice.label.toLowerCase().includes(focusValue))
: choices; : choices;
await interaction.respond( await interaction.respond(
filtered.slice(0, 25).map((choice) => ({ name: choice, value: choice })), filtered.slice(0, 25).map((choice) => ({ name: choice.label, value: choice.value })),
); );
} }
: undefined; : undefined;
const choices = const choices =
resolvedChoices.length > 0 && !autocomplete resolvedChoices.length > 0 && !autocomplete
? resolvedChoices.slice(0, 25).map((choice) => ({ name: choice, value: choice })) ? resolvedChoices
.slice(0, 25)
.map((choice) => ({ name: choice.label, value: choice.value }))
: undefined; : undefined;
return { return {
name: arg.name, name: arg.name,
@ -351,7 +353,11 @@ export function createDiscordCommandArgFallbackButton(params: DiscordCommandArgC
function buildDiscordCommandArgMenu(params: { function buildDiscordCommandArgMenu(params: {
command: ChatCommandDefinition; command: ChatCommandDefinition;
menu: { arg: CommandArgDefinition; choices: string[]; title?: string }; menu: {
arg: CommandArgDefinition;
choices: Array<{ value: string; label: string }>;
title?: string;
};
interaction: CommandInteraction; interaction: CommandInteraction;
cfg: ReturnType<typeof loadConfig>; cfg: ReturnType<typeof loadConfig>;
discordConfig: DiscordConfig; discordConfig: DiscordConfig;
@ -365,11 +371,11 @@ function buildDiscordCommandArgMenu(params: {
const buttons = choices.map( const buttons = choices.map(
(choice) => (choice) =>
new DiscordCommandArgButton({ new DiscordCommandArgButton({
label: choice, label: choice.label,
customId: buildDiscordCommandArgCustomId({ customId: buildDiscordCommandArgCustomId({
command: commandLabel, command: commandLabel,
arg: menu.arg.name, arg: menu.arg.name,
value: choice, value: choice.value,
userId, userId,
}), }),
cfg: params.cfg, cfg: params.cfg,

View File

@ -103,7 +103,7 @@ function buildSlackCommandArgMenuBlocks(params: {
title: string; title: string;
command: string; command: string;
arg: string; arg: string;
choices: string[]; choices: Array<{ value: string; label: string }>;
userId: string; userId: string;
}) { }) {
const rows = chunkItems(params.choices, 5).map((choices) => ({ const rows = chunkItems(params.choices, 5).map((choices) => ({
@ -111,11 +111,11 @@ function buildSlackCommandArgMenuBlocks(params: {
elements: choices.map((choice) => ({ elements: choices.map((choice) => ({
type: "button", type: "button",
action_id: SLACK_COMMAND_ARG_ACTION_ID, action_id: SLACK_COMMAND_ARG_ACTION_ID,
text: { type: "plain_text", text: choice }, text: { type: "plain_text", text: choice.label },
value: encodeSlackCommandArgValue({ value: encodeSlackCommandArgValue({
command: params.command, command: params.command,
arg: params.arg, arg: params.arg,
value: choice, value: choice.value,
userId: params.userId, userId: params.userId,
}), }),
})), })),

View File

@ -366,10 +366,10 @@ export const registerTelegramNativeCommands = ({
rows.push( rows.push(
slice.map((choice) => { slice.map((choice) => {
const args: CommandArgs = { const args: CommandArgs = {
values: { [menu.arg.name]: choice }, values: { [menu.arg.name]: choice.value },
}; };
return { return {
text: choice, text: choice.label,
callback_data: buildCommandTextFromArgs(commandDefinition, args), callback_data: buildCommandTextFromArgs(commandDefinition, args),
}; };
}), }),