perf (frontend panel): added a frontend panel runtime, significantly reducing the number of DOM nodes.

This commit is contained in:
Darkatse 2026-03-14 18:44:42 -04:00
parent ef5e596d1b
commit 01ec179cd6
18 changed files with 1382 additions and 5 deletions

@ -1 +0,0 @@
Subproject commit 4e1b05e987077ae763c9d8c8407f002a2e43f992

View file

@ -0,0 +1,89 @@
import { execFileSync } from 'node:child_process';
const SEARCH_ROOTS = [
'src',
'scripts',
'src-tauri',
'.cache',
];
const RG_EXCLUDES = [
'!node_modules/**',
'!src-tauri/target/**',
'!release/**',
'!dist/**',
'!.git/**',
];
const QUERIES = [
{
title: 'Phase 1: Anchor Zone (SPresets_script)',
patterns: [
'#openai_preset_import_file',
'#completion_prompt_manager',
'#extensions-settings-button .drawer-toggle',
],
},
{
title: 'Phase 1: API Connections Gates',
patterns: [
"#main_api').on('change",
"trigger('change')",
'toggleChatCompletionForms',
'showTypeSpecificControls',
'[data-source]',
'[data-tg-type]',
],
},
{
title: 'Phase 1: Extensions Gates',
patterns: [
'regex_container',
'extension_container',
],
},
];
function runRg(pattern) {
const args = [
'--no-ignore',
'--hidden',
'-n',
...RG_EXCLUDES.flatMap((g) => ['-g', g]),
pattern,
...SEARCH_ROOTS,
];
try {
return execFileSync('rg', args, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
} catch (error) {
const stderr = String(error?.stderr || '').trim();
// rg exits with 1 on no matches; treat as empty.
if (String(error?.status) === '1') {
return '';
}
throw new Error(`rg failed for pattern '${pattern}': ${stderr || error}`);
}
}
function truncateLines(text, maxLines = 40) {
const lines = String(text || '').split(/\r?\n/).filter(Boolean);
if (lines.length <= maxLines) {
return { total: lines.length, text: lines.join('\n') };
}
const shown = lines.slice(0, maxLines).join('\n');
return { total: lines.length, text: `${shown}\n... (${lines.length - maxLines} more lines)` };
}
for (const query of QUERIES) {
console.log(`\n## ${query.title}`);
for (const pattern of query.patterns) {
const output = runRg(pattern);
if (!output) {
console.log(`\n- ${pattern}: (no matches)`);
continue;
}
const truncated = truncateLines(output);
console.log(`\n- ${pattern} (${truncated.total} lines):\n${truncated.text}`);
}
}

View file

@ -9,6 +9,7 @@ use serde_json::Value;
pub struct TauriTavernSettingsDto {
pub updates: TauriTavernUpdateSettingsDto,
pub perf_profile: String,
pub panel_runtime_profile: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -25,6 +26,7 @@ pub struct StartupUpdatePopupSettingsDto {
pub struct UpdateTauriTavernSettingsDto {
pub updates: Option<TauriTavernUpdateSettingsDto>,
pub perf_profile: Option<String>,
pub panel_runtime_profile: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@ -95,6 +97,7 @@ impl From<TauriTavernSettings> for TauriTavernSettingsDto {
Self {
updates: TauriTavernUpdateSettingsDto::from(settings.updates),
perf_profile: settings.perf_profile,
panel_runtime_profile: settings.panel_runtime_profile,
}
}
}

View file

@ -46,6 +46,10 @@ impl SettingsService {
settings.perf_profile = perf_profile;
}
if let Some(panel_runtime_profile) = dto.panel_runtime_profile {
settings.panel_runtime_profile = panel_runtime_profile;
}
self.settings_repository
.save_tauritavern_settings(&settings)
.await?;

View file

@ -5,11 +5,17 @@ fn default_perf_profile() -> String {
"auto".to_string()
}
fn default_panel_runtime_profile() -> String {
"compat".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TauriTavernSettings {
pub updates: TauriTavernUpdateSettings,
#[serde(default = "default_perf_profile")]
pub perf_profile: String,
#[serde(default = "default_panel_runtime_profile")]
pub panel_runtime_profile: String,
}
impl Default for TauriTavernSettings {
@ -17,6 +23,7 @@ impl Default for TauriTavernSettings {
Self {
updates: TauriTavernUpdateSettings::default(),
perf_profile: default_perf_profile(),
panel_runtime_profile: default_panel_runtime_profile(),
}
}
}

View file

@ -1,6 +1,7 @@
import { callGenericPopup, POPUP_RESULT, POPUP_TYPE, Popup } from '../../popup.js';
import { isMobile } from '../../RossAscends-mods.js';
import { t, translate } from '../../i18n.js';
import { getTauriTavernSettings, updateTauriTavernSettings } from '../../../tauri-bridge.js';
const TAURITAVERN_SETTINGS_BUTTON_ID = 'tauritavern_settings_button';
const LAN_SYNC_DEVICES_CHANGED_EVENT = 'tauritavern:lan_sync_devices_changed';
@ -50,7 +51,7 @@ function bindLanSyncButton() {
}
button.addEventListener('click', () => {
void openLanSyncPopup();
runOrPopup(openTauriTavernSettingsPopup);
});
}
@ -290,6 +291,84 @@ async function scanPairUriFromCamera() {
return content;
}
async function openTauriTavernSettingsPopup() {
const settings = await getTauriTavernSettings();
const root = document.createElement('div');
root.className = 'flex-container flexFlowColumn';
root.style.gap = '12px';
root.innerHTML = `
<div class="flex-container flexFlowColumn" style="gap: 10px;">
<b data-i18n="TauriTavern Settings">TauriTavern Settings</b>
<div class="flex-container flexFlowColumn" style="gap: 8px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 10px;">
<b data-i18n="Panel Runtime">Panel Runtime</b>
<div class="flex-container alignItemsBaseline" style="gap: 10px; flex-wrap: wrap;">
<span data-i18n="Mode">Mode</span>:
<select id="tt-panel-runtime-profile" class="text_pole" style="margin: 0; width: auto; min-width: 260px; max-width: 100%; flex: 1;">
<option value="compat" data-i18n="Compatibility (Recommended)">Compatibility (Recommended)</option>
<option value="aggressive" data-i18n="Aggressive (More DOM Parking)">Aggressive (More DOM Parking)</option>
<option value="off" data-i18n="Off (Legacy)">Off (Legacy)</option>
</select>
</div>
<small style="opacity: 0.85;" data-i18n="Requires reload to apply.">Requires reload to apply.</small>
</div>
<div class="flex-container flexFlowColumn" style="gap: 8px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 10px;">
<b data-i18n="LAN Sync">LAN Sync</b>
<div class="flex-container flexFlowRow" style="gap: 10px;">
<div id="tt-open-lan-sync" class="menu_button" data-i18n="Open">Open</div>
</div>
</div>
</div>
`.trim();
const profileSelect = root.querySelector('#tt-panel-runtime-profile');
if (!(profileSelect instanceof HTMLSelectElement)) {
throw new Error('TauriTavern settings: panel runtime selector not found');
}
const currentProfile = settings.panel_runtime_profile;
profileSelect.value = typeof currentProfile === 'string' && currentProfile ? currentProfile : 'compat';
const openLanSyncButton = root.querySelector('#tt-open-lan-sync');
if (!(openLanSyncButton instanceof HTMLElement)) {
throw new Error('TauriTavern settings: LAN sync button not found');
}
openLanSyncButton.addEventListener('click', () => runOrPopup(openLanSyncPopup));
const result = await callGenericPopup(root, POPUP_TYPE.CONFIRM, '', {
okButton: translate('Save'),
cancelButton: translate('Close'),
allowVerticalScrolling: true,
wide: false,
large: false,
});
if (result !== POPUP_RESULT.AFFIRMATIVE) {
return;
}
const nextProfile = String(profileSelect.value || '').trim();
if (!nextProfile || nextProfile === currentProfile) {
return;
}
await updateTauriTavernSettings({
panel_runtime_profile: nextProfile,
});
// Keep in sync with:
// - src/tauri/main/services/panel-runtime/preinstall.js
// - src/tauri/main/services/panel-runtime/install.js
//
// Mirror the chosen profile so bootstrap can synchronously honor `off`
// before Tauri settings are loaded.
localStorage.setItem('tt:panelRuntimeProfile', nextProfile);
window.location.reload();
}
async function openLanSyncPopup() {
const panel = buildLanSyncPopup();

View file

@ -0,0 +1,313 @@
// @ts-check
import { PanelRuntimeKind } from '../../services/panel-runtime/panel-runtime-kinds.js';
/**
* @typedef {import('../../services/embedded-runtime/embedded-runtime-manager.js').createEmbeddedRuntimeManager} createEmbeddedRuntimeManager
* @typedef {ReturnType<createEmbeddedRuntimeManager>} EmbeddedRuntimeManager
*/
const MAIN_API_TO_CONNECTOR_ID = Object.freeze({
koboldhorde: 'kobold_horde',
kobold: 'kobold_api',
novel: 'novel_api',
textgenerationwebui: 'textgenerationwebui_api',
openai: 'openai_api',
});
/** @type {boolean} */
let preinstalled = false;
/** @type {EmbeddedRuntimeManager | null} */
let manager = null;
/** @type {Map<string, string> | null} */
let providerSlotIdByMainApi = null;
/** @type {ReturnType<typeof createAttributeChildGate> | null} */
let openaiSourceGate = null;
/** @type {ReturnType<typeof createAttributeChildGate> | null} */
let textgenTypeGate = null;
function requireJQuery() {
const jq = /** @type {any} */ (globalThis).jQuery || /** @type {any} */ (globalThis).$;
if (typeof jq !== 'function') {
throw new Error('PanelRuntime: jQuery not found');
}
return jq;
}
/**
* @param {string} id
* @returns {HTMLElement}
*/
function mustGetElementById(id) {
const el = document.getElementById(id);
if (!(el instanceof HTMLElement)) {
throw new Error(`PanelRuntime: #${id} not found`);
}
return el;
}
/**
* @param {HTMLElement} host
* @param {DocumentFragment} frag
*/
function parkChildrenInto(host, frag) {
while (host.firstChild) {
frag.appendChild(host.firstChild);
}
}
/**
* @param {HTMLElement} host
* @param {DocumentFragment} frag
*/
function restoreChildren(host, frag) {
if (!frag.firstChild) {
return;
}
host.appendChild(frag);
}
/**
* Registers a slot that parks/restores an element's children while keeping the
* element itself connected for selector compatibility.
*
* @param {EmbeddedRuntimeManager} manager
* @param {{ slotId: string; element: HTMLElement; initialVisible: boolean }} config
*/
function registerChildParkingSlot(manager, { slotId, element, initialVisible }) {
/** @type {DocumentFragment} */
let parked = document.createDocumentFragment();
manager.register({
id: slotId,
kind: PanelRuntimeKind.SubtreeGate,
element,
visibilityMode: 'manual',
initialVisible,
hydrate: () => {
restoreChildren(element, parked);
},
dehydrate: () => {
parkChildrenInto(element, parked);
},
});
}
/**
* @param {HTMLElement} root
* @param {string} attr
* @returns {HTMLElement[]}
*/
function collectAttributeHosts(root, attr) {
return Array.from(root.querySelectorAll(`[${attr}]`)).filter((el) => el instanceof HTMLElement);
}
/**
* @param {HTMLElement} root
* @param {{
* attr: string;
* modeAttr: string;
* includeAll?: boolean;
* }} config
*/
function createAttributeChildGate(root, { attr, modeAttr, includeAll = false }) {
/** @type {Array<{ host: HTMLElement; parked: DocumentFragment; values: string[]; mode: 'except' | 'include' }>} */
const gates = [];
for (const host of collectAttributeHosts(root, attr)) {
const rawList = String(host.getAttribute(attr) || '').trim();
if (!rawList) {
continue;
}
const values = rawList.split(',').map((s) => s.trim()).filter(Boolean);
const rawMode = String(host.getAttribute(modeAttr) || '').trim().toLowerCase();
const mode = rawMode === 'except' ? 'except' : 'include';
gates.push({
host,
parked: document.createDocumentFragment(),
values,
mode,
});
}
return {
/**
* @param {string} value
*/
apply: (value) => {
const v = String(value || '').trim();
for (const g of gates) {
const matchesValue = g.values.includes(v);
const shouldBeVisible = g.mode === 'except'
? !matchesValue
: matchesValue || (includeAll && g.values.includes('all'));
if (shouldBeVisible) {
restoreChildren(g.host, g.parked);
} else {
parkChildrenInto(g.host, g.parked);
}
}
},
};
}
/**
* @param {string} id
*/
function currentSelectValue(id) {
const el = document.getElementById(id);
if (!(el instanceof HTMLSelectElement)) {
return '';
}
return String(el.value || '').trim();
}
/**
* Hydrates the selected provider subtree and parks all others, then applies any
* nested gates (OpenAI source / Textgen type) for the active provider.
*
* Must run synchronously inside the `change` event so downstream handlers can
* safely query their expected DOM.
*
* @param {string} mainApi
*/
function handleMainApiChange(mainApi) {
if (!manager || !providerSlotIdByMainApi) {
return;
}
const next = String(mainApi || '').trim();
for (const [api, slotId] of providerSlotIdByMainApi.entries()) {
manager.setVisible(slotId, api === next);
}
manager.reconcile();
if (next === 'openai' && openaiSourceGate) {
openaiSourceGate.apply(currentSelectValue('chat_completion_source') || 'openai');
}
if (next === 'textgenerationwebui' && textgenTypeGate) {
textgenTypeGate.apply(currentSelectValue('textgen_type') || '');
}
}
/**
* @param {string} source
*/
function handleChatCompletionSourceChange(source) {
if (!openaiSourceGate) {
return;
}
const mainApi = currentSelectValue('main_api');
if (mainApi !== 'openai') {
return;
}
openaiSourceGate.apply(source || 'openai');
}
/**
* @param {string} type
*/
function handleTextgenTypeChange(type) {
if (!textgenTypeGate) {
return;
}
const mainApi = currentSelectValue('main_api');
if (mainApi !== 'textgenerationwebui') {
return;
}
textgenTypeGate.apply(type || '');
}
/**
* Phase 1 (preinstall): register early `change` listeners so our gate handlers
* run before SillyTavern's own handlers (including jQuery `.trigger('change')`).
*/
export function preinstallApiConnectionsSubtreeGates() {
if (preinstalled) {
return;
}
preinstalled = true;
const $ = requireJQuery();
const mainApi = mustGetElementById('main_api');
if (!(mainApi instanceof HTMLSelectElement)) {
throw new Error('PanelRuntime: #main_api is not a <select>');
}
$(mainApi).on('change.ttPanelRuntimeGate', () => {
handleMainApiChange(String(mainApi.value || ''));
});
const chatCompletionSource = mustGetElementById('chat_completion_source');
if (!(chatCompletionSource instanceof HTMLSelectElement)) {
throw new Error('PanelRuntime: #chat_completion_source is not a <select>');
}
$(chatCompletionSource).on('change.ttPanelRuntimeGate', () => {
handleChatCompletionSourceChange(String(chatCompletionSource.value || ''));
});
const textgenType = mustGetElementById('textgen_type');
if (!(textgenType instanceof HTMLSelectElement)) {
throw new Error('PanelRuntime: #textgen_type is not a <select>');
}
$(textgenType).on('change.ttPanelRuntimeGate', () => {
handleTextgenTypeChange(String(textgenType.value || ''));
});
}
/**
* Phase 1 (activate): build gates and immediately park inactive subtrees.
*
* @param {{ manager: EmbeddedRuntimeManager }} options
*/
export function activateApiConnectionsSubtreeGates({ manager: nextManager }) {
if (!nextManager) {
throw new Error('activateApiConnectionsSubtreeGates requires manager');
}
if (manager) {
throw new Error('activateApiConnectionsSubtreeGates called twice');
}
manager = nextManager;
const selectedMainApi = currentSelectValue('main_api');
providerSlotIdByMainApi = new Map();
for (const [mainApi, connectorId] of Object.entries(MAIN_API_TO_CONNECTOR_ID)) {
const element = mustGetElementById(connectorId);
const slotId = `tt:gate:rm_api_block:main_api:${mainApi}`;
providerSlotIdByMainApi.set(mainApi, slotId);
registerChildParkingSlot(manager, { slotId, element, initialVisible: mainApi === selectedMainApi });
}
const openaiApi = mustGetElementById('openai_api');
openaiSourceGate = createAttributeChildGate(openaiApi, {
attr: 'data-source',
modeAttr: 'data-source-mode',
});
const textgenApi = mustGetElementById('textgenerationwebui_api');
textgenTypeGate = createAttributeChildGate(textgenApi, {
attr: 'data-tg-type',
modeAttr: 'data-tg-type-mode',
includeAll: true,
});
manager.reconcile();
if (selectedMainApi === 'openai') {
openaiSourceGate.apply(currentSelectValue('chat_completion_source') || 'openai');
}
if (selectedMainApi === 'textgenerationwebui') {
textgenTypeGate.apply(currentSelectValue('textgen_type') || '');
}
}

View file

@ -0,0 +1,136 @@
// @ts-check
import { PanelRuntimeKind } from '../../services/panel-runtime/panel-runtime-kinds.js';
/**
* @typedef {import('../../services/embedded-runtime/embedded-runtime-manager.js').createEmbeddedRuntimeManager} createEmbeddedRuntimeManager
* @typedef {ReturnType<createEmbeddedRuntimeManager>} EmbeddedRuntimeManager
*/
/**
* @param {HTMLElement} drawerContent
*/
function isDrawerOpen(drawerContent) {
return drawerContent.classList.contains('openDrawer') && !drawerContent.classList.contains('closedDrawer');
}
const COMPAT_ALWAYS_CONNECTED_EXTENSION_CONTAINER_IDS = new Set([
'regex_container',
'qr_container',
]);
/**
* @param {string} id
* @returns {HTMLElement}
*/
function mustGetElementById(id) {
const el = document.getElementById(id);
if (!(el instanceof HTMLElement)) {
throw new Error(`PanelRuntime: #${id} not found`);
}
return el;
}
/**
* @param {HTMLElement} host
* @param {DocumentFragment} frag
*/
function parkChildrenInto(host, frag) {
while (host.firstChild) {
frag.appendChild(host.firstChild);
}
}
/**
* @param {HTMLElement} host
* @param {DocumentFragment} frag
*/
function restoreChildren(host, frag) {
if (!frag.firstChild) {
return;
}
host.appendChild(frag);
}
/**
* Phase 1: Park extension settings DOM under `#rm_extensions_block` while the
* drawer is closed, keeping the mount containers connected for compatibility.
*
* @param {{ manager: EmbeddedRuntimeManager }} options
*/
export function installExtensionsSubtreeGates({ manager }) {
if (!manager) {
throw new Error('installExtensionsSubtreeGates requires manager');
}
const host = mustGetElementById('rm_extensions_block');
const alwaysConnectedContainerIds = manager.profile === 'compat'
? COMPAT_ALWAYS_CONNECTED_EXTENSION_CONTAINER_IDS
: null;
const drawer = host.closest('.drawer');
if (!(drawer instanceof HTMLElement)) {
throw new Error('PanelRuntime(extensions): .drawer ancestor not found');
}
const toggle = drawer.querySelector('.drawer-toggle');
if (!(toggle instanceof HTMLElement)) {
throw new Error('PanelRuntime(extensions): .drawer-toggle not found');
}
const containers = Array.from(host.querySelectorAll('.extension_container')).filter((el) => el instanceof HTMLElement);
/** @type {Map<HTMLElement, DocumentFragment>} */
const parkedByContainer = new Map();
for (const container of containers) {
if (alwaysConnectedContainerIds && alwaysConnectedContainerIds.has(container.id)) {
continue;
}
parkedByContainer.set(container, document.createDocumentFragment());
}
const slotId = 'tt:gate:rm_extensions_block:extension_containers';
manager.register({
id: slotId,
kind: PanelRuntimeKind.SubtreeGate,
element: host,
visibilityMode: 'manual',
initialVisible: isDrawerOpen(host),
hydrate: () => {
for (const [container, frag] of parkedByContainer.entries()) {
restoreChildren(container, frag);
}
},
dehydrate: () => {
for (const [container, frag] of parkedByContainer.entries()) {
parkChildrenInto(container, frag);
}
},
});
const onToggleClickCapture = () => {
manager.setVisible(slotId, true);
manager.reconcile();
};
toggle.addEventListener('click', onToggleClickCapture, true);
const classObserver = new MutationObserver(() => {
const open = isDrawerOpen(host);
manager.setVisible(slotId, open);
if (open) {
manager.reconcile();
}
});
classObserver.observe(host, { attributes: true, attributeFilter: ['class'] });
manager.reconcile();
return {
slotId,
dispose: () => {
classObserver.disconnect();
toggle.removeEventListener('click', onToggleClickCapture, true);
},
};
}

View file

@ -0,0 +1,343 @@
// @ts-check
import { PanelRuntimeKind } from '../../services/panel-runtime/panel-runtime-kinds.js';
/**
* @typedef {import('../../services/embedded-runtime/embedded-runtime-manager.js').createEmbeddedRuntimeManager} createEmbeddedRuntimeManager
* @typedef {ReturnType<createEmbeddedRuntimeManager>} EmbeddedRuntimeManager
*/
/**
* @param {HTMLElement} drawerContent
*/
function isDrawerOpen(drawerContent) {
return drawerContent.classList.contains('openDrawer') && !drawerContent.classList.contains('closedDrawer');
}
const LEFT_NAV_MAIN_API_BLOCKS = Object.freeze({
koboldhorde: {
settingsId: 'kobold_api-settings',
presetsId: 'kobold_api-presets',
rangesId: null,
streamingId: null,
},
kobold: {
settingsId: 'kobold_api-settings',
presetsId: 'kobold_api-presets',
rangesId: null,
streamingId: 'streaming_kobold_block',
},
textgenerationwebui: {
settingsId: 'textgenerationwebui_api-settings',
presetsId: 'textgenerationwebui_api-presets',
rangesId: null,
streamingId: 'streaming_textgenerationwebui_block',
},
novel: {
settingsId: 'novel_api-settings',
presetsId: 'novel_api-presets',
rangesId: 'range_block_novel',
streamingId: 'streaming_novel_block',
},
openai: {
settingsId: 'openai_settings',
presetsId: 'openai_api-presets',
rangesId: 'range_block_openai',
streamingId: null,
},
});
/**
* @param {string} id
* @param {string} display
*/
function setDisplay(id, display) {
const el = document.getElementById(id);
if (!(el instanceof HTMLElement)) {
throw new Error(`PanelParking(left-nav): #${id} not found`);
}
el.style.display = display;
}
function syncLeftNavMainApiUi() {
const mainApi = document.getElementById('main_api');
if (!(mainApi instanceof HTMLSelectElement)) {
throw new Error('PanelParking(left-nav): #main_api <select> not found');
}
const selected = String(mainApi.value || '').trim();
const active = /** @type {keyof typeof LEFT_NAV_MAIN_API_BLOCKS} */ (selected);
if (!Object.prototype.hasOwnProperty.call(LEFT_NAV_MAIN_API_BLOCKS, active)) {
return;
}
for (const [apiName, blocks] of Object.entries(LEFT_NAV_MAIN_API_BLOCKS)) {
if (apiName === active) {
continue;
}
setDisplay(blocks.settingsId, 'none');
setDisplay(blocks.presetsId, 'none');
if (blocks.rangesId) {
setDisplay(blocks.rangesId, 'none');
}
if (blocks.streamingId) {
setDisplay(blocks.streamingId, 'none');
}
}
const activeBlocks = LEFT_NAV_MAIN_API_BLOCKS[active];
if (activeBlocks.streamingId) {
setDisplay(activeBlocks.streamingId, 'block');
}
setDisplay(activeBlocks.settingsId, 'block');
if (activeBlocks.rangesId) {
setDisplay(activeBlocks.rangesId, 'block');
}
setDisplay(activeBlocks.presetsId, active === 'openai' ? 'flex' : 'block');
setDisplay('ai_module_block_novel', active === 'novel' ? 'block' : 'none');
const textgenType = document.getElementById('textgen_type');
const isOpenRouter = textgenType instanceof HTMLSelectElement && String(textgenType.value || '').trim() === 'openrouter';
const showPromptCost = active === 'textgenerationwebui' && isOpenRouter;
setDisplay('prompt_cost_block', showPromptCost ? 'block' : 'none');
setDisplay('common-gen-settings-block', active === 'openai' ? 'none' : 'block');
}
/**
* @param {EmbeddedRuntimeManager} manager
* @param {{ panelId: string; parkedSelector: string; pinnedSelectors?: string[]; afterHydrate?: (reason: string) => void }} config
*/
function registerDrawerParking(manager, { panelId, parkedSelector, pinnedSelectors = [], afterHydrate }) {
const host = document.getElementById(panelId);
if (!(host instanceof HTMLElement)) {
throw new Error(`PanelParking: #${panelId} not found`);
}
const drawer = host.closest('.drawer');
if (!(drawer instanceof HTMLElement)) {
throw new Error(`PanelParking(${panelId}): .drawer ancestor not found`);
}
const toggle = drawer.querySelector('.drawer-toggle');
if (!(toggle instanceof HTMLElement)) {
throw new Error(`PanelParking(${panelId}): .drawer-toggle not found`);
}
const parkedRoot = host.querySelector(parkedSelector);
if (!(parkedRoot instanceof HTMLElement)) {
throw new Error(`PanelParking(${panelId}): parked root not found: ${parkedSelector}`);
}
/** @type {{ park: () => void; restore: () => void } | null} */
let pinned = null;
if (Array.isArray(pinnedSelectors) && pinnedSelectors.length > 0) {
const anchorZone = document.createElement('div');
anchorZone.dataset.ttPanelAnchorZone = panelId;
anchorZone.style.display = 'none';
host.appendChild(anchorZone);
/** @type {Array<{ selector: string; start: Comment; end: Comment; slot: HTMLElement }>} */
const ranges = [];
for (const rawSelector of pinnedSelectors) {
const selector = String(rawSelector || '').trim();
if (!selector) {
continue;
}
const anchor = parkedRoot.querySelector(selector);
if (!(anchor instanceof Element)) {
throw new Error(`PanelParking(${panelId}): pinned selector not found in parked root: ${selector}`);
}
const parent = anchor.parentNode;
if (!parent) {
throw new Error(`PanelParking(${panelId}): pinned selector has no parent: ${selector}`);
}
const start = document.createComment(`tt:pinned-start:${panelId}:${selector}`);
const end = document.createComment(`tt:pinned-end:${panelId}:${selector}`);
parent.insertBefore(start, anchor);
parent.insertBefore(end, anchor.nextSibling);
const slot = document.createElement('div');
slot.dataset.ttPinnedSelector = selector;
anchorZone.appendChild(slot);
ranges.push({ selector, start, end, slot });
}
pinned = {
park: () => {
for (const r of ranges) {
const parent = r.start.parentNode;
if (!parent) {
throw new Error(`PanelParking(${panelId}): pinned start lost: ${r.selector}`);
}
if (r.end.parentNode !== parent) {
throw new Error(`PanelParking(${panelId}): pinned range split: ${r.selector}`);
}
while (r.start.nextSibling && r.start.nextSibling !== r.end) {
r.slot.appendChild(r.start.nextSibling);
}
}
},
restore: () => {
for (const r of ranges) {
const parent = r.start.parentNode;
if (!parent) {
throw new Error(`PanelParking(${panelId}): pinned start lost: ${r.selector}`);
}
if (r.end.parentNode !== parent) {
throw new Error(`PanelParking(${panelId}): pinned range split: ${r.selector}`);
}
while (r.slot.firstChild) {
parent.insertBefore(r.slot.firstChild, r.end);
}
}
},
};
}
const anchor = document.createComment(`tt:panel-park:${panelId}`);
const parent = parkedRoot.parentNode;
if (!parent) {
throw new Error(`PanelParking(${panelId}): parked root has no parent`);
}
parent.insertBefore(anchor, parkedRoot.nextSibling);
const slotId = `panel:${panelId}`;
/** @param {string} reason */
const hydrate = (reason) => {
if (parkedRoot.isConnected) {
if (pinned) {
pinned.restore();
}
if (afterHydrate) {
afterHydrate(reason);
}
return;
}
if (!anchor.parentNode) {
throw new Error(`PanelParking(${panelId}).hydrate(${reason}): anchor lost`);
}
anchor.parentNode.insertBefore(parkedRoot, anchor);
if (pinned) {
pinned.restore();
}
if (afterHydrate) {
afterHydrate(reason);
}
};
/** @param {string} reason */
const dehydrate = (reason) => {
if (pinned) {
pinned.park();
}
if (!parkedRoot.isConnected) {
return;
}
parkedRoot.remove();
};
manager.register({
id: slotId,
kind: PanelRuntimeKind.DrawerContent,
element: host,
visibilityMode: 'manual',
initialVisible: isDrawerOpen(host),
hydrate,
dehydrate,
});
const onToggleClickCapture = () => {
manager.setVisible(slotId, true);
manager.reconcile();
};
toggle.addEventListener('click', onToggleClickCapture, true);
const classObserver = new MutationObserver(() => {
const open = isDrawerOpen(host);
manager.setVisible(slotId, open);
if (open) {
manager.reconcile();
}
});
classObserver.observe(host, { attributes: true, attributeFilter: ['class'] });
return {
drawerId: typeof drawer.id === 'string' ? drawer.id : '',
slotId,
dispose: () => {
classObserver.disconnect();
toggle.removeEventListener('click', onToggleClickCapture, true);
},
};
}
/**
* Phase 0: Park the heaviest closed drawers under #top-settings-holder by
* detaching their heavy subtrees from the main document tree.
*
* Controlled by `tauritavern-settings.panel_runtime_profile` (default: compat).
*
* @param {{ manager: EmbeddedRuntimeManager }} options
*/
export function installTopSettingsPanelParking({ manager }) {
const leftNavPinnedSelectors = manager.profile === 'compat'
? ['#openai_preset_import_file', '#completion_prompt_manager']
: [];
const registrations = [
registerDrawerParking(manager, {
panelId: 'left-nav-panel',
parkedSelector: '.scrollableInner',
pinnedSelectors: leftNavPinnedSelectors,
afterHydrate: () => syncLeftNavMainApiUi(),
}),
registerDrawerParking(manager, { panelId: 'AdvancedFormatting', parkedSelector: '.flex-container.spaceEvenly' }),
registerDrawerParking(manager, { panelId: 'WorldInfo', parkedSelector: '#wi-holder' }),
registerDrawerParking(manager, { panelId: 'user-settings-block', parkedSelector: '#user-settings-block-content' }),
registerDrawerParking(manager, { panelId: 'Backgrounds', parkedSelector: '#bg_tabs' }),
registerDrawerParking(manager, { panelId: 'PersonaManagement', parkedSelector: '#persona-management-block' }),
];
/** @type {Array<[string, string]>} */
const entries = [];
for (const r of registrations) {
if (r.drawerId) {
entries.push([r.drawerId, r.slotId]);
}
}
const slotByDrawerId = new Map(entries);
document.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof Element)) {
return;
}
const opener = target.closest('.drawer-opener');
if (!(opener instanceof HTMLElement)) {
return;
}
const drawerId = String(opener.getAttribute('data-target') || '').trim();
const slotId = drawerId ? slotByDrawerId.get(drawerId) : null;
if (!slotId) {
return;
}
manager.setVisible(slotId, true);
manager.reconcile();
}, true);
manager.reconcile();
}

