Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions lib/core/llm/regex_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
10 changes: 9 additions & 1 deletion lib/core/state/active_regex_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,13 @@ final activeRegexesProvider = FutureProvider<List<PresetRegex>>((ref) async {

final displayRegexesProvider = FutureProvider<List<PresetRegex>>((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();
});
55 changes: 55 additions & 0 deletions test/regex_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading