cleanup git

This commit is contained in:
Peter Steinberger 2025-06-16 16:20:46 +02:00
parent f7d72acfca
commit d013019b16
47 changed files with 0 additions and 5643 deletions

View file

@ -1,5 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// Entry point for the app
require("./app.js");
//# sourceMappingURL=app-entry.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"app-entry.js","sourceRoot":"","sources":["../../src/client/app-entry.ts"],"names":[],"mappings":";;AAAA,0BAA0B;AAC1B,oBAAkB"}

311
web/dist/client/app.js vendored
View file

@ -1,311 +0,0 @@
"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.VibeTunnelApp = void 0;
const lit_1 = require("lit");
const decorators_js_1 = require("lit/decorators.js");
// Import components
require("./components/app-header.js");
require("./components/session-create-form.js");
require("./components/session-list.js");
require("./components/session-view.js");
require("./components/session-card.js");
let VibeTunnelApp = class VibeTunnelApp extends lit_1.LitElement {
constructor() {
super(...arguments);
this.errorMessage = '';
this.sessions = [];
this.loading = false;
this.currentView = 'list';
this.selectedSession = null;
this.hideExited = true;
this.showCreateModal = false;
this.hotReloadWs = null;
this.handlePopState = (event) => {
// Handle browser back/forward navigation
this.parseUrlAndSetState();
};
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.setupHotReload();
this.loadSessions();
this.startAutoRefresh();
this.setupRouting();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.hotReloadWs) {
this.hotReloadWs.close();
}
// Clean up routing listeners
window.removeEventListener('popstate', this.handlePopState);
}
showError(message) {
this.errorMessage = message;
// Clear error after 5 seconds
setTimeout(() => {
this.errorMessage = '';
}, 5000);
}
clearError() {
this.errorMessage = '';
}
async loadSessions() {
this.loading = true;
try {
const response = await fetch('/api/sessions');
if (response.ok) {
const sessionsData = await response.json();
this.sessions = sessionsData.map((session) => ({
id: session.id,
command: session.command,
workingDir: session.workingDir,
status: session.status,
exitCode: session.exitCode,
startedAt: session.startedAt,
lastModified: session.lastModified,
pid: session.pid
}));
this.clearError();
}
else {
this.showError('Failed to load sessions');
}
}
catch (error) {
console.error('Error loading sessions:', error);
this.showError('Failed to load sessions');
}
finally {
this.loading = false;
}
}
startAutoRefresh() {
// Refresh sessions every 3 seconds, but only when showing session list
setInterval(() => {
if (this.currentView === 'list') {
this.loadSessions();
}
}, 3000);
}
async handleSessionCreated(e) {
const sessionId = e.detail.sessionId;
if (!sessionId) {
this.showError('Session created but ID not found in response');
return;
}
this.showCreateModal = false;
// Wait for session to appear in the list and then switch to it
await this.waitForSessionAndSwitch(sessionId);
}
async waitForSessionAndSwitch(sessionId) {
const maxAttempts = 10;
const delay = 500; // 500ms between attempts
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await this.loadSessions();
// Try to find by exact ID match first
let session = this.sessions.find(s => s.id === sessionId);
// If not found by ID, find the most recently created session
// This works around tty-fwd potentially using different IDs internally
if (!session && this.sessions.length > 0) {
const sortedSessions = [...this.sessions].sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
session = sortedSessions[0];
}
if (session) {
// Session found, switch to session view
this.selectedSession = session;
this.currentView = 'session';
// Update URL to include session ID
this.updateUrl(session.id);
return;
}
// Wait before next attempt
await new Promise(resolve => setTimeout(resolve, delay));
}
// If we get here, session creation might have failed
console.log('Session not found after all attempts');
this.showError('Session created but could not be found. Please refresh.');
}
handleSessionSelect(e) {
const session = e.detail;
console.log('Session selected:', session);
this.selectedSession = session;
this.currentView = 'session';
// Update URL to include session ID
this.updateUrl(session.id);
}
handleBack() {
this.currentView = 'list';
this.selectedSession = null;
// Update URL to remove session parameter
this.updateUrl();
}
handleSessionKilled(e) {
console.log('Session killed:', e.detail);
this.loadSessions(); // Refresh the list
}
handleRefresh() {
this.loadSessions();
}
handleError(e) {
this.showError(e.detail);
}
handleHideExitedChange(e) {
this.hideExited = e.detail;
}
handleCreateSession() {
this.showCreateModal = true;
}
handleCreateModalClose() {
this.showCreateModal = false;
}
// URL Routing methods
setupRouting() {
// Handle browser back/forward navigation
window.addEventListener('popstate', this.handlePopState.bind(this));
// Parse initial URL and set state
this.parseUrlAndSetState();
}
parseUrlAndSetState() {
const url = new URL(window.location.href);
const sessionId = url.searchParams.get('session');
if (sessionId) {
// Load the specific session
this.loadSessionFromUrl(sessionId);
}
else {
// Show session list
this.currentView = 'list';
this.selectedSession = null;
}
}
async loadSessionFromUrl(sessionId) {
// First ensure sessions are loaded
if (this.sessions.length === 0) {
await this.loadSessions();
}
// Find the session
const session = this.sessions.find(s => s.id === sessionId);
if (session) {
this.selectedSession = session;
this.currentView = 'session';
}
else {
// Session not found, go to list view
this.currentView = 'list';
this.selectedSession = null;
// Update URL to remove invalid session ID
this.updateUrl();
}
}
updateUrl(sessionId) {
const url = new URL(window.location.href);
if (sessionId) {
url.searchParams.set('session', sessionId);
}
else {
url.searchParams.delete('session');
}
// Update browser URL without triggering page reload
window.history.pushState(null, '', url.toString());
}
setupHotReload() {
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}?hotReload=true`;
this.hotReloadWs = new WebSocket(wsUrl);
this.hotReloadWs.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'reload') {
window.location.reload();
}
};
}
}
render() {
return (0, lit_1.html) `
<!-- Error notification overlay -->
${this.errorMessage ? (0, lit_1.html) `
<div class="fixed top-4 right-4 z-50">
<div class="bg-vs-warning text-vs-bg px-4 py-2 rounded shadow-lg font-mono text-sm">
${this.errorMessage}
<button @click=${this.clearError} class="ml-2 text-vs-bg hover:text-vs-muted"></button>
</div>
</div>
` : ''}
<!-- Main content -->
${this.currentView === 'session' ? (0, lit_1.html) `
<session-view
.session=${this.selectedSession}
@back=${this.handleBack}
></session-view>
` : (0, lit_1.html) `
<div class="max-w-4xl mx-auto">
<app-header
@create-session=${this.handleCreateSession}
></app-header>
<session-list
.sessions=${this.sessions}
.loading=${this.loading}
.hideExited=${this.hideExited}
.showCreateModal=${this.showCreateModal}
@session-select=${this.handleSessionSelect}
@session-killed=${this.handleSessionKilled}
@session-created=${this.handleSessionCreated}
@create-modal-close=${this.handleCreateModalClose}
@refresh=${this.handleRefresh}
@error=${this.handleError}
@hide-exited-change=${this.handleHideExitedChange}
></session-list>
</div>
`}
`;
}
};
exports.VibeTunnelApp = VibeTunnelApp;
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], VibeTunnelApp.prototype, "errorMessage", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Array)
], VibeTunnelApp.prototype, "sessions", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], VibeTunnelApp.prototype, "loading", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", String)
], VibeTunnelApp.prototype, "currentView", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], VibeTunnelApp.prototype, "selectedSession", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], VibeTunnelApp.prototype, "hideExited", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], VibeTunnelApp.prototype, "showCreateModal", void 0);
exports.VibeTunnelApp = VibeTunnelApp = __decorate([
(0, decorators_js_1.customElement)('vibetunnel-app')
], VibeTunnelApp);
//# sourceMappingURL=app.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,39 +0,0 @@
"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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppHeader = void 0;
const lit_1 = require("lit");
const decorators_js_1 = require("lit/decorators.js");
let AppHeader = class AppHeader extends lit_1.LitElement {
createRenderRoot() {
return this;
}
handleCreateSession() {
this.dispatchEvent(new CustomEvent('create-session'));
}
render() {
return (0, lit_1.html) `
<div class="p-4 border-b border-vs-border">
<div class="flex items-center justify-between">
<div class="text-vs-user font-mono text-sm">VibeTunnel</div>
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleCreateSession}
>
CREATE SESSION
</button>
</div>
</div>
`;
}
};
exports.AppHeader = AppHeader;
exports.AppHeader = AppHeader = __decorate([
(0, decorators_js_1.customElement)('app-header')
], AppHeader);
//# sourceMappingURL=app-header.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"app-header.js","sourceRoot":"","sources":["../../../src/client/components/app-header.ts"],"names":[],"mappings":";;;;;;;;;AAAA,6BAAuC;AACvC,qDAAkD;AAG3C,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,gBAAU;IACvC,gBAAgB;QACd,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,mBAAmB;QACzB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM;QACJ,OAAO,IAAA,UAAI,EAAA;;;;;;qBAMM,IAAI,CAAC,mBAAmB;;;;;;KAMxC,CAAC;IACJ,CAAC;CACF,CAAA;AAxBY,8BAAS;oBAAT,SAAS;IADrB,IAAA,6BAAa,EAAC,YAAY,CAAC;GACf,SAAS,CAwBrB"}

View file

@ -1,269 +0,0 @@
"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.FileBrowser = void 0;
const lit_1 = require("lit");
const decorators_js_1 = require("lit/decorators.js");
let FileBrowser = class FileBrowser extends lit_1.LitElement {
constructor() {
super(...arguments);
this.currentPath = '~';
this.visible = false;
this.files = [];
this.loading = false;
this.showCreateFolder = false;
this.newFolderName = '';
this.creating = false;
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
async connectedCallback() {
super.connectedCallback();
if (this.visible) {
await this.loadDirectory(this.currentPath);
}
}
async updated(changedProperties) {
if (changedProperties.has('visible') && this.visible) {
await this.loadDirectory(this.currentPath);
}
}
async loadDirectory(dirPath) {
this.loading = true;
try {
const response = await fetch(`/api/fs/browse?path=${encodeURIComponent(dirPath)}`);
if (response.ok) {
const data = await response.json();
this.currentPath = data.absolutePath;
this.files = data.files;
}
else {
console.error('Failed to load directory');
}
}
catch (error) {
console.error('Error loading directory:', error);
}
finally {
this.loading = false;
}
}
handleDirectoryClick(dirName) {
const newPath = this.currentPath + '/' + dirName;
this.loadDirectory(newPath);
}
handleParentClick() {
const parentPath = this.currentPath.split('/').slice(0, -1).join('/') || '/';
this.loadDirectory(parentPath);
}
handleSelect() {
this.dispatchEvent(new CustomEvent('directory-selected', {
detail: this.currentPath
}));
}
handleCancel() {
this.dispatchEvent(new CustomEvent('browser-cancel'));
}
handleCreateFolder() {
this.showCreateFolder = true;
this.newFolderName = '';
}
handleCancelCreateFolder() {
this.showCreateFolder = false;
this.newFolderName = '';
}
handleFolderNameInput(e) {
this.newFolderName = e.target.value;
}
handleFolderNameKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
this.createFolder();
}
else if (e.key === 'Escape') {
e.preventDefault();
this.handleCancelCreateFolder();
}
}
async createFolder() {
if (!this.newFolderName.trim())
return;
this.creating = true;
try {
const response = await fetch('/api/mkdir', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: this.currentPath,
name: this.newFolderName.trim()
})
});
if (response.ok) {
// Refresh directory listing
await this.loadDirectory(this.currentPath);
this.handleCancelCreateFolder();
}
else {
const error = await response.json();
alert(`Failed to create folder: ${error.error}`);
}
}
catch (error) {
console.error('Error creating folder:', error);
alert('Failed to create folder');
}
finally {
this.creating = false;
}
}
render() {
if (!this.visible) {
return (0, lit_1.html) ``;
}
return (0, lit_1.html) `
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="z-index: 9999;">
<div class="bg-vs-bg-secondary border border-vs-border font-mono text-sm w-96 h-96 flex flex-col">
<div class="p-4 border-b border-vs-border flex-shrink-0">
<div class="flex justify-between items-center mb-2">
<div class="text-vs-assistant text-sm">Select Directory</div>
<button
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-2 py-1 text-xs border-none rounded"
@click=${this.handleCreateFolder}
?disabled=${this.loading}
title="Create new folder"
>
+ folder
</button>
</div>
<div class="text-vs-muted text-sm break-all">${this.currentPath}</div>
</div>
<div class="p-4 flex-1 overflow-y-auto">
${this.loading ? (0, lit_1.html) `
<div class="text-vs-muted">Loading...</div>
` : (0, lit_1.html) `
${this.currentPath !== '/' ? (0, lit_1.html) `
<div
class="flex items-center gap-2 p-2 hover:bg-vs-nav-hover cursor-pointer text-vs-accent"
@click=${this.handleParentClick}
>
<span>📁</span>
<span>.. (parent directory)</span>
</div>
` : ''}
${this.files.filter(f => f.isDir).map(file => (0, lit_1.html) `
<div
class="flex items-center gap-2 p-2 hover:bg-vs-nav-hover cursor-pointer text-vs-accent"
@click=${() => this.handleDirectoryClick(file.name)}
>
<span>📁</span>
<span>${file.name}</span>
</div>
`)}
${this.files.filter(f => !f.isDir).map(file => (0, lit_1.html) `
<div class="flex items-center gap-2 p-2 text-vs-muted">
<span>📄</span>
<span>${file.name}</span>
</div>
`)}
`}
</div>
<!-- Create folder dialog -->
${this.showCreateFolder ? (0, lit_1.html) `
<div class="p-4 border-t border-vs-border flex-shrink-0">
<div class="text-vs-assistant text-sm mb-2">Create New Folder</div>
<div class="flex gap-2">
<input
type="text"
class="flex-1 bg-vs-bg border border-vs-border text-vs-text px-2 py-1 text-sm font-mono"
placeholder="Folder name"
.value=${this.newFolderName}
@input=${this.handleFolderNameInput}
@keydown=${this.handleFolderNameKeydown}
?disabled=${this.creating}
/>
<button
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-2 py-1 text-xs border-none"
@click=${this.createFolder}
?disabled=${this.creating || !this.newFolderName.trim()}
>
${this.creating ? '...' : 'create'}
</button>
<button
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-2 py-1 text-xs border-none"
@click=${this.handleCancelCreateFolder}
?disabled=${this.creating}
>
cancel
</button>
</div>
</div>
` : ''}
<div class="p-4 border-t border-vs-border flex gap-4 justify-end flex-shrink-0">
<button
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-4 py-2 border-none"
@click=${this.handleCancel}
>
cancel
</button>
<button
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-4 py-2 border-none"
@click=${this.handleSelect}
>
select
</button>
</div>
</div>
</div>
`;
}
};
exports.FileBrowser = FileBrowser;
__decorate([
(0, decorators_js_1.property)({ type: String }),
__metadata("design:type", Object)
], FileBrowser.prototype, "currentPath", void 0);
__decorate([
(0, decorators_js_1.property)({ type: Boolean }),
__metadata("design:type", Object)
], FileBrowser.prototype, "visible", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Array)
], FileBrowser.prototype, "files", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], FileBrowser.prototype, "loading", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], FileBrowser.prototype, "showCreateFolder", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], FileBrowser.prototype, "newFolderName", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], FileBrowser.prototype, "creating", void 0);
exports.FileBrowser = FileBrowser = __decorate([
(0, decorators_js_1.customElement)('file-browser')
], FileBrowser);
//# sourceMappingURL=file-browser.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,173 +0,0 @@
"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.SessionCard = void 0;
const lit_1 = require("lit");
const decorators_js_1 = require("lit/decorators.js");
const renderer_js_1 = require("../renderer.js");
let SessionCard = class SessionCard extends lit_1.LitElement {
constructor() {
super(...arguments);
this.renderer = null;
this.refreshInterval = null;
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.createRenderer();
this.startRefresh();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (this.renderer) {
this.renderer.dispose();
this.renderer = null;
}
}
createRenderer() {
const playerElement = this.querySelector('#player');
if (!playerElement)
return;
// Create single renderer for this card - use larger dimensions for better preview
this.renderer = new renderer_js_1.Renderer(playerElement, 80, 24, 10000, 8, true);
// Always use snapshot endpoint for cards
const url = `/api/sessions/${this.session.id}/snapshot`;
// Wait a moment for freshly created sessions before connecting
const sessionAge = Date.now() - new Date(this.session.startedAt).getTime();
const delay = sessionAge < 5000 ? 2000 : 0; // 2 second delay if session is less than 5 seconds old
setTimeout(() => {
if (this.renderer) {
this.renderer.loadFromUrl(url, false); // false = not a stream, use snapshot
// Disable pointer events so clicks pass through to the card
this.renderer.setPointerEventsEnabled(false);
}
}, delay);
}
startRefresh() {
this.refreshInterval = window.setInterval(() => {
if (this.renderer) {
const url = `/api/sessions/${this.session.id}/snapshot`;
this.renderer.loadFromUrl(url, false);
// Ensure pointer events stay disabled after refresh
this.renderer.setPointerEventsEnabled(false);
}
}, 10000); // Refresh every 10 seconds
}
handleCardClick() {
this.dispatchEvent(new CustomEvent('session-select', {
detail: this.session,
bubbles: true,
composed: true
}));
}
handleKillClick(e) {
e.stopPropagation();
e.preventDefault();
this.dispatchEvent(new CustomEvent('session-kill', {
detail: this.session.id,
bubbles: true,
composed: true
}));
}
async handlePidClick(e) {
e.stopPropagation();
e.preventDefault();
if (this.session.pid) {
try {
await navigator.clipboard.writeText(this.session.pid.toString());
console.log('PID copied to clipboard:', this.session.pid);
}
catch (error) {
console.error('Failed to copy PID to clipboard:', error);
// Fallback: select text manually
this.fallbackCopyToClipboard(this.session.pid.toString());
}
}
}
fallbackCopyToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
console.log('PID copied to clipboard (fallback):', text);
}
catch (error) {
console.error('Fallback copy failed:', error);
}
document.body.removeChild(textArea);
}
render() {
const isRunning = this.session.status === 'running';
return (0, lit_1.html) `
<div class="bg-vs-bg border border-vs-border rounded shadow cursor-pointer overflow-hidden"
@click=${this.handleCardClick}>
<!-- 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">${this.session.command}</div>
${this.session.status === 'running' ? (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=${this.handleKillClick}
>
${this.session.status === 'running' ? 'kill' : 'clean'}
</button>
` : ''}
</div>
<!-- XTerm renderer (main content) -->
<div class="session-preview bg-black overflow-hidden" style="aspect-ratio: 640/480;">
<div id="player" class="w-full h-full"></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="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'} text-xs">
${this.session.status}
</span>
${this.session.pid ? (0, lit_1.html) `
<span
class="cursor-pointer hover:text-vs-accent transition-colors"
@click=${this.handlePidClick}
title="Click to copy PID"
>
PID: ${this.session.pid} <span class="opacity-50">(click to copy)</span>
</span>
` : ''}
</div>
<div class="truncate text-xs opacity-75" title="${this.session.workingDir}">${this.session.workingDir}</div>
</div>
</div>
`;
}
};
exports.SessionCard = SessionCard;
__decorate([
(0, decorators_js_1.property)({ type: Object }),
__metadata("design:type", Object)
], SessionCard.prototype, "session", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionCard.prototype, "renderer", void 0);
exports.SessionCard = SessionCard = __decorate([
(0, decorators_js_1.customElement)('session-card')
], SessionCard);
//# sourceMappingURL=session-card.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"session-card.js","sourceRoot":"","sources":["../../../src/client/components/session-card.ts"],"names":[],"mappings":"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"}