View file

@ -21,6 +21,7 @@ import {
toUrl,
} from './http-utils.js';
import { registerRoutes } from './routes/index.js';
import { preinstallPanelRuntime } from './services/panel-runtime/preinstall.js';
let bootstrapped = false;
const HOST_ABI_VERSION = 1;
@ -318,8 +319,7 @@ export function bootstrapTauriMain() {
interceptors.patchFetch();
interceptors.patchJQueryAjax();
downloadBridge.patchWindow();
installSameOriginWindowPatches(interceptors, downloadBridge);
installSameOriginWindowPatches(interceptors, downloadBridge); preinstallPanelRuntime();
const readyPromise = initializeTauriIntegration(
context,
interceptors,
@ -338,6 +338,7 @@ export function bootstrapTauriMain() {
console.warn('TauriTavern: Failed to load LAN sync panel:', error);
}));
void readyPromise.then(() => import('./services/embedded-runtime/install.js').then(({ installEmbeddedRuntime }) => installEmbeddedRuntime()));
void readyPromise.then(() => import('./services/panel-runtime/install.js').then(({ installPanelRuntime }) => installPanelRuntime()));
if (perfEnabled) {
readyPromise

View file

@ -1,5 +1,6 @@
const GLOBAL_KEY = '__TAURITAVERN_PERF__';
const EMBEDDED_RUNTIME_KEY = '__TAURITAVERN_EMBEDDED_RUNTIME__';
const PANEL_RUNTIME_KEY = '__TAURITAVERN_PANEL_RUNTIME__';
const STORAGE_ENABLED_KEY = 'tt:perf';
const STORAGE_POSITION_KEY = 'tt:perf:pos';
@ -599,6 +600,19 @@ function createPerfController(options = {}) {
}
}
function getPanelRuntimeSample() {
try {
const runtime = globalThis[PANEL_RUNTIME_KEY];
if (!runtime || typeof runtime.getPerfSnapshot !== 'function') {
return null;
}
return runtime.getPerfSnapshot();
} catch {
return null;
}
}
function getHeapSample() {
try {
const mem = globalThis.performance?.memory;
@ -660,6 +674,138 @@ function createPerfController(options = {}) {
return items.slice(0, Math.max(0, limit));
}
function cssEscape(raw) {
if (typeof raw !== 'string') {
return '';
}
try {
return globalThis.CSS?.escape ? globalThis.CSS.escape(raw) : raw.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
} catch {
return raw;
}
}
/**
* @param {Element} el
*/
function buildSelectorPath(el) {
const parts = [];
let cur = el;
let hops = 0;
while (cur && hops < 6) {
const tag = String(cur.tagName || '').toLowerCase();
const id = typeof cur.id === 'string' && cur.id ? `#${cssEscape(cur.id)}` : '';
const classes = cur.classList && cur.classList.length
? `.${[...cur.classList].slice(0, 2).map(cssEscape).join('.')}`
: '';
parts.unshift(`${tag}${id}${classes}`);
if (id) {
break;
}
cur = cur.parentElement;
hops += 1;
}
return parts.join(' > ');
}
/**
* @param {object} options
* @param {Element | null} options.root
* @param {number} options.topN
* @param {number} options.minDescendants
*/
function computeTopDomSubtrees({ root, topN, minDescendants }) {
const host = root || document.body;
if (!(host instanceof Element)) {
throw new Error('domTop(): root must be an Element');
}
const n = clampInt(topN, 1, 200, 12);
const min = clampInt(minDescendants, 0, 1_000_000, 0);
/** @type {Array<{ el: Element; depth: number }>} */
const nodes = [];
/** @type {Array<{ el: Element; depth: number }>} */
const stack = [{ el: host, depth: 0 }];
while (stack.length) {
const item = stack.pop();
if (!item) {
break;
}
nodes.push(item);
const children = item.el.children;
for (let i = children.length - 1; i >= 0; i -= 1) {
const child = children[i];
if (child instanceof Element) {
stack.push({ el: child, depth: item.depth + 1 });
}
}
}
/** @type {WeakMap<Element, number>} */
const subtreeSizes = new WeakMap();
for (let i = nodes.length - 1; i >= 0; i -= 1) {
const el = nodes[i]?.el;
if (!el) {
continue;
}
let size = 1;
const children = el.children;
for (let j = 0; j < children.length; j += 1) {
const child = children[j];
if (!(child instanceof Element)) {
continue;
}
size += subtreeSizes.get(child) || 1;
}
subtreeSizes.set(el, size);
}
/** @type {Array<{ selector: string; tag: string; depth: number; directChildren: number; descendants: number; text: string }>} */
const top = [];
for (const { el, depth } of nodes) {
const size = subtreeSizes.get(el) || 1;
const descendants = size - 1;
if (descendants < min) {
continue;
}
if (top.length < n) {
top.push({
selector: buildSelectorPath(el),
tag: String(el.tagName || '').toLowerCase(),
depth,
directChildren: el.children.length,
descendants,
text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80),
});
top.sort((a, b) => b.descendants - a.descendants);
continue;
}
const tail = top[top.length - 1];
if (tail && descendants <= tail.descendants) {
continue;
}
top.pop();
top.push({
selector: buildSelectorPath(el),
tag: String(el.tagName || '').toLowerCase(),
depth,
directChildren: el.children.length,
descendants,
text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80),
});
top.sort((a, b) => b.descendants - a.descendants);
}
return top;
}
function clampHudPosition(x, y) {
const vw = Number(globalThis.innerWidth) || 0;
const vh = Number(globalThis.innerHeight) || 0;
@ -879,6 +1025,7 @@ function createPerfController(options = {}) {
const frameStats = computeFrameStats();
const dom = getDomSample();
const embeddedRuntime = getEmbeddedRuntimeSample();
const panelRuntime = getPanelRuntimeSample();
const heap = getHeapSample();
const lagStats = computeEventLoopLagStats();
@ -928,6 +1075,9 @@ function createPerfController(options = {}) {
if (embeddedRuntime) {
lines.push(`Runtime: active ${embeddedRuntime.active}/${embeddedRuntime.registered} iframes ${embeddedRuntime.activeIframes} parked ${embeddedRuntime.parked}`);
}
if (panelRuntime) {
lines.push(`Panels: active ${panelRuntime.active}/${panelRuntime.registered} parked ${panelRuntime.parked}`);
}
if (heap) {
const limitSuffix = heap.limit ? ` (limit ${formatMB(heap.limit)})` : '';
lines.push(`Heap: ${formatMB(heap.used)} / ${formatMB(heap.total)}${limitSuffix}`);
@ -970,6 +1120,7 @@ function createPerfController(options = {}) {
lines.push('');
lines.push(`timeOrigin: ${Math.round(state.timeOrigin)} now(): ${Math.round(safeNow())}`);
lines.push('Commands: __TAURITAVERN_PERF__.exportJson() / downloadReport() / copyReport() / saveReport()');
lines.push('DOM report: __TAURITAVERN_PERF__.domTop()');
lines.push('Auto-enable: localStorage tt:perf=1');
}
@ -1164,6 +1315,7 @@ function createPerfController(options = {}) {
const frameStats = computeFrameStats();
const dom = getDomSample();
const embeddedRuntime = getEmbeddedRuntimeSample();
const panelRuntime = getPanelRuntimeSample();
const heap = getHeapSample();
const eventLoopLag = computeEventLoopLagStats();
@ -1218,6 +1370,7 @@ function createPerfController(options = {}) {
},
dom,
embeddedRuntime,
panelRuntime,
heap,
init: {
totalMs: readMeasureDuration('tt:init:total'),
@ -1418,6 +1571,18 @@ function createPerfController(options = {}) {
}
}
function domTop(options = {}) {
const rootSelector = typeof options.rootSelector === 'string' ? options.rootSelector : '';
const root = rootSelector ? document.querySelector(rootSelector) : document.body;
const top = computeTopDomSubtrees({
root: root instanceof Element ? root : document.body,
topN: options.topN ?? 12,
minDescendants: options.minDescendants ?? 0,
});
console.table(top);
return top;
}
return {
config,
get enabled() {
@ -1435,6 +1600,7 @@ function createPerfController(options = {}) {
copyReport,
downloadReport,
saveReport,
domTop,
attachContext,
setPosition: (x, y) => setHudPosition(x, y, { persist: true }),
resetPosition: resetHudPosition,

View file

@ -31,6 +31,7 @@ export function createEmbeddedRuntimeManager({ profile, now = () => Date.now(),
const slots = new Map();
let reconcilePending = false;
let reconcileSeq = 0;
const counters = {
hydrate: 0,
@ -100,6 +101,18 @@ export function createEmbeddedRuntimeManager({ profile, now = () => Date.now(),
threshold: normalizedProfile.threshold,
});
/**
* Cancels a pending requestAnimationFrame reconcile, if any.
*/
function cancelPendingReconcile() {
if (!reconcilePending) {
return;
}
reconcilePending = false;
reconcileSeq += 1;
}
/**
* @param {string} reason
*/
@ -108,7 +121,12 @@ export function createEmbeddedRuntimeManager({ profile, now = () => Date.now(),
return;
}
reconcilePending = true;
const seq = (reconcileSeq += 1);
requestAnimationFrame(() => {
if (seq !== reconcileSeq) {
return;
}
reconcilePending = false;
reconcile(reason);
});
@ -415,7 +433,10 @@ export function createEmbeddedRuntimeManager({ profile, now = () => Date.now(),
touch,
invalidate,
setVisible,
reconcile: () => reconcile('manual'),
reconcile: () => {
cancelPendingReconcile();
reconcile('manual');
},
getPerfSnapshot,
get profile() {
return normalizedProfile.name;

View file

@ -0,0 +1,38 @@
// @ts-check
import { eventSource, event_types } from '../../../../scripts/events.js';
import { getTauriTavernSettings } from '../../../../tauri-bridge.js';
import { activateApiConnectionsSubtreeGates } from '../../adapters/panel-runtime/api-connections-subtree-gates.js';
import { installExtensionsSubtreeGates } from '../../adapters/panel-runtime/extensions-subtree-gates.js';
import { installTopSettingsPanelParking } from '../../adapters/panel-runtime/top-settings-panel-parking.js';
import { createPanelRuntimeService } from './panel-runtime-service.js';
import { validatePanelRuntimeInvariants } from './validate.js';
// Keep in sync with:
// - src/tauri/main/services/panel-runtime/preinstall.js
// - src/scripts/tauri/sync/sync-panel.js
const PANEL_RUNTIME_PROFILE_STORAGE_KEY = 'tt:panelRuntimeProfile';
export function installPanelRuntime() {
const ready = getTauriTavernSettings().then((settings) => {
const profileName = String(settings.panel_runtime_profile || '').trim();
localStorage.setItem(PANEL_RUNTIME_PROFILE_STORAGE_KEY, profileName);
if (profileName === 'off') {
return null;
}
const service = createPanelRuntimeService({ profileName });
eventSource.on(event_types.APP_READY, () => {
activateApiConnectionsSubtreeGates({ manager: service.manager });
installTopSettingsPanelParking({ manager: service.manager });
installExtensionsSubtreeGates({ manager: service.manager });
validatePanelRuntimeInvariants({ profileName: service.manager.profile });
});
return service;
});
return { ready };
}

View file

@ -0,0 +1,6 @@
// @ts-check
export const PanelRuntimeKind = Object.freeze({
DrawerContent: 'drawer-content',
SubtreeGate: 'subtree-gate',
});

View file

@ -0,0 +1,60 @@
// @ts-check
import { PanelRuntimeKind } from './panel-runtime-kinds.js';
/**
* @typedef {import('../embedded-runtime/types.js').EmbeddedRuntimeProfile} EmbeddedRuntimeProfile
*/
const COMPAT_PROFILE = Object.freeze({
name: 'compat',
maxActiveWeight: 0,
maxActiveIframes: 0,
maxActiveSlots: 0,
maxSoftParkedIframes: 0,
softParkTtlMs: 0,
parkWhenHiddenKinds: Object.freeze([
PanelRuntimeKind.DrawerContent,
PanelRuntimeKind.SubtreeGate,
]),
rootMargin: '0px',
threshold: 0,
});
const AGGRESSIVE_PROFILE = Object.freeze({
name: 'aggressive',
maxActiveWeight: 0,
maxActiveIframes: 0,
maxActiveSlots: 0,
maxSoftParkedIframes: 0,
softParkTtlMs: 0,
parkWhenHiddenKinds: Object.freeze([
PanelRuntimeKind.DrawerContent,
PanelRuntimeKind.SubtreeGate,
]),
rootMargin: '0px',
threshold: 0,
});
/** @type {Readonly<Record<string, EmbeddedRuntimeProfile>>} */
export const PANEL_RUNTIME_PROFILES = Object.freeze({
compat: COMPAT_PROFILE,
aggressive: AGGRESSIVE_PROFILE,
});
/**
* @param {string} profileName
*/
export function resolvePanelRuntimeProfile(profileName) {
const name = String(profileName || '').trim();
if (!name) {
throw new Error('PanelRuntime: profile name is required');
}
const profile = PANEL_RUNTIME_PROFILES[name];
if (!profile) {
throw new Error(`PanelRuntime: unknown profile '${name}'`);
}
return profile;
}

View file

@ -0,0 +1,25 @@
// @ts-check
import { createEmbeddedRuntimeManager } from '../embedded-runtime/embedded-runtime-manager.js';
import { resolvePanelRuntimeProfile } from './panel-runtime-profiles.js';
const GLOBAL_KEY = '__TAURITAVERN_PANEL_RUNTIME__';
/**
* @param {{ profileName: string }} options
*/
export function createPanelRuntimeService({ profileName }) {
const profile = resolvePanelRuntimeProfile(profileName);
const manager = createEmbeddedRuntimeManager({
profile,
now: () => globalThis.performance?.now?.() ?? Date.now(),
root: null,
});
/** @type {any} */ (globalThis)[GLOBAL_KEY] = manager;
return {
profile,
manager,
};
}

View file

@ -0,0 +1,22 @@
// @ts-check
import { preinstallApiConnectionsSubtreeGates } from '../../adapters/panel-runtime/api-connections-subtree-gates.js';
// Keep in sync with:
// - src/tauri/main/services/panel-runtime/install.js
// - src/scripts/tauri/sync/sync-panel.js
//
// Rationale: `panel_runtime_profile` is persisted in Tauri settings (async to load),
// but some panel-runtime hooks must be installed during bootstrap to preserve
// handler ordering for jQuery `.trigger('change')`. We mirror the last known
// profile into localStorage so bootstrap can synchronously honor `off`.
const PANEL_RUNTIME_PROFILE_STORAGE_KEY = 'tt:panelRuntimeProfile';
export function preinstallPanelRuntime() {
const profile = String(localStorage.getItem(PANEL_RUNTIME_PROFILE_STORAGE_KEY) || '').trim();
if (profile === 'off') {
return;
}
preinstallApiConnectionsSubtreeGates();
}

View file

@ -0,0 +1,65 @@
// @ts-check
/**
* @param {string} id
* @returns {HTMLElement}
*/
function mustGetConnectedElementById(id) {
const el = document.getElementById(id);
if (!(el instanceof HTMLElement)) {
throw new Error(`PanelRuntime validate: #${id} not found`);
}
if (!el.isConnected) {
throw new Error(`PanelRuntime validate: #${id} is not connected`);
}
return el;
}
/**
* @param {{ profileName: string }} options
*/
export function validatePanelRuntimeInvariants({ profileName }) {
const profile = String(profileName || '').trim();
if (!profile) {
throw new Error('PanelRuntime validate: profileName is required');
}
if (profile !== 'compat' && profile !== 'aggressive') {
throw new Error(`PanelRuntime validate: unknown profile '${profile}'`);
}
// Extensions mount points.
mustGetConnectedElementById('rm_extensions_block');
mustGetConnectedElementById('extensions_settings');
mustGetConnectedElementById('regex_container');
mustGetConnectedElementById('qr_container');
// Anchor-zone compatibility: third-party scripts expect these anchors to be selectable even when parked.
if (profile === 'compat') {
mustGetConnectedElementById('openai_preset_import_file');
mustGetConnectedElementById('completion_prompt_manager');
}
// API connections control surface (only meaningful when left-nav is hydrated).
const leftNavScrollable = document.querySelector('#left-nav-panel .scrollableInner');
const isLeftNavHydrated = leftNavScrollable instanceof HTMLElement && leftNavScrollable.isConnected;
if (!isLeftNavHydrated) {
return;
}
const mainApiEl = mustGetConnectedElementById('main_api');
if (!(mainApiEl instanceof HTMLSelectElement)) {
throw new Error('PanelRuntime validate: #main_api is not a <select>');
}
mustGetConnectedElementById('kobold_horde');
mustGetConnectedElementById('kobold_api');
mustGetConnectedElementById('novel_api');
mustGetConnectedElementById('textgenerationwebui_api');
mustGetConnectedElementById('openai_api');
if (String(mainApiEl.value || '').trim() === 'openai') {
mustGetConnectedElementById('chat_completion_source');
}
if (String(mainApiEl.value || '').trim() === 'textgenerationwebui') {
mustGetConnectedElementById('textgen_type');
}
}