diff --git a/.github/workflows/issues-updates-on-merge.yml b/.github/workflows/issues-updates-on-merge.yml
index 3efc9b37f..ddd76b459 100644
--- a/.github/workflows/issues-updates-on-merge.yml
+++ b/.github/workflows/issues-updates-on-merge.yml
@@ -36,10 +36,10 @@ jobs:
for ISSUE in $(echo $issues | jq -r '.[]'); do
if [ "${{ github.ref }}" == "refs/heads/staging" ]; then
LABEL="✅ Done (staging)"
- gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL"
+ gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" --remove-label "🧑💻 In Progress"
elif [ "${{ github.ref }}" == "refs/heads/release" ]; then
LABEL="✅ Done"
- gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL"
+ gh issue edit $ISSUE -R ${{ github.repository }} --add-label "$LABEL" --remove-label "🧑💻 In Progress"
fi
- echo "Added label '$LABEL' to issue #$ISSUE"
+ echo "Added label '$LABEL' (and removed '🧑💻 In Progress' if present) in issue #$ISSUE"
done
diff --git a/.github/workflows/pr-auto-manager.yml b/.github/workflows/pr-auto-manager.yml
index cf247ff9d..005fa8da6 100644
--- a/.github/workflows/pr-auto-manager.yml
+++ b/.github/workflows/pr-auto-manager.yml
@@ -262,6 +262,6 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for ISSUE in $(echo $final_issues | jq -r '.[]'); do
- gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)"
- echo "Added label '✅ Done (staging)' to issue #$ISSUE"
+ gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)" --remove-label "🧑💻 In Progress"
+ echo "Added label '✅ Done (staging)' (and removed '🧑💻 In Progress' if present) in issue #$ISSUE"
done
diff --git a/public/index.html b/public/index.html
index d6a19934f..3750c193e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5998,6 +5998,7 @@
+
diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js
index 6e515baa1..94d642afa 100644
--- a/public/scripts/PromptManager.js
+++ b/public/scripts/PromptManager.js
@@ -1,6 +1,6 @@
'use strict';
-import { DOMPurify, Popper } from '../lib.js';
+import { DOMPurify } from '../lib.js';
import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js';
import { is_group_generating } from './group-chats.js';
@@ -1440,36 +1440,8 @@ class PromptManager {
footerDiv.querySelector('select').selectedIndex = selectedPromptIndex;
// Add prompt export dialogue and options
-
- const exportForCharacter = await renderTemplateAsync('promptManagerExportForCharacter');
- const exportPopup = await renderTemplateAsync('promptManagerExportPopup', { isGlobalStrategy: 'global' === this.configuration.promptOrder.strategy, exportForCharacter });
- rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup);
-
- // Destroy previous popper instance if it exists
- if (this.exportPopper) {
- this.exportPopper.destroy();
- }
-
- this.exportPopper = Popper.createPopper(
- document.getElementById('prompt-manager-export'),
- document.getElementById('prompt-manager-export-format-popup'),
- { placement: 'bottom' },
- );
-
- const showExportSelection = () => {
- const popup = document.getElementById('prompt-manager-export-format-popup');
- const show = popup.hasAttribute('data-show');
-
- if (show) popup.removeAttribute('data-show');
- else popup.setAttribute('data-show', '');
-
- this.exportPopper.update();
- };
-
footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport);
- footerDiv.querySelector('#prompt-manager-export').addEventListener('click', showExportSelection);
- rangeBlockDiv.querySelector('.export-promptmanager-prompts-full').addEventListener('click', this.handleFullExport);
- rangeBlockDiv.querySelector('.export-promptmanager-prompts-character')?.addEventListener('click', this.handleCharacterExport);
+ footerDiv.querySelector('#prompt-manager-export').addEventListener('click', this.handleFullExport);
}
}
diff --git a/public/scripts/custom-request.js b/public/scripts/custom-request.js
index 250e74f11..5cd3f4827 100644
--- a/public/scripts/custom-request.js
+++ b/public/scripts/custom-request.js
@@ -2,7 +2,7 @@ import { getPresetManager } from './preset-manager.js';
import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
import { getTextGenServer } from './textgen-settings.js';
import { extractReasoningFromData } from './reasoning.js';
-import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';
+import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';
import { getStreamingReply, tryParseStreamingError } from './openai.js';
import EventSourceStream from './sse-stream.js';
@@ -190,6 +190,7 @@ export class TextCompletionService {
* @param {Object} options - Configuration options
* @param {string?} [options.presetName] - Name of the preset to use for generation settings
* @param {string?} [options.instructName] - Name of instruct preset for message formatting
+ * @param {Partial?} [options.instructSettings] - Override instruct settings
* @param {boolean} extractData - Whether to extract structured data from response
* @param {AbortSignal?} [signal]
* @returns {Promise AsyncGenerator)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
@@ -222,15 +223,20 @@ export class TextCompletionService {
}
}
+
+ /** @type {InstructSettings | undefined} */
+ let instructPreset;
// Handle instruct formatting if requested
if (Array.isArray(prompt) && instructName) {
const instructPresetManager = getPresetManager('instruct');
- let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
+ instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
if (instructPreset) {
// Clone the preset to avoid modifying the original
instructPreset = structuredClone(instructPreset);
- instructPreset.macro = false;
instructPreset.names_behavior = names_behavior_types.NONE;
+ if (options.instructSettings) {
+ Object.assign(instructPreset, options.instructSettings);
+ }
// Format messages using instruct formatting
const formattedMessages = [];
@@ -266,10 +272,9 @@ export class TextCompletionService {
formattedMessages.push(messageContent);
}
requestData.prompt = formattedMessages.join('');
- if (instructPreset.output_suffix) {
- requestData.stop = [instructPreset.output_suffix];
- requestData.stopping_strings = [instructPreset.output_suffix];
- }
+ const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false });
+ requestData.stop = stoppingStrings;
+ requestData.stopping_strings = stoppingStrings;
} else {
console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
requestData.prompt = prompt.map(x => x.content).join('\n\n');
@@ -283,7 +288,61 @@ export class TextCompletionService {
// @ts-ignore
const data = this.createRequestData(requestData);
- return await this.sendRequest(data, extractData, signal);
+ const response = await this.sendRequest(data, extractData, signal);
+ // Remove stopping strings from the end
+ if (!data.stream && extractData) {
+ /** @type {ExtractedData} */
+ // @ts-ignore
+ const extractedData = response;
+
+ let message = extractedData.content;
+
+ message = message.replace(/[^\S\r\n]+$/gm, '');
+
+ if (requestData.stopping_strings) {
+ for (const stoppingString of requestData.stopping_strings) {
+ if (stoppingString.length) {
+ for (let j = stoppingString.length; j > 0; j--) {
+ if (message.slice(-j) === stoppingString.slice(0, j)) {
+ message = message.slice(0, -j);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (instructPreset) {
+ [
+ instructPreset.stop_sequence,
+ instructPreset.input_sequence,
+ ].forEach(sequence => {
+ if (sequence?.trim()) {
+ const index = message.indexOf(sequence);
+ if (index !== -1) {
+ message = message.substring(0, index);
+ }
+ }
+ });
+
+ [
+ instructPreset.output_sequence,
+ instructPreset.last_output_sequence,
+ ].forEach(sequences => {
+ if (sequences) {
+ sequences.split('\n')
+ .filter(line => line.trim() !== '')
+ .forEach(line => {
+ message = message.replaceAll(line, '');
+ });
+ }
+ });
+ }
+
+ extractedData.content = message;
+ }
+
+ return response;
}
/**
diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js
index 144ac3d6b..e4cca98fa 100644
--- a/public/scripts/extensions/shared.js
+++ b/public/scripts/extensions/shared.js
@@ -285,6 +285,7 @@ export class ConnectionManagerRequestService {
extractData: true,
includePreset: true,
includeInstruct: true,
+ instructSettings: {},
};
static getAllowedTypes() {
@@ -298,11 +299,17 @@ export class ConnectionManagerRequestService {
* @param {string} profileId
* @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
* @param {number} maxTokens
- * @param {{stream?: boolean, signal?: AbortSignal, extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true
+ * @param {Object} custom
+ * @param {boolean?} [custom.stream=false]
+ * @param {AbortSignal?} [custom.signal]
+ * @param {boolean?} [custom.extractData=true]
+ * @param {boolean?} [custom.includePreset=true]
+ * @param {boolean?} [custom.includeInstruct=true]
+ * @param {Partial?} [custom.instructSettings] Override instruct settings
* @returns {Promise AsyncGenerator)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
*/
static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
- const { stream, signal, extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };
+ const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom };
const context = SillyTavern.getContext();
if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
@@ -346,6 +353,7 @@ export class ConnectionManagerRequestService {
}, {
instructName: includeInstruct ? profile.instruct : undefined,
presetName: includePreset ? profile.preset : undefined,
+ instructSettings: includeInstruct ? instructSettings : undefined,
}, extractData, signal);
}
default: {
diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js
index 000d242b0..56b16f560 100644
--- a/public/scripts/instruct-mode.js
+++ b/public/scripts/instruct-mode.js
@@ -243,9 +243,14 @@ export function autoSelectInstructPreset(modelId) {
/**
* Converts instruct mode sequences to an array of stopping strings.
+ * @param {Object} options
+ * @param {InstructSettings?} [options.customInstruct=null] - Custom instruct settings.
+ * @param {boolean?} [options.useStopStrings] - Decides whether to use "Chat Start" and "Example Separator"
* @returns {string[]} Array of instruct mode stopping strings.
*/
-export function getInstructStoppingSequences() {
+export function getInstructStoppingSequences({ customInstruct = null, useStopStrings = null } = {}) {
+ const instruct = structuredClone(customInstruct ?? power_user.instruct);
+
/**
* Adds instruct mode sequence to the result array.
* @param {string} sequence Sequence string.
@@ -254,7 +259,7 @@ export function getInstructStoppingSequences() {
function addInstructSequence(sequence) {
// Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
// But it's a problem for Metharme which doesn't use newlines to separate them.
- const wrap = (s) => power_user.instruct.wrap ? '\n' + s : s;
+ const wrap = (s) => instruct.wrap ? '\n' + s : s;
// Sequence must be a non-empty string
if (typeof sequence === 'string' && sequence.length > 0) {
// If sequence is just a whitespace or newline - we don't want to make it a stopping string
@@ -262,7 +267,7 @@ export function getInstructStoppingSequences() {
if (sequence.trim().length > 0) {
const wrappedSequence = wrap(sequence);
// Need to respect "insert macro" setting
- const stopString = power_user.instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;
+ const stopString = instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;
result.push(stopString);
}
}
@@ -270,14 +275,15 @@ export function getInstructStoppingSequences() {
const result = [];
- if (power_user.instruct.enabled) {
- const stop_sequence = power_user.instruct.stop_sequence || '';
- const input_sequence = power_user.instruct.input_sequence?.replace(/{{name}}/gi, name1) || '';
- const output_sequence = power_user.instruct.output_sequence?.replace(/{{name}}/gi, name2) || '';
- const first_output_sequence = power_user.instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || '';
- const last_output_sequence = power_user.instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || '';
- const system_sequence = power_user.instruct.system_sequence?.replace(/{{name}}/gi, 'System') || '';
- const last_system_sequence = power_user.instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || '';
+ // Since preset's don't have "enabled", we assume it's always enabled
+ if (customInstruct ?? instruct.enabled) {
+ const stop_sequence = instruct.stop_sequence || '';
+ const input_sequence = instruct.input_sequence?.replace(/{{name}}/gi, name1) || '';
+ const output_sequence = instruct.output_sequence?.replace(/{{name}}/gi, name2) || '';
+ const first_output_sequence = instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || '';
+ const last_output_sequence = instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || '';
+ const system_sequence = instruct.system_sequence?.replace(/{{name}}/gi, 'System') || '';
+ const last_system_sequence = instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || '';
const combined_sequence = [
stop_sequence,
@@ -292,7 +298,7 @@ export function getInstructStoppingSequences() {
combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);
}
- if (power_user.context.use_stop_strings) {
+ if (useStopStrings ?? power_user.context.use_stop_strings) {
if (power_user.context.chat_start) {
result.push(`\n${substituteParams(power_user.context.chat_start)}`);
}
diff --git a/public/scripts/personas.js b/public/scripts/personas.js
index 591b8d6ab..b302c37a0 100644
--- a/public/scripts/personas.js
+++ b/public/scripts/personas.js
@@ -111,6 +111,7 @@ export function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigate
reloadUserAvatar();
updatePersonaUIStates({ navigateToCurrent: navigateToCurrent });
selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange });
+ retriggerFirstMessageOnEmptyChat();
saveSettingsDebounced();
$('.zoomed_avatar[forchar]').remove();
}
@@ -465,7 +466,7 @@ export function initPersona(avatarId, personaName, personaDescription) {
* @returns {Promise} A promise that resolves to true if the character was converted, false otherwise.
*/
export async function convertCharacterToPersona(characterId = null) {
- if (null === characterId) characterId = this_chid;
+ if (null === characterId) characterId = Number(this_chid);
const avatarUrl = characters[characterId]?.avatar;
if (!avatarUrl) {
@@ -1243,7 +1244,7 @@ function getPersonaStates(avatarId) {
/** @type {PersonaConnection[]} */
const connections = power_user.persona_descriptions[avatarId]?.connections;
const hasCharLock = !!connections?.some(c =>
- (!selected_group && c.type === 'character' && c.id === characters[this_chid]?.avatar)
+ (!selected_group && c.type === 'character' && c.id === characters[Number(this_chid)]?.avatar)
|| (selected_group && c.type === 'group' && c.id === selected_group));
return {
@@ -1481,7 +1482,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
* @returns {string[]} - An array of persona keys that are connected to the given character key
*/
export function getConnectedPersonas(characterKey = undefined) {
- characterKey ??= selected_group || characters[this_chid]?.avatar;
+ characterKey ??= selected_group || characters[Number(this_chid)]?.avatar;
const connectedPersonas = Object.entries(power_user.persona_descriptions)
.filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey))
.map(([key, _]) => key);
@@ -1513,7 +1514,7 @@ export async function showCharConnections() {
console.log(`Unlocking persona ${personaId} from current character ${name2}`);
power_user.persona_descriptions[personaId].connections = connections.filter(c => {
if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false;
- else if (c.type == 'character' && c.id == characters[this_chid]?.avatar) return false;
+ else if (c.type == 'character' && c.id == characters[Number(this_chid)]?.avatar) return false;
return true;
});
saveSettingsDebounced();
@@ -1545,8 +1546,8 @@ export async function showCharConnections() {
export function getCurrentConnectionObj() {
if (selected_group)
return { type: 'group', id: selected_group };
- if (characters[this_chid]?.avatar)
- return { type: 'character', id: characters[this_chid]?.avatar };
+ if (characters[Number(this_chid)]?.avatar)
+ return { type: 'character', id: characters[Number(this_chid)]?.avatar };
return null;
}
@@ -1664,7 +1665,7 @@ async function syncUserNameToPersona() {
* Only works if only the first message is present, and not in group mode.
*/
export function retriggerFirstMessageOnEmptyChat() {
- if (this_chid >= 0 && !selected_group && chat.length === 1) {
+ if (Number(this_chid) >= 0 && !selected_group && chat.length === 1) {
$('#firstmessage_textarea').trigger('input');
}
}
@@ -1782,7 +1783,6 @@ function setNameCallback({ mode = 'all' }, name) {
if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
if (persona) {
autoSelectPersona(persona);
- retriggerFirstMessageOnEmptyChat();
return '';
} else if (mode === 'lookup') {
toastr.warning(`Persona ${name} not found`);
@@ -1793,7 +1793,6 @@ function setNameCallback({ mode = 'all' }, name) {
if (['temp', 'all'].includes(mode)) {
// Otherwise, set just the name
setUserName(name); //this prevented quickReply usage
- retriggerFirstMessageOnEmptyChat();
}
return '';
@@ -1944,9 +1943,6 @@ export async function initPersonas() {
$(document).on('click', '#user_avatar_block .avatar-container', function () {
const imgfile = $(this).attr('data-avatar-id');
setUserAvatar(imgfile);
-
- // force firstMes {{user}} update on persona switch
- retriggerFirstMessageOnEmptyChat();
});
$('#persona_rename_button').on('click', () => renamePersona(user_avatar));
@@ -1979,4 +1975,3 @@ export async function initPersonas() {
eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat);
switchPersonaGridView();
}
-
diff --git a/public/scripts/templates/promptManagerExportForCharacter.html b/public/scripts/templates/promptManagerExportForCharacter.html
deleted file mode 100644
index 2127f7dfb..000000000
--- a/public/scripts/templates/promptManagerExportForCharacter.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/public/scripts/templates/promptManagerExportPopup.html b/public/scripts/templates/promptManagerExportPopup.html
deleted file mode 100644
index 0beae5446..000000000
--- a/public/scripts/templates/promptManagerExportPopup.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/public/scripts/templates/worldInfoKeywordHeaders.html b/public/scripts/templates/worldInfoKeywordHeaders.html
index ec3eb946b..cf0c7f358 100644
--- a/public/scripts/templates/worldInfoKeywordHeaders.html
+++ b/public/scripts/templates/worldInfoKeywordHeaders.html
@@ -1,4 +1,4 @@
-