separate abort logic for commands

This commit is contained in:
LenAnderson
2024-04-30 09:12:21 -04:00
parent 0ce37981bf
commit f63f4ef304
12 changed files with 450 additions and 85 deletions

View File

@ -155,7 +155,7 @@ import {
} from './scripts/utils.js'; } from './scripts/utils.js';
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js'; import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, processChatSlashCommands, registerSlashCommand } from './scripts/slash-commands.js'; import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
import { import {
tag_map, tag_map,
tags, tags,
@ -1704,6 +1704,7 @@ export async function reloadCurrentChat() {
*/ */
export function sendTextareaMessage() { export function sendTextareaMessage() {
if (is_send_press) return; if (is_send_press) return;
if (isExecutingCommandsFromChatInput) return;
let generateType; let generateType;
// "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
@ -2395,37 +2396,16 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
* Executes slash commands and returns the new text and whether the generation was interrupted. * Executes slash commands and returns the new text and whether the generation was interrupted.
* @param {string} message Text to be sent * @param {string} message Text to be sent
* @returns {Promise<boolean>} Whether the message sending was interrupted * @returns {Promise<boolean>} Whether the message sending was interrupted
* @param {AbortController} abortController
*/ */
async function processCommands(message, abortController) { export async function processCommands(message) {
if (!message || !message.trim().startsWith('/')) { if (!message || !message.trim().startsWith('/')) {
return false; return false;
} }
await executeSlashCommandsOnChatInput(message, {
deactivateSendButtons(); clearChatInput: true,
is_send_press = true;
/**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea');
ta.value = '';
ta.dispatchEvent(new Event('input', { bubbles:true }));
await executeSlashCommandsWithOptions(message, {
abortController: abortController,
onProgress: (done, total)=>ta.style.setProperty('--prog', `${done / total * 100}%`),
}); });
delay(1000).then(()=>clearCommandProgressDebounced());
is_send_press = false;
activateSendButtons();
return true; return true;
} }
function clearCommandProgress() {
if (is_send_press) return;
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
}
const clearCommandProgressDebounced = debounce(clearCommandProgress);
function sendSystemMessage(type, text, extra = {}) { function sendSystemMessage(type, text, extra = {}) {
const systemMessage = system_messages[type]; const systemMessage = system_messages[type];
@ -3088,7 +3068,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `; let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) { if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
const interruptedByCommand = await processCommands(String($('#send_textarea').val()), abortController); const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
if (interruptedByCommand) { if (interruptedByCommand) {
//$("#send_textarea").val('')[0].dispatchEvent(new Event('input', { bubbles:true })); //$("#send_textarea").val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
@ -10188,6 +10168,18 @@ jQuery(async function () {
eventSource.emit(event_types.GENERATION_STOPPED); eventSource.emit(event_types.GENERATION_STOPPED);
}); });
$(document).on('click', '#form_sheld .stscript_continue', function () {
pauseScriptExecution();
});
$(document).on('click', '#form_sheld .stscript_pause', function () {
pauseScriptExecution();
});
$(document).on('click', '#form_sheld .stscript_stop', function () {
stopScriptExecution();
});
$('.drawer-toggle').on('click', function () { $('.drawer-toggle').on('click', function () {
var icon = $(this).find('.drawer-icon'); var icon = $(this).find('.drawer-icon');
var drawer = $(this).parent().find('.drawer-content'); var drawer = $(this).parent().find('.drawer-content');

View File

@ -190,7 +190,7 @@ const init = async () => {
} }
} }
if (qr && qr.onExecute) { if (qr && qr.onExecute) {
return await qr.execute(args); return await qr.execute(args, false, true);
} else { } else {
throw new Error(`No Quick Reply found for "${name}".`); throw new Error(`No Quick Reply found for "${name}".`);
} }

View File

@ -1,5 +1,6 @@
import { POPUP_TYPE, Popup } from '../../../popup.js'; import { POPUP_TYPE, Popup } from '../../../popup.js';
import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { getSortableDelay } from '../../../utils.js'; import { getSortableDelay } from '../../../utils.js';
import { log, warn } from '../index.js'; import { log, warn } from '../index.js';
@ -452,10 +453,23 @@ export class QuickReply {
this.editorPopup.dom.classList.add('qr--hide'); this.editorPopup.dom.classList.add('qr--hide');
} }
try { try {
this.editorExecutePromise = this.execute(); this.editorExecutePromise = this.execute({}, true);
await this.editorExecutePromise; await this.editorExecutePromise;
this.editorExecuteErrors.classList.remove('qr--hasErrors');
this.editorExecuteErrors.innerHTML = '';
} catch (ex) { } catch (ex) {
this.editorExecuteErrors.textContent = ex.message; this.editorExecuteErrors.classList.add('qr--hasErrors');
if (ex instanceof SlashCommandParserError) {
this.editorExecuteErrors.innerHTML = `
<div>${ex.message}</div>
<div>Line: ${ex.line} Column: ${ex.column}</div>
<pre style="text-align:left;">${ex.hint}</pre>
`;
} else {
this.editorExecuteErrors.innerHTML = `
<div>${ex.message}</div>
`;
}
} }
this.editorExecutePromise = null; this.editorExecutePromise = null;
this.editorExecuteBtn.classList.remove('qr--busy'); this.editorExecuteBtn.classList.remove('qr--busy');
@ -537,13 +551,19 @@ export class QuickReply {
} }
async execute(args = {}) { async execute(args = {}, isEditor = false, isRun = false) {
if (this.message?.length > 0 && this.onExecute) { if (this.message?.length > 0 && this.onExecute) {
const scope = new SlashCommandScope(); const scope = new SlashCommandScope();
for (const key of Object.keys(args)) { for (const key of Object.keys(args)) {
scope.setMacro(`arg::${key}`, args[key]); scope.setMacro(`arg::${key}`, args[key]);
} }
return await this.onExecute(this, this.message, args.isAutoExecute ?? false, scope); return await this.onExecute(this, {
message:this.message,
isAutoExecute: args.isAutoExecute ?? false,
isEditor,
isRun,
scope,
});
} }
} }

View File

@ -1,5 +1,5 @@
import { getRequestHeaders, substituteParams } from '../../../../script.js'; import { getRequestHeaders, substituteParams } from '../../../../script.js';
import { executeSlashCommands } from '../../../slash-commands.js'; import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounceAsync, warn } from '../index.js'; import { debounceAsync, warn } from '../index.js';
import { QuickReply } from './QuickReply.js'; import { QuickReply } from './QuickReply.js';
@ -101,16 +101,29 @@ export class QuickReplySet {
/** /**
* @param {QuickReply} qr *
* @param {String} [message] - optional altered message to be used * @param {QuickReply} qr The QR to execute.
* @param {SlashCommandScope} [scope] - optional scope to be used when running the command * @param {object} options
* @param {string} [options.message] (null) altered message to be used
* @param {boolean} [options.isAutoExecute] (false) whether the execution is triggered by auto execute
* @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
* @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
* @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
* @returns
*/ */
async execute(qr, message = null, isAutoExecute = false, scope = null) { async executeWithOptions(qr, options = {}) {
options = Object.assign({
message:null,
isAutoExecute:false,
isEditor:false,
isRun:false,
scope:null,
}, options);
/**@type {HTMLTextAreaElement}*/ /**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea'); const ta = document.querySelector('#send_textarea');
const finalMessage = message ?? qr.message; const finalMessage = options.message ?? qr.message;
let input = ta.value; let input = ta.value;
if (!isAutoExecute && this.injectInput && input.length > 0) { if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) {
if (this.placeBeforeInput) { if (this.placeBeforeInput) {
input = `${finalMessage} ${input}`; input = `${finalMessage} ${input}`;
} else { } else {
@ -121,7 +134,22 @@ export class QuickReplySet {
} }
if (input[0] == '/' && !this.disableSend) { if (input[0] == '/' && !this.disableSend) {
const result = await executeSlashCommands(input, true, scope); let result;
if (options.isAutoExecute || options.isRun) {
result = await executeSlashCommandsWithOptions(input, {
handleParserErrors: true,
scope: options.scope,
});
} else if (options.isEditor) {
result = await executeSlashCommandsWithOptions(input, {
handleParserErrors: false,
scope: options.scope,
});
} else {
result = await executeSlashCommandsOnChatInput(input, {
scope: options.scope,
});
}
return typeof result === 'object' ? result?.pipe : ''; return typeof result === 'object' ? result?.pipe : '';
} }
@ -133,6 +161,18 @@ export class QuickReplySet {
document.querySelector('#send_but').click(); document.querySelector('#send_but').click();
} }
} }
/**
* @param {QuickReply} qr
* @param {String} [message] - optional altered message to be used
* @param {SlashCommandScope} [scope] - optional scope to be used when running the command
*/
async execute(qr, message = null, isAutoExecute = false, scope = null) {
return this.executeWithOptions(qr, {
message,
isAutoExecute,
scope,
});
}
@ -154,7 +194,7 @@ export class QuickReplySet {
} }
hookQuickReply(qr) { hookQuickReply(qr) {
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute); qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
qr.onDelete = ()=>this.removeQuickReply(qr); qr.onDelete = ()=>this.removeQuickReply(qr);
qr.onUpdate = ()=>this.save(); qr.onUpdate = ()=>this.save();
} }

View File

@ -295,6 +295,20 @@
opacity: 0.5; opacity: 0.5;
cursor: wait; cursor: wait;
} }
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors {
display: none;
text-align: left;
font-size: smaller;
background-color: #bd362f;
color: white;
padding: 0.5em;
overflow: auto;
min-width: 100%;
width: 0;
}
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors {
display: block;
}
.shadow_popup.qr--hide { .shadow_popup.qr--hide {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@ -322,6 +322,20 @@
cursor: wait; cursor: wait;
} }
} }
#qr--modal-executeErrors {
display: none;
&.qr--hasErrors {
display: block;
}
text-align: left;
font-size: smaller;
background-color: rgb(189, 54, 47);
color: white;
padding: 0.5em;
overflow: auto;
min-width: 100%;
width: 0;
}
} }
} }
} }