View file

@ -1,235 +0,0 @@
"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.SessionCreateForm = void 0;
const lit_1 = require("lit");
const decorators_js_1 = require("lit/decorators.js");
require("./file-browser.js");
let SessionCreateForm = class SessionCreateForm extends lit_1.LitElement {
constructor() {
super(...arguments);
this.workingDir = '~/';
this.command = 'zsh';
this.disabled = false;
this.visible = false;
this.isCreating = false;
this.showFileBrowser = false;
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
handleWorkingDirChange(e) {
const input = e.target;
this.workingDir = input.value;
this.dispatchEvent(new CustomEvent('working-dir-change', {
detail: this.workingDir
}));
}
handleCommandChange(e) {
const input = e.target;
this.command = input.value;
}
handleBrowse() {
this.showFileBrowser = true;
}
handleDirectorySelected(e) {
this.workingDir = e.detail;
this.showFileBrowser = false;
}
handleBrowserCancel() {
this.showFileBrowser = false;
}
async handleCreate() {
if (!this.workingDir.trim() || !this.command.trim()) {
this.dispatchEvent(new CustomEvent('error', {
detail: 'Please fill in both working directory and command'
}));
return;
}
this.isCreating = true;
const sessionData = {
command: this.parseCommand(this.command.trim()),
workingDir: this.workingDir.trim()
};
try {
const response = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sessionData)
});
if (response.ok) {
const result = await response.json();
this.command = ''; // Clear command on success
this.dispatchEvent(new CustomEvent('session-created', {
detail: result
}));
}
else {
const error = await response.json();
this.dispatchEvent(new CustomEvent('error', {
detail: `Failed to create session: ${error.error}`
}));
}
}
catch (error) {
console.error('Error creating session:', error);
this.dispatchEvent(new CustomEvent('error', {
detail: 'Failed to create session'
}));
}
finally {
this.isCreating = false;
}
}
parseCommand(commandStr) {
// Simple command parsing - split by spaces but respect quotes
const args = [];
let current = '';
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < commandStr.length; i++) {
const char = commandStr[i];
if ((char === '"' || char === "'") && !inQuotes) {
inQuotes = true;
quoteChar = char;
}
else if (char === quoteChar && inQuotes) {
inQuotes = false;
quoteChar = '';
}
else if (char === ' ' && !inQuotes) {
if (current) {
args.push(current);
current = '';
}
}
else {
current += char;
}
}
if (current) {
args.push(current);
}
return args;
}
handleCancel() {
this.dispatchEvent(new CustomEvent('cancel'));
}
render() {
if (!this.visible) {
return (0, lit_1.html) ``;
}
return (0, lit_1.html) `
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="z-index: 9999;">
<div class="bg-vs-bg-secondary border border-vs-border font-mono text-sm w-96 max-w-full mx-4">
<div class="p-4 border-b border-vs-border flex justify-between items-center">
<div class="text-vs-assistant text-sm">Create New Session</div>
<button
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
@click=${this.handleCancel}
>×</button>
</div>
<div class="p-4">
<div class="mb-4">
<div class="text-vs-text mb-2">Working Directory:</div>
<div class="flex gap-4">
<input
type="text"
class="flex-1 bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
.value=${this.workingDir}
@input=${this.handleWorkingDirChange}
placeholder="~/"
?disabled=${this.disabled || this.isCreating}
/>
<button
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-2 border-none"
@click=${this.handleBrowse}
?disabled=${this.disabled || this.isCreating}
>
browse
</button>
</div>
</div>
<div class="mb-4">
<div class="text-vs-text mb-2">Command:</div>
<input
type="text"
class="w-full bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
.value=${this.command}
@input=${this.handleCommandChange}
@keydown=${(e) => e.key === 'Enter' && this.handleCreate()}
placeholder="zsh"
?disabled=${this.disabled || this.isCreating}
/>
</div>
<div class="flex gap-4 justify-end">
<button
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-4 py-2 border-none"
@click=${this.handleCancel}
?disabled=${this.isCreating}
>
cancel
</button>
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-vs-user"
@click=${this.handleCreate}
?disabled=${this.disabled || this.isCreating || !this.workingDir.trim() || !this.command.trim()}
>
${this.isCreating ? 'creating...' : 'create'}
</button>
</div>
</div>
</div>
</div>
<file-browser
.visible=${this.showFileBrowser}
.currentPath=${this.workingDir}
@directory-selected=${this.handleDirectorySelected}
@browser-cancel=${this.handleBrowserCancel}
></file-browser>
`;
}
};
exports.SessionCreateForm = SessionCreateForm;
__decorate([
(0, decorators_js_1.property)({ type: String }),
__metadata("design:type", Object)
], SessionCreateForm.prototype, "workingDir", void 0);
__decorate([
(0, decorators_js_1.property)({ type: String }),
__metadata("design:type", Object)
], SessionCreateForm.prototype, "command", void 0);
__decorate([
(0, decorators_js_1.property)({ type: Boolean }),
__metadata("design:type", Object)
], SessionCreateForm.prototype, "disabled", void 0);
__decorate([
(0, decorators_js_1.property)({ type: Boolean }),
__metadata("design:type", Object)
], SessionCreateForm.prototype, "visible", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionCreateForm.prototype, "isCreating", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionCreateForm.prototype, "showFileBrowser", void 0);
exports.SessionCreateForm = SessionCreateForm = __decorate([
(0, decorators_js_1.customElement)('session-create-form')
], SessionCreateForm);
//# sourceMappingURL=session-create-form.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"session-create-form.js","sourceRoot":"","sources":["../../../src/client/components/session-create-form.ts"],"names":[],"mappings":"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"}

View file

