From 98ca4743e573c3e08e94508bd3ad0a49493fd83c Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Wed, 22 Apr 2026 14:57:01 +0100 Subject: [PATCH] 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. --- README.md | 34 +- docs/quick-start.md | 24 +- frontend/src/css/forms.css | 724 +++++++++++++++---------------- frontend/src/css/terminal.css | 490 ++++++++++----------- frontend/src/js/profiles.js | 39 +- frontend/src/js/tabs.js | 774 ++++++++++++++++++---------------- 6 files changed, 1083 insertions(+), 1002 deletions(-) diff --git a/README.md b/README.md index 7dba755..e151ece 100644 --- a/README.md +++ b/README.md @@ -31,20 +31,20 @@ Prefer a one-off session? Use **Cmd+Shift+Q** / **Ctrl+Shift+Q** to quick connec All shortcuts use **Cmd** on macOS and **Ctrl** on Windows/Linux. -| macOS Shortcut | Windows/Linux Shortcut | Action | -| ------------- | ----------------------- | --------------------------------- | -| Cmd + K | Ctrl + K | Open command palette | -| Cmd + N | Ctrl + N | New connection | -| Cmd + W | Ctrl + W | Close current tab | -| Cmd + Tab | Ctrl + Tab | Next tab | -| Cmd + Shift + Tab | Ctrl + Shift + Tab | Previous tab | -| Cmd + 1–9 | Ctrl + 1–9 | Switch to tab by number | -| Cmd + , | Ctrl + , | Open settings | -| Cmd + F | Ctrl + F | Find in terminal | -| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | -| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | -| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | -| Cmd + Shift + B | Ctrl + Shift + B | Toggle broadcast input mode | -| Cmd + Shift + S | Ctrl + Shift + S | Open snippet library | -| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | -| F11 | F11 | Toggle fullscreen | +| macOS Shortcut | Windows/Linux Shortcut | Action | +| ----------------- | ---------------------- | --------------------------- | +| Cmd + K | Ctrl + K | Open command palette | +| Cmd + N | Ctrl + N | New connection | +| Cmd + W | Ctrl + W | Close current tab | +| Cmd + Tab | Ctrl + Tab | Next tab | +| Cmd + Shift + Tab | Ctrl + Shift + Tab | Previous tab | +| Cmd + 1–9 | Ctrl + 1–9 | Switch to tab by number | +| Cmd + , | Ctrl + , | Open settings | +| Cmd + F | Ctrl + F | Find in terminal | +| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | +| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | +| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | +| Cmd + Shift + B | Ctrl + Shift + B | Toggle broadcast input mode | +| Cmd + Shift + S | Ctrl + Shift + S | Open snippet library | +| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | +| F11 | F11 | Toggle fullscreen | diff --git a/docs/quick-start.md b/docs/quick-start.md index 55502e6..2a60e3e 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -42,18 +42,18 @@ The upload picker lists folders only and marks permissions: ## Common shortcuts -| macOS Shortcut | Windows/Linux Shortcut | Action | -| ------------- | ----------------------- | ------------------------ | -| Cmd + K | Ctrl + K | Command palette | -| Cmd + N | Ctrl + N | New connection | -| Cmd + W | Ctrl + W | Close tab | -| Cmd + F | Ctrl + F | Find in terminal | -| Cmd + Shift + S | Ctrl + Shift + S | Snippet library | -| Cmd + Shift + B | Ctrl + Shift + B | Broadcast input | -| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | -| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | -| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | -| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | +| macOS Shortcut | Windows/Linux Shortcut | Action | +| --------------- | ---------------------- | ------------------------ | +| Cmd + K | Ctrl + K | Command palette | +| Cmd + N | Ctrl + N | New connection | +| Cmd + W | Ctrl + W | Close tab | +| Cmd + F | Ctrl + F | Find in terminal | +| Cmd + Shift + S | Ctrl + Shift + S | Snippet library | +| Cmd + Shift + B | Ctrl + Shift + B | Broadcast input | +| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | +| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | +| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | +| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | ## Need more detail? diff --git a/frontend/src/css/forms.css b/frontend/src/css/forms.css index 1aa68a3..f971625 100644 --- a/frontend/src/css/forms.css +++ b/frontend/src/css/forms.css @@ -8,132 +8,132 @@ Form panel (profile editor container) ----------------------------------------------------------------------- */ .form-panel { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; } .form-panel-header { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3) var(--space-4); - border-bottom: 1px solid var(--border-subtle); - flex-shrink: 0; + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; } .form-panel-title { - font-size: var(--text-md); - font-weight: var(--weight-semibold); - color: var(--text-primary); - flex: 1; + font-size: var(--text-md); + font-weight: var(--weight-semibold); + color: var(--text-primary); + flex: 1; } .form-panel-body { - flex: 1; - overflow-y: auto; - padding: var(--space-4); + flex: 1; + overflow-y: auto; + padding: var(--space-4); } .form-panel-footer { - display: flex; - align-items: center; - justify-content: flex-end; - gap: var(--space-2); - padding: var(--space-3) var(--space-4); - border-top: 1px solid var(--border-subtle); - flex-shrink: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; } /* ----------------------------------------------------------------------- Form sections (disclosure-based grouping) ----------------------------------------------------------------------- */ .form-section { - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - margin-bottom: var(--space-4); - overflow: hidden; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + margin-bottom: var(--space-4); + overflow: hidden; } .form-section-header { - display: flex; - align-items: center; - gap: var(--space-2); - width: 100%; - padding: var(--space-3) var(--space-4); - font-size: var(--text-sm); - font-weight: var(--weight-semibold); - color: var(--text-primary); - cursor: pointer; - user-select: none; - -webkit-user-select: none; - background: var(--bg-elevated); - border: none; - cursor: pointer; - user-select: none; - text-align: left; - transition: background var(--transition-fast); + display: flex; + align-items: center; + gap: var(--space-2); + width: 100%; + padding: var(--space-3) var(--space-4); + font-size: var(--text-sm); + font-weight: var(--weight-semibold); + color: var(--text-primary); + cursor: pointer; + user-select: none; + -webkit-user-select: none; + background: var(--bg-elevated); + border: none; + cursor: pointer; + user-select: none; + text-align: left; + transition: background var(--transition-fast); } .form-section-header:hover { - background: var(--bg-hover); + background: var(--bg-hover); } .form-section-chevron { - width: 14px; - height: 14px; - color: var(--text-tertiary); - transition: transform var(--transition-normal); - flex-shrink: 0; + width: 14px; + height: 14px; + color: var(--text-tertiary); + transition: transform var(--transition-normal); + flex-shrink: 0; } .form-section.is-open .form-section-chevron { - transform: rotate(90deg); + transform: rotate(90deg); } .form-section-icon { - width: 16px; - text-align: center; - flex-shrink: 0; - font-size: var(--text-sm); - color: var(--text-tertiary); + width: 16px; + text-align: center; + flex-shrink: 0; + font-size: var(--text-sm); + color: var(--text-tertiary); } .form-section-body, .form-section-content { - padding: var(--space-4); - display: none; + padding: var(--space-4); + display: none; } .form-section.is-open .form-section-body, .form-section.is-open .form-section-content { - display: block; + display: block; } /* ----------------------------------------------------------------------- Field grid (two-column layout for compact forms) ----------------------------------------------------------------------- */ .form-fields { - display: flex; - flex-direction: column; - gap: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-3); } .form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-3); + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); } /* Single-column override */ .form-row.full-width { - grid-template-columns: 1fr; + grid-template-columns: 1fr; } /* Three-column for port + protocol combos */ .form-row.triple { - grid-template-columns: 1fr 100px 120px; + grid-template-columns: 1fr 100px 120px; } /* ----------------------------------------------------------------------- @@ -143,14 +143,14 @@ .proto-only-ssh, .proto-only-telnet, .proto-only-raw { - display: none; + display: none; } .protocol-ssh .proto-only-ssh, .protocol-telnet .proto-only-telnet, .protocol-raw .proto-only-raw, .protocol-github .proto-only-github { - display: flex; + display: flex; } /* For block-level protocol fields */ @@ -158,415 +158,423 @@ .protocol-telnet .proto-only-telnet.form-section, .protocol-raw .proto-only-raw.form-section, .protocol-github .proto-only-github.form-section { - display: block; + display: block; } /* ----------------------------------------------------------------------- Tag input ----------------------------------------------------------------------- */ .tag-input-wrapper { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-1); - min-height: 30px; - padding: var(--space-1) var(--space-2); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - cursor: text; - transition: border-color var(--transition-fast); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-1); + min-height: 30px; + padding: var(--space-1) var(--space-2); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + cursor: text; + transition: border-color var(--transition-fast); } .tag-input-wrapper:hover { - border-color: var(--border-strong); + border-color: var(--border-strong); } .tag-input-wrapper:focus-within { - border-color: var(--accent); - box-shadow: 0 0 0 2px var(--accent-muted); + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-muted); } .tag-item { - display: inline-flex; - align-items: center; - gap: 3px; - height: 20px; - padding: 0 var(--space-2); - font-size: var(--text-xs); - color: var(--accent); - background: var(--accent-muted); - border-radius: var(--radius-sm); - white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 3px; + height: 20px; + padding: 0 var(--space-2); + font-size: var(--text-xs); + color: var(--accent); + background: var(--accent-muted); + border-radius: var(--radius-sm); + white-space: nowrap; } .tag-item-remove { - display: flex; - align-items: center; - justify-content: center; - width: 12px; - height: 12px; - font-size: 10px; - cursor: pointer; - border-radius: 50%; - color: var(--accent); - transition: background var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + font-size: 10px; + cursor: pointer; + border-radius: 50%; + color: var(--accent); + transition: background var(--transition-fast); } .tag-item-remove:hover { - background: var(--accent-strong); + background: var(--accent-strong); } .tag-input { - flex: 1; - min-width: 60px; - border: none; - background: transparent; - font-size: var(--text-sm); - color: var(--text-primary); - outline: none; - height: 22px; + flex: 1; + min-width: 60px; + border: none; + background: transparent; + font-size: var(--text-sm); + color: var(--text-primary); + outline: none; + height: 22px; } .tag-input::placeholder { - color: var(--text-disabled); + color: var(--text-disabled); } /* ----------------------------------------------------------------------- Jump host / proxy list ----------------------------------------------------------------------- */ .jump-host-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .jump-host-item { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - background: var(--bg-input); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); - font-size: var(--text-sm); + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + font-size: var(--text-sm); } .jump-host-item .jump-host-order { - font-size: var(--text-xs); - color: var(--text-disabled); - min-width: 16px; - text-align: center; + font-size: var(--text-xs); + color: var(--text-disabled); + min-width: 16px; + text-align: center; } .jump-host-item .jump-host-value { - flex: 1; - font-family: var(--font-mono); - color: var(--text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + flex: 1; + font-family: var(--font-mono); + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .jump-host-item .jump-host-actions { - display: flex; - gap: var(--space-1); - opacity: 0; - transition: opacity var(--transition-fast); + display: flex; + gap: var(--space-1); + opacity: 0; + transition: opacity var(--transition-fast); } .jump-host-item:hover .jump-host-actions { - opacity: 1; + opacity: 1; } .jump-host-add { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2); - font-size: var(--text-sm); - color: var(--text-tertiary); - border: 1px dashed var(--border-default); - border-radius: var(--radius-md); - cursor: pointer; - transition: border-color var(--transition-fast), color var(--transition-fast); + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2); + font-size: var(--text-sm); + color: var(--text-tertiary); + border: 1px dashed var(--border-default); + border-radius: var(--radius-md); + cursor: pointer; + transition: + border-color var(--transition-fast), + color var(--transition-fast); } .jump-host-add:hover { - border-color: var(--accent); - color: var(--accent); + border-color: var(--accent); + color: var(--accent); } /* ----------------------------------------------------------------------- Key file selector row ----------------------------------------------------------------------- */ .file-selector { - display: flex; - gap: var(--space-2); + display: flex; + gap: var(--space-2); } .file-selector .form-input { - flex: 1; + flex: 1; } .file-selector .btn { - flex-shrink: 0; + flex-shrink: 0; } /* File path display */ .file-path { - font-family: var(--font-mono); - font-size: var(--text-xs); - color: var(--text-tertiary); - background: var(--bg-input); - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-sm); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-tertiary); + background: var(--bg-input); + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* ----------------------------------------------------------------------- Inline validation ----------------------------------------------------------------------- */ .form-group.is-valid .form-input { - border-color: var(--success); + border-color: var(--success); } .form-group.is-valid .form-input:focus { - box-shadow: 0 0 0 2px rgba(102, 187, 106, 0.20); + box-shadow: 0 0 0 2px rgba(102, 187, 106, 0.2); } .form-group.is-invalid .form-input { - border-color: var(--error); + border-color: var(--error); } .form-group.is-invalid .form-input:focus { - box-shadow: 0 0 0 2px rgba(239, 83, 80, 0.20); + box-shadow: 0 0 0 2px rgba(239, 83, 80, 0.2); } .form-validation-icon { - position: absolute; - right: var(--space-3); - top: 50%; - transform: translateY(-50%); - font-size: var(--text-sm); + position: absolute; + right: var(--space-3); + top: 50%; + transform: translateY(-50%); + font-size: var(--text-sm); } -.form-group.is-valid .form-validation-icon { color: var(--success); } -.form-group.is-invalid .form-validation-icon { color: var(--error); } +.form-group.is-valid .form-validation-icon { + color: var(--success); +} +.form-group.is-invalid .form-validation-icon { + color: var(--error); +} /* ----------------------------------------------------------------------- Help text under sections ----------------------------------------------------------------------- */ .form-section-help { - font-size: var(--text-xs); - color: var(--text-tertiary); - padding: 0 var(--space-4) var(--space-3); - line-height: var(--leading-relaxed); + font-size: var(--text-xs); + color: var(--text-tertiary); + padding: 0 var(--space-4) var(--space-3); + line-height: var(--leading-relaxed); } /* ----------------------------------------------------------------------- Colour preview (for terminal colour settings) ----------------------------------------------------------------------- */ .colour-preview { - display: inline-block; - width: 18px; - height: 18px; - border-radius: var(--radius-sm); - border: 1px solid var(--border-default); - vertical-align: middle; - cursor: pointer; + display: inline-block; + width: 18px; + height: 18px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-default); + vertical-align: middle; + cursor: pointer; } /* ----------------------------------------------------------------------- Number input with stepper ----------------------------------------------------------------------- */ .form-number { - display: flex; - align-items: center; + display: flex; + align-items: center; } .form-number .form-input { - border-radius: var(--radius-md) 0 0 var(--radius-md); - text-align: center; - width: 60px; - /* Hide browser spinners */ - -moz-appearance: textfield; + border-radius: var(--radius-md) 0 0 var(--radius-md); + text-align: center; + width: 60px; + /* Hide browser spinners */ + -moz-appearance: textfield; } .form-number .form-input::-webkit-inner-spin-button, .form-number .form-input::-webkit-outer-spin-button { - appearance: none; + appearance: none; } .form-number-btn { - width: 24px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-sm); - color: var(--text-secondary); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-left: none; - cursor: pointer; - transition: background var(--transition-fast), color var(--transition-fast); + width: 24px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-sm); + color: var(--text-secondary); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-left: none; + cursor: pointer; + transition: + background var(--transition-fast), + color var(--transition-fast); } .form-number-btn:last-child { - border-radius: 0 var(--radius-md) var(--radius-md) 0; + border-radius: 0 var(--radius-md) var(--radius-md) 0; } .form-number-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); + background: var(--bg-hover); + color: var(--text-primary); } /* ----------------------------------------------------------------------- Form action row (test connection, etc.) ----------------------------------------------------------------------- */ .form-actions-inline { - display: flex; - align-items: center; - gap: var(--space-2); - padding-top: var(--space-3); - border-top: 1px solid var(--border-subtle); - margin-top: var(--space-3); + display: flex; + align-items: center; + gap: var(--space-2); + padding-top: var(--space-3); + border-top: 1px solid var(--border-subtle); + margin-top: var(--space-3); } .form-actions-inline .btn { - flex-shrink: 0; + flex-shrink: 0; } .form-actions-inline .form-actions-status { - font-size: var(--text-xs); - color: var(--text-tertiary); - flex: 1; + font-size: var(--text-xs); + color: var(--text-tertiary); + flex: 1; } /* ----------------------------------------------------------------------- Form hint — inline help text below fields ----------------------------------------------------------------------- */ .form-hint { - font-size: var(--text-xs); - color: var(--text-tertiary); - margin-top: var(--space-1); - line-height: var(--leading-relaxed); + font-size: var(--text-xs); + color: var(--text-tertiary); + margin-top: var(--space-1); + line-height: var(--leading-relaxed); } .form-hint.secret-error { - color: var(--danger); + color: var(--danger); } .form-success { - font-size: var(--text-xs); - color: var(--success); - margin-top: var(--space-1); - line-height: var(--leading-relaxed); + font-size: var(--text-xs); + color: var(--success); + margin-top: var(--space-1); + line-height: var(--leading-relaxed); } /* ----------------------------------------------------------------------- Toggle wrapper — label + toggle switch in a row ----------------------------------------------------------------------- */ .form-toggle-wrapper { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - padding: var(--space-1) 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-1) 0; } .form-toggle-label { - font-size: var(--text-sm); - color: var(--text-secondary); - flex: 1; + font-size: var(--text-sm); + color: var(--text-secondary); + flex: 1; } /* ----------------------------------------------------------------------- Jump host rows ----------------------------------------------------------------------- */ .jump-hosts-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .jump-host-row { - display: flex; - flex-direction: column; - gap: var(--space-1); - padding: var(--space-2); - background: var(--surface-2); - border-radius: var(--radius-sm); - border: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: var(--space-1); + padding: var(--space-2); + background: var(--surface-2); + border-radius: var(--radius-sm); + border: 1px solid var(--border); } .jump-host-fields { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } /* ----------------------------------------------------------------------- Port-forwarding rows ----------------------------------------------------------------------- */ .port-forwards-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .port-forward-row { - display: flex; - flex-direction: column; - gap: var(--space-1); - padding: var(--space-2); - background: var(--surface-2); - border-radius: var(--radius-sm); - border: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: var(--space-1); + padding: var(--space-2); + background: var(--surface-2); + border-radius: var(--radius-sm); + border: 1px solid var(--border); } .port-forward-fields { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } .port-forward-arrow { - color: var(--text-muted); - font-size: var(--text-sm); - flex-shrink: 0; + color: var(--text-muted); + font-size: var(--text-sm); + flex-shrink: 0; } .port-forward-enabled-label { - display: flex; - gap: var(--space-1); - align-items: center; - font-size: var(--text-xs); - color: var(--text-secondary); - white-space: nowrap; - cursor: pointer; + display: flex; + gap: var(--space-1); + align-items: center; + font-size: var(--text-xs); + color: var(--text-secondary); + white-space: nowrap; + cursor: pointer; } .port-forward-enabled-label input[type="checkbox"] { - accent-color: var(--accent); + accent-color: var(--accent); } /* ----------------------------------------------------------------------- Payload preset rows ----------------------------------------------------------------------- */ .payload-presets-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .preset-row { - display: grid; - grid-template-columns: 120px 1fr 100px auto; - gap: var(--space-2); - align-items: center; + display: grid; + grid-template-columns: 120px 1fr 100px auto; + gap: var(--space-2); + align-items: center; } /* ----------------------------------------------------------------------- @@ -574,152 +582,154 @@ ----------------------------------------------------------------------- */ .form-input-sm, .form-select-sm { - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } /* ----------------------------------------------------------------------- Login Script Editor — expect-style send/wait steps ----------------------------------------------------------------------- */ .form-help { - font-size: var(--text-xs); - color: var(--text-muted); - margin-bottom: var(--space-3); - line-height: 1.5; + font-size: var(--text-xs); + color: var(--text-muted); + margin-bottom: var(--space-3); + line-height: 1.5; } .login-script-list { - display: flex; - flex-direction: column; - gap: var(--space-2); - margin-bottom: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-2); + margin-bottom: var(--space-3); } .login-script-empty { - font-size: var(--text-xs); - color: var(--text-muted); - font-style: italic; - padding: var(--space-2) 0; + font-size: var(--text-xs); + color: var(--text-muted); + font-style: italic; + padding: var(--space-2) 0; } .login-script-row { - display: flex; - align-items: center; - gap: var(--space-2); - background: var(--bg-base); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: var(--space-2); + display: flex; + align-items: center; + gap: var(--space-2); + background: var(--bg-base); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: var(--space-2); } .login-script-type { - width: 90px; - min-width: 90px; - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + width: 90px; + min-width: 90px; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } .login-script-value { - flex: 1; - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + flex: 1; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } .login-script-timeout { - width: 60px; - min-width: 60px; - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + width: 60px; + min-width: 60px; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } .login-script-timeout-label { - font-size: var(--text-xs); - color: var(--text-muted); - min-width: 10px; + font-size: var(--text-xs); + color: var(--text-muted); + min-width: 10px; } .login-script-actions { - display: flex; - gap: 2px; - flex-shrink: 0; + display: flex; + gap: 2px; + flex-shrink: 0; } .login-script-btn { - background: var(--bg-elevated); - color: var(--text-secondary); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-xs); - width: 24px; - height: 24px; - font-size: 10px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s, color 0.15s; + background: var(--bg-elevated); + color: var(--text-secondary); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-xs); + width: 24px; + height: 24px; + font-size: 10px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: + background 0.15s, + color 0.15s; } .login-script-btn:hover { - background: var(--bg-surface); - color: var(--text-primary); + background: var(--bg-surface); + color: var(--text-primary); } .login-script-btn-remove:hover { - background: var(--danger); - color: #fff; + background: var(--danger); + color: #fff; } .login-script-add-row { - display: flex; - gap: var(--space-2); + display: flex; + gap: var(--space-2); } /* ----------------------------------------------------------------------- TOTP / 2FA — secret input row with action buttons ----------------------------------------------------------------------- */ .totp-input-row { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } .totp-input-row .form-input { - flex: 1; + flex: 1; } /* SSH key path row */ .key-path-row { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } .key-path-row .form-input { - flex: 1; + flex: 1; } .jump-hosts-list .form-error { - margin-bottom: var(--space-2); + margin-bottom: var(--space-2); } .totp-toggle, .totp-test, .totp-save, .totp-delete { - flex-shrink: 0; - white-space: nowrap; + flex-shrink: 0; + white-space: nowrap; } .btn-danger { - background: var(--error); - color: #fff; - border: 1px solid transparent; + background: var(--error); + color: #fff; + border: 1px solid transparent; } .btn-danger:hover { - background: #d32f2f; + background: #d32f2f; } diff --git a/frontend/src/css/terminal.css b/frontend/src/css/terminal.css index 51cb3cd..437afad 100644 --- a/frontend/src/css/terminal.css +++ b/frontend/src/css/terminal.css @@ -7,10 +7,10 @@ Terminal wrapper — fills the workspace panel ----------------------------------------------------------------------- */ .terminal-wrapper { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; } /* ----------------------------------------------------------------------- @@ -18,223 +18,227 @@ Shows connection info above the terminal. ----------------------------------------------------------------------- */ .terminal-header { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-1) var(--space-3); - font-size: var(--text-xs); - color: var(--text-tertiary); - background: var(--bg-surface); - border-bottom: 1px solid var(--border-subtle); - flex-shrink: 0; - min-height: 24px; + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-1) var(--space-3); + font-size: var(--text-xs); + color: var(--text-tertiary); + background: var(--bg-surface); + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; + min-height: 24px; } .terminal-header-label { - font-weight: var(--weight-medium); - color: var(--text-secondary); + font-weight: var(--weight-medium); + color: var(--text-secondary); } .terminal-header-value { - font-family: var(--font-mono); - color: var(--text-tertiary); + font-family: var(--font-mono); + color: var(--text-tertiary); } .terminal-header-separator { - width: 1px; - height: 10px; - background: var(--border-subtle); + width: 1px; + height: 10px; + background: var(--border-subtle); } .terminal-header-spacer { - flex: 1; + flex: 1; } .terminal-header-actions { - display: flex; - gap: var(--space-1); + display: flex; + gap: var(--space-1); } /* ----------------------------------------------------------------------- xterm.js container ----------------------------------------------------------------------- */ .terminal-container { - flex: 1; - position: relative; - overflow: hidden; - padding: 0; + flex: 1; + position: relative; + overflow: hidden; + padding: 0; } /* ----------------------------------------------------------------------- Disconnected overlay ----------------------------------------------------------------------- */ .session-panel.is-disconnected > *:not(.terminal-disconnect-overlay) { - filter: blur(2px); - pointer-events: none; + filter: blur(2px); + pointer-events: none; } .terminal-disconnect-overlay { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background: rgba(10, 14, 18, 0.68); - z-index: 6; - opacity: 0; - visibility: hidden; - transition: opacity var(--transition-fast), visibility var(--transition-fast); + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(10, 14, 18, 0.68); + z-index: 6; + opacity: 0; + visibility: hidden; + transition: + opacity var(--transition-fast), + visibility var(--transition-fast); } .terminal-disconnect-overlay.is-visible { - opacity: 1; - visibility: visible; + opacity: 1; + visibility: visible; } .terminal-disconnect-content { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-3); - text-align: center; - padding: var(--space-4) var(--space-5); - background: var(--bg-elevated); - border: 1px solid var(--border-default); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); + text-align: center; + padding: var(--space-4) var(--space-5); + background: var(--bg-elevated); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); } .terminal-disconnect-title { - font-size: var(--text-lg); - letter-spacing: 0.45em; - text-indent: 0.45em; - text-transform: uppercase; - color: var(--text-primary); - font-weight: var(--weight-semibold); + font-size: var(--text-lg); + letter-spacing: 0.45em; + text-indent: 0.45em; + text-transform: uppercase; + color: var(--text-primary); + font-weight: var(--weight-semibold); } .terminal-disconnect-message { - font-size: var(--text-sm); - color: var(--text-secondary); - max-width: 360px; + font-size: var(--text-sm); + color: var(--text-secondary); + max-width: 360px; } /* xterm.js overrides for consistent look */ .terminal-container .xterm, .session-panel .xterm { - flex: 1; - height: 100%; - min-height: 0; - padding: 3px 0 0 3px; + flex: 1; + height: 100%; + min-height: 0; + padding: 3px 0 0 3px; } .terminal-container .xterm-screen, .session-panel .xterm-screen { - height: 100%; + height: 100%; } .terminal-container .xterm-viewport, .session-panel .xterm-viewport { - /* Allow xterm's own scrollbar to work */ - overflow-y: auto !important; + /* Allow xterm's own scrollbar to work */ + overflow-y: auto !important; } .terminal-container .xterm-viewport::-webkit-scrollbar, .session-panel .xterm-viewport::-webkit-scrollbar { - width: 6px; + width: 6px; } .terminal-container .xterm-viewport::-webkit-scrollbar-track, .session-panel .xterm-viewport::-webkit-scrollbar-track { - background: transparent; + background: transparent; } .terminal-container .xterm-viewport::-webkit-scrollbar-thumb, .session-panel .xterm-viewport::-webkit-scrollbar-thumb { - background: var(--border-strong); - border-radius: var(--radius-full); + background: var(--border-strong); + border-radius: var(--radius-full); } .terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover, .session-panel .xterm-viewport::-webkit-scrollbar-thumb:hover { - background: var(--text-tertiary); + background: var(--text-tertiary); } /* Ensure cursor blink is visible */ .terminal-container .xterm-cursor-layer, .session-panel .xterm-cursor-layer { - z-index: 1; + z-index: 1; } /* Selection styling within terminal */ .terminal-container .xterm .xterm-selection div, .session-panel .xterm .xterm-selection div { - background: var(--accent-strong) !important; + background: var(--accent-strong) !important; } /* ----------------------------------------------------------------------- Terminal search overlay ----------------------------------------------------------------------- */ .terminal-search { - position: absolute; - top: var(--space-2); - right: var(--space-4); - display: flex; - align-items: center; - gap: var(--space-1); - padding: var(--space-1); - background: var(--bg-overlay); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - box-shadow: var(--shadow-md); - z-index: 10; - opacity: 0; - visibility: hidden; - transform: translateY(-8px); - transition: - opacity var(--transition-fast), - visibility var(--transition-fast), - transform var(--transition-fast); + position: absolute; + top: var(--space-2); + right: var(--space-4); + display: flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-1); + background: var(--bg-overlay); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + z-index: 10; + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + transition: + opacity var(--transition-fast), + visibility var(--transition-fast), + transform var(--transition-fast); } .terminal-search.is-visible { - opacity: 1; - visibility: visible; - transform: translateY(0); + opacity: 1; + visibility: visible; + transform: translateY(0); } .terminal-search input { - width: 200px; - height: 24px; - padding: 0 var(--space-2); - font-size: var(--text-sm); - font-family: var(--font-mono); - color: var(--text-primary); - background: var(--bg-input); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + width: 200px; + height: 24px; + padding: 0 var(--space-2); + font-size: var(--text-sm); + font-family: var(--font-mono); + color: var(--text-primary); + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); } .terminal-search input:focus { - border-color: var(--accent); - outline: none; + border-color: var(--accent); + outline: none; } .terminal-search-count { - font-size: var(--text-xs); - color: var(--text-tertiary); - padding: 0 var(--space-1); - white-space: nowrap; - min-width: 70px; + font-size: var(--text-xs); + color: var(--text-tertiary); + padding: 0 var(--space-1); + white-space: nowrap; + min-width: 70px; } .terminal-search-toggle { - font-family: var(--font-mono); - font-size: var(--text-xs); - opacity: 0.5; - transition: opacity var(--transition-fast), color var(--transition-fast); + font-family: var(--font-mono); + font-size: var(--text-xs); + opacity: 0.5; + transition: + opacity var(--transition-fast), + color var(--transition-fast); } .terminal-search-toggle.is-active { - opacity: 1; - color: var(--accent); + opacity: 1; + color: var(--accent); } /* ----------------------------------------------------------------------- @@ -243,231 +247,233 @@ /* RAW text view — scrollable, monospace output */ .raw-text-view { - flex: 1; - overflow-y: auto; - padding: var(--space-3); - font-family: var(--font-mono); - font-size: var(--text-sm); - line-height: var(--leading-relaxed); - color: var(--text-primary); - background: var(--bg-base); - white-space: pre-wrap; - word-break: break-all; + flex: 1; + overflow-y: auto; + padding: var(--space-3); + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: var(--leading-relaxed); + color: var(--text-primary); + background: var(--bg-base); + white-space: pre-wrap; + word-break: break-all; } /* RAW hex view */ .raw-hex-view { - flex: 1; - overflow-y: auto; - padding: var(--space-3); - font-family: var(--font-mono); - font-size: var(--text-xs); - line-height: var(--leading-relaxed); - color: var(--text-primary); - background: var(--bg-base); + flex: 1; + overflow-y: auto; + padding: var(--space-3); + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: var(--leading-relaxed); + color: var(--text-primary); + background: var(--bg-base); } .raw-hex-row { - display: flex; - gap: var(--space-4); + display: flex; + gap: var(--space-4); } .raw-hex-offset { - color: var(--text-tertiary); - min-width: 60px; + color: var(--text-tertiary); + min-width: 60px; } .raw-hex-bytes { - min-width: 350px; - letter-spacing: 0.08em; + min-width: 350px; + letter-spacing: 0.08em; } .raw-hex-bytes .byte-highlight { - color: var(--accent); + color: var(--accent); } .raw-hex-ascii { - color: var(--text-secondary); + color: var(--text-secondary); } .raw-hex-ascii .non-printable { - color: var(--text-disabled); + color: var(--text-disabled); } /* RAW split view (text on left, hex on right) */ .raw-split-view { - flex: 1; - display: flex; - overflow: hidden; + flex: 1; + display: flex; + overflow: hidden; } .raw-split-view .raw-text-view, .raw-split-view .raw-hex-view { - flex: 1; + flex: 1; } .raw-split-divider { - width: 1px; - background: var(--border-subtle); - flex-shrink: 0; + width: 1px; + background: var(--border-subtle); + flex-shrink: 0; } /* ----------------------------------------------------------------------- RAW mode view toggle buttons ----------------------------------------------------------------------- */ .raw-view-toggle { - display: flex; - gap: 1px; - background: var(--border-subtle); - border-radius: var(--radius-md); - overflow: hidden; + display: flex; + gap: 1px; + background: var(--border-subtle); + border-radius: var(--radius-md); + overflow: hidden; } .raw-view-toggle-btn { - padding: 0 var(--space-2); - height: 22px; - font-size: var(--text-xs); - font-weight: var(--weight-medium); - color: var(--text-tertiary); - background: var(--bg-surface); - border: none; - cursor: pointer; - transition: background var(--transition-fast), color var(--transition-fast); + padding: 0 var(--space-2); + height: 22px; + font-size: var(--text-xs); + font-weight: var(--weight-medium); + color: var(--text-tertiary); + background: var(--bg-surface); + border: none; + cursor: pointer; + transition: + background var(--transition-fast), + color var(--transition-fast); } .raw-view-toggle-btn:hover { - color: var(--text-secondary); + color: var(--text-secondary); } .raw-view-toggle-btn.active { - background: var(--bg-active); - color: var(--text-primary); + background: var(--bg-active); + color: var(--text-primary); } /* ----------------------------------------------------------------------- Send bar (RAW TCP mode — manual data entry) ----------------------------------------------------------------------- */ .send-bar { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - background: var(--bg-surface); - border-top: 1px solid var(--border-subtle); - flex-shrink: 0; + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + background: var(--bg-surface); + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; } .send-bar-input { - flex: 1; - height: 28px; - padding: 0 var(--space-3); - font-family: var(--font-mono); - font-size: var(--text-sm); - color: var(--text-primary); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - transition: border-color var(--transition-fast); + flex: 1; + height: 28px; + padding: 0 var(--space-3); + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--text-primary); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + transition: border-color var(--transition-fast); } .send-bar-input:focus { - border-color: var(--accent); - outline: none; + border-color: var(--accent); + outline: none; } .send-bar-input::placeholder { - color: var(--text-disabled); + color: var(--text-disabled); } /* Line ending selector */ .send-bar-eol { - height: 28px; - padding: 0 var(--space-2); - font-size: var(--text-xs); - color: var(--text-secondary); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - appearance: none; - cursor: pointer; - min-width: 60px; + height: 28px; + padding: 0 var(--space-2); + font-size: var(--text-xs); + color: var(--text-secondary); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + appearance: none; + cursor: pointer; + min-width: 60px; } .send-bar-eol:focus { - border-color: var(--accent); - outline: none; + border-color: var(--accent); + outline: none; } /* Encoding selector */ .send-bar-encoding { - font-size: var(--text-xs); - color: var(--text-tertiary); - padding: 0 var(--space-1); + font-size: var(--text-xs); + color: var(--text-tertiary); + padding: 0 var(--space-1); } /* ----------------------------------------------------------------------- Session overlay (connecting / error states) ----------------------------------------------------------------------- */ .session-overlay { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: rgba(28, 28, 28, 0.85); - z-index: 5; + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(28, 28, 28, 0.85); + z-index: 5; } .session-overlay-connecting { - text-align: center; + text-align: center; } .session-overlay-connecting .spinner { - width: 24px; - height: 24px; - margin: 0 auto var(--space-4); + width: 24px; + height: 24px; + margin: 0 auto var(--space-4); } .session-overlay-connecting p { - font-size: var(--text-sm); - color: var(--text-secondary); + font-size: var(--text-sm); + color: var(--text-secondary); } .session-overlay-error { - text-align: center; - max-width: 360px; + text-align: center; + max-width: 360px; } .session-overlay-error h3 { - font-size: var(--text-md); - font-weight: var(--weight-semibold); - color: var(--error); - margin-bottom: var(--space-2); + font-size: var(--text-md); + font-weight: var(--weight-semibold); + color: var(--error); + margin-bottom: var(--space-2); } .session-overlay-error p { - font-size: var(--text-sm); - color: var(--text-tertiary); - line-height: var(--leading-relaxed); - margin-bottom: var(--space-4); + font-size: var(--text-sm); + color: var(--text-tertiary); + line-height: var(--leading-relaxed); + margin-bottom: var(--space-4); } .session-overlay-error .session-error-detail { - font-family: var(--font-mono); - font-size: var(--text-xs); - color: var(--text-disabled); - background: var(--bg-input); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-md); - margin-bottom: var(--space-4); - text-align: left; - max-height: 120px; - overflow-y: auto; - word-break: break-all; + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-disabled); + background: var(--bg-input); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + margin-bottom: var(--space-4); + text-align: left; + max-height: 120px; + overflow-y: auto; + word-break: break-all; } .session-overlay-actions { - display: flex; - gap: var(--space-2); + display: flex; + gap: var(--space-2); } diff --git a/frontend/src/js/profiles.js b/frontend/src/js/profiles.js index c84db8d..b9d54f5 100644 --- a/frontend/src/js/profiles.js +++ b/frontend/src/js/profiles.js @@ -1659,7 +1659,9 @@ `; - const keyInput = row.querySelector(`input[name="jumpHost-${i}-privateKeyPath"]`); + const keyInput = row.querySelector( + `input[name="jumpHost-${i}-privateKeyPath"]`, + ); const browseBtn = row.querySelector(".jump-host-browse"); const error = createElement("span", "form-error"); error.style.display = "none"; @@ -1712,7 +1714,9 @@ _attachKeyPathValidation(input, errorEl, successEl) { if (!input || !errorEl) return; - input.addEventListener("blur", () => this._validateKeyPath(input, errorEl, successEl)); + input.addEventListener("blur", () => + this._validateKeyPath(input, errorEl, successEl), + ); input.addEventListener("input", () => { errorEl.style.display = "none"; errorEl.textContent = ""; @@ -1757,24 +1761,23 @@ } return; } - return window.go.api.Service.FileIsPrivateKey(value) - .then((isKey) => { - if (!isKey) { - errorEl.textContent = "File is not a recognised private key."; - errorEl.style.display = "block"; - if (successEl) { - successEl.style.display = "none"; - successEl.textContent = ""; - } - return; - } - errorEl.style.display = "none"; - errorEl.textContent = ""; + return window.go.api.Service.FileIsPrivateKey(value).then((isKey) => { + if (!isKey) { + errorEl.textContent = "File is not a recognised private key."; + errorEl.style.display = "block"; if (successEl) { - successEl.textContent = "Key verified."; - successEl.style.display = "block"; + successEl.style.display = "none"; + successEl.textContent = ""; } - }); + return; + } + errorEl.style.display = "none"; + errorEl.textContent = ""; + if (successEl) { + successEl.textContent = "Key verified."; + successEl.style.display = "block"; + } + }); }) .catch(() => { errorEl.textContent = "Could not verify this path."; diff --git a/frontend/src/js/tabs.js b/frontend/src/js/tabs.js index cf27a3a..04bae0d 100644 --- a/frontend/src/js/tabs.js +++ b/frontend/src/js/tabs.js @@ -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 = ` `; - 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; })();