Move default slash commands to respective module (#4240) * Move default slash commands to respective module * Update public/scripts/slash-commands.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

28189bb1c780130bf473345c05a269af2ec84596

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

Signed
3 files changed, +565 -577Showing whitespace changes
public/script.js+5 -559
@@ -1,7 +1,6 @@
11import {
22 showdown,
33 moment,
4- Fuse,
54 DOMPurify,
65 hljs,
76 Handlebars,
@@ -89,7 +88,6 @@ import {
8988 sortEntitiesList,
9089 registerDebugFunction,
9190 flushEphemeralStoppingStrings,
92- context_presets,
9391 resetMovableStyles,
9492 forceCharacterEditorTokenize,
9593 applyPowerUserSettings,
@@ -167,7 +165,6 @@ import {
167165 isValidUrl,
168166 ensureImageFormatSupported,
169167 flashHighlight,
170- isTrueBoolean,
171168 toggleDrawer,
172169 isElementInViewport,
173170 copyText,
@@ -182,7 +179,7 @@ import {
182179import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js';
183180
184181import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
185182import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
186183import {
187184 tag_map,
188185 tags,
@@ -223,9 +220,6 @@ import {
223220 formatInstructModeExamples,
224221 getInstructStoppingSequences,
225222 formatInstructModeSystemPrompt,
226- selectInstructPreset,
227- instruct_presets,
228- selectContextPreset,
229223} from './scripts/instruct-mode.js';
230224import { initLocales, t } from './scripts/i18n.js';
231225import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
@@ -251,16 +245,11 @@ import { currentUser, setUserControls } from './scripts/user.js';
251245import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
252246import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
253247import { initScrapers } from './scripts/scrapers.js';
254-import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
255-import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
256-import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
257248import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
258249import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
259250import { DragAndDropHandler } from './scripts/dragdrop.js';
260251import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
261252import { initDynamicStyles } from './scripts/dynamic-styles.js';
262-import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
263-import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
264253import { initInputMarkdown } from './scripts/input-md-formatting.js';
265254import { AbortReason } from './scripts/util/AbortReason.js';
266255import { initSystemPrompts } from './scripts/sysprompt.js';
@@ -306,6 +295,8 @@ export {
306295 koboldai_setting_names,
307296 novelai_settings,
308297 novelai_setting_names,
298+ UNIQUE_APIS,
299+ CONNECT_API_MAP,
309300};
310301
311302/**
@@ -5257,7 +5248,7 @@ function addChatsSeparator(mesSendString) {
52575248 }
52585249}
52595250
52605251export async function duplicateCharacter() {
52615252 if (this_chid === undefined || !characters[this_chid]) {
52625253 toastr.warning(t`You must first select a character to duplicate!`);
52635254 return '';
@@ -7385,7 +7376,7 @@ export async function getPastCharacterChats(characterId = null) {
73857376/**
73867377 * Helper for `displayPastChats`, to make the same info consistently available for other functions
73877378 */
73887379export function getCurrentChatDetails() {
73897380 if (!characters[this_chid] && !selected_group) {
73907381 return { sessionName: '', group: null, characterName: '', avatarImgURL: '' };
73917382 }
@@ -8931,219 +8922,6 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
89318922}
89328923
89338924/**
8934- * @typedef {object} ConnectAPIMap
8935- * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
8936- * @property {string?} [button] - CSS selector for the API button
8937- * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
8938- * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
8939- */
8940-
8941-/**
8942- * @type {Record<string, ConnectAPIMap>}
8943- */
8944-export const CONNECT_API_MAP = {
8945- // Default APIs not contined inside text gen / chat gen
8946- 'kobold': {
8947- selected: 'kobold',
8948- button: '#api_button',
8949- },
8950- 'horde': {
8951- selected: 'koboldhorde',
8952- },
8953- 'novel': {
8954- selected: 'novel',
8955- button: '#api_button_novel',
8956- },
8957- 'koboldcpp': {
8958- selected: 'textgenerationwebui',
8959- button: '#api_button_textgenerationwebui',
8960- type: textgen_types.KOBOLDCPP,
8961- },
8962- // KoboldCpp alias
8963- 'kcpp': {
8964- selected: 'textgenerationwebui',
8965- button: '#api_button_textgenerationwebui',
8966- type: textgen_types.KOBOLDCPP,
8967- },
8968- 'openai': {
8969- selected: 'openai',
8970- button: '#api_button_openai',
8971- source: chat_completion_sources.OPENAI,
8972- },
8973- // OpenAI alias
8974- 'oai': {
8975- selected: 'openai',
8976- button: '#api_button_openai',
8977- source: chat_completion_sources.OPENAI,
8978- },
8979- // Google alias
8980- 'google': {
8981- selected: 'openai',
8982- button: '#api_button_openai',
8983- source: chat_completion_sources.MAKERSUITE,
8984- },
8985- // OpenRouter special naming, to differentiate between chat comp and text comp
8986- 'openrouter': {
8987- selected: 'openai',
8988- button: '#api_button_openai',
8989- source: chat_completion_sources.OPENROUTER,
8990- },
8991- 'openrouter-text': {
8992- selected: 'textgenerationwebui',
8993- button: '#api_button_textgenerationwebui',
8994- type: textgen_types.OPENROUTER,
8995- },
8996-};
8997-
8998-// Collect all unique API names in an array
8999-export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
9000-
9001-// Fill connections map from textgen_types and chat_completion_sources
9002-for (const textGenType of Object.values(textgen_types)) {
9003- if (CONNECT_API_MAP[textGenType]) continue;
9004- CONNECT_API_MAP[textGenType] = {
9005- selected: 'textgenerationwebui',
9006- button: '#api_button_textgenerationwebui',
9007- type: textGenType,
9008- };
9009-}
9010-for (const chatCompletionSource of Object.values(chat_completion_sources)) {
9011- if (CONNECT_API_MAP[chatCompletionSource]) continue;
9012- CONNECT_API_MAP[chatCompletionSource] = {
9013- selected: 'openai',
9014- button: '#api_button_openai',
9015- source: chatCompletionSource,
9016- };
9017-}
9018-
9019-async function selectContextCallback(args, name) {
9020- if (!name) {
9021- return power_user.context.preset;
9022- }
9023-
9024- const quiet = isTrueBoolean(args?.quiet);
9025- const contextNames = context_presets.map(preset => preset.name);
9026- const fuse = new Fuse(contextNames);
9027- const result = fuse.search(name);
9028-
9029- if (result.length === 0) {
9030- !quiet && toastr.warning(t`Context template '${name}' not found`);
9031- return '';
9032- }
9033-
9034- const foundName = result[0].item;
9035- selectContextPreset(foundName, { quiet: quiet });
9036- return foundName;
9037-}
9038-
9039-async function selectInstructCallback(args, name) {
9040- if (!name) {
9041- return power_user.instruct.enabled || isTrueBoolean(args?.forceGet) ? power_user.instruct.preset : '';
9042- }
9043-
9044- const quiet = isTrueBoolean(args?.quiet);
9045- const instructNames = instruct_presets.map(preset => preset.name);
9046- const fuse = new Fuse(instructNames);
9047- const result = fuse.search(name);
9048-
9049- if (result.length === 0) {
9050- !quiet && toastr.warning(t`Instruct template '${name}' not found`);
9051- return '';
9052- }
9053-
9054- const foundName = result[0].item;
9055- selectInstructPreset(foundName, { quiet: quiet });
9056- return foundName;
9057-}
9058-
9059-async function enableInstructCallback() {
9060- $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change');
9061- return '';
9062-}
9063-
9064-async function disableInstructCallback() {
9065- $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change');
9066- return '';
9067-}
9068-
9069-/**
9070- * @param {string} text API name
9071- */
9072-async function connectAPISlash(args, text) {
9073- if (!text.trim()) {
9074- for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
9075- if (config.selected !== main_api) continue;
9076-
9077- if (config.source) {
9078- if (oai_settings.chat_completion_source === config.source) {
9079- return key;
9080- } else {
9081- continue;
9082- }
9083- }
9084-
9085- if (config.type) {
9086- if (textgen_settings.type === config.type) {
9087- return key;
9088- } else {
9089- continue;
9090- }
9091- }
9092-
9093- return key;
9094- }
9095-
9096- console.error('FIXME: The current API is not in the API map');
9097- return '';
9098- }
9099-
9100- const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
9101- if (!apiConfig) {
9102- toastr.error(t`Error: ${text} is not a valid API`);
9103- return '';
9104- }
9105-
9106- let connectionRequired = false;
9107-
9108- if (main_api !== apiConfig.selected) {
9109- $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
9110- $('#main_api').trigger('change');
9111- connectionRequired = true;
9112- }
9113-
9114- if (apiConfig.source && oai_settings.chat_completion_source !== apiConfig.source) {
9115- $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true);
9116- $('#chat_completion_source').trigger('change');
9117- connectionRequired = true;
9118- }
9119-
9120- if (apiConfig.type && textgen_settings.type !== apiConfig.type) {
9121- $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true);
9122- $('#textgen_type').trigger('change');
9123- connectionRequired = true;
9124- }
9125-
9126- if (connectionRequired && apiConfig.button) {
9127- $(apiConfig.button).trigger('click');
9128- }
9129-
9130- const quiet = isTrueBoolean(args?.quiet);
9131- const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
9132-
9133- try {
9134- if (connectionRequired) {
9135- await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
9136- }
9137- console.log('Connection successful');
9138- } catch {
9139- console.log('Could not connect after 5 seconds, skipping.');
9140- }
9141-
9142- toastr.clear(toast);
9143- return text;
9144-}
9145-
9146-/**
91478925 * Imports supported files dropped into the app window.
91488926 * @param {File[]} files Array of files to process
91498927 * @param {Map<File, string>} [data] Extra data to pass to the import function
@@ -9286,32 +9064,6 @@ async function importFromURL(items, files) {
92869064 }
92879065}
92889066
9289-async function doImpersonate(args, prompt) {
9290- const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {};
9291- const shouldAwait = isTrueBoolean(args?.await);
9292- const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
9293- try {
9294- await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
9295- } catch {
9296- console.warn('Timeout waiting for generation unlock');
9297- toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
9298- return '';
9299- }
9300-
9301- // Prevent generate recursion
9302- $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
9303-
9304- outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1)));
9305- }, 1));
9306-
9307- if (shouldAwait) {
9308- const innerPromise = await outerPromise;
9309- await innerPromise;
9310- }
9311-
9312- return '';
9313-}
9314-
93159067export async function doNewChat({ deleteCurrentChat = false } = {}) {
93169068 //Make a new chat for selected character
93179069 if ((!selected_group && this_chid == undefined) || menu_type == 'create') {
@@ -9346,53 +9098,6 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
93469098
93479099}
93489100
9349-async function doDeleteChat() {
9350- return displayPastChats().then(() => new Promise((resolve) => {
9351- let resolved = false;
9352- const timeOutId = setTimeout(() => {
9353- toastr.error(t`Chat deletion timed out. Please try again.`);
9354- setResolved();
9355- }, 5000);
9356-
9357- const setResolved = () => {
9358- if (resolved) {
9359- return;
9360- }
9361- resolved = true;
9362- [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
9363- eventSource.removeListener(eventType, setResolved);
9364- });
9365- clearTimeout(timeOutId);
9366- resolve('');
9367- };
9368-
9369- [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
9370- eventSource.on(eventType, setResolved);
9371- });
9372-
9373- const currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross');
9374- $(currentChatDeleteButton).trigger('click', { fromSlashCommand: true });
9375- }));
9376-}
9377-
9378-async function doRenameChat(_, chatName) {
9379- if (!chatName) {
9380- toastr.warning(t`Name must be provided as an argument to rename this chat.`);
9381- return '';
9382- }
9383-
9384- const currentChatName = getCurrentChatId();
9385- if (!currentChatName) {
9386- toastr.warning(t`No chat selected that can be renamed.`);
9387- return '';
9388- }
9389-
9390- await renameChat(currentChatName, chatName);
9391-
9392- toastr.success(t`Successfully renamed chat to: ${chatName}`);
9393- return '';
9394-}
9395-
93969101/**
93979102 * Renames a group or character chat.
93989103 * @param {object} param Parameters for renaming chat
@@ -9506,12 +9211,6 @@ export async function updateRemoteChatName(characterId, newName) {
95069211 }
95079212}
95089213
9509-/**
9510- * /getchatname` slash command
9511- */
9512-async function doGetChatName() {
9513- return getCurrentChatDetails().sessionName;
9514-}
95159214
95169215function doCharListDisplaySwitch() {
95179216 power_user.charListGrid = !power_user.charListGrid;
@@ -9519,11 +9218,6 @@ function doCharListDisplaySwitch() {
95199218 saveSettingsDebounced();
95209219}
95219220
9522-function doCloseChat() {
9523- $('#option_close_chat').trigger('click');
9524- return '';
9525-}
9526-
95279221/**
95289222 * Function to handle the deletion of a character, given a specific popup type and character ID.
95299223 * If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function.
@@ -9634,11 +9328,6 @@ export async function newAssistantChat({ temporary = false } = {}) {
96349328 sendSystemMessage(system_message_types.ASSISTANT_NOTE);
96359329}
96369330
9637-function doTogglePanels() {
9638- $('#option_settings').trigger('click');
9639- return '';
9640-}
9641-
96429331/**
96439332 * Event handler to open a navbar drawer when a drawer open button is clicked.
96449333 * Handles click events on .drawer-opener elements.
@@ -9776,249 +9465,6 @@ API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api
97769465
97779466// MARK: DOM Handlers Start
97789467jQuery(async function () {
9779- async function doForceSave() {
9780- await saveSettings();
9781- await saveChatConditional();
9782- toastr.success('Chat and settings saved.');
9783- return '';
9784- }
9785-
9786- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9787- name: 'dupe',
9788- callback: duplicateCharacter,
9789- helpString: 'Duplicates the currently selected character.',
9790- }));
9791- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9792- name: 'api',
9793- callback: connectAPISlash,
9794- returns: 'the current API',
9795- namedArgumentList: [
9796- SlashCommandNamedArgument.fromProps({
9797- name: 'quiet',
9798- description: 'Suppress the toast message on connection',
9799- typeList: [ARGUMENT_TYPE.BOOLEAN],
9800- defaultValue: 'false',
9801- enumList: commonEnumProviders.boolean('trueFalse')(),
9802- }),
9803- ],
9804- unnamedArgumentList: [
9805- SlashCommandArgument.fromProps({
9806- description: 'API to connect to',
9807- typeList: [ARGUMENT_TYPE.STRING],
9808- enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
9809- new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
9810- selected[0].toUpperCase() ?? enumIcons.default)),
9811- }),
9812- ],
9813- helpString: `
9814- <div>
9815- Connect to an API. If no argument is provided, it will return the currently connected API.
9816- </div>
9817- <div>
9818- <strong>Available APIs:</strong>
9819- <pre><code>${Object.keys(CONNECT_API_MAP).join(', ')}</code></pre>
9820- </div>
9821- `,
9822- }));
9823- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9824- name: 'impersonate',
9825- callback: doImpersonate,
9826- aliases: ['imp'],
9827- namedArgumentList: [
9828- new SlashCommandNamedArgument(
9829- 'await',
9830- 'Whether to await for the triggered generation before continuing',
9831- [ARGUMENT_TYPE.BOOLEAN],
9832- false,
9833- false,
9834- 'false',
9835- ),
9836- ],
9837- unnamedArgumentList: [
9838- new SlashCommandArgument(
9839- 'prompt', [ARGUMENT_TYPE.STRING], false,
9840- ),
9841- ],
9842- helpString: `
9843- <div>
9844- Calls an impersonation response, with an optional additional prompt.
9845- </div>
9846- <div>
9847- If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.
9848- </div>
9849- <div>
9850- <strong>Example:</strong>
9851- <ul>
9852- <li>
9853- <pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
9854- </li>
9855- </ul>
9856- </div>
9857- `,
9858- }));
9859- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9860- name: 'delchat',
9861- callback: doDeleteChat,
9862- helpString: 'Deletes the current chat.',
9863- }));
9864- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9865- name: 'renamechat',
9866- callback: doRenameChat,
9867- unnamedArgumentList: [
9868- new SlashCommandArgument(
9869- 'new chat name', [ARGUMENT_TYPE.STRING], true,
9870- ),
9871- ],
9872- helpString: 'Renames the current chat.',
9873- }));
9874- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9875- name: 'getchatname',
9876- callback: doGetChatName,
9877- returns: 'chat file name',
9878- helpString: 'Returns the name of the current chat file into the pipe.',
9879- }));
9880- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9881- name: 'closechat',
9882- callback: doCloseChat,
9883- helpString: 'Closes the current chat.',
9884- }));
9885- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9886- name: 'tempchat',
9887- callback: () => {
9888- return new Promise((resolve, reject) => {
9889- const eventCallback = async (chatId) => {
9890- if (chatId) {
9891- return reject('Not in a temporary chat');
9892- }
9893- await newAssistantChat({ temporary: true });
9894- return resolve('');
9895- };
9896- eventSource.once(event_types.CHAT_CHANGED, eventCallback);
9897- doCloseChat();
9898- setTimeout(() => {
9899- reject('Failed to open temporary chat');
9900- eventSource.removeListener(event_types.CHAT_CHANGED, eventCallback);
9901- }, debounce_timeout.relaxed);
9902- });
9903- },
9904- helpString: 'Opens a temporary chat with Assistant.',
9905- }));
9906- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9907- name: 'panels',
9908- callback: doTogglePanels,
9909- aliases: ['togglepanels'],
9910- helpString: 'Toggle UI panels on/off',
9911- }));
9912- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9913- name: 'forcesave',
9914- callback: doForceSave,
9915- helpString: 'Forces a save of the current chat and settings',
9916- }));
9917- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9918- name: 'instruct',
9919- callback: selectInstructCallback,
9920- returns: 'current template',
9921- namedArgumentList: [
9922- SlashCommandNamedArgument.fromProps({
9923- name: 'quiet',
9924- description: 'Suppress the toast message on template change',
9925- typeList: [ARGUMENT_TYPE.BOOLEAN],
9926- defaultValue: 'false',
9927- enumList: commonEnumProviders.boolean('trueFalse')(),
9928- }),
9929- SlashCommandNamedArgument.fromProps({
9930- name: 'forceGet',
9931- description: 'Force getting a name even if instruct mode is disabled',
9932- typeList: [ARGUMENT_TYPE.BOOLEAN],
9933- defaultValue: 'false',
9934- enumList: commonEnumProviders.boolean('trueFalse')(),
9935- }),
9936- ],
9937- unnamedArgumentList: [
9938- SlashCommandArgument.fromProps({
9939- description: 'instruct template name',
9940- typeList: [ARGUMENT_TYPE.STRING],
9941- enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
9942- }),
9943- ],
9944- helpString: `
9945- <div>
9946- Selects instruct mode template by name. Enables instruct mode if not already enabled.
9947- Gets the current instruct template if no name is provided and instruct mode is enabled or <code>forceGet=true</code> is passed.
9948- </div>
9949- <div>
9950- <strong>Example:</strong>
9951- <ul>
9952- <li>
9953- <pre><code class="language-stscript">/instruct creative</code></pre>
9954- </li>
9955- </ul>
9956- </div>
9957- `,
9958- }));
9959- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9960- name: 'instruct-on',
9961- callback: enableInstructCallback,
9962- helpString: 'Enables instruct mode.',
9963- }));
9964- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9965- name: 'instruct-off',
9966- callback: disableInstructCallback,
9967- helpString: 'Disables instruct mode',
9968- }));
9969- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9970- name: 'instruct-state',
9971- aliases: ['instruct-toggle'],
9972- helpString: 'Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.',
9973- unnamedArgumentList: [
9974- SlashCommandArgument.fromProps({
9975- description: 'instruct mode state',
9976- typeList: [ARGUMENT_TYPE.BOOLEAN],
9977- enumList: commonEnumProviders.boolean('trueFalse')(),
9978- }),
9979- ],
9980- callback: async (_args, state) => {
9981- if (!state || typeof state !== 'string') {
9982- return String(power_user.instruct.enabled);
9983- }
9984-
9985- const newState = isTrueBoolean(state);
9986- newState ? enableInstructCallback() : disableInstructCallback();
9987- return String(power_user.instruct.enabled);
9988- },
9989- }));
9990- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9991- name: 'context',
9992- callback: selectContextCallback,
9993- returns: 'template name',
9994- namedArgumentList: [
9995- SlashCommandNamedArgument.fromProps({
9996- name: 'quiet',
9997- description: 'Suppress the toast message on template change',
9998- typeList: [ARGUMENT_TYPE.BOOLEAN],
9999- defaultValue: 'false',
10000- enumList: commonEnumProviders.boolean('trueFalse')(),
10001- }),
10002- ],
10003- unnamedArgumentList: [
10004- SlashCommandArgument.fromProps({
10005- description: 'context template name',
10006- typeList: [ARGUMENT_TYPE.STRING],
10007- enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
10008- }),
10009- ],
10010- helpString: 'Selects context template by name. Gets the current template if no name is provided',
10011- }));
10012- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10013- name: 'chat-manager',
10014- callback: () => {
10015- $('#option_select_chat').trigger('click');
10016- return '';
10017- },
10018- aliases: ['chat-history', 'manage-chats'],
10019- helpString: 'Opens the chat manager for the current character/group.',
10020- }));
10021-
100229468 setTimeout(function () {
100239469 $('#groupControlsToggle').trigger('click');
100249470 $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click');
public/scripts/extensions/shared.js+1 -1
@@ -471,7 +471,7 @@ export class ConnectionManagerRequestService {
471471
472472 /**
473473 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
474474 * @return {import('../../scriptslash-commands.js').ConnectAPIMap}
475475 * @throws {Error}
476476 */
477477 static validateProfile(profile) {
public/scripts/slash-commands.js+559 -17
@@ -3,7 +3,6 @@ import { copyText, flashHighlight } from './utils.js';
33
44import {
55 Generate,
6- UNIQUE_APIS,
76 activateSendButtons,
87 addOneMessage,
98 characters,
@@ -13,6 +12,8 @@ import {
1312 deactivateSendButtons,
1413 default_avatar,
1514 deleteSwipe,
15+ displayPastChats,
16+ duplicateCharacter,
1617 eventSource,
1718 event_types,
1819 extension_prompt_roles,
@@ -20,6 +21,8 @@ import {
2021 extractMessageBias,
2122 generateQuietPrompt,
2223 generateRaw,
24+ getCurrentChatDetails,
25+ getCurrentChatId,
2326 getFirstDisplayedMessageId,
2427 getThumbnailUrl,
2528 is_send_press,
@@ -27,10 +30,14 @@ import {
2730 name1,
2831 name2,
2932 neutralCharacterName,
33+ newAssistantChat,
34+ online_status,
3035 reloadCurrentChat,
3136 removeMacros,
3237 renameCharacter,
38+ renameChat,
3339 saveChatConditional,
40+ saveSettings,
3441 saveSettingsDebounced,
3542 sendMessageAsUser,
3643 sendSystemMessage,
@@ -56,7 +63,7 @@ import { getRegexedString, regex_placement } from './extensions/regex/engine.js'
5663import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';
5764import { chat_completion_sources, oai_settings, promptManager } from './openai.js';
5865import { user_avatar } from './personas.js';
5966import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, power_user } from './power-user.js';
6067import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
6168import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
6269import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
@@ -80,6 +87,8 @@ import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugC
8087import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
8188import { t } from './i18n.js';
8289import { kai_settings } from './kai-settings.js';
90+import { instruct_presets, selectContextPreset, selectInstructPreset } from './instruct-mode.js';
91+import { debounce_timeout } from './constants.js';
8392export {
8493 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
8594};
@@ -110,7 +119,536 @@ function closureToFilter(closure) {
110119 };
111120}
112121
122+/**
123+ * @typedef {object} ConnectAPIMap
124+ * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
125+ * @property {string?} [button] - CSS selector for the API button
126+ * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
127+ * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
128+ */
129+
130+/** @type {Record<string, ConnectAPIMap>} */
131+export const CONNECT_API_MAP = {};
132+
133+/** @type {string[]} */
134+export const UNIQUE_APIS = [];
135+
136+function setupConnectAPIMap() {
137+ /** @type {Record<string, ConnectAPIMap>} */
138+ const result = {
139+ // Default APIs not contained inside text gen / chat gen
140+ 'kobold': {
141+ selected: 'kobold',
142+ button: '#api_button',
143+ },
144+ 'horde': {
145+ selected: 'koboldhorde',
146+ },
147+ 'novel': {
148+ selected: 'novel',
149+ button: '#api_button_novel',
150+ },
151+ 'koboldcpp': {
152+ selected: 'textgenerationwebui',
153+ button: '#api_button_textgenerationwebui',
154+ type: textgen_types.KOBOLDCPP,
155+ },
156+ // KoboldCpp alias
157+ 'kcpp': {
158+ selected: 'textgenerationwebui',
159+ button: '#api_button_textgenerationwebui',
160+ type: textgen_types.KOBOLDCPP,
161+ },
162+ 'openai': {
163+ selected: 'openai',
164+ button: '#api_button_openai',
165+ source: chat_completion_sources.OPENAI,
166+ },
167+ // OpenAI alias
168+ 'oai': {
169+ selected: 'openai',
170+ button: '#api_button_openai',
171+ source: chat_completion_sources.OPENAI,
172+ },
173+ // Google alias
174+ 'google': {
175+ selected: 'openai',
176+ button: '#api_button_openai',
177+ source: chat_completion_sources.MAKERSUITE,
178+ },
179+ // OpenRouter special naming, to differentiate between chat comp and text comp
180+ 'openrouter': {
181+ selected: 'openai',
182+ button: '#api_button_openai',
183+ source: chat_completion_sources.OPENROUTER,
184+ },
185+ 'openrouter-text': {
186+ selected: 'textgenerationwebui',
187+ button: '#api_button_textgenerationwebui',
188+ type: textgen_types.OPENROUTER,
189+ },
190+ };
191+
192+ // Fill connections map from textgen_types and chat_completion_sources
193+ for (const textGenType of Object.values(textgen_types)) {
194+ if (result[textGenType]) continue;
195+ result[textGenType] = {
196+ selected: 'textgenerationwebui',
197+ button: '#api_button_textgenerationwebui',
198+ type: textGenType,
199+ };
200+ }
201+
202+ for (const chatCompletionSource of Object.values(chat_completion_sources)) {
203+ if (result[chatCompletionSource]) continue;
204+ result[chatCompletionSource] = {
205+ selected: 'openai',
206+ button: '#api_button_openai',
207+ source: chatCompletionSource,
208+ };
209+ }
210+
211+ Object.assign(CONNECT_API_MAP, result);
212+ UNIQUE_APIS.push(...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected)));
213+}
214+
113215export function initDefaultSlashCommands() {
216+ setupConnectAPIMap();
217+
218+ async function enableInstructCallback() {
219+ $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change');
220+ return '';
221+ }
222+
223+ async function disableInstructCallback() {
224+ $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change');
225+ return '';
226+ }
227+
228+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
229+ name: 'dupe',
230+ callback: duplicateCharacter,
231+ helpString: 'Duplicates the currently selected character.',
232+ }));
233+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
234+ name: 'api',
235+ callback: async function (args, text) {
236+ if (!text?.toString()?.trim()) {
237+ for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
238+ if (config.selected !== main_api) continue;
239+
240+ if (config.source) {
241+ if (oai_settings.chat_completion_source === config.source) {
242+ return key;
243+ } else {
244+ continue;
245+ }
246+ }
247+
248+ if (config.type) {
249+ if (textgenerationwebui_settings.type === config.type) {
250+ return key;
251+ } else {
252+ continue;
253+ }
254+ }
255+
256+ return key;
257+ }
258+
259+ console.error('FIXME: The current API is not in the API map');
260+ return '';
261+ }
262+
263+ const apiConfig = CONNECT_API_MAP[text?.toString()?.toLowerCase() ?? ''];
264+ if (!apiConfig) {
265+ toastr.error(t`Error: ${text} is not a valid API`);
266+ return '';
267+ }
268+
269+ let connectionRequired = false;
270+
271+ if (main_api !== apiConfig.selected) {
272+ $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
273+ $('#main_api').trigger('change');
274+ connectionRequired = true;
275+ }
276+
277+ if (apiConfig.source && oai_settings.chat_completion_source !== apiConfig.source) {
278+ $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true);
279+ $('#chat_completion_source').trigger('change');
280+ connectionRequired = true;
281+ }
282+
283+ if (apiConfig.type && textgenerationwebui_settings.type !== apiConfig.type) {
284+ $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true);
285+ $('#textgen_type').trigger('change');
286+ connectionRequired = true;
287+ }
288+
289+ if (connectionRequired && apiConfig.button) {
290+ $(apiConfig.button).trigger('click');
291+ }
292+
293+ const quiet = isTrueBoolean(args?.quiet?.toString());
294+ const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
295+
296+ try {
297+ if (connectionRequired) {
298+ await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
299+ }
300+ console.log('Connection successful');
301+ } catch {
302+ console.log('Could not connect after 5 seconds, skipping.');
303+ }
304+
305+ toastr.clear(toast);
306+ return text?.toString()?.trim() ?? '';
307+ },
308+ returns: 'the current API',
309+ namedArgumentList: [
310+ SlashCommandNamedArgument.fromProps({
311+ name: 'quiet',
312+ description: 'Suppress the toast message on connection',
313+ typeList: [ARGUMENT_TYPE.BOOLEAN],
314+ defaultValue: 'false',
315+ enumList: commonEnumProviders.boolean('trueFalse')(),
316+ }),
317+ ],
318+ unnamedArgumentList: [
319+ SlashCommandArgument.fromProps({
320+ description: 'API to connect to',
321+ typeList: [ARGUMENT_TYPE.STRING],
322+ enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
323+ new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
324+ selected[0].toUpperCase() ?? enumIcons.default)),
325+ }),
326+ ],
327+ helpString: `
328+ <div>
329+ Connect to an API. If no argument is provided, it will return the currently connected API.
330+ </div>
331+ <div>
332+ <strong>Available APIs:</strong>
333+ <pre><code>${Object.keys(CONNECT_API_MAP).sort((a, b) => a.localeCompare(b)).join(', ')}</code></pre>
334+ </div>
335+ `,
336+ }));
337+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
338+ name: 'impersonate',
339+ callback: async function (args, prompt) {
340+ const options = prompt?.toString()?.trim() ? { quiet_prompt: prompt.toString().trim(), quietToLoud: true } : {};
341+ const shouldAwait = isTrueBoolean(args?.await?.toString());
342+ const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
343+ try {
344+ await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
345+ } catch {
346+ console.warn('Timeout waiting for generation unlock');
347+ toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
348+ return '';
349+ }
350+
351+ // Prevent generate recursion
352+ $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
353+
354+ outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1)));
355+ }, 1));
356+
357+ if (shouldAwait) {
358+ const innerPromise = await outerPromise;
359+ await innerPromise;
360+ }
361+
362+ return '';
363+ }
364+ ,
365+ aliases: ['imp'],
366+ namedArgumentList: [
367+ new SlashCommandNamedArgument(
368+ 'await',
369+ 'Whether to await for the triggered generation before continuing',
370+ [ARGUMENT_TYPE.BOOLEAN],
371+ false,
372+ false,
373+ 'false',
374+ ),
375+ ],
376+ unnamedArgumentList: [
377+ new SlashCommandArgument(
378+ 'prompt', [ARGUMENT_TYPE.STRING], false,
379+ ),
380+ ],
381+ helpString: `
382+ <div>
383+ Calls an impersonation response, with an optional additional prompt.
384+ </div>
385+ <div>
386+ If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.
387+ </div>
388+ <div>
389+ <strong>Example:</strong>
390+ <ul>
391+ <li>
392+ <pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
393+ </li>
394+ </ul>
395+ </div>
396+ `,
397+ }));
398+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
399+ name: 'delchat',
400+ callback: async function () {
401+ return displayPastChats().then(() => new Promise((resolve) => {
402+ let resolved = false;
403+ const timeOutId = setTimeout(() => {
404+ toastr.error(t`Chat deletion timed out. Please try again.`);
405+ setResolved();
406+ }, 5000);
407+
408+ const setResolved = () => {
409+ if (resolved) {
410+ return;
411+ }
412+ resolved = true;
413+ [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
414+ eventSource.removeListener(eventType, setResolved);
415+ });
416+ clearTimeout(timeOutId);
417+ resolve('');
418+ };
419+
420+ [event_types.CHAT_DELETED, event_types.GROUP_CHAT_DELETED].forEach((eventType) => {
421+ eventSource.on(eventType, setResolved);
422+ });
423+
424+ const currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross');
425+ $(currentChatDeleteButton).trigger('click', { fromSlashCommand: true });
426+ }));
427+ },
428+ helpString: 'Deletes the current chat.',
429+ }));
430+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
431+ name: 'renamechat',
432+ callback: async function doRenameChat(_, chatName) {
433+ if (!chatName) {
434+ toastr.warning(t`Name must be provided as an argument to rename this chat.`);
435+ return '';
436+ }
437+
438+ const currentChatName = getCurrentChatId();
439+ if (!currentChatName) {
440+ toastr.warning(t`No chat selected that can be renamed.`);
441+ return '';
442+ }
443+
444+ await renameChat(currentChatName, chatName.toString());
445+
446+ toastr.success(t`Successfully renamed chat to: ${chatName}`);
447+ return '';
448+ },
449+ unnamedArgumentList: [
450+ new SlashCommandArgument(
451+ 'new chat name', [ARGUMENT_TYPE.STRING], true,
452+ ),
453+ ],
454+ helpString: 'Renames the current chat.',
455+ }));
456+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
457+ name: 'getchatname',
458+ callback: async function doGetChatName() {
459+ return getCurrentChatDetails().sessionName;
460+ },
461+ returns: 'chat file name',
462+ helpString: 'Returns the name of the current chat file into the pipe.',
463+ }));
464+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
465+ name: 'closechat',
466+ callback: function () {
467+ $('#option_close_chat').trigger('click');
468+ return '';
469+ },
470+ helpString: 'Closes the current chat.',
471+ }));
472+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
473+ name: 'tempchat',
474+ callback: () => {
475+ return new Promise((resolve, reject) => {
476+ const eventCallback = async (chatId) => {
477+ if (chatId) {
478+ return reject('Not in a temporary chat');
479+ }
480+ await newAssistantChat({ temporary: true });
481+ return resolve('');
482+ };
483+ eventSource.once(event_types.CHAT_CHANGED, eventCallback);
484+ $('#option_close_chat').trigger('click');
485+ setTimeout(() => {
486+ reject('Failed to open temporary chat');
487+ eventSource.removeListener(event_types.CHAT_CHANGED, eventCallback);
488+ }, debounce_timeout.relaxed);
489+ });
490+ },
491+ helpString: 'Opens a temporary chat with Assistant.',
492+ }));
493+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
494+ name: 'panels',
495+ callback: function () {
496+ $('#option_settings').trigger('click');
497+ return '';
498+ },
499+ aliases: ['togglepanels'],
500+ helpString: 'Toggle UI panels on/off',
501+ }));
502+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
503+ name: 'forcesave',
504+ callback: async function () {
505+ await saveSettings();
506+ await saveChatConditional();
507+ toastr.success('Chat and settings saved.');
508+ return '';
509+ },
510+ helpString: 'Forces a save of the current chat and settings',
511+ }));
512+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
513+ name: 'instruct',
514+ callback: async function (args, name) {
515+ if (!name) {
516+ return power_user.instruct.enabled || isTrueBoolean(args?.forceGet?.toString()) ? power_user.instruct.preset : '';
517+ }
518+
519+ const quiet = isTrueBoolean(args?.quiet?.toString());
520+ const instructNames = instruct_presets.map(preset => preset.name);
521+ const fuse = new Fuse(instructNames);
522+ const result = fuse.search(name?.toString() ?? '');
523+
524+ if (result.length === 0) {
525+ !quiet && toastr.warning(t`Instruct template '${name}' not found`);
526+ return '';
527+ }
528+
529+ const foundName = result[0].item;
530+ selectInstructPreset(foundName, { quiet: quiet });
531+ return foundName;
532+ },
533+ returns: 'current template',
534+ namedArgumentList: [
535+ SlashCommandNamedArgument.fromProps({
536+ name: 'quiet',
537+ description: 'Suppress the toast message on template change',
538+ typeList: [ARGUMENT_TYPE.BOOLEAN],
539+ defaultValue: 'false',
540+ enumList: commonEnumProviders.boolean('trueFalse')(),
541+ }),
542+ SlashCommandNamedArgument.fromProps({
543+ name: 'forceGet',
544+ description: 'Force getting a name even if instruct mode is disabled',
545+ typeList: [ARGUMENT_TYPE.BOOLEAN],
546+ defaultValue: 'false',
547+ enumList: commonEnumProviders.boolean('trueFalse')(),
548+ }),
549+ ],
550+ unnamedArgumentList: [
551+ SlashCommandArgument.fromProps({
552+ description: 'instruct template name',
553+ typeList: [ARGUMENT_TYPE.STRING],
554+ enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
555+ }),
556+ ],
557+ helpString: `
558+ <div>
559+ Selects instruct mode template by name. Enables instruct mode if not already enabled.
560+ Gets the current instruct template if no name is provided and instruct mode is enabled or <code>forceGet=true</code> is passed.
561+ </div>
562+ <div>
563+ <strong>Example:</strong>
564+ <ul>
565+ <li>
566+ <pre><code class="language-stscript">/instruct creative</code></pre>
567+ </li>
568+ </ul>
569+ </div>
570+ `,
571+ }));
572+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
573+ name: 'instruct-on',
574+ callback: enableInstructCallback,
575+ helpString: 'Enables instruct mode.',
576+ }));
577+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
578+ name: 'instruct-off',
579+ callback: disableInstructCallback,
580+ helpString: 'Disables instruct mode',
581+ }));
582+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
583+ name: 'instruct-state',
584+ aliases: ['instruct-toggle'],
585+ helpString: 'Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.',
586+ unnamedArgumentList: [
587+ SlashCommandArgument.fromProps({
588+ description: 'instruct mode state',
589+ typeList: [ARGUMENT_TYPE.BOOLEAN],
590+ enumList: commonEnumProviders.boolean('trueFalse')(),
591+ }),
592+ ],
593+ callback: async (_args, state) => {
594+ if (!state || typeof state !== 'string') {
595+ return String(power_user.instruct.enabled);
596+ }
597+
598+ const newState = isTrueBoolean(state);
599+ newState ? enableInstructCallback() : disableInstructCallback();
600+ return String(power_user.instruct.enabled);
601+ },
602+ }));
603+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
604+ name: 'context',
605+ callback: async function (args, name) {
606+ if (!name) {
607+ return power_user.context.preset;
608+ }
609+
610+ const quiet = isTrueBoolean(args?.quiet?.toString());
611+ const contextNames = context_presets.map(preset => preset.name);
612+ const fuse = new Fuse(contextNames);
613+ const result = fuse.search(name?.toString() ?? '');
614+
615+ if (result.length === 0) {
616+ !quiet && toastr.warning(t`Context template '${name}' not found`);
617+ return '';
618+ }
619+
620+ const foundName = result[0].item;
621+ selectContextPreset(foundName, { quiet: quiet });
622+ return foundName;
623+ },
624+ returns: 'template name',
625+ namedArgumentList: [
626+ SlashCommandNamedArgument.fromProps({
627+ name: 'quiet',
628+ description: 'Suppress the toast message on template change',
629+ typeList: [ARGUMENT_TYPE.BOOLEAN],
630+ defaultValue: 'false',
631+ enumList: commonEnumProviders.boolean('trueFalse')(),
632+ }),
633+ ],
634+ unnamedArgumentList: [
635+ SlashCommandArgument.fromProps({
636+ description: 'context template name',
637+ typeList: [ARGUMENT_TYPE.STRING],
638+ enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
639+ }),
640+ ],
641+ helpString: 'Selects context template by name. Gets the current template if no name is provided',
642+ }));
643+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
644+ name: 'chat-manager',
645+ callback: () => {
646+ $('#option_select_chat').trigger('click');
647+ return '';
648+ },
649+ aliases: ['chat-history', 'manage-chats'],
650+ helpString: 'Opens the chat manager for the current character/group.',
651+ }));
114652 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
115653 name: '?',
116654 callback: helpCommandCallback,
@@ -3558,7 +4096,7 @@ async function countGroupMemberCallback() {
35584096 return '';
35594097 }
35604098
35614099 return String(getGroupMembers(selected_group).length);
35624100}
35634101
35644102async function removeGroupMemberCallback(_, arg) {
@@ -4684,18 +5222,21 @@ export function stopScriptExecution() {
46845222 */
46855223async function clearCommandProgress() {
46865224 if (isExecutingCommandsFromChatInput) return;
4687- document.querySelector('#send_textarea').style.setProperty('--progDone', '1');
5225+ const ta = document.getElementById('send_textarea');
5226+ const fs = document.getElementById('form_sheld');
5227+ if (!ta || !fs) return;
5228+ ta.style.setProperty('--progDone', '1');
46885229 await delay(250);
46895230 if (isExecutingCommandsFromChatInput) return;
46905231 document.querySelector('#send_textarea')ta.style.transition = 'none';
46915232 await delay(1);
46925233 document.querySelector('#send_textarea')ta.style.setProperty('--prog', '0%');
46935234 document.querySelector('#send_textarea')ta.style.setProperty('--progDone', '0');
46945235 document.querySelector('#form_sheld')fs.classList.remove('script_success');
46955236 document.querySelector('#form_sheld')fs.classList.remove('script_error');
46965237 document.querySelector('#form_sheld')fs.classList.remove('script_aborted');
46975238 await delay(1);
46985239 document.querySelector('#send_textarea')ta.style.transition = null;
46995240}
47005241/**
47015242 * Debounced version of clearCommandProgress.
@@ -4744,17 +5285,18 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
47445285
47455286 /** @type {HTMLTextAreaElement} */
47465287 const ta = document.querySelector('#send_textarea');
5288+ const fs = document.querySelector('#form_sheld');
47475289
47485290 if (options.clearChatInput) {
47495291 ta.value = '';
47505292 ta.dispatchEvent(new Event('input', { bubbles: true }));
47515293 }
47525294
47535295 document.querySelector('#send_textarea')ta.style.setProperty('--prog', '0%');
47545296 document.querySelector('#send_textarea')ta.style.setProperty('--progDone', '0');
47555297 document.querySelector('#form_sheld')fs.classList.remove('script_success');
47565298 document.querySelector('#form_sheld')fs.classList.remove('script_error');
47575299 document.querySelector('#form_sheld')fs.classList.remove('script_aborted');
47585300
47595301 /**@type {SlashCommandClosureResult} */
47605302 let result = null;
@@ -4905,7 +5447,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
49055447 * @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw
49065448 * @param {SlashCommandScope} scope The scope to be used when executing the commands.
49075449 * @param {boolean} handleExecutionErrors Whether to handle execution errors (show toast on error) or throw
49085450 * @param {{[id:import('./slash-commands/SlashCommandParser.js').PARSER_FLAG]:boolean}} parserFlags Parser flags to apply
49095451 * @param {SlashCommandAbortController} abortController Controller used to abort or pause command execution
49105452 * @param {(done:number, total:number)=>void} onProgress Callback to handle progress events
49115453 * @returns {Promise<SlashCommandClosureResult>}