diff --git a/CHANGELOG.md b/CHANGELOG.md index b1967d0e..dc2240e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/docs/pr-media/2210/after-mcp-tools-paginated.png b/docs/pr-media/2210/after-mcp-tools-paginated.png new file mode 100644 index 00000000..e2793510 Binary files /dev/null and b/docs/pr-media/2210/after-mcp-tools-paginated.png differ diff --git a/docs/pr-media/2210/before-mcp-tools-unbounded.png b/docs/pr-media/2210/before-mcp-tools-unbounded.png new file mode 100644 index 00000000..9123a563 Binary files /dev/null and b/docs/pr-media/2210/before-mcp-tools-unbounded.png differ diff --git a/static/i18n.js b/static/i18n.js index b0a28ade..4e9fe34d 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -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', diff --git a/static/index.html b/static/index.html index 8a530d16..f7c3f07d 100644 --- a/static/index.html +++ b/static/index.html @@ -1138,7 +1138,9 @@
Search known tools across active MCP servers.
-
+
+
+
Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.
diff --git a/static/panels.js b/static/panels.js index d942fac0..c3c6bfe3 100644 --- a/static/panels.js +++ b/static/panels.js @@ -6285,6 +6285,10 @@ function loadMcpServers(){ }).catch(()=>{list.innerHTML=`
${esc(t('mcp_load_failed'))}
`}); } 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=`
${esc(t(key))}
`; +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=>``).join(''); + return ``; +} +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}
${esc(t('mcp_tools_inactive_configured_servers',unavailable.join(', ')))}`; +} +function _renderMcpToolPager(filteredCount, page, pages){ + const pager=$('mcpToolPager'); + if(!pager) return; + if(pages<=1){ + pager.innerHTML=''; return; } - list.innerHTML=filtered.map(tool=>{ + pager.innerHTML=` + ${page} / ${pages} + `; +} +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=`${esc(_mcpToolsSummary(total,filtered.length,_mcpToolsPage,pages,query))}${_mcpToolPageSizeControl()}`; + _renderMcpToolPager(filtered.length,_mcpToolsPage,pages); + if(!filtered.length){ + list.innerHTML=`
${_mcpToolsEmptyMessage(query)}
`; + return; + } + const visible=filtered.slice((_mcpToolsPage-1)*_mcpToolsPageSize,_mcpToolsPage*_mcpToolsPageSize); + list.innerHTML=visible.map(tool=>{ const status=tool.status||'unknown'; const statusBadge=`${esc(_mcpStatusLabel(status))}`; const schemaText=_mcpToolSchemaText(tool.schema_summary); @@ -6325,16 +6369,42 @@ function _renderMcpTools(tools, query){ `; }).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=`
${esc(t('loading'))}
`; api('/api/mcp/tools').then(r=>{ _mcpToolsCache=(r&&Array.isArray(r.tools))?r.tools:[]; + _mcpToolsMeta=r||{}; + _mcpToolsPage=1; filterMcpTools(); }).catch(()=>{list.innerHTML=`
${esc(t('mcp_tools_load_failed'))}
`}); } diff --git a/static/style.css b/static/style.css index 67b9b6f5..4c10f894 100644 --- a/static/style.css +++ b/static/style.css @@ -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. */ diff --git a/tests/test_mcp_tools_list_overflow.py b/tests/test_mcp_tools_list_overflow.py new file mode 100644 index 00000000..9e418cd9 --- /dev/null +++ b/tests/test_mcp_tools_list_overflow.py @@ -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