Merge pull request #2922 from SillyTavern/send-commands-return-value Update various sending commands to return a value

be103534e4122ada55d1416f8cc802232de59aac

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +255 -71Showing whitespace changes
public/script.js+3 -1
@@ -4785,7 +4785,7 @@ export function removeMacros(str) {
4785 * @param {boolean} [compact] Send as a compact display message.4785 * @param {boolean} [compact] Send as a compact display message.
4786 * @param {string} [name] Name of the user sending the message. Defaults to name1.4786 * @param {string} [name] Name of the user sending the message. Defaults to name1.
4787 * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.4787 * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.
4788 * @returns {Promise<void>} A promise that resolves when the message is inserted.4788 * @returns {Promise<any>} A promise that resolves to the message when it is inserted.
4789 */4789 */
4790export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {4790export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {
4791 messageText = getRegexedString(messageText, regex_placement.USER_INPUT);4791 messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
@@ -4832,6 +4832,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
4832 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);4832 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
4833 await saveChatConditional();4833 await saveChatConditional();
4834 }4834 }
4835
4836 return message;
4835}4837}
48364838
4837/**4839/**
public/scripts/extensions/expressions/index.js+31 -5
@@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
12import { isFunctionCallingSupported } from '../../openai.js';12import { isFunctionCallingSupported } from '../../openai.js';
13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
15import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
16import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
15export { MODULE_NAME };17export { MODULE_NAME };
1618
17const MODULE_NAME = 'expressions';19const MODULE_NAME = 'expressions';
@@ -2134,18 +2136,42 @@ function migrateSettings() {
2134 name: 'classify-expressions',2136 name: 'classify-expressions',
2135 aliases: ['expressions'],2137 aliases: ['expressions'],
2136 callback: async (args) => {2138 callback: async (args) => {
2137 const list = await getExpressionsList();2139 /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
2138 switch (String(args.format).toLowerCase()) {2140 // @ts-ignore
2141 let returnType = args.return;
2142
2143 // Old legacy return type handling
2144 if (args.format) {
2145 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
2146 const type = String(args?.format).toLowerCase().trim();
2147 switch (type) {
2139 case 'json':2148 case 'json':
2140 return JSON.stringify(list);2149 returnType = 'object';
2150 break;
2141 default:2151 default:
2142 return list.join(', ');2152 returnType = 'pipe';
2153 break;
2154 }
2143 }2155 }
2156
2157 // Now the actual new return type handling
2158 const list = await getExpressionsList();
2159
2160 return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
2144 },2161 },
2145 namedArgumentList: [2162 namedArgumentList: [
2146 SlashCommandNamedArgument.fromProps({2163 SlashCommandNamedArgument.fromProps({
2164 name: 'return',
2165 description: 'The way how you want the return value to be provided',
2166 typeList: [ARGUMENT_TYPE.STRING],
2167 defaultValue: 'pipe',
2168 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
2169 forceEnum: true,
2170 }),
2171 // TODO remove some day
2172 SlashCommandNamedArgument.fromProps({
2147 name: 'format',2173 name: 'format',
2148 description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',2174 description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
2149 typeList: [ARGUMENT_TYPE.STRING],2175 typeList: [ARGUMENT_TYPE.STRING],
2150 enumList: [2176 enumList: [
2151 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),2177 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
public/scripts/slash-commands.js+98 -43
@@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
73import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
73export {74export {
74 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,75 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
75};76};
@@ -242,6 +243,7 @@ export function initDefaultSlashCommands() {
242 SlashCommandParser.addCommandObject(SlashCommand.fromProps({243 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
243 name: 'sendas',244 name: 'sendas',
244 callback: sendMessageAs,245 callback: sendMessageAs,
246 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
245 namedArgumentList: [247 namedArgumentList: [
246 SlashCommandNamedArgument.fromProps({248 SlashCommandNamedArgument.fromProps({
247 name: 'name',249 name: 'name',
@@ -269,6 +271,14 @@ export function initDefaultSlashCommands() {
269 typeList: [ARGUMENT_TYPE.NUMBER],271 typeList: [ARGUMENT_TYPE.NUMBER],
270 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),272 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
271 }),273 }),
274 SlashCommandNamedArgument.fromProps({
275 name: 'return',
276 description: 'The way how you want the return value to be provided',
277 typeList: [ARGUMENT_TYPE.STRING],
278 defaultValue: 'none',
279 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
280 forceEnum: true,
281 }),
272 ],282 ],
273 unnamedArgumentList: [283 unnamedArgumentList: [
274 new SlashCommandArgument(284 new SlashCommandArgument(
@@ -301,6 +311,7 @@ export function initDefaultSlashCommands() {
301 name: 'sys',311 name: 'sys',
302 callback: sendNarratorMessage,312 callback: sendNarratorMessage,
303 aliases: ['nar'],313 aliases: ['nar'],
314 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
304 namedArgumentList: [315 namedArgumentList: [
305 new SlashCommandNamedArgument(316 new SlashCommandNamedArgument(
306 'compact',317 'compact',
@@ -316,6 +327,14 @@ export function initDefaultSlashCommands() {
316 typeList: [ARGUMENT_TYPE.NUMBER],327 typeList: [ARGUMENT_TYPE.NUMBER],
317 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),328 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
318 }),329 }),
330 SlashCommandNamedArgument.fromProps({
331 name: 'return',
332 description: 'The way how you want the return value to be provided',
333 typeList: [ARGUMENT_TYPE.STRING],
334 defaultValue: 'none',
335 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
336 forceEnum: true,
337 }),
319 ],338 ],
320 unnamedArgumentList: [339 unnamedArgumentList: [
321 new SlashCommandArgument(340 new SlashCommandArgument(
@@ -355,6 +374,7 @@ export function initDefaultSlashCommands() {
355 SlashCommandParser.addCommandObject(SlashCommand.fromProps({374 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
356 name: 'comment',375 name: 'comment',
357 callback: sendCommentMessage,376 callback: sendCommentMessage,
377 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
358 namedArgumentList: [378 namedArgumentList: [
359 new SlashCommandNamedArgument(379 new SlashCommandNamedArgument(
360 'compact',380 'compact',
@@ -370,6 +390,14 @@ export function initDefaultSlashCommands() {
370 typeList: [ARGUMENT_TYPE.NUMBER],390 typeList: [ARGUMENT_TYPE.NUMBER],
371 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),391 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
372 }),392 }),
393 SlashCommandNamedArgument.fromProps({
394 name: 'return',
395 description: 'The way how you want the return value to be provided',
396 typeList: [ARGUMENT_TYPE.STRING],
397 defaultValue: 'none',
398 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
399 forceEnum: true,
400 }),
373 ],401 ],
374 unnamedArgumentList: [402 unnamedArgumentList: [
375 new SlashCommandArgument(403 new SlashCommandArgument(
@@ -509,7 +537,7 @@ export function initDefaultSlashCommands() {
509 SlashCommandParser.addCommandObject(SlashCommand.fromProps({537 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
510 name: 'ask',538 name: 'ask',
511 callback: askCharacter,539 callback: askCharacter,
512 returns: 'the generated text',540 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
513 namedArgumentList: [541 namedArgumentList: [
514 SlashCommandNamedArgument.fromProps({542 SlashCommandNamedArgument.fromProps({
515 name: 'name',543 name: 'name',
@@ -518,6 +546,14 @@ export function initDefaultSlashCommands() {
518 isRequired: true,546 isRequired: true,
519 enumProvider: commonEnumProviders.characters('character'),547 enumProvider: commonEnumProviders.characters('character'),
520 }),548 }),
549 SlashCommandNamedArgument.fromProps({
550 name: 'return',
551 description: 'The way how you want the return value to be provided',
552 typeList: [ARGUMENT_TYPE.STRING],
553 defaultValue: 'pipe',
554 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
555 forceEnum: true,
556 }),
521 ],557 ],
522 unnamedArgumentList: [558 unnamedArgumentList: [
523 new SlashCommandArgument(559 new SlashCommandArgument(
@@ -556,6 +592,7 @@ export function initDefaultSlashCommands() {
556 SlashCommandParser.addCommandObject(SlashCommand.fromProps({592 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
557 name: 'send',593 name: 'send',
558 callback: sendUserMessageCallback,594 callback: sendUserMessageCallback,
595 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
559 namedArgumentList: [596 namedArgumentList: [
560 new SlashCommandNamedArgument(597 new SlashCommandNamedArgument(
561 'compact',598 'compact',
@@ -578,6 +615,14 @@ export function initDefaultSlashCommands() {
578 defaultValue: '{{user}}',615 defaultValue: '{{user}}',
579 enumProvider: commonEnumProviders.personas,616 enumProvider: commonEnumProviders.personas,
580 }),617 }),
618 SlashCommandNamedArgument.fromProps({
619 name: 'return',
620 description: 'The way how you want the return value to be provided',
621 typeList: [ARGUMENT_TYPE.STRING],
622 defaultValue: 'none',
623 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
624 forceEnum: true,
625 }),
581 ],626 ],
582 unnamedArgumentList: [627 unnamedArgumentList: [
583 new SlashCommandArgument(628 new SlashCommandArgument(
@@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() {
1568 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1613 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1569 name: 'listinjects',1614 name: 'listinjects',
1570 callback: listInjectsCallback,1615 callback: listInjectsCallback,
1571 helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>format</code> argument to change the output format.',1616 helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.',
1572 returns: 'JSON object of script injections',1617 returns: 'Optionalls the JSON object of script injections',
1573 namedArgumentList: [1618 namedArgumentList: [
1574 SlashCommandNamedArgument.fromProps({1619 SlashCommandNamedArgument.fromProps({
1620 name: 'return',
1621 description: 'The way how you want the return value to be provided',
1622 typeList: [ARGUMENT_TYPE.STRING],
1623 defaultValue: 'popup-html',
1624 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
1625 forceEnum: true,
1626 }),
1627 // TODO remove some day
1628 SlashCommandNamedArgument.fromProps({
1575 name: 'format',1629 name: 'format',
1576 description: 'output format',1630 description: '!!! DEPRECATED - use "return" instead !!! output format',
1577 typeList: [ARGUMENT_TYPE.STRING],1631 typeList: [ARGUMENT_TYPE.STRING],
1578 isRequired: true,1632 isRequired: true,
1579 forceEnum: true,1633 forceEnum: true,
@@ -1842,37 +1896,43 @@ function injectCallback(args, value) {
1842}1896}
18431897
1844async function listInjectsCallback(args) {1898async function listInjectsCallback(args) {
1899 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
1900 let returnType = args.return;
1901
1902 // Old legacy return type handling
1903 if (args.format) {
1904 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
1845 const type = String(args?.format).toLowerCase().trim();1905 const type = String(args?.format).toLowerCase().trim();
1846 if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {1906 if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
1847 type !== 'none' && toastr.info('No script injections for the current chat');1907 type !== 'none' && toastr.info('No script injections for the current chat');
1848 return JSON.stringify({});
1849 }1908 }
1850
1851 const injects = Object.entries(chat_metadata.script_injects)
1852 .map(([id, inject]) => {
1853 const position = Object.entries(extension_prompt_types);
1854 const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
1855 return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
1856 })
1857 .join('\n');
1858
1859 const converter = new showdown.Converter();
1860 const messageText = `### Script injections:\n${injects}`;
1861 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));
1862
1863 switch (type) {1909 switch (type) {
1864 case 'none':1910 case 'none':
1911 returnType = 'none';
1865 break;1912 break;
1866 case 'chat':1913 case 'chat':
1867 sendSystemMessage(system_message_types.GENERIC, htmlMessage);1914 returnType = 'chat-html';
1868 break;1915 break;
1869 case 'popup':1916 case 'popup':
1870 default:1917 default:
1871 await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);1918 returnType = 'popup-html';
1872 break;1919 break;
1873 }1920 }
1921 }
18741922
1875 return JSON.stringify(chat_metadata.script_injects);1923 // Now the actual new return type handling
1924 const buildTextValue = (injects) => {
1925 const injectsStr = Object.entries(injects)
1926 .map(([id, inject]) => {
1927 const position = Object.entries(extension_prompt_types);
1928 const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
1929 return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
1930 })
1931 .join('\n');
1932 return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`;
1933 };
1934
1935 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
1876}1936}
18771937
1878/**1938/**
@@ -2559,7 +2619,7 @@ async function askCharacter(args, text) {
2559 // Not supported in group chats2619 // Not supported in group chats
2560 // TODO: Maybe support group chats?2620 // TODO: Maybe support group chats?
2561 if (selected_group) {2621 if (selected_group) {
2562 toastr.error('Cannot run /ask command in a group chat!');2622 toastr.warning('Cannot run /ask command in a group chat!');
2563 return '';2623 return '';
2564 }2624 }
25652625
@@ -2633,7 +2693,9 @@ async function askCharacter(args, text) {
2633 }2693 }
2634 }2694 }
26352695
2636 return askResult;2696 const message = askResult ? chat[chat.length - 1] : null;
2697
2698 return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
2637}2699}
26382700
2639async function hideMessageCallback(_, arg) {2701async function hideMessageCallback(_, arg) {
@@ -2908,7 +2970,7 @@ function findPersonaByName(name) {
29082970
2909async function sendUserMessageCallback(args, text) {2971async function sendUserMessageCallback(args, text) {
2910 if (!text) {2972 if (!text) {
2911 console.warn('WARN: No text provided for /send command');2973 toastr.warning('You must specify text to send');
2912 return;2974 return;
2913 }2975 }
29142976
@@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) {
2924 insertAt = chat.length + insertAt;2986 insertAt = chat.length + insertAt;
2925 }2987 }
29262988
2989 let message;
2927 if ('name' in args) {2990 if ('name' in args) {
2928 const name = args.name || '';2991 const name = args.name || '';
2929 const avatar = findPersonaByName(name) || user_avatar;2992 const avatar = findPersonaByName(name) || user_avatar;
2930 await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);2993 message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
2931 }2994 }
2932 else {2995 else {
2933 await sendMessageAsUser(text, bias, insertAt, compact);2996 message = await sendMessageAsUser(text, bias, insertAt, compact);
2934 }2997 }
29352998
2936 return '';2999 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
2937}3000}
29383001
2939async function deleteMessagesByNameCallback(_, name) {3002async function deleteMessagesByNameCallback(_, name) {
@@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) {
32213284
3222export async function sendMessageAs(args, text) {3285export async function sendMessageAs(args, text) {
3223 if (!text) {3286 if (!text) {
3287 toastr.warning('You must specify text to send as');
3224 return '';3288 return '';
3225 }3289 }
32263290
3227 let name;3291 let name = args.name?.trim();
3228 let mesText;3292 let mesText;
32293293
3230 if (args.name) {3294 if (!name) {
3231 name = args.name.trim();
3232
3233 if (!name && !text) {
3234 toastr.warning('You must specify a name and text to send as');
3235 return '';
3236 }
3237 } else {
3238 const namelessWarningKey = 'sendAsNamelessWarningShown';3295 const namelessWarningKey = 'sendAsNamelessWarningShown';
3239 if (localStorage.getItem(namelessWarningKey) !== 'true') {3296 if (localStorage.getItem(namelessWarningKey) !== 'true') {
3240 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });3297 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
3241 localStorage.setItem(namelessWarningKey, 'true');3298 localStorage.setItem(namelessWarningKey, 'true');
3242 }3299 }
3243 name = name2;3300 name = name2;
3244 if (!text) {
3245 toastr.warning('You must specify text to send as');
3246 return '';
3247 }
3248 }3301 }
32493302
3250 mesText = text.trim();3303 mesText = text.trim();
@@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) {
3321 await saveChatConditional();3374 await saveChatConditional();
3322 }3375 }
33233376
3324 return '';3377 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3325}3378}
33263379
3327export async function sendNarratorMessage(args, text) {3380export async function sendNarratorMessage(args, text) {
3328 if (!text) {3381 if (!text) {
3382 toastr.warning('You must specify text to send');
3329 return '';3383 return '';
3330 }3384 }
33313385
@@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) {
3374 await saveChatConditional();3428 await saveChatConditional();
3375 }3429 }
33763430
3377 return '';3431 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3378}3432}
33793433
3380export async function promptQuietForLoudResponse(who, text) {3434export async function promptQuietForLoudResponse(who, text) {
@@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) {
34203474
3421async function sendCommentMessage(args, text) {3475async function sendCommentMessage(args, text) {
3422 if (!text) {3476 if (!text) {
3477 toastr.warning('You must specify text to send');
3423 return '';3478 return '';
3424 }3479 }
34253480
@@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) {
3462 await saveChatConditional();3517 await saveChatConditional();
3463 }3518 }
34643519
3465 return '';3520 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3466}3521}
34673522
3468/**3523/**
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -36,6 +36,7 @@ export const enumIcons = {
36 message: '💬',36 message: '💬',
37 voice: '🎤',37 voice: '🎤',
38 server: '🖥️',38 server: '🖥️',
39 popup: '🗔',
3940
40 true: '✔️',41 true: '✔️',
41 false: '❌',42 false: '❌',
public/scripts/slash-commands/SlashCommandReturnHelper.js+80 -0
@@ -0,0 +1,80 @@
1import { sendSystemMessage, system_message_types } from '../../script.js';
2import { callGenericPopup, POPUP_TYPE } from '../popup.js';
3import { escapeHtml } from '../utils.js';
4import { enumIcons } from './SlashCommandCommonEnumsProvider.js';
5import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js';
6
7/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */
8
9export const slashCommandReturnHelper = {
10 // Without this, VSCode formatter fucks up JS docs. Don't ask me why.
11 _: false,
12
13 /**
14 * Gets/creates the enum list of types of return relevant for a slash command
15 *
16 * @param {object} [options={}] Options
17 * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value
18 * @param {boolean} [options.allowObject=false] Allow option to return the value as an object
19 * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message
20 * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup
21 * @param {boolean}[options.allowTextVersion=true] Used in combination with chat/popup/toast, some of them do not make sense for text versions, e.g.if you are building a HTML string anyway
22 * @returns {SlashCommandEnumValue[]} The enum list
23 */
24 enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [
25 allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
26 allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
27 allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
28 allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
29 allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
30 allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
31 new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹ️'),
32 allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹ️'),
33 new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'),
34 new SlashCommandEnumValue('none', 'No return value'),
35 ].filter(x => !!x),
36
37 /**
38 * Handles the return value based on the specified type
39 *
40 * @param {SlashCommandReturnType} type The type of return
41 * @param {object|number|string} value The value to return
42 * @param {object} [options={}] Options
43 * @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type
44 * @param {(o: object) => string} [options.objectToHtmlFunc=null] Analog to 'objectToStringFunc', which will be used here if not provided - but can do a different string layout if HTML is requested
45 * @returns {Promise<*>} The processed return value
46 */
47 async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) {
48 const shouldHtml = type.endsWith('html');
49 const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc;
50 const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value;
51
52 switch (type) {
53 case 'popup-html':
54 case 'popup-text':
55 case 'chat-text':
56 case 'chat-html':
57 case 'toast-text':
58 case 'toast-html': {
59 const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
60
61 if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT);
62 if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
63 if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
64
65 return '';
66 }
67 case 'pipe':
68 return stringValue ?? '';
69 case 'object':
70 return JSON.stringify(value);
71 case 'console':
72 console.info(value);
73 return '';
74 case 'none':
75 return '';
76 default:
77 throw new Error(`Unknown return type: ${type}`);
78 }
79 },
80};
public/scripts/variables.js+42 -22
@@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR
11import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';11import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
14import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
14import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';15import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
15import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';16import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1617
@@ -305,7 +306,28 @@ export function replaceVariableMacros(input) {
305}306}
306307
307async function listVariablesCallback(args) {308async function listVariablesCallback(args) {
308 const type = String(args?.format || '').toLowerCase().trim() || 'popup';309 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
310 let returnType = args.return;
311
312 // Old legacy return type handling
313 if (args.format) {
314 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
315 const type = String(args?.format).toLowerCase().trim();
316 switch (type) {
317 case 'none':
318 returnType = 'none';
319 break;
320 case 'chat':
321 returnType = 'chat-html';
322 break;
323 case 'popup':
324 default:
325 returnType = 'popup-html';
326 break;
327 }
328 }
329
330 // Now the actual new return type handling
309 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';331 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
310 if (!chat_metadata.variables) {332 if (!chat_metadata.variables) {
311 chat_metadata.variables = {};333 chat_metadata.variables = {};
@@ -317,35 +339,24 @@ async function listVariablesCallback(args) {
317 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];339 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
318 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];340 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
319341
320 const jsonVariables = [342 const buildTextValue = (_) => {
321 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
322 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
323 ];
324
325 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';343 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
326 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';344 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
327 const chatName = getCurrentChatId();345 const chatName = getCurrentChatId();
328346
329 const converter = new showdown.Converter();
330 const message = [347 const message = [
331 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',348 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
332 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',349 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
333 ].filter(x => x).join('\n\n');350 ].filter(x => x).join('\n\n');
334 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));351 return message;
352 };
335353
336 switch (type) {354 const jsonVariables = [
337 case 'none':355 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
338 break;356 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
339 case 'chat':357 ];
340 sendSystemMessage(system_message_types.GENERIC, htmlMessage);
341 break;
342 case 'popup':
343 default:
344 await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
345 break;
346 }
347358
348 return JSON.stringify(jsonVariables);359 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue });
349}360}
350361
351/**362/**
@@ -916,7 +927,7 @@ export function registerVariableCommands() {
916 name: 'listvar',927 name: 'listvar',
917 callback: listVariablesCallback,928 callback: listVariablesCallback,
918 aliases: ['listchatvar'],929 aliases: ['listchatvar'],
919 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>format</code> argument to change the output format.',930 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>return</code> argument to change the return type.',
920 returns: 'JSON list of local variables',931 returns: 'JSON list of local variables',
921 namedArgumentList: [932 namedArgumentList: [
922 SlashCommandNamedArgument.fromProps({933 SlashCommandNamedArgument.fromProps({
@@ -933,8 +944,17 @@ export function registerVariableCommands() {
933 ],944 ],
934 }),945 }),
935 SlashCommandNamedArgument.fromProps({946 SlashCommandNamedArgument.fromProps({
947 name: 'return',
948 description: 'The way how you want the return value to be provided',
949 typeList: [ARGUMENT_TYPE.STRING],
950 defaultValue: 'popup-html',
951 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
952 forceEnum: true,
953 }),
954 // TODO remove some day
955 SlashCommandNamedArgument.fromProps({
936 name: 'format',956 name: 'format',
937 description: 'output format',957 description: '!!! DEPRECATED - use "return" instead !!! output format',
938 typeList: [ARGUMENT_TYPE.STRING],958 typeList: [ARGUMENT_TYPE.STRING],
939 isRequired: true,959 isRequired: true,
940 forceEnum: true,960 forceEnum: true,