refactor: improve code readability and formatting in profiles.js and tabs.js

- Updated formatting for better readability, including consistent use of line breaks and indentation.
- Enhanced the clarity of event listener functions in profiles.js.
- Refactored tab management logic in tabs.js for improved maintainability.
- Ensured consistent use of single quotes and double quotes across the files.
This commit is contained in:
Andy Dixon
2026-04-22 14:57:01 +01:00
parent 8fa9fb5be5
commit 98ca4743e5
6 changed files with 1083 additions and 1002 deletions
+418 -356
View File
@@ -1,32 +1,38 @@
(function() {
/**
* Nexus Terminal — tab management.
* Handles the tab bar: adding, removing, reordering, renaming,
* pinning tabs, and context menus.
*/
(function () {
/**
* Nexus Terminal — tab management.
* Handles the tab bar: adding, removing, reordering, renaming,
* pinning tabs, and context menus.
*/
const { createElement, escapeHtml, protocolColour, stateLabel, dismissContextMenus } = window.NexusTerminal;
const {
createElement,
escapeHtml,
protocolColour,
stateLabel,
dismissContextMenus,
} = window.NexusTerminal;
class TabManager {
class TabManager {
/**
* @param {App} app
*/
constructor(app) {
this.app = app;
this.app = app;
/** @type {HTMLElement} The inner tab list where tab elements are inserted. */
this.tabList = document.getElementById('tab-list');
/** @type {HTMLElement} The inner tab list where tab elements are inserted. */
this.tabList = document.getElementById("tab-list");
/** @type {HTMLElement} The outer tab bar wrapper (used for drag-and-drop). */
this.tabBar = document.getElementById('tab-bar');
/** @type {HTMLElement} The outer tab bar wrapper (used for drag-and-drop). */
this.tabBar = document.getElementById("tab-bar");
/** @type {HTMLElement} The container for terminal/viewer panels. */
this.panelContainer = document.getElementById('panel-container');
/** @type {HTMLElement} The container for terminal/viewer panels. */
this.panelContainer = document.getElementById("panel-container");
/** @type {string[]} Ordered list of session IDs. */
this.order = [];
/** @type {string[]} Ordered list of session IDs. */
this.order = [];
this._setupDragAndDrop();
this._setupDragAndDrop();
}
// ── Public API ───────────────────────────────────────────────────
@@ -36,21 +42,21 @@ class TabManager {
* @param {Object} sessionInfo
*/
addTab(sessionInfo) {
this.order.push(sessionInfo.id);
this._renderTab(sessionInfo);
this.order.push(sessionInfo.id);
this._renderTab(sessionInfo);
// Create panel container for this session's terminal/viewer
const panel = createElement('div', 'session-panel', {
id: `panel-${sessionInfo.id}`,
dataset: { sessionId: sessionInfo.id },
});
panel.style.display = 'none';
this.panelContainer.appendChild(panel);
// Create panel container for this session's terminal/viewer
const panel = createElement("div", "session-panel", {
id: `panel-${sessionInfo.id}`,
dataset: { sessionId: sessionInfo.id },
});
panel.style.display = "none";
this.panelContainer.appendChild(panel);
// Hide the empty-state BEFORE activating the panel so the
// panel-container gets the full workspace height immediately.
this._updateEmptyState();
this.setActive(sessionInfo.id);
// Hide the empty-state BEFORE activating the panel so the
// panel-container gets the full workspace height immediately.
this._updateEmptyState();
this.setActive(sessionInfo.id);
}
/**
@@ -58,30 +64,33 @@ class TabManager {
* @param {string} sessionId
*/
removeTab(sessionId) {
const idx = this.order.indexOf(sessionId);
if (idx !== -1) this.order.splice(idx, 1);
const idx = this.order.indexOf(sessionId);
if (idx !== -1) this.order.splice(idx, 1);
// Remove tab element
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`);
if (tabEl) tabEl.remove();
// Remove tab element
const tabEl = this.tabList.querySelector(
`[data-session-id="${sessionId}"]`,
);
if (tabEl) tabEl.remove();
// Remove panel
const panel = document.getElementById(`panel-${sessionId}`);
if (panel) panel.remove();
// Remove panel
const panel = document.getElementById(`panel-${sessionId}`);
if (panel) panel.remove();
// If this was the active tab, activate the nearest neighbour
if (this.app.state.activeSessionId === sessionId) {
const next = this.order.length > 0
? this.order[Math.min(idx, this.order.length - 1)]
: null;
if (next) {
this.setActive(next);
} else {
this.app.state.setActiveSession(null);
}
// If this was the active tab, activate the nearest neighbour
if (this.app.state.activeSessionId === sessionId) {
const next =
this.order.length > 0
? this.order[Math.min(idx, this.order.length - 1)]
: null;
if (next) {
this.setActive(next);
} else {
this.app.state.setActiveSession(null);
}
}
this._updateEmptyState();
this._updateEmptyState();
}
/**
@@ -89,35 +98,35 @@ class TabManager {
* @param {string} sessionId
*/
setActive(sessionId) {
this.app.state.setActiveSession(sessionId);
this.app.state.setActiveSession(sessionId);
// Highlight active tab
this.tabList.querySelectorAll('.tab').forEach((t) => {
t.classList.toggle('tab-active', t.dataset.sessionId === sessionId);
});
// Highlight active tab
this.tabList.querySelectorAll(".tab").forEach((t) => {
t.classList.toggle("tab-active", t.dataset.sessionId === sessionId);
});
// Show corresponding panel, hide others
this.panelContainer.querySelectorAll('.session-panel').forEach((p) => {
p.style.display = p.dataset.sessionId === sessionId ? 'flex' : 'none';
});
// Show corresponding panel, hide others
this.panelContainer.querySelectorAll(".session-panel").forEach((p) => {
p.style.display = p.dataset.sessionId === sessionId ? "flex" : "none";
});
// Notify sidebar
const session = this.app.state.getSession(sessionId);
if (session) {
this.app.sidebar.highlightActive(session.profileId);
}
// Notify sidebar
const session = this.app.state.getSession(sessionId);
if (session) {
this.app.sidebar.highlightActive(session.profileId);
}
// Fit the terminal for the newly active session
this.app.terminals.fitActive();
// Fit the terminal for the newly active session
this.app.terminals.fitActive();
}
/**
* Closes the currently active tab.
*/
closeActive() {
if (this.app.state.activeSessionId) {
this.closeTab(this.app.state.activeSessionId);
}
if (this.app.state.activeSessionId) {
this.closeTab(this.app.state.activeSessionId);
}
}
/**
@@ -125,7 +134,7 @@ class TabManager {
* @param {string} sessionId
*/
reconnectSession(sessionId) {
return this._reconnect(sessionId);
return this._reconnect(sessionId);
}
/**
@@ -133,25 +142,29 @@ class TabManager {
* @param {string} sessionId
*/
async closeTab(sessionId) {
const session = this.app.state.getSession(sessionId);
const session = this.app.state.getSession(sessionId);
// Ask for confirmation if connected and the preference is set
if (session && session.state === 'connected' && this.app.state.preferences.confirmOnDisconnect) {
const ok = await this._confirmClose(sessionId);
if (!ok) return;
}
// Ask for confirmation if connected and the preference is set
if (
session &&
session.state === "connected" &&
this.app.state.preferences.confirmOnDisconnect
) {
const ok = await this._confirmClose(sessionId);
if (!ok) return;
}
try {
await window.go.api.Service.DisconnectSession(sessionId);
} catch (_) {
// Session may already be closed — carry on
}
try {
await window.go.api.Service.DisconnectSession(sessionId);
} catch (_) {
// Session may already be closed — carry on
}
this.app.terminals.destroyTerminal(sessionId);
this.app.rawViewer.destroyViewer(sessionId);
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
this.app.state.removeSession(sessionId);
this.removeTab(sessionId);
this.app.terminals.destroyTerminal(sessionId);
this.app.rawViewer.destroyViewer(sessionId);
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
this.app.state.removeSession(sessionId);
this.removeTab(sessionId);
}
/**
@@ -161,12 +174,12 @@ class TabManager {
* @private
*/
_confirmClose(sessionId) {
const createElement = window.NexusTerminal.createElement;
const label = this._tabLabel(sessionId);
return new Promise((resolve) => {
const backdrop = createElement('div', 'modal-backdrop');
const modal = createElement('div', 'modal modal-sm');
modal.innerHTML = `
const createElement = window.NexusTerminal.createElement;
const label = this._tabLabel(sessionId);
return new Promise((resolve) => {
const backdrop = createElement("div", "modal-backdrop");
const modal = createElement("div", "modal modal-sm");
modal.innerHTML = `
<div class="modal-header">
<h3 class="modal-title">Close Session</h3>
<button class="btn btn-icon btn-xs modal-close">&times;</button>
@@ -181,24 +194,32 @@ class TabManager {
<button class="btn btn-sm btn-danger confirm-ok">Disconnect</button>
</div>
`;
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
requestAnimationFrame(() => {
backdrop.classList.add('is-visible');
modal.classList.add('is-visible');
});
const close = (val) => {
backdrop.classList.remove('is-visible');
modal.classList.remove('is-visible');
setTimeout(() => backdrop.remove(), 200);
resolve(val);
};
modal.querySelector('.modal-close').addEventListener('click', () => close(false));
modal.querySelector('.confirm-cancel').addEventListener('click', () => close(false));
modal.querySelector('.confirm-ok').addEventListener('click', () => close(true));
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(false); });
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
requestAnimationFrame(() => {
backdrop.classList.add("is-visible");
modal.classList.add("is-visible");
});
const close = (val) => {
backdrop.classList.remove("is-visible");
modal.classList.remove("is-visible");
setTimeout(() => backdrop.remove(), 200);
resolve(val);
};
modal
.querySelector(".modal-close")
.addEventListener("click", () => close(false));
modal
.querySelector(".confirm-cancel")
.addEventListener("click", () => close(false));
modal
.querySelector(".confirm-ok")
.addEventListener("click", () => close(true));
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close(false);
});
});
}
/**
@@ -207,11 +228,13 @@ class TabManager {
* @param {string} name
*/
renameTab(sessionId, name) {
this.app.state.updateSession(sessionId, { tabName: name });
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`);
if (tabEl) {
tabEl.querySelector('.tab-name').textContent = name;
}
this.app.state.updateSession(sessionId, { tabName: name });
const tabEl = this.tabList.querySelector(
`[data-session-id="${sessionId}"]`,
);
if (tabEl) {
tabEl.querySelector(".tab-name").textContent = name;
}
}
/**
@@ -219,9 +242,9 @@ class TabManager {
* @param {string} sessionId
*/
pinTab(sessionId) {
this.app.state.updateSession(sessionId, { pinned: true });
this._reorderDOM();
this._updateTabStyles(sessionId);
this.app.state.updateSession(sessionId, { pinned: true });
this._reorderDOM();
this._updateTabStyles(sessionId);
}
/**
@@ -229,9 +252,9 @@ class TabManager {
* @param {string} sessionId
*/
unpinTab(sessionId) {
this.app.state.updateSession(sessionId, { pinned: false });
this._reorderDOM();
this._updateTabStyles(sessionId);
this.app.state.updateSession(sessionId, { pinned: false });
this._reorderDOM();
this._updateTabStyles(sessionId);
}
/**
@@ -239,11 +262,14 @@ class TabManager {
* @param {string} sessionId
*/
moveLeft(sessionId) {
const idx = this.order.indexOf(sessionId);
if (idx > 0) {
[this.order[idx - 1], this.order[idx]] = [this.order[idx], this.order[idx - 1]];
this._reorderDOM();
}
const idx = this.order.indexOf(sessionId);
if (idx > 0) {
[this.order[idx - 1], this.order[idx]] = [
this.order[idx],
this.order[idx - 1],
];
this._reorderDOM();
}
}
/**
@@ -251,11 +277,14 @@ class TabManager {
* @param {string} sessionId
*/
moveRight(sessionId) {
const idx = this.order.indexOf(sessionId);
if (idx >= 0 && idx < this.order.length - 1) {
[this.order[idx], this.order[idx + 1]] = [this.order[idx + 1], this.order[idx]];
this._reorderDOM();
}
const idx = this.order.indexOf(sessionId);
if (idx >= 0 && idx < this.order.length - 1) {
[this.order[idx], this.order[idx + 1]] = [
this.order[idx + 1],
this.order[idx],
];
this._reorderDOM();
}
}
/**
@@ -263,10 +292,10 @@ class TabManager {
* @param {string} sessionId
*/
async duplicateTab(sessionId) {
const session = this.app.state.getSession(sessionId);
if (session && session.profileId) {
await this.app.connectProfile(session.profileId);
}
const session = this.app.state.getSession(sessionId);
if (session && session.profileId) {
await this.app.connectProfile(session.profileId);
}
}
/**
@@ -276,20 +305,22 @@ class TabManager {
* @param {string} [error]
*/
updateSessionState(sessionId, state, error) {
this.app.state.updateSession(sessionId, { state, error: error || '' });
this.app.state.updateSession(sessionId, { state, error: error || "" });
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`);
if (!tabEl) return;
const tabEl = this.tabList.querySelector(
`[data-session-id="${sessionId}"]`,
);
if (!tabEl) return;
const dot = tabEl.querySelector('.tab-status');
if (dot) {
dot.className = `tab-status status-${state}`;
dot.title = stateLabel(state);
}
const dot = tabEl.querySelector(".tab-status");
if (dot) {
dot.className = `tab-status status-${state}`;
dot.title = stateLabel(state);
}
if (state === 'error' && error) {
this.app.notifications.error(`${this._tabLabel(sessionId)}: ${error}`);
}
if (state === "error" && error) {
this.app.notifications.error(`${this._tabLabel(sessionId)}: ${error}`);
}
}
/**
@@ -297,7 +328,7 @@ class TabManager {
* @param {string} sessionId
*/
handleSessionClosed(sessionId) {
this.updateSessionState(sessionId, 'disconnected');
this.updateSessionState(sessionId, "disconnected");
}
/**
@@ -307,58 +338,73 @@ class TabManager {
* @param {number} y
*/
showContextMenu(sessionId, x, y) {
dismissContextMenus();
const session = this.app.state.getSession(sessionId);
if (!session) return;
dismissContextMenus();
const session = this.app.state.getSession(sessionId);
if (!session) return;
const menu = createElement('div', 'context-menu');
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
const menu = createElement("div", "context-menu");
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
const isPinned = session.pinned;
const isPinned = session.pinned;
const items = [
{ label: 'Rename…', action: () => this._promptRename(sessionId) },
{ label: 'Duplicate', action: () => this.duplicateTab(sessionId) },
{ divider: true },
{ label: isPinned ? 'Unpin' : 'Pin', action: () => isPinned ? this.unpinTab(sessionId) : this.pinTab(sessionId) },
{ label: 'Move Left', action: () => this.moveLeft(sessionId) },
{ label: 'Move Right', action: () => this.moveRight(sessionId) },
{ divider: true },
{
label: 'Reconnect',
action: () => this._reconnect(sessionId),
disabled: session.state === 'connected' || session.state === 'connecting',
},
{ divider: true },
{ label: 'Close', className: 'context-menu-danger', action: () => this.closeTab(sessionId) },
];
const items = [
{ label: "Rename…", action: () => this._promptRename(sessionId) },
{ label: "Duplicate", action: () => this.duplicateTab(sessionId) },
{ divider: true },
{
label: isPinned ? "Unpin" : "Pin",
action: () =>
isPinned ? this.unpinTab(sessionId) : this.pinTab(sessionId),
},
{ label: "Move Left", action: () => this.moveLeft(sessionId) },
{ label: "Move Right", action: () => this.moveRight(sessionId) },
{ divider: true },
{
label: "Reconnect",
action: () => this._reconnect(sessionId),
disabled:
session.state === "connected" || session.state === "connecting",
},
{ divider: true },
{
label: "Close",
className: "context-menu-danger",
action: () => this.closeTab(sessionId),
},
];
for (const entry of items) {
if (entry.divider) {
menu.appendChild(createElement('div', 'context-menu-divider'));
continue;
}
const btn = createElement('button', `context-menu-item ${entry.className || ''}`, {
textContent: entry.label,
});
if (entry.disabled) {
btn.disabled = true;
btn.classList.add('context-menu-disabled');
}
btn.addEventListener('click', () => {
dismissContextMenus();
entry.action();
});
menu.appendChild(btn);
for (const entry of items) {
if (entry.divider) {
menu.appendChild(createElement("div", "context-menu-divider"));
continue;
}
const btn = createElement(
"button",
`context-menu-item ${entry.className || ""}`,
{
textContent: entry.label,
},
);
if (entry.disabled) {
btn.disabled = true;
btn.classList.add("context-menu-disabled");
}
btn.addEventListener("click", () => {
dismissContextMenus();
entry.action();
});
menu.appendChild(btn);
}
document.body.appendChild(menu);
document.body.appendChild(menu);
// Keep within viewport
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth) menu.style.left = `${window.innerWidth - rect.width - 8}px`;
if (rect.bottom > window.innerHeight) menu.style.top = `${window.innerHeight - rect.height - 8}px`;
// Keep within viewport
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth)
menu.style.left = `${window.innerWidth - rect.width - 8}px`;
if (rect.bottom > window.innerHeight)
menu.style.top = `${window.innerHeight - rect.height - 8}px`;
}
/**
@@ -366,7 +412,7 @@ class TabManager {
* @returns {string[]}
*/
getTabOrder() {
return [...this.order];
return [...this.order];
}
/**
@@ -374,9 +420,9 @@ class TabManager {
* @returns {string|null}
*/
getNextTab() {
if (this.order.length === 0) return null;
const idx = this.order.indexOf(this.app.state.activeSessionId);
return this.order[(idx + 1) % this.order.length];
if (this.order.length === 0) return null;
const idx = this.order.indexOf(this.app.state.activeSessionId);
return this.order[(idx + 1) % this.order.length];
}
/**
@@ -384,9 +430,9 @@ class TabManager {
* @returns {string|null}
*/
getPrevTab() {
if (this.order.length === 0) return null;
const idx = this.order.indexOf(this.app.state.activeSessionId);
return this.order[(idx - 1 + this.order.length) % this.order.length];
if (this.order.length === 0) return null;
const idx = this.order.indexOf(this.app.state.activeSessionId);
return this.order[(idx - 1 + this.order.length) % this.order.length];
}
// ── Rendering ────────────────────────────────────────────────────
@@ -397,70 +443,81 @@ class TabManager {
* @private
*/
_renderTab(sessionInfo) {
const tab = createElement('div', 'tab', {
dataset: { sessionId: sessionInfo.id },
draggable: 'true',
const tab = createElement("div", "tab", {
dataset: { sessionId: sessionInfo.id },
draggable: "true",
});
// Protocol badge
const protoLabel =
sessionInfo.protocol === "github"
? "GitHub"
: sessionInfo.protocol.toUpperCase();
const badge = createElement(
"span",
`tab-badge badge-${sessionInfo.protocol}`,
{
textContent: protoLabel,
},
);
tab.appendChild(badge);
// Name
const name = createElement("span", "tab-name", {
textContent: sessionInfo.tabName || sessionInfo.protocol.toUpperCase(),
});
tab.appendChild(name);
// Status dot
const status = createElement(
"span",
`tab-status status-${sessionInfo.state || "connecting"}`,
{
title: stateLabel(sessionInfo.state),
},
);
tab.appendChild(status);
// Pin indicator
if (sessionInfo.pinned) {
tab.classList.add("tab-pinned");
}
// SFTP toggle button (SSH sessions only)
if (sessionInfo.protocol === "ssh") {
const sftpBtn = createElement("button", "tab-sftp", {
textContent: "📁",
title: "Toggle SFTP file browser",
"aria-label": "Toggle SFTP",
});
// Protocol badge
const protoLabel = sessionInfo.protocol === 'github' ? 'GitHub' : sessionInfo.protocol.toUpperCase();
const badge = createElement('span', `tab-badge badge-${sessionInfo.protocol}`, {
textContent: protoLabel,
sftpBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.app.sftp.toggle(sessionInfo.id);
});
tab.appendChild(badge);
tab.appendChild(sftpBtn);
}
// Name
const name = createElement('span', 'tab-name', {
textContent: sessionInfo.tabName || sessionInfo.protocol.toUpperCase(),
});
tab.appendChild(name);
// Close button
const closeBtn = createElement("button", "tab-close", {
textContent: "×",
"aria-label": "Close tab",
});
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.closeTab(sessionInfo.id);
});
tab.appendChild(closeBtn);
// Status dot
const status = createElement('span', `tab-status status-${sessionInfo.state || 'connecting'}`, {
title: stateLabel(sessionInfo.state),
});
tab.appendChild(status);
// Click → activate
tab.addEventListener("click", () => this.setActive(sessionInfo.id));
// Pin indicator
if (sessionInfo.pinned) {
tab.classList.add('tab-pinned');
}
// Right-click → context menu
tab.addEventListener("contextmenu", (e) => {
e.preventDefault();
this.showContextMenu(sessionInfo.id, e.clientX, e.clientY);
});
// SFTP toggle button (SSH sessions only)
if (sessionInfo.protocol === 'ssh') {
const sftpBtn = createElement('button', 'tab-sftp', {
textContent: '📁',
title: 'Toggle SFTP file browser',
'aria-label': 'Toggle SFTP',
});
sftpBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.app.sftp.toggle(sessionInfo.id);
});
tab.appendChild(sftpBtn);
}
// Close button
const closeBtn = createElement('button', 'tab-close', {
textContent: '×',
'aria-label': 'Close tab',
});
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.closeTab(sessionInfo.id);
});
tab.appendChild(closeBtn);
// Click → activate
tab.addEventListener('click', () => this.setActive(sessionInfo.id));
// Right-click → context menu
tab.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showContextMenu(sessionInfo.id, e.clientX, e.clientY);
});
this.tabList.appendChild(tab);
this.tabList.appendChild(tab);
}
// ── Internal helpers ─────────────────────────────────────────────
@@ -471,20 +528,20 @@ class TabManager {
* @private
*/
_reorderDOM() {
// Sort: pinned first, then by order index
const sorted = [...this.order].sort((a, b) => {
const sa = this.app.state.getSession(a);
const sb = this.app.state.getSession(b);
const pinnedA = sa?.pinned ? 0 : 1;
const pinnedB = sb?.pinned ? 0 : 1;
if (pinnedA !== pinnedB) return pinnedA - pinnedB;
return this.order.indexOf(a) - this.order.indexOf(b);
});
// Sort: pinned first, then by order index
const sorted = [...this.order].sort((a, b) => {
const sa = this.app.state.getSession(a);
const sb = this.app.state.getSession(b);
const pinnedA = sa?.pinned ? 0 : 1;
const pinnedB = sb?.pinned ? 0 : 1;
if (pinnedA !== pinnedB) return pinnedA - pinnedB;
return this.order.indexOf(a) - this.order.indexOf(b);
});
for (const id of sorted) {
const el = this.tabList.querySelector(`[data-session-id="${id}"]`);
if (el) this.tabList.appendChild(el);
}
for (const id of sorted) {
const el = this.tabList.querySelector(`[data-session-id="${id}"]`);
if (el) this.tabList.appendChild(el);
}
}
/**
@@ -493,11 +550,13 @@ class TabManager {
* @private
*/
_updateTabStyles(sessionId) {
const session = this.app.state.getSession(sessionId);
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`);
if (tabEl && session) {
tabEl.classList.toggle('tab-pinned', !!session.pinned);
}
const session = this.app.state.getSession(sessionId);
const tabEl = this.tabList.querySelector(
`[data-session-id="${sessionId}"]`,
);
if (tabEl && session) {
tabEl.classList.toggle("tab-pinned", !!session.pinned);
}
}
/**
@@ -505,10 +564,10 @@ class TabManager {
* @private
*/
_updateEmptyState() {
const empty = document.getElementById('empty-state');
if (empty) {
empty.style.display = this.order.length === 0 ? 'flex' : 'none';
}
const empty = document.getElementById("empty-state");
if (empty) {
empty.style.display = this.order.length === 0 ? "flex" : "none";
}
}
/**
@@ -517,14 +576,14 @@ class TabManager {
* @private
*/
_promptRename(sessionId) {
const session = this.app.state.getSession(sessionId);
const current = session?.tabName || '';
// Use the app's custom prompt modal (window.prompt is unavailable in WKWebView)
this.app._inputPrompt('Rename Tab', 'Tab name', current).then((name) => {
if (name !== null && name.trim()) {
this.renameTab(sessionId, name.trim());
}
});
const session = this.app.state.getSession(sessionId);
const current = session?.tabName || "";
// Use the app's custom prompt modal (window.prompt is unavailable in WKWebView)
this.app._inputPrompt("Rename Tab", "Tab name", current).then((name) => {
if (name !== null && name.trim()) {
this.renameTab(sessionId, name.trim());
}
});
}
/**
@@ -533,17 +592,17 @@ class TabManager {
* @private
*/
async _reconnect(sessionId) {
const session = this.app.state.getSession(sessionId);
if (!session || !session.profileId) return;
const session = this.app.state.getSession(sessionId);
if (!session || !session.profileId) return;
// Close the current tab, then open a fresh connection
this.app.terminals.destroyTerminal(sessionId);
this.app.rawViewer.destroyViewer(sessionId);
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
this.app.state.removeSession(sessionId);
this.removeTab(sessionId);
// Close the current tab, then open a fresh connection
this.app.terminals.destroyTerminal(sessionId);
this.app.rawViewer.destroyViewer(sessionId);
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
this.app.state.removeSession(sessionId);
this.removeTab(sessionId);
await this.app.connectProfile(session.profileId);
await this.app.connectProfile(session.profileId);
}
/**
@@ -553,8 +612,8 @@ class TabManager {
* @private
*/
_tabLabel(sessionId) {
const session = this.app.state.getSession(sessionId);
return session?.tabName || 'Session';
const session = this.app.state.getSession(sessionId);
return session?.tabName || "Session";
}
// ── Drag and drop ────────────────────────────────────────────────
@@ -564,68 +623,71 @@ class TabManager {
* @private
*/
_setupDragAndDrop() {
if (!this.tabList) return;
if (!this.tabList) return;
let draggedId = null;
let draggedId = null;
this.tabList.addEventListener('dragstart', (e) => {
const tab = e.target.closest('.tab');
if (!tab) return;
draggedId = tab.dataset.sessionId;
tab.classList.add('tab-dragging');
e.dataTransfer.effectAllowed = 'move';
});
this.tabList.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('.tab');
if (!target || target.dataset.sessionId === draggedId) return;
const rect = target.getBoundingClientRect();
const midX = rect.left + rect.width / 2;
target.classList.toggle('tab-drop-before', e.clientX < midX);
target.classList.toggle('tab-drop-after', e.clientX >= midX);
});
this.tabList.addEventListener('dragleave', (e) => {
const target = e.target.closest('.tab');
if (target) {
target.classList.remove('tab-drop-before', 'tab-drop-after');
}
});
this.tabList.addEventListener('drop', (e) => {
e.preventDefault();
const target = e.target.closest('.tab');
if (!target || !draggedId) return;
target.classList.remove('tab-drop-before', 'tab-drop-after');
const targetId = target.dataset.sessionId;
if (targetId === draggedId) return;
// Reorder
const fromIdx = this.order.indexOf(draggedId);
const toIdx = this.order.indexOf(targetId);
if (fromIdx === -1 || toIdx === -1) return;
this.order.splice(fromIdx, 1);
this.order.splice(toIdx, 0, draggedId);
this._reorderDOM();
});
this.tabList.addEventListener('dragend', () => {
this.tabList.querySelectorAll('.tab').forEach((t) => {
t.classList.remove('tab-dragging', 'tab-drop-before', 'tab-drop-after');
});
draggedId = null;
this.tabList.addEventListener("dragstart", (e) => {
const tab = e.target.closest(".tab");
if (!tab) return;
draggedId = tab.dataset.sessionId;
tab.classList.add("tab-dragging");
e.dataTransfer.effectAllowed = "move";
});
this.tabList.addEventListener("dragover", (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const target = e.target.closest(".tab");
if (!target || target.dataset.sessionId === draggedId) return;
const rect = target.getBoundingClientRect();
const midX = rect.left + rect.width / 2;
target.classList.toggle("tab-drop-before", e.clientX < midX);
target.classList.toggle("tab-drop-after", e.clientX >= midX);
});
this.tabList.addEventListener("dragleave", (e) => {
const target = e.target.closest(".tab");
if (target) {
target.classList.remove("tab-drop-before", "tab-drop-after");
}
});
this.tabList.addEventListener("drop", (e) => {
e.preventDefault();
const target = e.target.closest(".tab");
if (!target || !draggedId) return;
target.classList.remove("tab-drop-before", "tab-drop-after");
const targetId = target.dataset.sessionId;
if (targetId === draggedId) return;
// Reorder
const fromIdx = this.order.indexOf(draggedId);
const toIdx = this.order.indexOf(targetId);
if (fromIdx === -1 || toIdx === -1) return;
this.order.splice(fromIdx, 1);
this.order.splice(toIdx, 0, draggedId);
this._reorderDOM();
});
this.tabList.addEventListener("dragend", () => {
this.tabList.querySelectorAll(".tab").forEach((t) => {
t.classList.remove(
"tab-dragging",
"tab-drop-before",
"tab-drop-after",
);
});
draggedId = null;
});
}
}
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.TabManager = TabManager;
}
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.TabManager = TabManager;
})();