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>

0ac31c8fcdc91818ff6f5238ee41ac1ce3f7b385

Wolfsblvt <wolfsblvt@gmail.com>

Signed
1 files changed, +127 -14Ignore whitespace
public/scripts/personas.js+127 -14
@@ -24,7 +24,30 @@ import {
2424} from '../script.js';
2525import { persona_description_positions, power_user } from './power-user.js';
2626import { getTokenCountAsync } from './tokenizers.js';
27-import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler, addLongPressEvent, uuidv4 } from './utils.js';
27+import {
28+ PAGINATION_TEMPLATE,
29+ clearInfoBlock,
30+ debounce,
31+ delay,
32+ download,
33+ ensureImageFormatSupported,
34+ flashHighlight,
35+ getBase64Async,
36+ getCharIndex,
37+ isFalseBoolean,
38+ isTrueBoolean,
39+ onlyUnique,
40+ parseJsonFile,
41+ setInfoBlock,
42+ localizePagination,
43+ renderPaginationDropdown,
44+ paginationDropdownChangeHandler,
45+ addLongPressEvent,
46+ stringToRange,
47+ sortIgnoreCaseAndAccents,
48+ equalsIgnoreCaseAndAccents,
49+ uuidv4,
50+} from './utils.js';
2851import { debounce_timeout } from './constants.js';
2952import { FILTER_TYPES, FilterHelper } from './filters.js';
3053import { groups, selected_group } from './group-chats.js';
@@ -36,8 +59,8 @@ import { saveMetadataDebounced } from './extensions.js';
3659import { accountStorage } from './util/AccountStorage.js';
3760import { SlashCommand } from './slash-commands/SlashCommand.js';
3861import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
3962import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
4063import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
4164import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
4265import { isFirefox } from './browser-fixes.js';
4366
@@ -1760,15 +1783,36 @@ async function onPersonasRestoreInput(e) {
17601783 $('#personas_restore_input').val('');
17611784}
17621785
1763-async function syncUserNameToPersona() {
1786+/**
1764- const confirmation = await Popup.show.confirm(t`Are you sure?`, t`All user-sent messages in this chat will be attributed to ${name1}.`);
1787+ * Synchronizes user-sent messages in the chat to the current persona.
1765-
1788+ * @param {object} [options={}] - Optional parameters
1766- if (!confirmation) {
1789+ * @param {number} [options.start=0] - Start index of the message range (inclusive)
1767- return;
1790+ * @param {number} [options.end=chat.length - 1] - End index of the message range (inclusive)
1791+ * @param {boolean} [options.quiet=false] - If true, skips the confirmation popup
1792+ * @param {string} [options.nameFilter=''] - Filter messages by name (case-insensitive)
1793+ * @returns {Promise<void>}
1794+ */
1795+async function syncUserNameToPersona({ start = 0, end = chat.length - 1, quiet = false, nameFilter = '' } = {}) {
1796+ const isRangeAll = start === 0 && end === chat.length - 1;
1797+ const hasNameFilter = nameFilter?.trim();
1798+ const confirmMessage = isRangeAll && !hasNameFilter
1799+ ? t`All user-sent messages in this chat will be attributed to ${name1}.`
1800+ : isRangeAll && hasNameFilter
1801+ ? t`User-sent messages with name "${nameFilter}" will be attributed to ${name1}.`
1802+ : !isRangeAll && !hasNameFilter
1803+ ? t`User-sent messages in the specified range will be attributed to ${name1}.`
1804+ : t`User-sent messages with name "${nameFilter}" in the specified range will be attributed to ${name1}.`;
1805+
1806+ if (!quiet) {
1807+ const confirmation = await Popup.show.confirm(t`Are you sure?`, confirmMessage);
1808+ if (!confirmation) {
1809+ return;
1810+ }
17681811 }
17691812
1770- for (const mes of chat) {
1813+ for (let i = start; i <= end; i++) {
1771- if (mes.is_user) {
1814+ const mes = chat[i];
1815+ if (mes?.is_user && (!hasNameFilter || equalsIgnoreCaseAndAccents(mes.name, nameFilter))) {
17721816 mes.name = name1;
17731817 mes.force_avatar = getThumbnailUrl('persona', user_avatar);
17741818 }
@@ -1931,11 +1975,37 @@ async function setNameCallback({ mode = 'all' }, name) {
19311975 return '';
19321976}
19331977
19341978async function syncCallback(args, value) {
1935- $('#sync_name_button').trigger('click');
1979+ const range = value ? stringToRange(value, 0, chat.length - 1) : null;
1980+
1981+ if (value && !range) {
1982+ console.warn(`WARN: Invalid range provided for /persona-sync command: ${value}`);
1983+ return '';
1984+ }
1985+
1986+ const quiet = !isFalseBoolean(args?.quiet);
1987+ const nameFilter = typeof args?.from === 'string' ? args.from.trim() : '';
1988+ const start = range ? range.start : 0;
1989+ const end = range ? range.end : chat.length - 1;
1990+
1991+ await syncUserNameToPersona({ start, end, quiet, nameFilter });
1992+
19361993 return '';
19371994}
19381995
1996+/**
1997+ * Returns all unique user message names in the current chat for enum autocomplete.
1998+ * @returns {SlashCommandEnumValue[]}
1999+ */
2000+function userMessageNamesEnumProvider() {
2001+ return chat
2002+ .filter(mes => mes.is_user)
2003+ .map(mes => mes.name)
2004+ .filter(onlyUnique)
2005+ .sort(sortIgnoreCaseAndAccents)
2006+ .map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.persona));
2007+}
2008+
19392009function registerPersonaSlashCommands() {
19402010 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
19412011 name: 'persona-lock',
@@ -1988,7 +2058,50 @@ function registerPersonaSlashCommands() {
19882058 name: 'persona-sync',
19892059 aliases: ['sync'],
19902060 callback: syncCallback,
1991- helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
2061+ namedArgumentList: [
2062+ SlashCommandNamedArgument.fromProps({
2063+ name: 'from',
2064+ description: t`only sync messages from a certain persona name`,
2065+ typeList: [ARGUMENT_TYPE.STRING],
2066+ enumProvider: userMessageNamesEnumProvider,
2067+ }),
2068+ SlashCommandNamedArgument.fromProps({
2069+ name: 'quiet',
2070+ description: t`suppress the confirmation popup`,
2071+ typeList: [ARGUMENT_TYPE.BOOLEAN],
2072+ enumList: commonEnumProviders.boolean('trueFalse')(),
2073+ defaultValue: 'true',
2074+ }),
2075+ ],
2076+ unnamedArgumentList: [
2077+ SlashCommandArgument.fromProps({
2078+ description: t`message index (starts with 0) or range, syncs all user messages if not provided`,
2079+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
2080+ defaultValue: '0-{{lastMessageId}}',
2081+ }),
2082+ ],
2083+ helpString: `
2084+ <div>
2085+ ${t`Syncs the user persona (name and avatar) in user-attributed messages in the current chat.`}
2086+ </div>
2087+ <div>
2088+ ${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.`}
2089+ </div>
2090+ <div>
2091+ ${t`If <code>quiet</code> is set to <code>false</code>, a confirmation popup will be shown before syncing.`}
2092+ </div>
2093+ <div>
2094+ <strong>${t`Examples:`}</strong>
2095+ <ul>
2096+ <li><pre><code>/persona-sync</code></pre> ${t`- Sync all user messages`}</li>
2097+ <li><pre><code>/persona-sync 5</code></pre> ${t`- Sync only message 5`}</li>
2098+ <li><pre><code>/persona-sync 0-10</code></pre> ${t`- Sync messages 0 through 10`}</li>
2099+ <li><pre><code>/persona-sync from=OldPersona 0-20</code></pre> ${t`- Sync only messages with name "OldPersona" in range 0-20`}</li>
2100+ <li><pre><code>/persona-sync quiet=false</code></pre> ${t`- Sync all with confirmation popup`}</li>
2101+ <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>
2102+ </ul>
2103+ </div>
2104+ `,
19922105 }));
19932106}
19942107
@@ -2046,7 +2159,7 @@ export async function initPersonas() {
20462159 debouncedPersonaSearch(searchQuery);
20472160 });
20482161
20492162 $('#sync_name_button').on('click', async () => await syncUserNameToPersona());
20502163 $('#avatar_upload_file').on('change', changeUserAvatar);
20512164
20522165 $(document).on('click', '#user_avatar_block .avatar-container', async function () {