Spaces:
Paused
Paused
| import { DOMPurify, Fuse } from '../../../lib.js'; | |
| import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; | |
| import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; | |
| import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; | |
| import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | |
| import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js'; | |
| import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | |
| import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | |
| import { SlashCommandDebugController } from '../../slash-commands/SlashCommandDebugController.js'; | |
| import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; | |
| import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | |
| import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js'; | |
| import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4 } from '../../utils.js'; | |
| import { t } from '../../i18n.js'; | |
| import { getSecretLabelById } from '../../secrets.js'; | |
| const MODULE_NAME = 'connection-manager'; | |
| const NONE = '<None>'; | |
| const EMPTY = '<Empty>'; | |
| const DEFAULT_SETTINGS = { | |
| profiles: [], | |
| selectedProfile: null, | |
| }; | |
| // Commands that can record an empty value into the profile | |
| const ALLOW_EMPTY = [ | |
| 'stop-strings', | |
| 'start-reply-with', | |
| ]; | |
| const CC_COMMANDS = [ | |
| 'api', | |
| 'preset', | |
| // Do not fix; CC needs to set the API twice because it could be overridden by the preset | |
| 'api', | |
| 'api-url', | |
| 'model', | |
| 'proxy', | |
| 'stop-strings', | |
| 'start-reply-with', | |
| 'reasoning-template', | |
| 'prompt-post-processing', | |
| 'secret-id', | |
| ]; | |
| const TC_COMMANDS = [ | |
| 'api', | |
| 'preset', | |
| 'api-url', | |
| 'model', | |
| 'sysprompt', | |
| 'sysprompt-state', | |
| 'instruct', | |
| 'context', | |
| 'instruct-state', | |
| 'tokenizer', | |
| 'stop-strings', | |
| 'start-reply-with', | |
| 'reasoning-template', | |
| 'secret-id', | |
| ]; | |
| const FANCY_NAMES = { | |
| 'api': 'API', | |
| 'api-url': 'Server URL', | |
| 'preset': 'Settings Preset', | |
| 'model': 'Model', | |
| 'proxy': 'Proxy Preset', | |
| 'sysprompt-state': 'Use System Prompt', | |
| 'sysprompt': 'System Prompt Name', | |
| 'instruct-state': 'Instruct Mode', | |
| 'instruct': 'Instruct Template', | |
| 'context': 'Context Template', | |
| 'tokenizer': 'Tokenizer', | |
| 'stop-strings': 'Custom Stopping Strings', | |
| 'start-reply-with': 'Start Reply With', | |
| 'reasoning-template': 'Reasoning Template', | |
| 'prompt-post-processing': 'Prompt Post-Processing', | |
| 'secret-id': 'Secret', | |
| }; | |
| /** | |
| * A wrapper for the connection manager spinner. | |
| */ | |
| class ConnectionManagerSpinner { | |
| /** | |
| * @type {AbortController[]} | |
| */ | |
| static abortControllers = []; | |
| /** @type {HTMLElement} */ | |
| spinnerElement; | |
| /** @type {AbortController} */ | |
| abortController = new AbortController(); | |
| constructor() { | |
| // @ts-ignore | |
| this.spinnerElement = document.getElementById('connection_profile_spinner'); | |
| this.abortController = new AbortController(); | |
| } | |
| start() { | |
| ConnectionManagerSpinner.abortControllers.push(this.abortController); | |
| this.spinnerElement.classList.remove('hidden'); | |
| } | |
| stop() { | |
| this.spinnerElement.classList.add('hidden'); | |
| } | |
| isAborted() { | |
| return this.abortController.signal.aborted; | |
| } | |
| static abort() { | |
| for (const controller of ConnectionManagerSpinner.abortControllers) { | |
| controller.abort(); | |
| } | |
| ConnectionManagerSpinner.abortControllers = []; | |
| } | |
| } | |
| /** | |
| * Get named arguments for the command callback. | |
| * @param {object} [args] Additional named arguments | |
| * @param {string} [args.force] Whether to force setting the value | |
| * @returns {object} Named arguments | |
| */ | |
| function getNamedArguments(args = {}) { | |
| // None of the commands here use underscored args, but better safe than sorry | |
| return { | |
| _scope: new SlashCommandScope(), | |
| _abortController: new SlashCommandAbortController(), | |
| _debugController: new SlashCommandDebugController(), | |
| _parserFlags: {}, | |
| _hasUnnamedArgument: false, | |
| quiet: 'true', | |
| ...args, | |
| }; | |
| } | |
| /** @type {() => SlashCommandEnumValue[]} */ | |
| const profilesProvider = () => [ | |
| new SlashCommandEnumValue(NONE), | |
| ...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, null, enumTypes.name, enumIcons.server)), | |
| ]; | |
| /** | |
| * @typedef {Object} ConnectionProfile | |
| * @property {string} id Unique identifier | |
| * @property {string} mode Mode of the connection profile | |
| * @property {string} [name] Name of the connection profile | |
| * @property {string} [api] API | |
| * @property {string} [preset] Settings Preset | |
| * @property {string} [model] Model | |
| * @property {string} [proxy] Proxy Preset | |
| * @property {string} [instruct] Instruct Template | |
| * @property {string} [context] Context Template | |
| * @property {string} [instruct-state] Instruct Mode | |
| * @property {string} [tokenizer] Tokenizer | |
| * @property {string} [stop-strings] Custom Stopping Strings | |
| * @property {string} [start-reply-with] Start Reply With | |
| * @property {string} [reasoning-template] Reasoning Template | |
| * @property {string[]} [exclude] Commands to exclude | |
| */ | |
| /** | |
| * Finds the best match for the search value. | |
| * @param {string} value Search value | |
| * @returns {ConnectionProfile|null} Best match or null | |
| */ | |
| function findProfileByName(value) { | |
| // Try to find exact match | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.name === value); | |
| if (profile) { | |
| return profile; | |
| } | |
| // Try to find fuzzy match | |
| const fuse = new Fuse(extension_settings.connectionManager.profiles, { keys: ['name'] }); | |
| const results = fuse.search(value); | |
| if (results.length === 0) { | |
| return null; | |
| } | |
| const bestMatch = results[0]; | |
| return bestMatch.item; | |
| } | |
| /** | |
| * Reads the connection profile from the commands. | |
| * @param {string} mode Mode of the connection profile | |
| * @param {ConnectionProfile} profile Connection profile | |
| * @param {boolean} [cleanUp] Whether to clean up the profile | |
| */ | |
| async function readProfileFromCommands(mode, profile, cleanUp = false) { | |
| const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; | |
| const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS; | |
| const excludeList = Array.isArray(profile.exclude) ? profile.exclude : []; | |
| for (const command of commands) { | |
| try { | |
| if (excludeList.includes(command)) { | |
| continue; | |
| } | |
| const allowEmpty = ALLOW_EMPTY.includes(command); | |
| const args = getNamedArguments(); | |
| const result = await SlashCommandParser.commands[command].callback(args, ''); | |
| if (result || (allowEmpty && result === '')) { | |
| profile[command] = result; | |
| continue; | |
| } | |
| } catch (error) { | |
| console.error(`Failed to execute command: ${command}`, error); | |
| } | |
| } | |
| if (cleanUp) { | |
| for (const command of commands) { | |
| if (command.endsWith('-state') && profile[command] === 'false') { | |
| delete profile[command.replace('-state', '')]; | |
| } | |
| } | |
| for (const command of opposingCommands) { | |
| if (commands.includes(command)) { | |
| continue; | |
| } | |
| delete profile[command]; | |
| } | |
| } | |
| } | |
| /** | |
| * Creates a new connection profile. | |
| * @param {string} [forceName] Name of the connection profile | |
| * @returns {Promise<ConnectionProfile>} Created connection profile | |
| */ | |
| async function createConnectionProfile(forceName = null) { | |
| const mode = main_api === 'openai' ? 'cc' : 'tc'; | |
| const id = uuidv4(); | |
| /** @type {ConnectionProfile} */ | |
| const profile = { | |
| id, | |
| mode, | |
| exclude: [], | |
| }; | |
| await readProfileFromCommands(mode, profile); | |
| const profileForDisplay = makeFancyProfile(profile); | |
| const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay })); | |
| template.find('input[name="exclude"]').on('input', function () { | |
| const fancyName = String($(this).val()); | |
| const keyName = Object.entries(FANCY_NAMES).find(x => x[1] === fancyName)?.[0]; | |
| if (!keyName) { | |
| console.warn('Key not found for fancy name:', fancyName); | |
| return; | |
| } | |
| if (!Array.isArray(profile.exclude)) { | |
| profile.exclude = []; | |
| } | |
| const excludeState = !$(this).prop('checked'); | |
| if (excludeState) { | |
| profile.exclude.push(keyName); | |
| } else { | |
| const index = profile.exclude.indexOf(keyName); | |
| index !== -1 && profile.exclude.splice(index, 1); | |
| } | |
| }); | |
| const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); | |
| const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); | |
| let name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); | |
| // If it's cancelled, it will be false | |
| if (!name) { | |
| return null; | |
| } | |
| name = DOMPurify.sanitize(String(name)); | |
| if (!name) { | |
| toastr.error('Name cannot be empty.'); | |
| return null; | |
| } | |
| if (isNameTaken(name) || name === NONE) { | |
| toastr.error('A profile with the same name already exists.'); | |
| return null; | |
| } | |
| if (Array.isArray(profile.exclude)) { | |
| for (const command of profile.exclude) { | |
| delete profile[command]; | |
| } | |
| } | |
| profile.name = String(name); | |
| return profile; | |
| } | |
| /** | |
| * Deletes the selected connection profile. | |
| * @returns {Promise<void>} | |
| */ | |
| async function deleteConnectionProfile() { | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| if (!selectedProfile) { | |
| return; | |
| } | |
| const index = extension_settings.connectionManager.profiles.findIndex(p => p.id === selectedProfile); | |
| if (index === -1) { | |
| return; | |
| } | |
| const profile = extension_settings.connectionManager.profiles[index]; | |
| const name = profile.name; | |
| const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name); | |
| if (!confirm) { | |
| return; | |
| } | |
| extension_settings.connectionManager.profiles.splice(index, 1); | |
| extension_settings.connectionManager.selectedProfile = null; | |
| saveSettingsDebounced(); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_DELETED, profile); | |
| } | |
| /** | |
| * Formats the connection profile for display. | |
| * @param {ConnectionProfile} profile Connection profile | |
| * @returns {Object} Fancy profile | |
| */ | |
| function makeFancyProfile(profile) { | |
| return Object.entries(FANCY_NAMES).reduce((acc, [key, value]) => { | |
| const allowEmpty = ALLOW_EMPTY.includes(key); | |
| if (!profile[key]) { | |
| if (profile[key] === '' && allowEmpty) { | |
| acc[value] = EMPTY; | |
| } | |
| return acc; | |
| } | |
| // UUID is not very useful in the UI, so we replace it with a label (if available) | |
| if (key === 'secret-id') { | |
| const label = getSecretLabelById(profile[key]); | |
| if (label) { | |
| acc[value] = label; | |
| return acc; | |
| } | |
| } | |
| acc[value] = profile[key]; | |
| return acc; | |
| }, {}); | |
| } | |
| /** | |
| * Applies the connection profile. | |
| * @param {ConnectionProfile} profile Connection profile | |
| * @returns {Promise<void>} | |
| */ | |
| async function applyConnectionProfile(profile) { | |
| if (!profile) { | |
| return; | |
| } | |
| // Abort any ongoing profile application | |
| ConnectionManagerSpinner.abort(); | |
| const mode = profile.mode; | |
| const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; | |
| const spinner = new ConnectionManagerSpinner(); | |
| spinner.start(); | |
| for (const command of commands) { | |
| if (spinner.isAborted()) { | |
| throw new Error('Profile application aborted'); | |
| } | |
| const argument = profile[command]; | |
| const allowEmpty = ALLOW_EMPTY.includes(command); | |
| if (!argument && !(allowEmpty && argument === '')) { | |
| continue; | |
| } | |
| try { | |
| const args = getNamedArguments(allowEmpty ? { force: 'true' } : {}); | |
| await SlashCommandParser.commands[command].callback(args, argument); | |
| } catch (error) { | |
| console.error(`Failed to execute command: ${command} ${argument}`, error); | |
| } | |
| } | |
| spinner.stop(); | |
| } | |
| /** | |
| * Updates the selected connection profile. | |
| * @param {ConnectionProfile} profile Connection profile | |
| * @returns {Promise<void>} | |
| */ | |
| async function updateConnectionProfile(profile) { | |
| profile.mode = main_api === 'openai' ? 'cc' : 'tc'; | |
| await readProfileFromCommands(profile.mode, profile, true); | |
| } | |
| /** | |
| * Renders the connection profile details. | |
| * @param {HTMLSelectElement} profiles Select element containing connection profiles | |
| */ | |
| function renderConnectionProfiles(profiles) { | |
| profiles.innerHTML = ''; | |
| const noneOption = document.createElement('option'); | |
| noneOption.value = ''; | |
| noneOption.textContent = NONE; | |
| noneOption.selected = !extension_settings.connectionManager.selectedProfile; | |
| profiles.appendChild(noneOption); | |
| for (const profile of extension_settings.connectionManager.profiles.sort((a, b) => a.name.localeCompare(b.name))) { | |
| const option = document.createElement('option'); | |
| option.value = profile.id; | |
| option.textContent = profile.name; | |
| option.selected = profile.id === extension_settings.connectionManager.selectedProfile; | |
| profiles.appendChild(option); | |
| } | |
| } | |
| /** | |
| * Renders the content of the details element. | |
| * @param {HTMLElement} detailsContent Content element of the details | |
| */ | |
| async function renderDetailsContent(detailsContent) { | |
| detailsContent.innerHTML = ''; | |
| if (detailsContent.classList.contains('hidden')) { | |
| return; | |
| } | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | |
| if (profile) { | |
| const profileForDisplay = makeFancyProfile(profile); | |
| const templateParams = { profile: profileForDisplay }; | |
| if (Array.isArray(profile.exclude) && profile.exclude.length > 0) { | |
| templateParams.omitted = profile.exclude.map(e => FANCY_NAMES[e]).join(', '); | |
| } | |
| const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', templateParams); | |
| detailsContent.innerHTML = template; | |
| } else { | |
| detailsContent.textContent = t`No profile selected`; | |
| } | |
| } | |
| (async function () { | |
| extension_settings.connectionManager = extension_settings.connectionManager || structuredClone(DEFAULT_SETTINGS); | |
| for (const key of Object.keys(DEFAULT_SETTINGS)) { | |
| if (extension_settings.connectionManager[key] === undefined) { | |
| extension_settings.connectionManager[key] = DEFAULT_SETTINGS[key]; | |
| } | |
| } | |
| const container = document.getElementById('rm_api_block'); | |
| const settings = await renderExtensionTemplateAsync(MODULE_NAME, 'settings'); | |
| container.insertAdjacentHTML('afterbegin', settings); | |
| /** @type {HTMLSelectElement} */ | |
| // @ts-ignore | |
| const profiles = document.getElementById('connection_profiles'); | |
| renderConnectionProfiles(profiles); | |
| function toggleProfileSpecificButtons() { | |
| const profileId = extension_settings.connectionManager.selectedProfile; | |
| const profileSpecificButtons = ['update_connection_profile', 'reload_connection_profile', 'delete_connection_profile']; | |
| profileSpecificButtons.forEach(id => document.getElementById(id).classList.toggle('disabled', !profileId)); | |
| } | |
| toggleProfileSpecificButtons(); | |
| profiles.addEventListener('change', async function () { | |
| const selectedProfile = profiles.selectedOptions[0]; | |
| if (!selectedProfile) { | |
| // Safety net for preventing the command getting stuck | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); | |
| return; | |
| } | |
| const profileId = selectedProfile.value; | |
| extension_settings.connectionManager.selectedProfile = profileId; | |
| saveSettingsDebounced(); | |
| await renderDetailsContent(detailsContent); | |
| toggleProfileSpecificButtons(); | |
| // None option selected | |
| if (!profileId) { | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); | |
| return; | |
| } | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === profileId); | |
| if (!profile) { | |
| console.log(`Profile not found: ${profileId}`); | |
| return; | |
| } | |
| await applyConnectionProfile(profile); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); | |
| }); | |
| const reloadButton = document.getElementById('reload_connection_profile'); | |
| reloadButton.addEventListener('click', async () => { | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | |
| if (!profile) { | |
| console.log('No profile selected'); | |
| return; | |
| } | |
| await applyConnectionProfile(profile); | |
| await renderDetailsContent(detailsContent); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); | |
| toastr.success('Connection profile reloaded', '', { timeOut: 1500 }); | |
| }); | |
| const createButton = document.getElementById('create_connection_profile'); | |
| createButton.addEventListener('click', async () => { | |
| const profile = await createConnectionProfile(); | |
| if (!profile) { | |
| return; | |
| } | |
| extension_settings.connectionManager.profiles.push(profile); | |
| extension_settings.connectionManager.selectedProfile = profile.id; | |
| saveSettingsDebounced(); | |
| renderConnectionProfiles(profiles); | |
| await renderDetailsContent(detailsContent); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); | |
| }); | |
| const updateButton = document.getElementById('update_connection_profile'); | |
| updateButton.addEventListener('click', async () => { | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | |
| if (!profile) { | |
| console.log('No profile selected'); | |
| return; | |
| } | |
| const oldProfile = structuredClone(profile); | |
| await updateConnectionProfile(profile); | |
| await renderDetailsContent(detailsContent); | |
| saveSettingsDebounced(); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name); | |
| toastr.success('Connection profile updated', '', { timeOut: 1500 }); | |
| }); | |
| const deleteButton = document.getElementById('delete_connection_profile'); | |
| deleteButton.addEventListener('click', async () => { | |
| await deleteConnectionProfile(); | |
| renderConnectionProfiles(profiles); | |
| await renderDetailsContent(detailsContent); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); | |
| }); | |
| const editButton = document.getElementById('edit_connection_profile'); | |
| editButton.addEventListener('click', async () => { | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | |
| if (!profile) { | |
| console.log('No profile selected'); | |
| return; | |
| } | |
| if (!Array.isArray(profile.exclude)) { | |
| profile.exclude = []; | |
| } | |
| let saveChanges = false; | |
| const sortByViewOrder = (a, b) => Object.keys(FANCY_NAMES).indexOf(a) - Object.keys(FANCY_NAMES).indexOf(b); | |
| const commands = profile.mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; | |
| const settings = commands.slice().sort(sortByViewOrder).reduce((acc, command) => { | |
| const fancyName = FANCY_NAMES[command]; | |
| acc[fancyName] = !profile.exclude.includes(command); | |
| return acc; | |
| }, {}); | |
| const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings })); | |
| let newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, { | |
| rows: 2, | |
| customButtons: [{ | |
| text: t`Save and Update`, | |
| classes: ['popup-button-ok'], | |
| result: POPUP_RESULT.AFFIRMATIVE, | |
| action: () => { | |
| saveChanges = true; | |
| }, | |
| }], | |
| }); | |
| // If it's cancelled, it will be false | |
| if (!newName) { | |
| return; | |
| } | |
| newName = DOMPurify.sanitize(String(newName)); | |
| if (!newName) { | |
| toastr.error('Name cannot be empty.'); | |
| return; | |
| } | |
| if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) { | |
| toastr.error('A profile with the same name already exists.'); | |
| return; | |
| } | |
| const newExcludeList = template.find('input[name="exclude"]:not(:checked)').map(function () { | |
| return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0]; | |
| }).get(); | |
| const oldProfile = structuredClone(profile); | |
| if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) { | |
| profile.exclude = newExcludeList; | |
| for (const command of newExcludeList) { | |
| delete profile[command]; | |
| } | |
| if (saveChanges) { | |
| await updateConnectionProfile(profile); | |
| } else { | |
| toastr.info('Press "Update" to record them into the profile.', 'Included settings list updated'); | |
| } | |
| } | |
| if (profile.name !== newName) { | |
| toastr.success('Connection profile renamed.'); | |
| profile.name = newName; | |
| } | |
| saveSettingsDebounced(); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile); | |
| renderConnectionProfiles(profiles); | |
| await renderDetailsContent(detailsContent); | |
| }); | |
| /** @type {HTMLElement} */ | |
| const viewDetails = document.getElementById('view_connection_profile'); | |
| const detailsContent = document.getElementById('connection_profile_details_content'); | |
| viewDetails.addEventListener('click', async () => { | |
| viewDetails.classList.toggle('active'); | |
| detailsContent.classList.toggle('hidden'); | |
| await renderDetailsContent(detailsContent); | |
| }); | |
| SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| name: 'profile', | |
| helpString: 'Switch to a connection profile or return the name of the current profile in no argument is provided. Use <code><None></code> to switch to no profile.', | |
| returns: 'name of the profile', | |
| unnamedArgumentList: [ | |
| SlashCommandArgument.fromProps({ | |
| description: 'Name of the connection profile', | |
| enumProvider: profilesProvider, | |
| isRequired: false, | |
| }), | |
| ], | |
| namedArgumentList: [ | |
| SlashCommandNamedArgument.fromProps({ | |
| name: 'await', | |
| description: 'Wait for the connection profile to be applied before returning.', | |
| isRequired: false, | |
| typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| defaultValue: 'true', | |
| enumList: commonEnumProviders.boolean('trueFalse')(), | |
| }), | |
| ], | |
| callback: async (args, value) => { | |
| if (!value || typeof value !== 'string') { | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | |
| if (!profile) { | |
| return NONE; | |
| } | |
| return profile.name; | |
| } | |
| if (value === NONE) { | |
| profiles.selectedIndex = 0; | |
| profiles.dispatchEvent(new Event('change')); | |
| return NONE; | |
| } | |
| const profile = findProfileByName(value); | |
| if (!profile) { | |
| return ''; | |
| } | |
| const shouldAwait = !isFalseBoolean(String(args?.await)); | |
| const awaitPromise = new Promise((resolve) => eventSource.once(event_types.CONNECTION_PROFILE_LOADED, resolve)); | |
| profiles.selectedIndex = Array.from(profiles.options).findIndex(o => o.value === profile.id); | |
| profiles.dispatchEvent(new Event('change')); | |
| if (shouldAwait) { | |
| await awaitPromise; | |
| } | |
| return profile.name; | |
| }, | |
| })); | |
| SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| name: 'profile-list', | |
| helpString: 'List all connection profile names.', | |
| returns: 'list of profile names', | |
| callback: () => JSON.stringify(extension_settings.connectionManager.profiles.map(p => p.name)), | |
| })); | |
| SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| name: 'profile-create', | |
| returns: 'name of the new profile', | |
| helpString: 'Create a new connection profile using the current settings.', | |
| unnamedArgumentList: [ | |
| SlashCommandArgument.fromProps({ | |
| description: 'name of the new connection profile', | |
| isRequired: true, | |
| typeList: [ARGUMENT_TYPE.STRING], | |
| }), | |
| ], | |
| callback: async (_args, name) => { | |
| if (!name || typeof name !== 'string') { | |
| toastr.warning('Please provide a name for the new connection profile.'); | |
| return ''; | |
| } | |
| const profile = await createConnectionProfile(name); | |
| if (!profile) { | |
| return ''; | |
| } | |
| extension_settings.connectionManager.profiles.push(profile); | |
| extension_settings.connectionManager.selectedProfile = profile.id; | |
| saveSettingsDebounced(); | |
| renderConnectionProfiles(profiles); | |
| await renderDetailsContent(detailsContent); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_CREATED, profile); | |
| return profile.name; | |
| }, | |
| })); | |
| SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| name: 'profile-update', | |
| helpString: 'Update the selected connection profile.', | |
| callback: async () => { | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | |
| if (!profile) { | |
| toastr.warning('No profile selected.'); | |
| return ''; | |
| } | |
| const oldProfile = structuredClone(profile); | |
| await updateConnectionProfile(profile); | |
| await renderDetailsContent(detailsContent); | |
| saveSettingsDebounced(); | |
| await eventSource.emit(event_types.CONNECTION_PROFILE_UPDATED, oldProfile, profile); | |
| return profile.name; | |
| }, | |
| })); | |
| SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| name: 'profile-get', | |
| helpString: 'Get the details of the connection profile. Returns the selected profile if no argument is provided.', | |
| returns: 'object of the selected profile', | |
| unnamedArgumentList: [ | |
| SlashCommandArgument.fromProps({ | |
| description: 'Name of the connection profile', | |
| enumProvider: profilesProvider, | |
| isRequired: false, | |
| }), | |
| ], | |
| callback: async (_args, value) => { | |
| if (!value || typeof value !== 'string') { | |
| const selectedProfile = extension_settings.connectionManager.selectedProfile; | |
| const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | |
| if (!profile) { | |
| return ''; | |
| } | |
| return JSON.stringify(profile); | |
| } | |
| const profile = findProfileByName(value); | |
| if (!profile) { | |
| return ''; | |
| } | |
| return JSON.stringify(profile); | |
| }, | |
| })); | |
| })(); | |