Merge branch 'staging' into geminiStructured

7fd0f3e2bfc9fbdfb6518c1fe526bf95d52e5109

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

27 files changed, +1387 -56Ignore whitespace
default/config.yaml+8 -2
@@ -1,8 +1,6 @@
11# -- DATA CONFIGURATION --
22# Root directory for user data storage
33dataRoot: ./data
4-# The maximum amount of memory that parsed character cards can use in MB
5-cardsCacheCapacity: 100
64# -- SERVER CONFIGURATION --
75# Listen for incoming connections
86listen: false
@@ -135,6 +133,14 @@ thumbnails:
135133 # Maximum thumbnail dimensions per type [width, height]
136134 dimensions: { 'bg': [160, 90], 'avatar': [96, 144] }
137135
136+# PERFORMANCE-RELATED CONFIGURATION
137+performance:
138+ # Enables lazy loading of character cards. Improves performances with large card libraries.
139+ # May have compatibility issues with some extensions.
140+ lazyLoadCharacters: false
141+ # The maximum amount of memory that parsed character cards can use. Set to 0 to disable memory caching.
142+ memoryCacheCapacity: '100mb'
143+
138144# Allow secret keys exposure via API
139145allowKeysExposure: false
140146# Skip new default content checks
index.d.ts+31 -3
@@ -1,8 +1,36 @@
11import { UserDirectoryList, UserEventEmitter } from "./src/users"'node:events';
22import { CommandLineArgumentsCsrfSyncedToken } from "./src/command'csrf-line"sync';
33import { CsrfSyncedTokenUserDirectoryList, User } from "csrf-sync"'./src/users.js';
4+import { CommandLineArguments } from './src/command-line.js';
5+import { EVENT_NAMES } from './src/server-events.js';
6+
7+/**
8+ * Event payload for SERVER_STARTED event.
9+ */
10+export interface ServerStartedEvent {
11+ /**
12+ * The URL the server is listening on.
13+ */
14+ url: URL;
15+}
16+
17+/**
18+ * Map of all server events to their payload types.
19+ */
20+export interface ServerEventMap {
21+ [EVENT_NAMES.SERVER_STARTED]: [ServerStartedEvent];
22+}
423
524declare global {
25+ declare namespace NodeJS {
26+ export interface Process {
27+ /**
28+ * A global instance of the server events emitter.
29+ */
30+ serverEvents: EventEmitter<ServerEventMap>;
31+ }
32+ }
33+
634 declare namespace CookieSessionInterfaces {
735 export interface CookieSessionObject {
836 /**
package-lock.json+12 -0
@@ -21,6 +21,7 @@
2121 "bing-translate-api": "^4.0.2",
2222 "body-parser": "^1.20.2",
2323 "bowser": "^2.11.0",
24+ "bytes": "^3.1.2",
2425 "chalk": "^5.4.1",
2526 "command-exists": "^1.2.9",
2627 "compression": "^1.8.0",
@@ -83,6 +84,7 @@
8384 },
8485 "devDependencies": {
8586 "@types/archiver": "^6.0.3",
87+ "@types/bytes": "^3.1.5",
8688 "@types/command-exists": "^1.2.3",
8789 "@types/compression": "^1.7.5",
8890 "@types/cookie-parser": "^1.4.8",
@@ -1105,6 +1107,13 @@
11051107 "@types/node": "*"
11061108 }
11071109 },
1110+ "node_modules/@types/bytes": {
1111+ "version": "3.1.5",
1112+ "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.5.tgz",
1113+ "integrity": "sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==",
1114+ "dev": true,
1115+ "license": "MIT"
1116+ },
11081117 "node_modules/@types/cacheable-request": {
11091118 "version": "6.0.3",
11101119 "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -3399,6 +3408,9 @@
33993408 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
34003409 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
34013410 "license": "MIT",
3411+ "dependencies": {
3412+ "get-intrinsic": "^1.2.4"
3413+ },
34023414 "engines": {
34033415 "node": ">= 0.4"
34043416 }
package.json+4 -1
@@ -11,6 +11,7 @@
1111 "bing-translate-api": "^4.0.2",
1212 "body-parser": "^1.20.2",
1313 "bowser": "^2.11.0",
14+ "bytes": "^3.1.2",
1415 "chalk": "^5.4.1",
1516 "command-exists": "^1.2.9",
1617 "compression": "^1.8.0",
@@ -92,7 +93,8 @@
9293 "version": "1.12.12",
9394 "scripts": {
9495 "start": "node server.js",
9596 "debug": "node server.js --inspect server.js",
97+ "electron": "electron ./src/electron",
9698 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
9799 "start:bun": "bun server.js",
98100 "start:no-csrf": "node server.js --disableCsrf",
@@ -112,6 +114,7 @@
112114 "main": "server.js",
113115 "devDependencies": {
114116 "@types/archiver": "^6.0.3",
117+ "@types/bytes": "^3.1.5",
115118 "@types/command-exists": "^1.2.3",
116119 "@types/compression": "^1.7.5",
117120 "@types/cookie-parser": "^1.4.8",
post-install.js+5 -0
@@ -96,6 +96,11 @@ const keyMigrationMap = [
9696 newKey: 'logging.minLogLevel',
9797 migrate: (value) => value,
9898 },
99+ {
100+ oldKey: 'cardsCacheCapacity',
101+ newKey: 'performance.memoryCacheCapacity',
102+ migrate: (value) => `${value}mb`,
103+ },
99104 // uncomment one release after 1.12.13
100105 /*
101106 {
public/index.html+12 -1
@@ -1951,7 +1951,18 @@
19511951 </span>
19521952 </div>
19531953 </div>
19541954 <div class="range-block" data-source="openai,cohere,mistralai,custom,claudemakersuite,openrouter,groq,deepseek">
1955+ <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
1956+ <input id="openai_enable_web_search" type="checkbox" />
1957+ <span data-i18n="Enable web search">Enable web search</span>
1958+ </label>
1959+ <div class="flexBasis100p toggle-description justifyLeft">
1960+ <span>
1961+ Use search capabilities provided by the backend.
1962+ </span>
1963+ </div>
1964+ </div>
1965+ <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite">
19551966 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19561967 <input id="openai_function_calling" type="checkbox" />
19571968 <span data-i18n="Enable function calling">Enable function calling</span>
public/script.js+40 -3
@@ -453,6 +453,8 @@ export const event_types = {
453453 MESSAGE_DELETED: 'message_deleted',
454454 MESSAGE_UPDATED: 'message_updated',
455455 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
456+ MESSAGE_REASONING_EDITED: 'message_reasoning_edited',
457+ MESSAGE_REASONING_DELETED: 'message_reasoning_deleted',
456458 MORE_MESSAGES_LOADED: 'more_messages_loaded',
457459 IMPERSONATE_READY: 'impersonate_ready',
458460 CHAT_CHANGED: 'chat_id_changed',
@@ -1780,9 +1782,7 @@ export async function getCharacters() {
17801782 const response = await fetch('/api/characters/all', {
17811783 method: 'POST',
17821784 headers: getRequestHeaders(),
17831785 body: JSON.stringify({}),
1784- '': '',
1785- }),
17861786 });
17871787 if (response.ok === true) {
17881788 characters.splice(0, characters.length);
@@ -3678,6 +3678,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
36783678 setGenerationProgress(0);
36793679 generation_started = new Date();
36803680
3681+ // Prevent generation from shallow characters
3682+ await unshallowCharacter(this_chid);
3683+
36813684 // Occurs every time, even if the generation is aborted due to slash commands execution
36823685 await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);
36833686
@@ -6724,9 +6727,43 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
67246727 }
67256728}
67266729
6730+/**
6731+ * Loads all the data of a shallow character.
6732+ * @param {string|undefined} characterId Array index
6733+ * @returns {Promise<void>} Promise that resolves when the character is unshallowed
6734+ */
6735+export async function unshallowCharacter(characterId) {
6736+ if (characterId === undefined) {
6737+ console.warn('Undefined character cannot be unshallowed');
6738+ return;
6739+ }
6740+
6741+ /** @type {import('./scripts/char-data.js').v1CharData} */
6742+ const character = characters[characterId];
6743+ if (!character) {
6744+ console.warn('Character not found:', characterId);
6745+ return;
6746+ }
6747+
6748+ // Character is not shallow
6749+ if (!character.shallow) {
6750+ return;
6751+ }
6752+
6753+ const avatar = character.avatar;
6754+ if (!avatar) {
6755+ console.warn('Character has no avatar field:', characterId);
6756+ return;
6757+ }
6758+
6759+ await getOneCharacter(avatar);
6760+}
6761+
67276762export async function getChat() {
67286763 //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
67296764 try {
6765+ await unshallowCharacter(this_chid);
6766+
67306767 const response = await $.ajax({
67316768 type: 'POST',
67326769 url: '/api/chats/get',
public/scripts/RossAscends-mods.js+4 -1
@@ -316,7 +316,10 @@ export async function favsToHotswap() {
316316 const entities = getEntitiesList({ doFilter: false });
317317 const container = $('#right-nav-panel .hotswap');
318318
319- const favs = entities.filter(x => x.item.fav || x.item.fav == 'true');
319+ // Hard limit is required because even if all hotswaps don't fit the screen, their images would still be loaded
320+ // 25 is roughly calculated as the maximum number of favs that can fit an ultrawide monitor with the default theme
321+ const FAVS_LIMIT = 25;
322+ const favs = entities.filter(x => x.item.fav || x.item.fav == 'true').slice(0, FAVS_LIMIT);
320323
321324 //helpful instruction message if no characters are favorited
322325 if (favs.length == 0) {
public/scripts/char-data.js+1 -0
@@ -113,5 +113,6 @@
113113 * @property {string} chat - name of the current chat file chat
114114 * @property {string} avatar - file name of the avatar image (acts as a unique identifier)
115115 * @property {string} json_data - the full raw JSON data of the character
116+ * @property {boolean?} shallow - if the data is shallow (lazy-loaded)
116117 */
117118export default 0;// now this file is a module
public/scripts/extensions/translate/index.js+110 -10
@@ -11,6 +11,7 @@ import {
1111} from '../../../script.js';
1212import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
1313import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
14+import { updateReasoningUI } from '../../reasoning.js';
1415import { findSecret, secret_state, writeSecret } from '../../secrets.js';
1516import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1617import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@@ -172,21 +173,38 @@ function loadSettings() {
172173 showKeysButton();
173174}
174175
176+/**
177+ * Check if the swipe is being generated for a message.
178+ * @param {string|number} messageId Message ID
179+ * @returns {boolean} Whether the swipe is being generated
180+ */
181+function isGeneratingSwipe(messageId) {
182+ return $(`#chat .mes[mesid="${messageId}"] .mes_text`).text() === '...';
183+}
184+
175185async function translateImpersonate(text) {
176186 const translatedText = await translate(text, extension_settings.translate.target_language);
177187 $('#send_textarea').val(translatedText);
178188}
179189
190+/**
191+ * Translates the contents of an incoming message.
192+ * @param {string | number} messageId Message ID
193+ * @returns {Promise<void>}
194+ */
180195async function translateIncomingMessage(messageId) {
181196 const context = getContext();
182197 const message = context.chat[messageId];
183198
199+ if (!message) {
200+ return;
201+ }
202+
184203 if (typeof message.extra !== 'object') {
185204 message.extra = {};
186205 }
187206
188- // New swipe is being generated. Don't translate that
207+ if (isGeneratingSwipe(messageId)) {
189- if ($(`#chat .mes[mesid="${messageId}"] .mes_text`).text() == '...') {
190208 return;
191209 }
192210
@@ -194,7 +212,36 @@ async function translateIncomingMessage(messageId) {
194212 const translation = await translate(textToTranslate, extension_settings.translate.target_language);
195213 message.extra.display_text = translation;
196214
197215 updateMessageBlock(Number(messageId), message);
216+}
217+
218+/**
219+ * Translates the reasoning of an incoming message.
220+ * @param {string | number} messageId
221+ * @returns {Promise<boolean>} translated or not
222+ */
223+async function translateIncomingMessageReasoning(messageId) {
224+ const context = getContext();
225+ const message = context.chat[messageId];
226+
227+ if (!message) {
228+ return false;
229+ }
230+
231+ if (typeof message.extra !== 'object') {
232+ message.extra = {};
233+ }
234+
235+ if (!message.extra.reasoning || isGeneratingSwipe(messageId)) {
236+ return false;
237+ }
238+
239+ const textToTranslate = substituteParams(message.extra.reasoning, context.name1, message.name);
240+ const translation = await translate(textToTranslate, extension_settings.translate.target_language);
241+ message.extra.reasoning_display_text = translation;
242+
243+ updateReasoningUI(Number(messageId));
244+ return true;
198245}
199246
200247async function translateProviderOneRing(text, lang) {
@@ -535,6 +582,7 @@ async function onTranslateChatClick() {
535582 toastr.info(`${chat.length} message(s) queued for translation.`, 'Please wait...');
536583
537584 for (let i = 0; i < chat.length; i++) {
585+ await translateIncomingMessageReasoning(i);
538586 await translateIncomingMessage(i);
539587 }
540588
@@ -561,6 +609,7 @@ async function onTranslationsClearClick() {
561609 for (const mes of chat) {
562610 if (mes.extra) {
563611 delete mes.extra.display_text;
612+ delete mes.extra.reasoning_display_text;
564613 }
565614 }
566615
@@ -573,12 +622,47 @@ async function translateMessageEdit(messageId) {
573622 const chat = context.chat;
574623 const message = chat[messageId];
575624
576- if (message.is_system || extension_settings.translate.auto_mode == autoModeOptions.NONE) {
625+ let anyChange = false;
577- return;
626+ if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.display_text)) {
627+ delete message.extra.display_text;
628+ updateMessageBlock(messageId, message);
629+ anyChange = true;
630+ } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
631+ await translateIncomingMessage(messageId);
632+ anyChange = true;
578633 }
579634
580- if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
635+ if (anyChange) {
581636 await translateIncomingMessagecontext.saveChat(messageId);
637+ }
638+}
639+
640+async function translateMessageReasoningEdit(messageId) {
641+ const context = getContext();
642+ const chat = context.chat;
643+ const message = chat[messageId];
644+
645+ let anyChange = false;
646+ if (message.is_system || (extension_settings.translate.auto_mode == autoModeOptions.NONE && message.extra?.reasoning_display_text)) {
647+ delete message.extra.reasoning_display_text;
648+ updateReasoningUI(Number(messageId));
649+ anyChange = true;
650+ } else if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) {
651+ anyChange = await translateIncomingMessageReasoning(messageId);
652+ }
653+
654+ if (anyChange) {
655+ await context.saveChat();
656+ }
657+}
658+
659+async function removeReasoningDisplayText(messageId) {
660+ const context = getContext();
661+ const message = context.chat[messageId];
662+ if (message.extra?.reasoning_display_text) {
663+ delete message.extra.reasoning_display_text;
664+ updateReasoningUI(Number(messageId));
665+ await context.saveChat();
582666 }
583667}
584668
@@ -588,22 +672,36 @@ async function onMessageTranslateClick() {
588672 const message = context.chat[messageId];
589673
590674 // If the message is already translated, revert it back to the original text
675+ let alreadyTranslated = false;
591676 if (message?.extra?.display_text) {
592677 delete message.extra.display_text;
593678 updateMessageBlock(Number(messageId), message);
679+ alreadyTranslated = true;
680+ }
681+ if (message?.extra?.reasoning_display_text) {
682+ delete message.extra.reasoning_display_text;
683+ updateReasoningUI(Number(messageId));
684+ alreadyTranslated = true;
594685 }
686+
595687 // If the message is not translated, translate it
596- else {
688+ if (!alreadyTranslated) {
689+ await translateIncomingMessageReasoning(messageId);
597690 await translateIncomingMessage(messageId);
598691 }
599692
600693 await context.saveChat();
601694}
602695
603696const handleIncomingMessage = createEventHandler(translateIncomingMessage,async (messageId) => shouldTranslate(incomingTypes));{
697+ await translateIncomingMessageReasoning(messageId);
698+ await translateIncomingMessage(messageId);
699+}, () => shouldTranslate(incomingTypes));
604700const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes));
605701const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
606702const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
703+const handleMessageReasoningEdit = createEventHandler(translateMessageReasoningEdit, () => true);
704+const handleMessageReasoningDelete = createEventHandler(removeReasoningDisplayText, () => true);
607705
608706globalThis.translate = translate;
609707
@@ -717,6 +815,8 @@ jQuery(async () => {
717815 eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
718816 eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
719817 eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
818+ eventSource.on(event_types.MESSAGE_REASONING_EDITED, handleMessageReasoningEdit);
819+ eventSource.on(event_types.MESSAGE_REASONING_DELETED, handleMessageReasoningDelete);
720820
721821 document.body.classList.add('translate');
722822
public/scripts/group-chats.js+33 -4
@@ -72,6 +72,7 @@ import {
7272 animation_duration,
7373 depth_prompt_role_default,
7474 shouldAutoContinue,
75+ unshallowCharacter,
7576} from '../script.js';
7677import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
7778import { FILTER_TYPES, FilterHelper } from './filters.js';
@@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) {
216217
217218 // Run validation before any loading
218219 validateGroup(group);
220+ await unshallowGroupMembers(groupId);
219221
220222 const chat_id = group.chat_id;
221223 const data = await loadGroupChat(chat_id);
@@ -824,6 +826,8 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
824826 }
825827
826828 try {
829+ await unshallowGroupMembers(selected_group);
830+
827831 throwIfAborted();
828832 hideSwipeButtons();
829833 is_group_generating = true;
@@ -1137,6 +1141,29 @@ export async function editGroup(id, immediately, reload = true) {
11371141 saveGroupDebounced(group, reload);
11381142}
11391143
1144+/**
1145+ * Unshallows all definitions of group members.
1146+ * @param {string} groupId Id of the group
1147+ * @returns {Promise<void>} Promise that resolves when all group members are unshallowed
1148+ */
1149+export async function unshallowGroupMembers(groupId) {
1150+ const group = groups.find(x => x.id == groupId);
1151+ if (!group) {
1152+ return;
1153+ }
1154+ const members = group.members;
1155+ if (!Array.isArray(members)) {
1156+ return;
1157+ }
1158+ for (const member of members) {
1159+ const index = characters.findIndex(x => x.avatar === member);
1160+ if (index === -1) {
1161+ continue;
1162+ }
1163+ await unshallowCharacter(String(index));
1164+ }
1165+}
1166+
11401167let groupAutoModeAbortController = null;
11411168
11421169async function groupChatAutoModeWorker() {
@@ -1158,9 +1185,9 @@ async function groupChatAutoModeWorker() {
11581185 await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
11591186}
11601187
11611188async function modifyGroupMember(chat_idgroupId, groupMember, isDelete) {
11621189 const id = groupMember.data('id');
11631190 const thisGroup = groups.find((x) => x.id == chat_idgroupId);
11641191 const membersArray = thisGroup?.members ?? newGroupMembers;
11651192
11661193 if (isDelete) {
@@ -1173,6 +1200,7 @@ async function modifyGroupMember(chat_id, groupMember, isDelete) {
11731200 }
11741201
11751202 if (openGroupId) {
1203+ await unshallowGroupMembers(openGroupId);
11761204 await editGroup(openGroupId, false, false);
11771205 updateGroupAvatar(thisGroup);
11781206 }
@@ -1638,7 +1666,7 @@ async function onGroupActionClick(event) {
16381666 }
16391667
16401668 if (action === 'view') {
16411669 await openCharacterDefinition(member);
16421670 }
16431671
16441672 if (action === 'speak') {
@@ -1690,7 +1718,7 @@ export async function openGroupById(groupId) {
16901718 return false;
16911719}
16921720
16931721async function openCharacterDefinition(characterSelect) {
16941722 if (is_group_generating) {
16951723 toastr.warning(t`Can't peek a character while group reply is being generated`);
16961724 console.warn('Can\'t peek a character def while group reply is being generated');
@@ -1703,6 +1731,7 @@ function openCharacterDefinition(characterSelect) {
17031731 return;
17041732 }
17051733
1734+ await unshallowCharacter(chid);
17061735 setCharacterId(chid);
17071736 select_selected_character(chid);
17081737 // Gentle nudge to recalculate tokens
public/scripts/openai.js+12 -0
@@ -300,6 +300,7 @@ export const settingsToUpdate = {
300300 function_calling: ['#openai_function_calling', 'function_calling', true],
301301 show_thoughts: ['#openai_show_thoughts', 'show_thoughts', true],
302302 reasoning_effort: ['#openai_reasoning_effort', 'reasoning_effort', false],
303+ enable_web_search: ['#openai_enable_web_search', 'enable_web_search', true],
303304 seed: ['#seed_openai', 'seed', false],
304305 n: ['#n_openai', 'n', false],
305306 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true],
@@ -380,6 +381,7 @@ const default_settings = {
380381 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
381382 show_thoughts: true,
382383 reasoning_effort: 'medium',
384+ enable_web_search: false,
383385 seed: -1,
384386 n: 1,
385387};
@@ -459,6 +461,7 @@ const oai_settings = {
459461 custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
460462 show_thoughts: true,
461463 reasoning_effort: 'medium',
464+ enable_web_search: false,
462465 seed: -1,
463466 n: 1,
464467};
@@ -2000,6 +2003,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20002003 'group_names': getGroupNames(),
20012004 'include_reasoning': Boolean(oai_settings.show_thoughts),
20022005 'reasoning_effort': String(oai_settings.reasoning_effort),
2006+ 'enable_web_search': Boolean(oai_settings.enable_web_search),
20032007 };
20042008
20052009 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
@@ -3222,6 +3226,7 @@ function loadOpenAISettings(data, settings) {
32223226 oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
32233227 oai_settings.show_thoughts = settings.show_thoughts ?? default_settings.show_thoughts;
32243228 oai_settings.reasoning_effort = settings.reasoning_effort ?? default_settings.reasoning_effort;
3229+ oai_settings.enable_web_search = settings.enable_web_search ?? default_settings.enable_web_search;
32253230 oai_settings.seed = settings.seed ?? default_settings.seed;
32263231 oai_settings.n = settings.n ?? default_settings.n;
32273232
@@ -3349,6 +3354,7 @@ function loadOpenAISettings(data, settings) {
33493354 $('#seed_openai').val(oai_settings.seed);
33503355 $('#n_openai').val(oai_settings.n);
33513356 $('#openai_show_thoughts').prop('checked', oai_settings.show_thoughts);
3357+ $('#openai_enable_web_search').prop('checked', oai_settings.enable_web_search);
33523358
33533359 $('#openai_reasoning_effort').val(oai_settings.reasoning_effort);
33543360 $(`#openai_reasoning_effort option[value="${oai_settings.reasoning_effort}"]`).prop('selected', true);
@@ -3613,6 +3619,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
36133619 function_calling: settings.function_calling,
36143620 show_thoughts: settings.show_thoughts,
36153621 reasoning_effort: settings.reasoning_effort,
3622+ enable_web_search: settings.enable_web_search,
36163623 seed: settings.seed,
36173624 n: settings.n,
36183625 };
@@ -5572,6 +5579,11 @@ export function initOpenAI() {
55725579 saveSettingsDebounced();
55735580 });
55745581
5582+ $('#openai_enable_web_search').on('input', function () {
5583+ oai_settings.enable_web_search = !!$(this).prop('checked');
5584+ saveSettingsDebounced();
5585+ });
5586+
55755587 if (!CSS.supports('field-sizing', 'content')) {
55765588 $(document).on('input', '#openai_settings .autoSetHeight', function () {
55775589 resetScrollHeight($(this));
public/scripts/reasoning.js+13 -3
@@ -167,6 +167,8 @@ export class ReasoningHandler {
167167 this.type = null;
168168 /** @type {string} The reasoning output */
169169 this.reasoning = '';
170+ /** @type {string?} The reasoning output display in case of translate or other */
171+ this.reasoningDisplayText = null;
170172 /** @type {Date} When the reasoning started */
171173 this.startTime = null;
172174 /** @type {Date} When the reasoning ended */
@@ -234,6 +236,7 @@ export class ReasoningHandler {
234236
235237 this.type = extra?.reasoning_type;
236238 this.reasoning = extra?.reasoning ?? '';
239+ this.reasoningDisplayText = extra?.reasoning_display_text ?? null;
237240
238241 if (this.state !== ReasoningState.None) {
239242 this.initialTime = new Date(chat[messageId].gen_started);
@@ -249,6 +252,7 @@ export class ReasoningHandler {
249252 this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
250253 this.type = null;
251254 this.reasoning = '';
255+ this.reasoningDisplayText = null;
252256 this.initialTime = new Date();
253257 this.startTime = null;
254258 this.endTime = null;
@@ -434,7 +438,7 @@ export class ReasoningHandler {
434438 setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
435439
436440 // Update the reasoning message
437441 const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning);
438442 const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
439443 this.messageReasoningContentDom.innerHTML = displayReasoning;
440444
@@ -888,12 +892,17 @@ function setReasoningEventHandlers() {
888892 }
889893
890894 const textarea = messageBlock.find('.reasoning_edit_textarea');
891895 updateReasoningFromValue(message,const newReasoning = String(textarea.val()));
896+ textarea.remove();
897+ if (newReasoning === message.extra.reasoning) {
898+ return;
899+ }
900+ updateReasoningFromValue(message, newReasoning);
892901 await saveChatConditional();
893902 updateMessageBlock(messageId, message);
894- textarea.remove();
895903
896904 messageBlock.find('.mes_edit_done:visible').trigger('click');
905+ await eventSource.emit(event_types.MESSAGE_REASONING_EDITED, messageId);
897906 });
898907
899908 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
@@ -955,6 +964,7 @@ function setReasoningEventHandlers() {
955964 updateMessageBlock(messageId, message);
956965 const textarea = messageBlock.find('.reasoning_edit_textarea');
957966 textarea.remove();
967+ await eventSource.emit(event_types.MESSAGE_REASONING_DELETED, messageId);
958968 });
959969
960970 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
public/scripts/sse-stream.js+5 -0
@@ -137,9 +137,14 @@ async function* parseStreamData(json) {
137137 else if (Array.isArray(json.candidates)) {
138138 for (let i = 0; i < json.candidates.length; i++) {
139139 const isNotPrimary = json.candidates?.[0]?.index > 0;
140+ const hasToolCalls = json?.candidates?.[0]?.content?.parts?.some(p => p?.functionCall);
140141 if (isNotPrimary || json.candidates.length === 0) {
141142 return null;
142143 }
144+ if (hasToolCalls) {
145+ yield { data: json, chunk: '' };
146+ return;
147+ }
143148 if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) {
144149 for (let j = 0; j < json.candidates[i].content.parts.length; j++) {
145150 if (typeof json.candidates[i].content.parts[j].text === 'string') {
public/scripts/st-context.js+6 -1
@@ -47,6 +47,7 @@ import {
4747 updateMessageBlock,
4848 printMessages,
4949 clearChat,
50+ unshallowCharacter,
5051} from '../script.js';
5152import {
5253 extension_settings,
@@ -55,7 +56,7 @@ import {
5556 renderExtensionTemplateAsync,
5657 writeExtensionField,
5758} from './extensions.js';
5859import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js';
5960import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js';
6061import { hideLoader, showLoader } from './loader.js';
6162import { MacrosParser } from './macros.js';
@@ -78,6 +79,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
7879import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
7980import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
8081import { ChatCompletionService, TextCompletionService } from './custom-request.js';
82+import { updateReasoningUI } from './reasoning.js';
8183
8284export function getContext() {
8385 return {
@@ -210,6 +212,9 @@ export function getContext() {
210212 clearChat,
211213 ChatCompletionService,
212214 TextCompletionService,
215+ updateReasoningUI,
216+ unshallowCharacter,
217+ unshallowGroupMembers,
213218 };
214219}
215220
public/scripts/tool-calling.js+33 -0
@@ -506,6 +506,26 @@ export class ToolManager {
506506 }
507507 }
508508 }
509+ if (Array.isArray(parsed?.candidates)) {
510+ for (let choiceIndex = 0; choiceIndex < parsed.candidates.length; choiceIndex++) {
511+ const candidate = parsed.candidates[choiceIndex];
512+ if (Array.isArray(candidate?.content?.parts)) {
513+ for (let toolCallIndex = 0; toolCallIndex < candidate.content.parts.length; toolCallIndex++) {
514+ const part = candidate.content.parts[toolCallIndex];
515+ if (part.functionCall) {
516+ if (!Array.isArray(toolCalls[choiceIndex])) {
517+ toolCalls[choiceIndex] = [];
518+ }
519+ if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
520+ toolCalls[choiceIndex][toolCallIndex] = {};
521+ }
522+ const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
523+ ToolManager.#applyToolCallDelta(targetToolCall, part.functionCall);
524+ }
525+ }
526+ }
527+ }
528+ }
509529 }
510530
511531 /**
@@ -564,6 +584,7 @@ export class ToolManager {
564584 chat_completion_sources.GROQ,
565585 chat_completion_sources.COHERE,
566586 chat_completion_sources.DEEPSEEK,
587+ chat_completion_sources.MAKERSUITE,
567588 ];
568589 return supportedSources.includes(oai_settings.chat_completion_source);
569590 }
@@ -585,8 +606,11 @@ export class ToolManager {
585606 * @returns {any[]} Tool calls from the response data
586607 */
587608 static #getToolCallsFromData(data) {
609+ const getRandomId = () => Math.random().toString(36).substring(2);
588610 const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id;
611+ const isGoogleToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isGoogleToolCall) : c?.name && c?.args;
589612 const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } });
613+ const convertGoogleToolCall = (c) => ({ id: getRandomId(), function: { name: c.name, arguments: c.args } });
590614
591615 // Parsed tool calls from streaming data
592616 if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
@@ -594,6 +618,10 @@ export class ToolManager {
594618 return data[0].filter(x => x).map(convertClaudeToolCall);
595619 }
596620
621+ if (isGoogleToolCall(data[0])) {
622+ return data[0].filter(x => x).map(convertGoogleToolCall);
623+ }
624+
597625 if (typeof data[0]?.[0]?.tool_calls === 'object') {
598626 return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls];
599627 }
@@ -601,6 +629,11 @@ export class ToolManager {
601629 return data[0];
602630 }
603631
632+ // Google AI Studio tool calls
633+ if (Array.isArray(data?.responseContent?.parts)) {
634+ return data.responseContent.parts.filter(p => p.functionCall).map(p => convertGoogleToolCall(p.functionCall));
635+ }
636+
604637 // Parsed tool calls from non-streaming data
605638 if (Array.isArray(data?.choices)) {
606639 // Find a choice with 0-index
server.js+2 -0
@@ -20,6 +20,7 @@ import bodyParser from 'body-parser';
2020import open from 'open';
2121
2222// local library imports
23+import { serverEvents, EVENT_NAMES } from './src/server-events.js';
2324import { CommandLineParser } from './src/command-line.js';
2425import { loadPlugins } from './src/plugin-loader.js';
2526import {
@@ -348,6 +349,7 @@ async function postSetupTasks(result) {
348349 console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
349350
350351 setupLogLevel();
352+ serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl });
351353}
352354
353355/**
src/electron/Start.bat+6 -0
@@ -0,0 +1,6 @@
1+@echo off
2+pushd %~dp0
3+call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev
4+npm run start server.js %*
5+pause
6+popd
src/electron/index.js+62 -0
@@ -0,0 +1,62 @@
1+import { app, BrowserWindow } from 'electron';
2+import path from 'path';
3+import { fileURLToPath } from 'url';
4+import yargs from 'yargs';
5+import { serverEvents, EVENT_NAMES } from '../server-events.js';
6+
7+const cliArguments = yargs(process.argv)
8+ .usage('Usage: <your-start-script> [options]')
9+ .option('width', {
10+ type: 'number',
11+ default: 800,
12+ describe: 'The width of the window',
13+ })
14+ .option('height', {
15+ type: 'number',
16+ default: 600,
17+ describe: 'The height of the window',
18+ })
19+ .parseSync();
20+
21+/** @type {string} The URL to load in the window. */
22+let appUrl;
23+
24+function createSillyTavernWindow() {
25+ if (!appUrl) {
26+ console.error('The server has not started yet.');
27+ return;
28+ }
29+ new BrowserWindow({
30+ height: cliArguments.height,
31+ width: cliArguments.width,
32+ }).loadURL(appUrl);
33+}
34+
35+function startServer() {
36+ return new Promise((_resolve, _reject) => {
37+ serverEvents.addListener(EVENT_NAMES.SERVER_STARTED, ({ url }) => {
38+ appUrl = url.toString();
39+ createSillyTavernWindow();
40+ });
41+ const sillyTavernRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
42+ process.chdir(sillyTavernRoot);
43+
44+ import('../../server.js');
45+ });
46+}
47+
48+app.whenReady().then(() => {
49+ app.on('activate', () => {
50+ if (BrowserWindow.getAllWindows().length === 0) {
51+ createSillyTavernWindow();
52+ }
53+ });
54+
55+ startServer();
56+});
57+
58+app.on('window-all-closed', () => {
59+ if (process.platform !== 'darwin') {
60+ app.quit();
61+ }
62+});
src/electron/package-lock.json+802 -0
@@ -0,0 +1,802 @@
1+{
2+ "name": "sillytavern-electron",
3+ "version": "1.0.0",
4+ "lockfileVersion": 3,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "sillytavern-electron",
9+ "version": "1.0.0",
10+ "license": "AGPL-3.0",
11+ "dependencies": {
12+ "electron": "^35.0.0"
13+ }
14+ },
15+ "node_modules/@electron/get": {
16+ "version": "2.0.3",
17+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
18+ "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
19+ "license": "MIT",
20+ "dependencies": {
21+ "debug": "^4.1.1",
22+ "env-paths": "^2.2.0",
23+ "fs-extra": "^8.1.0",
24+ "got": "^11.8.5",
25+ "progress": "^2.0.3",
26+ "semver": "^6.2.0",
27+ "sumchecker": "^3.0.1"
28+ },
29+ "engines": {
30+ "node": ">=12"
31+ },
32+ "optionalDependencies": {
33+ "global-agent": "^3.0.0"
34+ }
35+ },
36+ "node_modules/@sindresorhus/is": {
37+ "version": "4.6.0",
38+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
39+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
40+ "license": "MIT",
41+ "engines": {
42+ "node": ">=10"
43+ },
44+ "funding": {
45+ "url": "https://github.com/sindresorhus/is?sponsor=1"
46+ }
47+ },
48+ "node_modules/@szmarczak/http-timer": {
49+ "version": "4.0.6",
50+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
51+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
52+ "license": "MIT",
53+ "dependencies": {
54+ "defer-to-connect": "^2.0.0"
55+ },
56+ "engines": {
57+ "node": ">=10"
58+ }
59+ },
60+ "node_modules/@types/cacheable-request": {
61+ "version": "6.0.3",
62+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
63+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
64+ "license": "MIT",
65+ "dependencies": {
66+ "@types/http-cache-semantics": "*",
67+ "@types/keyv": "^3.1.4",
68+ "@types/node": "*",
69+ "@types/responselike": "^1.0.0"
70+ }
71+ },
72+ "node_modules/@types/http-cache-semantics": {
73+ "version": "4.0.4",
74+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
75+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
76+ "license": "MIT"
77+ },
78+ "node_modules/@types/keyv": {
79+ "version": "3.1.4",
80+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
81+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
82+ "license": "MIT",
83+ "dependencies": {
84+ "@types/node": "*"
85+ }
86+ },
87+ "node_modules/@types/node": {
88+ "version": "22.13.9",
89+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
90+ "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
91+ "license": "MIT",
92+ "dependencies": {
93+ "undici-types": "~6.20.0"
94+ }
95+ },
96+ "node_modules/@types/responselike": {
97+ "version": "1.0.3",
98+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
99+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
100+ "license": "MIT",
101+ "dependencies": {
102+ "@types/node": "*"
103+ }
104+ },
105+ "node_modules/@types/yauzl": {
106+ "version": "2.10.3",
107+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
108+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
109+ "license": "MIT",
110+ "optional": true,
111+ "dependencies": {
112+ "@types/node": "*"
113+ }
114+ },
115+ "node_modules/boolean": {
116+ "version": "3.2.0",
117+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
118+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
119+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
120+ "license": "MIT",
121+ "optional": true
122+ },
123+ "node_modules/buffer-crc32": {
124+ "version": "0.2.13",
125+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
126+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
127+ "license": "MIT",
128+ "engines": {
129+ "node": "*"
130+ }
131+ },
132+ "node_modules/cacheable-lookup": {
133+ "version": "5.0.4",
134+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
135+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
136+ "license": "MIT",
137+ "engines": {
138+ "node": ">=10.6.0"
139+ }
140+ },
141+ "node_modules/cacheable-request": {
142+ "version": "7.0.4",
143+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
144+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
145+ "license": "MIT",
146+ "dependencies": {
147+ "clone-response": "^1.0.2",
148+ "get-stream": "^5.1.0",
149+ "http-cache-semantics": "^4.0.0",
150+ "keyv": "^4.0.0",
151+ "lowercase-keys": "^2.0.0",
152+ "normalize-url": "^6.0.1",
153+ "responselike": "^2.0.0"
154+ },
155+ "engines": {
156+ "node": ">=8"
157+ }
158+ },
159+ "node_modules/clone-response": {
160+ "version": "1.0.3",
161+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
162+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
163+ "license": "MIT",
164+ "dependencies": {
165+ "mimic-response": "^1.0.0"
166+ },
167+ "funding": {
168+ "url": "https://github.com/sponsors/sindresorhus"
169+ }
170+ },
171+ "node_modules/debug": {
172+ "version": "4.4.0",
173+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
174+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
175+ "license": "MIT",
176+ "dependencies": {
177+ "ms": "^2.1.3"
178+ },
179+ "engines": {
180+ "node": ">=6.0"
181+ },
182+ "peerDependenciesMeta": {
183+ "supports-color": {
184+ "optional": true
185+ }
186+ }
187+ },
188+ "node_modules/decompress-response": {
189+ "version": "6.0.0",
190+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
191+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
192+ "license": "MIT",
193+ "dependencies": {
194+ "mimic-response": "^3.1.0"
195+ },
196+ "engines": {
197+ "node": ">=10"
198+ },
199+ "funding": {
200+ "url": "https://github.com/sponsors/sindresorhus"
201+ }
202+ },
203+ "node_modules/decompress-response/node_modules/mimic-response": {
204+ "version": "3.1.0",
205+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
206+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
207+ "license": "MIT",
208+ "engines": {
209+ "node": ">=10"
210+ },
211+ "funding": {
212+ "url": "https://github.com/sponsors/sindresorhus"
213+ }
214+ },
215+ "node_modules/defer-to-connect": {
216+ "version": "2.0.1",
217+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
218+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
219+ "license": "MIT",
220+ "engines": {
221+ "node": ">=10"
222+ }
223+ },
224+ "node_modules/define-data-property": {
225+ "version": "1.1.4",
226+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
227+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
228+ "license": "MIT",
229+ "optional": true,
230+ "dependencies": {
231+ "es-define-property": "^1.0.0",
232+ "es-errors": "^1.3.0",
233+ "gopd": "^1.0.1"
234+ },
235+ "engines": {
236+ "node": ">= 0.4"
237+ },
238+ "funding": {
239+ "url": "https://github.com/sponsors/ljharb"
240+ }
241+ },
242+ "node_modules/define-properties": {
243+ "version": "1.2.1",
244+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
245+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
246+ "license": "MIT",
247+ "optional": true,
248+ "dependencies": {
249+ "define-data-property": "^1.0.1",
250+ "has-property-descriptors": "^1.0.0",
251+ "object-keys": "^1.1.1"
252+ },
253+ "engines": {
254+ "node": ">= 0.4"
255+ },
256+ "funding": {
257+ "url": "https://github.com/sponsors/ljharb"
258+ }
259+ },
260+ "node_modules/detect-node": {
261+ "version": "2.1.0",
262+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
263+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
264+ "license": "MIT",
265+ "optional": true
266+ },
267+ "node_modules/electron": {
268+ "version": "35.0.0",
269+ "resolved": "https://registry.npmjs.org/electron/-/electron-35.0.0.tgz",
270+ "integrity": "sha512-mwNQNktYLPnUWZVR8iNkfWCBjmM5e2/CmB1rhACwE9ASDbVU7CYPgp/jLUB3bj/LyQsfSuubD82OUite6SN8Uw==",
271+ "hasInstallScript": true,
272+ "license": "MIT",
273+ "dependencies": {
274+ "@electron/get": "^2.0.0",
275+ "@types/node": "^22.7.7",
276+ "extract-zip": "^2.0.1"
277+ },
278+ "bin": {
279+ "electron": "cli.js"
280+ },
281+ "engines": {
282+ "node": ">= 12.20.55"
283+ }
284+ },
285+ "node_modules/end-of-stream": {
286+ "version": "1.4.4",
287+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
288+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
289+ "license": "MIT",
290+ "dependencies": {
291+ "once": "^1.4.0"
292+ }
293+ },
294+ "node_modules/env-paths": {
295+ "version": "2.2.1",
296+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
297+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
298+ "license": "MIT",
299+ "engines": {
300+ "node": ">=6"
301+ }
302+ },
303+ "node_modules/es-define-property": {
304+ "version": "1.0.1",
305+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
306+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
307+ "license": "MIT",
308+ "optional": true,
309+ "engines": {
310+ "node": ">= 0.4"
311+ }
312+ },
313+ "node_modules/es-errors": {
314+ "version": "1.3.0",
315+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
316+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
317+ "license": "MIT",
318+ "optional": true,
319+ "engines": {
320+ "node": ">= 0.4"
321+ }
322+ },
323+ "node_modules/es6-error": {
324+ "version": "4.1.1",
325+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
326+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
327+ "license": "MIT",
328+ "optional": true
329+ },
330+ "node_modules/escape-string-regexp": {
331+ "version": "4.0.0",
332+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
333+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
334+ "license": "MIT",
335+ "optional": true,
336+ "engines": {
337+ "node": ">=10"
338+ },
339+ "funding": {
340+ "url": "https://github.com/sponsors/sindresorhus"
341+ }
342+ },
343+ "node_modules/extract-zip": {
344+ "version": "2.0.1",
345+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
346+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
347+ "license": "BSD-2-Clause",
348+ "dependencies": {
349+ "debug": "^4.1.1",
350+ "get-stream": "^5.1.0",
351+ "yauzl": "^2.10.0"
352+ },
353+ "bin": {
354+ "extract-zip": "cli.js"
355+ },
356+ "engines": {
357+ "node": ">= 10.17.0"
358+ },
359+ "optionalDependencies": {
360+ "@types/yauzl": "^2.9.1"
361+ }
362+ },
363+ "node_modules/fd-slicer": {
364+ "version": "1.1.0",
365+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
366+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
367+ "license": "MIT",
368+ "dependencies": {
369+ "pend": "~1.2.0"
370+ }
371+ },
372+ "node_modules/fs-extra": {
373+ "version": "8.1.0",
374+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
375+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
376+ "license": "MIT",
377+ "dependencies": {
378+ "graceful-fs": "^4.2.0",
379+ "jsonfile": "^4.0.0",
380+ "universalify": "^0.1.0"
381+ },
382+ "engines": {
383+ "node": ">=6 <7 || >=8"
384+ }
385+ },
386+ "node_modules/get-stream": {
387+ "version": "5.2.0",
388+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
389+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
390+ "license": "MIT",
391+ "dependencies": {
392+ "pump": "^3.0.0"
393+ },
394+ "engines": {
395+ "node": ">=8"
396+ },
397+ "funding": {
398+ "url": "https://github.com/sponsors/sindresorhus"
399+ }
400+ },
401+ "node_modules/global-agent": {
402+ "version": "3.0.0",
403+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
404+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
405+ "license": "BSD-3-Clause",
406+ "optional": true,
407+ "dependencies": {
408+ "boolean": "^3.0.1",
409+ "es6-error": "^4.1.1",
410+ "matcher": "^3.0.0",
411+ "roarr": "^2.15.3",
412+ "semver": "^7.3.2",
413+ "serialize-error": "^7.0.1"
414+ },
415+ "engines": {
416+ "node": ">=10.0"
417+ }
418+ },
419+ "node_modules/global-agent/node_modules/semver": {
420+ "version": "7.7.1",
421+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
422+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
423+ "license": "ISC",
424+ "optional": true,
425+ "bin": {
426+ "semver": "bin/semver.js"
427+ },
428+ "engines": {
429+ "node": ">=10"
430+ }
431+ },
432+ "node_modules/globalthis": {
433+ "version": "1.0.4",
434+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
435+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
436+ "license": "MIT",
437+ "optional": true,
438+ "dependencies": {
439+ "define-properties": "^1.2.1",
440+ "gopd": "^1.0.1"
441+ },
442+ "engines": {
443+ "node": ">= 0.4"
444+ },
445+ "funding": {
446+ "url": "https://github.com/sponsors/ljharb"
447+ }
448+ },
449+ "node_modules/gopd": {
450+ "version": "1.2.0",
451+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
452+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
453+ "license": "MIT",
454+ "optional": true,
455+ "engines": {
456+ "node": ">= 0.4"
457+ },
458+ "funding": {
459+ "url": "https://github.com/sponsors/ljharb"
460+ }
461+ },
462+ "node_modules/got": {
463+ "version": "11.8.6",
464+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
465+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
466+ "license": "MIT",
467+ "dependencies": {
468+ "@sindresorhus/is": "^4.0.0",
469+ "@szmarczak/http-timer": "^4.0.5",
470+ "@types/cacheable-request": "^6.0.1",
471+ "@types/responselike": "^1.0.0",
472+ "cacheable-lookup": "^5.0.3",
473+ "cacheable-request": "^7.0.2",
474+ "decompress-response": "^6.0.0",
475+ "http2-wrapper": "^1.0.0-beta.5.2",
476+ "lowercase-keys": "^2.0.0",
477+ "p-cancelable": "^2.0.0",
478+ "responselike": "^2.0.0"
479+ },
480+ "engines": {
481+ "node": ">=10.19.0"
482+ },
483+ "funding": {
484+ "url": "https://github.com/sindresorhus/got?sponsor=1"
485+ }
486+ },
487+ "node_modules/graceful-fs": {
488+ "version": "4.2.11",
489+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
490+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
491+ "license": "ISC"
492+ },
493+ "node_modules/has-property-descriptors": {
494+ "version": "1.0.2",
495+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
496+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
497+ "license": "MIT",
498+ "optional": true,
499+ "dependencies": {
500+ "es-define-property": "^1.0.0"
501+ },
502+ "funding": {
503+ "url": "https://github.com/sponsors/ljharb"
504+ }
505+ },
506+ "node_modules/http-cache-semantics": {
507+ "version": "4.1.1",
508+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
509+ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
510+ "license": "BSD-2-Clause"
511+ },
512+ "node_modules/http2-wrapper": {
513+ "version": "1.0.3",
514+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
515+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
516+ "license": "MIT",
517+ "dependencies": {
518+ "quick-lru": "^5.1.1",
519+ "resolve-alpn": "^1.0.0"
520+ },
521+ "engines": {
522+ "node": ">=10.19.0"
523+ }
524+ },
525+ "node_modules/json-buffer": {
526+ "version": "3.0.1",
527+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
528+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
529+ "license": "MIT"
530+ },
531+ "node_modules/json-stringify-safe": {
532+ "version": "5.0.1",
533+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
534+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
535+ "license": "ISC",
536+ "optional": true
537+ },
538+ "node_modules/jsonfile": {
539+ "version": "4.0.0",
540+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
541+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
542+ "license": "MIT",
543+ "optionalDependencies": {
544+ "graceful-fs": "^4.1.6"
545+ }
546+ },
547+ "node_modules/keyv": {
548+ "version": "4.5.4",
549+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
550+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
551+ "license": "MIT",
552+ "dependencies": {
553+ "json-buffer": "3.0.1"
554+ }
555+ },
556+ "node_modules/lowercase-keys": {
557+ "version": "2.0.0",
558+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
559+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
560+ "license": "MIT",
561+ "engines": {
562+ "node": ">=8"
563+ }
564+ },
565+ "node_modules/matcher": {
566+ "version": "3.0.0",
567+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
568+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
569+ "license": "MIT",
570+ "optional": true,
571+ "dependencies": {
572+ "escape-string-regexp": "^4.0.0"
573+ },
574+ "engines": {
575+ "node": ">=10"
576+ }
577+ },
578+ "node_modules/mimic-response": {
579+ "version": "1.0.1",
580+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
581+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
582+ "license": "MIT",
583+ "engines": {
584+ "node": ">=4"
585+ }
586+ },
587+ "node_modules/ms": {
588+ "version": "2.1.3",
589+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
590+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
591+ "license": "MIT"
592+ },
593+ "node_modules/normalize-url": {
594+ "version": "6.1.0",
595+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
596+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
597+ "license": "MIT",
598+ "engines": {
599+ "node": ">=10"
600+ },
601+ "funding": {
602+ "url": "https://github.com/sponsors/sindresorhus"
603+ }
604+ },
605+ "node_modules/object-keys": {
606+ "version": "1.1.1",
607+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
608+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
609+ "license": "MIT",
610+ "optional": true,
611+ "engines": {
612+ "node": ">= 0.4"
613+ }
614+ },
615+ "node_modules/once": {
616+ "version": "1.4.0",
617+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
618+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
619+ "license": "ISC",
620+ "dependencies": {
621+ "wrappy": "1"
622+ }
623+ },
624+ "node_modules/p-cancelable": {
625+ "version": "2.1.1",
626+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
627+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
628+ "license": "MIT",
629+ "engines": {
630+ "node": ">=8"
631+ }
632+ },
633+ "node_modules/pend": {
634+ "version": "1.2.0",
635+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
636+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
637+ "license": "MIT"
638+ },
639+ "node_modules/progress": {
640+ "version": "2.0.3",
641+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
642+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
643+ "license": "MIT",
644+ "engines": {
645+ "node": ">=0.4.0"
646+ }
647+ },
648+ "node_modules/pump": {
649+ "version": "3.0.2",
650+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
651+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
652+ "license": "MIT",
653+ "dependencies": {
654+ "end-of-stream": "^1.1.0",
655+ "once": "^1.3.1"
656+ }
657+ },
658+ "node_modules/quick-lru": {
659+ "version": "5.1.1",
660+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
661+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
662+ "license": "MIT",
663+ "engines": {
664+ "node": ">=10"
665+ },
666+ "funding": {
667+ "url": "https://github.com/sponsors/sindresorhus"
668+ }
669+ },
670+ "node_modules/resolve-alpn": {
671+ "version": "1.2.1",
672+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
673+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
674+ "license": "MIT"
675+ },
676+ "node_modules/responselike": {
677+ "version": "2.0.1",
678+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
679+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
680+ "license": "MIT",
681+ "dependencies": {
682+ "lowercase-keys": "^2.0.0"
683+ },
684+ "funding": {
685+ "url": "https://github.com/sponsors/sindresorhus"
686+ }
687+ },
688+ "node_modules/roarr": {
689+ "version": "2.15.4",
690+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
691+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
692+ "license": "BSD-3-Clause",
693+ "optional": true,
694+ "dependencies": {
695+ "boolean": "^3.0.1",
696+ "detect-node": "^2.0.4",
697+ "globalthis": "^1.0.1",
698+ "json-stringify-safe": "^5.0.1",
699+ "semver-compare": "^1.0.0",
700+ "sprintf-js": "^1.1.2"
701+ },
702+ "engines": {
703+ "node": ">=8.0"
704+ }
705+ },
706+ "node_modules/semver": {
707+ "version": "6.3.1",
708+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
709+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
710+ "license": "ISC",
711+ "bin": {
712+ "semver": "bin/semver.js"
713+ }
714+ },
715+ "node_modules/semver-compare": {
716+ "version": "1.0.0",
717+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
718+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
719+ "license": "MIT",
720+ "optional": true
721+ },
722+ "node_modules/serialize-error": {
723+ "version": "7.0.1",
724+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
725+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
726+ "license": "MIT",
727+ "optional": true,
728+ "dependencies": {
729+ "type-fest": "^0.13.1"
730+ },
731+ "engines": {
732+ "node": ">=10"
733+ },
734+ "funding": {
735+ "url": "https://github.com/sponsors/sindresorhus"
736+ }
737+ },
738+ "node_modules/sprintf-js": {
739+ "version": "1.1.3",
740+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
741+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
742+ "license": "BSD-3-Clause",
743+ "optional": true
744+ },
745+ "node_modules/sumchecker": {
746+ "version": "3.0.1",
747+ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
748+ "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
749+ "license": "Apache-2.0",
750+ "dependencies": {
751+ "debug": "^4.1.0"
752+ },
753+ "engines": {
754+ "node": ">= 8.0"
755+ }
756+ },
757+ "node_modules/type-fest": {
758+ "version": "0.13.1",
759+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
760+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
761+ "license": "(MIT OR CC0-1.0)",
762+ "optional": true,
763+ "engines": {
764+ "node": ">=10"
765+ },
766+ "funding": {
767+ "url": "https://github.com/sponsors/sindresorhus"
768+ }
769+ },
770+ "node_modules/undici-types": {
771+ "version": "6.20.0",
772+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
773+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
774+ "license": "MIT"
775+ },
776+ "node_modules/universalify": {
777+ "version": "0.1.2",
778+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
779+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
780+ "license": "MIT",
781+ "engines": {
782+ "node": ">= 4.0.0"
783+ }
784+ },
785+ "node_modules/wrappy": {
786+ "version": "1.0.2",
787+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
788+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
789+ "license": "ISC"
790+ },
791+ "node_modules/yauzl": {
792+ "version": "2.10.0",
793+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
794+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
795+ "license": "MIT",
796+ "dependencies": {
797+ "buffer-crc32": "~0.2.3",
798+ "fd-slicer": "~1.1.0"
799+ }
800+ }
801+ }
802+}
src/electron/package.json+16 -0
@@ -0,0 +1,16 @@
1+{
2+ "name": "sillytavern-electron",
3+ "version": "1.0.0",
4+ "description": "Electron server for SillyTavern",
5+ "license": "AGPL-3.0",
6+ "author": "",
7+ "type": "module",
8+ "main": "index.js",
9+ "scripts": {
10+ "test": "echo \"Error: no test specified\" && exit 1",
11+ "start": "electron ."
12+ },
13+ "dependencies": {
14+ "electron": "^35.0.0"
15+ }
16+}
src/electron/start.sh+11 -0
@@ -0,0 +1,11 @@
1+#!/usr/bin/env bash
2+
3+# Make sure pwd is the directory of the script
4+cd "$(dirname "$0")"
5+
6+echo "Assuming nodejs and npm is already installed. If you haven't installed them already, do so now"
7+echo "Installing Electron Wrapper's Node Modules..."
8+npm i --no-audit --no-fund --loglevel=error --no-progress --omit=dev
9+
10+echo "Starting Electron Wrapper..."
11+npm run start -- "$@"
src/endpoints/backends/chat-completions.js+45 -1
@@ -99,6 +99,21 @@ function getOpenRouterTransforms(request) {
9999}
100100
101101/**
102+ * Gets OpenRouter plugins based on the request.
103+ * @param {import('express').Request} request
104+ * @returns {any[]} OpenRouter plugins
105+ */
106+function getOpenRouterPlugins(request) {
107+ const plugins = [];
108+
109+ if (request.body.enable_web_search) {
110+ plugins.push({ 'id': 'web' });
111+ }
112+
113+ return plugins;
114+}
115+
116+/**
102117 * Sends a request to Claude API.
103118 * @param {express.Request} request Express request
104119 * @param {express.Response} response Express response
@@ -323,6 +338,7 @@ async function sendMakerSuiteRequest(request, response) {
323338
324339 const model = String(request.body.model);
325340 const stream = Boolean(request.body.stream);
341+ const enableWebSearch = Boolean(request.body.enable_web_search);
326342 const isThinking = model.includes('thinking');
327343
328344 const generationConfig = {
@@ -350,6 +366,7 @@ async function sendMakerSuiteRequest(request, response) {
350366 model.startsWith('gemini-exp')
351367 ) && request.body.use_makersuite_sysprompt;
352368
369+ const tools = [];
353370 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, getPromptNames(request));
354371 let safetySettings = GEMINI_SAFETY;
355372
@@ -363,6 +380,26 @@ async function sendMakerSuiteRequest(request, response) {
363380 }
364381 // Most of the other models allow for setting the threshold of filters, except for HARM_CATEGORY_CIVIC_INTEGRITY, to OFF.
365382
383+ if (enableWebSearch) {
384+ const searchTool = model.includes('1.5') || model.includes('1.0')
385+ ? ({ google_search_retrieval: {} })
386+ : ({ google_search: {} });
387+ tools.push(searchTool);
388+ }
389+
390+ if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
391+ const functionDeclarations = [];
392+ for (const tool of request.body.tools) {
393+ if (tool.type === 'function') {
394+ if (tool.function.parameters?.$schema) {
395+ delete tool.function.parameters.$schema;
396+ }
397+ functionDeclarations.push(tool.function);
398+ }
399+ }
400+ tools.push({ function_declarations: functionDeclarations });
401+ }
402+
366403 let body = {
367404 contents: prompt.contents,
368405 safetySettings: safetySettings,
@@ -374,6 +411,10 @@ async function sendMakerSuiteRequest(request, response) {
374411 body.systemInstruction = prompt.system_instruction;
375412 }
376413
414+ if (tools.length) {
415+ body.tools = tools;
416+ }
417+
377418 return body;
378419 }
379420
@@ -429,10 +470,11 @@ async function sendMakerSuiteRequest(request, response) {
429470 }
430471
431472 const responseContent = candidates[0].content ?? candidates[0].output;
473+ const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);
432474 console.warn('Google AI Studio response:', responseContent);
433475
434476 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
435477 if (!responseText && !functionCall) {
436478 let message = 'Google AI Studio Candidate text empty';
437479 console.warn(message, generateResponseJson);
438480 return response.send({ error: { message } });
@@ -1017,6 +1059,7 @@ router.post('/generate', jsonParser, function (request, response) {
10171059 headers = { ...OPENROUTER_HEADERS };
10181060 bodyParams = {
10191061 'transforms': getOpenRouterTransforms(request),
1062+ 'plugins': getOpenRouterPlugins(request),
10201063 'include_reasoning': Boolean(request.body.include_reasoning),
10211064 };
10221065
@@ -1185,6 +1228,7 @@ router.post('/generate', jsonParser, function (request, response) {
11851228 */
11861229 async function makeRequest(config, response, request, retries = 5, timeout = 5000) {
11871230 try {
1231+ controller.signal.throwIfAborted();
11881232 const fetchResponse = await fetch(endpointUrl, config);
11891233
11901234 if (request.body.stream) {
src/endpoints/characters.js+47 -15
@@ -23,12 +23,13 @@ import { invalidateThumbnail } from './thumbnails.js';
2323import { importRisuSprites } from './sprites.js';
2424const defaultAvatarPath = './public/img/ai4.png';
2525
26-// KV-store for parsed character data
27-const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100, 'number')); // MB
2826// With 100 MB limit it would take roughly 3000 characters to reach this limit
29-const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity);
27+const memoryCacheCapacity = getConfigValue('performance.memoryCacheCapacity', '100mb');
28+const memoryCache = new MemoryLimitedMap(memoryCacheCapacity);
3029// Some Android devices require tighter memory management
3130const isAndroid = process.platform === 'android';
31+// Use shallow character data for the character list
32+const useShallowCharacters = !!getConfigValue('performance.lazyLoadCharacters', false, 'boolean');
3233
3334/**
3435 * Reads the character card from the specified image file.
@@ -39,12 +40,12 @@ const isAndroid = process.platform === 'android';
3940async function readCharacterData(inputFile, inputFormat = 'png') {
4041 const stat = fs.statSync(inputFile);
4142 const cacheKey = `${inputFile}-${stat.mtimeMs}`;
4243 if (characterDataCachememoryCache.has(cacheKey)) {
4344 return characterDataCachememoryCache.get(cacheKey);
4445 }
4546
4647 const result = parse(inputFile, inputFormat);
4748 !isAndroid && characterDataCachememoryCache.set(cacheKey, result);
4849 return result;
4950}
5051
@@ -60,12 +61,12 @@ async function readCharacterData(inputFile, inputFormat = 'png') {
6061async function writeCharacterData(inputFile, data, outputFile, request, crop = undefined) {
6162 try {
6263 // Reset the cache
6364 for (const key of characterDataCachememoryCache.keys()) {
6465 if (Buffer.isBuffer(inputFile)) {
6566 break;
6667 }
6768 if (key.startsWith(inputFile)) {
6869 characterDataCachememoryCache.delete(key);
6970 break;
7071 }
7172 }
@@ -201,13 +202,44 @@ const calculateDataSize = (data) => {
201202};
202203
203204/**
205+ * Only get fields that are used to display the character list.
206+ * @param {object} character Character object
207+ * @returns {{shallow: true, [key: string]: any}} Shallow character
208+ */
209+const toShallow = (character) => {
210+ return {
211+ shallow: true,
212+ name: character.name,
213+ avatar: character.avatar,
214+ chat: character.chat,
215+ fav: character.fav,
216+ date_added: character.date_added,
217+ create_date: character.create_date,
218+ date_last_chat: character.date_last_chat,
219+ chat_size: character.chat_size,
220+ data_size: character.data_size,
221+ data: {
222+ name: _.get(character, 'data.name', ''),
223+ character_version: _.get(character, 'data.character_version', ''),
224+ creator: _.get(character, 'data.creator', ''),
225+ creator_notes: _.get(character, 'data.creator_notes', ''),
226+ extensions: {
227+ fav: _.get(character, 'data.extensions.fav', false),
228+ },
229+ },
230+ };
231+};
232+
233+/**
204234 * processCharacter - Process a given character, read its data and calculate its statistics.
205235 *
206236 * @param {string} item The name of the character.
207237 * @param {import('../users.js').UserDirectoryList} directories User directories
238+ * @param {object} options Options for the character processing
239+ * @param {boolean} options.shallow If true, only return the core character's metadata
208240 * @return {Promise<object>} A Promise that resolves when the character processing is done.
209241 */
210242const processCharacter = async (item, directories, { shallow }) => {
211243 try {
212244 const imgFile = path.join(directories.characters, item);
213245 const imgData = await readCharacterData(imgFile);
@@ -226,7 +258,7 @@ const processCharacter = async (item, directories) => {
226258 character['chat_size'] = chatSize;
227259 character['date_last_chat'] = dateLastChat;
228260 character['data_size'] = calculateDataSize(jsonObject?.data);
229- return character;
261+ return shallow ? toShallow(character) : character;
230262 }
231263 catch (err) {
232264 console.error(`Could not process character: ${item}`);
@@ -993,7 +1025,7 @@ router.post('/all', jsonParser, async function (request, response) {
9931025 try {
9941026 const files = fs.readdirSync(request.user.directories.characters);
9951027 const pngFiles = files.filter(file => file.endsWith('.png'));
9961028 const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories, { shallow: useShallowCharacters }));
9971029 const data = (await Promise.all(processingPromises)).filter(c => c.name);
9981030 return response.send(data);
9991031 } catch (err) {
@@ -1012,7 +1044,7 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req
10121044 return response.sendStatus(404);
10131045 }
10141046
10151047 const data = await processCharacter(item, request.user.directories, { shallow: false });
10161048
10171049 return response.send(data);
10181050 } catch (err) {
@@ -1022,11 +1054,11 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req
10221054});
10231055
10241056router.post('/chats', jsonParser, validateAvatarUrlMiddleware, async function (request, response) {
1025- if (!request.body) return response.sendStatus(400);
1057+ try {
1058+ if (!request.body) return response.sendStatus(400);
10261059
10271060 const characterDirectory = (request.body.avatar_url).replace('.png', '');
10281061
1029- try {
10301062 const chatsDirectory = path.join(request.user.directories.chats, characterDirectory);
10311063
10321064 if (!fs.existsSync(chatsDirectory)) {
src/prompt-converters.js+38 -4
@@ -1,5 +1,5 @@
11import crypto from 'node:crypto';
22import { getConfigValue, tryParse } from './util.js';
33
44const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
55
@@ -411,11 +411,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
411411 }
412412
413413 const system_instruction = { parts: { text: sys_prompt.trim() } };
414+ const toolNameMap = {};
414415
415416 const contents = [];
416417 messages.forEach((message, index) => {
417418 // fix the roles
418419 if (message.role === 'system' || message.role === 'tool') {
419420 message.role = 'user';
420421 } else if (message.role === 'assistant') {
421422 message.role = 'model';
@@ -423,7 +424,21 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
423424
424425 // Convert the content to an array of parts
425426 if (!Array.isArray(message.content)) {
426- message.content = [{ type: 'text', text: String(message.content ?? '') }];
427+ const content = (() => {
428+ const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
429+ const hasToolCallId = typeof message.tool_call_id === 'string' && message.tool_call_id.length > 0;
430+
431+ if (hasToolCalls) {
432+ return { type: 'tool_calls', tool_calls: message.tool_calls };
433+ }
434+
435+ if (hasToolCallId) {
436+ return { type: 'tool_call_id', tool_call_id: message.tool_call_id, content: String(message.content ?? '') };
437+ }
438+
439+ return { type: 'text', text: String(message.content ?? '') };
440+ })();
441+ message.content = [content];
427442 }
428443
429444 // similar story as claude
@@ -455,6 +470,25 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
455470 message.content.forEach((part) => {
456471 if (part.type === 'text') {
457472 parts.push({ text: part.text });
473+ } else if (part.type === 'tool_call_id') {
474+ const name = toolNameMap[part.tool_call_id] ?? 'unknown';
475+ parts.push({
476+ functionResponse: {
477+ name: name,
478+ response: { name: name, content: part.content },
479+ },
480+ });
481+ } else if (part.type === 'tool_calls') {
482+ part.tool_calls.forEach((toolCall) => {
483+ parts.push({
484+ functionCall: {
485+ name: toolCall.function.name,
486+ args: tryParse(toolCall.function.arguments) ?? toolCall.function.arguments,
487+ },
488+ });
489+
490+ toolNameMap[toolCall.id] = toolCall.function.name;
491+ });
458492 } else if (part.type === 'image_url' && isMultimodal) {
459493 const mimeType = part.image_url.url.split(';')[0].split(':')[1];
460494 const base64Data = part.image_url.url.split(',')[1];
@@ -473,7 +507,7 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) {
473507 if (part.text) {
474508 contents[contents.length - 1].parts[0].text += '\n\n' + part.text;
475509 }
476510 if (part.inlineData || part.functionCall || part.functionResponse) {
477511 contents[contents.length - 1].parts.push(part);
478512 }
479513 });
src/server-events.js+21 -0
@@ -0,0 +1,21 @@
1+import EventEmitter from 'node:events';
2+import process from 'node:process';
3+
4+/**
5+ * @typedef {import('../index').ServerEventMap} ServerEventMap
6+ * @type {EventEmitter<ServerEventMap>} The default event source.
7+ */
8+export const serverEvents = new EventEmitter();
9+process.serverEvents = serverEvents;
10+export default serverEvents;
11+
12+/**
13+ * @enum {string}
14+ * @readonly
15+ */
16+export const EVENT_NAMES = Object.freeze({
17+ /**
18+ * Emitted when the server has started.
19+ */
20+ SERVER_STARTED: 'server-started',
21+});
src/util.js+8 -7
@@ -16,6 +16,7 @@ import mime from 'mime-types';
1616import { default as simpleGit } from 'simple-git';
1717import chalk from 'chalk';
1818import { LOG_LEVELS } from './constants.js';
19+import bytes from 'bytes';
1920
2021/**
2122 * Parsed config object.
@@ -856,14 +857,10 @@ export function setupLogLevel() {
856857export class MemoryLimitedMap {
857858 /**
858859 * Creates an instance of MemoryLimitedMap.
859860 * @param {numberstring} maxMemoryInBytescacheCapacity - The maximum allowedMaximum memory usage in byteshuman-readable forformat string(e.g., values'1 GB').
860861 */
861862 constructor(maxMemoryInBytescacheCapacity) {
862- if (typeof maxMemoryInBytes !== 'number' || maxMemoryInBytes <= 0 || isNaN(maxMemoryInBytes)) {
863+ this.maxMemory = bytes.parse(cacheCapacity) ?? 0;
863- console.warn('Invalid maxMemoryInBytes, using a fallback value of 1 GB.');
864- maxMemoryInBytes = 1024 * 1024 * 1024; // 1 GB
865- }
866- this.maxMemory = maxMemoryInBytes;
867864 this.currentMemory = 0;
868865 this.map = new Map();
869866 this.queue = [];
@@ -886,6 +883,10 @@ export class MemoryLimitedMap {
886883 * @param {string} value
887884 */
888885 set(key, value) {
886+ if (this.maxMemory <= 0) {
887+ return;
888+ }
889+
889890 if (typeof key !== 'string' || typeof value !== 'string') {
890891 return;
891892 }