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