/** * Session List Component * * Displays a grid of session cards and manages the session creation modal. * Handles session filtering (hide/show exited) and cleanup operations. * * @fires navigate-to-session - When a session is selected (detail: { sessionId: string }) * @fires refresh - When session list needs refreshing * @fires error - When an error occurs (detail: string) * @fires session-created - When a new session is created (detail: { sessionId: string, message?: string }) * @fires create-modal-close - When create modal should close * @fires hide-exited-change - When hide exited state changes (detail: boolean) * @fires kill-all-sessions - When all sessions should be killed * * @listens session-killed - From session-card when a session is killed * @listens session-kill-error - From session-card when kill fails * @listens clean-exited-sessions - To trigger cleanup of exited sessions */ import { LitElement, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import type { Session } from '../../shared/types.js'; import './session-create-form.js'; import './session-card.js'; import { createLogger } from '../utils/logger.js'; const logger = createLogger('session-list'); // Re-export Session type for backward compatibility export type { Session }; @customElement('session-list') export class SessionList extends LitElement { // Disable shadow DOM to use Tailwind createRenderRoot() { return this; } @property({ type: Array }) sessions: Session[] = []; @property({ type: Boolean }) loading = false; @property({ type: Boolean }) hideExited = true; @property({ type: Boolean }) showCreateModal = false; @state() private cleaningExited = false; private previousRunningCount = 0; private handleRefresh() { this.dispatchEvent(new CustomEvent('refresh')); } private handleSessionSelect(e: CustomEvent) { const session = e.detail as Session; // Dispatch a custom event that the app can handle with view transitions this.dispatchEvent( new CustomEvent('navigate-to-session', { detail: { sessionId: session.id }, bubbles: true, composed: true, }) ); } private handleSessionKilled(e: CustomEvent) { const { sessionId } = e.detail; logger.debug(`session ${sessionId} killed, updating session list`); // Immediately remove the session from the local state for instant UI feedback this.sessions = this.sessions.filter((session) => session.id !== sessionId); // Then trigger a refresh to get the latest server state this.dispatchEvent(new CustomEvent('refresh')); } private handleSessionKillError(e: CustomEvent) { const { sessionId, error } = e.detail; logger.error(`failed to kill session ${sessionId}:`, error); // Dispatch error event to parent for user notification this.dispatchEvent( new CustomEvent('error', { detail: `Failed to kill session: ${error}`, }) ); } public async handleCleanupExited() { if (this.cleaningExited) return; this.cleaningExited = true; this.requestUpdate(); try { const response = await fetch('/api/cleanup-exited', { method: 'POST', }); if (response.ok) { this.dispatchEvent(new CustomEvent('refresh')); } else { this.dispatchEvent( new CustomEvent('error', { detail: 'Failed to cleanup exited sessions' }) ); } } catch (error) { logger.error('error cleaning up exited sessions:', error); this.dispatchEvent(new CustomEvent('error', { detail: 'Failed to cleanup exited sessions' })); } finally { this.cleaningExited = false; this.requestUpdate(); } } render() { const filteredSessions = this.hideExited ? this.sessions.filter((session) => session.status !== 'exited') : this.sessions; return html`
${filteredSessions.length === 0 ? html`
${this.loading ? 'Loading sessions...' : this.hideExited && this.sessions.length > 0 ? html`
No running sessions
There are exited sessions. Show them by toggling "Hide exited" above.
` : html`
No terminal sessions yet!
Get started by using the vt command in your terminal:
vt npm run dev
# Monitor your dev server
vt claude --dangerously...
# Keep an eye on AI agents
vt --shell
# Open an interactive shell
vt python train.py
# Watch long-running scripts
Haven't installed the CLI yet?
→ Click the VibeTunnel menu bar icon
→ Go to Settings → Advanced → Install CLI Tools
Once installed, any command prefixed with vt will appear here, accessible from any browser at localhost:4020.
`}
` : html`
${repeat( filteredSessions, (session) => session.id, (session) => html` ` )}
`} this.dispatchEvent(new CustomEvent('session-created', { detail: e.detail }))} @cancel=${() => this.dispatchEvent(new CustomEvent('create-modal-close'))} @error=${(e: CustomEvent) => this.dispatchEvent(new CustomEvent('error', { detail: e.detail }))} >
`; } }