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 json
import logging
import re
import socket
from collections import deque
from dataclasses import dataclass
@@ -27,6 +28,10 @@ REPLAY_BUFFER_SIZE = 256 * 1024 # 256KB
DEFAULT_SCREEN_WIDTH = 132
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)
class DockerExecSpec:
@@ -173,8 +178,8 @@ class DockerExecSession(Session):
sock.close()
detail = body.decode("utf-8", errors="replace")
raise RuntimeError(f"Docker API exec start failed ({status}): {detail}")
if body:
self._pending_output += body
# Don't save body from HTTP upgrade - it contains protocol handshake data,
# not real terminal output (e.g., device attribute responses like "\x1b[?1;10;0c")
sock.settimeout(None)
return sock
@@ -297,6 +302,11 @@ class DockerExecSession(Session):
data = await queue.get()
if not data:
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._update_screen(data)
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:
"""Callback when a Docker container is added."""
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
self._notify_activity("__dashboard__")
def _on_docker_container_removed(self, slug: str) -> None:
"""Callback when a Docker container is removed."""
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
self._screenshot_cache.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 .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-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-title {{ font-weight: bold; margin-bottom: 4px; }}
.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
this.waitForFonts().then(() => {
console.log("[webterm:init] Fonts loaded, reapplying font family and fitting...");
this.terminal.options.fontFamily = this.fontFamily;
if (typeof (this.terminal as unknown as { loadFonts?: () => void }).loadFonts === "function") {
(this.terminal as unknown as { loadFonts: () => void }).loadFonts();
// Use renderer's setFontFamily method to properly update fonts
const renderer = (this.terminal as unknown as { renderer?: { setFontFamily: (family: string) => void; remeasureFont: () => void } }).renderer;
if (renderer) {
renderer.setFontFamily(this.fontFamily);
renderer.remeasureFont();
console.log("[webterm:init] Font family updated via renderer");
}
this.fit();
console.log("[webterm:init] fit() completed");