Merge pull request #2903 from SillyTavern/extensions-slash-commands
Slash commands to manage Extensions
This commit is contained in:
commit
ff56cb9c2e
|
@ -158,7 +158,7 @@ import {
|
||||||
} from './scripts/utils.js';
|
} from './scripts/utils.js';
|
||||||
import { debounce_timeout } from './scripts/constants.js';
|
import { debounce_timeout } from './scripts/constants.js';
|
||||||
|
|
||||||
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
|
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, initExtensions, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
|
||||||
import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
|
import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
|
||||||
import {
|
import {
|
||||||
tag_map,
|
tag_map,
|
||||||
|
@ -244,6 +244,7 @@ import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCo
|
||||||
import { initInputMarkdown } from './scripts/input-md-formatting.js';
|
import { initInputMarkdown } from './scripts/input-md-formatting.js';
|
||||||
import { AbortReason } from './scripts/util/AbortReason.js';
|
import { AbortReason } from './scripts/util/AbortReason.js';
|
||||||
import { initSystemPrompts } from './scripts/sysprompt.js';
|
import { initSystemPrompts } from './scripts/sysprompt.js';
|
||||||
|
import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
|
||||||
|
|
||||||
//exporting functions and vars for mods
|
//exporting functions and vars for mods
|
||||||
export {
|
export {
|
||||||
|
@ -956,6 +957,8 @@ async function firstLoadInit() {
|
||||||
initCfg();
|
initCfg();
|
||||||
initLogprobs();
|
initLogprobs();
|
||||||
initInputMarkdown();
|
initInputMarkdown();
|
||||||
|
initExtensions();
|
||||||
|
initExtensionSlashCommands();
|
||||||
doDailyExtensionUpdatesCheck();
|
doDailyExtensionUpdatesCheck();
|
||||||
await hideLoader();
|
await hideLoader();
|
||||||
await fixViewport();
|
await fixViewport();
|
||||||
|
|
|
@ -0,0 +1,320 @@
|
||||||
|
import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js';
|
||||||
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||||
|
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||||
|
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||||
|
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
|
||||||
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
|
import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
|
||||||
|
* @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise<string>}
|
||||||
|
*/
|
||||||
|
function getExtensionActionCallback(action) {
|
||||||
|
return async (args, extensionName) => {
|
||||||
|
if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
|
||||||
|
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
|
||||||
|
if (!extensionName) {
|
||||||
|
toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const reload = !isFalseBoolean(args?.reload);
|
||||||
|
const internalExtensionName = findExtension(extensionName);
|
||||||
|
if (!internalExtensionName) {
|
||||||
|
toastr.warning(`Extension ${extensionName} does not exist.`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
|
||||||
|
|
||||||
|
if (action === 'enable' && isEnabled) {
|
||||||
|
toastr.info(`Extension ${extensionName} is already enabled.`);
|
||||||
|
return internalExtensionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'disable' && !isEnabled) {
|
||||||
|
toastr.info(`Extension ${extensionName} is already disabled.`);
|
||||||
|
return internalExtensionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'toggle') {
|
||||||
|
action = isEnabled ? 'disable' : 'enable';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reload) {
|
||||||
|
toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
|
||||||
|
|
||||||
|
// Clear input, so it doesn't stay because the command didn't "finish",
|
||||||
|
// and wait for a bit to both show the toast and let the clear bubble through.
|
||||||
|
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'enable') {
|
||||||
|
await enableExtension(internalExtensionName, reload);
|
||||||
|
} else {
|
||||||
|
await disableExtension(internalExtensionName, reload);
|
||||||
|
}
|
||||||
|
|
||||||
|
toastr.success(`Extension ${extensionName} ${action}d.`);
|
||||||
|
|
||||||
|
|
||||||
|
console.info(`Extension ${action}ed: ${extensionName}`);
|
||||||
|
if (!reload) {
|
||||||
|
console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return internalExtensionName;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an extension by name, allowing omission of the "third-party/" prefix.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the extension to find
|
||||||
|
* @returns {string?} - The matched extension name or undefined if not found
|
||||||
|
*/
|
||||||
|
function findExtension(name) {
|
||||||
|
return extensionNames.find(extName => {
|
||||||
|
return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an array of SlashCommandEnumValue objects based on the extension names.
|
||||||
|
* Each object contains the name of the extension and a description indicating if it is a third-party extension.
|
||||||
|
*
|
||||||
|
* @returns {SlashCommandEnumValue[]} An array of SlashCommandEnumValue objects
|
||||||
|
*/
|
||||||
|
const extensionNamesEnumProvider = () => extensionNames.map(name => {
|
||||||
|
const isThirdParty = name.startsWith('third-party/');
|
||||||
|
if (isThirdParty) name = name.slice('third-party/'.length);
|
||||||
|
|
||||||
|
const description = isThirdParty ? 'third party extension' : null;
|
||||||
|
|
||||||
|
return new SlashCommandEnumValue(name, description, !isThirdParty ? enumTypes.name : enumTypes.enum);
|
||||||
|
});
|
||||||
|
|
||||||
|
export function registerExtensionSlashCommands() {
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'extension-enable',
|
||||||
|
callback: getExtensionActionCallback('enable'),
|
||||||
|
returns: 'The internal extension name',
|
||||||
|
namedArgumentList: [
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'reload',
|
||||||
|
description: 'Whether to reload the page after enabling the extension',
|
||||||
|
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||||
|
defaultValue: 'true',
|
||||||
|
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'Extension name',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: true,
|
||||||
|
enumProvider: extensionNamesEnumProvider,
|
||||||
|
forceEnum: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Enables a specified extension.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
By default, the page will be reloaded automatically, stopping any further commands.<br />
|
||||||
|
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
|
||||||
|
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/extension-enable Summarize</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'extension-disable',
|
||||||
|
callback: getExtensionActionCallback('disable'),
|
||||||
|
returns: 'The internal extension name',
|
||||||
|
namedArgumentList: [
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'reload',
|
||||||
|
description: 'Whether to reload the page after disabling the extension',
|
||||||
|
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||||
|
defaultValue: 'true',
|
||||||
|
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'Extension name',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: true,
|
||||||
|
enumProvider: extensionNamesEnumProvider,
|
||||||
|
forceEnum: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Disables a specified extension.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
By default, the page will be reloaded automatically, stopping any further commands.<br />
|
||||||
|
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
|
||||||
|
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/extension-disable Summarize</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'extension-toggle',
|
||||||
|
callback: async (args, extensionName) => {
|
||||||
|
if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
|
||||||
|
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
|
||||||
|
|
||||||
|
const action = isTrueBoolean(args?.state) ? 'enable' :
|
||||||
|
isFalseBoolean(args?.state) ? 'disable' :
|
||||||
|
'toggle';
|
||||||
|
|
||||||
|
return await getExtensionActionCallback(action)(args, extensionName);
|
||||||
|
},
|
||||||
|
returns: 'The internal extension name',
|
||||||
|
namedArgumentList: [
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'reload',
|
||||||
|
description: 'Whether to reload the page after toggling the extension',
|
||||||
|
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||||
|
defaultValue: 'true',
|
||||||
|
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||||||
|
}),
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'state',
|
||||||
|
description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.',
|
||||||
|
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||||
|
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'Extension name',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: true,
|
||||||
|
enumProvider: extensionNamesEnumProvider,
|
||||||
|
forceEnum: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Toggles the state of a specified extension.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
By default, the page will be reloaded automatically, stopping any further commands.<br />
|
||||||
|
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed.
|
||||||
|
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/extension-toggle Summarize</code></pre>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/extension-toggle Summarize state=true</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'extension-state',
|
||||||
|
callback: async (_, extensionName) => {
|
||||||
|
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
|
||||||
|
const internalExtensionName = findExtension(extensionName);
|
||||||
|
if (!internalExtensionName) {
|
||||||
|
toastr.warning(`Extension ${extensionName} does not exist.`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
|
||||||
|
return String(isEnabled);
|
||||||
|
},
|
||||||
|
returns: 'The state of the extension, whether it is enabled.',
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'Extension name',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: true,
|
||||||
|
enumProvider: extensionNamesEnumProvider,
|
||||||
|
forceEnum: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Returns the state of a specified extension (true if enabled, false if disabled).
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/extension-state Summarize</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'extension-exists',
|
||||||
|
aliases: ['extension-installed'],
|
||||||
|
callback: async (_, extensionName) => {
|
||||||
|
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
|
||||||
|
const exists = findExtension(extensionName) !== undefined;
|
||||||
|
return exists ? 'true' : 'false';
|
||||||
|
},
|
||||||
|
returns: 'Whether the extension exists and is installed.',
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'Extension name',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: true,
|
||||||
|
enumProvider: extensionNamesEnumProvider,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Checks if a specified extension exists.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/extension-exists SillyTavern-LALib</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'reload-page',
|
||||||
|
callback: async () => {
|
||||||
|
toastr.info('Reloading the page...');
|
||||||
|
location.reload();
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
helpString: 'Reloads the current page. All further commands will not be processed.',
|
||||||
|
}));
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
|
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
|
||||||
import { hideLoader, showLoader } from './loader.js';
|
import { showLoader } from './loader.js';
|
||||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||||
import { renderTemplate, renderTemplateAsync } from './templates.js';
|
import { renderTemplate, renderTemplateAsync } from './templates.js';
|
||||||
import { isSubsetOf, setValueByPath } from './utils.js';
|
import { isSubsetOf, setValueByPath } from './utils.js';
|
||||||
|
@ -14,7 +14,9 @@ export {
|
||||||
ModuleWorkerWrapper,
|
ModuleWorkerWrapper,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @type {string[]} */
|
||||||
export let extensionNames = [];
|
export let extensionNames = [];
|
||||||
|
|
||||||
let manifests = {};
|
let manifests = {};
|
||||||
const defaultUrl = 'http://localhost:5100';
|
const defaultUrl = 'http://localhost:5100';
|
||||||
|
|
||||||
|
@ -241,7 +243,7 @@ function onEnableExtensionClick() {
|
||||||
enableExtension(name, false);
|
enableExtension(name, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enableExtension(name, reload = true) {
|
export async function enableExtension(name, reload = true) {
|
||||||
extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
|
extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
|
||||||
stateChanged = true;
|
stateChanged = true;
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
|
@ -252,7 +254,7 @@ async function enableExtension(name, reload = true) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disableExtension(name, reload = true) {
|
export async function disableExtension(name, reload = true) {
|
||||||
extension_settings.disabledExtensions.push(name);
|
extension_settings.disabledExtensions.push(name);
|
||||||
stateChanged = true;
|
stateChanged = true;
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
|
@ -1041,7 +1043,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
|
||||||
await installExtension(url);
|
await installExtension(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
jQuery(async function () {
|
|
||||||
|
|
||||||
|
export async function initExtensions() {
|
||||||
await addExtensionsButtonAndMenu();
|
await addExtensionsButtonAndMenu();
|
||||||
$('#extensionsMenuButton').css('display', 'flex');
|
$('#extensionsMenuButton').css('display', 'flex');
|
||||||
|
|
||||||
|
@ -1060,4 +1064,4 @@ jQuery(async function () {
|
||||||
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
|
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
|
||||||
*/
|
*/
|
||||||
$('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
|
$('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
|
||||||
});
|
}
|
||||||
|
|
Loading…
Reference in New Issue