Skip Diceroll when a manual instruction steers the generation Guided Generations (and similar tools) inject their custom instruction via /inject id=instruct before triggering a swipe or response. When such an inject is active — the id list is configurable, default "instruct" — or the generation was started with its own custom prompt, Diceroll neither rolls nor injects a direction, letting the manual instruction steer alone. Plain swipes afterwards dice again as usual.

f328c40cd00cca6de77d6d3eeead02277509c900

permissionBRICK <40219477+permissionBRICK@users.noreply.github.com>

2 files changed, +52 -0Showing whitespace changes
public/scripts/extensions/diceroll/index.js+48 -0
@@ -72,6 +72,9 @@ const defaultSettings = {
72 useStructuredOutput: true,72 useStructuredOutput: true,
73 notify: true,73 notify: true,
74 debugDisplay: false,74 debugDisplay: false,
75 // Comma-separated /inject ids whose presence means another tool is already steering this
76 // generation (Guided Generations uses id "instruct" for guided response/swipe/corrections).
77 skipInjectIds: 'instruct',
75 optionsPrompt: DEFAULT_OPTIONS_PROMPT,78 optionsPrompt: DEFAULT_OPTIONS_PROMPT,
76 directionTemplate: DEFAULT_DIRECTION_TEMPLATE,79 directionTemplate: DEFAULT_DIRECTION_TEMPLATE,
77};80};
@@ -83,6 +86,11 @@ let isRolling = false;
83// Roll data waiting to be stamped onto the message the steered generation produces.86// Roll data waiting to be stamped onto the message the steered generation produces.
84let pendingStamp = null;87let pendingStamp = null;
8588
89// Whether the main generation being intercepted was started with its own custom prompt
90// (e.g. a swipe/regenerate triggered with an additional instruction). Tracked via
91// GENERATION_STARTED because the interceptor does not receive the generation params.
92let mainGenHasCustomPrompt = false;
93
86globalThis.dicerollGenerateInterceptor = (...args) => onGenerationIntercept(...args);94globalThis.dicerollGenerateInterceptor = (...args) => onGenerationIntercept(...args);
8795
88function getSettings() {96function getSettings() {
@@ -97,6 +105,26 @@ function getSettings() {
97 return extension_settings[MODULE];105 return extension_settings[MODULE];
98}106}
99107
108/**
109 * True when another tool is already steering this generation with a manual instruction:
110 * either a script inject with a configured id (Guided Generations' guided swipe/response
111 * inject `id=instruct` before triggering the generation), or a custom prompt passed
112 * directly to the generation call.
113 * @param {object} s Settings
114 * @returns {boolean}
115 */
116function hasManualSteering(s) {
117 if (mainGenHasCustomPrompt) {
118 return true;
119 }
120 const injects = chat_metadata.script_injects ?? {};
121 return String(s.skipInjectIds ?? '')
122 .split(',')
123 .map(id => id.trim())
124 .filter(Boolean)
125 .some(id => String(injects[id]?.value ?? '').trim());
126}
127
100function getRole(name) {128function getRole(name) {
101 return name === 'user' ? extension_prompt_roles.USER : extension_prompt_roles.SYSTEM;129 return name === 'user' ? extension_prompt_roles.USER : extension_prompt_roles.SYSTEM;
102}130}
@@ -288,6 +316,13 @@ async function onGenerationIntercept(coreChat, _contextSize, _abort, type) {
288 return;316 return;
289 }317 }
290318
319 // A guided swipe/response (or any generation carrying its own instruction) takes precedence:
320 // no roll, no direction injection — the manual instruction alone steers this generation.
321 if (hasManualSteering(s)) {
322 console.debug('[Diceroll] Manual instruction detected, skipping the roll for this generation.');
323 return;
324 }
325
291 // The roll belongs to the current user turn; swipes/regenerates of the same turn can reuse it.326 // The roll belongs to the current user turn; swipes/regenerates of the same turn can reuse it.
292 const anchor = chat.findLastIndex(x => x.is_user);327 const anchor = chat.findLastIndex(x => x.is_user);
293 const stored = chat_metadata[METADATA_KEY];328 const stored = chat_metadata[METADATA_KEY];
@@ -413,6 +448,7 @@ function loadSettingsUi() {
413 $('#diceroll_max_options').val(s.maxOptions);448 $('#diceroll_max_options').val(s.maxOptions);
414 $('#diceroll_options_role').val(s.optionsRole);449 $('#diceroll_options_role').val(s.optionsRole);
415 $('#diceroll_direction_role').val(s.directionRole);450 $('#diceroll_direction_role').val(s.directionRole);
451 $('#diceroll_skip_inject_ids').val(s.skipInjectIds);
416 $('#diceroll_options_prompt').val(s.optionsPrompt);452 $('#diceroll_options_prompt').val(s.optionsPrompt);
417 $('#diceroll_direction_template').val(s.directionTemplate);453 $('#diceroll_direction_template').val(s.directionTemplate);
418}454}
@@ -452,6 +488,10 @@ function setupListeners() {
452 getSettings().directionRole = String($(this).val());488 getSettings().directionRole = String($(this).val());
453 saveSettingsDebounced();489 saveSettingsDebounced();
454 });490 });
491 $('#diceroll_skip_inject_ids').on('input', function () {
492 getSettings().skipInjectIds = String($(this).val());
493 saveSettingsDebounced();
494 });
455 $('#diceroll_options_prompt').on('input', function () {495 $('#diceroll_options_prompt').on('input', function () {
456 getSettings().optionsPrompt = String($(this).val());496 getSettings().optionsPrompt = String($(this).val());
457 saveSettingsDebounced();497 saveSettingsDebounced();
@@ -483,6 +523,14 @@ async function init() {
483 delete chat_metadata[METADATA_KEY];523 delete chat_metadata[METADATA_KEY];
484 });524 });
485525
526 // Runs before the interceptor within the same Generate() call, so the flag is always fresh.
527 // The nested option request (type 'quiet') must not overwrite the outer generation's flag.
528 eventSource.on(event_types.GENERATION_STARTED, (type, params, dryRun) => {
529 if (type !== 'quiet' && !dryRun) {
530 mainGenHasCustomPrompt = !!params?.quiet_prompt;
531 }
532 });
533
486 eventSource.on(event_types.MESSAGE_RECEIVED, (chatId) => stampMessage(chatId));534 eventSource.on(event_types.MESSAGE_RECEIVED, (chatId) => stampMessage(chatId));
487 eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (chatId) => {535 eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (chatId) => {
488 stampMessage(chatId);536 stampMessage(chatId);
public/scripts/extensions/diceroll/settings.html+4 -0
@@ -61,6 +61,10 @@
61 </div>61 </div>
62 </div>62 </div>
6363
64 <label for="diceroll_skip_inject_ids" data-i18n="ext_diceroll_skip_inject_ids">Skip when these /inject ids are active (comma-separated)</label>
65 <input id="diceroll_skip_inject_ids" class="text_pole" type="text" placeholder="instruct" />
66 <small data-i18n="ext_diceroll_skip_inject_ids_help">When another tool steers the reply with its own instruction — e.g. Guided Generations' guided response/swipe use /inject id=instruct — Diceroll stays out of the way and neither rolls nor injects a direction for that generation. Generations triggered with a custom prompt are skipped as well.</small>
67
64 <label for="diceroll_options_prompt" data-i18n="ext_diceroll_options_prompt">Option-generation prompt (\{{min}}, \{{max}} available)</label>68 <label for="diceroll_options_prompt" data-i18n="ext_diceroll_options_prompt">Option-generation prompt (\{{min}}, \{{max}} available)</label>
65 <textarea id="diceroll_options_prompt" class="text_pole textarea_compact" rows="7"></textarea>69 <textarea id="diceroll_options_prompt" class="text_pole textarea_compact" rows="7"></textarea>
66 <div id="diceroll_options_prompt_restore" class="menu_button menu_button_icon">70 <div id="diceroll_options_prompt_restore" class="menu_button menu_button_icon">