From ddd1f78654471db29e7bd942cacbf7a28491aadd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 11:36:19 +0000 Subject: [PATCH] fix(regex): let a script tick both display-only and prompt-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SillyTavern treats "Only Format Display" (markdownOnly) and "Only Format Prompt" (promptOnly) as two independent opt-ins and ORs them; Glaze required both at once, so a script with both flags never ran — no caller passes isMarkdown and isPrompt together. An imported pair such as "Use Figure Spaces" / "Return Normal Spaces" therefore swapped spaces for U+2007 in the prompt but never swapped them back for display. - applyRegexes now runs a script when either opt-in matches the current pass, while the storage (run-on-edit) pass still skips both kinds so a display rewrite is never baked into the stored message - displayRegexesProvider stops dropping a promptOnly script that also ticks markdownOnly - tests cover both passes plus the storage pass for the both-flags case and the prompt-only half of the pair Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PeAHpjepbusNwo4X5DQDsp --- lib/core/llm/regex_service.dart | 18 +++++++- lib/core/state/active_regex_provider.dart | 10 ++++- test/regex_service_test.dart | 55 +++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/lib/core/llm/regex_service.dart b/lib/core/llm/regex_service.dart index 96545a58d..c1c6c7454 100644 --- a/lib/core/llm/regex_service.dart +++ b/lib/core/llm/regex_service.dart @@ -49,8 +49,22 @@ String applyRegexes( continue; } } - if (script.promptOnly && !isPrompt) continue; - if (script.markdownOnly && !isMarkdown) continue; + // ST semantics: "Only Format Display" (markdownOnly) and "Only Format + // Prompt" (promptOnly) are two independent opt-ins, not one exclusive + // switch — `getRegexedString` ORs the two clauses. A script that ticks + // both therefore runs in the display pass *and* in the prompt pass. + // Requiring both at once made such a script dead everywhere, because no + // caller passes `isMarkdown` and `isPrompt` together. + // + // The storage pass (run-on-edit) passes neither flag, and still skips + // both kinds: a display- or prompt-scoped rewrite must never be baked + // into the stored message. + if (script.promptOnly || script.markdownOnly) { + final wantsThisPass = + (script.promptOnly && isPrompt) || + (script.markdownOnly && isMarkdown); + if (!wantsThisPass) continue; + } final sEphemerality = script.ephemerality; if (!ignoreEphemerality && diff --git a/lib/core/state/active_regex_provider.dart b/lib/core/state/active_regex_provider.dart index 7efd3c154..f9cf04a2b 100644 --- a/lib/core/state/active_regex_provider.dart +++ b/lib/core/state/active_regex_provider.dart @@ -26,5 +26,13 @@ final activeRegexesProvider = FutureProvider>((ref) async { final displayRegexesProvider = FutureProvider>((ref) async { final all = await ref.watch(activeRegexesProvider.future); - return all.where((r) => r.ephemerality.contains(1) && !r.promptOnly).toList(); + // "Only Format Prompt" keeps a script out of the display pass — unless it + // also ticks "Only Format Display", which opts it into both passes (ST ORs + // the two flags; see [applyRegexes]). + return all + .where( + (r) => + r.ephemerality.contains(1) && (!r.promptOnly || r.markdownOnly), + ) + .toList(); }); diff --git a/test/regex_service_test.dart b/test/regex_service_test.dart index f48e0eb81..225e1cf79 100644 --- a/test/regex_service_test.dart +++ b/test/regex_service_test.dart @@ -129,6 +129,61 @@ void main() { expect(prompt, equals('aYb')); }); + test( + 'markdownOnly + promptOnly runs in both passes, never on stored text', + () { + // The pair a user imports to swap spaces for U+2007 on the way out and + // back on the way in: the "return" half ticks both "Only Format + // Display" and "Only Format Prompt". ST ORs the two flags, so it must + // fire in the display pass and in the prompt pass alike. + final script = PresetRegex.fromJson({ + 'id': 'both-only', + 'name': 'Return Normal Spaces', + 'regex': '/\u2007/g', + 'replacement': ' ', + 'markdownOnly': true, + 'promptOnly': true, + 'placement': [1, 2], + 'ephemerality': [1, 2], + }); + + const input = 'a\u2007b'; + + expect( + applyRegexes(input, 2, 2, [script], ctx(), isPrompt: true), + equals('a b'), + ); + expect( + applyRegexes(input, 2, 1, [script], ctx(), isMarkdown: true), + equals('a b'), + ); + // Storage pass (run-on-edit): neither flag is set, so the rewrite must + // not be baked into the message. + expect(applyRegexes(input, 2, 1, [script], ctx()), equals(input)); + }, + ); + + test('promptOnly script rewrites spaces to figure spaces in the prompt', () { + final script = PresetRegex.fromJson({ + 'id': 'figure-spaces', + 'name': 'Use Figure Spaces', + 'regex': '/ /g', + 'replacement': '\u2007', + 'promptOnly': true, + 'placement': [1, 2], + 'ephemerality': [1, 2], + }); + + expect( + applyRegexes('a b', 2, 2, [script], ctx(), isPrompt: true), + equals('a\u2007b'), + ); + expect( + applyRegexes('a b', 2, 1, [script], ctx(), isMarkdown: true), + equals('a b'), + ); + }); + test('World Info placement 5 applies to lorebook blocks', () { final script = PresetRegex.fromJson({ 'id': 'wi-only',