Enhanced /persona-sync Command with Range, Name Filter, and Quiet Mode (#5460)

* Add range and silent parameters to /persona-sync command with optional confirmation suppression

- Add optional start/end range parameters to syncUserNameToPersona() function
- Add silent parameter to suppress confirmation popup when needed
- Update /persona-sync slash command to accept range argument (index or range string) and named silent argument
- Parse range using stringToRange() utility, default to full chat if not provided
- Update confirmation message to reflect whether syncing all messages or specified

* Add `from` named argument to /persona-sync command for filtering by persona name

- Add `from` named argument to filter messages by persona name (case-insensitive)
- Rename `silent` argument to `quiet` with inverted default (true) for consistency
- Add userMessageNamesEnumProvider() to provide autocomplete for existing user message names in chat
- Update syncUserNameToPersona() to accept nameFilter parameter and filter messages accordingly
- Update confirmation message to reflect name filtering when

* Add async/await wrapper to sync_name_button click handler for proper promise handling

Function now has arguments, so using just the function as the event is shown as wrong usage

* Post-merge imports fix

* Use canonical command name in examples

---------

Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
Wolfsblvt
2026-04-15 22:41:10 +02:00
committed by GitHub
parent 3f72d3df80
commit 0ac31c8fcd
+126 -13
View File
@@ -24,7 +24,30 @@ import {
} from '../script.js';
import { persona_description_positions, power_user } from './power-user.js';
import { getTokenCountAsync } from './tokenizers.js';
import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler, addLongPressEvent, uuidv4 } from './utils.js';
import {
PAGINATION_TEMPLATE,
clearInfoBlock,
debounce,
delay,
download,
ensureImageFormatSupported,
flashHighlight,
getBase64Async,
getCharIndex,
isFalseBoolean,
isTrueBoolean,
onlyUnique,
parseJsonFile,
setInfoBlock,
localizePagination,
renderPaginationDropdown,
paginationDropdownChangeHandler,
addLongPressEvent,
stringToRange,
sortIgnoreCaseAndAccents,
equalsIgnoreCaseAndAccents,
uuidv4,
} from './utils.js';
import { debounce_timeout } from './constants.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
import { groups, selected_group } from './group-chats.js';
@@ -36,8 +59,8 @@ import { saveMetadataDebounced } from './extensions.js';
import { accountStorage } from './util/AccountStorage.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { isFirefox } from './browser-fixes.js';
@@ -1760,15 +1783,36 @@ async function onPersonasRestoreInput(e) {
$('#personas_restore_input').val('');
}
async function syncUserNameToPersona() {
const confirmation = await Popup.show.confirm(t`Are you sure?`, t`All user-sent messages in this chat will be attributed to ${name1}.`);
/**
* Synchronizes user-sent messages in the chat to the current persona.
* @param {object} [options={}] - Optional parameters
* @param {number} [options.start=0] - Start index of the message range (inclusive)
* @param {number} [options.end=chat.length - 1] - End index of the message range (inclusive)
* @param {boolean} [options.quiet=false] - If true, skips the confirmation popup
* @param {string} [options.nameFilter=''] - Filter messages by name (case-insensitive)
* @returns {Promise<void>}
*/
async function syncUserNameToPersona({ start = 0, end = chat.length - 1, quiet = false, nameFilter = '' } = {}) {
const isRangeAll = start === 0 && end === chat.length - 1;
const hasNameFilter = nameFilter?.trim();
const confirmMessage = isRangeAll && !hasNameFilter
? t`All user-sent messages in this chat will be attributed to ${name1}.`
: isRangeAll && hasNameFilter
? t`User-sent messages with name "${nameFilter}" will be attributed to ${name1}.`
: !isRangeAll && !hasNameFilter
? t`User-sent messages in the specified range will be attributed to ${name1}.`
: t`User-sent messages with name "${nameFilter}" in the specified range will be attributed to ${name1}.`;
if (!confirmation) {
return;
if (!quiet) {
const confirmation = await Popup.show.confirm(t`Are you sure?`, confirmMessage);
if (!confirmation) {
return;
}
}
for (const mes of chat) {
if (mes.is_user) {
for (let i = start; i <= end; i++) {
const mes = chat[i];
if (mes?.is_user && (!hasNameFilter || equalsIgnoreCaseAndAccents(mes.name, nameFilter))) {
mes.name = name1;
mes.force_avatar = getThumbnailUrl('persona', user_avatar);
}
@@ -1931,11 +1975,37 @@ async function setNameCallback({ mode = 'all' }, name) {
return '';
}
function syncCallback() {
$('#sync_name_button').trigger('click');
async function syncCallback(args, value) {
const range = value ? stringToRange(value, 0, chat.length - 1) : null;
if (value && !range) {
console.warn(`WARN: Invalid range provided for /persona-sync command: ${value}`);
return '';
}
const quiet = !isFalseBoolean(args?.quiet);
const nameFilter = typeof args?.from === 'string' ? args.from.trim() : '';
const start = range ? range.start : 0;
const end = range ? range.end : chat.length - 1;
await syncUserNameToPersona({ start, end, quiet, nameFilter });
return '';
}
/**
* Returns all unique user message names in the current chat for enum autocomplete.
* @returns {SlashCommandEnumValue[]}
*/
function userMessageNamesEnumProvider() {
return chat
.filter(mes => mes.is_user)
.map(mes => mes.name)
.filter(onlyUnique)
.sort(sortIgnoreCaseAndAccents)
.map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.persona));
}
function registerPersonaSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'persona-lock',
@@ -1988,7 +2058,50 @@ function registerPersonaSlashCommands() {
name: 'persona-sync',
aliases: ['sync'],
callback: syncCallback,
helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'from',
description: t`only sync messages from a certain persona name`,
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: userMessageNamesEnumProvider,
}),
SlashCommandNamedArgument.fromProps({
name: 'quiet',
description: t`suppress the confirmation popup`,
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumList: commonEnumProviders.boolean('trueFalse')(),
defaultValue: 'true',
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: t`message index (starts with 0) or range, syncs all user messages if not provided`,
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
defaultValue: '0-{{lastMessageId}}',
}),
],
helpString: `
<div>
${t`Syncs the user persona (name and avatar) in user-attributed messages in the current chat.`}
</div>
<div>
${t`If <code>from</code> is set, only messages with that specific persona name will be synced. Useful when multiple personas have been used in the same chat.`}
</div>
<div>
${t`If <code>quiet</code> is set to <code>false</code>, a confirmation popup will be shown before syncing.`}
</div>
<div>
<strong>${t`Examples:`}</strong>
<ul>
<li><pre><code>/persona-sync</code></pre> ${t`- Sync all user messages`}</li>
<li><pre><code>/persona-sync 5</code></pre> ${t`- Sync only message 5`}</li>
<li><pre><code>/persona-sync 0-10</code></pre> ${t`- Sync messages 0 through 10`}</li>
<li><pre><code>/persona-sync from=OldPersona 0-20</code></pre> ${t`- Sync only messages with name "OldPersona" in range 0-20`}</li>
<li><pre><code>/persona-sync quiet=false</code></pre> ${t`- Sync all with confirmation popup`}</li>
<li><pre><code>/persona-sync from=TempName quiet=false 5-15</code></pre> ${t`- Sync messages with name "TempName" in range 5-15 with confirmation`}</li>
</ul>
</div>
`,
}));
}
@@ -2046,7 +2159,7 @@ export async function initPersonas() {
debouncedPersonaSearch(searchQuery);
});
$('#sync_name_button').on('click', syncUserNameToPersona);
$('#sync_name_button').on('click', async () => await syncUserNameToPersona());
$('#avatar_upload_file').on('change', changeUserAvatar);
$(document).on('click', '#user_avatar_block .avatar-container', async function () {