@ -1,181 +0,0 @@
"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");
const repeat_js_1 = require("lit/directives/repeat.js");
require("./session-create-form.js");
require("./session-card.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.cleaningExited = false;
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
handleRefresh() {
this.dispatchEvent(new CustomEvent('refresh'));
}
handleSessionSelect(e) {
const session = e.detail;
window.location.search = `?session=${session.id}`;
}
async handleSessionKill(e) {
const sessionId = e.detail;
if (this.killingSessionIds.has(sessionId))
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 }));
}
else {
this.dispatchEvent(new CustomEvent('error', { detail: 'Failed to kill session' }));
}
}
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 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) {
console.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 (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.handleCleanupExited}
?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=${(e) => this.dispatchEvent(new CustomEvent('hide-exited-change', { detail: e.target.checked }))}
>
<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>
${filteredSessions.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">
${(0, repeat_js_1.repeat)(filteredSessions, (session) => session.id, (session) => (0, lit_1.html) `
<session-card
.session=${session}
@session-select=${this.handleSessionSelect}
@session-kill=${this.handleSessionKill}>
</session-card>
`)}
</div>
`}
<session-create-form
.visible=${this.showCreateModal}
@session-created=${(e) => this.dispatchEvent(new CustomEvent('session-created', { detail: e.detail }))}
@cancel=${() => this.dispatchEvent(new CustomEvent('create-modal-close'))}
@error=${(e) => this.dispatchEvent(new CustomEvent('error', { detail: e.detail }))}
></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, "cleaningExited", void 0);
exports.SessionList = SessionList = __decorate([
(0, decorators_js_1.customElement)('session-list')
], SessionList);
//# sourceMappingURL=session-list.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"session-list.js","sourceRoot":"","sources":["../../../src/client/components/session-list.ts"],"names":[],"mappings":"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"}

View file

@ -1,727 +0,0 @@
"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.SessionView = void 0;
const lit_1 = require("lit");
const decorators_js_1 = require("lit/decorators.js");
const renderer_js_1 = require("../renderer.js");
let SessionView = class SessionView extends lit_1.LitElement {
constructor() {
super(...arguments);
this.session = null;
this.connected = false;
this.renderer = null;
this.sessionStatusInterval = null;
this.showMobileInput = false;
this.mobileInputText = '';
this.isMobile = false;
this.touchStartX = 0;
this.touchStartY = 0;
this.loading = false;
this.loadingFrame = 0;
this.loadingInterval = null;
this.keyboardHandler = (e) => {
if (!this.session)
return;
e.preventDefault();
e.stopPropagation();
this.handleKeyboardInput(e);
};
this.touchStartHandler = (e) => {
if (!this.isMobile)
return;
const touch = e.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
};
this.touchEndHandler = (e) => {
if (!this.isMobile)
return;
const touch = e.changedTouches[0];
const touchEndX = touch.clientX;
const touchEndY = touch.clientY;
const deltaX = touchEndX - this.touchStartX;
const deltaY = touchEndY - this.touchStartY;
// Check for horizontal swipe from left edge (back gesture)
const isSwipeRight = deltaX > 100;
const isVerticallyStable = Math.abs(deltaY) < 100;
const startedFromLeftEdge = this.touchStartX < 50;
if (isSwipeRight && isVerticallyStable && startedFromLeftEdge) {
// Trigger back navigation
this.handleBack();
}
};
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.connected = true;
// Show loading animation if no session yet
if (!this.session) {
this.startLoading();
}
// Detect mobile device
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth <= 768;
// Add global keyboard event listener only for desktop
if (!this.isMobile) {
document.addEventListener('keydown', this.keyboardHandler);
}
else {
// Add touch event listeners for mobile swipe gestures
document.addEventListener('touchstart', this.touchStartHandler, { passive: true });
document.addEventListener('touchend', this.touchEndHandler, { passive: true });
}
// Start polling session status
this.startSessionStatusPolling();
}
disconnectedCallback() {
super.disconnectedCallback();
this.connected = false;
// Remove global keyboard event listener
if (!this.isMobile) {
document.removeEventListener('keydown', this.keyboardHandler);
}
else {
// Remove touch event listeners
document.removeEventListener('touchstart', this.touchStartHandler);
document.removeEventListener('touchend', this.touchEndHandler);
}
// Stop polling session status
this.stopSessionStatusPolling();
// Stop loading animation
this.stopLoading();
// Cleanup renderer if it exists
if (this.renderer) {
this.renderer.dispose();
this.renderer = null;
}
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
if (this.session) {
this.stopLoading();
this.createInteractiveTerminal();
}
}
updated(changedProperties) {
super.updated(changedProperties);
// Stop loading and create terminal when session becomes available
if (changedProperties.has('session') && this.session && this.loading) {
this.stopLoading();
this.createInteractiveTerminal();
}
// Adjust terminal height for mobile buttons after render
if (changedProperties.has('showMobileInput') || changedProperties.has('isMobile')) {
requestAnimationFrame(() => {
this.adjustTerminalForMobileButtons();
});
}
}
createInteractiveTerminal() {
if (!this.session)
return;
const terminalElement = this.querySelector('#interactive-terminal');
if (!terminalElement)
return;
// Create renderer once and connect to current session
this.renderer = new renderer_js_1.Renderer(terminalElement);
// Wait a moment for freshly created sessions before connecting
const sessionAge = Date.now() - new Date(this.session.startedAt).getTime();
const delay = sessionAge < 5000 ? 2000 : 0; // 2 second delay if session is less than 5 seconds old
if (delay > 0) {
// Show loading animation during delay for fresh sessions
this.startLoading();
}
setTimeout(() => {
if (this.renderer && this.session) {
this.stopLoading(); // Stop loading before connecting
this.renderer.connectToStream(this.session.id);
}
}, delay);
// Listen for session exit events
terminalElement.addEventListener('session-exit', this.handleSessionExit.bind(this));
}
async handleKeyboardInput(e) {
if (!this.session)
return;
// Don't send input to exited sessions
if (this.session.status === 'exited') {
console.log('Ignoring keyboard input - session has exited');
return;
}
let inputText = '';
// Handle special keys
switch (e.key) {
case 'Enter':
if (e.ctrlKey) {
// Ctrl+Enter - send to tty-fwd for proper handling
inputText = 'ctrl_enter';
}
else if (e.shiftKey) {
// Shift+Enter - send to tty-fwd for proper handling
inputText = 'shift_enter';
}
else {
// Regular Enter
inputText = 'enter';
}
break;
case 'Escape':
inputText = 'escape';
break;
case 'ArrowUp':
inputText = 'arrow_up';
break;
case 'ArrowDown':
inputText = 'arrow_down';
break;
case 'ArrowLeft':
inputText = 'arrow_left';
break;
case 'ArrowRight':
inputText = 'arrow_right';
break;
case 'Tab':
inputText = '\t';
break;
case 'Backspace':
inputText = '\b';
break;
case 'Delete':
inputText = '\x7f';
break;
case ' ':
inputText = ' ';
break;
default:
// Handle regular printable characters
if (e.key.length === 1) {
inputText = e.key;
}
else {
// Ignore other special keys
return;
}
break;
}
// Handle Ctrl combinations (but not if we already handled Ctrl+Enter above)
if (e.ctrlKey && e.key.length === 1 && e.key !== 'Enter') {
const charCode = e.key.toLowerCase().charCodeAt(0);
if (charCode >= 97 && charCode <= 122) { // a-z
inputText = String.fromCharCode(charCode - 96); // Ctrl+A = \x01, etc.
}
}
// Send the input to the session
try {
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: inputText })
});
if (!response.ok) {
if (response.status === 400) {
console.log('Session no longer accepting input (likely exited)');
// Update session status to exited if we get 400 error
if (this.session && this.session.status !== 'exited') {
this.session = { ...this.session, status: 'exited' };
this.requestUpdate();
this.stopSessionStatusPolling();
}
}
else {
console.error('Failed to send input to session:', response.status);
}
}
}
catch (error) {
console.error('Error sending input:', error);
}
}
handleBack() {
window.location.search = '';
}
handleSessionExit(e) {
const customEvent = e;
console.log('Session exit event received:', customEvent.detail);
if (this.session && customEvent.detail.sessionId === this.session.id) {
// Update session status to exited
this.session = { ...this.session, status: 'exited' };
this.requestUpdate();
// Stop polling immediately
this.stopSessionStatusPolling();
// Switch to snapshot mode
requestAnimationFrame(() => {
this.createInteractiveTerminal();
});
}
}
// Mobile input methods
handleMobileInputToggle() {
this.showMobileInput = !this.showMobileInput;
if (this.showMobileInput) {
// Focus the textarea after a short delay to ensure it's rendered
requestAnimationFrame(() => {
const textarea = this.querySelector('#mobile-input-textarea');
if (textarea) {
textarea.focus();
this.adjustTextareaForKeyboard();
}
});
}
else {
// Clean up viewport listener when closing overlay
const textarea = this.querySelector('#mobile-input-textarea');
if (textarea && textarea._viewportCleanup) {
textarea._viewportCleanup();
}
}
}
adjustTextareaForKeyboard() {
// Adjust the layout when virtual keyboard appears
const textarea = this.querySelector('#mobile-input-textarea');
const controls = this.querySelector('#mobile-controls');
if (!textarea || !controls)
return;
const adjustLayout = () => {
const viewportHeight = window.visualViewport?.height || window.innerHeight;
const windowHeight = window.innerHeight;
const keyboardHeight = windowHeight - viewportHeight;
// If keyboard is visible (viewport height is significantly smaller)
if (keyboardHeight > 100) {
// Move controls above the keyboard
controls.style.transform = `translateY(-${keyboardHeight}px)`;
controls.style.transition = 'transform 0.3s ease';
// Calculate available space for textarea
const header = this.querySelector('.flex.items-center.justify-between.p-4.border-b');
const headerHeight = header?.offsetHeight || 60;
const controlsHeight = controls?.offsetHeight || 120;
const padding = 48; // Additional padding for spacing
// Available height is viewport height minus header and controls (controls are now above keyboard)
const maxTextareaHeight = viewportHeight - headerHeight - controlsHeight - padding;
const inputArea = textarea.parentElement;
if (inputArea && maxTextareaHeight > 0) {
// Set the input area to not exceed the available space
inputArea.style.height = `${maxTextareaHeight}px`;
inputArea.style.maxHeight = `${maxTextareaHeight}px`;
inputArea.style.overflow = 'hidden';
// Set textarea height within the container
const labelHeight = 40; // Height of the label above textarea
const textareaMaxHeight = Math.max(maxTextareaHeight - labelHeight, 80);
textarea.style.height = `${textareaMaxHeight}px`;
textarea.style.maxHeight = `${textareaMaxHeight}px`;
}
}
else {
// Reset position when keyboard is hidden
controls.style.transform = 'translateY(0px)';
controls.style.transition = 'transform 0.3s ease';
// Reset textarea height and constraints
const inputArea = textarea.parentElement;
if (inputArea) {
inputArea.style.height = '';
inputArea.style.maxHeight = '';
inputArea.style.overflow = '';
textarea.style.height = '';
textarea.style.maxHeight = '';
}
}
};
// Listen for viewport changes (keyboard show/hide)
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', adjustLayout);
// Clean up listener when overlay is closed
const cleanup = () => {
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', adjustLayout);
}
};
// Store cleanup function for later use
textarea._viewportCleanup = cleanup;
}
// Initial adjustment
requestAnimationFrame(adjustLayout);
}
handleMobileInputChange(e) {
const textarea = e.target;
this.mobileInputText = textarea.value;
}
async handleMobileInputSendOnly() {
// Get the current value from the textarea directly
const textarea = this.querySelector('#mobile-input-textarea');
const textToSend = textarea?.value?.trim() || this.mobileInputText.trim();
if (!textToSend)
return;
try {
// Send text without enter key
await this.sendInputText(textToSend);
// Clear both the reactive property and textarea
this.mobileInputText = '';
if (textarea) {
textarea.value = '';
}
// Trigger re-render to update button state
this.requestUpdate();
// Hide the input overlay after sending
this.showMobileInput = false;
}
catch (error) {
console.error('Error sending mobile input:', error);
// Don't hide the overlay if there was an error
}
}
async handleMobileInputSend() {
// Get the current value from the textarea directly
const textarea = this.querySelector('#mobile-input-textarea');
const textToSend = textarea?.value?.trim() || this.mobileInputText.trim();
if (!textToSend)
return;
try {
// Add enter key at the end to execute the command
await this.sendInputText(textToSend + '\n');
// Clear both the reactive property and textarea
this.mobileInputText = '';
if (textarea) {
textarea.value = '';
}
// Trigger re-render to update button state
this.requestUpdate();
// Hide the input overlay after sending
this.showMobileInput = false;
}
catch (error) {
console.error('Error sending mobile input:', error);
// Don't hide the overlay if there was an error
}
}
async handleSpecialKey(key) {
await this.sendInputText(key);
}
async sendInputText(text) {
if (!this.session)
return;
try {
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text })
});
if (!response.ok) {
console.error('Failed to send input to session');
}
}
catch (error) {
console.error('Error sending input:', error);
}
}
adjustTerminalForMobileButtons() {
// Disabled for now to avoid viewport issues
// The mobile buttons will overlay the terminal
}
startLoading() {
this.loading = true;
this.loadingFrame = 0;
this.loadingInterval = window.setInterval(() => {
this.loadingFrame = (this.loadingFrame + 1) % 4;
this.requestUpdate();
}, 200); // Update every 200ms for smooth animation
}
stopLoading() {
this.loading = false;
if (this.loadingInterval) {
clearInterval(this.loadingInterval);
this.loadingInterval = null;
}
}
getLoadingText() {
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
return frames[this.loadingFrame % frames.length];
}
startSessionStatusPolling() {
if (this.sessionStatusInterval) {
clearInterval(this.sessionStatusInterval);
}
// Only poll for running sessions - exited sessions don't need polling
if (this.session?.status !== 'exited') {
this.sessionStatusInterval = window.setInterval(() => {
this.checkSessionStatus();
}, 2000);
}
}
stopSessionStatusPolling() {
if (this.sessionStatusInterval) {
clearInterval(this.sessionStatusInterval);
this.sessionStatusInterval = null;
}
}
async checkSessionStatus() {
if (!this.session)
return;
try {
const response = await fetch('/api/sessions');
if (!response.ok)
return;
const sessions = await response.json();
const currentSession = sessions.find((s) => s.id === this.session.id);
if (currentSession && currentSession.status !== this.session.status) {
// Store old status before updating
const oldStatus = this.session.status;
// Session status changed
this.session = { ...this.session, status: currentSession.status };
this.requestUpdate();
// Session status polling is now only for detecting new sessions
// Exit events are handled via SSE stream directly
}
}
catch (error) {
console.error('Error checking session status:', error);
}
}
render() {
if (!this.session) {
return (0, lit_1.html) `
<div class="p-4 text-vs-muted">
No session selected
</div>
`;
}
return (0, lit_1.html) `
<style>
session-view *, session-view *:focus, session-view *:focus-visible {
outline: none !important;
box-shadow: none !important;
}
</style>
<div class="flex flex-col bg-vs-bg font-mono" style="height: 100vh; outline: none !important; box-shadow: none !important;">
<!-- Compact Header -->
<div class="flex items-center justify-between px-3 py-2 border-b border-vs-border bg-vs-bg-secondary text-sm">
<div class="flex items-center gap-3">
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-2 py-1 border-none rounded transition-colors text-xs"
@click=${this.handleBack}
>
BACK
</button>
<div class="text-vs-text">
<div class="text-vs-accent">${this.session.command}</div>
<div class="text-vs-muted text-xs">${this.session.workingDir}</div>
</div>
</div>
<div class="flex items-center gap-3 text-xs">
<span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
${this.session.status.toUpperCase()}
</span>
</div>
</div>
<!-- Terminal Container -->
<div class="flex-1 bg-black overflow-x-auto overflow-y-hidden min-h-0 relative" id="terminal-container">
<div id="interactive-terminal" class="w-full h-full"></div>
${this.loading ? (0, lit_1.html) `
<!-- Loading overlay -->
<div class="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center">
<div class="text-vs-text font-mono text-center">
<div class="text-2xl mb-2">${this.getLoadingText()}</div>
<div class="text-sm text-vs-muted">Connecting to session...</div>
</div>
</div>
` : ''}
</div>
<!-- Mobile Input Controls -->
${this.isMobile && !this.showMobileInput ? (0, lit_1.html) `
<div class="flex-shrink-0 p-4 bg-vs-bg">
<!-- First row: Arrow keys -->
<div class="flex gap-2 mb-2">
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_up')}
>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_down')}
>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_left')}
>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_right')}
>
</button>
</div>
<!-- Second row: Special keys -->
<div class="flex gap-2">
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\t')}
>
TAB
</button>
<button
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('enter')}
>
ENTER
</button>
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('escape')}
>
ESC
</button>
<button
class="bg-vs-error text-vs-text hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\x03')}
>
^C
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleMobileInputToggle}
>
TYPE
</button>
</div>
</div>
` : ''}
<!-- Full-Screen Input Overlay (only when opened) -->
${this.isMobile && this.showMobileInput ? (0, lit_1.html) `
<div class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col" style="height: 100vh; height: 100dvh;">
<!-- Input Header -->
<div class="flex items-center justify-between p-4 border-b border-vs-border flex-shrink-0">
<div class="text-vs-text font-mono text-sm">Terminal Input</div>
<button
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
@click=${this.handleMobileInputToggle}
>
×
</button>
</div>
<!-- Input Area with dynamic height -->
<div class="flex-1 p-4 flex flex-col min-h-0">
<div class="text-vs-muted text-sm mb-2 flex-shrink-0">
Type your command(s) below. Supports multiline input.
</div>
<textarea
id="mobile-input-textarea"
class="flex-1 bg-vs-bg text-vs-text border border-vs-border font-mono text-sm p-4 resize-none outline-none"
placeholder="Enter your command here..."
.value=${this.mobileInputText}
@input=${this.handleMobileInputChange}
@keydown=${(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleMobileInputSend();
}
}}
style="min-height: 120px; margin-bottom: 16px;"
></textarea>
</div>
<!-- Controls - Fixed above keyboard -->
<div id="mobile-controls" class="fixed bottom-0 left-0 right-0 p-4 border-t border-vs-border bg-vs-bg-secondary z-60" style="padding-bottom: max(1rem, env(safe-area-inset-bottom)); transform: translateY(0px);">
<!-- Send Buttons Row -->
<div class="flex gap-2 mb-3">
<button
class="flex-1 bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSendOnly}
?disabled=${!this.mobileInputText.trim()}
>
SEND
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSend}
?disabled=${!this.mobileInputText.trim()}
>
SEND + ENTER
</button>
</div>
<div class="text-vs-muted text-xs text-center">
SEND: text only SEND + ENTER: text with enter key
</div>
</div>
</div>
` : ''}
</div>
`;
}
};
exports.SessionView = SessionView;
__decorate([
(0, decorators_js_1.property)({ type: Object }),
__metadata("design:type", Object)
], SessionView.prototype, "session", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "connected", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "renderer", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "sessionStatusInterval", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "showMobileInput", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "mobileInputText", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "isMobile", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "touchStartX", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "touchStartY", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "loading", void 0);
__decorate([
(0, decorators_js_1.state)(),
__metadata("design:type", Object)
], SessionView.prototype, "loadingFrame", void 0);
exports.SessionView = SessionView = __decorate([
(0, decorators_js_1.customElement)('session-view')
], SessionView);
//# sourceMappingURL=session-view.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,7 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
// Entry point for renderer bundle - exports XTerm-based renderer
var renderer_1 = require("./renderer");
Object.defineProperty(exports, "Renderer", { enumerable: true, get: function () { return renderer_1.Renderer; } });
//# sourceMappingURL=renderer-entry.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"renderer-entry.js","sourceRoot":"","sources":["../../src/client/renderer-entry.ts"],"names":[],"mappings":";;;AAAA,iEAAiE;AACjE,uCAAsC;AAA7B,oGAAA,QAAQ,OAAA"}

View file

@ -1,310 +0,0 @@
"use strict";
// Terminal renderer for asciinema cast format using XTerm.js
// Professional-grade terminal emulation with full VT compatibility
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const xterm_1 = require("@xterm/xterm");
const addon_fit_1 = require("@xterm/addon-fit");
const addon_web_links_1 = require("@xterm/addon-web-links");
const scale_fit_addon_js_1 = require("./scale-fit-addon.js");
class Renderer {
constructor(container, width = 80, height = 20, scrollback = 1000000, fontSize = 14, isPreview = false) {
this.eventSource = null;
Renderer.activeCount++;
console.log(`Renderer constructor called (active: ${Renderer.activeCount})`);
this.container = container;
this.isPreview = isPreview;
// Create terminal with options similar to the custom renderer
this.terminal = new xterm_1.Terminal({
cols: width,
rows: height,
fontFamily: 'Monaco, "Lucida Console", monospace',
fontSize: fontSize,
lineHeight: 1.2,
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#ffffff',
cursorAccent: '#1e1e1e',
selectionBackground: '#264f78',
// VS Code Dark theme colors
black: '#000000',
red: '#f14c4c',
green: '#23d18b',
yellow: '#f5f543',
blue: '#3b8eea',
magenta: '#d670d6',
cyan: '#29b8db',
white: '#e5e5e5',
// Bright colors
brightBlack: '#666666',
brightRed: '#f14c4c',
brightGreen: '#23d18b',
brightYellow: '#f5f543',
brightBlue: '#3b8eea',
brightMagenta: '#d670d6',
brightCyan: '#29b8db',
brightWhite: '#ffffff'
},
allowProposedApi: true,
scrollback: scrollback, // Configurable scrollback buffer
convertEol: true,
altClickMovesCursor: false,
rightClickSelectsWord: false,
disableStdin: true, // We handle input separately
});
// Add addons
this.fitAddon = new addon_fit_1.FitAddon();
this.scaleFitAddon = new scale_fit_addon_js_1.ScaleFitAddon();
this.webLinksAddon = new addon_web_links_1.WebLinksAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.loadAddon(this.scaleFitAddon);
this.terminal.loadAddon(this.webLinksAddon);
this.setupDOM();
}
setupDOM() {
// Clear container and add CSS
this.container.innerHTML = '';
// Different styling for preview vs full terminals
if (this.isPreview) {
// No padding for previews, let container control sizing
this.container.style.padding = '0';
this.container.style.backgroundColor = '#1e1e1e';
this.container.style.overflow = 'hidden';
}
else {
// Full terminals get padding
this.container.style.padding = '10px';
this.container.style.backgroundColor = '#1e1e1e';
this.container.style.overflow = 'hidden';
}
// Create terminal wrapper
const terminalWrapper = document.createElement('div');
terminalWrapper.style.width = '100%';
terminalWrapper.style.height = '100%';
this.container.appendChild(terminalWrapper);
// Open terminal in the wrapper
this.terminal.open(terminalWrapper);
// Always use ScaleFitAddon for better scaling
this.scaleFitAddon.fit();
// Handle container resize
const resizeObserver = new ResizeObserver(() => {
this.scaleFitAddon.fit();
});
resizeObserver.observe(this.container);
}
// Public API methods - maintain compatibility with custom renderer
async loadCastFile(url) {
const response = await fetch(url);
const text = await response.text();
this.parseCastFile(text);
}
parseCastFile(content) {
const lines = content.trim().split('\n');
let header = null;
// Clear terminal
this.terminal.clear();
for (const line of lines) {
if (!line.trim())
continue;
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
// Header
header = parsed;
this.resize(parsed.width, parsed.height);
}
else if (Array.isArray(parsed) && parsed.length >= 3) {
// Event: [timestamp, type, data]
const event = {
timestamp: parsed[0],
type: parsed[1],
data: parsed[2]
};
if (event.type === 'o') {
this.processOutput(event.data);
}
else if (event.type === 'r') {
this.processResize(event.data);
}
}
}
catch (e) {
console.warn('Failed to parse cast line:', line);
}
}
}
processOutput(data) {
// XTerm handles all ANSI escape sequences automatically
this.terminal.write(data);
}
processResize(data) {
// Parse resize data in format "WIDTHxHEIGHT" (e.g., "80x24")
const match = data.match(/^(\d+)x(\d+)$/);
if (match) {
const width = parseInt(match[1], 10);
const height = parseInt(match[2], 10);
this.resize(width, height);
}
}
processEvent(event) {
if (event.type === 'o') {
this.processOutput(event.data);
}
else if (event.type === 'r') {
this.processResize(event.data);
}
}
resize(width, height) {
if (this.isPreview) {
// For previews, resize to session dimensions then apply scaling
this.terminal.resize(width, height);
}
// Always use ScaleFitAddon for consistent scaling behavior
this.scaleFitAddon.fit();
}
clear() {
this.terminal.clear();
}
// Stream support - connect to SSE endpoint
connectToStream(sessionId) {
console.log('connectToStream called for session:', sessionId);
return this.connectToUrl(`/api/sessions/${sessionId}/stream`);
}
// Connect to any SSE URL
connectToUrl(url) {
console.log('Creating new EventSource connection to:', url);
const eventSource = new EventSource(url);
// Don't clear terminal for live streams - just append new content
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.version && data.width && data.height) {
// Header
console.log('Received header:', data);
this.resize(data.width, data.height);
}
else if (Array.isArray(data) && data.length >= 3) {
// Check if this is an exit event
if (data[0] === 'exit') {
const exitCode = data[1];
const sessionId = data[2];
console.log(`Session ${sessionId} exited with code ${exitCode}`);
// Close the SSE connection immediately
if (this.eventSource) {
console.log('Closing SSE connection due to session exit');
this.eventSource.close();
this.eventSource = null;
}
// Dispatch custom event that session-view can listen to
const exitEvent = new CustomEvent('session-exit', {
detail: { sessionId, exitCode }
});
this.container.dispatchEvent(exitEvent);
return;
}
// Regular cast event
const castEvent = {
timestamp: data[0],
type: data[1],
data: data[2]
};
// Process event without verbose logging
this.processEvent(castEvent);
}
}
catch (e) {
console.warn('Failed to parse stream event:', event.data);
}
};
eventSource.onerror = (error) => {
console.error('Stream error:', error);
// Close the connection to prevent automatic reconnection attempts
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Stream closed, cleaning up...');
if (this.eventSource === eventSource) {
this.eventSource = null;
}
}
};
return eventSource;
}
// Load content from URL - pass isStream to determine how to handle it
async loadFromUrl(url, isStream) {
// Clean up existing connection
if (this.eventSource) {
console.log('Explicitly closing existing EventSource connection');
this.eventSource.close();
this.eventSource = null;
}
if (isStream) {
// It's a stream URL, connect via SSE (don't clear - append to existing content)
this.eventSource = this.connectToUrl(url);
}
else {
// It's a snapshot URL, clear first then load as cast file
this.terminal.clear();
await this.loadCastFile(url);
}
}
// Additional methods for terminal control
focus() {
this.terminal.focus();
}
blur() {
this.terminal.blur();
}
getTerminal() {
return this.terminal;
}
dispose() {
if (this.eventSource) {
console.log('Explicitly closing EventSource connection in dispose()');
this.eventSource.close();
this.eventSource = null;
}
this.terminal.dispose();
Renderer.activeCount--;
console.log(`Renderer disposed (active: ${Renderer.activeCount})`);
}
// Method to fit terminal to container (useful for responsive layouts)
fit() {
this.fitAddon.fit();
}
// Get terminal dimensions
getDimensions() {
return {
cols: this.terminal.cols,
rows: this.terminal.rows
};
}
// Write raw data to terminal (useful for testing)
write(data) {
this.terminal.write(data);
}
// Enable/disable input (though we keep it disabled by default)
setInputEnabled(enabled) {
// XTerm doesn't have a direct way to disable input, so we override onData
if (enabled) {
// Remove any existing handler first
this.terminal.onData(() => {
// Input is handled by the session component
});
}
else {
this.terminal.onData(() => {
// Do nothing - input disabled
});
}
}
// Disable all pointer events for previews so clicks pass through to parent
setPointerEventsEnabled(enabled) {
const terminalElement = this.container.querySelector('.xterm');
if (terminalElement) {
terminalElement.style.pointerEvents = enabled ? 'auto' : 'none';
}
}
}
exports.Renderer = Renderer;
Renderer.activeCount = 0;
//# sourceMappingURL=renderer.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,109 +0,0 @@
"use strict";
/**
* Custom FitAddon that scales font size to fit terminal columns to container width,
* then calculates optimal rows for the container height.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScaleFitAddon = void 0;
const MINIMUM_ROWS = 1;
const MIN_FONT_SIZE = 6;
const MAX_FONT_SIZE = 16;
class ScaleFitAddon {
activate(terminal) {
this._terminal = terminal;
}
dispose() { }
fit() {
const dims = this.proposeDimensions();
if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) {
return;
}
// Only resize rows, keep cols the same (font scaling handles width)
if (this._terminal.rows !== dims.rows) {
this._terminal.resize(this._terminal.cols, dims.rows);
}
}
proposeDimensions() {
if (!this._terminal?.element?.parentElement) {
return undefined;
}
// Get the renderer container (parent of parent - the one with 10px padding)
const terminalWrapper = this._terminal.element.parentElement;
const rendererContainer = terminalWrapper.parentElement;
if (!rendererContainer)
return undefined;
// Get container dimensions and exact padding
const containerStyle = window.getComputedStyle(rendererContainer);
const containerWidth = parseInt(containerStyle.getPropertyValue('width'));
const containerHeight = parseInt(containerStyle.getPropertyValue('height'));
const containerPadding = {
top: parseInt(containerStyle.getPropertyValue('padding-top')),
bottom: parseInt(containerStyle.getPropertyValue('padding-bottom')),
left: parseInt(containerStyle.getPropertyValue('padding-left')),
right: parseInt(containerStyle.getPropertyValue('padding-right'))
};
// Calculate exact available space using known padding
const availableWidth = containerWidth - containerPadding.left - containerPadding.right;
const availableHeight = containerHeight - containerPadding.top - containerPadding.bottom;
// Current terminal dimensions
const currentCols = this._terminal.cols;
// Calculate optimal font size to fit current cols in available width
// Character width is approximately 0.6 * fontSize for monospace fonts
const charWidthRatio = 0.6;
const calculatedFontSize = availableWidth / (currentCols * charWidthRatio);
const optimalFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, calculatedFontSize));
// Apply the calculated font size (outside of proposeDimensions to avoid recursion)
requestAnimationFrame(() => this.applyFontSize(optimalFontSize));
// Get the actual line height from the rendered XTerm element
const xtermElement = this._terminal.element;
const currentStyle = window.getComputedStyle(xtermElement);
const actualLineHeight = parseFloat(currentStyle.lineHeight);
// If we can't get the line height, fall back to configuration
const lineHeight = actualLineHeight || (optimalFontSize * (this._terminal.options.lineHeight || 1.2));
// Calculate how many rows fit with this line height
const optimalRows = Math.max(MINIMUM_ROWS, Math.floor(availableHeight / lineHeight));
return {
cols: currentCols, // Keep existing cols
rows: optimalRows // Fit as many rows as possible
};
}
applyFontSize(fontSize) {
if (!this._terminal?.element)
return;
// Prevent infinite recursion by checking if font size changed significantly
const currentFontSize = this._terminal.options.fontSize || 14;
if (Math.abs(fontSize - currentFontSize) < 0.1)
return;
const terminalElement = this._terminal.element;
// Update terminal's font size
this._terminal.options.fontSize = fontSize;
// Apply CSS font size to the element
terminalElement.style.fontSize = `${fontSize}px`;
// Force a refresh to apply the new font size
requestAnimationFrame(() => {
if (this._terminal) {
this._terminal.refresh(0, this._terminal.rows - 1);
}
});
}
/**
* Get the calculated font size that would fit the current columns in the container
*/
getOptimalFontSize() {
if (!this._terminal?.element?.parentElement) {
return this._terminal?.options.fontSize || 14;
}
const parentElement = this._terminal.element.parentElement;
const parentStyle = window.getComputedStyle(parentElement);
const parentWidth = parseInt(parentStyle.getPropertyValue('width'));
const elementStyle = window.getComputedStyle(this._terminal.element);
const paddingHor = parseInt(elementStyle.getPropertyValue('padding-left')) +
parseInt(elementStyle.getPropertyValue('padding-right'));
const availableWidth = parentWidth - paddingHor;
const charWidthRatio = 0.6;
const calculatedFontSize = availableWidth / (this._terminal.cols * charWidthRatio);
return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, calculatedFontSize));
}
}
exports.ScaleFitAddon = ScaleFitAddon;
//# sourceMappingURL=scale-fit-addon.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"scale-fit-addon.js","sourceRoot":"","sources":["../../src/client/scale-fit-addon.ts"],"names":[],"mappings":"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"}

403
web/dist/server-new.js vendored
View file

@ -1,403 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const http_1 = require("http");
const ws_1 = require("ws");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const child_process_1 = require("child_process");
const app = (0, express_1.default)();
const server = (0, http_1.createServer)(app);
const wss = new ws_1.WebSocketServer({ server });
const PORT = process.env.PORT || 3000;
// tty-fwd binary path - check multiple possible locations
const possibleTtyFwdPaths = [
path_1.default.resolve(__dirname, '..', '..', 'tty-fwd', 'target', 'release', 'tty-fwd'),
path_1.default.resolve(__dirname, '..', '..', '..', 'tty-fwd', 'target', 'release', 'tty-fwd'),
'tty-fwd' // System PATH
];
let TTY_FWD_PATH = '';
for (const pathToCheck of possibleTtyFwdPaths) {
if (fs_1.default.existsSync(pathToCheck)) {
TTY_FWD_PATH = pathToCheck;
break;
}
}
if (!TTY_FWD_PATH) {
console.error('tty-fwd binary not found. Please ensure it is built and available.');
process.exit(1);
}
const TTY_FWD_CONTROL_DIR = process.env.TTY_FWD_CONTROL_DIR || path_1.default.join(os_1.default.homedir(), '.vibetunnel');
// Ensure control directory exists
if (!fs_1.default.existsSync(TTY_FWD_CONTROL_DIR)) {
fs_1.default.mkdirSync(TTY_FWD_CONTROL_DIR, { recursive: true });
console.log(`Created control directory: ${TTY_FWD_CONTROL_DIR}`);
}
console.log(`Using tty-fwd at: ${TTY_FWD_PATH}`);
console.log(`Control directory: ${TTY_FWD_CONTROL_DIR}`);
// Helper function to execute tty-fwd commands
async function executeTtyFwd(args) {
return new Promise((resolve, reject) => {
const child = (0, child_process_1.spawn)(TTY_FWD_PATH, args);
let output = '';
child.stdout.on('data', (data) => {
output += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
resolve(output);
}
else {
reject(new Error(`tty-fwd failed with code ${code}`));
}
});
child.on('error', (error) => {
reject(error);
});
});
}
// Helper function to resolve paths with ~ expansion
function resolvePath(inputPath, fallback) {
if (!inputPath) {
return fallback || process.cwd();
}
if (inputPath.startsWith('~')) {
return path_1.default.join(os_1.default.homedir(), inputPath.slice(1));
}
return path_1.default.resolve(inputPath);
}
// Middleware
app.use(express_1.default.json());
app.use(express_1.default.static(path_1.default.join(__dirname, '..', 'public')));
// Hot reload functionality for development
const hotReloadClients = new Set();
// === SESSION MANAGEMENT ===
// List all sessions
app.get('/api/sessions', async (req, res) => {
try {
const output = await executeTtyFwd(['--control-path', TTY_FWD_CONTROL_DIR, '--list-sessions']);
const sessions = JSON.parse(output || '{}');
const sessionData = Object.entries(sessions).map(([sessionId, sessionInfo]) => {
// Get actual last modified time from stream-out file
let lastModified = sessionInfo.started_at;
try {
if (fs_1.default.existsSync(sessionInfo["stream-out"])) {
const stats = fs_1.default.statSync(sessionInfo["stream-out"]);
lastModified = stats.mtime.toISOString();
}
}
catch (e) {
// Use started_at as fallback
}
return {
id: sessionId,
command: sessionInfo.cmdline.join(' '),
workingDir: sessionInfo.cwd,
status: sessionInfo.status,
exitCode: sessionInfo.exit_code,
startedAt: sessionInfo.started_at,
lastModified: lastModified,
pid: sessionInfo.pid
};
});
// Sort by lastModified, most recent first
sessionData.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
res.json(sessionData);
}
catch (error) {
console.error('Failed to list sessions:', error);
res.status(500).json({ error: 'Failed to list sessions' });
}
});
// Create new session
app.post('/api/sessions', async (req, res) => {
try {
const { command, workingDir } = req.body;
if (!command || !Array.isArray(command)) {
return res.status(400).json({ error: 'Command array is required' });
}
const sessionName = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const cwd = resolvePath(workingDir, process.cwd());
const args = [
'--control-path', TTY_FWD_CONTROL_DIR,
'--session-name', sessionName,
'--'
].concat(command);
console.log(`Creating session: ${TTY_FWD_PATH} ${args.join(' ')}`);
const child = (0, child_process_1.spawn)(TTY_FWD_PATH, args, {
cwd: cwd,
detached: true,
stdio: 'ignore'
});
child.unref();
// Respond immediately - session creation is detached
res.json({ sessionId: sessionName });
}
catch (error) {
console.error('Error creating session:', error);
res.status(500).json({ error: 'Failed to create session' });
}
});
// Kill session (just kill the process)
app.delete('/api/sessions/:sessionId', async (req, res) => {
const sessionId = req.params.sessionId;
try {
const output = await executeTtyFwd(['--control-path', TTY_FWD_CONTROL_DIR, '--list-sessions']);
const sessions = JSON.parse(output || '{}');
const session = sessions[sessionId];
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (session.pid) {
try {
process.kill(session.pid, 'SIGTERM');
setTimeout(() => {
try {
process.kill(session.pid, 0); // Check if still alive
process.kill(session.pid, 'SIGKILL'); // Force kill
}
catch (e) {
// Process already dead
}
}, 1000);
}
catch (error) {
// Process already dead
}
}
res.json({ success: true, message: 'Session killed' });
}
catch (error) {
console.error('Error killing session:', error);
res.status(500).json({ error: 'Failed to kill session' });
}
});
// Cleanup session files
app.delete('/api/sessions/:sessionId/cleanup', async (req, res) => {
const sessionId = req.params.sessionId;
try {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--cleanup'
]);
res.json({ success: true, message: 'Session cleaned up' });
}
catch (error) {
// If tty-fwd cleanup fails, force remove directory
console.log('tty-fwd cleanup failed, force removing directory');
const sessionDir = path_1.default.join(TTY_FWD_CONTROL_DIR, sessionId);
try {
if (fs_1.default.existsSync(sessionDir)) {
fs_1.default.rmSync(sessionDir, { recursive: true, force: true });
}
res.json({ success: true, message: 'Session force cleaned up' });
}
catch (fsError) {
console.error('Error force removing session directory:', fsError);
res.status(500).json({ error: 'Failed to cleanup session' });
}
}
});
// === TERMINAL I/O ===
// Server-sent events for terminal output streaming
app.get('/api/sessions/:sessionId/stream', (req, res) => {
const sessionId = req.params.sessionId;
const streamOutPath = path_1.default.join(TTY_FWD_CONTROL_DIR, sessionId, 'stream-out');
if (!fs_1.default.existsSync(streamOutPath)) {
return res.status(404).json({ error: 'Session not found' });
}
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
});
const startTime = Date.now() / 1000;
let headerSent = false;
// Send existing content first
// NOTE: Small race condition possible between reading file and starting tail
try {
const content = fs_1.default.readFileSync(streamOutPath, 'utf8');
const lines = content.trim().split('\n');
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
res.write(`data: ${line}\n\n`);
headerSent = true;
}
else if (Array.isArray(parsed) && parsed.length >= 3) {
const instantEvent = [0, parsed[1], parsed[2]];
res.write(`data: ${JSON.stringify(instantEvent)}\n\n`);
}
}
catch (e) {
// Skip invalid lines
}
}
}
}
catch (error) {
console.error('Error reading existing content:', error);
}
// Send default header if none found
if (!headerSent) {
const defaultHeader = {
version: 2,
width: 80,
height: 24,
timestamp: Math.floor(startTime),
env: { TERM: "xterm-256color" }
};
res.write(`data: ${JSON.stringify(defaultHeader)}\n\n`);
}
// Stream new content
const tailProcess = (0, child_process_1.spawn)('tail', ['-f', streamOutPath]);
let buffer = '';
tailProcess.stdout.on('data', (chunk) => {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
return; // Skip duplicate headers
}
if (Array.isArray(parsed) && parsed.length >= 3) {
const currentTime = Date.now() / 1000;
const realTimeEvent = [currentTime - startTime, parsed[1], parsed[2]];
res.write(`data: ${JSON.stringify(realTimeEvent)}\n\n`);
}
}
catch (e) {
// Handle non-JSON as raw output
const currentTime = Date.now() / 1000;
const castEvent = [currentTime - startTime, "o", line];
res.write(`data: ${JSON.stringify(castEvent)}\n\n`);
}
}
}
});
// Cleanup on disconnect
req.on('close', () => tailProcess.kill('SIGTERM'));
req.on('aborted', () => tailProcess.kill('SIGTERM'));
});
// Send input to session
app.post('/api/sessions/:sessionId/input', async (req, res) => {
const sessionId = req.params.sessionId;
const { text } = req.body;
if (text === undefined || text === null) {
return res.status(400).json({ error: 'Text is required' });
}
console.log(`Sending input to session ${sessionId}:`, JSON.stringify(text));
try {
// Check if this is a special key that should use --send-key
const specialKeys = ['arrow_up', 'arrow_down', 'arrow_left', 'arrow_right', 'escape', 'enter'];
const isSpecialKey = specialKeys.includes(text);
if (isSpecialKey) {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--send-key', text
]);
console.log(`Successfully sent key: ${text}`);
}
else {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--send-text', text
]);
console.log(`Successfully sent text: ${text}`);
}
res.json({ success: true });
}
catch (error) {
console.error('Error sending input via tty-fwd:', error);
res.status(500).json({ error: 'Failed to send input' });
}
});
// === FILE SYSTEM ===
// Directory listing for file browser
app.get('/api/fs/browse', (req, res) => {
const dirPath = req.query.path || '~';
try {
const expandedPath = resolvePath(dirPath, '~');
if (!fs_1.default.existsSync(expandedPath)) {
return res.status(404).json({ error: 'Directory not found' });
}
const stats = fs_1.default.statSync(expandedPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: 'Path is not a directory' });
}
const files = fs_1.default.readdirSync(expandedPath).map(name => {
const filePath = path_1.default.join(expandedPath, name);
const fileStats = fs_1.default.statSync(filePath);
return {
name,
created: fileStats.birthtime.toISOString(),
lastModified: fileStats.mtime.toISOString(),
size: fileStats.size,
isDir: fileStats.isDirectory()
};
});
res.json({
absolutePath: expandedPath,
files: files.sort((a, b) => {
// Directories first, then files
if (a.isDir && !b.isDir)
return -1;
if (!a.isDir && b.isDir)
return 1;
return a.name.localeCompare(b.name);
})
});
}
catch (error) {
console.error('Error listing directory:', error);
res.status(500).json({ error: 'Failed to list directory' });
}
});
// === WEBSOCKETS ===
// WebSocket for hot reload
wss.on('connection', (ws, req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const isHotReload = url.searchParams.get('hotReload') === 'true';
if (isHotReload) {
hotReloadClients.add(ws);
ws.on('close', () => {
hotReloadClients.delete(ws);
});
return;
}
ws.close(1008, 'Only hot reload connections supported');
});
// Hot reload file watching in development
if (process.env.NODE_ENV !== 'production') {
const chokidar = require('chokidar');
const watcher = chokidar.watch(['public/**/*', 'src/**/*'], {
ignored: /node_modules/,
persistent: true
});
watcher.on('change', (path) => {
console.log(`File changed: ${path}`);
hotReloadClients.forEach((ws) => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));
}
});
});
}
server.listen(PORT, () => {
console.log(`VibeTunnel New Server running on http://localhost:${PORT}`);
console.log(`Using tty-fwd: ${TTY_FWD_PATH}`);
});

