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
Signed| @@ -12,6 +12,7 @@ import { | ||
| 12 | 12 | saveChatConditional, |
| 13 | 13 | saveItemizedPrompts, |
| 14 | 14 | setActiveGroup, |
| 15 | + getCurrentChatDetails, | |
| 15 | 16 | } from '../script.js'; |
| 16 | 17 | import { humanizedDateTime } from './RossAscends-mods.js'; |
| 17 | 18 | import { |
| @@ -81,24 +82,29 @@ async function getExistingChatNames() { | ||
| 81 | 82 | } |
| 82 | 83 | |
| 83 | 84 | async function getBookmarkName({ isReplace = false, forceName = null } = {}) { |
| 84 | 85 | const chatNamesmainChatName = await getExistingChatNames(getCurrentChatDetails()).sessionName; |
| 86 | + | |
| 87 | + function buildCheckpointName(name, i) { | |
| 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 }); | |
| 85 | 96 | |
| 86 | 97 | const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace, suggestedName: suggestedName }); |
| 87 | 98 | let name = forceName ?? await Popup.show.input('Create Checkpoint', body, suggestedName); |
| 88 | 99 | // Special handling for confirmed empty input (=> auto-generate name) |
| 89 | 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 | 103 | if (!name) { |
| 98 | 104 | return null; |
| 99 | 105 | } |
| 100 | 106 | |
| 101 | - return `${name} - ${humanizedDateTime()}`; | |
| 107 | + return name; | |
| 102 | 108 | } |
| 103 | 109 | |
| 104 | 110 | function getMainChatName() { |
| @@ -170,9 +176,23 @@ export async function createBranch(mesId) { | ||
| 170 | 176 | } |
| 171 | 177 | |
| 172 | 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 | 180 | const newMetadata = { main_chat: mainChatmainChatName }; |
| 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 | + } | |
| 176 | 196 | |
| 177 | 197 | if (selected_group) { |
| 178 | 198 | await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId); |
| @@ -679,18 +679,28 @@ export function isElementInViewport(el) { | ||
| 679 | 679 | |
| 680 | 680 | /** |
| 681 | 681 | * Returns a name that is unique among the names that exist. |
| 682 | 682 | * @param {string} namebaseName The name to check. |
| 683 | 683 | * @param {{ (name: string): boolean; }} exists Function to check if name exists. |
| 684 | 684 | * @returnsparam {stringObject} A[options] uniqueThe nameoptions. |
| 685 | - */ | |
| 685 | + * @param {((baseName: string, i: number) => string)|null} [options.nameBuilder=null] Function to build the name. | |
| 686 | -export 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 | + */ | |
| 692 | +export 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 | 701 | i++; |
| 692 | 702 | } |
| 693 | 703 | return namenull; |
| 694 | 704 | } |
| 695 | 705 | |
| 696 | 706 | /** |
| @@ -580,18 +580,28 @@ export function clientRelativePath(root, inputPath) { | ||
| 580 | 580 | |
| 581 | 581 | /** |
| 582 | 582 | * Returns a name that is unique among the names that exist. |
| 583 | 583 | * @param {string} namebaseName The name to check. |
| 584 | 584 | * @param {{ (name: string): boolean; }} exists Function to check if name exists. |
| 585 | 585 | * @returnsparam {stringObject} A[options] uniqueThe nameoptions. |
| 586 | + * @param {((baseName: string, i: number) => string)|null} [options.nameBuilder=null] Function to build the name. | |
| 587 | + * Starts with the index provided by `startIndex` (default is 1). If not provided, uses "${baseName} (${i})". | |
| 588 | + * @param {number} [options.maxTries=1000] The maximum number of tries to find a unique name. Default is 1000. | |
| 589 | + * @param {number} [options.startIndex=1] The index to start with when building the name. Default is 1. | |
| 590 | + * When set to 0, the intention is to also check if the basename (without applied index) is free. | |
| 591 | + * @returns {string|null} A unique name. Null if no unique name could be found in `maxTries`. | |
| 586 | 592 | */ |
| 587 | -export function getUniqueName(name, exists) { | |
| 593 | +export function getUniqueName(baseName, exists, { nameBuilder = null, maxTries = 1000, startIndex = 1 } = {}) { | |
| 588 | - let i = 1; | |
| 594 | + nameBuilder ??= (baseName, i) => i === 0 ? baseName : `${baseName} (${i})`; | |
| 589 | 595 | let baseNamei = namestartIndex; |
| 590 | - while (exists(name)) { | |
| 596 | + let name; | |
| 591 | - name = `${baseName} (${i})`; | |
| 597 | + while (i < maxTries + startIndex) { | |
| 598 | + name = nameBuilder(baseName, i); | |
| 599 | + if (!exists(name)) { | |
| 600 | + return name; | |
| 601 | + } | |
| 592 | 602 | i++; |
| 593 | 603 | } |
| 594 | 604 | return namenull; |
| 595 | 605 | } |
| 596 | 606 | |
| 597 | 607 | /** |