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 | saveChatConditional, | 12 | saveChatConditional, |
| 13 | saveItemizedPrompts, | 13 | saveItemizedPrompts, |
| 14 | setActiveGroup, | 14 | setActiveGroup, |
| 15 | getCurrentChatDetails, | ||
| 15 | } from '../script.js'; | 16 | } from '../script.js'; |
| 16 | import { humanizedDateTime } from './RossAscends-mods.js'; | 17 | import { humanizedDateTime } from './RossAscends-mods.js'; |
| 17 | import { | 18 | import { |
| @@ -81,24 +82,29 @@ async function getExistingChatNames() { | |||
| 81 | } | 82 | } |
| 82 | 83 | ||
| 83 | async function getBookmarkName({ isReplace = false, forceName = null } = {}) { | 84 | async function getBookmarkName({ isReplace = false, forceName = null } = {}) { |
| 84 | const chatNames = await getExistingChatNames(); | 85 | const mainChatName = (getCurrentChatDetails()).sessionName; |
| 85 | 86 | ||
| 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 | } |
| 100 | 106 | ||
| 101 | return `${name} - ${humanizedDateTime()}`; | 107 | return name; |
| 102 | } | 108 | } |
| 103 | 109 | ||
| 104 | function getMainChatName() { | 110 | function getMainChatName() { |
| @@ -170,9 +176,23 @@ export async function createBranch(mesId) { | |||
| 170 | } | 176 | } |
| 171 | 177 | ||
| 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 | } | ||
| 176 | 196 | ||
| 177 | if (selected_group) { | 197 | if (selected_group) { |
| 178 | await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId); | 198 | await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId); |
| @@ -679,18 +679,28 @@ export function isElementInViewport(el) { | |||
| 679 | 679 | ||
| 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. |
| 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 | i++; | 701 | i++; |
| 692 | } | 702 | } |
| 693 | return name; | 703 | return null; |
| 694 | } | 704 | } |
| 695 | 705 | ||
| 696 | /** | 706 | /** |
| @@ -580,18 +580,28 @@ export function clientRelativePath(root, inputPath) { | |||
| 580 | 580 | ||
| 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. |
| 587 | export 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 | */ | ||
| 593 | export 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 | } |
| 596 | 606 | ||
| 597 | /** | 607 | /** |