diff --git a/web/docs/multiplexer-integration.md b/web/docs/multiplexer-integration.md new file mode 100644 index 00000000..acadf0ac --- /dev/null +++ b/web/docs/multiplexer-integration.md @@ -0,0 +1,204 @@ +# Terminal Multiplexer Integration + +VibeTunnel supports seamless integration with terminal multiplexers like tmux, Zellij, and GNU Screen, allowing you to attach to existing sessions and manage them through the web interface. + +## Overview + +The multiplexer integration allows you to: +- List and attach to existing tmux/Zellij/Screen sessions +- Navigate between windows and panes (tmux) +- Create new sessions +- Kill sessions, windows (tmux), and panes (tmux) +- Maintain persistent terminal sessions across connections + +## Supported Multiplexers + +### tmux +- Full support for sessions, windows, and panes +- Shows session details including creation time, attached status, and window count +- Navigate to specific windows and panes +- Create sessions with optional initial commands +- Kill individual panes, windows, or entire sessions + +### Zellij +- Session management with creation time tracking +- Automatic session creation on first attach +- Layout support for new sessions +- ANSI color code handling in session names +- Proper cleanup of exited sessions + +### GNU Screen +- Session listing and management +- Shows session status (attached/detached) +- Create new sessions with optional commands +- Attach to existing sessions +- Kill sessions + +## Usage + +### Accessing Multiplexer Sessions + +1. Click the terminal icon in the session list +2. The multiplexer modal will open showing available sessions +3. For tmux sessions, expand to see windows and panes +4. Click "Attach" to connect to any session, window, or pane + +### Creating New Sessions + +#### tmux +```bash +# Create a new session +POST /api/multiplexer/sessions +{ + "type": "tmux", + "name": "dev-session", + "command": "vim" // optional initial command +} +``` + +#### Zellij +```bash +# Create a new session (created on first attach) +POST /api/multiplexer/sessions +{ + "type": "zellij", + "name": "dev-session", + "layout": "compact" // optional layout +} +``` + +#### GNU Screen +```bash +# Create a new session +POST /api/multiplexer/sessions +{ + "type": "screen", + "name": "dev-session", + "command": "vim" // optional initial command +} +``` + +### API Endpoints + +#### Get Multiplexer Status +```bash +GET /api/multiplexer/status +``` +Returns the availability and session list for all multiplexers. + +#### Get tmux Windows +```bash +GET /api/multiplexer/tmux/sessions/:session/windows +``` +Returns all windows in a tmux session. + +#### Get tmux Panes +```bash +GET /api/multiplexer/tmux/sessions/:session/panes?window=:windowIndex +``` +Returns panes in a session or specific window. + +#### Attach to Session +```bash +POST /api/multiplexer/attach +{ + "type": "tmux|zellij|screen", + "sessionName": "main", + "windowIndex": 0, // tmux only, optional + "paneIndex": 1, // tmux only, optional + "cols": 120, // optional terminal dimensions + "rows": 40 +} +``` + +#### Kill Session +```bash +DELETE /api/multiplexer/:type/sessions/:sessionName +``` + +#### Kill Window (tmux only) +```bash +DELETE /api/multiplexer/tmux/sessions/:sessionName/windows/:windowIndex +``` + +#### Kill Pane (tmux only) +```bash +DELETE /api/multiplexer/tmux/sessions/:sessionName/panes/:paneId +``` + +### Legacy tmux API Compatibility + +The following legacy endpoints are maintained for backward compatibility: +- `GET /api/tmux/sessions` - List tmux sessions +- `POST /api/tmux/attach` - Attach to tmux session + +## Implementation Details + +### Architecture + +The multiplexer integration consists of: +- `MultiplexerManager` - Unified interface for all multiplexers +- `TmuxManager` - tmux-specific implementation +- `ZellijManager` - Zellij-specific implementation +- `ScreenManager` - GNU Screen-specific implementation +- `multiplexer-modal` - LitElement component for the UI + +### Session Attachment + +When attaching to a multiplexer session: +1. A new VibeTunnel PTY session is created +2. The session runs the appropriate attach command: + - tmux: `tmux attach-session -t main` + - Zellij: `zellij attach main` + - Screen: `screen -r 12345.main` +3. The multiplexer takes over the terminal, providing its own UI +4. Users can navigate within the multiplexer using native keybindings + +### Key Features + +#### Automatic Detection +The system automatically detects installed multiplexers and only shows available options. + +#### Session Persistence +Multiplexer sessions persist even when VibeTunnel is restarted, allowing you to maintain long-running processes. + +#### Native Experience +Once attached, you interact with the multiplexer using its native keybindings: +- tmux: `Ctrl-B` (default prefix) +- Zellij: `Ctrl-G` (default prefix) +- Screen: `Ctrl-A` (default prefix) + +#### Clean Session Names +Zellij session names are automatically cleaned of ANSI escape codes for better display. + +#### Kill Confirmation +All destructive actions (killing sessions, windows, panes) require confirmation to prevent accidental data loss. + +## Best Practices + +1. **Use descriptive session names** - Makes it easier to identify sessions later +2. **Organize with windows** (tmux) - Group related tasks in different windows +3. **Leverage layouts** (Zellij) - Use predefined layouts for common workflows +4. **Clean up old sessions** - Kill sessions you're no longer using to free resources + +## Troubleshooting + +### Sessions Not Showing +- Ensure tmux/Zellij/Screen is installed on the system +- Check that sessions exist by running: + - tmux: `tmux ls` + - Zellij: `zellij list-sessions` + - Screen: `screen -ls` + +### Cannot Attach to Session +- Verify the session name is correct +- Check if the session is already attached elsewhere (some configurations prevent multiple attachments) + +### Display Issues +- Ensure terminal dimensions match between client and server +- Try resizing the browser window to trigger a resize event + +### Screen-Specific Issues +- Screen returns exit code 1 when sessions exist (this is normal behavior) +- Session names include PID prefix (e.g., `12345.session-name`) +- Use `screen -R` instead of `screen -r` for more forgiving reattachment \ No newline at end of file diff --git a/web/src/client/app.ts b/web/src/client/app.ts index 090b6f50..d14d1bba 100644 --- a/web/src/client/app.ts +++ b/web/src/client/app.ts @@ -26,6 +26,7 @@ import { titleManager } from './utils/title-manager.js'; // Import components import './components/app-header.js'; import './components/session-create-form.js'; +import './components/multiplexer-modal.js'; import './components/session-list.js'; import './components/session-view.js'; import './components/session-card.js'; @@ -69,6 +70,7 @@ export class VibeTunnelApp extends LitElement { @state() private hideExited = this.loadHideExitedState(); @state() private showCreateModal = false; @state() private createDialogWorkingDir = ''; + @state() private showTmuxModal = false; @state() private showSSHKeyManager = false; @state() private showSettings = false; @state() private isAuthenticated = false; @@ -136,10 +138,12 @@ export class VibeTunnelApp extends LitElement { // Update hasActiveOverlay whenever any overlay state changes if ( changedProperties.has('showCreateModal') || + changedProperties.has('showTmuxModal') || changedProperties.has('showSSHKeyManager') || changedProperties.has('showSettings') ) { - this.hasActiveOverlay = this.showCreateModal || this.showSSHKeyManager || this.showSettings; + this.hasActiveOverlay = + this.showCreateModal || this.showTmuxModal || this.showSSHKeyManager || this.showSettings; } // Force re-render when sessions change or view changes to update log button position @@ -1529,6 +1533,10 @@ export class VibeTunnelApp extends LitElement { this.handleCreateSession(); }; + private handleOpenTmuxSessions = () => { + this.showTmuxModal = true; + }; + private handleCaptureToggled = (e: CustomEvent) => { logger.log(`🎯 handleCaptureToggled called with:`, e.detail); this.keyboardCaptureActive = e.detail.active; @@ -1760,6 +1768,7 @@ export class VibeTunnelApp extends LitElement { @kill-all-sessions=${this.handleKillAll} @clean-exited-sessions=${this.handleCleanExited} @open-file-browser=${this.handleOpenFileBrowser} + @open-tmux-sessions=${this.handleOpenTmuxSessions} @open-settings=${this.handleOpenSettings} @logout=${this.handleLogout} @navigate-to-list=${this.handleNavigateToList} @@ -1874,6 +1883,15 @@ export class VibeTunnelApp extends LitElement { + + { + this.showTmuxModal = false; + }} + @navigate-to-session=${this.handleNavigateToSession} + @create-session=${this.handleCreateSession} + > `; } } diff --git a/web/src/client/components/app-header.ts b/web/src/client/components/app-header.ts index 0c34c41b..117d097b 100644 --- a/web/src/client/components/app-header.ts +++ b/web/src/client/components/app-header.ts @@ -9,6 +9,7 @@ * @fires kill-all-sessions - When kill all button is clicked * @fires clean-exited-sessions - When clean exited button is clicked * @fires open-file-browser - When browse button is clicked + * @fires open-tmux-sessions - When tmux sessions button is clicked * @fires logout - When logout is clicked * @fires toggle-sidebar - When sidebar toggle button is clicked */ @@ -59,6 +60,7 @@ export class AppHeader extends LitElement { @kill-all-sessions=${this.forwardEvent} @clean-exited-sessions=${this.forwardEvent} @open-file-browser=${this.forwardEvent} + @open-tmux-sessions=${this.forwardEvent} @open-settings=${this.forwardEvent} @logout=${this.forwardEvent} @navigate-to-list=${this.forwardEvent} @@ -79,6 +81,7 @@ export class AppHeader extends LitElement { @kill-all-sessions=${this.forwardEvent} @clean-exited-sessions=${this.forwardEvent} @open-file-browser=${this.forwardEvent} + @open-tmux-sessions=${this.forwardEvent} @open-settings=${this.forwardEvent} @logout=${this.forwardEvent} @navigate-to-list=${this.forwardEvent} diff --git a/web/src/client/components/full-header.ts b/web/src/client/components/full-header.ts index f3a2398b..28826abe 100644 --- a/web/src/client/components/full-header.ts +++ b/web/src/client/components/full-header.ts @@ -59,6 +59,15 @@ export class FullHeader extends HeaderBase { /> + + ` + : null + } + ${ + status.zellij.available + ? html` + + ` + : null + } + ${ + status.screen.available + ? html` + + ` + : null + } + + ` + : null + } + + ${ + this.loading + ? html`
Loading terminal sessions...
` + : !status + ? html`
No multiplexer status available
` + : !status.tmux.available && !status.zellij.available && !status.screen.available + ? html` +
+

