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 -0Ignore whitespace
public/scripts/extensions/diceroll/index.js+48 -0
@@ -72,6 +72,9 @@ const defaultSettings = {
7272 useStructuredOutput: true,
7373 notify: true,
7474 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',
7578 optionsPrompt: DEFAULT_OPTIONS_PROMPT,
7679 directionTemplate: DEFAULT_DIRECTION_TEMPLATE,
7780};
@@ -83,6 +86,11 @@ let isRolling = false;
8386// Roll data waiting to be stamped onto the message the steered generation produces.
8487let 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.
92+let mainGenHasCustomPrompt = false;
93+
8694globalThis.dicerollGenerateInterceptor = (...args) => onGenerationIntercept(...args);
8795
8896function getSettings() {
@@ -97,6 +105,26 @@ function getSettings() {
97105 return extension_settings[MODULE];
98106}
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+ */
116+function 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+
100128function getRole(name) {
101129 return name === 'user' ? extension_prompt_roles.USER : extension_prompt_roles.SYSTEM;
102130}
@@ -288,6 +316,13 @@ async function onGenerationIntercept(coreChat, _contextSize, _abort, type) {
288316 return;
289317 }
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+
291326 // The roll belongs to the current user turn; swipes/regenerates of the same turn can reuse it.
292327 const anchor = chat.findLastIndex(x => x.is_user);
293328 const stored = chat_metadata[METADATA_KEY];
@@ -413,6 +448,7 @@ function loadSettingsUi() {
413448 $('#diceroll_max_options').val(s.maxOptions);
414449 $('#diceroll_options_role').val(s.optionsRole);
415450 $('#diceroll_direction_role').val(s.directionRole);
451+ $('#diceroll_skip_inject_ids').val(s.skipInjectIds);
416452 $('#diceroll_options_prompt').val(s.optionsPrompt);
417453 $('#diceroll_direction_template').val(s.directionTemplate);
418454}
@@ -452,6 +488,10 @@ function setupListeners() {
452488 getSettings().directionRole = String($(this).val());
453489 saveSettingsDebounced();
454490 });
491+ $('#diceroll_skip_inject_ids').on('input', function () {
492+ getSettings().skipInjectIds = String($(this).val());
493+ saveSettingsDebounced();
494+ });
455495 $('#diceroll_options_prompt').on('input', function () {
456496 getSettings().optionsPrompt = String($(this).val());
457497 saveSettingsDebounced();
@@ -483,6 +523,14 @@ async function init() {
483523 delete chat_metadata[METADATA_KEY];
484524 });
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+
486534 eventSource.on(event_types.MESSAGE_RECEIVED, (chatId) => stampMessage(chatId));
487535 eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (chatId) => {
488536 stampMessage(chatId);
public/scripts/extensions/diceroll/settings.html+4 -0
@@ -61,6 +61,10 @@
6161 </div>
6262 </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+
6468 <label for="diceroll_options_prompt" data-i18n="ext_diceroll_options_prompt">Option-generation prompt (\{{min}}, \{{max}} available)</label>
6569 <textarea id="diceroll_options_prompt" class="text_pole textarea_compact" rows="7"></textarea>
6670 <div id="diceroll_options_prompt_restore" class="menu_button menu_button_icon">