Add 'start-reply-with' to Connection Profiles (#3632) * Connection Profiles: Add support for 'start-reply-with' command and allow empty values for 'stop-strings' command * Add handling for empty profile values in makeFancyProfile function * Fix application of empty values * Handle undefined values * Improve argument validation * Replace || with && * I got it right this time, swear * Who wrote this?

7845994315e80da00c87c9ba109c938ba509dd22

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +98 -7Showing whitespace changes
public/scripts/extensions/connection-manager/index.js+25 -4
@@ -16,12 +16,19 @@ import { t } from '../../i18n.js';
1616
17const MODULE_NAME = 'connection-manager';17const MODULE_NAME = 'connection-manager';
18const NONE = '<None>';18const NONE = '<None>';
19const EMPTY = '<Empty>';
1920
20const DEFAULT_SETTINGS = {21const DEFAULT_SETTINGS = {
21 profiles: [],22 profiles: [],
22 selectedProfile: null,23 selectedProfile: null,
23};24};
2425
26// Commands that can record an empty value into the profile
27const ALLOW_EMPTY = [
28 'stop-strings',
29 'start-reply-with',
30];
31
25const CC_COMMANDS = [32const CC_COMMANDS = [
26 'api',33 'api',
27 'preset',34 'preset',
@@ -31,6 +38,7 @@ const CC_COMMANDS = [
31 'model',38 'model',
32 'proxy',39 'proxy',
33 'stop-strings',40 'stop-strings',
41 'start-reply-with',
34];42];
3543
36const TC_COMMANDS = [44const TC_COMMANDS = [
@@ -45,6 +53,7 @@ const TC_COMMANDS = [
45 'instruct-state',53 'instruct-state',
46 'tokenizer',54 'tokenizer',
47 'stop-strings',55 'stop-strings',
56 'start-reply-with',
48];57];
4958
50const FANCY_NAMES = {59const FANCY_NAMES = {
@@ -60,6 +69,7 @@ const FANCY_NAMES = {
60 'context': 'Context Template',69 'context': 'Context Template',
61 'tokenizer': 'Tokenizer',70 'tokenizer': 'Tokenizer',
62 'stop-strings': 'Custom Stopping Strings',71 'stop-strings': 'Custom Stopping Strings',
72 'start-reply-with': 'Start Reply With',
63};73};
6474
65/**75/**
@@ -107,6 +117,7 @@ class ConnectionManagerSpinner {
107/**117/**
108 * Get named arguments for the command callback.118 * Get named arguments for the command callback.
109 * @param {object} [args] Additional named arguments119 * @param {object} [args] Additional named arguments
120 * @param {string} [args.force] Whether to force setting the value
110 * @returns {object} Named arguments121 * @returns {object} Named arguments
111 */122 */
112function getNamedArguments(args = {}) {123function getNamedArguments(args = {}) {
@@ -142,6 +153,7 @@ const profilesProvider = () => [
142 * @property {string} [instruct-state] Instruct Mode153 * @property {string} [instruct-state] Instruct Mode
143 * @property {string} [tokenizer] Tokenizer154 * @property {string} [tokenizer] Tokenizer
144 * @property {string} [stop-strings] Custom Stopping Strings155 * @property {string} [stop-strings] Custom Stopping Strings
156 * @property {string} [start-reply-with] Start Reply With
145 * @property {string[]} [exclude] Commands to exclude157 * @property {string[]} [exclude] Commands to exclude
146 */158 */
147159
@@ -186,9 +198,10 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) {
186 continue;198 continue;
187 }199 }
188200
201 const allowEmpty = ALLOW_EMPTY.includes(command);
189 const args = getNamedArguments();202 const args = getNamedArguments();
190 const result = await SlashCommandParser.commands[command].callback(args, '');203 const result = await SlashCommandParser.commands[command].callback(args, '');
191 if (result) {204 if (result || (allowEmpty && result === '')) {
192 profile[command] = result;205 profile[command] = result;
193 continue;206 continue;
194 }207 }
@@ -309,7 +322,14 @@ async function deleteConnectionProfile() {
309 */322 */
310function makeFancyProfile(profile) {323function makeFancyProfile(profile) {
311 return Object.entries(FANCY_NAMES).reduce((acc, [key, value]) => {324 return Object.entries(FANCY_NAMES).reduce((acc, [key, value]) => {
312 if (!profile[key]) return acc;325 const allowEmpty = ALLOW_EMPTY.includes(key);
326 if (!profile[key]) {
327 if (profile[key] === '' && allowEmpty) {
328 acc[value] = EMPTY;
329 }
330 return acc;
331 }
332
313 acc[value] = profile[key];333 acc[value] = profile[key];
314 return acc;334 return acc;
315 }, {});335 }, {});
@@ -339,11 +359,12 @@ async function applyConnectionProfile(profile) {
339 }359 }
340360
341 const argument = profile[command];361 const argument = profile[command];
342 if (!argument) {362 const allowEmpty = ALLOW_EMPTY.includes(command);
363 if (!argument && !(allowEmpty && argument === '')) {
343 continue;364 continue;
344 }365 }
345 try {366 try {
346 const args = getNamedArguments();367 const args = getNamedArguments(allowEmpty ? { force: 'true' } : {});
347 await SlashCommandParser.commands[command].callback(args, argument);368 await SlashCommandParser.commands[command].callback(args, argument);
348 } catch (error) {369 } catch (error) {
349 console.error(`Failed to execute command: ${command} ${argument}`, error);370 console.error(`Failed to execute command: ${command} ${argument}`, error);
public/scripts/power-user.js+73 -3
@@ -4134,16 +4134,27 @@ $(document).ready(() => {
4134 helpString: `4134 helpString: `
4135 <div>4135 <div>
4136 Sets a list of custom stopping strings. Gets the list if no value is provided.4136 Sets a list of custom stopping strings. Gets the list if no value is provided.
4137 Use a "force" argument to force set an empty value.
4137 </div>4138 </div>
4138 <div>4139 <div>
4139 <strong>Examples:</strong>4140 <strong>Examples:</strong>
4140 </div>4141 </div>
4141 <ul>4142 <ul>
4143 <li>Force set an empty value: <pre><code class="language-stscript">/stop-strings force="true" {{noop}}</code></pre></li>
4142 <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>4144 <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
4143 <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>4145 <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
4144 </ul>4146 </ul>
4145 `,4147 `,
4146 returns: ARGUMENT_TYPE.LIST,4148 returns: ARGUMENT_TYPE.LIST,
4149 namedArgumentList: [
4150 SlashCommandNamedArgument.fromProps({
4151 name: 'force',
4152 description: 'force set a value if empty',
4153 typeList: [ARGUMENT_TYPE.BOOLEAN],
4154 defaultValue: 'false',
4155 enumList: commonEnumProviders.boolean('trueFalse')(),
4156 }),
4157 ],
4147 unnamedArgumentList: [4158 unnamedArgumentList: [
4148 SlashCommandArgument.fromProps({4159 SlashCommandArgument.fromProps({
4149 description: 'list of strings',4160 description: 'list of strings',
@@ -4152,8 +4163,20 @@ $(document).ready(() => {
4152 isRequired: false,4163 isRequired: false,
4153 }),4164 }),
4154 ],4165 ],
4155 callback: (_, value) => {4166 callback: (args, value) => {
4156 if (String(value ?? '').trim()) {4167 const force = isTrueBoolean(String(args?.force ?? false));
4168 value = String(value ?? '').trim();
4169
4170 // Skip processing if no value and not forced
4171 if (!force && !value) {
4172 return power_user.custom_stopping_strings;
4173 }
4174
4175 // Use empty array for forced empty value
4176 if (force && !value) {
4177 value = JSON.stringify([]);
4178 }
4179
4157 const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);4180 const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
4158 if (!parsedValue || !Array.isArray(parsedValue)) {4181 if (!parsedValue || !Array.isArray(parsedValue)) {
4159 throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');4182 throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
@@ -4164,9 +4187,56 @@ $(document).ready(() => {
4164 power_user.custom_stopping_strings = JSON.stringify(parsedValue);4187 power_user.custom_stopping_strings = JSON.stringify(parsedValue);
4165 $('#custom_stopping_strings').val(power_user.custom_stopping_strings);4188 $('#custom_stopping_strings').val(power_user.custom_stopping_strings);
4166 saveSettingsDebounced();4189 saveSettingsDebounced();
4167 }
41684190
4169 return power_user.custom_stopping_strings;4191 return power_user.custom_stopping_strings;
4170 },4192 },
4171 }));4193 }));
4194 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4195 name: 'start-reply-with',
4196 helpString: `
4197 <div>
4198 Sets a "Start Reply With". Gets the current value if no value is provided.
4199 Use a "force" argument to force set an empty value.
4200 </div>
4201 <div>
4202 <strong>Examples:</strong>
4203 </div>
4204 <ul>
4205 <li>Set the field value: <pre><code class="language-stscript">/start-reply-with Sure!</code></pre></li>
4206 <li>Force set an empty value: <pre><code class="language-stscript">/start-reply-with force="true" {{noop}}</code></pre></li>
4207 </ul>
4208 `,
4209 namedArgumentList: [
4210 SlashCommandNamedArgument.fromProps({
4211 name: 'force',
4212 description: 'force set a value if empty',
4213 typeList: [ARGUMENT_TYPE.BOOLEAN],
4214 defaultValue: 'false',
4215 enumList: commonEnumProviders.boolean('trueFalse')(),
4216 }),
4217 ],
4218 unnamedArgumentList:[
4219 SlashCommandArgument.fromProps({
4220 description: 'value',
4221 typeList: [ARGUMENT_TYPE.STRING],
4222 acceptsMultiple: false,
4223 isRequired: false,
4224 }),
4225 ],
4226 callback: (args, value) => {
4227 const force = isTrueBoolean(String(args?.force ?? false));
4228 value = String(value ?? '').trim();
4229
4230 // Skip processing if no value and not forced
4231 if (!force && !value) {
4232 return power_user.user_prompt_bias;
4233 }
4234
4235 power_user.user_prompt_bias = value;
4236 $('#start_reply_with').val(power_user.user_prompt_bias);
4237 saveSettingsDebounced();
4238
4239 return power_user.user_prompt_bias;
4240 },
4241 }));
4172});4242});