fix(panel-runtime): normalize custom source variants at gate activation (#353)

* fix(panel-runtime): normalize custom source variants at gate activation

activateApiConnectionsSubtreeGates applied the openai source gate with
the raw select value. Custom source variants (custom_openai_responses,
custom_claude_messages, custom_gemini_generate_content,
custom_gemini_interactions) do not appear in any data-source list, so
every gated host under #openai_api was parked at startup while
SillyTavern's own toggle still showed the matching blocks. The Custom
Endpoint form rendered as an empty shell: URL input, model id and model
list were missing until the user manually re-selected a source.

Normalize the value through normalizeChatCompletionSourceForGate like
the change-event handlers already do.

* test(panel-runtime): cover custom source variants at gate activation

Activating the API connections subtree gates with a saved custom source
variant must keep the data-source="custom" hosts (Custom Endpoint form,
model id, model list) connected while the plain OpenAI form stays parked.
Fails on unpatched activation that applies the raw variant value.

* refactor(panel-runtime): unify API connection gate updates

Route activation and selection changes through one nested gate function. Keep the custom-source regression in the existing parking tests and remove the duplicate test file.

---------

Co-authored-by: Darkatse <39111369+Darkatse@users.noreply.github.com>
This commit is contained in:
RAMU324E 2026-10-14 07:31:37 +08:00 • committed by GitHub
parent 5c0b48c34b
commit 0a84236364
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 57 deletions

View file

@ -169,19 +169,25 @@ function currentSelectValue(id) {
}
/**
* @param {string} source
* Applies the nested gate (OpenAI source / Textgen type) of the given main API
* from the current selection.
*
* @param {string} mainApi
*/
function normalizeChatCompletionSourceForGate(source) {
const value = String(source || '').trim();
if (value.startsWith('custom_')) {
return 'custom';
function applyNestedGate(mainApi) {
if (mainApi === 'openai' && openaiSourceGate) {
// Custom API format variants (`custom_*`) share the `custom` data-source hosts.
const source = currentSelectValue('chat_completion_source');
openaiSourceGate.apply(source.startsWith('custom_') ? 'custom' : source || 'openai');
}
if (mainApi === 'textgenerationwebui' && textgenTypeGate) {
textgenTypeGate.apply(currentSelectValue('textgen_type'));
}
return value;
}
/**
* Hydrates the selected provider subtree and parks all others, then applies any
* nested gates (OpenAI source / Textgen type) for the active provider.
* Hydrates the selected provider subtree and parks all others, then applies the
* nested gate of the active provider.
*
* Must run synchronously inside the `change` event so downstream handlers can
* safely query their expected DOM.
@ -198,46 +204,7 @@ function handleMainApiChange(mainApi) {
manager.setVisible(slotId, api === next);
}
manager.reconcile();
if (next === 'openai' && openaiSourceGate) {
const selection = currentSelectValue('chat_completion_source') || 'openai';
openaiSourceGate.apply(normalizeChatCompletionSourceForGate(selection) || '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(normalizeChatCompletionSourceForGate(source) || 'openai');
}
/**
* @param {string} type
*/
function handleTextgenTypeChange(type) {
if (!textgenTypeGate) {
return;
}
const mainApi = currentSelectValue('main_api');
if (mainApi !== 'textgenerationwebui') {
return;
}
textgenTypeGate.apply(type || '');
applyNestedGate(next);
}
/**
@ -265,7 +232,7 @@ export function preinstallApiConnectionsSubtreeGates() {
throw new Error('PanelRuntime: #chat_completion_source is not a <select>');
}
$(chatCompletionSource).on('change.ttPanelRuntimeGate', () => {
handleChatCompletionSourceChange(String(chatCompletionSource.value || ''));
applyNestedGate(currentSelectValue('main_api'));
});
const textgenType = mustGetElementById('textgen_type');
@ -273,7 +240,7 @@ export function preinstallApiConnectionsSubtreeGates() {
throw new Error('PanelRuntime: #textgen_type is not a <select>');
}
$(textgenType).on('change.ttPanelRuntimeGate', () => {
handleTextgenTypeChange(String(textgenType.value || ''));
applyNestedGate(currentSelectValue('main_api'));
});
}
@ -315,11 +282,5 @@ export function activateApiConnectionsSubtreeGates({ manager: nextManager }) {
});
manager.reconcile();
if (selectedMainApi === 'openai') {
openaiSourceGate.apply(currentSelectValue('chat_completion_source') || 'openai');
}
if (selectedMainApi === 'textgenerationwebui') {
textgenTypeGate.apply(currentSelectValue('textgen_type') || '');
}
applyNestedGate(selectedMainApi);
}

View file

@ -63,3 +63,24 @@ test('aggressive parking keeps the OpenAI bounds but drops the third-party ancho
dom.cleanup();
}
});
test('API connections gates keep the custom form for a saved custom source variant', async () => {
const dom = installFakeDom();
try {
renderIndexBody(dom.document);
dom.document.getElementById('main_api').value = 'openai';
dom.document.getElementById('chat_completion_source').value = 'custom_gemini_generate_content';
const { createEmbeddedRuntimeManager } = await import('../src/tauri/main/services/embedded-runtime/embedded-runtime-manager.js');
const { resolvePanelRuntimeProfile } = await import('../src/tauri/main/services/panel-runtime/panel-runtime-profiles.js');
const { activateApiConnectionsSubtreeGates } = await import('../src/tauri/main/adapters/panel-runtime/api-connections-subtree-gates.js');
activateApiConnectionsSubtreeGates({ manager: createEmbeddedRuntimeManager({ profile: resolvePanelRuntimeProfile('compat') }) });
assert.ok(dom.document.getElementById('custom_api_url_text'));
assert.ok(dom.document.getElementById('custom_model_id'));
assert.ok(dom.document.getElementById('model_custom_select'));
assert.equal(dom.document.getElementById('openai_form').children.length, 0);
} finally {
dom.cleanup();
}
});