Macros 2.0 (v0.5.2) - Onboarding for new Macro Engine (#4955) * Macros 2.0 [Feature] - Add onboarding popup for experimental macro engine features - Add `onbordingExperimentalMacroEngine()` function in MacroDiagnostics.js to detect and prompt users about experimental features - Detect experimental macro features in `substituteParamsLegacy()`: {{if}}, scoped macros, macro flags, variable shorthands, nested macros - Add feature detection in autocomplete parser for macro flags, variable shorthands, and leading whitespace * Fix typo: rename `onbording` to `onboarding` throughout macro diagnostics system * Add `void` operator to async onboarding calls and fix typo in comment - Add `void` operator to all `onboardingExperimentalMacroEngine()` calls to explicitly discard Promise return values - Fix typo in MacroDiagnostics.js comment: "hat" → "that" * Fix typo in experimental macro engine onboarding popup: "anytime time" → "any time" --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

e40b31b06d20122c14714351cdf9ac39d65691a4

Wolfsblvt <wolfsblvt@gmail.com>

Signed
4 files changed, +78 -0Ignore whitespace
public/script.js+15 -0
@@ -282,6 +282,7 @@ import { AudioPlayer } from './scripts/audio-player.js';
282import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';282import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';
283import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';283import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';
284import { addChatBackupsBrowser } from './scripts/chat-backups.js';284import { addChatBackupsBrowser } from './scripts/chat-backups.js';
285import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js';
285286
286// API OBJECT FOR EXTERNAL WIRING287// API OBJECT FOR EXTERNAL WIRING
287globalThis.SillyTavern = {288globalThis.SillyTavern = {
@@ -2711,6 +2712,20 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou
2711 });2712 });
2712 }2713 }
27132714
2715 // Try to roughly detect experimental macro features to show the onboarding if needed.
2716 // This does not have to be 100% accurate, only best effort what we can quickly check.
2717 // Only do this if the warning wasn't shown yet, to prevent needless regex checks.
2718 if (accountStorage.getItem('slash_command_experimental_engine_warning_shown') !== 'true') {
2719 let feature = /** @type {string|null} */ (null);
2720 if (/{{\s*if/.test(content)) feature = '{{if}} macro';
2721 else if (/{{\s*\//.test(content)) feature = 'scoped macro';
2722 else if (/{{\s*[!?~#/]/.test(content)) feature = 'macro flags';
2723 else if (/{{\s*[.$]/.test(content)) feature = 'variable shorthands';
2724 else if (/\{\{(?:(?!\}\}).)*\{\{(?=[\s\S]*?\}\}[\s\S]*?\}\})/.test(content)) feature = 'nested macro';
2725
2726 if (feature) void onboardingExperimentalMacroEngine(feature);
2727 }
2728
2714 const environment = {};2729 const environment = {};
27152730
2716 if (typeof _original === 'string') {2731 if (typeof _original === 'string') {
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+11 -0
@@ -13,6 +13,7 @@ import {
13import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';13import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';
14import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js';14import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js';
15import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js';15import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js';
16import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js';
1617
17/** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */18/** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */
1819
@@ -1093,6 +1094,10 @@ export function parseMacroContext(macroText, cursorOffset) {
1093 }1094 }
1094 }1095 }
10951096
1097 if (flags.length > 0) {
1098 void onboardingExperimentalMacroEngine('macro flags');
1099 }
1100
1096 // Check for variable shorthand prefix (. or $)1101 // Check for variable shorthand prefix (. or $)
1097 // These trigger variable expression mode instead of regular macro parsing1102 // These trigger variable expression mode instead of regular macro parsing
1098 /** @type {'.'|'$'|null} */1103 /** @type {'.'|'$'|null} */
@@ -1183,6 +1188,8 @@ export function parseMacroContext(macroText, cursorOffset) {
1183 // For invalid trailing chars, none of the typing flags will be true1188 // For invalid trailing chars, none of the typing flags will be true
1184 const isOperatorComplete = (variableOperator === '++' || variableOperator === '--');1189 const isOperatorComplete = (variableOperator === '++' || variableOperator === '--');
11851190
1191 void onboardingExperimentalMacroEngine('variable shorthands');
1192
1186 // Return early for variable shorthand - different structure than regular macros1193 // Return early for variable shorthand - different structure than regular macros
1187 return {1194 return {
1188 fullText: macroText,1195 fullText: macroText,
@@ -1308,6 +1315,10 @@ export function parseMacroContext(macroText, cursorOffset) {
13081315
1309 const leftPadding = macroText.match(/^\s+/)?.[0] ?? '';1316 const leftPadding = macroText.match(/^\s+/)?.[0] ?? '';
13101317
1318 if (leftPadding) {
1319 void onboardingExperimentalMacroEngine('leading whitespace');
1320 }
1321
1311 // Clean identifier: strip trailing colons (for partial :: typing)1322 // Clean identifier: strip trailing colons (for partial :: typing)
1312 let cleanIdentifier = identifierOnly.replace(/:+$/, '');1323 let cleanIdentifier = identifierOnly.replace(/:+$/, '');
13131324
public/scripts/macros/engine/MacroDiagnostics.js+47 -0
@@ -3,6 +3,13 @@
3/** @typedef {import('chevrotain').ILexingError} ILexingError */3/** @typedef {import('chevrotain').ILexingError} ILexingError */
4/** @typedef {import('chevrotain').IRecognitionException} IRecognitionException */4/** @typedef {import('chevrotain').IRecognitionException} IRecognitionException */
55
6import { saveSettingsDebounced } from '/script.js';
7import { t } from '/scripts/i18n.js';
8import { Popup, POPUP_RESULT } from '/scripts/popup.js';
9import { power_user } from '/scripts/power-user.js';
10import { accountStorage } from '/scripts/util/AccountStorage.js';
11import { SimpleMutex } from '/scripts/util/SimpleMutex.js';
12
6/**13/**
7 * @typedef {Object} MacroErrorContext14 * @typedef {Object} MacroErrorContext
8 * @property {string} [macroName]15 * @property {string} [macroName]
@@ -22,6 +29,46 @@
22 * @typedef {MacroErrorContext & { message: string, error?: any }} MacroLogOptions29 * @typedef {MacroErrorContext & { message: string, error?: any }} MacroLogOptions
23 */30 */
2431
32
33// Use mutex here so even on parallel usage without awaiting the popup, this will only show up once.
34export const onboardingExperimentalMacroEngineMutex = new SimpleMutex(onboardingExperimentalMacroEngineUnsafe);
35
36/**
37 * Onboards the user to use the experimental macro engine.
38 * Asks the user to enable it if they haven't already.
39 *
40 * @param {string|null} feature - The feature that requires the experimental macro engine, or null if not applicable or unknown.
41 * @returns {Promise<void>} - A promise that resolves when the user has been onboarded.
42 */
43export const onboardingExperimentalMacroEngine = onboardingExperimentalMacroEngineMutex.update.bind(onboardingExperimentalMacroEngineMutex);
44
45async function onboardingExperimentalMacroEngineUnsafe(feature = null) {
46 // Show a popup once telling a user that they are using experimental features that only work with the new engine.
47 // Ask them if they want to turn the experimental engine on.
48 if (power_user.experimental_macro_engine) return;
49
50 // If already shown, do not show again
51 const shown = accountStorage.getItem('slash_command_experimental_engine_warning_shown');
52 if (shown === 'true') return;
53
54 const result = await Popup.show.confirm(t`Experimental Macro Engine`, `
55 <p>${t`You are using experimental macro features that require the new macro engine.`}</p>
56 ${feature ? `<div class="info-block hint">
57 <span>${t`Recognized Feature: `}<strong>${feature}</strong></span>
58 </div>` : ''}
59 <p>${t`For more information on the new macro engine, visit the <br />${`<a href="https://docs.sillytavern.app/usage/core-concepts/macros/">${t`Macro Documentation`}</a>`}.`}</p>
60 <p>${t`You can enable the engine any time under:<br />${t`User Settings`} → ${t`Experimental Macro Engine`}`}</p>
61 <p>${t`Would you like to enable it now?`}</p>`);
62 if (result == POPUP_RESULT.AFFIRMATIVE) {
63 power_user.experimental_macro_engine = true;
64 $('#experimental_macro_engine').prop('checked', power_user.experimental_macro_engine);
65 saveSettingsDebounced();
66 }
67
68 // Only show this once
69 accountStorage.setItem('slash_command_experimental_engine_warning_shown', 'true');
70}
71
25/**72/**
26 * Creates an error representing a runtime macro invocation problem (such as73 * Creates an error representing a runtime macro invocation problem (such as
27 * arity or type mismatches). These errors are intended to be caught by the74 * arity or type mismatches). These errors are intended to be caught by the
public/scripts/slash-commands/SlashCommandParser.js+5 -0
@@ -39,6 +39,7 @@ import { macros as macroSystem } from '../macros/macro-system.js';
39import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';39import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
40import { chat_metadata } from '/script.js';40import { chat_metadata } from '/script.js';
41import { extension_settings } from '../extensions.js';41import { extension_settings } from '../extensions.js';
42import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js';
4243
43/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */44/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
44/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */45/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
@@ -618,6 +619,8 @@ export class SlashCommandParser {
618 resultStart = conditionStartInText + macroNameStart;619 resultStart = conditionStartInText + macroNameStart;
619 }620 }
620621
622 await onboardingExperimentalMacroEngine('{{if}} macro');
623
621 const result = new AutoCompleteNameResult(624 const result = new AutoCompleteNameResult(
622 resultIdentifier,625 resultIdentifier,
623 resultStart,626 resultStart,
@@ -736,6 +739,8 @@ export class SlashCommandParser {
736 scopedMacroName: scopedMacro.name,739 scopedMacroName: scopedMacro.name,
737 };740 };
738741
742 await onboardingExperimentalMacroEngine('scoped macros');
743
739 // Only show the scoped macro's details - no list of other macros744 // Only show the scoped macro's details - no list of other macros
740 // This creates a "details only" view showing the scoped arg being typed745 // This creates a "details only" view showing the scoped arg being typed
741 const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name);746 const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name);