746
web/dist/server.js vendored
View file

@ -1,746 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const http_1 = require("http");
const ws_1 = require("ws");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const child_process_1 = require("child_process");
const app = (0, express_1.default)();
const server = (0, http_1.createServer)(app);
const wss = new ws_1.WebSocketServer({ server });
const PORT = process.env.PORT || 3000;
// tty-fwd binary path - check multiple possible locations
const possibleTtyFwdPaths = [
path_1.default.resolve(__dirname, '..', '..', 'tty-fwd', 'target', 'release', 'tty-fwd'),
path_1.default.resolve(__dirname, '..', '..', '..', 'tty-fwd', 'target', 'release', 'tty-fwd'),
'tty-fwd' // System PATH
];
let TTY_FWD_PATH = '';
for (const pathToCheck of possibleTtyFwdPaths) {
if (fs_1.default.existsSync(pathToCheck)) {
TTY_FWD_PATH = pathToCheck;
break;
}
}
if (!TTY_FWD_PATH) {
console.error('tty-fwd binary not found. Please ensure it is built and available.');
process.exit(1);
}
const TTY_FWD_CONTROL_DIR = process.env.TTY_FWD_CONTROL_DIR || path_1.default.join(os_1.default.homedir(), '.vibetunnel');
// Ensure control directory exists and is clean
if (fs_1.default.existsSync(TTY_FWD_CONTROL_DIR)) {
// Clean existing directory contents
try {
const files = fs_1.default.readdirSync(TTY_FWD_CONTROL_DIR);
for (const file of files) {
const filePath = path_1.default.join(TTY_FWD_CONTROL_DIR, file);
const stat = fs_1.default.statSync(filePath);
if (stat.isDirectory()) {
fs_1.default.rmSync(filePath, { recursive: true, force: true });
}
else {
fs_1.default.unlinkSync(filePath);
}
}
console.log(`Cleaned control directory: ${TTY_FWD_CONTROL_DIR}`);
}
catch (error) {
console.error('Error cleaning control directory:', error);
}
}
else {
fs_1.default.mkdirSync(TTY_FWD_CONTROL_DIR, { recursive: true });
console.log(`Created control directory: ${TTY_FWD_CONTROL_DIR}`);
}
console.log(`Using tty-fwd at: ${TTY_FWD_PATH}`);
console.log(`Control directory: ${TTY_FWD_CONTROL_DIR}`);
// Helper function to execute tty-fwd commands
async function executeTtyFwd(args) {
return new Promise((resolve, reject) => {
const child = (0, child_process_1.spawn)(TTY_FWD_PATH, args);
let output = '';
let isResolved = false;
// Set a timeout to prevent hanging
const timeout = setTimeout(() => {
if (!isResolved) {
isResolved = true;
child.kill('SIGTERM');
reject(new Error('tty-fwd command timed out after 5 seconds'));
}
}, 5000);
child.stdout.on('data', (data) => {
output += data.toString();
});
child.on('close', (code) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
if (code === 0) {
resolve(output);
}
else {
reject(new Error(`tty-fwd failed with code ${code}`));
}
}
});
child.on('error', (error) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
reject(error);
}
});
});
}
// Helper function to resolve paths with ~ expansion
function resolvePath(inputPath, fallback) {
if (!inputPath) {
return fallback || process.cwd();
}
if (inputPath.startsWith('~')) {
return path_1.default.join(os_1.default.homedir(), inputPath.slice(1));
}
return path_1.default.resolve(inputPath);
}
// Middleware
app.use(express_1.default.json());
app.use(express_1.default.static(path_1.default.join(__dirname, '..', 'public')));
// Hot reload functionality for development
const hotReloadClients = new Set();
// === SESSION MANAGEMENT ===
// List all sessions
app.get('/api/sessions', async (req, res) => {
try {
const output = await executeTtyFwd(['--control-path', TTY_FWD_CONTROL_DIR, '--list-sessions']);
const sessions = JSON.parse(output || '{}');
const sessionData = Object.entries(sessions).map(([sessionId, sessionInfo]) => {
// Get actual last modified time from stream-out file
let lastModified = sessionInfo.started_at;
try {
if (fs_1.default.existsSync(sessionInfo["stream-out"])) {
const stats = fs_1.default.statSync(sessionInfo["stream-out"]);
lastModified = stats.mtime.toISOString();
}
}
catch (e) {
// Use started_at as fallback
}
return {
id: sessionId,
command: sessionInfo.cmdline.join(' '),
workingDir: sessionInfo.cwd,
status: sessionInfo.status,
exitCode: sessionInfo.exit_code,
startedAt: sessionInfo.started_at,
lastModified: lastModified,
pid: sessionInfo.pid
};
});
// Sort by lastModified, most recent first
sessionData.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
res.json(sessionData);
}
catch (error) {
console.error('Failed to list sessions:', error);
res.status(500).json({ error: 'Failed to list sessions' });
}
});
// Create new session
app.post('/api/sessions', async (req, res) => {
try {
const { command, workingDir } = req.body;
if (!command || !Array.isArray(command) || command.length === 0) {
return res.status(400).json({ error: 'Command array is required and cannot be empty' });
}
const sessionName = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const cwd = resolvePath(workingDir, process.cwd());
const args = [
'--control-path', TTY_FWD_CONTROL_DIR,
'--session-name', sessionName,
'--'
].concat(command);
console.log(`Creating session: ${TTY_FWD_PATH} ${args.join(' ')}`);
const child = (0, child_process_1.spawn)(TTY_FWD_PATH, args, {
cwd: cwd,
detached: false,
stdio: 'pipe'
});
// Capture session ID from stdout
let sessionId = '';
child.stdout.on('data', (data) => {
const output = data.toString().trim();
if (output && !sessionId) {
// First line of output should be the session ID
sessionId = output;
console.log(`Session created with ID: ${sessionId}`);
}
});
child.stderr.on('data', (data) => {
// Only log stderr if it contains actual errors
const output = data.toString();
if (output.includes('error') || output.includes('Error')) {
console.error(`Session ${sessionName} stderr:`, output);
}
});
child.on('close', async (code) => {
console.log(`Session ${sessionId || sessionName} exited with code: ${code}`);
// Send exit event to all clients watching this session
const streamInfo = activeStreams.get(sessionId);
if (streamInfo) {
console.log(`Sending exit event to stream ${sessionId}`);
const exitEvent = JSON.stringify(['exit', code, sessionId]);
const eventData = `data: ${exitEvent}\n\n`;
streamInfo.clients.forEach(client => {
try {
client.write(eventData);
}
catch (error) {
console.error('Error sending exit event to client:', error);
}
});
}
});
// Wait for session ID from tty-fwd or timeout after 3 seconds
const waitForSessionId = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Failed to get session ID from tty-fwd within 3 seconds'));
}, 3000);
const checkSessionId = () => {
if (sessionId) {
clearTimeout(timeout);
resolve(sessionId);
}
else {
setTimeout(checkSessionId, 100);
}
};
checkSessionId();
});
const finalSessionId = await waitForSessionId;
res.json({ sessionId: finalSessionId });
}
catch (error) {
console.error('Error creating session:', error);
res.status(500).json({ error: 'Failed to create session' });
}
});
// Kill session (just kill the process)
app.delete('/api/sessions/:sessionId', async (req, res) => {
const sessionId = req.params.sessionId;
try {
const output = await executeTtyFwd(['--control-path', TTY_FWD_CONTROL_DIR, '--list-sessions']);
const sessions = JSON.parse(output || '{}');
const session = sessions[sessionId];
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (session.pid) {
try {
process.kill(session.pid, 'SIGTERM');
setTimeout(() => {
try {
process.kill(session.pid, 0); // Check if still alive
process.kill(session.pid, 'SIGKILL'); // Force kill
}
catch (e) {
// Process already dead
}
}, 1000);
}
catch (error) {
// Process already dead
}
}
res.json({ success: true, message: 'Session killed' });
}
catch (error) {
console.error('Error killing session:', error);
res.status(500).json({ error: 'Failed to kill session' });
}
});
// Cleanup session files
app.delete('/api/sessions/:sessionId/cleanup', async (req, res) => {
const sessionId = req.params.sessionId;
try {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--cleanup'
]);
res.json({ success: true, message: 'Session cleaned up' });
}
catch (error) {
// If tty-fwd cleanup fails, force remove directory
console.log('tty-fwd cleanup failed, force removing directory');
const sessionDir = path_1.default.join(TTY_FWD_CONTROL_DIR, sessionId);
try {
if (fs_1.default.existsSync(sessionDir)) {
fs_1.default.rmSync(sessionDir, { recursive: true, force: true });
}
res.json({ success: true, message: 'Session force cleaned up' });
}
catch (fsError) {
console.error('Error force removing session directory:', fsError);
res.status(500).json({ error: 'Failed to cleanup session' });
}
}
});
// Cleanup all exited sessions
app.post('/api/cleanup-exited', async (req, res) => {
try {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--cleanup'
]);
res.json({ success: true, message: 'All exited sessions cleaned up' });
}
catch (error) {
console.error('Error cleaning up exited sessions:', error);
res.status(500).json({ error: 'Failed to cleanup exited sessions' });
}
});
// === TERMINAL I/O ===
// Track active streams per session to avoid multiple tail processes
const activeStreams = new Map();
// Live streaming cast file for XTerm renderer
app.get('/api/sessions/:sessionId/stream', async (req, res) => {
const sessionId = req.params.sessionId;
const streamOutPath = path_1.default.join(TTY_FWD_CONTROL_DIR, sessionId, 'stream-out');
if (!fs_1.default.existsSync(streamOutPath)) {
return res.status(404).json({ error: 'Session not found' });
}
console.log(`New SSE client connected to session ${sessionId} from ${req.get('User-Agent')?.substring(0, 50) || 'unknown'}`);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
});
const startTime = Date.now() / 1000;
let headerSent = false;
// Send existing content first
try {
const content = fs_1.default.readFileSync(streamOutPath, 'utf8');
const lines = content.trim().split('\n');
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
res.write(`data: ${line}\n\n`);
headerSent = true;
}
else if (Array.isArray(parsed) && parsed.length >= 3) {
const instantEvent = [0, parsed[1], parsed[2]];
res.write(`data: ${JSON.stringify(instantEvent)}\n\n`);
}
}
catch (e) {
// Skip invalid lines
}
}
}
}
catch (error) {
console.error('Error reading existing content:', error);
}
// Send default header if none found
if (!headerSent) {
const defaultHeader = {
version: 2,
width: 80,
height: 24,
timestamp: Math.floor(startTime),
env: { TERM: "xterm-256color" }
};
res.write(`data: ${JSON.stringify(defaultHeader)}\n\n`);
}
// Get or create shared stream for this session
let streamInfo = activeStreams.get(sessionId);
if (!streamInfo) {
console.log(`Creating new shared tail process for session ${sessionId}`);
// Create new tail process for this session
const tailProcess = (0, child_process_1.spawn)('tail', ['-f', streamOutPath]);
let buffer = '';
streamInfo = {
clients: new Set(),
tailProcess,
lastPosition: 0
};
activeStreams.set(sessionId, streamInfo);
// Handle tail output - broadcast to all clients
tailProcess.stdout.on('data', (chunk) => {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
let eventData;
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
continue; // Skip duplicate headers
}
if (Array.isArray(parsed) && parsed.length >= 3) {
const currentTime = Date.now() / 1000;
const realTimeEvent = [currentTime - startTime, parsed[1], parsed[2]];
eventData = `data: ${JSON.stringify(realTimeEvent)}\n\n`;
}
}
catch (e) {
// Handle non-JSON as raw output
const currentTime = Date.now() / 1000;
const castEvent = [currentTime - startTime, "o", line];
eventData = `data: ${JSON.stringify(castEvent)}\n\n`;
}
if (eventData && streamInfo) {
// Broadcast to all connected clients
streamInfo.clients.forEach(client => {
try {
client.write(eventData);
}
catch (error) {
console.error('Error writing to client:', error);
if (streamInfo) {
streamInfo.clients.delete(client);
console.log(`Removed failed client from session ${sessionId}, remaining clients: ${streamInfo.clients.size}`);
}
}
});
}
}
}
});
tailProcess.on('error', (error) => {
console.error(`Shared tail process error for session ${sessionId}:`, error);
// Cleanup all clients
const currentStreamInfo = activeStreams.get(sessionId);
if (currentStreamInfo) {
currentStreamInfo.clients.forEach(client => {
try {
client.end();
}
catch (e) { }
});
}
activeStreams.delete(sessionId);
});
tailProcess.on('exit', (code) => {
console.log(`Shared tail process exited for session ${sessionId} with code ${code}`);
// Cleanup all clients
const currentStreamInfo = activeStreams.get(sessionId);
if (currentStreamInfo) {
currentStreamInfo.clients.forEach(client => {
try {
client.end();
}
catch (e) { }
});
}
activeStreams.delete(sessionId);
});
}
// Add this client to the shared stream
streamInfo.clients.add(res);
console.log(`Added client to session ${sessionId}, total clients: ${streamInfo.clients.size}`);
// Cleanup when client disconnects
const cleanup = () => {
if (streamInfo && streamInfo.clients.has(res)) {
streamInfo.clients.delete(res);
console.log(`Removed client from session ${sessionId}, remaining clients: ${streamInfo.clients.size}`);
// If no more clients, cleanup the tail process
if (streamInfo.clients.size === 0) {
console.log(`No more clients for session ${sessionId}, cleaning up tail process`);
try {
streamInfo.tailProcess.kill('SIGTERM');
}
catch (e) { }
activeStreams.delete(sessionId);
}
}
};
req.on('close', cleanup);
req.on('aborted', cleanup);
req.on('error', cleanup);
res.on('close', cleanup);
res.on('finish', cleanup);
});
// Get session snapshot (cast with adjusted timestamps for immediate playback)
app.get('/api/sessions/:sessionId/snapshot', (req, res) => {
const sessionId = req.params.sessionId;
const streamOutPath = path_1.default.join(TTY_FWD_CONTROL_DIR, sessionId, 'stream-out');
if (!fs_1.default.existsSync(streamOutPath)) {
return res.status(404).json({ error: 'Session not found' });
}
try {
const content = fs_1.default.readFileSync(streamOutPath, 'utf8');
const lines = content.trim().split('\n');
let header = null;
const events = [];
let startTime = null;
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
// Header line
if (parsed.version && parsed.width && parsed.height) {
header = parsed;
}
// Event line [timestamp, type, data]
else if (Array.isArray(parsed) && parsed.length >= 3) {
if (startTime === null) {
startTime = parsed[0];
}
events.push([0, parsed[1], parsed[2]]);
}
}
catch (e) {
// Skip invalid lines
}
}
}
// Build the complete cast
const cast = [];
// Add header if found, otherwise use default
if (header) {
cast.push(JSON.stringify(header));
}
else {
cast.push(JSON.stringify({
version: 2,
width: 80,
height: 24,
timestamp: Math.floor(Date.now() / 1000),
env: { TERM: "xterm-256color" }
}));
}
// Add all events
events.forEach(event => {
cast.push(JSON.stringify(event));
});
res.setHeader('Content-Type', 'text/plain');
res.send(cast.join('\n'));
}
catch (error) {
console.error('Error reading session snapshot:', error);
res.status(500).json({ error: 'Failed to read session snapshot' });
}
});
// Send input to session
app.post('/api/sessions/:sessionId/input', async (req, res) => {
const sessionId = req.params.sessionId;
const { text } = req.body;
if (text === undefined || text === null) {
return res.status(400).json({ error: 'Text is required' });
}
console.log(`Sending input to session ${sessionId}:`, JSON.stringify(text));
try {
// Validate session exists and is running
const output = await executeTtyFwd(['--control-path', TTY_FWD_CONTROL_DIR, '--list-sessions']);
const sessions = JSON.parse(output || '{}');
if (!sessions[sessionId]) {
console.error(`Session ${sessionId} not found in active sessions`);
return res.status(404).json({ error: 'Session not found' });
}
const session = sessions[sessionId];
if (session.status !== 'running') {
console.error(`Session ${sessionId} is not running (status: ${session.status})`);
return res.status(400).json({ error: 'Session is not running' });
}
// Check if the process is actually still alive
if (session.pid) {
try {
process.kill(session.pid, 0); // Signal 0 just checks if process exists
}
catch (error) {
console.error(`Session ${sessionId} process ${session.pid} is dead, cleaning up`);
// Try to cleanup the stale session
try {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--cleanup'
]);
}
catch (cleanupError) {
console.error('Failed to cleanup stale session:', cleanupError);
}
return res.status(410).json({ error: 'Session process has died' });
}
}
// Check if this is a special key that should use --send-key
const specialKeys = ['arrow_up', 'arrow_down', 'arrow_left', 'arrow_right', 'escape', 'enter', 'ctrl_enter', 'shift_enter'];
const isSpecialKey = specialKeys.includes(text);
const startTime = Date.now();
if (isSpecialKey) {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--send-key', text
]);
// Key sent successfully (removed verbose logging)
}
else {
await executeTtyFwd([
'--control-path', TTY_FWD_CONTROL_DIR,
'--session', sessionId,
'--send-text', text
]);
// Text sent successfully (removed verbose logging)
}
res.json({ success: true });
}
catch (error) {
console.error('Error sending input via tty-fwd:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ error: 'Failed to send input', details: errorMessage });
}
});
// === CAST FILE SERVING ===
// Serve test cast file
app.get('/api/test-cast', (req, res) => {
const testCastPath = path_1.default.join(__dirname, '..', 'public', 'stream-out');
try {
if (fs_1.default.existsSync(testCastPath)) {
res.setHeader('Content-Type', 'text/plain');
const content = fs_1.default.readFileSync(testCastPath, 'utf8');
res.send(content);
}
else {
res.status(404).json({ error: 'Test cast file not found' });
}
}
catch (error) {
console.error('Error serving test cast file:', error);
res.status(500).json({ error: 'Failed to serve test cast file' });
}
});
// === FILE SYSTEM ===
// Directory listing for file browser
app.get('/api/fs/browse', (req, res) => {
const dirPath = req.query.path || '~';
try {
const expandedPath = resolvePath(dirPath, '~');
if (!fs_1.default.existsSync(expandedPath)) {
return res.status(404).json({ error: 'Directory not found' });
}
const stats = fs_1.default.statSync(expandedPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: 'Path is not a directory' });
}
const files = fs_1.default.readdirSync(expandedPath).map(name => {
const filePath = path_1.default.join(expandedPath, name);
const fileStats = fs_1.default.statSync(filePath);
return {
name,
created: fileStats.birthtime.toISOString(),
lastModified: fileStats.mtime.toISOString(),
size: fileStats.size,
isDir: fileStats.isDirectory()
};
});
res.json({
absolutePath: expandedPath,
files: files.sort((a, b) => {
// Directories first, then files
if (a.isDir && !b.isDir)
return -1;
if (!a.isDir && b.isDir)
return 1;
return a.name.localeCompare(b.name);
})
});
}
catch (error) {
console.error('Error listing directory:', error);
res.status(500).json({ error: 'Failed to list directory' });
}
});
// Create directory
app.post('/api/mkdir', (req, res) => {
try {
const { path: dirPath, name } = req.body;
if (!dirPath || !name) {
return res.status(400).json({ error: 'Missing path or name parameter' });
}
// Validate directory name (no path separators, no hidden files starting with .)
if (name.includes('/') || name.includes('\\') || name.startsWith('.')) {
return res.status(400).json({ error: 'Invalid directory name' });
}
// Expand tilde in path
const expandedPath = dirPath.startsWith('~')
? path_1.default.join(os_1.default.homedir(), dirPath.slice(1))
: path_1.default.resolve(dirPath);
// Security check: ensure we're not trying to access outside allowed areas
const allowedBasePaths = [os_1.default.homedir(), process.cwd()];
const isAllowed = allowedBasePaths.some(basePath => expandedPath.startsWith(path_1.default.resolve(basePath)));
if (!isAllowed) {
return res.status(403).json({ error: 'Access denied' });
}
// Check if parent directory exists
if (!fs_1.default.existsSync(expandedPath)) {
return res.status(404).json({ error: 'Parent directory not found' });
}
const stats = fs_1.default.statSync(expandedPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: 'Parent path is not a directory' });
}
const newDirPath = path_1.default.join(expandedPath, name);
// Check if directory already exists
if (fs_1.default.existsSync(newDirPath)) {
return res.status(409).json({ error: 'Directory already exists' });
}
// Create the directory
fs_1.default.mkdirSync(newDirPath, { recursive: false });
res.json({
success: true,
path: newDirPath,
message: `Directory '${name}' created successfully`
});
}
catch (error) {
console.error('Error creating directory:', error);
res.status(500).json({ error: 'Failed to create directory' });
}
});
// === WEBSOCKETS ===
// WebSocket for hot reload
wss.on('connection', (ws, req) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const isHotReload = url.searchParams.get('hotReload') === 'true';
if (isHotReload) {
hotReloadClients.add(ws);
ws.on('close', () => {
hotReloadClients.delete(ws);
});
return;
}
ws.close(1008, 'Only hot reload connections supported');
});
// Hot reload file watching in development
if (process.env.NODE_ENV !== 'production') {
const chokidar = require('chokidar');
const watcher = chokidar.watch(['public/**/*', 'src/**/*'], {
ignored: /node_modules/,
persistent: true
});
watcher.on('change', (path) => {
console.log(`File changed: ${path}`);
hotReloadClients.forEach((ws) => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));
}
});
});
}
server.listen(PORT, () => {
console.log(`VibeTunnel New Server running on http://localhost:${PORT}`);
console.log(`Using tty-fwd: ${TTY_FWD_PATH}`);
});
//# sourceMappingURL=server.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,3 +0,0 @@
// Entry point for the app
import './app.js';
//# sourceMappingURL=app-entry.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"app-entry.js","sourceRoot":"","sources":["../src/client/app-entry.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,OAAO,UAAU,CAAC","sourcesContent":["// Entry point for the app\nimport './app.js';"]}

