fix: resolve session switching failure when same-named sessions exist on different devices

The sidebar click guard, active highlight, mobile bottom sheet, and kill-session
auto-close all compared sessions by name only, ignoring remoteId. This caused
same-named sessions on different devices to be treated as identical — clicking
one while viewing the other was silently discarded as a no-op.

Now all 6 comparison sites check both name AND remoteId, using the _viewingRemoteId
that was already being tracked but never used in guards.

Bumps version to 0.4.4.
This commit is contained in:
Brian Krabach
2026-05-02 06:55:39 -07:00
parent 3538ecf48d
commit 821c595abe
2 changed files with 17 additions and 15 deletions
+16 -14
View File
@@ -354,7 +354,7 @@ async function pollSessions() {
_pollFailCount = 0; _pollFailCount = 0;
setConnectionStatus('ok'); setConnectionStatus('ok');
renderGrid(sessions); renderGrid(sessions);
renderSidebar(sessions, _viewingSession); renderSidebar(sessions, _viewingSession, _viewingRemoteId);
handleBellTransitions(prev, sessions); handleBellTransitions(prev, sessions);
updateSessionPill(sessions); updateSessionPill(sessions);
updateFaviconBadge(); updateFaviconBadge();
@@ -554,12 +554,13 @@ function buildTileHTML(session, index, mobile) {
* Build the HTML string for a single session sidebar card. * Build the HTML string for a single session sidebar card.
* @param {object} session * @param {object} session
* @param {string} currentSession - name of the currently active session * @param {string} currentSession - name of the currently active session
* @param {string} currentRemoteId - remoteId of the currently active session
* @returns {string} * @returns {string}
*/ */
function buildSidebarHTML(session, currentSession) { function buildSidebarHTML(session, currentSession, currentRemoteId) {
const name = session.name || ''; const name = session.name || '';
const escapedName = escapeHtml(name); const escapedName = escapeHtml(name);
const isActive = name === currentSession; const isActive = name === currentSession && (session.remoteId ?? '') === (currentRemoteId ?? '');
var ds = getDisplaySettings(); var ds = getDisplaySettings();
var actIndicator = ds.activityIndicator !== undefined ? ds.activityIndicator : 'both'; var actIndicator = ds.activityIndicator !== undefined ? ds.activityIndicator : 'both';
@@ -697,8 +698,9 @@ function _resolveActiveView(activeView, views) {
* Binds click handlers on each sidebar-item to switch sessions. * Binds click handlers on each sidebar-item to switch sessions.
* @param {object[]} sessions * @param {object[]} sessions
* @param {string|null} currentSession - name of the currently active session * @param {string|null} currentSession - name of the currently active session
* @param {string} [currentRemoteId] - remoteId of the currently active session
*/ */
function renderSidebar(sessions, currentSession) { function renderSidebar(sessions, currentSession, currentRemoteId) {
if (_viewMode !== 'fullscreen') return; if (_viewMode !== 'fullscreen') return;
const list = $('sidebar-list'); const list = $('sidebar-list');
@@ -724,11 +726,11 @@ function renderSidebar(sessions, currentSession) {
for (const [deviceName, deviceSessions] of groups) { for (const [deviceName, deviceSessions] of groups) {
html += `<h4 class="sidebar-device-header">${escapeHtml(deviceName)}</h4>`; html += `<h4 class="sidebar-device-header">${escapeHtml(deviceName)}</h4>`;
html += deviceSessions.map((session) => buildSidebarHTML(session, currentSession)).join(''); html += deviceSessions.map((session) => buildSidebarHTML(session, currentSession, currentRemoteId)).join('');
} }
} else { } else {
// Single source: flat list with no device headers // Single source: flat list with no device headers
html = visible.map((session) => buildSidebarHTML(session, currentSession)).join(''); html = visible.map((session) => buildSidebarHTML(session, currentSession, currentRemoteId)).join('');
} }
list.innerHTML = html; list.innerHTML = html;
@@ -740,7 +742,7 @@ function renderSidebar(sessions, currentSession) {
const remoteId = item.dataset.remoteId || ''; const remoteId = item.dataset.remoteId || '';
on(item, 'click', (e) => { on(item, 'click', (e) => {
if (e.target.closest && e.target.closest('.tile-options-btn')) return; if (e.target.closest && e.target.closest('.tile-options-btn')) return;
if (name !== currentSession) openSession(name, { remoteId }); if (name !== currentSession || remoteId !== (currentRemoteId ?? '')) openSession(name, { remoteId });
}); });
}); });
} }
@@ -1470,7 +1472,7 @@ function switchView(viewName) {
_activeView = viewName; _activeView = viewName;
closeViewDropdown(); closeViewDropdown();
renderGrid(_currentSessions || []); renderGrid(_currentSessions || []);
renderSidebar(_currentSessions || [], _viewingSession); renderSidebar(_currentSessions || [], _viewingSession, _viewingRemoteId);
renderViewDropdown(); renderViewDropdown();
// Update sidebar view label to match the active view // Update sidebar view label to match the active view
var sidebarLabel = $('sidebar-view-label'); var sidebarLabel = $('sidebar-view-label');
@@ -2330,7 +2332,7 @@ function _executeKill(name, remoteId) {
.then(function() { .then(function() {
closeFlyoutMenu(); closeFlyoutMenu();
showToast('Session \'' + name + '\' killed'); showToast('Session \'' + name + '\' killed');
if (_viewingSession === name) { if (_viewingSession === name && (_viewingRemoteId ?? '') === (remoteId || '')) {
closeSession(); closeSession();
} }
pollSessions(); pollSessions();
@@ -2903,7 +2905,7 @@ async function openSession(name, opts = {}) {
// Pre-render sidebar with current sessions before first poll tick // Pre-render sidebar with current sessions before first poll tick
initSidebar(); initSidebar();
renderSidebar(_currentSessions, name); renderSidebar(_currentSessions, name, _viewingRemoteId);
// Update expanded header // Update expanded header
const nameEl = $('expanded-session-name'); const nameEl = $('expanded-session-name');
@@ -2941,7 +2943,7 @@ async function openSession(name, opts = {}) {
} }
// Re-render sidebar after DOM is visible and dimensions are correct // Re-render sidebar after DOM is visible and dimensions are correct
initSidebar(); initSidebar();
renderSidebar(_currentSessions, name); renderSidebar(_currentSessions, name, _viewingRemoteId);
resolve(); resolve();
}, opts.skipAnimation ? 0 : 260); }, opts.skipAnimation ? 0 : 260);
// If setTimeout is stubbed (e.g. in test env), resolve immediately so we don't hang // If setTimeout is stubbed (e.g. in test env), resolve immediately so we don't hang
@@ -3643,7 +3645,7 @@ function renderSheetList() {
list.innerHTML = sorted.map(function(s) { list.innerHTML = sorted.map(function(s) {
var hasBell = s.bell && s.bell.unseen_count > 0 && var hasBell = s.bell && s.bell.unseen_count > 0 &&
(s.bell.seen_at === null || s.bell.last_fired_at > s.bell.seen_at); (s.bell.seen_at === null || s.bell.last_fired_at > s.bell.seen_at);
var isActive = s.name === _viewingSession; var isActive = s.name === _viewingSession && (s.remoteId ?? '') === (_viewingRemoteId ?? '');
var escapedName = escapeHtml(s.name || ''); var escapedName = escapeHtml(s.name || '');
var remoteIdAttr = s.remoteId != null ? ' data-remote-id="' + escapeHtml(s.remoteId) + '"' : ''; var remoteIdAttr = s.remoteId != null ? ' data-remote-id="' + escapeHtml(s.remoteId) + '"' : '';
return '<li class="sheet-item' + (isActive ? ' sheet-item--active' : '') + '"' + return '<li class="sheet-item' + (isActive ? ' sheet-item--active' : '') + '"' +
@@ -3659,7 +3661,7 @@ function renderSheetList() {
closeBottomSheet(); closeBottomSheet();
var name = item.dataset.session; var name = item.dataset.session;
var remoteId = item.dataset.remoteId || ''; var remoteId = item.dataset.remoteId || '';
if (name !== _viewingSession) openSession(name, { remoteId: remoteId }); if (name !== _viewingSession || remoteId !== (_viewingRemoteId ?? '')) openSession(name, { remoteId: remoteId });
}); });
}); });
} }
@@ -3971,7 +3973,7 @@ function killSession(name, remoteId) {
.then(function() { .then(function() {
showToast('Session \'' + name + '\' killed'); showToast('Session \'' + name + '\' killed');
// If we deleted the session we're currently viewing, return to dashboard // If we deleted the session we're currently viewing, return to dashboard
if (_viewingSession === name) { if (_viewingSession === name && (_viewingRemoteId ?? '') === (remoteId || '')) {
closeSession(); closeSession();
} }
pollSessions(); pollSessions();
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "muxplex" name = "muxplex"
version = "0.4.3" version = "0.4.4"
description = "Web-based tmux session dashboard — access all your tmux sessions from any browser" description = "Web-based tmux session dashboard — access all your tmux sessions from any browser"
readme = "README.md" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }