From c120aa8a2e128d5f9bee65f1b50c470e06983184 Mon Sep 17 00:00:00 2001 From: Glucksberg Date: Sun, 25 Jan 2026 21:11:08 +0000 Subject: [PATCH] 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 --- src/auto-reply/commands-registry.data.ts | 27 ++++++++++++++++--- src/auto-reply/commands-registry.ts | 32 ++++++++++++++--------- src/auto-reply/commands-registry.types.ts | 6 +++-- src/discord/monitor/native-command.ts | 18 ++++++++----- src/slack/monitor/slash.ts | 6 ++--- src/telegram/bot-native-commands.ts | 4 +-- 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 35c00892c..5ba6826fe 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -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", diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index 5bca565f0..f772ac7fc 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -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; diff --git a/src/auto-reply/commands-registry.types.ts b/src/auto-reply/commands-registry.types.ts index c19c9d9a7..5e5bdd8cb 100644 --- a/src/auto-reply/commands-registry.types.ts +++ b/src/auto-reply/commands-registry.types.ts @@ -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; }; diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index 75c9b3b2b..2340da2da 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -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; 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, diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index d1c2a00ca..ae6d61106 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -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, }), })), diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index c33f1e18e..e9d287d0d 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -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), }; }),