View file

@ -1,298 +0,0 @@
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;
};
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
// Import components
import './components/app-header.js';
import './components/session-create-form.js';
import './components/session-list.js';
import './components/session-view.js';
import './components/session-card.js';
let VibeTunnelApp = class VibeTunnelApp extends LitElement {
constructor() {
super(...arguments);
this.errorMessage = '';
this.sessions = [];
this.loading = false;
this.currentView = 'list';
this.selectedSession = null;
this.hideExited = true;
this.showCreateModal = false;
this.hotReloadWs = null;
this.handlePopState = (event) => {
// Handle browser back/forward navigation
this.parseUrlAndSetState();
};
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.setupHotReload();
this.loadSessions();
this.startAutoRefresh();
this.setupRouting();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.hotReloadWs) {
this.hotReloadWs.close();
}
// Clean up routing listeners
window.removeEventListener('popstate', this.handlePopState);
}
showError(message) {
this.errorMessage = message;
// Clear error after 5 seconds
setTimeout(() => {
this.errorMessage = '';
}, 5000);
}
clearError() {
this.errorMessage = '';
}
async loadSessions() {
this.loading = true;
try {
const response = await fetch('/api/sessions');
if (response.ok) {
const sessionsData = await response.json();
this.sessions = sessionsData.map((session) => ({
id: session.id,
command: session.command,
workingDir: session.workingDir,
status: session.status,
exitCode: session.exitCode,
startedAt: session.startedAt,
lastModified: session.lastModified,
pid: session.pid
}));
this.clearError();
}
else {
this.showError('Failed to load sessions');
}
}
catch (error) {
console.error('Error loading sessions:', error);
this.showError('Failed to load sessions');
}
finally {
this.loading = false;
}
}
startAutoRefresh() {
// Refresh sessions every 3 seconds, but only when showing session list
setInterval(() => {
if (this.currentView === 'list') {
this.loadSessions();
}
}, 3000);
}
async handleSessionCreated(e) {
const sessionId = e.detail.sessionId;
if (!sessionId) {
this.showError('Session created but ID not found in response');
return;
}
this.showCreateModal = false;
// Wait for session to appear in the list and then switch to it
await this.waitForSessionAndSwitch(sessionId);
}
async waitForSessionAndSwitch(sessionId) {
const maxAttempts = 10;
const delay = 500; // 500ms between attempts
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await this.loadSessions();
// Try to find by exact ID match first
let session = this.sessions.find(s => s.id === sessionId);
// If not found by ID, find the most recently created session
// This works around tty-fwd potentially using different IDs internally
if (!session && this.sessions.length > 0) {
const sortedSessions = [...this.sessions].sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
session = sortedSessions[0];
}
if (session) {
// Session found, switch to session view
this.selectedSession = session;
this.currentView = 'session';
// Update URL to include session ID
this.updateUrl(session.id);
return;
}
// Wait before next attempt
await new Promise(resolve => setTimeout(resolve, delay));
}
// If we get here, session creation might have failed
console.log('Session not found after all attempts');
this.showError('Session created but could not be found. Please refresh.');
}
handleSessionSelect(e) {
const session = e.detail;
console.log('Session selected:', session);
this.selectedSession = session;
this.currentView = 'session';
// Update URL to include session ID
this.updateUrl(session.id);
}
handleBack() {
this.currentView = 'list';
this.selectedSession = null;
// Update URL to remove session parameter
this.updateUrl();
}
handleSessionKilled(e) {
console.log('Session killed:', e.detail);
this.loadSessions(); // Refresh the list
}
handleRefresh() {
this.loadSessions();
}
handleError(e) {
this.showError(e.detail);
}
handleHideExitedChange(e) {
this.hideExited = e.detail;
}
handleCreateSession() {
this.showCreateModal = true;
}
handleCreateModalClose() {
this.showCreateModal = false;
}
// URL Routing methods
setupRouting() {
// Handle browser back/forward navigation
window.addEventListener('popstate', this.handlePopState.bind(this));
// Parse initial URL and set state
this.parseUrlAndSetState();
}
parseUrlAndSetState() {
const url = new URL(window.location.href);
const sessionId = url.searchParams.get('session');
if (sessionId) {
// Load the specific session
this.loadSessionFromUrl(sessionId);
}
else {
// Show session list
this.currentView = 'list';
this.selectedSession = null;
}
}
async loadSessionFromUrl(sessionId) {
// First ensure sessions are loaded
if (this.sessions.length === 0) {
await this.loadSessions();
}
// Find the session
const session = this.sessions.find(s => s.id === sessionId);
if (session) {
this.selectedSession = session;
this.currentView = 'session';
}
else {
// Session not found, go to list view
this.currentView = 'list';
this.selectedSession = null;
// Update URL to remove invalid session ID
this.updateUrl();
}
}
updateUrl(sessionId) {
const url = new URL(window.location.href);
if (sessionId) {
url.searchParams.set('session', sessionId);
}
else {
url.searchParams.delete('session');
}
// Update browser URL without triggering page reload
window.history.pushState(null, '', url.toString());
}
setupHotReload() {
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}?hotReload=true`;
this.hotReloadWs = new WebSocket(wsUrl);
this.hotReloadWs.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'reload') {
window.location.reload();
}
};
}
}
render() {
return html `
<!-- Error notification overlay -->
${this.errorMessage ? html `
<div class="fixed top-4 right-4 z-50">
<div class="bg-vs-warning text-vs-bg px-4 py-2 rounded shadow-lg font-mono text-sm">
${this.errorMessage}
<button @click=${this.clearError} class="ml-2 text-vs-bg hover:text-vs-muted"></button>
</div>
</div>
` : ''}
<!-- Main content -->
${this.currentView === 'session' ? html `
<session-view
.session=${this.selectedSession}
@back=${this.handleBack}
></session-view>
` : html `
<div class="max-w-4xl mx-auto">
<app-header
@create-session=${this.handleCreateSession}
></app-header>
<session-list
.sessions=${this.sessions}
.loading=${this.loading}
.hideExited=${this.hideExited}
.showCreateModal=${this.showCreateModal}
@session-select=${this.handleSessionSelect}
@session-killed=${this.handleSessionKilled}
@session-created=${this.handleSessionCreated}
@create-modal-close=${this.handleCreateModalClose}
@refresh=${this.handleRefresh}
@error=${this.handleError}
@hide-exited-change=${this.handleHideExitedChange}
></session-list>
</div>
`}
`;
}
};
__decorate([
state()
], VibeTunnelApp.prototype, "errorMessage", void 0);
__decorate([
state()
], VibeTunnelApp.prototype, "sessions", void 0);
__decorate([
state()
], VibeTunnelApp.prototype, "loading", void 0);
__decorate([
state()
], VibeTunnelApp.prototype, "currentView", void 0);
__decorate([
state()
], VibeTunnelApp.prototype, "selectedSession", void 0);
__decorate([
state()
], VibeTunnelApp.prototype, "hideExited", void 0);
__decorate([
state()
], VibeTunnelApp.prototype, "showCreateModal", void 0);
VibeTunnelApp = __decorate([
customElement('vibetunnel-app')
], VibeTunnelApp);
export { VibeTunnelApp };
//# sourceMappingURL=app.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,36 +0,0 @@
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;
};
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
let AppHeader = class AppHeader extends LitElement {
createRenderRoot() {
return this;
}
handleCreateSession() {
this.dispatchEvent(new CustomEvent('create-session'));
}
render() {
return html `
<div class="p-4 border-b border-vs-border">
<div class="flex items-center justify-between">
<div class="text-vs-user font-mono text-sm">VibeTunnel</div>
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleCreateSession}
>
CREATE SESSION
</button>
</div>
</div>
`;
}
};
AppHeader = __decorate([
customElement('app-header')
], AppHeader);
export { AppHeader };
//# sourceMappingURL=app-header.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"app-header.js","sourceRoot":"","sources":["../../src/client/components/app-header.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG3C,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;IACvC,gBAAgB;QACd,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,mBAAmB;QACzB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;;;;;qBAMM,IAAI,CAAC,mBAAmB;;;;;;KAMxC,CAAC;IACJ,CAAC;CACF,CAAA;AAxBY,SAAS;IADrB,aAAa,CAAC,YAAY,CAAC;GACf,SAAS,CAwBrB","sourcesContent":["import { LitElement, html } from 'lit';\nimport { customElement } from 'lit/decorators.js';\n\n@customElement('app-header')\nexport class AppHeader extends LitElement {\n createRenderRoot() {\n return this;\n }\n\n private handleCreateSession() {\n this.dispatchEvent(new CustomEvent('create-session'));\n }\n\n render() {\n return html`\n <div class=\"p-4 border-b border-vs-border\">\n <div class=\"flex items-center justify-between\">\n <div class=\"text-vs-user font-mono text-sm\">VibeTunnel</div>\n <button\n class=\"bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none rounded transition-colors text-sm\"\n @click=${this.handleCreateSession}\n >\n CREATE SESSION\n </button>\n </div>\n </div>\n `;\n }\n}"]}