View File

@ -49,7 +49,7 @@ import { autoSelectPersona } from './personas.js';
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js'; import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
import { delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js'; import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js'; import { background_settings } from './backgrounds.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
@ -60,6 +60,7 @@ import { SlashCommandAutoCompleteOption } from './slash-commands/SlashCommandAut
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { SlashCommandAutoComplete } from './slash-commands/SlashCommandAutoComplete.js'; import { SlashCommandAutoComplete } from './slash-commands/SlashCommandAutoComplete.js';
import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
export { export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
}; };
@ -2551,51 +2552,176 @@ function modelCallback(_, model) {
} }
} }
export let isExecutingCommandsFromChatInput = false;
export let commandsFromChatInputAbortController;
/**
* Show command execution pause/stop buttons next to chat input.
*/
export function activateScriptButtons() {
document.querySelector('#form_sheld').classList.add('isExecutingCommandsFromChatInput');
}
/**
* Hide command execution pause/stop buttons next to chat input.
*/
export function deactivateScriptButtons() {
document.querySelector('#form_sheld').classList.remove('isExecutingCommandsFromChatInput');
}
/**
* Toggle pause/continue command execution. Only for commands executed via chat input.
*/
export function pauseScriptExecution() {
if (commandsFromChatInputAbortController) {
if (commandsFromChatInputAbortController.signal.paused) {
commandsFromChatInputAbortController.continue('Clicked pause button');
document.querySelector('#form_sheld').classList.remove('script_paused');
} else {
commandsFromChatInputAbortController.pause('Clicked pause button');
document.querySelector('#form_sheld').classList.add('script_paused');
}
}
}
/**
* Stop command execution. Only for commands executed via chat input.
*/
export function stopScriptExecution() {
commandsFromChatInputAbortController?.abort('Clicked stop button');
}
/**
* Clear up command execution progress bar above chat input.
* @returns Promise<void>
*/
async function clearCommandProgress() {
if (isExecutingCommandsFromChatInput) return;
document.querySelector('#send_textarea').style.setProperty('--progDone', '1');
await delay(250);
if (isExecutingCommandsFromChatInput) return;
document.querySelector('#send_textarea').style.transition = 'none';
await delay(1);
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
document.querySelector('#send_textarea').style.setProperty('--progDone', '0');
document.querySelector('#form_sheld').classList.remove('script_success');
document.querySelector('#form_sheld').classList.remove('script_error');
document.querySelector('#form_sheld').classList.remove('script_aborted');
await delay(1);
document.querySelector('#send_textarea').style.transition = null;
}
/**
* Debounced version of clearCommandProgress.
*/
const clearCommandProgressDebounced = debounce(clearCommandProgress);
/**
* @typedef ExecuteSlashCommandsOptions
* @prop {boolean} [handleParserErrors] (true) Whether to handle parser errors (show toast on error) or throw.
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
* @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
* @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
* @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
*/
/**
* @typedef ExecuteSlashCommandsOnChatInputOptions
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
*/
/**
* Execute slash commands while showing progress indicator and pause/stop buttons on
* chat input.
* @param {string} text Slash command text
* @param {ExecuteSlashCommandsOnChatInputOptions} options
*/
export async function executeSlashCommandsOnChatInput(text, options = {}) {
if (isExecutingCommandsFromChatInput) return null;
options = Object.assign({
scope: null,
parserFlags: null,
clearChatInput: false,
}, options);
isExecutingCommandsFromChatInput = true;
commandsFromChatInputAbortController?.abort('processCommands was called');
activateScriptButtons();
/**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea');
if (options.clearChatInput) {
ta.value = '';
ta.dispatchEvent(new Event('input', { bubbles:true }));
}
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
document.querySelector('#send_textarea').style.setProperty('--progDone', '0');
document.querySelector('#form_sheld').classList.remove('script_success');
document.querySelector('#form_sheld').classList.remove('script_error');
document.querySelector('#form_sheld').classList.remove('script_aborted');
/**@type {SlashCommandClosureResult} */
let result = null;
try {
commandsFromChatInputAbortController = new SlashCommandAbortController();
result = await executeSlashCommandsWithOptions(text, {
abortController: commandsFromChatInputAbortController,
onProgress: (done, total)=>ta.style.setProperty('--prog', `${done / total * 100}%`),
});
if (commandsFromChatInputAbortController.signal.aborted) {
document.querySelector('#form_sheld').classList.add('script_aborted');
} else {
document.querySelector('#form_sheld').classList.add('script_success');
}
} catch (e) {
document.querySelector('#form_sheld').classList.add('script_error');
toastr.error(e.message);
result = new SlashCommandClosureResult();
result.interrupt = true;
result.isError = true;
result.errorMessage = e.message;
} finally {
delay(1000).then(()=>clearCommandProgressDebounced());
commandsFromChatInputAbortController = null;
deactivateScriptButtons();
isExecutingCommandsFromChatInput = false;
}
return result;
}
/** /**
* *
* @param {string} text Slash command txt * @param {string} text Slash command text
* @param {object} [options] * @param {ExecuteSlashCommandsOptions} [options]
* @param {boolean} [options.handleParserErrors]
* @param {SlashCommandScope} [options.scope]
* @param {boolean} [options.handleExecutionErrors]
* @param {PARSER_FLAG[]} [options.parserFlags]
* @param {AbortController} [options.abortController]
* @param {(done:number, total:number)=>void} [options.onProgress]
* @returns {Promise<SlashCommandClosureResult>} * @returns {Promise<SlashCommandClosureResult>}
*/ */
async function executeSlashCommandsWithOptions(text, options = {}) { async function executeSlashCommandsWithOptions(text, options = {}) {
return await executeSlashCommands(
text,
options.handleParserErrors ?? true,
options.scope ?? null,
options.handleExecutionErrors ?? false,
options.parserFlags ?? null,
options.abortController ?? null,
options.onProgress ?? null,
);
}
/**
* Executes slash commands in the provided text
* @param {string} text Slash command text
* @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw
* @param {SlashCommandScope} scope The scope to be used when executing the commands.
* @param {boolean} handleExecutionErrors
* @param {PARSER_FLAG[]} parserFlags
* @param {AbortController} abortController
* @returns {Promise<SlashCommandClosureResult>}
*/
async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false, parserFlags = null, abortController = null, onProgress = null) {
if (!text) { if (!text) {
return null; return null;
} }
options = Object.assign({
handleParserErrors: true,
scope: null,
handleExecutionErrors: false,
parserFlags: null,
abortController: null,
onProgress: null,
}, options);
let closure; let closure;
try { try {
closure = parser.parse(text, true, parserFlags, abortController); closure = parser.parse(text, true, options.parserFlags, options.abortController);
closure.scope.parent = scope; closure.scope.parent = options.scope;
closure.onProgress = onProgress; closure.onProgress = options.onProgress;
} catch (e) { } catch (e) {
if (handleParserErrors && e instanceof SlashCommandParserError) { if (options.handleParserErrors && e instanceof SlashCommandParserError) {
/**@type {SlashCommandParserError}*/ /**@type {SlashCommandParserError}*/
const ex = e; const ex = e;
const toast = ` const toast = `
@ -2624,16 +2750,40 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul
} }
return result; return result;
} catch (e) { } catch (e) {
if (handleExecutionErrors) { if (options.handleExecutionErrors) {
toastr.error(e.message); toastr.error(e.message);
const result = new SlashCommandClosureResult(); const result = new SlashCommandClosureResult();
result.interrupt = true; result.interrupt = true;
result.isError = true;
result.errorMessage = e.message;
return result; return result;
} else { } else {
throw e; throw e;
} }
} }
} }
/**
* Executes slash commands in the provided text
* @deprecated Use executeSlashCommandWithOptions instead
* @param {string} text Slash command text
* @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw
* @param {SlashCommandScope} scope The scope to be used when executing the commands.
* @param {boolean} handleExecutionErrors Whether to handle execution errors (show toast on error) or throw
* @param {PARSER_FLAG[]} parserFlags Parser flags to apply
* @param {SlashCommandAbortController} abortController Controller used to abort or pause command execution
* @param {(done:number, total:number)=>void} onProgress Callback to handle progress events
* @returns {Promise<SlashCommandClosureResult>}
*/
async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false, parserFlags = null, abortController = null, onProgress = null) {
return executeSlashCommandsWithOptions(text, {
handleParserErrors,
scope,
handleExecutionErrors,
parserFlags,
abortController,
onProgress,
});
}
/** /**
* *

View File

@ -0,0 +1,27 @@
export class SlashCommandAbortController {
/**@type {SlashCommandAbortSignal}*/ signal;
constructor() {
this.signal = new SlashCommandAbortSignal();
}
abort(reason = 'No reason.') {
this.signal.aborted = true;
this.signal.reason = reason;
}
pause(reason = 'No reason.') {
this.signal.paused = true;
this.signal.reason = reason;
}
continue(reason = 'No reason.') {
this.signal.paused = false;
this.signal.reason = reason;
}
}
export class SlashCommandAbortSignal {
/**@type {boolean}*/ paused = false;
/**@type {boolean}*/ aborted = false;
/**@type {string}*/ reason = null;
}

