16141614let profiles = [];
16151615let activeProfile = 'core';
16161616let selectedNode = null;
1617+ let detailInvoker = null;
1618+ let credInvoker = null;
1619+ // Bumps on every openServerDetail call so a late response from an older
1620+ // fetch cannot reopen or overwrite the panel after the user moved on.
1621+ let detailRequestGen = 0;
16171622
16181623// Overview live state.
16191624let routeFilter = 'all';
@@ -2318,14 +2323,15 @@ class ApiError extends Error {
23182323
23192324async function openSelectedRoute() {
23202325 if (routeSel < 0 || !routeRows[routeSel]) return;
2321- const name = routeRows[routeSel].dataset.name;
2326+ const invoker = routeRows[routeSel];
2327+ const name = invoker.dataset.name;
23222328 let node = servers.find(s => s.name === name);
23232329 if (!node) return;
23242330 if (!node.mcp) {
23252331 try { node = await api('/api/mcp/servers/' + encodeURIComponent(node.name)); }
23262332 catch (err) { /* fall back */ }
23272333 }
2328- openDetail(node);
2334+ openDetail(node, false, invoker );
23292335}
23302336
23312337function buildNodes() {
@@ -2349,7 +2355,7 @@ class ApiError extends Error {
23492355 try { node = await api('/api/mcp/servers/' + encodeURIComponent(node.name)); }
23502356 catch (err) { /* fall back */ }
23512357 }
2352- openDetail(node);
2358+ openDetail(node, false, row );
23532359}
23542360
23552361function startAnimationLoop() {}
@@ -2364,7 +2370,21 @@ class ApiError extends Error {
23642370 return '-';
23652371}
23662372
2367- function openDetail(node) {
2373+ function openDetail(node, refresh, invoker) {
2374+ // Track the latest trigger from outside the panel: selecting another server
2375+ // while the panel is open must move the return target to that row, but focus
2376+ // already inside the panel (close button, actions) is not a return target.
2377+ // Background refreshes (WebSocket updates, post-action reloads) pass
2378+ // refresh=true and must not overwrite the invoker: whatever happens to hold
2379+ // focus then (e.g. the command bar) never opened the panel.
2380+ // Callers that await a fetch pass the pre-captured invoker so a focus move
2381+ // during the request cannot steal the return target.
2382+ const trigger = invoker !== undefined ? invoker : document.activeElement;
2383+ const openPanel = document.getElementById('detail-panel');
2384+ if (!refresh && trigger && trigger.tagName !== 'BODY'
2385+ && !openPanel.contains(trigger)) {
2386+ detailInvoker = trigger;
2387+ }
23682388 selectedNode = node;
23692389 document.getElementById('detail-name').textContent = node.name || '-';
23702390 document.getElementById('detail-desc').textContent = node.description || '-';
@@ -2450,6 +2470,12 @@ class ApiError extends Error {
24502470 document.getElementById('detail-panel').classList.remove('open');
24512471 selectedNode = null;
24522472 writeUrlState();
2473+ const invoker = detailInvoker;
2474+ detailInvoker = null;
2475+ if (invoker && typeof invoker.focus === 'function'
2476+ && document.contains(invoker)) {
2477+ invoker.focus();
2478+ }
24532479}
24542480
24552481// ── Credentials modal ──────────────────
@@ -2467,18 +2493,24 @@ class ApiError extends Error {
24672493 if (selectedNode && selectedNode.name) promptCredentials(selectedNode.name);
24682494}
24692495
2470- async function promptCredentials(name) {
2496+ async function promptCredentials(name, invoker ) {
24712497 // Always work from the full server doc: the catalog payload omits the list
24722498 // of required env vars, the detail endpoint has it.
2499+ // Capture the trigger before the await — focus may move while loading.
2500+ const captured = invoker !== undefined ? invoker : document.activeElement;
24732501 try {
24742502 const server = await api('/api/mcp/servers/' + encodeURIComponent(name));
2475- if (server && !server.error) openCredentialsModal(server);
2503+ if (server && !server.error) openCredentialsModal(server, captured );
24762504 } catch (e) {
24772505 toast('Could not load ' + name + ': ' + (e.message || 'failed'), 'error');
24782506 }
24792507}
24802508
2481- function openCredentialsModal(server) {
2509+ function openCredentialsModal(server, invoker) {
2510+ const trigger = invoker !== undefined ? invoker : document.activeElement;
2511+ if (!credInvoker && trigger && trigger.tagName !== 'BODY') {
2512+ credInvoker = trigger;
2513+ }
24822514 credServer = server;
24832515 const reqs = server.env_required || [];
24842516 document.getElementById('cred-title').textContent = 'Connect ' + server.name;
@@ -2527,6 +2559,12 @@ class ApiError extends Error {
25272559function closeCredentialsModal() {
25282560 document.getElementById('cred-modal').classList.remove('show');
25292561 credServer = null;
2562+ const invoker = credInvoker;
2563+ credInvoker = null;
2564+ if (invoker && typeof invoker.focus === 'function'
2565+ && document.contains(invoker)) {
2566+ invoker.focus();
2567+ }
25302568}
25312569
25322570async function saveCredentials(btn) {
@@ -2554,7 +2592,7 @@ class ApiError extends Error {
25542592 closeCredentialsModal();
25552593 await loadCatalog();
25562594 if (currentView === 'catalog') await loadCatalogView();
2557- if (selectedNode && selectedNode.name === name) openServerDetail(name);
2595+ if (selectedNode && selectedNode.name === name) openServerDetail(name, true );
25582596 } catch (e) {
25592597 toast('Could not save credentials: ' + (e.message || 'failed'), 'error');
25602598 } finally {
@@ -2592,7 +2630,7 @@ class ApiError extends Error {
25922630 // don't keep rendering a stale, optimistically-mutated object.
25932631 try { await loadCatalog(); } catch (e) { /* handled */ }
25942632 const fresh = servers.find(s => s.name === name);
2595- if (fresh) openDetail(fresh);
2633+ if (fresh) openDetail(fresh, true );
25962634 // Enabling something that still needs a token? Bring up the connect popup.
25972635 if (enable && fresh && fresh.env_configured === false) promptCredentials(name);
25982636}
@@ -2653,7 +2691,7 @@ class ApiError extends Error {
26532691 return;
26542692 }
26552693 const card = e.target.closest('.server-card');
2656- if (card && card.dataset.name) openServerDetail(card.dataset.name);
2694+ if (card && card.dataset.name) openServerDetail(card.dataset.name, false, card );
26572695 });
26582696 grid.addEventListener('keydown', (e) => {
26592697 if (e.key === 'Enter' || e.key === ' ') {
@@ -2667,7 +2705,7 @@ class ApiError extends Error {
26672705 const card = e.target.closest('.server-card');
26682706 if (card && card.dataset.name && e.target === card) {
26692707 e.preventDefault();
2670- openServerDetail(card.dataset.name);
2708+ openServerDetail(card.dataset.name, false, card );
26712709 }
26722710 }
26732711 });
@@ -3026,13 +3064,13 @@ class ApiError extends Error {
30263064 // refresh the open detail panel so its status reflects the change.
30273065 scheduleCatalogReload();
30283066 if (selectedNode && selectedNode.name === data.name) {
3029- openServerDetail(data.name);
3067+ openServerDetail(data.name, true );
30303068 }
30313069 }
30323070 if (data.type === 'server_credentials') {
30333071 scheduleCatalogReload();
30343072 if (selectedNode && selectedNode.name === data.name) {
3035- openServerDetail(data.name);
3073+ openServerDetail(data.name, true );
30363074 }
30373075 }
30383076 if (data.type === 'tool_call' || data.type === 'chain_run'
@@ -3542,10 +3580,25 @@ class ApiError extends Error {
35423580 }
35433581}
35443582
3545- async function openServerDetail(name) {
3583+ async function openServerDetail(name, refresh, invoker) {
3584+ // Capture before the await so a focus move during the fetch cannot steal
3585+ // the return target. Background refreshes skip capture entirely.
3586+ const captured = refresh
3587+ ? null
3588+ : (invoker !== undefined ? invoker : document.activeElement);
3589+ const gen = ++detailRequestGen;
35463590 try {
35473591 const data = await api('/api/mcp/servers/' + encodeURIComponent(name));
3548- openDetail(data);
3592+ if (gen !== detailRequestGen) return;
3593+ if (refresh) {
3594+ // Drop stale refreshes: panel closed, or user selected another server.
3595+ const panel = document.getElementById('detail-panel');
3596+ if (!panel || !panel.classList.contains('open')) return;
3597+ if (!selectedNode || selectedNode.name !== name) return;
3598+ openDetail(data, true);
3599+ return;
3600+ }
3601+ openDetail(data, false, captured);
35493602 } catch (e) {
35503603 toast(name + ': ' + (e.message || 'not found'), 'error');
35513604 }
0 commit comments