vibetunnel/web/dist/client/components/session-list.js
Mario Zechner 9bc6fcc4b1 Replace CDN imports with local bundle in all tests
- Create renderer-entry.ts to bundle both Renderer and XTermRenderer
- Add bundle:renderer script to generate public/bundle/renderer.js
- Update all test files to import from ../bundle/renderer.js
- Remove all unpkg/CDN XTerm.js script imports
- Tests now use bundled dependencies for faster loading

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-16 10:38:19 +02:00

437 lines
No EOL
18 KiB
JavaScript

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionList = void 0;
const lit_1 = require("lit");
const decorators_js_1 = require("lit/decorators.js");
require("./session-create-form.js");
let SessionList = class SessionList extends lit_1.LitElement {
constructor() {
super(...arguments);
this.sessions = [];
this.loading = false;
this.hideExited = true;
this.showCreateModal = false;
this.killingSessionIds = new Set();
this.loadedSnapshots = new Map();
this.loadingSnapshots = new Set();
this.cleaningExited = false;
this.newSessionIds = new Set();
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
handleRefresh() {
this.dispatchEvent(new CustomEvent('refresh'));
}
async loadSnapshot(sessionId) {
if (this.loadedSnapshots.has(sessionId) || this.loadingSnapshots.has(sessionId)) {
return;
}
this.loadingSnapshots.add(sessionId);
this.requestUpdate();
try {
// Just mark as loaded and create the player with the endpoint URL
this.loadedSnapshots.set(sessionId, sessionId);
this.requestUpdate();
// Create asciinema player after the element is rendered
setTimeout(() => this.createPlayer(sessionId), 10);
}
catch (error) {
console.error('Error loading snapshot:', error);
}
finally {
this.loadingSnapshots.delete(sessionId);
this.requestUpdate();
}
}
loadAllSnapshots() {
this.sessions.forEach(session => {
this.loadSnapshot(session.id);
});
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('sessions')) {
// Auto-load snapshots for existing sessions immediately, but delay for new ones
const prevSessions = changedProperties.get('sessions') || [];
const newSessionIdsList = this.sessions
.filter(session => !prevSessions.find((prev) => prev.id === session.id))
.map(session => session.id);
// Track new sessions
newSessionIdsList.forEach(id => this.newSessionIds.add(id));
// Load existing sessions immediately
const existingSessions = this.sessions.filter(session => !newSessionIdsList.includes(session.id));
existingSessions.forEach(session => this.loadSnapshot(session.id));
// Load new sessions after a delay to let them generate some output
if (newSessionIdsList.length > 0) {
setTimeout(() => {
newSessionIdsList.forEach(sessionId => {
this.newSessionIds.delete(sessionId); // Remove from new sessions set
this.loadSnapshot(sessionId);
});
this.requestUpdate(); // Update UI to show the players
}, 500); // Wait 500ms for new sessions
}
}
// If hideExited changed, recreate players for newly visible sessions
if (changedProperties.has('hideExited')) {
// Use a slight delay to avoid blocking the checkbox click
setTimeout(() => {
requestAnimationFrame(() => {
this.filteredSessions.forEach(session => {
const playerElement = this.querySelector(`#player-${session.id}`);
if (playerElement && this.loadedSnapshots.has(session.id)) {
// Player element exists but might not have a player instance
// Check if it's empty and recreate if needed
if (!playerElement.hasChildNodes() || playerElement.children.length === 0) {
this.createPlayer(session.id);
}
}
});
});
}, 10);
}
}
createPlayer(sessionId) {
const playerElement = this.querySelector(`#player-${sessionId}`);
if (!playerElement) {
// Element not ready yet, retry on next frame
requestAnimationFrame(() => this.createPlayer(sessionId));
return;
}
if (window.AsciinemaPlayer) {
try {
// Find the session to check its status
const session = this.sessions.find(s => s.id === sessionId);
// For ended sessions, use snapshot instead of stream to avoid reloading
const url = session?.status === 'exited'
? `/api/sessions/${sessionId}/snapshot`
: `/api/sessions/${sessionId}/stream`;
const config = session?.status === 'exited'
? { url } // Static snapshot
: { driver: "eventsource", url }; // Live stream
window.AsciinemaPlayer.create(config, playerElement, {
autoPlay: true,
loop: false,
controls: false,
fit: 'width',
terminalFontSize: '8px',
idleTimeLimit: 0.5,
preload: true,
poster: 'npt:999999'
});
}
catch (error) {
console.error('Error creating asciinema player:', error);
}
}
}
handleSessionClick(session) {
this.dispatchEvent(new CustomEvent('session-select', {
detail: session
}));
}
async handleKillSession(e, sessionId) {
e.stopPropagation(); // Prevent session selection
if (!confirm('Are you sure you want to kill this session?')) {
return;
}
this.killingSessionIds.add(sessionId);
this.requestUpdate();
try {
const response = await fetch(`/api/sessions/${sessionId}`, {
method: 'DELETE'
});
if (response.ok) {
this.dispatchEvent(new CustomEvent('session-killed', {
detail: { sessionId }
}));
// Refresh the list after a short delay
setTimeout(() => {
this.handleRefresh();
}, 1000);
}
else {
const error = await response.json();
this.dispatchEvent(new CustomEvent('error', {
detail: `Failed to kill session: ${error.error}`
}));
}
}
catch (error) {
console.error('Error killing session:', error);
this.dispatchEvent(new CustomEvent('error', {
detail: 'Failed to kill session'
}));
}
finally {
this.killingSessionIds.delete(sessionId);
this.requestUpdate();
}
}
async handleCleanSession(e, sessionId) {
e.stopPropagation(); // Prevent session selection
if (!confirm('Are you sure you want to clean up this session?')) {
return;
}
this.killingSessionIds.add(sessionId);
this.requestUpdate();
try {
const response = await fetch(`/api/sessions/${sessionId}/cleanup`, {
method: 'DELETE'
});
if (response.ok) {
this.dispatchEvent(new CustomEvent('session-killed', {
detail: { sessionId }
}));
// Refresh the list after a short delay
setTimeout(() => {
this.handleRefresh();
}, 500);
}
else {
const error = await response.json();
this.dispatchEvent(new CustomEvent('error', {
detail: `Failed to clean session: ${error.error}`
}));
}
}
catch (error) {
console.error('Error cleaning session:', error);
this.dispatchEvent(new CustomEvent('error', {
detail: 'Failed to clean session'
}));
}
finally {
this.killingSessionIds.delete(sessionId);
this.requestUpdate();
}
}
formatTime(timestamp) {
try {
const date = new Date(timestamp);
return date.toLocaleTimeString();
}
catch {
return 'Unknown';
}
}
truncateId(id) {
return id.length > 8 ? `${id.substring(0, 8)}...` : id;
}
handleSessionCreated(e) {
this.dispatchEvent(new CustomEvent('session-created', {
detail: e.detail
}));
}
handleCreateError(e) {
this.dispatchEvent(new CustomEvent('error', {
detail: e.detail
}));
}
handleCreateModalClose() {
this.dispatchEvent(new CustomEvent('create-modal-close'));
}
async handleCleanExited() {
const exitedSessions = this.sessions.filter(session => session.status === 'exited');
if (exitedSessions.length === 0) {
this.dispatchEvent(new CustomEvent('error', {
detail: 'No exited sessions to clean'
}));
return;
}
if (!confirm(`Are you sure you want to delete ${exitedSessions.length} exited session${exitedSessions.length > 1 ? 's' : ''}?`)) {
return;
}
this.cleaningExited = true;
this.requestUpdate();
try {
// Use the bulk cleanup API endpoint
const response = await fetch('/api/cleanup-exited', {
method: 'POST'
});
if (!response.ok) {
throw new Error('Failed to cleanup exited sessions');
}
this.dispatchEvent(new CustomEvent('error', {
detail: `Successfully cleaned ${exitedSessions.length} exited session${exitedSessions.length > 1 ? 's' : ''}`
}));
// Refresh the list after cleanup
setTimeout(() => {
this.handleRefresh();
}, 500);
}
catch (error) {
console.error('Error cleaning exited sessions:', error);
this.dispatchEvent(new CustomEvent('error', {
detail: 'Failed to clean exited sessions'
}));
}
finally {
this.cleaningExited = false;
this.requestUpdate();
}
}
handleHideExitedChange(e) {
const checked = e.target.checked;
this.dispatchEvent(new CustomEvent('hide-exited-change', {
detail: checked
}));
}
get filteredSessions() {
return this.hideExited
? this.sessions.filter(session => session.status === 'running')
: this.sessions;
}
render() {
const sessionsToShow = this.filteredSessions;
return (0, lit_1.html) `
<div class="font-mono text-sm p-4">
<!-- Controls -->
<div class="mb-4 flex items-center justify-between">
${!this.hideExited ? (0, lit_1.html) `
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-4 py-2 border-none rounded transition-colors disabled:opacity-50"
@click=${this.handleCleanExited}
?disabled=${this.cleaningExited || this.sessions.filter(s => s.status === 'exited').length === 0}
>
${this.cleaningExited ? '[~] CLEANING...' : 'CLEAN EXITED'}
</button>
` : (0, lit_1.html) `<div></div>`}
<label class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors">
<div class="relative">
<input
type="checkbox"
class="sr-only"
.checked=${this.hideExited}
@change=${this.handleHideExitedChange}
>
<div class="w-4 h-4 border border-vs-border rounded bg-vs-bg-secondary flex items-center justify-center transition-all ${this.hideExited ? 'bg-vs-user border-vs-user' : 'hover:border-vs-accent'}">
${this.hideExited ? (0, lit_1.html) `
<svg class="w-3 h-3 text-vs-bg" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
` : ''}
</div>
</div>
hide exited
</label>
</div>
${sessionsToShow.length === 0 ? (0, lit_1.html) `
<div class="text-vs-muted text-center py-8">
${this.loading ? 'Loading sessions...' : (this.hideExited && this.sessions.length > 0 ? 'No running sessions' : 'No sessions found')}
</div>
` : (0, lit_1.html) `
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${sessionsToShow.map(session => (0, lit_1.html) `
<div
class="bg-vs-bg border border-vs-border rounded shadow cursor-pointer overflow-hidden"
@click=${() => this.handleSessionClick(session)}
>
<!-- Compact Header -->
<div class="flex justify-between items-center px-3 py-2 border-b border-vs-border">
<div class="text-vs-text text-xs font-mono truncate pr-2 flex-1">${session.command}</div>
${session.status === 'running' || !this.hideExited ? (0, lit_1.html) `
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-2 py-0.5 border-none text-xs disabled:opacity-50 flex-shrink-0 rounded"
@click=${(e) => session.status === 'running' ? this.handleKillSession(e, session.id) : this.handleCleanSession(e, session.id)}
?disabled=${this.killingSessionIds.has(session.id)}
>
${this.killingSessionIds.has(session.id)
? (session.status === 'running' ? '[~] killing...' : '[~] cleaning...')
: (session.status === 'running' ? 'kill' : 'clean')}
</button>
` : ''}
</div>
<!-- Asciinema player (main content) -->
<div class="session-preview bg-black flex items-center justify-center overflow-hidden" style="aspect-ratio: 640/480;">
${this.loadedSnapshots.has(session.id) ? (0, lit_1.html) `
<div id="player-${session.id}" class="w-full h-full overflow-hidden"></div>
` : (0, lit_1.html) `
<div class="text-vs-muted text-xs">
${this.newSessionIds.has(session.id)
? '[~] init_session...'
: (this.loadingSnapshots.has(session.id) ? '[~] loading...' : '[~] loading...')}
</div>
`}
</div>
<!-- Compact Footer -->
<div class="px-3 py-2 text-vs-muted text-xs border-t border-vs-border">
<div class="flex justify-between items-center">
<span class="${session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'} text-xs">
${session.status}
</span>
<span class="truncate">${this.truncateId(session.id)}</span>
</div>
<div class="truncate text-xs opacity-75" title="${session.workingDir}">${session.workingDir}</div>
</div>
</div>
`)}
</div>
`}
<session-create-form
.visible=${this.showCreateModal}
@session-created=${this.handleSessionCreated}
@cancel=${this.handleCreateModalClose}
@error=${this.handleCreateError}
></session-create-form>
</div>
`;
}
};
exports.SessionList = SessionList;
__decorate([
(0, decorators_js_1.property)({ type: Array }),
__metadata("design:type", Array)
], SessionList.prototype, "sessions", void 0);
__decorate([
(0, decorators_js_1.property)({ type: Boolean }),
__metadata("design:type", Object)
], SessionList.prototype, "loading", void 0);
__decorate([
(0, decorators_js_1.property)({ type: Boolean }),
__metadata("design:type", Object)
], SessionList.prototype, "hideExited", void 0);
__decorate([
(0, decorators_js_1.property)({ type: Boolean }),
__metadata("design:type", Object)
], SessionList.prototype, "showCreateModal", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionList.prototype, "killingSessionIds", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionList.prototype, "loadedSnapshots", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionList.prototype, "loadingSnapshots", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionList.prototype, "cleaningExited", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionList.prototype, "newSessionIds", void 0);
exports.SessionList = SessionList = __decorate([
(0, decorators_js_1.customElement)('session-list')
], SessionList);
//# sourceMappingURL=session-list.js.map