View File

@ -1,5 +1,6 @@
import { substituteParams } from '../../script.js'; import { substituteParams } from '../../script.js';
import { escapeRegex } from '../utils.js'; import { delay, escapeRegex } from '../utils.js';
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js'; import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js'; import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js';
@ -14,7 +15,7 @@ export class SlashCommandClosure {
/**@type {Object.<string,string|SlashCommandClosure>}*/ providedArguments = {}; /**@type {Object.<string,string|SlashCommandClosure>}*/ providedArguments = {};
/**@type {SlashCommandExecutor[]}*/ executorList = []; /**@type {SlashCommandExecutor[]}*/ executorList = [];
/**@type {string}*/ keptText; /**@type {string}*/ keptText;
/**@type {AbortController}*/ abortController; /**@type {SlashCommandAbortController}*/ abortController;
/**@type {(done:number, total:number)=>void}*/ onProgress; /**@type {(done:number, total:number)=>void}*/ onProgress;
constructor(parent) { constructor(parent) {
@ -232,15 +233,15 @@ export class SlashCommandClosure {
; ;
} }
let abortResult = this.testAbortController(); let abortResult = await this.testAbortController();
if (abortResult) { if (abortResult) {
return abortResult; return abortResult;
} }
this.scope.pipe = await executor.command.callback(args, value ?? ''); this.scope.pipe = await executor.command.callback(args, value ?? '');
done += 0.5; done += 0.5;
this.onProgress?.(done, this.executorList.length); this.onProgress?.(done, this.executorList.length);
// eslint-disable-next-line no-cond-assign abortResult = await this.testAbortController();
if (abortResult = this.testAbortController()) { if (abortResult) {
return abortResult; return abortResult;
} }
} }
@ -250,7 +251,13 @@ export class SlashCommandClosure {
return result; return result;
} }
testAbortController() { async testPaused() {
while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) {
await delay(200);
}
}
async testAbortController() {
await this.testPaused();
if (this.abortController?.signal?.aborted) { if (this.abortController?.signal?.aborted) {
const result = new SlashCommandClosureResult(); const result = new SlashCommandClosureResult();
result.isAborted = true; result.isAborted = true;

View File

@ -4,4 +4,6 @@ export class SlashCommandClosureResult {
/**@type {string}*/ pipe; /**@type {string}*/ pipe;
/**@type {boolean}*/ isAborted = false; /**@type {boolean}*/ isAborted = false;
/**@type {string}*/ abortReason; /**@type {string}*/ abortReason;
/**@type {boolean}*/ isError = false;
/**@type {string}*/ errorMessage;
} }

View File

@ -30,12 +30,15 @@ export class SlashCommandParserError extends Error {
let hint = []; let hint = [];
let lines = this.text.slice(start + 1, end - 1).split('\n'); let lines = this.text.slice(start + 1, end - 1).split('\n');
let lineNum = this.line - lines.length + 1; let lineNum = this.line - lines.length + 1;
let tabOffset = 0;
for (const line of lines) { for (const line of lines) {
const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`; const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`;
lineNum++; lineNum++;
hint.push(`${num}: ${line}`); const untabbedLine = line.replace(/\t/g, ' '.repeat(4));
tabOffset = untabbedLine.length - line.length;
hint.push(`${num}: ${untabbedLine}`);
} }
hint.push(`${' '.repeat(this.index - lineStart + lineOffset + 1)}^^^^^`); hint.push(`${' '.repeat(this.index - 2 - lineStart + lineOffset + 1 + tabOffset)}^^^^^`);
return hint.join('\n'); return hint.join('\n');
} }

View File

@ -685,6 +685,64 @@ body .panelControlBar {
opacity: 0.7; opacity: 0.7;
} }
#form_sheld.isExecutingCommandsFromChatInput {
#send_but {
visibility: hidden;
}
#rightSendForm > div:not(.mes_send).stscript_btn {
&.stscript_pause, &.stscript_stop {
display: flex;
}
}
&.paused {
#rightSendForm > div:not(.mes_send).stscript_btn {
&.stscript_pause {
display: none;
}
&.stscript_continue {
display: flex;
}
}
}
}
#rightSendForm > div:not(.mes_send) {
&.stscript_btn {
padding-right: 2px;
place-self: center;
cursor: pointer;
transition: 0.3s;
opacity: 1;
display: none;
&.stscript_pause > .fa-solid {
background-color: rgb(146, 190, 252);
}
&.stscript_continue > .fa-solid {
background-color: rgb(146, 190, 252);
}
&.stscript_stop > .fa-solid {
background-color: rgb(215, 136, 114);
}
> .fa-solid {
--toastInfoColor: #2F96B4;
--progColor: rgba(0, 128, 0, 0.839);
border-radius: 35%;
border: 0 solid var(--progColor);
aspect-ratio: 1 / 1;
display: flex;
color: rgb(24 24 24);
font-size: 0.5em;
height: var(--bottomFormIconSize);
justify-content: center;
align-items: center;
box-shadow:
0 0 0 var(--progColor),
0 0 0 var(--progColor)
;
}
}
}
#options_button { #options_button {
width: var(--bottomFormBlockSize); width: var(--bottomFormBlockSize);
height: var(--bottomFormBlockSize); height: var(--bottomFormBlockSize);
@ -1063,13 +1121,51 @@ select {
flex: 1; flex: 1;
order: 3; order: 3;
--progColor: rgba(0, 128, 0, 0.839); --progColor: rgb(146, 190, 252);
--progFlashColor: rgb(215, 136, 114);
--progSuccessColor: rgb(81, 163, 81);
--progErrorColor: rgb(189, 54, 47);
--progAbortedColor: rgb(215, 136, 114);
--progWidth: 3px; --progWidth: 3px;
--progWidthClip: calc(var(--progWidth) + 2px);
--prog: 0%; --prog: 0%;
--progDone: 0;
border-top: var(--progWidth) solid var(--progColor); border-top: var(--progWidth) solid var(--progColor);
clip-path: polygon(0% 0%, var(--prog) 0%, var(--prog) 5px, 100% 5px, 100% 100%, 0% 100%); clip-path: polygon(
0% calc(var(--progDone) * var(--progWidthClip)),
var(--prog) calc(var(--progDone) * var(--progWidthClip)),
var(--prog) var(--progWidthClip),
100% var(--progWidthClip),
100% 100%,
0% 100%
);
transition: clip-path 200ms; transition: clip-path 200ms;
} }
@keyframes script_progress_pulse {
0%, 100% {
border-top-color: var(--progColor);
}
50% {
border-top-color: var(--progFlashColor);
}
}
#form_sheld.isExecutingCommandsFromChatInput.script_paused #send_textarea {
animation-name: script_progress_pulse;
animation-duration: 1500ms;
animation-timing-function: ease-in-out;
animation-delay: 0s;
animation-iteration-count: infinite;
}
#form_sheld.script_success #send_textarea {
border-top-color: var(--progSuccessColor);
}
#form_sheld.script_error #send_textarea {
border-top-color: var(--progErrorColor);
}
#form_sheld.script_aborted #send_textarea {
border-top-color: var(--progAbortedColor);
}
.slashCommandAutoComplete-wrap { .slashCommandAutoComplete-wrap {
--targetOffset: 0; --targetOffset: 0;