diff --git a/web/src/client/components/vibe-terminal-buffer.ts b/web/src/client/components/vibe-terminal-buffer.ts index b5442794..7a30a7a9 100644 --- a/web/src/client/components/vibe-terminal-buffer.ts +++ b/web/src/client/components/vibe-terminal-buffer.ts @@ -1,5 +1,6 @@ import { LitElement, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { TerminalRenderer, type BufferCell } from '../utils/terminal-renderer.js'; interface BufferSnapshot { @@ -28,6 +29,7 @@ export class VibeTerminalBuffer extends LitElement { @state() private loading = false; @state() private actualRows = 0; @state() private displayedFontSize = 14; + @state() private containerCols = 80; // Calculated columns that fit private container: HTMLElement | null = null; private pollTimer: NodeJS.Timeout | null = null; @@ -86,29 +88,38 @@ export class VibeTerminalBuffer extends LitElement { private calculateDimensions() { if (!this.container) return; + const containerWidth = this.container.clientWidth; const containerHeight = this.container.clientHeight; - const lineHeight = this.fontSize * 1.2; - const newActualRows = Math.floor(containerHeight / lineHeight); if (this.fitHorizontally && this.buffer) { - // Calculate font size to fit terminal width - const containerWidth = this.container.clientWidth; - const charWidth = this.fontSize * 0.6; // Approximate char width - const requiredWidth = this.buffer.cols * charWidth; + // Horizontal fitting: calculate fontSize to fit buffer.cols characters in container width + const targetCharWidth = containerWidth / this.buffer.cols; - if (requiredWidth > containerWidth) { - const scale = containerWidth / requiredWidth; - this.displayedFontSize = Math.floor(this.fontSize * scale); - } else { - this.displayedFontSize = this.fontSize; + // Estimate font size needed (assuming monospace font with ~0.6 char/font ratio) + const calculatedFontSize = targetCharWidth / 0.6; + this.displayedFontSize = Math.max(4, Math.min(32, Math.floor(calculatedFontSize))); + + // Calculate actual rows with new font size + const lineHeight = this.displayedFontSize * 1.2; + const newActualRows = Math.max(1, Math.floor(containerHeight / lineHeight)); + + if (newActualRows !== this.actualRows) { + this.actualRows = newActualRows; + this.fetchBuffer(); } } else { + // Normal mode: use original font size and calculate cols that fit this.displayedFontSize = this.fontSize; - } + const lineHeight = this.fontSize * 1.2; + const charWidth = this.fontSize * 0.6; - if (newActualRows !== this.actualRows) { - this.actualRows = newActualRows; - this.fetchBuffer(); + const newActualRows = Math.max(1, Math.floor(containerHeight / lineHeight)); + this.containerCols = Math.max(20, Math.floor(containerWidth / charWidth)); + + if (newActualRows !== this.actualRows) { + this.actualRows = newActualRows; + this.fetchBuffer(); + } } } @@ -148,12 +159,9 @@ export class VibeTerminalBuffer extends LitElement { return; // No changes } - // Fetch buffer data - request at least one full terminal screen worth of lines - // This ensures we get the complete visible terminal state - const linesToFetch = Math.max(this.actualRows, stats.rows); - const lines = Math.min(linesToFetch, stats.totalRows); + // Always fetch the entire buffer to show all content const response = await fetch( - `/api/sessions/${this.sessionId}/buffer?lines=${lines}&format=json` + `/api/sessions/${this.sessionId}/buffer?viewportY=0&lines=${stats.totalRows}&format=json` ); if (!response.ok) { @@ -164,14 +172,6 @@ export class VibeTerminalBuffer extends LitElement { this.lastModified = stats.lastModified; this.error = null; - // Debug logging - console.log(`Buffer loaded for ${this.sessionId}:`, { - cols: this.buffer.cols, - rows: this.buffer.rows, - cellCount: this.buffer.cells.length, - firstLineSample: this.buffer.cells[0]?.slice(0, 10), - }); - this.requestUpdate(); } catch (error) { console.error('Error fetching buffer:', error); @@ -210,24 +210,37 @@ export class VibeTerminalBuffer extends LitElement { const lineHeight = this.displayedFontSize * 1.2; - // Render lines - if we have more lines than can fit, show the bottom portion - const startIndex = Math.max(0, this.buffer.cells.length - this.actualRows); - const visibleCells = this.buffer.cells.slice(startIndex); + // In fitHorizontally mode, we show all content scaled to fit + // Otherwise, we show from the top and let it overflow + if (this.fitHorizontally) { + // Render all lines - the buffer is already trimmed of blank lines from the bottom + return this.buffer.cells.map((row, index) => { + const isCursorLine = index === this.buffer.cursorY; + const cursorCol = isCursorLine ? this.buffer.cursorX : -1; + const lineContent = TerminalRenderer.renderLineFromCells(row, cursorCol); - return visibleCells.map((row, index) => { - const actualIndex = startIndex + index; - const isCursorLine = actualIndex === this.buffer.cursorY; - const cursorCol = isCursorLine ? this.buffer.cursorX : -1; - const lineContent = TerminalRenderer.renderLineFromCells(row, cursorCol); + return html` +
+ ${unsafeHTML(lineContent)} +
+ `; + }); + } else { + // Show only what fits in the viewport + const visibleCells = this.buffer.cells.slice(0, this.actualRows); - return html` -
- `; - }); + return visibleCells.map((row, index) => { + const isCursorLine = index === this.buffer.cursorY; + const cursorCol = isCursorLine ? this.buffer.cursorX : -1; + const lineContent = TerminalRenderer.renderLineFromCells(row, cursorCol); + + return html` +
+ ${unsafeHTML(lineContent)} +
+ `; + }); + } } /** diff --git a/web/src/client/utils/terminal-renderer.ts b/web/src/client/utils/terminal-renderer.ts index 6faa73fb..d0b1b90c 100644 --- a/web/src/client/utils/terminal-renderer.ts +++ b/web/src/client/utils/terminal-renderer.ts @@ -214,6 +214,9 @@ export class TerminalRenderer { const b = cell.fg & 0xff; style += `color: rgb(${r}, ${g}, ${b});`; } + } else { + // Default foreground color if not specified + style += `color: #d4d4d4;`; } // Get background color diff --git a/web/src/terminal-manager.ts b/web/src/terminal-manager.ts index c475a646..52a09e8c 100644 --- a/web/src/terminal-manager.ts +++ b/web/src/terminal-manager.ts @@ -284,13 +284,31 @@ export class TerminalManager { cells.push(rowCells); } + // Trim blank lines from the bottom + let lastNonBlankRow = cells.length - 1; + while (lastNonBlankRow >= 0) { + const row = cells[lastNonBlankRow]; + const hasContent = row.some( + (cell) => + cell.char !== ' ' || + cell.fg !== undefined || + cell.bg !== undefined || + cell.attributes !== undefined + ); + if (hasContent) break; + lastNonBlankRow--; + } + + // Keep at least one row + const trimmedCells = cells.slice(0, Math.max(1, lastNonBlankRow + 1)); + return { cols: terminal.cols, - rows: actualLines, + rows: trimmedCells.length, viewportY: actualViewportY, cursorX, cursorY, - cells, + cells: trimmedCells, }; } diff --git a/web/test-buffer-component.js b/web/test-buffer-component.js deleted file mode 100755 index fc2f8f9c..00000000 --- a/web/test-buffer-component.js +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script for the new buffer component - */ - -const http = require('http'); - -const BASE_URL = 'http://localhost:3000'; - -function httpGet(url) { - return new Promise((resolve, reject) => { - http.get(url, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - if (res.statusCode === 200) { - try { - resolve(JSON.parse(data)); - } catch (e) { - resolve(data); - } - } else { - reject(new Error(`HTTP ${res.statusCode}: ${data}`)); - } - }); - }).on('error', reject); - }); -} - -async function testBufferEndpoints(sessionId) { - console.log(`\n=== Testing buffer endpoints for session ${sessionId} ===`); - - // Test stats endpoint - console.log('\n1. Testing /buffer/stats endpoint:'); - try { - const stats = await httpGet(`${BASE_URL}/api/sessions/${sessionId}/buffer/stats`); - console.log(' ✅ Stats:', JSON.stringify(stats, null, 2)); - } catch (error) { - console.error(' ❌ Error:', error.message); - return; - } - - // Test JSON buffer endpoint - console.log('\n2. Testing /buffer endpoint with JSON format:'); - try { - const buffer = await httpGet(`${BASE_URL}/api/sessions/${sessionId}/buffer?lines=10&format=json`); - console.log(' ✅ Buffer dimensions:', `${buffer.cols}x${buffer.rows}`); - console.log(' ✅ Viewport Y:', buffer.viewportY); - console.log(' ✅ Cursor position:', `(${buffer.cursorX}, ${buffer.cursorY})`); - console.log(' ✅ Cell count:', buffer.cells.length); - - // Show sample of first line - if (buffer.cells.length > 0) { - const firstLine = buffer.cells[0]; - const text = firstLine.map(cell => cell.char).join(''); - console.log(' ✅ First line preview:', text.substring(0, 40) + '...'); - } - } catch (error) { - console.error(' ❌ Error:', error.message); - return; - } - - // Test bottom-up lines (without viewportY) - console.log('\n3. Testing bottom-up lines (no viewportY):'); - try { - const buffer = await httpGet(`${BASE_URL}/api/sessions/${sessionId}/buffer?lines=5&format=json`); - console.log(' ✅ Got', buffer.rows, 'lines from bottom'); - console.log(' ✅ ViewportY:', buffer.viewportY); - } catch (error) { - console.error(' ❌ Error:', error.message); - } -} - -async function main() { - console.log('Buffer Component Test Script'); - console.log('============================'); - - // First get list of sessions - console.log('\nFetching sessions...'); - try { - const sessions = await httpGet(`${BASE_URL}/api/sessions`); - console.log(`Found ${sessions.length} sessions`); - - if (sessions.length === 0) { - console.log('\nNo sessions found. Please create a session first.'); - return; - } - - // Test first running session - const runningSessions = sessions.filter(s => s.status === 'running'); - if (runningSessions.length === 0) { - console.log('\nNo running sessions found. Testing with first session...'); - await testBufferEndpoints(sessions[0].id); - } else { - console.log(`\nTesting with running session: ${runningSessions[0].command}`); - await testBufferEndpoints(runningSessions[0].id); - } - - } catch (error) { - console.error('Error:', error.message); - } -} - -if (require.main === module) { - main().catch(console.error); -} \ No newline at end of file diff --git a/web/test-buffer-endpoint.js b/web/test-buffer-endpoint.js deleted file mode 100755 index 9b2c8888..00000000 --- a/web/test-buffer-endpoint.js +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env node - -// Test script for the new buffer endpoint - -const BASE_URL = 'http://localhost:3000'; - -async function testBufferEndpoint() { - try { - // First, get list of sessions - const sessionsRes = await fetch(`${BASE_URL}/api/sessions`); - const sessions = await sessionsRes.json(); - - if (sessions.length === 0) { - console.log('No sessions available. Create a session first.'); - return; - } - - const sessionId = sessions[0].id; - console.log(`Testing with session: ${sessionId}`); - - // Test buffer endpoint - const bufferRes = await fetch(`${BASE_URL}/api/sessions/${sessionId}/buffer?viewportY=0&lines=24`); - - if (!bufferRes.ok) { - console.error('Buffer request failed:', bufferRes.status, await bufferRes.text()); - return; - } - - const buffer = await bufferRes.arrayBuffer(); - const bytes = new Uint8Array(buffer); - - console.log(`Received ${bytes.length} bytes`); - - // Parse header - if (bytes.length < 16) { - console.error('Buffer too small for header'); - return; - } - - const magic = (bytes[1] << 8) | bytes[0]; - const version = bytes[2]; - const flags = bytes[3]; - const cols = (bytes[5] << 8) | bytes[4]; - const rows = (bytes[7] << 8) | bytes[6]; - const viewportY = (bytes[9] << 8) | bytes[8]; - const cursorX = (bytes[11] << 8) | bytes[10]; - const cursorY = (bytes[13] << 8) | bytes[12]; - - console.log('\nHeader:'); - console.log(` Magic: 0x${magic.toString(16)} (${magic === 0x5654 ? 'Valid' : 'Invalid'})`); - console.log(` Version: ${version}`); - console.log(` Flags: ${flags}`); - console.log(` Terminal: ${cols}x${rows}`); - console.log(` ViewportY: ${viewportY}`); - console.log(` Cursor: (${cursorX}, ${cursorY})`); - - // Sample first few cells - console.log('\nFirst few cells:'); - let offset = 16; - for (let i = 0; i < Math.min(10, bytes.length - 16); i++) { - if (offset >= bytes.length) break; - - const byte = bytes[offset]; - if (byte === 0xFF) { - // RLE marker - const count = bytes[offset + 1]; - console.log(` RLE: ${count} repeated cells`); - offset += 2; - } else if (byte === 0xFE) { - // Empty line marker - const count = bytes[offset + 1]; - console.log(` Empty lines: ${count}`); - offset += 2; - } else if (byte & 0x80) { - // Extended cell - console.log(` Extended cell at offset ${offset}`); - offset += 4; // Skip for now - } else { - // Basic cell - const char = String.fromCharCode(byte); - const attrs = bytes[offset + 1]; - const fg = bytes[offset + 2]; - const bg = bytes[offset + 3]; - console.log(` Cell: '${char}' fg=${fg} bg=${bg} attrs=${attrs}`); - offset += 4; - } - } - - } catch (error) { - console.error('Test failed:', error); - } -} - -testBufferEndpoint(); \ No newline at end of file diff --git a/web/test-terminal-manager.js b/web/test-terminal-manager.js deleted file mode 100644 index cb8bd3b4..00000000 --- a/web/test-terminal-manager.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -const path = require('path'); - -// Test the terminal manager directly -async function testTerminalManager() { - console.log('Testing Terminal Manager...\n'); - - const { TerminalManager } = require('./dist/terminal-manager.js'); - const controlDir = path.join(process.env.HOME, '.vibetunnel/control'); - - const tm = new TerminalManager(controlDir); - - // Get a specific session - const sessionId = process.argv[2] || '725f848c-c6d7-4bd4-8030-b83b20b1ee45'; - - console.log(`Testing session: ${sessionId}`); - - try { - // Get terminal - const terminal = await tm.getTerminal(sessionId); - console.log('Terminal created successfully'); - - // Wait for content to load - console.log('Waiting for content to load...'); - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Get buffer stats - const stats = await tm.getBufferStats(sessionId); - console.log('\nBuffer stats:', stats); - - // Get buffer snapshot - const snapshot = await tm.getBufferSnapshot(sessionId, undefined, 10); - console.log('\nBuffer snapshot:'); - console.log('- Dimensions:', `${snapshot.cols}x${snapshot.rows}`); - console.log('- ViewportY:', snapshot.viewportY); - console.log('- Cursor:', `(${snapshot.cursorX}, ${snapshot.cursorY})`); - - // Check content - if (snapshot.cells.length > 0) { - console.log('\nFirst line content:'); - const firstLine = snapshot.cells[0]; - const text = firstLine.map(cell => cell.char).join(''); - console.log(`"${text}"`); - - // Check for non-space content - let hasContent = false; - for (const row of snapshot.cells) { - const rowText = row.map(cell => cell.char).join('').trim(); - if (rowText.length > 0) { - hasContent = true; - console.log(`\nFound content: "${rowText}"`); - break; - } - } - - if (!hasContent) { - console.log('\n⚠️ Warning: All lines appear to be empty!'); - } - } - - // Close terminal - tm.closeTerminal(sessionId); - console.log('\nTerminal closed'); - - } catch (error) { - console.error('Error:', error); - } -} - -testTerminalManager().catch(console.error); \ No newline at end of file