fix: resolve terminal rendering issues in vibe-terminal-buffer

- Move terminal color definitions to shared CSS in input.css
- Use bright color palette for dark backgrounds
- Switch from Lit template rendering to direct innerHTML for terminal content
- Add display: inline-block to terminal-char for proper rendering
- Remove redundant style definitions from terminal.ts
- Fix issue where Lit's template system was interfering with terminal output

The key fix was using innerHTML directly instead of Lit's template system for
the terminal content, matching the approach used in terminal.ts.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-06-20 03:21:16 +02:00
parent 01df54c63b
commit 3bf543e694
3 changed files with 189 additions and 281 deletions

View file

@ -1168,196 +1168,17 @@ export class Terminal extends LitElement {
render() { render() {
return html` return html`
<style> <style>
/* Terminal color variables */ /* Dynamic terminal sizing */
:root {
--terminal-color-0: #000000;
--terminal-color-1: #f14c4c;
--terminal-color-2: #23d18b;
--terminal-color-3: #f5f543;
--terminal-color-4: #3b8eea;
--terminal-color-5: #d670d6;
--terminal-color-6: #29b8db;
--terminal-color-7: #e5e5e5;
--terminal-color-8: #666666;
--terminal-color-9: #ff6b6b;
--terminal-color-10: #5af78e;
--terminal-color-11: #f4f99d;
--terminal-color-12: #70a5ed;
--terminal-color-13: #d670d6;
--terminal-color-14: #5fb3d3;
--terminal-color-15: #ffffff;
}
.terminal-container { .terminal-container {
color: #d4d4d4;
font-family: 'Fira Code', ui-monospace, SFMono-Regular, monospace;
font-size: ${this.fontSize}px; font-size: ${this.fontSize}px;
line-height: ${this.fontSize * 1.2}px; line-height: ${this.fontSize * 1.2}px;
white-space: pre;
touch-action: none !important; touch-action: none !important;
overflow: hidden !important;
} }
.terminal-line { .terminal-line {
display: block;
height: ${this.fontSize * 1.2}px; height: ${this.fontSize * 1.2}px;
line-height: ${this.fontSize * 1.2}px; line-height: ${this.fontSize * 1.2}px;
} }
.terminal-char {
font-family: inherit;
display: inline-block;
}
.terminal-char.bold {
font-weight: bold;
}
.terminal-char.dim {
opacity: 0.5;
}
.terminal-char.italic {
font-style: italic;
}
.terminal-char.underline {
text-decoration: underline;
}
.terminal-char.strikethrough {
text-decoration: line-through;
}
.terminal-char.inverse {
filter: invert(1);
}
.terminal-char.invisible {
opacity: 0;
}
.terminal-char.cursor {
animation: cursor-blink 1s infinite;
}
@keyframes cursor-blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0.3;
}
}
.terminal-link {
color: #4fc3f7;
text-decoration: underline;
cursor: pointer;
transition: background-color 0.2s ease;
}
.terminal-link:hover {
background-color: rgba(79, 195, 247, 0.2);
}
.scroll-to-bottom {
position: absolute;
bottom: 12px;
left: 12px;
width: 48px;
height: 48px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #444;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #d4d4d4;
font-size: 24px;
transition: all 0.2s ease;
user-select: none;
z-index: 10;
}
.scroll-to-bottom:hover {
background: rgba(0, 0, 0, 0.9);
border-color: #666;
transform: translateY(-1px);
}
.scroll-to-bottom:active {
transform: translateY(0px);
}
.debug-overlay {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #444;
border-radius: 4px;
padding: 8px 12px;
font-family: 'Fira Code', monospace;
font-size: 11px;
color: #d4d4d4;
user-select: none;
z-index: 10;
line-height: 1.4;
}
.debug-overlay .metric {
display: flex;
justify-content: space-between;
min-width: 120px;
}
.debug-overlay .metric-label {
opacity: 0.7;
}
.debug-overlay .metric-value {
font-weight: bold;
margin-left: 8px;
}
.fit-toggle {
position: absolute;
top: 12px;
right: 12px;
width: 48px;
height: 48px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #444;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #d4d4d4;
font-size: 20px;
transition: all 0.2s ease;
user-select: none;
z-index: 10;
}
.fit-toggle:hover {
background: rgba(0, 0, 0, 0.9);
border-color: #666;
transform: translateY(-1px);
}
.fit-toggle:active {
transform: translateY(0px);
}
.fit-toggle.active {
border-color: #569cd6;
color: #569cd6;
}
</style> </style>
<div style="position: relative; width: 100%; height: 100%;"> <div style="position: relative; width: 100%; height: 100%;">
<div <div

View file

@ -1,6 +1,5 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js'; 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'; import { TerminalRenderer, type BufferCell } from '../utils/terminal-renderer.js';
interface BufferSnapshot { interface BufferSnapshot {
@ -36,10 +35,7 @@ export class VibeTerminalBuffer extends LitElement {
private resizeObserver: ResizeObserver | null = null; private resizeObserver: ResizeObserver | null = null;
private lastModified: string | null = null; private lastModified: string | null = null;
connectedCallback() { // Moved to render() method above
super.connectedCallback();
this.startPolling();
}
disconnectedCallback() { disconnectedCallback() {
this.stopPolling(); this.stopPolling();
@ -59,6 +55,8 @@ export class VibeTerminalBuffer extends LitElement {
} }
updated(changedProperties: Map<string, unknown>) { updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
if (changedProperties.has('sessionId')) { if (changedProperties.has('sessionId')) {
this.buffer = null; this.buffer = null;
this.error = null; this.error = null;
@ -74,6 +72,11 @@ export class VibeTerminalBuffer extends LitElement {
if (changedProperties.has('fontSize') || changedProperties.has('fitHorizontally')) { if (changedProperties.has('fontSize') || changedProperties.has('fitHorizontally')) {
this.calculateDimensions(); this.calculateDimensions();
} }
// Update buffer content after any render
if (this.container && this.buffer) {
this.updateBufferContent();
}
} }
private setupResize() { private setupResize() {
@ -172,6 +175,7 @@ export class VibeTerminalBuffer extends LitElement {
this.lastModified = stats.lastModified; this.lastModified = stats.lastModified;
this.error = null; this.error = null;
// Request update which will trigger updated() lifecycle
this.requestUpdate(); this.requestUpdate();
} catch (error) { } catch (error) {
console.error('Error fetching buffer:', error); console.error('Error fetching buffer:', error);
@ -181,51 +185,25 @@ export class VibeTerminalBuffer extends LitElement {
} }
} }
connectedCallback() {
super.connectedCallback();
this.startPolling();
}
render() { render() {
const lineHeight = this.displayedFontSize * 1.2;
return html` return html`
<style> <style>
/* Import terminal color variables from parent scope */ /* Dynamic terminal sizing for this instance */
.terminal-container { vibe-terminal-buffer .terminal-container {
/* Ensure CSS variables are inherited */ font-size: ${this.displayedFontSize}px;
color: #d4d4d4; line-height: ${lineHeight}px;
background-color: transparent;
} }
/* Terminal character styling */ vibe-terminal-buffer .terminal-line {
.terminal-char { height: ${lineHeight}px;
font-variant-ligatures: none; line-height: ${lineHeight}px;
font-feature-settings: 'liga' 0;
white-space: pre;
}
.terminal-char.bold {
font-weight: bold;
}
.terminal-char.italic {
font-style: italic;
}
.terminal-char.underline {
text-decoration: underline;
}
.terminal-char.dim {
opacity: 0.5;
}
.terminal-char.strikethrough {
text-decoration: line-through;
}
.terminal-char.cursor {
background-color: #23d18b !important;
}
/* Terminal line styling */
.terminal-line {
white-space: pre;
overflow: hidden;
} }
</style> </style>
<div class="relative w-full h-full overflow-hidden bg-[#1e1e1e]"> <div class="relative w-full h-full overflow-hidden bg-[#1e1e1e]">
@ -239,53 +217,34 @@ export class VibeTerminalBuffer extends LitElement {
<div <div
id="buffer-container" id="buffer-container"
class="terminal-container w-full h-full overflow-x-auto overflow-y-hidden font-mono antialiased" class="terminal-container w-full h-full overflow-x-auto overflow-y-hidden font-mono antialiased"
style="font-size: ${this.displayedFontSize}px; line-height: 1.2;" ></div>
>
${this.renderBuffer()}
</div>
`} `}
</div> </div>
`; `;
} }
private renderBuffer() { private updateBufferContent() {
if (!this.buffer) { if (!this.container || !this.buffer) return;
return html`<div class="terminal-line"></div>`;
}
const lineHeight = this.displayedFontSize * 1.2; const lineHeight = this.displayedFontSize * 1.2;
let html = '';
// In fitHorizontally mode, we show all content scaled to fit // In fitHorizontally mode, we show all content scaled to fit
// Otherwise, we show from the top and let it overflow // Otherwise, we show from the top and let it overflow
if (this.fitHorizontally) { const cellsToRender = this.fitHorizontally
// Render all lines - the buffer is already trimmed of blank lines from the bottom ? this.buffer.cells
return this.buffer.cells.map((row, index) => { : this.buffer.cells.slice(0, this.actualRows);
const isCursorLine = index === this.buffer.cursorY;
const cursorCol = isCursorLine ? this.buffer.cursorX : -1;
const lineContent = TerminalRenderer.renderLineFromCells(row, cursorCol);
return html` cellsToRender.forEach((row, index) => {
<div class="terminal-line" style="height: ${lineHeight}px; line-height: ${lineHeight}px;"> const isCursorLine = index === this.buffer.cursorY;
${unsafeHTML(lineContent)} const cursorCol = isCursorLine ? this.buffer.cursorX : -1;
</div> const lineContent = TerminalRenderer.renderLineFromCells(row, cursorCol);
`;
});
} else {
// Show only what fits in the viewport
const visibleCells = this.buffer.cells.slice(0, this.actualRows);
return visibleCells.map((row, index) => { html += `<div class="terminal-line" style="height: ${lineHeight}px; line-height: ${lineHeight}px;">${lineContent}</div>`;
const isCursorLine = index === this.buffer.cursorY; });
const cursorCol = isCursorLine ? this.buffer.cursorX : -1;
const lineContent = TerminalRenderer.renderLineFromCells(row, cursorCol);
return html` // Set innerHTML directly like terminal.ts does
<div class="terminal-line" style="height: ${lineHeight}px; line-height: ${lineHeight}px;"> this.container.innerHTML = html;
${unsafeHTML(lineContent)}
</div>
`;
});
}
} }
/** /**

View file

@ -110,15 +110,24 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
/* Terminal line styling */ /* Terminal container styling */
.terminal-line { .terminal-container {
color: #d4d4d4;
white-space: pre;
overflow: hidden;
}
/* Terminal line styling */
.terminal-line {
display: block;
white-space: pre; white-space: pre;
overflow: hidden; overflow: hidden;
color: #d4d4d4;
} }
/* Terminal character specific styling */ /* Terminal character specific styling */
.terminal-char { .terminal-char {
display: inline-block;
font-family: inherit;
font-variant-ligatures: none; font-variant-ligatures: none;
font-feature-settings: 'liga' 0; font-feature-settings: 'liga' 0;
white-space: pre; white-space: pre;
@ -149,12 +158,20 @@ body {
text-decoration: overline; text-decoration: overline;
} }
/* Cursor styling */ .terminal-char.inverse {
.terminal-char.cursor { filter: invert(1);
animation: blink 1s infinite;
} }
@keyframes blink { .terminal-char.invisible {
opacity: 0;
}
/* Cursor styling */
.terminal-char.cursor {
animation: cursor-blink 1s infinite;
}
@keyframes cursor-blink {
0%, 0%,
50% { 50% {
opacity: 1; opacity: 1;
@ -221,23 +238,23 @@ body {
opacity: 0 !important; opacity: 0 !important;
} }
/* XTerm 256-color palette - Complete */ /* Terminal color palette - Bright colors for dark backgrounds */
:root { :root {
--terminal-color-0: #000000; --terminal-color-0: #000000;
--terminal-color-1: #800000; --terminal-color-1: #f14c4c;
--terminal-color-2: #008000; --terminal-color-2: #23d18b;
--terminal-color-3: #808000; --terminal-color-3: #f5f543;
--terminal-color-4: #000080; --terminal-color-4: #3b8eea;
--terminal-color-5: #800080; --terminal-color-5: #d670d6;
--terminal-color-6: #008080; --terminal-color-6: #29b8db;
--terminal-color-7: #c0c0c0; --terminal-color-7: #e5e5e5;
--terminal-color-8: #808080; --terminal-color-8: #666666;
--terminal-color-9: #ff0000; --terminal-color-9: #ff6b6b;
--terminal-color-10: #00ff00; --terminal-color-10: #5af78e;
--terminal-color-11: #ffff00; --terminal-color-11: #f4f99d;
--terminal-color-12: #0000ff; --terminal-color-12: #70a5ed;
--terminal-color-13: #ff00ff; --terminal-color-13: #d670d6;
--terminal-color-14: #00ffff; --terminal-color-14: #5fb3d3;
--terminal-color-15: #ffffff; --terminal-color-15: #ffffff;
--terminal-color-16: #000000; --terminal-color-16: #000000;
--terminal-color-17: #00005f; --terminal-color-17: #00005f;
@ -480,3 +497,114 @@ body {
--terminal-color-254: #e4e4e4; --terminal-color-254: #e4e4e4;
--terminal-color-255: #eeeeee; --terminal-color-255: #eeeeee;
} }
/* Terminal link styling */
.terminal-link {
color: #4fc3f7;
text-decoration: underline;
cursor: pointer;
transition: background-color 0.2s ease;
}
.terminal-link:hover {
background-color: rgba(79, 195, 247, 0.2);
}
/* Scroll to bottom button */
.scroll-to-bottom {
position: absolute;
bottom: 12px;
left: 12px;
width: 48px;
height: 48px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #444;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #d4d4d4;
font-size: 24px;
transition: all 0.2s ease;
user-select: none;
z-index: 10;
}
.scroll-to-bottom:hover {
background: rgba(0, 0, 0, 0.9);
border-color: #666;
transform: translateY(-1px);
}
.scroll-to-bottom:active {
transform: translateY(0px);
}
/* Debug overlay */
.debug-overlay {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #444;
border-radius: 4px;
padding: 8px 12px;
font-family: 'Fira Code', monospace;
font-size: 11px;
color: #d4d4d4;
user-select: none;
z-index: 10;
line-height: 1.4;
}
.debug-overlay .metric {
display: flex;
justify-content: space-between;
min-width: 120px;
}
.debug-overlay .metric-label {
opacity: 0.7;
}
.debug-overlay .metric-value {
font-weight: bold;
margin-left: 8px;
}
/* Fit toggle button */
.fit-toggle {
position: absolute;
top: 12px;
right: 12px;
width: 48px;
height: 48px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #444;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #d4d4d4;
font-size: 20px;
transition: all 0.2s ease;
user-select: none;
z-index: 10;
}
.fit-toggle:hover {
background: rgba(0, 0, 0, 0.9);
border-color: #666;
transform: translateY(-1px);
}
.fit-toggle:active {
transform: translateY(0px);
}
.fit-toggle.active {
border-color: #569cd6;
color: #569cd6;
}