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: [
{
name: "action",
description: "on | off | status | provider | limit | summary | audio | help",
description: "TTS action",
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",
@ -197,7 +206,19 @@ function buildChatCommands(): ChatCommandDefinition[] {
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({
key: "whoami",

View File

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

View File

@ -12,14 +12,16 @@ export type CommandArgChoiceContext = {
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 = {
name: string;
description: string;
type: CommandArgType;
required?: boolean;
choices?: string[] | CommandArgChoicesProvider;
choices?: CommandArgChoice[] | CommandArgChoicesProvider;
captureRemaining?: boolean;
};

View File

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

View File

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

View File

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