View file

@ -1,256 +0,0 @@
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;
};
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
let FileBrowser = class FileBrowser extends LitElement {
constructor() {
super(...arguments);
this.currentPath = '~';
this.visible = false;
this.files = [];
this.loading = false;
this.showCreateFolder = false;
this.newFolderName = '';
this.creating = false;
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
async connectedCallback() {
super.connectedCallback();
if (this.visible) {
await this.loadDirectory(this.currentPath);
}
}
async updated(changedProperties) {
if (changedProperties.has('visible') && this.visible) {
await this.loadDirectory(this.currentPath);
}
}
async loadDirectory(dirPath) {
this.loading = true;
try {
const response = await fetch(`/api/fs/browse?path=${encodeURIComponent(dirPath)}`);
if (response.ok) {
const data = await response.json();
this.currentPath = data.absolutePath;
this.files = data.files;
}
else {
console.error('Failed to load directory');
}
}
catch (error) {
console.error('Error loading directory:', error);
}
finally {
this.loading = false;
}
}
handleDirectoryClick(dirName) {
const newPath = this.currentPath + '/' + dirName;
this.loadDirectory(newPath);
}
handleParentClick() {
const parentPath = this.currentPath.split('/').slice(0, -1).join('/') || '/';
this.loadDirectory(parentPath);
}
handleSelect() {
this.dispatchEvent(new CustomEvent('directory-selected', {
detail: this.currentPath
}));
}
handleCancel() {
this.dispatchEvent(new CustomEvent('browser-cancel'));
}
handleCreateFolder() {
this.showCreateFolder = true;
this.newFolderName = '';
}
handleCancelCreateFolder() {
this.showCreateFolder = false;
this.newFolderName = '';
}
handleFolderNameInput(e) {
this.newFolderName = e.target.value;
}
handleFolderNameKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
this.createFolder();
}
else if (e.key === 'Escape') {
e.preventDefault();
this.handleCancelCreateFolder();
}
}
async createFolder() {
if (!this.newFolderName.trim())
return;
this.creating = true;
try {
const response = await fetch('/api/mkdir', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: this.currentPath,
name: this.newFolderName.trim()
})
});
if (response.ok) {
// Refresh directory listing
await this.loadDirectory(this.currentPath);
this.handleCancelCreateFolder();
}
else {
const error = await response.json();
alert(`Failed to create folder: ${error.error}`);
}
}
catch (error) {
console.error('Error creating folder:', error);
alert('Failed to create folder');
}
finally {
this.creating = false;
}
}
render() {
if (!this.visible) {
return html ``;
}
return html `
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="z-index: 9999;">
<div class="bg-vs-bg-secondary border border-vs-border font-mono text-sm w-96 h-96 flex flex-col">
<div class="p-4 border-b border-vs-border flex-shrink-0">
<div class="flex justify-between items-center mb-2">
<div class="text-vs-assistant text-sm">Select Directory</div>
<button
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-2 py-1 text-xs border-none rounded"
@click=${this.handleCreateFolder}
?disabled=${this.loading}
title="Create new folder"
>
+ folder
</button>
</div>
<div class="text-vs-muted text-sm break-all">${this.currentPath}</div>
</div>
<div class="p-4 flex-1 overflow-y-auto">
${this.loading ? html `
<div class="text-vs-muted">Loading...</div>
` : html `
${this.currentPath !== '/' ? html `
<div
class="flex items-center gap-2 p-2 hover:bg-vs-nav-hover cursor-pointer text-vs-accent"
@click=${this.handleParentClick}
>
<span>📁</span>
<span>.. (parent directory)</span>
</div>
` : ''}
${this.files.filter(f => f.isDir).map(file => html `
<div
class="flex items-center gap-2 p-2 hover:bg-vs-nav-hover cursor-pointer text-vs-accent"
@click=${() => this.handleDirectoryClick(file.name)}
>
<span>📁</span>
<span>${file.name}</span>
</div>
`)}
${this.files.filter(f => !f.isDir).map(file => html `
<div class="flex items-center gap-2 p-2 text-vs-muted">
<span>📄</span>
<span>${file.name}</span>
</div>
`)}
`}
</div>
<!-- Create folder dialog -->
${this.showCreateFolder ? html `
<div class="p-4 border-t border-vs-border flex-shrink-0">
<div class="text-vs-assistant text-sm mb-2">Create New Folder</div>
<div class="flex gap-2">
<input
type="text"
class="flex-1 bg-vs-bg border border-vs-border text-vs-text px-2 py-1 text-sm font-mono"
placeholder="Folder name"
.value=${this.newFolderName}
@input=${this.handleFolderNameInput}
@keydown=${this.handleFolderNameKeydown}
?disabled=${this.creating}
/>
<button
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-2 py-1 text-xs border-none"
@click=${this.createFolder}
?disabled=${this.creating || !this.newFolderName.trim()}
>
${this.creating ? '...' : 'create'}
</button>
<button
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-2 py-1 text-xs border-none"
@click=${this.handleCancelCreateFolder}
?disabled=${this.creating}
>
cancel
</button>
</div>
</div>
` : ''}
<div class="p-4 border-t border-vs-border flex gap-4 justify-end flex-shrink-0">
<button
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-4 py-2 border-none"
@click=${this.handleCancel}
>
cancel
</button>
<button
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-4 py-2 border-none"
@click=${this.handleSelect}
>
select
</button>
</div>
</div>
</div>
`;
}
};
__decorate([
property({ type: String })
], FileBrowser.prototype, "currentPath", void 0);
__decorate([
property({ type: Boolean })
], FileBrowser.prototype, "visible", void 0);
__decorate([
state()
], FileBrowser.prototype, "files", void 0);
__decorate([
state()
], FileBrowser.prototype, "loading", void 0);
__decorate([
state()
], FileBrowser.prototype, "showCreateFolder", void 0);
__decorate([
state()
], FileBrowser.prototype, "newFolderName", void 0);
__decorate([
state()
], FileBrowser.prototype, "creating", void 0);
FileBrowser = __decorate([
customElement('file-browser')
], FileBrowser);
export { FileBrowser };
//# sourceMappingURL=file-browser.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,165 +0,0 @@
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;
};
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Renderer } from '../renderer.js';
let SessionCard = class SessionCard extends LitElement {
constructor() {
super(...arguments);
this.renderer = null;
this.refreshInterval = null;
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.createRenderer();
this.startRefresh();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (this.renderer) {
this.renderer.dispose();
this.renderer = null;
}
}
createRenderer() {
const playerElement = this.querySelector('#player');
if (!playerElement)
return;
// Create single renderer for this card - use larger dimensions for better preview
this.renderer = new Renderer(playerElement, 80, 24, 10000, 8, true);
// Always use snapshot endpoint for cards
const url = `/api/sessions/${this.session.id}/snapshot`;
// Wait a moment for freshly created sessions before connecting
const sessionAge = Date.now() - new Date(this.session.startedAt).getTime();
const delay = sessionAge < 5000 ? 2000 : 0; // 2 second delay if session is less than 5 seconds old
setTimeout(() => {
if (this.renderer) {
this.renderer.loadFromUrl(url, false); // false = not a stream, use snapshot
// Disable pointer events so clicks pass through to the card
this.renderer.setPointerEventsEnabled(false);
}
}, delay);
}
startRefresh() {
this.refreshInterval = window.setInterval(() => {
if (this.renderer) {
const url = `/api/sessions/${this.session.id}/snapshot`;
this.renderer.loadFromUrl(url, false);
// Ensure pointer events stay disabled after refresh
this.renderer.setPointerEventsEnabled(false);
}
}, 10000); // Refresh every 10 seconds
}
handleCardClick() {
this.dispatchEvent(new CustomEvent('session-select', {
detail: this.session,
bubbles: true,
composed: true
}));
}
handleKillClick(e) {
e.stopPropagation();
e.preventDefault();
this.dispatchEvent(new CustomEvent('session-kill', {
detail: this.session.id,
bubbles: true,
composed: true
}));
}
async handlePidClick(e) {
e.stopPropagation();
e.preventDefault();
if (this.session.pid) {
try {
await navigator.clipboard.writeText(this.session.pid.toString());
console.log('PID copied to clipboard:', this.session.pid);
}
catch (error) {
console.error('Failed to copy PID to clipboard:', error);
// Fallback: select text manually
this.fallbackCopyToClipboard(this.session.pid.toString());
}
}
}
fallbackCopyToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
console.log('PID copied to clipboard (fallback):', text);
}
catch (error) {
console.error('Fallback copy failed:', error);
}
document.body.removeChild(textArea);
}
render() {
const isRunning = this.session.status === 'running';
return html `
<div class="bg-vs-bg border border-vs-border rounded shadow cursor-pointer overflow-hidden"
@click=${this.handleCardClick}>
<!-- 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">${this.session.command}</div>
${this.session.status === 'running' ? 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=${this.handleKillClick}
>
${this.session.status === 'running' ? 'kill' : 'clean'}
</button>
` : ''}
</div>
<!-- XTerm renderer (main content) -->
<div class="session-preview bg-black overflow-hidden" style="aspect-ratio: 640/480;">
<div id="player" class="w-full h-full"></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="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'} text-xs">
${this.session.status}
</span>
${this.session.pid ? html `
<span
class="cursor-pointer hover:text-vs-accent transition-colors"
@click=${this.handlePidClick}
title="Click to copy PID"
>
PID: ${this.session.pid} <span class="opacity-50">(click to copy)</span>
</span>
` : ''}
</div>
<div class="truncate text-xs opacity-75" title="${this.session.workingDir}">${this.session.workingDir}</div>
</div>
</div>
`;
}
};
__decorate([
property({ type: Object })
], SessionCard.prototype, "session", void 0);
__decorate([
state()
], SessionCard.prototype, "renderer", void 0);
SessionCard = __decorate([
customElement('session-card')
], SessionCard);
export { SessionCard };
//# sourceMappingURL=session-card.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,223 +0,0 @@
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;
};
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import './file-browser.js';
let SessionCreateForm = class SessionCreateForm extends LitElement {
constructor() {
super(...arguments);
this.workingDir = '~/';
this.command = 'zsh';
this.disabled = false;
this.visible = false;
this.isCreating = false;
this.showFileBrowser = false;
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
handleWorkingDirChange(e) {
const input = e.target;
this.workingDir = input.value;
this.dispatchEvent(new CustomEvent('working-dir-change', {
detail: this.workingDir
}));
}
handleCommandChange(e) {
const input = e.target;
this.command = input.value;
}
handleBrowse() {
this.showFileBrowser = true;
}
handleDirectorySelected(e) {
this.workingDir = e.detail;
this.showFileBrowser = false;
}
handleBrowserCancel() {
this.showFileBrowser = false;
}
async handleCreate() {
if (!this.workingDir.trim() || !this.command.trim()) {
this.dispatchEvent(new CustomEvent('error', {
detail: 'Please fill in both working directory and command'
}));
return;
}
this.isCreating = true;
const sessionData = {
command: this.parseCommand(this.command.trim()),
workingDir: this.workingDir.trim()
};
try {
const response = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sessionData)
});
if (response.ok) {
const result = await response.json();
this.command = ''; // Clear command on success
this.dispatchEvent(new CustomEvent('session-created', {
detail: result
}));
}
else {
const error = await response.json();
this.dispatchEvent(new CustomEvent('error', {
detail: `Failed to create session: ${error.error}`
}));
}
}
catch (error) {
console.error('Error creating session:', error);
this.dispatchEvent(new CustomEvent('error', {
detail: 'Failed to create session'
}));
}
finally {
this.isCreating = false;
}
}
parseCommand(commandStr) {
// Simple command parsing - split by spaces but respect quotes
const args = [];
let current = '';
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < commandStr.length; i++) {
const char = commandStr[i];
if ((char === '"' || char === "'") && !inQuotes) {
inQuotes = true;
quoteChar = char;
}
else if (char === quoteChar && inQuotes) {
inQuotes = false;
quoteChar = '';
}
else if (char === ' ' && !inQuotes) {
if (current) {
args.push(current);
current = '';
}
}
else {
current += char;
}
}
if (current) {
args.push(current);
}
return args;
}
handleCancel() {
this.dispatchEvent(new CustomEvent('cancel'));
}
render() {
if (!this.visible) {
return html ``;
}
return html `
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="z-index: 9999;">
<div class="bg-vs-bg-secondary border border-vs-border font-mono text-sm w-96 max-w-full mx-4">
<div class="p-4 border-b border-vs-border flex justify-between items-center">
<div class="text-vs-assistant text-sm">Create New Session</div>
<button
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
@click=${this.handleCancel}
>×</button>
</div>
<div class="p-4">
<div class="mb-4">
<div class="text-vs-text mb-2">Working Directory:</div>
<div class="flex gap-4">
<input
type="text"
class="flex-1 bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
.value=${this.workingDir}
@input=${this.handleWorkingDirChange}
placeholder="~/"
?disabled=${this.disabled || this.isCreating}
/>
<button
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-2 border-none"
@click=${this.handleBrowse}
?disabled=${this.disabled || this.isCreating}
>
browse
</button>
</div>
</div>
<div class="mb-4">
<div class="text-vs-text mb-2">Command:</div>
<input
type="text"
class="w-full bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
.value=${this.command}
@input=${this.handleCommandChange}
@keydown=${(e) => e.key === 'Enter' && this.handleCreate()}
placeholder="zsh"
?disabled=${this.disabled || this.isCreating}
/>
</div>
<div class="flex gap-4 justify-end">
<button
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-4 py-2 border-none"
@click=${this.handleCancel}
?disabled=${this.isCreating}
>
cancel
</button>
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-vs-user"
@click=${this.handleCreate}
?disabled=${this.disabled || this.isCreating || !this.workingDir.trim() || !this.command.trim()}
>
${this.isCreating ? 'creating...' : 'create'}
</button>
</div>
</div>
</div>
</div>
<file-browser
.visible=${this.showFileBrowser}
.currentPath=${this.workingDir}
@directory-selected=${this.handleDirectorySelected}
@browser-cancel=${this.handleBrowserCancel}
></file-browser>
`;
}
};
__decorate([
property({ type: String })
], SessionCreateForm.prototype, "workingDir", void 0);
__decorate([
property({ type: String })
], SessionCreateForm.prototype, "command", void 0);
__decorate([
property({ type: Boolean })
], SessionCreateForm.prototype, "disabled", void 0);
__decorate([
property({ type: Boolean })
], SessionCreateForm.prototype, "visible", void 0);
__decorate([
state()
], SessionCreateForm.prototype, "isCreating", void 0);
__decorate([
state()
], SessionCreateForm.prototype, "showFileBrowser", void 0);
SessionCreateForm = __decorate([
customElement('session-create-form')
], SessionCreateForm);
export { SessionCreateForm };
//# sourceMappingURL=session-create-form.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,710 +0,0 @@
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;
};
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Renderer } from '../renderer.js';
let SessionView = class SessionView extends LitElement {
constructor() {
super(...arguments);
this.session = null;
this.connected = false;
this.renderer = null;
this.sessionStatusInterval = null;
this.showMobileInput = false;
this.mobileInputText = '';
this.isMobile = false;
this.touchStartX = 0;
this.touchStartY = 0;
this.loading = false;
this.loadingFrame = 0;
this.loadingInterval = null;
this.keyboardHandler = (e) => {
if (!this.session)
return;
e.preventDefault();
e.stopPropagation();
this.handleKeyboardInput(e);
};
this.touchStartHandler = (e) => {
if (!this.isMobile)
return;
const touch = e.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
};
this.touchEndHandler = (e) => {
if (!this.isMobile)
return;
const touch = e.changedTouches[0];
const touchEndX = touch.clientX;
const touchEndY = touch.clientY;
const deltaX = touchEndX - this.touchStartX;
const deltaY = touchEndY - this.touchStartY;
// Check for horizontal swipe from left edge (back gesture)
const isSwipeRight = deltaX > 100;
const isVerticallyStable = Math.abs(deltaY) < 100;
const startedFromLeftEdge = this.touchStartX < 50;
if (isSwipeRight && isVerticallyStable && startedFromLeftEdge) {
// Trigger back navigation
this.handleBack();
}
};
}
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.connected = true;
// Show loading animation if no session yet
if (!this.session) {
this.startLoading();
}
// Detect mobile device
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth <= 768;
// Add global keyboard event listener only for desktop
if (!this.isMobile) {
document.addEventListener('keydown', this.keyboardHandler);
}
else {
// Add touch event listeners for mobile swipe gestures
document.addEventListener('touchstart', this.touchStartHandler, { passive: true });
document.addEventListener('touchend', this.touchEndHandler, { passive: true });
}
// Start polling session status
this.startSessionStatusPolling();
}
disconnectedCallback() {
super.disconnectedCallback();
this.connected = false;
// Remove global keyboard event listener
if (!this.isMobile) {
document.removeEventListener('keydown', this.keyboardHandler);
}
else {
// Remove touch event listeners
document.removeEventListener('touchstart', this.touchStartHandler);
document.removeEventListener('touchend', this.touchEndHandler);
}
// Stop polling session status
this.stopSessionStatusPolling();
// Stop loading animation
this.stopLoading();
// Cleanup renderer if it exists
if (this.renderer) {
this.renderer.dispose();
this.renderer = null;
}
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
if (this.session) {
this.stopLoading();
this.createInteractiveTerminal();
}
}
updated(changedProperties) {
super.updated(changedProperties);
// Stop loading and create terminal when session becomes available
if (changedProperties.has('session') && this.session && this.loading) {
this.stopLoading();
this.createInteractiveTerminal();
}
// Adjust terminal height for mobile buttons after render
if (changedProperties.has('showMobileInput') || changedProperties.has('isMobile')) {
requestAnimationFrame(() => {
this.adjustTerminalForMobileButtons();
});
}
}
createInteractiveTerminal() {
if (!this.session)
return;
const terminalElement = this.querySelector('#interactive-terminal');
if (!terminalElement)
return;
// Create renderer once and connect to current session
this.renderer = new Renderer(terminalElement);
// Wait a moment for freshly created sessions before connecting
const sessionAge = Date.now() - new Date(this.session.startedAt).getTime();
const delay = sessionAge < 5000 ? 2000 : 0; // 2 second delay if session is less than 5 seconds old
if (delay > 0) {
// Show loading animation during delay for fresh sessions
this.startLoading();
}
setTimeout(() => {
if (this.renderer && this.session) {
this.stopLoading(); // Stop loading before connecting
this.renderer.connectToStream(this.session.id);
}
}, delay);
// Listen for session exit events
terminalElement.addEventListener('session-exit', this.handleSessionExit.bind(this));
}
async handleKeyboardInput(e) {
if (!this.session)
return;
// Don't send input to exited sessions
if (this.session.status === 'exited') {
console.log('Ignoring keyboard input - session has exited');
return;
}
let inputText = '';
// Handle special keys
switch (e.key) {
case 'Enter':
if (e.ctrlKey) {
// Ctrl+Enter - send to tty-fwd for proper handling
inputText = 'ctrl_enter';
}
else if (e.shiftKey) {
// Shift+Enter - send to tty-fwd for proper handling
inputText = 'shift_enter';
}
else {
// Regular Enter
inputText = 'enter';
}
break;
case 'Escape':
inputText = 'escape';
break;
case 'ArrowUp':
inputText = 'arrow_up';
break;
case 'ArrowDown':
inputText = 'arrow_down';
break;
case 'ArrowLeft':
inputText = 'arrow_left';
break;
case 'ArrowRight':
inputText = 'arrow_right';
break;
case 'Tab':
inputText = '\t';
break;
case 'Backspace':
inputText = '\b';
break;
case 'Delete':
inputText = '\x7f';
break;
case ' ':
inputText = ' ';
break;
default:
// Handle regular printable characters
if (e.key.length === 1) {
inputText = e.key;
}
else {
// Ignore other special keys
return;
}
break;
}
// Handle Ctrl combinations (but not if we already handled Ctrl+Enter above)
if (e.ctrlKey && e.key.length === 1 && e.key !== 'Enter') {
const charCode = e.key.toLowerCase().charCodeAt(0);
if (charCode >= 97 && charCode <= 122) { // a-z
inputText = String.fromCharCode(charCode - 96); // Ctrl+A = \x01, etc.
}
}
// Send the input to the session
try {
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: inputText })
});
if (!response.ok) {
if (response.status === 400) {
console.log('Session no longer accepting input (likely exited)');
// Update session status to exited if we get 400 error
if (this.session && this.session.status !== 'exited') {
this.session = { ...this.session, status: 'exited' };
this.requestUpdate();
this.stopSessionStatusPolling();
}
}
else {
console.error('Failed to send input to session:', response.status);
}
}
}
catch (error) {
console.error('Error sending input:', error);
}
}
handleBack() {
window.location.search = '';
}
handleSessionExit(e) {
const customEvent = e;
console.log('Session exit event received:', customEvent.detail);
if (this.session && customEvent.detail.sessionId === this.session.id) {
// Update session status to exited
this.session = { ...this.session, status: 'exited' };
this.requestUpdate();
// Stop polling immediately
this.stopSessionStatusPolling();
// Switch to snapshot mode
requestAnimationFrame(() => {
this.createInteractiveTerminal();
});
}
}
// Mobile input methods
handleMobileInputToggle() {
this.showMobileInput = !this.showMobileInput;
if (this.showMobileInput) {
// Focus the textarea after a short delay to ensure it's rendered
requestAnimationFrame(() => {
const textarea = this.querySelector('#mobile-input-textarea');
if (textarea) {
textarea.focus();
this.adjustTextareaForKeyboard();
}
});
}
else {
// Clean up viewport listener when closing overlay
const textarea = this.querySelector('#mobile-input-textarea');
if (textarea && textarea._viewportCleanup) {
textarea._viewportCleanup();
}
}
}
adjustTextareaForKeyboard() {
// Adjust the layout when virtual keyboard appears
const textarea = this.querySelector('#mobile-input-textarea');
const controls = this.querySelector('#mobile-controls');
if (!textarea || !controls)
return;
const adjustLayout = () => {
const viewportHeight = window.visualViewport?.height || window.innerHeight;
const windowHeight = window.innerHeight;
const keyboardHeight = windowHeight - viewportHeight;
// If keyboard is visible (viewport height is significantly smaller)
if (keyboardHeight > 100) {
// Move controls above the keyboard
controls.style.transform = `translateY(-${keyboardHeight}px)`;
controls.style.transition = 'transform 0.3s ease';
// Calculate available space for textarea
const header = this.querySelector('.flex.items-center.justify-between.p-4.border-b');
const headerHeight = header?.offsetHeight || 60;
const controlsHeight = controls?.offsetHeight || 120;
const padding = 48; // Additional padding for spacing
// Available height is viewport height minus header and controls (controls are now above keyboard)
const maxTextareaHeight = viewportHeight - headerHeight - controlsHeight - padding;
const inputArea = textarea.parentElement;
if (inputArea && maxTextareaHeight > 0) {
// Set the input area to not exceed the available space
inputArea.style.height = `${maxTextareaHeight}px`;
inputArea.style.maxHeight = `${maxTextareaHeight}px`;
inputArea.style.overflow = 'hidden';
// Set textarea height within the container
const labelHeight = 40; // Height of the label above textarea
const textareaMaxHeight = Math.max(maxTextareaHeight - labelHeight, 80);
textarea.style.height = `${textareaMaxHeight}px`;
textarea.style.maxHeight = `${textareaMaxHeight}px`;
}
}
else {
// Reset position when keyboard is hidden
controls.style.transform = 'translateY(0px)';
controls.style.transition = 'transform 0.3s ease';
// Reset textarea height and constraints
const inputArea = textarea.parentElement;
if (inputArea) {
inputArea.style.height = '';
inputArea.style.maxHeight = '';
inputArea.style.overflow = '';
textarea.style.height = '';
textarea.style.maxHeight = '';
}
}
};
// Listen for viewport changes (keyboard show/hide)
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', adjustLayout);
// Clean up listener when overlay is closed
const cleanup = () => {
if (window.visualViewport) {
window.visualViewport.removeEventListener('resize', adjustLayout);
}
};
// Store cleanup function for later use
textarea._viewportCleanup = cleanup;
}
// Initial adjustment
requestAnimationFrame(adjustLayout);
}
handleMobileInputChange(e) {
const textarea = e.target;
this.mobileInputText = textarea.value;
}
async handleMobileInputSendOnly() {
// Get the current value from the textarea directly
const textarea = this.querySelector('#mobile-input-textarea');
const textToSend = textarea?.value?.trim() || this.mobileInputText.trim();
if (!textToSend)
return;
try {
// Send text without enter key
await this.sendInputText(textToSend);
// Clear both the reactive property and textarea
this.mobileInputText = '';
if (textarea) {
textarea.value = '';
}
// Trigger re-render to update button state
this.requestUpdate();
// Hide the input overlay after sending
this.showMobileInput = false;
}
catch (error) {
console.error('Error sending mobile input:', error);
// Don't hide the overlay if there was an error
}
}
async handleMobileInputSend() {
// Get the current value from the textarea directly
const textarea = this.querySelector('#mobile-input-textarea');
const textToSend = textarea?.value?.trim() || this.mobileInputText.trim();
if (!textToSend)
return;
try {
// Add enter key at the end to execute the command
await this.sendInputText(textToSend + '\n');
// Clear both the reactive property and textarea
this.mobileInputText = '';
if (textarea) {
textarea.value = '';
}
// Trigger re-render to update button state
this.requestUpdate();
// Hide the input overlay after sending
this.showMobileInput = false;
}
catch (error) {
console.error('Error sending mobile input:', error);
// Don't hide the overlay if there was an error
}
}
async handleSpecialKey(key) {
await this.sendInputText(key);
}
async sendInputText(text) {
if (!this.session)
return;
try {
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text })
});
if (!response.ok) {
console.error('Failed to send input to session');
}
}
catch (error) {
console.error('Error sending input:', error);
}
}
adjustTerminalForMobileButtons() {
// Disabled for now to avoid viewport issues
// The mobile buttons will overlay the terminal
}
startLoading() {
this.loading = true;
this.loadingFrame = 0;
this.loadingInterval = window.setInterval(() => {
this.loadingFrame = (this.loadingFrame + 1) % 4;
this.requestUpdate();
}, 200); // Update every 200ms for smooth animation
}
stopLoading() {
this.loading = false;
if (this.loadingInterval) {
clearInterval(this.loadingInterval);
this.loadingInterval = null;
}
}
getLoadingText() {
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
return frames[this.loadingFrame % frames.length];
}
startSessionStatusPolling() {
if (this.sessionStatusInterval) {
clearInterval(this.sessionStatusInterval);
}
// Only poll for running sessions - exited sessions don't need polling
if (this.session?.status !== 'exited') {
this.sessionStatusInterval = window.setInterval(() => {
this.checkSessionStatus();
}, 2000);
}
}
stopSessionStatusPolling() {
if (this.sessionStatusInterval) {
clearInterval(this.sessionStatusInterval);
this.sessionStatusInterval = null;
}
}
async checkSessionStatus() {
if (!this.session)
return;
try {
const response = await fetch('/api/sessions');
if (!response.ok)
return;
const sessions = await response.json();
const currentSession = sessions.find((s) => s.id === this.session.id);
if (currentSession && currentSession.status !== this.session.status) {
// Store old status before updating
const oldStatus = this.session.status;
// Session status changed
this.session = { ...this.session, status: currentSession.status };
this.requestUpdate();
// Session status polling is now only for detecting new sessions
// Exit events are handled via SSE stream directly
}
}
catch (error) {
console.error('Error checking session status:', error);
}
}
render() {
if (!this.session) {
return html `
<div class="p-4 text-vs-muted">
No session selected
</div>
`;
}
return html `
<style>
session-view *, session-view *:focus, session-view *:focus-visible {
outline: none !important;
box-shadow: none !important;
}
</style>
<div class="flex flex-col bg-vs-bg font-mono" style="height: 100vh; outline: none !important; box-shadow: none !important;">
<!-- Compact Header -->
<div class="flex items-center justify-between px-3 py-2 border-b border-vs-border bg-vs-bg-secondary text-sm">
<div class="flex items-center gap-3">
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-2 py-1 border-none rounded transition-colors text-xs"
@click=${this.handleBack}
>
BACK
</button>
<div class="text-vs-text">
<div class="text-vs-accent">${this.session.command}</div>
<div class="text-vs-muted text-xs">${this.session.workingDir}</div>
</div>
</div>
<div class="flex items-center gap-3 text-xs">
<span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
${this.session.status.toUpperCase()}
</span>
</div>
</div>
<!-- Terminal Container -->
<div class="flex-1 bg-black overflow-x-auto overflow-y-hidden min-h-0 relative" id="terminal-container">
<div id="interactive-terminal" class="w-full h-full"></div>
${this.loading ? html `
<!-- Loading overlay -->
<div class="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center">
<div class="text-vs-text font-mono text-center">
<div class="text-2xl mb-2">${this.getLoadingText()}</div>
<div class="text-sm text-vs-muted">Connecting to session...</div>
</div>
</div>
` : ''}
</div>
<!-- Mobile Input Controls -->
${this.isMobile && !this.showMobileInput ? html `
<div class="flex-shrink-0 p-4 bg-vs-bg">
<!-- First row: Arrow keys -->
<div class="flex gap-2 mb-2">
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_up')}
>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_down')}
>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_left')}
>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_right')}
>
</button>
</div>
<!-- Second row: Special keys -->
<div class="flex gap-2">
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\t')}
>
TAB
</button>
<button
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('enter')}
>
ENTER
</button>
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('escape')}
>
ESC
</button>
<button
class="bg-vs-error text-vs-text hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\x03')}
>
^C
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleMobileInputToggle}
>
TYPE
</button>
</div>
</div>
` : ''}
<!-- Full-Screen Input Overlay (only when opened) -->
${this.isMobile && this.showMobileInput ? html `
<div class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col" style="height: 100vh; height: 100dvh;">
<!-- Input Header -->
<div class="flex items-center justify-between p-4 border-b border-vs-border flex-shrink-0">
<div class="text-vs-text font-mono text-sm">Terminal Input</div>
<button
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
@click=${this.handleMobileInputToggle}
>
×
</button>
</div>
<!-- Input Area with dynamic height -->
<div class="flex-1 p-4 flex flex-col min-h-0">
<div class="text-vs-muted text-sm mb-2 flex-shrink-0">
Type your command(s) below. Supports multiline input.
</div>
<textarea
id="mobile-input-textarea"
class="flex-1 bg-vs-bg text-vs-text border border-vs-border font-mono text-sm p-4 resize-none outline-none"
placeholder="Enter your command here..."
.value=${this.mobileInputText}
@input=${this.handleMobileInputChange}
@keydown=${(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleMobileInputSend();
}
}}
style="min-height: 120px; margin-bottom: 16px;"
></textarea>
</div>
<!-- Controls - Fixed above keyboard -->
<div id="mobile-controls" class="fixed bottom-0 left-0 right-0 p-4 border-t border-vs-border bg-vs-bg-secondary z-60" style="padding-bottom: max(1rem, env(safe-area-inset-bottom)); transform: translateY(0px);">
<!-- Send Buttons Row -->
<div class="flex gap-2 mb-3">
<button
class="flex-1 bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSendOnly}
?disabled=${!this.mobileInputText.trim()}
>
SEND
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSend}
?disabled=${!this.mobileInputText.trim()}
>
SEND + ENTER
</button>
</div>
<div class="text-vs-muted text-xs text-center">
SEND: text only SEND + ENTER: text with enter key
</div>
</div>
</div>
` : ''}
</div>
`;
}
};
__decorate([
property({ type: Object })
], SessionView.prototype, "session", void 0);
__decorate([
state()
], SessionView.prototype, "connected", void 0);
__decorate([
state()
], SessionView.prototype, "renderer", void 0);
__decorate([
state()
], SessionView.prototype, "sessionStatusInterval", void 0);
__decorate([
state()
], SessionView.prototype, "showMobileInput", void 0);
__decorate([
state()
], SessionView.prototype, "mobileInputText", void 0);
__decorate([
state()
], SessionView.prototype, "isMobile", void 0);
__decorate([
state()
], SessionView.prototype, "touchStartX", void 0);
__decorate([
state()
], SessionView.prototype, "touchStartY", void 0);
__decorate([
state()
], SessionView.prototype, "loading", void 0);
__decorate([
state()
], SessionView.prototype, "loadingFrame", void 0);
SessionView = __decorate([
customElement('session-view')
], SessionView);
export { SessionView };
//# sourceMappingURL=session-view.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,3 +0,0 @@
// Entry point for renderer bundle - exports XTerm-based renderer
export { Renderer } from './renderer';
//# sourceMappingURL=renderer-entry.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"renderer-entry.js","sourceRoot":"","sources":["../src/client/renderer-entry.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC","sourcesContent":["// Entry point for renderer bundle - exports XTerm-based renderer\nexport { Renderer } from './renderer';"]}

View file

@ -1,306 +0,0 @@
// Terminal renderer for asciinema cast format using XTerm.js
// Professional-grade terminal emulation with full VT compatibility
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { ScaleFitAddon } from './scale-fit-addon.js';
export class Renderer {
constructor(container, width = 80, height = 20, scrollback = 1000000, fontSize = 14, isPreview = false) {
this.eventSource = null;
Renderer.activeCount++;
console.log(`Renderer constructor called (active: ${Renderer.activeCount})`);
this.container = container;
this.isPreview = isPreview;
// Create terminal with options similar to the custom renderer
this.terminal = new Terminal({
cols: width,
rows: height,
fontFamily: 'Monaco, "Lucida Console", monospace',
fontSize: fontSize,
lineHeight: 1.2,
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#ffffff',
cursorAccent: '#1e1e1e',
selectionBackground: '#264f78',
// VS Code Dark theme colors
black: '#000000',
red: '#f14c4c',
green: '#23d18b',
yellow: '#f5f543',
blue: '#3b8eea',
magenta: '#d670d6',
cyan: '#29b8db',
white: '#e5e5e5',
// Bright colors
brightBlack: '#666666',
brightRed: '#f14c4c',
brightGreen: '#23d18b',
brightYellow: '#f5f543',
brightBlue: '#3b8eea',
brightMagenta: '#d670d6',
brightCyan: '#29b8db',
brightWhite: '#ffffff'
},
allowProposedApi: true,
scrollback: scrollback, // Configurable scrollback buffer
convertEol: true,
altClickMovesCursor: false,
rightClickSelectsWord: false,
disableStdin: true, // We handle input separately
});
// Add addons
this.fitAddon = new FitAddon();
this.scaleFitAddon = new ScaleFitAddon();
this.webLinksAddon = new WebLinksAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.loadAddon(this.scaleFitAddon);
this.terminal.loadAddon(this.webLinksAddon);
this.setupDOM();
}
setupDOM() {
// Clear container and add CSS
this.container.innerHTML = '';
// Different styling for preview vs full terminals
if (this.isPreview) {
// No padding for previews, let container control sizing
this.container.style.padding = '0';
this.container.style.backgroundColor = '#1e1e1e';
this.container.style.overflow = 'hidden';
}
else {
// Full terminals get padding
this.container.style.padding = '10px';
this.container.style.backgroundColor = '#1e1e1e';
this.container.style.overflow = 'hidden';
}
// Create terminal wrapper
const terminalWrapper = document.createElement('div');
terminalWrapper.style.width = '100%';
terminalWrapper.style.height = '100%';
this.container.appendChild(terminalWrapper);
// Open terminal in the wrapper
this.terminal.open(terminalWrapper);
// Always use ScaleFitAddon for better scaling
this.scaleFitAddon.fit();
// Handle container resize
const resizeObserver = new ResizeObserver(() => {
this.scaleFitAddon.fit();
});
resizeObserver.observe(this.container);
}
// Public API methods - maintain compatibility with custom renderer
async loadCastFile(url) {
const response = await fetch(url);
const text = await response.text();
this.parseCastFile(text);
}
parseCastFile(content) {
const lines = content.trim().split('\n');
let header = null;
// Clear terminal
this.terminal.clear();
for (const line of lines) {
if (!line.trim())
continue;
try {
const parsed = JSON.parse(line);
if (parsed.version && parsed.width && parsed.height) {
// Header
header = parsed;
this.resize(parsed.width, parsed.height);
}
else if (Array.isArray(parsed) && parsed.length >= 3) {
// Event: [timestamp, type, data]
const event = {
timestamp: parsed[0],
type: parsed[1],
data: parsed[2]
};
if (event.type === 'o') {
this.processOutput(event.data);
}
else if (event.type === 'r') {
this.processResize(event.data);
}
}
}
catch (e) {
console.warn('Failed to parse cast line:', line);
}
}
}
processOutput(data) {
// XTerm handles all ANSI escape sequences automatically
this.terminal.write(data);
}
processResize(data) {
// Parse resize data in format "WIDTHxHEIGHT" (e.g., "80x24")
const match = data.match(/^(\d+)x(\d+)$/);
if (match) {
const width = parseInt(match[1], 10);
const height = parseInt(match[2], 10);
this.resize(width, height);
}
}
processEvent(event) {
if (event.type === 'o') {
this.processOutput(event.data);
}
else if (event.type === 'r') {
this.processResize(event.data);
}
}
resize(width, height) {
if (this.isPreview) {
// For previews, resize to session dimensions then apply scaling
this.terminal.resize(width, height);
}
// Always use ScaleFitAddon for consistent scaling behavior
this.scaleFitAddon.fit();
}
clear() {
this.terminal.clear();
}
// Stream support - connect to SSE endpoint
connectToStream(sessionId) {
console.log('connectToStream called for session:', sessionId);
return this.connectToUrl(`/api/sessions/${sessionId}/stream`);
}
// Connect to any SSE URL
connectToUrl(url) {
console.log('Creating new EventSource connection to:', url);
const eventSource = new EventSource(url);
// Don't clear terminal for live streams - just append new content
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.version && data.width && data.height) {
// Header
console.log('Received header:', data);
this.resize(data.width, data.height);
}
else if (Array.isArray(data) && data.length >= 3) {
// Check if this is an exit event
if (data[0] === 'exit') {
const exitCode = data[1];
const sessionId = data[2];
console.log(`Session ${sessionId} exited with code ${exitCode}`);
// Close the SSE connection immediately
if (this.eventSource) {
console.log('Closing SSE connection due to session exit');
this.eventSource.close();
this.eventSource = null;
}
// Dispatch custom event that session-view can listen to
const exitEvent = new CustomEvent('session-exit', {
detail: { sessionId, exitCode }
});
this.container.dispatchEvent(exitEvent);
return;
}
// Regular cast event
const castEvent = {
timestamp: data[0],
type: data[1],
data: data[2]
};
// Process event without verbose logging
this.processEvent(castEvent);
}
}
catch (e) {
console.warn('Failed to parse stream event:', event.data);
}
};
eventSource.onerror = (error) => {
console.error('Stream error:', error);
// Close the connection to prevent automatic reconnection attempts
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Stream closed, cleaning up...');
if (this.eventSource === eventSource) {
this.eventSource = null;
}
}
};
return eventSource;
}
// Load content from URL - pass isStream to determine how to handle it
async loadFromUrl(url, isStream) {
// Clean up existing connection
if (this.eventSource) {
console.log('Explicitly closing existing EventSource connection');
this.eventSource.close();
this.eventSource = null;
}
if (isStream) {
// It's a stream URL, connect via SSE (don't clear - append to existing content)
this.eventSource = this.connectToUrl(url);
}
else {
// It's a snapshot URL, clear first then load as cast file
this.terminal.clear();
await this.loadCastFile(url);
}
}
// Additional methods for terminal control
focus() {
this.terminal.focus();
}
blur() {
this.terminal.blur();
}
getTerminal() {
return this.terminal;
}
dispose() {
if (this.eventSource) {
console.log('Explicitly closing EventSource connection in dispose()');
this.eventSource.close();
this.eventSource = null;
}
this.terminal.dispose();
Renderer.activeCount--;
console.log(`Renderer disposed (active: ${Renderer.activeCount})`);
}
// Method to fit terminal to container (useful for responsive layouts)
fit() {
this.fitAddon.fit();
}
// Get terminal dimensions
getDimensions() {
return {
cols: this.terminal.cols,
rows: this.terminal.rows
};
}
// Write raw data to terminal (useful for testing)
write(data) {
this.terminal.write(data);
}
// Enable/disable input (though we keep it disabled by default)
setInputEnabled(enabled) {
// XTerm doesn't have a direct way to disable input, so we override onData
if (enabled) {
// Remove any existing handler first
this.terminal.onData(() => {
// Input is handled by the session component
});
}
else {
this.terminal.onData(() => {
// Do nothing - input disabled
});
}
}
// Disable all pointer events for previews so clicks pass through to parent
setPointerEventsEnabled(enabled) {
const terminalElement = this.container.querySelector('.xterm');
if (terminalElement) {
terminalElement.style.pointerEvents = enabled ? 'auto' : 'none';
}
}
}
Renderer.activeCount = 0;
//# sourceMappingURL=renderer.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,105 +0,0 @@
/**
* Custom FitAddon that scales font size to fit terminal columns to container width,
* then calculates optimal rows for the container height.
*/
const MINIMUM_ROWS = 1;
const MIN_FONT_SIZE = 6;
const MAX_FONT_SIZE = 16;
export class ScaleFitAddon {
activate(terminal) {
this._terminal = terminal;
}
dispose() { }
fit() {
const dims = this.proposeDimensions();
if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) {
return;
}
// Only resize rows, keep cols the same (font scaling handles width)
if (this._terminal.rows !== dims.rows) {
this._terminal.resize(this._terminal.cols, dims.rows);
}
}
proposeDimensions() {
if (!this._terminal?.element?.parentElement) {
return undefined;
}
// Get the renderer container (parent of parent - the one with 10px padding)
const terminalWrapper = this._terminal.element.parentElement;
const rendererContainer = terminalWrapper.parentElement;
if (!rendererContainer)
return undefined;
// Get container dimensions and exact padding
const containerStyle = window.getComputedStyle(rendererContainer);
const containerWidth = parseInt(containerStyle.getPropertyValue('width'));
const containerHeight = parseInt(containerStyle.getPropertyValue('height'));
const containerPadding = {
top: parseInt(containerStyle.getPropertyValue('padding-top')),
bottom: parseInt(containerStyle.getPropertyValue('padding-bottom')),
left: parseInt(containerStyle.getPropertyValue('padding-left')),
right: parseInt(containerStyle.getPropertyValue('padding-right'))
};
// Calculate exact available space using known padding
const availableWidth = containerWidth - containerPadding.left - containerPadding.right;
const availableHeight = containerHeight - containerPadding.top - containerPadding.bottom;
// Current terminal dimensions
const currentCols = this._terminal.cols;
// Calculate optimal font size to fit current cols in available width
// Character width is approximately 0.6 * fontSize for monospace fonts
const charWidthRatio = 0.6;
const calculatedFontSize = availableWidth / (currentCols * charWidthRatio);
const optimalFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, calculatedFontSize));
// Apply the calculated font size (outside of proposeDimensions to avoid recursion)
requestAnimationFrame(() => this.applyFontSize(optimalFontSize));
// Get the actual line height from the rendered XTerm element
const xtermElement = this._terminal.element;
const currentStyle = window.getComputedStyle(xtermElement);
const actualLineHeight = parseFloat(currentStyle.lineHeight);
// If we can't get the line height, fall back to configuration
const lineHeight = actualLineHeight || (optimalFontSize * (this._terminal.options.lineHeight || 1.2));
// Calculate how many rows fit with this line height
const optimalRows = Math.max(MINIMUM_ROWS, Math.floor(availableHeight / lineHeight));
return {
cols: currentCols, // Keep existing cols
rows: optimalRows // Fit as many rows as possible
};
}
applyFontSize(fontSize) {
if (!this._terminal?.element)
return;
// Prevent infinite recursion by checking if font size changed significantly
const currentFontSize = this._terminal.options.fontSize || 14;
if (Math.abs(fontSize - currentFontSize) < 0.1)
return;
const terminalElement = this._terminal.element;
// Update terminal's font size
this._terminal.options.fontSize = fontSize;
// Apply CSS font size to the element
terminalElement.style.fontSize = `${fontSize}px`;
// Force a refresh to apply the new font size
requestAnimationFrame(() => {
if (this._terminal) {
this._terminal.refresh(0, this._terminal.rows - 1);
}
});
}
/**
* Get the calculated font size that would fit the current columns in the container
*/
getOptimalFontSize() {
if (!this._terminal?.element?.parentElement) {
return this._terminal?.options.fontSize || 14;
}
const parentElement = this._terminal.element.parentElement;
const parentStyle = window.getComputedStyle(parentElement);
const parentWidth = parseInt(parentStyle.getPropertyValue('width'));
const elementStyle = window.getComputedStyle(this._terminal.element);
const paddingHor = parseInt(elementStyle.getPropertyValue('padding-left')) +
parseInt(elementStyle.getPropertyValue('padding-right'));
const availableWidth = parentWidth - paddingHor;
const charWidthRatio = 0.6;
const calculatedFontSize = availableWidth / (this._terminal.cols * charWidthRatio);
return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, calculatedFontSize));
}
}
//# sourceMappingURL=scale-fit-addon.js.map

File diff suppressed because one or more lines are too long