No Terminal Multiplexer Available

+

No terminal multiplexer (tmux, Zellij, or Screen) is installed on this system.

+

Install tmux, Zellij, or GNU Screen to use this feature.

+
+ ` + : !activeMultiplexer?.available + ? html` +
+

${this.activeTab} Not Available

+

${this.activeTab} is not installed or not available on this system.

+

Install ${this.activeTab} to use this feature.

+
+ ` + : this.error + ? html`
${this.error}
` + : activeMultiplexer.sessions.length === 0 + ? html` +
+

No ${this.activeTab} Sessions

+

There are no active ${this.activeTab} sessions.

+ +
+ ` + : html` +
+ ${repeat( + activeMultiplexer.sessions, + (session) => `${session.type}-${session.name}`, + (session) => { + const sessionWindows = this.windows.get(session.name) || []; + const isExpanded = this.expandedSessions.has(session.name); + + return html` +
+
+ session.type === 'tmux' ? this.toggleSession(session.name) : null} + style="cursor: ${session.type === 'tmux' ? 'pointer' : 'default'}" + > +
+
${session.name}
+
+ ${ + session.windows !== undefined + ? html`${session.windows} window${session.windows !== 1 ? 's' : ''}` + : null + } + ${ + session.exited + ? html`EXITED` + : null + } + ${ + session.activity + ? html`Last activity: ${this.formatTimestamp(session.activity)}` + : null + } +
+
+
+ ${ + session.attached + ? html`
` + : null + } + ${ + session.current + ? html`
` + : null + } + + + ${ + session.type === 'tmux' + ? html`â–¶` + : null + } +
+
+ + ${ + session.type === 'tmux' && isExpanded && sessionWindows.length > 0 + ? html` +
+ ${repeat( + sessionWindows, + (window) => `${session.name}-${window.index}`, + (window) => { + const windowKey = `${session.name}:${window.index}`; + const isWindowExpanded = + this.expandedWindows.has(windowKey); + const windowPanes = this.panes.get(windowKey) || []; + + return html` +
+
{ + e.stopPropagation(); + if (window.panes > 1) { + this.toggleWindow(session.name, window.index); + } else { + this.attachToSession({ + type: session.type, + session: session.name, + window: window.index, + }); + } + }} + > +
+ ${window.index}: + ${window.name} +
+
+ + + ${window.panes} pane${window.panes !== 1 ? 's' : ''} + ${window.panes > 1 ? html`â–¶` : ''} + +
+
+ + ${ + isWindowExpanded && windowPanes.length > 0 + ? html` +
+ ${repeat( + windowPanes, + (pane) => + `${session.name}:${window.index}.${pane.index}`, + (pane) => html` +
{ + e.stopPropagation(); + this.attachToSession({ + type: session.type, + session: session.name, + window: window.index, + pane: pane.index, + }); + }} + > +
+ %${pane.index} + ${this.formatPaneInfo(pane)} +
+
+ + ${pane.width}×${pane.height} +
+
+ ` + )} +
+ ` + : null + } +
+ `; + } + )} +
+ ` + : null + } +
+ `; + } + )} +
+ ` + } + +
+ + ${ + !this.loading && activeMultiplexer?.available + ? html` + + ` + : null + } +
+ + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'multiplexer-modal': MultiplexerModal; + } +} diff --git a/web/src/client/components/sidebar-header.ts b/web/src/client/components/sidebar-header.ts index d4dcc40d..206c8e4f 100644 --- a/web/src/client/components/sidebar-header.ts +++ b/web/src/client/components/sidebar-header.ts @@ -76,6 +76,16 @@ export class SidebarHeader extends HeaderBase {
+ +