fix: terminal rendering and dashboard issues

- Fix escape sequence display: filter DA1 responses that can be split across socket reads
- Fix font rendering: use ghostty-web renderer API (setFontFamily/remeasureFont)
- Fix sparklines: update slug-to-service mapping when containers are added/removed
- Improve typeahead thumbnails: increase to 96x72px (4:3 ratio)
This commit is contained in:
GitHub Copilot
2026-01-29 18:23:44 +00:00
parent 074832cff2
commit 83bbd65c49
4 changed files with 27 additions and 7 deletions
+12 -2
View File
@@ -6,6 +6,7 @@ import asyncio
import contextlib import contextlib
import json import json
import logging import logging
import re
import socket import socket
from collections import deque from collections import deque
from dataclasses import dataclass from dataclasses import dataclass
@@ -27,6 +28,10 @@ REPLAY_BUFFER_SIZE = 256 * 1024 # 256KB
DEFAULT_SCREEN_WIDTH = 132 DEFAULT_SCREEN_WIDTH = 132
DEFAULT_SCREEN_HEIGHT = 45 DEFAULT_SCREEN_HEIGHT = 45
# Pattern to filter out terminal device attribute responses that cause display issues
# These are responses to queries that shouldn't be displayed as text
DA_RESPONSE_PATTERN = re.compile(rb'\x1b\[\?[\d;]+c')
@dataclass(frozen=True) @dataclass(frozen=True)
class DockerExecSpec: class DockerExecSpec:
@@ -173,8 +178,8 @@ class DockerExecSession(Session):
sock.close() sock.close()
detail = body.decode("utf-8", errors="replace") detail = body.decode("utf-8", errors="replace")
raise RuntimeError(f"Docker API exec start failed ({status}): {detail}") raise RuntimeError(f"Docker API exec start failed ({status}): {detail}")
if body: # Don't save body from HTTP upgrade - it contains protocol handshake data,
self._pending_output += body # not real terminal output (e.g., device attribute responses like "\x1b[?1;10;0c")
sock.settimeout(None) sock.settimeout(None)
return sock return sock
@@ -297,6 +302,11 @@ class DockerExecSession(Session):
data = await queue.get() data = await queue.get()
if not data: if not data:
break break
# Filter out device attribute responses that can cause display issues
# when split across socket reads
data = DA_RESPONSE_PATTERN.sub(b'', data)
if not data:
continue
await self._add_to_replay_buffer(data) await self._add_to_replay_buffer(data)
await self._update_screen(data) await self._update_screen(data)
if self._connector: if self._connector:
+8 -1
View File
@@ -360,12 +360,19 @@ class LocalServer:
def _on_docker_container_added(self, slug: str, name: str, command: str) -> None: def _on_docker_container_added(self, slug: str, name: str, command: str) -> None:
"""Callback when a Docker container is added.""" """Callback when a Docker container is added."""
log.info("Container added to dashboard: %s -> %s", name, slug) log.info("Container added to dashboard: %s -> %s", name, slug)
# Update slug-to-service mapping for sparklines
if self._docker_stats:
self._slug_to_service[slug] = name
log.debug("Added sparkline mapping: %s -> %s", slug, name)
# Notify SSE subscribers about dashboard change # Notify SSE subscribers about dashboard change
self._notify_activity("__dashboard__") self._notify_activity("__dashboard__")
def _on_docker_container_removed(self, slug: str) -> None: def _on_docker_container_removed(self, slug: str) -> None:
"""Callback when a Docker container is removed.""" """Callback when a Docker container is removed."""
log.info("Container removed from dashboard: %s", slug) log.info("Container removed from dashboard: %s", slug)
# Remove slug-to-service mapping
if self._docker_stats and slug in self._slug_to_service:
del self._slug_to_service[slug]
# Invalidate any cached screenshots # Invalidate any cached screenshots
self._screenshot_cache.pop(slug, None) self._screenshot_cache.pop(slug, None)
self._screenshot_cache_etag.pop(slug, None) self._screenshot_cache_etag.pop(slug, None)
@@ -769,7 +776,7 @@ class LocalServer:
.floating-results .search-query {{ font-size: 18px; font-weight: bold; color: #3b82f6; }} .floating-results .search-query {{ font-size: 18px; font-weight: bold; color: #3b82f6; }}
.floating-results .result-item {{ display: flex; align-items: center; gap: 12px; padding: 12px; margin: 6px 0; border: 1px solid #334155; border-radius: 6px; cursor: pointer; transition: all 0.15s; }} .floating-results .result-item {{ display: flex; align-items: center; gap: 12px; padding: 12px; margin: 6px 0; border: 1px solid #334155; border-radius: 6px; cursor: pointer; transition: all 0.15s; }}
.floating-results .result-item:hover, .floating-results .result-item.active {{ background: #334155; border-color: #3b82f6; }} .floating-results .result-item:hover, .floating-results .result-item.active {{ background: #334155; border-color: #3b82f6; }}
.floating-results .result-thumb {{ width: 72px; height: 40px; flex: 0 0 auto; border-radius: 4px; border: 1px solid #334155; background: #0b1220; object-fit: contain; }} .floating-results .result-thumb {{ width: 96px; height: 72px; flex: 0 0 auto; border-radius: 4px; border: 1px solid #334155; background: #0b1220; object-fit: contain; }}
.floating-results .result-content {{ display: flex; flex-direction: column; gap: 2px; }} .floating-results .result-content {{ display: flex; flex-direction: column; gap: 2px; }}
.floating-results .result-title {{ font-weight: bold; margin-bottom: 4px; }} .floating-results .result-title {{ font-weight: bold; margin-bottom: 4px; }}
.floating-results .result-meta {{ font-size: 12px; color: #94a3b8; }} .floating-results .result-meta {{ font-size: 12px; color: #94a3b8; }}
File diff suppressed because one or more lines are too long
+6 -3
View File
@@ -515,9 +515,12 @@ class WebTerminal {
// Wait for fonts to load before fitting to ensure correct measurements // Wait for fonts to load before fitting to ensure correct measurements
this.waitForFonts().then(() => { this.waitForFonts().then(() => {
console.log("[webterm:init] Fonts loaded, reapplying font family and fitting..."); console.log("[webterm:init] Fonts loaded, reapplying font family and fitting...");
this.terminal.options.fontFamily = this.fontFamily; // Use renderer's setFontFamily method to properly update fonts
if (typeof (this.terminal as unknown as { loadFonts?: () => void }).loadFonts === "function") { const renderer = (this.terminal as unknown as { renderer?: { setFontFamily: (family: string) => void; remeasureFont: () => void } }).renderer;
(this.terminal as unknown as { loadFonts: () => void }).loadFonts(); if (renderer) {
renderer.setFontFamily(this.fontFamily);
renderer.remeasureFont();
console.log("[webterm:init] Font family updated via renderer");
} }
this.fit(); this.fit();
console.log("[webterm:init] fit() completed"); console.log("[webterm:init] fit() completed");