Feature: Enhanced Branch and Checkpoint Naming (#4993) * Add customizable name builder and max tries to getUniqueName utility - Add optional `nameBuilder` parameter to allow custom name formatting - Add `maxTries` parameter to client-side version to prevent infinite loops - Update JSDoc with new optional parameters - Use nullish coalescing to set default name builder function - Default name builder maintains existing "${baseName} (${i})" format * Add startIndex option to getUniqueName utility for flexible name generation * Enhance branch creation to use current chat name and ensure unique names - Import `getCurrentChatDetails` from script.js - Replace hardcoded branch name format with current chat name as base - Add custom `buildBranchName` function to format branch names with suffix - Strip existing " - Branch #N" suffixes before generating new branch name - Use `getUniqueName` utility with `nameBuilder` to ensure unique branch names - Remove unused `mainChat` variable and replace with `mainChatName` * Enhance checkpoint creation to use current chat name and suggest unique names - Import `getCurrentChatDetails` from script.js to get current chat name - Add custom `buildCheckpointName` function to format checkpoint names with suffix - Strip existing " - Checkpoint #N" suffixes before generating new checkpoint name - Use `getUniqueName` utility with `nameBuilder` to ensure unique checkpoint names - Pass `suggestedName` to template and popup input for better UX - Replace manual loop with `getUniqueName` * Remove automatic timestamp suffix from bookmark names * Fix getUniqueName utility to correctly handle maxTries and return null on failure * shut up copilot * Strip legacy bookmark and branch name prefixes when generating unique names - Add removal of old "Checkpoint #N - " prefix format in `buildCheckpointName` - Add removal of old "Branch #N - " prefix format in `buildBranchName` - Change `cleanName` from const to let to allow multiple replacements - Ensures clean base names regardless of legacy or current naming format

491f2a3afdbb5b62f091f59a973760bb9cfd6d51

Wolfsblvt <wolfsblvt@gmail.com>

Signed
3 files changed, +71 -31Showing whitespace changes
public/scripts/bookmarks.js+33 -13
@@ -12,6 +12,7 @@ import {
12 saveChatConditional,12 saveChatConditional,
13 saveItemizedPrompts,13 saveItemizedPrompts,
14 setActiveGroup,14 setActiveGroup,
15 getCurrentChatDetails,
15} from '../script.js';16} from '../script.js';
16import { humanizedDateTime } from './RossAscends-mods.js';17import { humanizedDateTime } from './RossAscends-mods.js';
17import {18import {
@@ -81,24 +82,29 @@ async function getExistingChatNames() {
81}82}
8283
83async function getBookmarkName({ isReplace = false, forceName = null } = {}) {84async function getBookmarkName({ isReplace = false, forceName = null } = {}) {
84 const chatNames = await getExistingChatNames();85 const mainChatName = (getCurrentChatDetails()).sessionName;
8586
86 const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace });87 function buildCheckpointName(name, i) {
87 let name = forceName ?? await Popup.show.input('Create Checkpoint', body);88 // Strip off existing suffixes, then build new name
89 let cleanName = name.replace(new RegExp(` - ${bookmarkNameToken}\\d+$`), '');
90 // Strip off legacy old name prefix too
91 cleanName = name.replace(new RegExp(`^${bookmarkNameToken}\\d+ - `), '');
92 return `${cleanName} - ${bookmarkNameToken}${i}`;
93 }
94 const existingChats = await getExistingChatNames();
95 const suggestedName = getUniqueName(mainChatName, (x) => existingChats.includes(x), { nameBuilder: buildCheckpointName });
96
97 const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace, suggestedName: suggestedName });
98 let name = forceName ?? await Popup.show.input('Create Checkpoint', body, suggestedName);
88 // Special handling for confirmed empty input (=> auto-generate name)99 // Special handling for confirmed empty input (=> auto-generate name)
89 if (name === '') {100 if (name === '') {
90 for (let i = chatNames.length; i < 1000; i++) {101 name = suggestedName;
91 name = bookmarkNameToken + i;
92 if (!chatNames.includes(name)) {
93 break;
94 }
95 }
96 }102 }
97 if (!name) {103 if (!name) {
98 return null;104 return null;
99 }105 }
100106
101 return `${name} - ${humanizedDateTime()}`;107 return name;
102}108}
103109
104function getMainChatName() {110function getMainChatName() {
@@ -170,9 +176,23 @@ export async function createBranch(mesId) {
170 }176 }
171177
172 const lastMes = chat[mesId];178 const lastMes = chat[mesId];
173 const mainChat = selected_group ? groups?.find(x => x.id == selected_group)?.chat_id : characters[this_chid].chat;179 const mainChatName = (getCurrentChatDetails()).sessionName;
174 const newMetadata = { main_chat: mainChat };180 const newMetadata = { main_chat: mainChatName };
175 let name = `Branch #${mesId} - ${humanizedDateTime()}`;181
182 function buildBranchName(name, i) {
183 // Strip off existing suffixes, then build new name
184 let cleanName = name.replace(/ - Branch #\d+$/, '');
185 // Strip off legacy old name prefix too
186 cleanName = name.replace(/^Branch #\d+ - /, '');
187 return `${cleanName} - Branch #${i}`;
188 }
189 const existingChats = await getExistingChatNames();
190 const name = getUniqueName(mainChatName, (x) => existingChats.includes(x), { nameBuilder: buildBranchName });
191 if (!name) {
192 console.error('Could not generate a unique branch name.');
193 toastr.error('Could not generate a unique branch name.', 'Branch creation failed');
194 return;
195 }
176196
177 if (selected_group) {197 if (selected_group) {
178 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);198 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);
public/scripts/utils.js+19 -9
@@ -679,18 +679,28 @@ export function isElementInViewport(el) {
679679
680/**680/**
681 * Returns a name that is unique among the names that exist.681 * Returns a name that is unique among the names that exist.
682 * @param {string} name The name to check.682 * @param {string} baseName The name to check.
683 * @param {{ (name: string): boolean; }} exists Function to check if name exists.683 * @param {{ (name: string): boolean; }} exists Function to check if name exists.
684 * @returns {string} A unique name.684 * @param {Object} [options] The options.
685 */685 * @param {((baseName: string, i: number) => string)|null} [options.nameBuilder=null] Function to build the name.
686export function getUniqueName(name, exists) {686 * Starts with the index provided by `startIndex` (default is 1). If not provided, uses "${baseName} (${i})".
687 let i = 1;687 * @param {number} [options.maxTries=1000] The maximum number of tries to find a unique name. Default is 1000.
688 let baseName = name;688 * @param {number} [options.startIndex=1] The index to start with when building the name. Default is 1.
689 while (exists(name)) {689 * When set to 0, the intention is to also check if the basename (without applied index) is free.
690 name = `${baseName} (${i})`;690 * @returns {string|null} A unique name. Null if no unique name could be found in `maxTries`.
691 */
692export function getUniqueName(baseName, exists, { nameBuilder = null, maxTries = 1000, startIndex = 1 } = {}) {
693 nameBuilder ??= (baseName, i) => i === 0 ? baseName : `${baseName} (${i})`;
694 let i = startIndex;
695 let name;
696 while (i < maxTries + startIndex) {
697 name = nameBuilder(baseName, i);
698 if (!exists(name)) {
699 return name;
700 }
691 i++;701 i++;
692 }702 }
693 return name;703 return null;
694}704}
695705
696/**706/**
src/util.js+19 -9
@@ -580,18 +580,28 @@ export function clientRelativePath(root, inputPath) {
580580
581/**581/**
582 * Returns a name that is unique among the names that exist.582 * Returns a name that is unique among the names that exist.
583 * @param {string} name The name to check.583 * @param {string} baseName The name to check.
584 * @param {{ (name: string): boolean; }} exists Function to check if name exists.584 * @param {{ (name: string): boolean; }} exists Function to check if name exists.
585 * @returns {string} A unique name.585 * @param {Object} [options] The options.
586 */586 * @param {((baseName: string, i: number) => string)|null} [options.nameBuilder=null] Function to build the name.
587export function getUniqueName(name, exists) {587 * Starts with the index provided by `startIndex` (default is 1). If not provided, uses "${baseName} (${i})".
588 let i = 1;588 * @param {number} [options.maxTries=1000] The maximum number of tries to find a unique name. Default is 1000.
589 let baseName = name;589 * @param {number} [options.startIndex=1] The index to start with when building the name. Default is 1.
590 while (exists(name)) {590 * When set to 0, the intention is to also check if the basename (without applied index) is free.
591 name = `${baseName} (${i})`;591 * @returns {string|null} A unique name. Null if no unique name could be found in `maxTries`.
592 */
593export function getUniqueName(baseName, exists, { nameBuilder = null, maxTries = 1000, startIndex = 1 } = {}) {
594 nameBuilder ??= (baseName, i) => i === 0 ? baseName : `${baseName} (${i})`;
595 let i = startIndex;
596 let name;
597 while (i < maxTries + startIndex) {
598 name = nameBuilder(baseName, i);
599 if (!exists(name)) {
600 return name;
601 }
592 i++;602 i++;
593 }603 }
594 return name;604 return null;
595}605}
596606
597/**607/**