Add broadcast input mode for multi-session typing

Broadcast mode sends keystrokes from the active terminal to all other
connected sessions simultaneously. Useful for applying the same
commands across multiple servers.

- Toggle via Ctrl+Shift+B keyboard shortcut or broadcast button in tab bar
- Visual indicator: broadcast button pulses red when active
- Only sends to sessions in 'connected' state
- Notification on toggle
- State stored in app state object
This commit is contained in:
2026-04-17 00:00:00 +01:00
parent 79e6f20d3f
commit 9cf2010c70
5 changed files with 87 additions and 0 deletions
+11
View File
@@ -128,6 +128,17 @@
<!-- Tabs are injected dynamically by JS -->
</div>
<button class="tab-new" id="btn-new-tab" title="New Tab">+</button>
<button class="tab-broadcast" id="btn-broadcast" title="Toggle Broadcast Mode (Ctrl+Shift+B)">
<!-- Broadcast icon (loudspeaker) -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M19.69 14a6.9 6.9 0 0 0 .31-2 6.9 6.9 0 0 0-.31-2"></path>
<path d="M22.82 16.18a10 10 0 0 0 0-8.36"></path>
<path d="M1 12s3-7 9-7 9 7 9 7-3 7-9 7-9-7-9-7z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<!-- ============================================================
+30
View File
@@ -309,6 +309,36 @@
background: var(--bg-active);
}
/* -----------------------------------------------------------------------
Broadcast toggle button
----------------------------------------------------------------------- */
.tab-broadcast {
display: flex;
align-items: center;
justify-content: center;
width: var(--tab-bar-height);
height: var(--tab-bar-height);
color: var(--text-tertiary);
cursor: pointer;
flex-shrink: 0;
transition: background var(--transition-fast), color var(--transition-fast);
border: none;
background: none;
opacity: 0.5;
}
.tab-broadcast:hover {
background: var(--bg-hover);
color: var(--text-primary);
opacity: 0.8;
}
.tab-broadcast.is-active {
color: var(--error);
opacity: 1;
animation: pulse-dot 1.5s ease infinite;
}
/* -----------------------------------------------------------------------
Tab overflow indicators (chevrons at edges)
----------------------------------------------------------------------- */
+30
View File
@@ -385,6 +385,13 @@ class App {
return;
}
// Ctrl/Cmd+Shift+B — toggle broadcast mode
if (mod && e.shiftKey && e.key === 'B') {
e.preventDefault();
this.toggleBroadcast();
return;
}
});
}
@@ -401,6 +408,7 @@ class App {
this._bindClick('btn-export', () => this.exportProfiles());
this._bindClick('btn-recordings', () => this.recordings.openBrowser());
this._bindClick('btn-new-tab', () => this.profileEditor.openNew());
this._bindClick('btn-broadcast', () => this.toggleBroadcast());
this._bindClick('btn-empty-connect', () => this.profileEditor.openNew());
this._bindClick('btn-empty-quick', () => this.quickConnect());
@@ -432,6 +440,28 @@ class App {
}
}
// ── Broadcast mode ─────────────────────────────────────────────
/**
* Toggles broadcast mode — when active, keystrokes are sent to all
* connected sessions simultaneously.
*/
toggleBroadcast() {
this.state.broadcastMode = !this.state.broadcastMode;
const btn = document.getElementById('btn-broadcast');
if (btn) {
btn.classList.toggle('is-active', this.state.broadcastMode);
btn.title = this.state.broadcastMode
? 'Broadcast Mode ON — keystrokes sent to all sessions (Ctrl+Shift+B)'
: 'Toggle Broadcast Mode (Ctrl+Shift+B)';
}
this.notifications.info(
this.state.broadcastMode
? 'Broadcast mode enabled — input sent to all sessions'
: 'Broadcast mode disabled'
);
}
// ── Connection lifecycle ─────────────────────────────────────────
/**
+3
View File
@@ -25,6 +25,9 @@ class State {
/** @type {boolean} */
this.secretStoreInitialised = false;
/** @type {boolean} Whether broadcast mode is active (keystrokes to all sessions). */
this.broadcastMode = false;
/** @type {Map<string, Set<Function>>} event name → callbacks */
this._listeners = new Map();
}
+13
View File
@@ -92,6 +92,19 @@ class TerminalManager {
try {
const b64 = btoa(data);
window.go.api.Service.WriteSession(sessionId, b64);
// Broadcast to all other connected sessions if enabled
if (this.app.state.broadcastMode) {
for (const [otherId] of this.terminals) {
if (otherId === sessionId) continue;
const otherSession = this.app.state.getSession(otherId);
if (otherSession && otherSession.state === 'connected') {
try {
window.go.api.Service.WriteSession(otherId, b64);
} catch (_) { /* non-critical */ }
}
}
}
} catch (err) {
console.error('WriteSession error:', err);
}