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';
282282import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js';
283283import { MacroEngine } from './scripts/macros/engine/MacroEngine.js';
284284import { addChatBackupsBrowser } from './scripts/chat-backups.js';
285+import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js';
285286
286287// API OBJECT FOR EXTERNAL WIRING
287288globalThis.SillyTavern = {
@@ -2711,6 +2712,20 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou
27112712 });
27122713 }
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+
27142729 const environment = {};
27152730
27162731 if (typeof _original === 'string') {
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+11 -0
@@ -13,6 +13,7 @@ import {
1313import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';
1414import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js';
1515import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js';
16+import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js';
1617
1718/** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */
1819
@@ -1093,6 +1094,10 @@ export function parseMacroContext(macroText, cursorOffset) {
10931094 }
10941095 }
10951096
1097+ if (flags.length > 0) {
1098+ void onboardingExperimentalMacroEngine('macro flags');
1099+ }
1100+
10961101 // Check for variable shorthand prefix (. or $)
10971102 // These trigger variable expression mode instead of regular macro parsing
10981103 /** @type {'.'|'$'|null} */
@@ -1183,6 +1188,8 @@ export function parseMacroContext(macroText, cursorOffset) {
11831188 // For invalid trailing chars, none of the typing flags will be true
11841189 const isOperatorComplete = (variableOperator === '++' || variableOperator === '--');
11851190
1191+ void onboardingExperimentalMacroEngine('variable shorthands');
1192+
11861193 // Return early for variable shorthand - different structure than regular macros
11871194 return {
11881195 fullText: macroText,
@@ -1308,6 +1315,10 @@ export function parseMacroContext(macroText, cursorOffset) {
13081315
13091316 const leftPadding = macroText.match(/^\s+/)?.[0] ?? '';
13101317
1318+ if (leftPadding) {
1319+ void onboardingExperimentalMacroEngine('leading whitespace');
1320+ }
1321+
13111322 // Clean identifier: strip trailing colons (for partial :: typing)
13121323 let cleanIdentifier = identifierOnly.replace(/:+$/, '');
13131324
public/scripts/macros/engine/MacroDiagnostics.js+47 -0
@@ -3,6 +3,13 @@
33/** @typedef {import('chevrotain').ILexingError} ILexingError */
44/** @typedef {import('chevrotain').IRecognitionException} IRecognitionException */
55
6+import { saveSettingsDebounced } from '/script.js';
7+import { t } from '/scripts/i18n.js';
8+import { Popup, POPUP_RESULT } from '/scripts/popup.js';
9+import { power_user } from '/scripts/power-user.js';
10+import { accountStorage } from '/scripts/util/AccountStorage.js';
11+import { SimpleMutex } from '/scripts/util/SimpleMutex.js';
12+
613/**
714 * @typedef {Object} MacroErrorContext
815 * @property {string} [macroName]
@@ -22,6 +29,46 @@
2229 * @typedef {MacroErrorContext & { message: string, error?: any }} MacroLogOptions
2330 */
2431
32+
33+// Use mutex here so even on parallel usage without awaiting the popup, this will only show up once.
34+export 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+ */
43+export const onboardingExperimentalMacroEngine = onboardingExperimentalMacroEngineMutex.update.bind(onboardingExperimentalMacroEngineMutex);
44+
45+async 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+
2572/**
2673 * Creates an error representing a runtime macro invocation problem (such as
2774 * 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';
3939import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
4040import { chat_metadata } from '/script.js';
4141import { extension_settings } from '../extensions.js';
42+import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js';
4243
4344/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
4445/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
@@ -618,6 +619,8 @@ export class SlashCommandParser {
618619 resultStart = conditionStartInText + macroNameStart;
619620 }
620621
622+ await onboardingExperimentalMacroEngine('{{if}} macro');
623+
621624 const result = new AutoCompleteNameResult(
622625 resultIdentifier,
623626 resultStart,
@@ -736,6 +739,8 @@ export class SlashCommandParser {
736739 scopedMacroName: scopedMacro.name,
737740 };
738741
742+ await onboardingExperimentalMacroEngine('scoped macros');
743+
739744 // Only show the scoped macro's details - no list of other macros
740745 // This creates a "details only" view showing the scoped arg being typed
741746 const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name);