Merge pull request #2210 into stage-351

Fix MCP tools list overflow with pagination/search (Jordan-SkyLF)
This commit is contained in:
Hermes Agent
2026-05-13 23:51:25 +00:00
8 changed files with 325 additions and 10 deletions
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>`});
}
+11
View File
@@ -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. */
+90
View File
@@ -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