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 -->
|
<!-- Tabs are injected dynamically by JS -->
|
||||||
</div>
|
</div>
|
||||||
<button class="tab-new" id="btn-new-tab" title="New Tab">+</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
|
|||||||
@@ -309,6 +309,36 @@
|
|||||||
background: var(--bg-active);
|
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)
|
Tab overflow indicators (chevrons at edges)
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
|
|||||||
@@ -385,6 +385,13 @@ class App {
|
|||||||
return;
|
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-export', () => this.exportProfiles());
|
||||||
this._bindClick('btn-recordings', () => this.recordings.openBrowser());
|
this._bindClick('btn-recordings', () => this.recordings.openBrowser());
|
||||||
this._bindClick('btn-new-tab', () => this.profileEditor.openNew());
|
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-connect', () => this.profileEditor.openNew());
|
||||||
this._bindClick('btn-empty-quick', () => this.quickConnect());
|
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 ─────────────────────────────────────────
|
// ── Connection lifecycle ─────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ class State {
|
|||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
this.secretStoreInitialised = false;
|
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 */
|
/** @type {Map<string, Set<Function>>} event name → callbacks */
|
||||||
this._listeners = new Map();
|
this._listeners = new Map();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,19 @@ class TerminalManager {
|
|||||||
try {
|
try {
|
||||||
const b64 = btoa(data);
|
const b64 = btoa(data);
|
||||||
window.go.api.Service.WriteSession(sessionId, b64);
|
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) {
|
} catch (err) {
|
||||||
console.error('WriteSession error:', err);
|
console.error('WriteSession error:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user