Add configurable welcome screen recent chats, fix pin loss on rename, and add popup number input validation (#5436) * Add configurable recent chats settings and fix pin loss on chat rename - Add gear icon button to welcome screen header for settings - Settings popup with Max recent chats (default: 15) and Collapsed recent chats (default: 3) options - Settings stored in accountStorage as 'recentChatsSettings' - Add PinnedChatsManager.rename() to migrate pin state when chats are renamed - Call rename() from both character and group chat rename functions Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/99fa84c4-a82b-4253-96ff-a0621fb63f5c Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Fix pin rename: use file_name with .jsonl extension to match stored pin keys The data-file attribute contains chat_name (without .jsonl), but PinnedChatsManager stores keys using file_name (with .jsonl). Append .jsonl to both old and new names when calling PinnedChatsManager.rename() in character and group chat rename functions. Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/c8e4f720-1d0b-49ef-b57b-ba9d85c42d4f Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Refactor to support number inputs * Restore package-lock * Use clamp() from utils.js instead of manual Math.min/Math.max Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/1669884c-1bd3-4b60-808d-5610f24838e6 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Revert unintended package-lock.json changes Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/1669884c-1bd3-4b60-808d-5610f24838e6 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Use input values instead of magic numbers * Add auto-clamping on number inputs in popups and range placeholders in welcome-screen settings Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/9af8ad07-77bc-46db-9ec6-0030fecf0f8d Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Use constants for min/max chat limits to avoid hardcoded duplication in tooltips Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/9af8ad07-77bc-46db-9ec6-0030fecf0f8d Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Improve input validation by using Number.isFinite for min/max values in Popup class --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>

ff10249ab5ef7accac43815ae24b67877b3ce008

Copilot <198982749+Copilot@users.noreply.github.com>

Signed
4 files changed, +170 -10Ignore whitespace
public/css/welcome.css+1 -0
@@ -17,6 +17,7 @@
1717.welcomePanel.recentHidden .welcomeRecent,
1818.welcomePanel.recentHidden .recentChatsTitle,
1919.welcomePanel.recentHidden .hideRecentChats,
20+.welcomePanel.recentHidden .recentChatsSettings,
2021.welcomePanel:not(.recentHidden) .showRecentChats {
2122 display: none;
2223}
public/scripts/popup.js+45 -4
@@ -2,7 +2,7 @@ import dialogPolyfill from '../lib/dialog-polyfill.esm.js';
22import { shouldSendOnEnter } from './RossAscends-mods.js';
33import { t } from './i18n.js';
44import { power_user, toastPositionClasses } from './power-user.js';
55import { clamp, removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
66
77/** @readonly */
88/** @enum {Number} */
@@ -79,8 +79,11 @@ export const POPUP_RESULT = {
7979 * @property {string} label - The label text for the input
8080 * @property {string?} [tooltip=null] - Optional tooltip to be displayed. Default placeholder in input controls, tooltip icon behind the checkbox for those.
8181 * @property {boolean|string|undefined} [defaultState=false] - The default state when opening the popup (false if not set)
8282 * @property {('checkbox'|'text'|'textarea'|'number')?} [type='checkbox'] - The type of the input (default is checkbox)
8383 * @property {number?} [rows=1] - The number of rows for the input field, if the input is 'textarea'
84+ * @property {number?} [min] - The minimum value for number inputs
85+ * @property {number?} [max] - The maximum value for number inputs
86+ * @property {number?} [step] - The step value for number inputs
8487 */
8588
8689/**
@@ -387,8 +390,46 @@ export class Popup {
387390 label.appendChild(inputElement);
388391
389392 this.inputControls.appendChild(label);
393+ } else if (input.type === 'number') {
394+ const label = document.createElement('label');
395+ label.classList.add('text_label', 'justifyCenter');
396+ label.setAttribute('for', input.id);
397+
398+ const inputElement = document.createElement('input');
399+ inputElement.classList.add('text_pole', 'result-control');
400+ inputElement.type = 'number';
401+ inputElement.id = input.id;
402+ inputElement.value = String(input.defaultState ?? '');
403+ inputElement.placeholder = input.tooltip ?? '';
404+ inputElement.min = String(input.min ?? '');
405+ inputElement.max = String(input.max ?? '');
406+ inputElement.step = String(input.step ?? '');
407+ setTitleFromTooltip(inputElement, input.tooltip);
408+
409+ inputElement.addEventListener('change', () => {
410+ const value = parseFloat(inputElement.value);
411+ if (isNaN(value)) return;
412+
413+ const min = Number.isFinite(input.min) ? input.min : -Infinity;
414+ const max = Number.isFinite(input.max) ? input.max : Infinity;
415+ const clamped = clamp(value, min, max);
416+
417+ if (clamped !== value) {
418+ inputElement.value = String(clamped);
419+ toastr.warning(t`Value must be between ${min} and ${max}. Clamped to ${clamped}.`);
420+ }
421+ });
422+
423+ const labelText = document.createElement('span');
424+ labelText.innerText = input.label;
425+ labelText.dataset.i18n = input.label;
426+
427+ label.appendChild(labelText);
428+ label.appendChild(inputElement);
429+
430+ this.inputControls.appendChild(label);
390431 } else {
391432 console.warn('Unknown custom input type. Only checkbox, text, number and textarea are supported.', input);
392433 return;
393434 }
394435 });
@@ -720,7 +761,7 @@ export class Popup {
720761 this.inputResults = new Map(this.customInputs.map(input => {
721762 /** @type {HTMLInputElement} */
722763 const inputControl = this.dlg.querySelector(`#${input.id}`);
723764 const value = ['text', 'textarea', 'number'].includes(input.type) ? inputControl.value : inputControl.checked;
724765 return [inputControl.id, value];
725766 }));
726767 }
public/scripts/templates/welcomePanel.html+3 -0
@@ -5,6 +5,9 @@
55 <div class="mes_button showRecentChats" title="Show recent chats" data-i18n="[title]Show recent chats">
66 <i class="fa-solid fa-circle-chevron-down fa-fw fa-lg"></i>
77 </div>
8+ <div class="mes_button recentChatsSettings" title="Recent chats settings" data-i18n="[title]Recent chats settings">
9+ <i class="fa-solid fa-gear fa-fw fa-lg"></i>
10+ </div>
811 <div class="mes_button hideRecentChats" title="Hide recent chats" data-i18n="[title]Hide recent chats">
912 <i class="fa-solid fa-circle-xmark fa-fw fa-lg"></i>
1013 </div>
public/scripts/welcome-screen.js+121 -6
@@ -35,14 +35,43 @@ import { callGenericPopup, POPUP_TYPE } from './popup.js';
3535import { getMessageTimeStamp } from './RossAscends-mods.js';
3636import { renderTemplateAsync } from './templates.js';
3737import { accountStorage } from './util/AccountStorage.js';
3838import { clamp, flashHighlight, isElementInViewport, sortMoments, timestampToMoment } from './utils.js';
3939
4040const assistantAvatarKey = 'assistant';
4141const pinnedChatsKey = 'pinnedChats';
42+const recentChatsSettingsKey = 'recentChatsSettings';
4243const defaultAssistantAvatar = 'default_Assistant.png';
4344
4445const DEFAULT_DISPLAYEDDEFAULT_MAX_DISPLAYED = 315;
4546const MAX_DISPLAYEDDEFAULT_COLLAPSED_DISPLAYED = 153;
47+
48+/**
49+ * Gets the current recent chats settings from account storage.
50+ * @returns {{ maxDisplayed: number, collapsedDisplayed: number }}
51+ */
52+function getRecentChatsSettings() {
53+ const value = accountStorage.getItem(recentChatsSettingsKey);
54+ if (value) {
55+ try {
56+ const parsed = JSON.parse(value);
57+ return {
58+ maxDisplayed: Math.max(1, parseInt(parsed.maxDisplayed) || DEFAULT_MAX_DISPLAYED),
59+ collapsedDisplayed: Math.max(1, parseInt(parsed.collapsedDisplayed) || DEFAULT_COLLAPSED_DISPLAYED),
60+ };
61+ } catch {
62+ // Ignore parse errors
63+ }
64+ }
65+ return { maxDisplayed: DEFAULT_MAX_DISPLAYED, collapsedDisplayed: DEFAULT_COLLAPSED_DISPLAYED };
66+}
67+
68+/**
69+ * Saves recent chats settings to account storage.
70+ * @param {{ maxDisplayed: number, collapsedDisplayed: number }} settings
71+ */
72+function saveRecentChatsSettings(settings) {
73+ accountStorage.setItem(recentChatsSettingsKey, JSON.stringify(settings));
74+}
4675
4776/**
4877 * @typedef {Pick<RecentChat, 'group' | 'avatar' | 'file_name'>} PinnedChat
@@ -82,7 +111,7 @@ class PinnedChatsManager {
82111
83112 /**
84113 * Generates a key for pinned chat storage.
85114 * @param {Partial<RecentChat>} recentChat Recent chat data
86115 * @returns {string} Key for pinned chat storage
87116 */
88117 static getKey(recentChat) {
@@ -141,6 +170,28 @@ class PinnedChatsManager {
141170 }
142171
143172 /**
173+ * Migrates pinned state when a chat is renamed.
174+ * @param {Partial<RecentChat>} recentChat Recent chat data (with original file_name)
175+ * @param {string} newFileName New file name after rename
176+ */
177+ static rename(recentChat, newFileName) {
178+ const oldKey = this.getKey(recentChat);
179+ const pinState = { ...this.getState() };
180+ if (!(oldKey in pinState)) {
181+ return;
182+ }
183+ const updatedChat = { ...recentChat, file_name: newFileName };
184+ const newKey = this.getKey(updatedChat);
185+ pinState[newKey] = {
186+ group: recentChat.group,
187+ avatar: recentChat.avatar,
188+ file_name: newFileName,
189+ };
190+ delete pinState[oldKey];
191+ this.#saveState(pinState);
192+ }
193+
194+ /**
144195 * Gets all pinned chats.
145196 * @returns {PinnedChat[]}
146197 */
@@ -295,6 +346,12 @@ async function sendWelcomePanel(chats, expand = false) {
295346 accountStorage.setItem(recentHiddenKey, 'true');
296347 });
297348 });
349+ root.querySelectorAll('.recentChatsSettings').forEach((button) => {
350+ button.addEventListener('click', async (event) => {
351+ event.stopPropagation();
352+ await openRecentChatsSettingsPopup();
353+ });
354+ });
298355 });
299356 fragment.querySelectorAll('.recentChat').forEach((item) => {
300357 item.addEventListener('click', () => {
@@ -493,6 +550,7 @@ async function renameRecentCharacterChat(avatarId, fileName) {
493550 newFileName: newName,
494551 loader: false,
495552 });
553+ PinnedChatsManager.rename({ avatar: avatarId, group: '', file_name: fileName }, newName);
496554 await updateRemoteChatName(characterId, newName);
497555 await refreshWelcomeScreen();
498556 toastr.success(t`Chat renamed.`);
@@ -526,6 +584,7 @@ async function renameRecentGroupChat(groupId, fileName) {
526584 newFileName: String(newName),
527585 loader: false,
528586 });
587+ PinnedChatsManager.rename({ avatar: '', group: groupId, file_name: fileName }, String(newName));
529588 await refreshWelcomeScreen();
530589 toastr.success(t`Group chat renamed.`);
531590 } catch (error) {
@@ -628,6 +687,61 @@ async function refreshWelcomeScreen({ flashChat = null } = {}) {
628687}
629688
630689/**
690+ * Opens a popup to configure recent chats settings.
691+ */
692+async function openRecentChatsSettingsPopup() {
693+ const settings = getRecentChatsSettings();
694+
695+ const MIN_CHATS = 1;
696+ const MAX_CHATS = 1000;
697+
698+ /** @type {import('./popup.js').CustomPopupInput} */
699+ const maxRecentChatsInput = {
700+ id: 'maxRecentChats',
701+ type: 'number',
702+ label: t`Max recent chats`,
703+ tooltip: t`${MIN_CHATS} - ${MAX_CHATS}`,
704+ defaultState: String(settings.maxDisplayed),
705+ min: MIN_CHATS,
706+ max: MAX_CHATS,
707+ step: 1,
708+ };
709+
710+ /** @type {import('./popup.js').CustomPopupInput} */
711+ const collapsedRecentChatsInput = {
712+ id: 'collapsedRecentChats',
713+ type: 'number',
714+ label: t`Collapsed recent chats`,
715+ tooltip: t`${MIN_CHATS} - ${MAX_CHATS}`,
716+ defaultState: String(settings.collapsedDisplayed),
717+ min: MIN_CHATS,
718+ max: MAX_CHATS,
719+ step: 1,
720+ };
721+
722+ await callGenericPopup(t`Recent Chats Settings`, POPUP_TYPE.CONFIRM, null, {
723+ okButton: t`Save`,
724+ cancelButton: t`Cancel`,
725+ customInputs: [maxRecentChatsInput, collapsedRecentChatsInput],
726+ onClose: (popup) => {
727+ if (!popup.result) {
728+ return;
729+ }
730+
731+ const maxInputValue = popup.inputResults.get(maxRecentChatsInput.id)?.toString() ?? String(DEFAULT_MAX_DISPLAYED);
732+ const collapsedInputValue = popup.inputResults.get(collapsedRecentChatsInput.id)?.toString() ?? String(DEFAULT_COLLAPSED_DISPLAYED);
733+
734+ const newMax = clamp(parseInt(maxInputValue) || DEFAULT_MAX_DISPLAYED, maxRecentChatsInput.min, maxRecentChatsInput.max);
735+ const newCollapsed = clamp(parseInt(collapsedInputValue) || DEFAULT_COLLAPSED_DISPLAYED, collapsedRecentChatsInput.min, newMax);
736+
737+ saveRecentChatsSettings({ maxDisplayed: newMax, collapsedDisplayed: newCollapsed });
738+ },
739+ });
740+
741+ await refreshWelcomeScreen();
742+}
743+
744+/**
631745 * Gets the list of recent chats from the server.
632746 * @returns {Promise<RecentChat[]>} List of recent chats
633747 *
@@ -649,10 +763,11 @@ async function refreshWelcomeScreen({ flashChat = null } = {}) {
649763 * @property {boolean} pinned Indicates if the chat is pinned
650764 */
651765async function getRecentChats() {
766+ const settings = getRecentChatsSettings();
652767 const response = await fetch('/api/chats/recent', {
653768 method: 'POST',
654769 headers: getRequestHeaders(),
655770 body: JSON.stringify({ max: MAX_DISPLAYEDsettings.maxDisplayed, pinned: PinnedChatsManager.getAll() }),
656771 cache: 'no-cache',
657772 });
658773
@@ -694,7 +809,7 @@ async function getRecentChats() {
694809 chat.chat_name = chat.file_name.replace('.jsonl', '');
695810 chat.char_thumbnail = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar;
696811 chat.is_group = !!group;
697812 chat.hidden = index >= DEFAULT_DISPLAYEDsettings.collapsedDisplayed;
698813 chat.avatar = chat.avatar || '';
699814 chat.group = chat.group || '';
700815 chat.pinned = PinnedChatsManager.isPinned(chat);