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:
@@ -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>
|
||||
|
||||
<!-- ============================================================
|
||||
|
||||
@@ -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)
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
@@ -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 ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user