mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
Merge pull request #2210 into stage-351
Fix MCP tools list overflow with pagination/search (Jordan-SkyLF)
This commit is contained in:
@@ -38,6 +38,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- MCP Tools in Settings → System now uses a bounded scroll region with 5-item default pages, a per-page selector up to 40 tools, and a visible result summary, so large MCP tool inventories no longer make the settings panel balloon indefinitely.
|
||||
|
||||
- **PR #2201** by @MrFant — Multi-turn conversations with thinking-mode providers (MiMo/Xiaomi, DeepSeek, Kimi/Moonshot) no longer 400 with `Param Incorrect: reasoning_content must be passed back`. WebUI's `_sanitize_messages_for_api()` strips fields not in `_API_SAFE_MSG_KEYS` before sending conversation history to the LLM; `reasoning_content` was missing from the whitelist, so when history was replayed on the second turn, the assistant message with `tool_calls` arrived without `reasoning_content` and providers enforcing thinking-mode echo-back rejected it. One-line fix: adds `'reasoning_content'` to `_API_SAFE_MSG_KEYS`. CLI was unaffected because `run_agent.py` has its own `_copy_reasoning_content_for_api()` that doesn't go through this filter.
|
||||
|
||||
- **PR #2198** by @Michaelyklam — Fork-from-here keep-count was off-by-one (or larger) for truncated sessions where the visible-message index didn't match the absolute transcript index. JS now sends `_oldestIdx + msgIdx` (the absolute message index in the full transcript) as `keep_count` instead of the visible-window-relative index — captured *before* `_ensureAllMessagesLoaded()` resets `_oldestIdx`, so the index remains stable. Backend `source_messages[:keep_count]` then forks from the correct point even when the user has only loaded a tail window. When the full transcript is loaded (`_oldestIdx==0`), behavior is unchanged. 186-line regression suite in `tests/test_issue2184_fork_from_here_absolute_index.py` explicitly pins `keep_count: absoluteKeepCount` (and forbids the old `keep_count: msgIdx` form).
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
+140
@@ -92,6 +92,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Failed to load MCP tools.',
|
||||
mcp_tools_schema_empty: 'No schema parameters.',
|
||||
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
// PDF preview (#480)
|
||||
pdf_loading: 'Loading PDF {0}…',
|
||||
pdf_too_large: 'PDF too large for inline preview',
|
||||
@@ -1217,6 +1231,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Caricamento strumenti MCP fallito.',
|
||||
mcp_tools_schema_empty: 'Nessun parametro schema.',
|
||||
mcp_tools_runtime_note: "L'inventario strumenti usa solo dati runtime MCP già noti; la WebUI non avvia né interroga i server.",
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
// PDF preview (#480)
|
||||
pdf_loading: 'Caricamento PDF {0}…',
|
||||
pdf_too_large: "PDF troppo grande per l'anteprima inline",
|
||||
@@ -2334,6 +2362,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'MCP ツールの読み込みに失敗しました。',
|
||||
mcp_tools_schema_empty: 'スキーマパラメータはありません。',
|
||||
mcp_tools_runtime_note: 'ツール一覧は既知の MCP ランタイム情報のみを使用します。WebUI はサーバーの起動や探索を行いません。',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
// PDF preview (#480)
|
||||
pdf_loading: 'PDF {0} を読み込み中…',
|
||||
pdf_too_large: 'PDF が大きすぎてインラインプレビューできません',
|
||||
@@ -3453,6 +3495,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Failed to load MCP tools.',
|
||||
mcp_tools_schema_empty: 'No schema parameters.',
|
||||
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
thinking: 'Думаю',
|
||||
expand_all: 'Развернуть всё',
|
||||
collapse_all: 'Свернуть всё',
|
||||
@@ -4504,6 +4560,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Failed to load MCP tools.',
|
||||
mcp_tools_schema_empty: 'No schema parameters.',
|
||||
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
thinking: 'Pensando',
|
||||
expand_all: 'Expandir todo',
|
||||
collapse_all: 'Contraer todo',
|
||||
@@ -5558,6 +5628,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Failed to load MCP tools.',
|
||||
mcp_tools_schema_empty: 'No schema parameters.',
|
||||
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
thinking: 'Nachdenken',
|
||||
expand_all: 'Alle ausklappen',
|
||||
collapse_all: 'Alle einklappen',
|
||||
@@ -6616,6 +6700,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: '加载 MCP 工具失败。',
|
||||
mcp_tools_schema_empty: '无参数。',
|
||||
mcp_tools_runtime_note: '工具清单仅使用已知的活跃 MCP 运行时数据;WebUI 不会启动或探测服务器。',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
thinking: '思考过程',
|
||||
expand_all: '全部展开',
|
||||
collapse_all: '全部折叠',
|
||||
@@ -7662,6 +7760,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Failed to load MCP tools.',
|
||||
mcp_tools_schema_empty: 'No schema parameters.',
|
||||
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
thinking: '\u601d\u8003\u904e\u7a0b',
|
||||
expand_all: '\u5168\u90e8\u5c55\u958b',
|
||||
collapse_all: '\u5168\u90e8\u6298\u758a',
|
||||
@@ -9764,6 +9876,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Failed to load MCP tools.',
|
||||
mcp_tools_schema_empty: 'No schema parameters.',
|
||||
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
thinking: '생각 중',
|
||||
expand_all: '모두 펼치기',
|
||||
collapse_all: '모두 접기',
|
||||
@@ -10878,6 +11004,20 @@ const LOCALES = {
|
||||
mcp_tools_load_failed: 'Échec du chargement des outils MCP.',
|
||||
mcp_tools_schema_empty: 'Aucun paramètre de schéma.',
|
||||
mcp_tools_runtime_note: 'L\'inventaire des outils utilise uniquement les données d\'exécution MCP actives déjà connues ; le WebUI ne démarre pas et ne sonde pas les serveurs.',
|
||||
mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
|
||||
mcp_tools_summary_none: 'No MCP tools to show.',
|
||||
mcp_tools_summary_matching: (query) => ` matching “${query}”`,
|
||||
mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
|
||||
mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
|
||||
mcp_tools_page_size_prefix: 'Show',
|
||||
mcp_tools_page_size_suffix: 'per page',
|
||||
mcp_tools_per_page_aria: 'MCP tools per page',
|
||||
mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
|
||||
mcp_tools_pagination_label: 'MCP tools pagination',
|
||||
mcp_tools_previous_page: '‹ Prev',
|
||||
mcp_tools_previous_page_aria: 'Previous MCP tools page',
|
||||
mcp_tools_next_page: 'Next ›',
|
||||
mcp_tools_next_page_aria: 'Next MCP tools page',
|
||||
pdf_loading: 'Chargement du PDF {0}…',
|
||||
pdf_too_large: 'PDF trop volumineux pour un aperçu en ligne',
|
||||
pdf_no_pages: 'Le PDF n\'a pas de pages',
|
||||
|
||||
+3
-1
@@ -1138,7 +1138,9 @@
|
||||
<label data-i18n="mcp_tools_title">MCP Tools</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-bottom:8px" data-i18n="mcp_tools_desc">Search known tools across active MCP servers.</div>
|
||||
<input type="search" id="mcpToolSearch" class="mcp-tool-search" data-i18n-placeholder="mcp_tools_search_placeholder" placeholder="Search tools by name, server, or description…" oninput="filterMcpTools()" autocomplete="off">
|
||||
<div id="mcpToolList"></div>
|
||||
<div class="mcp-tool-toolbar" id="mcpToolToolbar" aria-live="polite"></div>
|
||||
<div id="mcpToolList" class="mcp-tool-list"></div>
|
||||
<div id="mcpToolPager" class="mcp-tool-pager" aria-label="MCP tools pagination" data-i18n-aria-label="mcp_tools_pagination_label"></div>
|
||||
<div class="mcp-restart-hint" data-i18n="mcp_tools_runtime_note">Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.</div>
|
||||
</div>
|
||||
<button class="sm-btn" onclick="saveSettings()" style="margin-top:12px;width:100%;padding:8px;font-weight:600" data-i18n="settings_save_btn">Save Settings</button>
|
||||
|
||||
+79
-9
@@ -6285,6 +6285,10 @@ function loadMcpServers(){
|
||||
}).catch(()=>{list.innerHTML=`<div class="mcp-error-state" style="color:#ef4444;font-size:12px;padding:6px 0">${esc(t('mcp_load_failed'))}</div>`});
|
||||
}
|
||||
let _mcpToolsCache=[];
|
||||
let _mcpToolsMeta={};
|
||||
let _mcpToolsPage=1;
|
||||
let _mcpToolsPageSize=5;
|
||||
const MCP_TOOLS_PAGE_SIZE_OPTIONS=[5,10,20,40];
|
||||
function _filterMcpToolsForSearch(tools, query){
|
||||
const q=(query||'').trim().toLowerCase();
|
||||
if(!q) return Array.isArray(tools)?tools:[];
|
||||
@@ -6301,16 +6305,56 @@ function _mcpToolSchemaText(schemaSummary){
|
||||
return `${p.name}${req}: ${p.type||'unknown'}${desc}`;
|
||||
}).join('\n');
|
||||
}
|
||||
function _renderMcpTools(tools, query){
|
||||
const list=$('mcpToolList');
|
||||
if(!list) return;
|
||||
const filtered=_filterMcpToolsForSearch(tools, query);
|
||||
if(!filtered.length){
|
||||
const key=query?'mcp_tools_no_matches':'mcp_tools_no_tools';
|
||||
list.innerHTML=`<div class="mcp-tool-empty-state" style="color:var(--muted);font-size:12px;padding:6px 0">${esc(t(key))}</div>`;
|
||||
function _mcpToolsSummary(total, filtered, page, pages, query){
|
||||
const trimmedQuery=(query||'').trim();
|
||||
if(!filtered){
|
||||
if(trimmedQuery) return t('mcp_tools_summary_no_matches',trimmedQuery,total);
|
||||
return total?t('mcp_tools_summary_none'):'';
|
||||
}
|
||||
const pageSize=_mcpToolsPageSize||5;
|
||||
const start=(page-1)*pageSize+1;
|
||||
const end=Math.min(filtered,page*pageSize);
|
||||
const searchNote=trimmedQuery?t('mcp_tools_summary_matching',trimmedQuery):'';
|
||||
const totalNote=filtered===total?'':t('mcp_tools_summary_total_note',total);
|
||||
return t('mcp_tools_summary_showing',start,end,filtered,searchNote,totalNote,page,pages);
|
||||
}
|
||||
function _mcpToolPageSizeControl(){
|
||||
const options=MCP_TOOLS_PAGE_SIZE_OPTIONS.map(size=>`<option value="${size}" ${size===_mcpToolsPageSize?'selected':''}>${size}</option>`).join('');
|
||||
return `<label class="mcp-tool-page-size">${esc(t('mcp_tools_page_size_prefix'))} <select aria-label="${esc(t('mcp_tools_per_page_aria'))}" onchange="setMcpToolsPageSize(this.value)">${options}</select> ${esc(t('mcp_tools_page_size_suffix'))}</label>`;
|
||||
}
|
||||
function _mcpToolsEmptyMessage(query){
|
||||
const base=esc(t(query?'mcp_tools_no_matches':'mcp_tools_no_tools'));
|
||||
const unavailable=Array.isArray(_mcpToolsMeta.unavailable_servers)?_mcpToolsMeta.unavailable_servers:[];
|
||||
if(query||!unavailable.length) return base;
|
||||
return `${base}<br><span class="mcp-tool-empty-detail">${esc(t('mcp_tools_inactive_configured_servers',unavailable.join(', ')))}</span>`;
|
||||
}
|
||||
function _renderMcpToolPager(filteredCount, page, pages){
|
||||
const pager=$('mcpToolPager');
|
||||
if(!pager) return;
|
||||
if(pages<=1){
|
||||
pager.innerHTML='';
|
||||
return;
|
||||
}
|
||||
list.innerHTML=filtered.map(tool=>{
|
||||
pager.innerHTML=`<button type="button" class="mcp-tool-page-btn" onclick="setMcpToolsPage(${page-1})" ${page<=1?'disabled':''} aria-label="${esc(t('mcp_tools_previous_page_aria'))}">${esc(t('mcp_tools_previous_page'))}</button>
|
||||
<span class="mcp-tool-page-label">${page} / ${pages}</span>
|
||||
<button type="button" class="mcp-tool-page-btn" onclick="setMcpToolsPage(${page+1})" ${page>=pages?'disabled':''} aria-label="${esc(t('mcp_tools_next_page_aria'))}">${esc(t('mcp_tools_next_page'))}</button>`;
|
||||
}
|
||||
function _renderMcpTools(tools, query){
|
||||
const list=$('mcpToolList');
|
||||
const toolbar=$('mcpToolToolbar');
|
||||
if(!list) return;
|
||||
const filtered=_filterMcpToolsForSearch(tools, query);
|
||||
const total=Array.isArray(tools)?tools.length:0;
|
||||
const pages=Math.max(1,Math.ceil(filtered.length/_mcpToolsPageSize));
|
||||
_mcpToolsPage=Math.min(Math.max(1,_mcpToolsPage||1),pages);
|
||||
if(toolbar) toolbar.innerHTML=`<span class="mcp-tool-summary">${esc(_mcpToolsSummary(total,filtered.length,_mcpToolsPage,pages,query))}</span>${_mcpToolPageSizeControl()}`;
|
||||
_renderMcpToolPager(filtered.length,_mcpToolsPage,pages);
|
||||
if(!filtered.length){
|
||||
list.innerHTML=`<div class="mcp-tool-empty-state" style="color:var(--muted);font-size:12px;padding:6px 0">${_mcpToolsEmptyMessage(query)}</div>`;
|
||||
return;
|
||||
}
|
||||
const visible=filtered.slice((_mcpToolsPage-1)*_mcpToolsPageSize,_mcpToolsPage*_mcpToolsPageSize);
|
||||
list.innerHTML=visible.map(tool=>{
|
||||
const status=tool.status||'unknown';
|
||||
const statusBadge=`<span class="mcp-status-badge mcp-status-${esc(status)}">${esc(_mcpStatusLabel(status))}</span>`;
|
||||
const schemaText=_mcpToolSchemaText(tool.schema_summary);
|
||||
@@ -6325,16 +6369,42 @@ function _renderMcpTools(tools, query){
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
function filterMcpTools(){
|
||||
function setMcpToolsPage(page){
|
||||
_mcpToolsPage=page;
|
||||
const input=$('mcpToolSearch');
|
||||
_renderMcpTools(_mcpToolsCache,input?input.value:'');
|
||||
const list=$('mcpToolList');
|
||||
if(list) list.scrollTop=0;
|
||||
}
|
||||
function setMcpToolsPageSize(size){
|
||||
const next=Number(size);
|
||||
if(!MCP_TOOLS_PAGE_SIZE_OPTIONS.includes(next)) return;
|
||||
_mcpToolsPageSize=next;
|
||||
_mcpToolsPage=1;
|
||||
const input=$('mcpToolSearch');
|
||||
_renderMcpTools(_mcpToolsCache,input?input.value:'');
|
||||
const list=$('mcpToolList');
|
||||
if(list) list.scrollTop=0;
|
||||
}
|
||||
function filterMcpTools(){
|
||||
_mcpToolsPage=1;
|
||||
const input=$('mcpToolSearch');
|
||||
_renderMcpTools(_mcpToolsCache,input?input.value:'');
|
||||
const list=$('mcpToolList');
|
||||
if(list) list.scrollTop=0;
|
||||
}
|
||||
function loadMcpTools(){
|
||||
const list=$('mcpToolList');
|
||||
const toolbar=$('mcpToolToolbar');
|
||||
const pager=$('mcpToolPager');
|
||||
if(!list) return;
|
||||
if(toolbar) toolbar.textContent='';
|
||||
if(pager) pager.innerHTML='';
|
||||
list.innerHTML=`<div style="color:var(--muted);font-size:12px;padding:6px 0">${esc(t('loading'))}</div>`;
|
||||
api('/api/mcp/tools').then(r=>{
|
||||
_mcpToolsCache=(r&&Array.isArray(r.tools))?r.tools:[];
|
||||
_mcpToolsMeta=r||{};
|
||||
_mcpToolsPage=1;
|
||||
filterMcpTools();
|
||||
}).catch(()=>{list.innerHTML=`<div class="mcp-tool-error-state" style="color:#ef4444;font-size:12px;padding:6px 0">${esc(t('mcp_tools_load_failed'))}</div>`});
|
||||
}
|
||||
|
||||
@@ -2493,10 +2493,21 @@ main.main.showing-logs > #mainLogs{display:flex;}
|
||||
.mcp-readonly-note,.mcp-restart-hint{margin-top:8px;color:var(--muted);font-size:11px;line-height:1.45;background:var(--code-bg);border:1px solid var(--border2);border-radius:6px;padding:8px 10px;}
|
||||
.mcp-tool-search{width:100%;margin:0 0 8px 0;padding:8px 10px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:8px;font-size:12px;outline:none;}
|
||||
.mcp-tool-search:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-bg-soft);}
|
||||
.mcp-tool-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 8px;color:var(--muted);font-size:11px;line-height:1.35;flex-wrap:wrap;}
|
||||
.mcp-tool-summary{min-width:0;}
|
||||
.mcp-tool-page-size{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--muted);font-size:11px;}
|
||||
.mcp-tool-page-size select{appearance:none;border:1px solid var(--border2);background:var(--code-bg);color:var(--text);border-radius:7px;padding:4px 22px 4px 8px;font-size:11px;font-weight:600;line-height:1.2;cursor:pointer;}
|
||||
.mcp-tool-empty-detail{display:inline-block;margin-top:4px;color:var(--muted);font-size:11px;line-height:1.35;}
|
||||
.mcp-tool-list{max-height:min(52vh,560px);overflow:auto;padding-right:3px;scrollbar-gutter:stable;}
|
||||
.mcp-tool-row{display:flex;flex-direction:column;gap:5px;padding:9px 10px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;font-size:12px;background:var(--surface);}
|
||||
.mcp-tool-name{font-weight:600;color:var(--text);overflow-wrap:anywhere;}
|
||||
.mcp-tool-server{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);background:var(--code-bg);border:1px solid var(--border2);border-radius:999px;padding:2px 6px;}
|
||||
.mcp-tool-schema{margin:2px 0 0 0;padding:7px 8px;white-space:pre-wrap;max-height:140px;overflow:auto;background:var(--code-bg);border:1px solid var(--border2);border-radius:6px;color:var(--muted);font-size:11px;line-height:1.45;}
|
||||
.mcp-tool-pager{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:8px;}
|
||||
.mcp-tool-page-label{color:var(--muted);font-size:11px;min-width:44px;text-align:center;}
|
||||
.mcp-tool-page-btn{border:1px solid var(--border2);background:var(--code-bg);color:var(--text);border-radius:7px;padding:5px 9px;font-size:11px;font-weight:600;cursor:pointer;}
|
||||
.mcp-tool-page-btn:hover:not(:disabled){border-color:var(--accent-bg-strong);color:var(--accent-text);}
|
||||
.mcp-tool-page-btn:disabled{opacity:.45;cursor:not-allowed;}
|
||||
|
||||
/* Picker grids (theme / skin / font-size): make the card chrome use
|
||||
tokens so all skins flip correctly. */
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Regression coverage for large MCP tool inventories in Settings → System."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
CHANGELOG = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_mcp_tool_list_has_summary_list_and_pager_mounts():
|
||||
assert 'id="mcpToolToolbar"' in INDEX_HTML
|
||||
assert 'aria-live="polite"' in INDEX_HTML
|
||||
assert 'id="mcpToolList" class="mcp-tool-list"' in INDEX_HTML
|
||||
assert 'id="mcpToolPager"' in INDEX_HTML
|
||||
assert 'aria-label="MCP tools pagination"' in INDEX_HTML
|
||||
assert 'data-i18n-aria-label="mcp_tools_pagination_label"' in INDEX_HTML
|
||||
|
||||
|
||||
def test_mcp_tool_rendering_is_paginated_not_full_list_rendered():
|
||||
assert "let _mcpToolsPageSize=5" in PANELS_JS
|
||||
assert "const MCP_TOOLS_PAGE_SIZE_OPTIONS=[5,10,20,40]" in PANELS_JS
|
||||
assert "filtered.slice((_mcpToolsPage-1)*_mcpToolsPageSize,_mcpToolsPage*_mcpToolsPageSize)" in PANELS_JS
|
||||
assert "list.innerHTML=visible.map(tool=>" in PANELS_JS
|
||||
assert "list.innerHTML=filtered.map(tool=>" not in PANELS_JS
|
||||
|
||||
|
||||
def test_mcp_tool_page_size_selector_resets_to_first_page():
|
||||
assert "function setMcpToolsPageSize(size){" in PANELS_JS
|
||||
assert "if(!MCP_TOOLS_PAGE_SIZE_OPTIONS.includes(next)) return;" in PANELS_JS
|
||||
assert "_mcpToolsPageSize=next;\n _mcpToolsPage=1;" in PANELS_JS
|
||||
assert "mcp_tools_per_page_aria" in PANELS_JS
|
||||
|
||||
|
||||
def test_mcp_tool_search_respects_selected_page_size():
|
||||
assert "const filtered=_filterMcpToolsForSearch(tools, query);" in PANELS_JS
|
||||
assert "const pages=Math.max(1,Math.ceil(filtered.length/_mcpToolsPageSize));" in PANELS_JS
|
||||
assert "mcp_tools_summary_showing" in PANELS_JS
|
||||
assert "t('mcp_tools_summary_showing',start,end,filtered,searchNote,totalNote,page,pages)" in PANELS_JS
|
||||
assert "mcp_tools_summary_no_matches" in PANELS_JS
|
||||
|
||||
|
||||
def test_mcp_tool_search_resets_to_first_page_and_page_changes_scroll_top():
|
||||
assert "function setMcpToolsPage(page){" in PANELS_JS
|
||||
assert "function filterMcpTools(){\n _mcpToolsPage=1;" in PANELS_JS
|
||||
search_block = PANELS_JS.split("function filterMcpTools(){", 1)[1].split("function loadMcpTools(){", 1)[0]
|
||||
assert "if(list) list.scrollTop=0;" in search_block
|
||||
|
||||
|
||||
def test_mcp_tool_empty_state_mentions_inactive_configured_servers():
|
||||
assert "let _mcpToolsMeta={}" in PANELS_JS
|
||||
assert "mcp_tools_inactive_configured_servers" in PANELS_JS
|
||||
assert "_mcpToolsMeta=r||{};" in PANELS_JS
|
||||
|
||||
|
||||
def test_mcp_tool_list_is_bounded_scroll_region_with_pager_chrome():
|
||||
assert ".mcp-tool-list{max-height:min(52vh,560px);overflow:auto" in STYLE_CSS
|
||||
assert "scrollbar-gutter:stable" in STYLE_CSS
|
||||
assert ".mcp-tool-pager{display:flex" in STYLE_CSS
|
||||
assert ".mcp-tool-page-btn" in STYLE_CSS
|
||||
assert ".mcp-tool-page-size" in STYLE_CSS
|
||||
|
||||
|
||||
def test_mcp_tool_pagination_strings_are_i18n_backed():
|
||||
for key in [
|
||||
"mcp_tools_summary_no_matches",
|
||||
"mcp_tools_summary_none",
|
||||
"mcp_tools_summary_matching",
|
||||
"mcp_tools_summary_total_note",
|
||||
"mcp_tools_summary_showing",
|
||||
"mcp_tools_page_size_prefix",
|
||||
"mcp_tools_page_size_suffix",
|
||||
"mcp_tools_per_page_aria",
|
||||
"mcp_tools_inactive_configured_servers",
|
||||
"mcp_tools_pagination_label",
|
||||
"mcp_tools_previous_page",
|
||||
"mcp_tools_previous_page_aria",
|
||||
"mcp_tools_next_page",
|
||||
"mcp_tools_next_page_aria",
|
||||
]:
|
||||
assert f"{key}:" in I18N_JS
|
||||
|
||||
|
||||
def test_changelog_mentions_large_mcp_tool_inventory_fix():
|
||||
assert "large MCP tool inventories" in CHANGELOG
|
||||
assert "5-item default pages" in CHANGELOG
|
||||
assert "per-page selector up to 40 tools" in CHANGELOG
|
||||
Reference in New Issue
Block a user