fix(web): repeindre les terminaux au retour-visible + flush ACK ciblé
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled

Un terminal caché dans une grille de groupe (ou hors-écran) n'était pas repeint
au retour — `visibilitychange` ne couvre que l'onglet — d'où l'écran noir avec
curseur ; et son ACK ne progressait plus (callback xterm.write throttlé) → le
serveur mettait le PTY en pause → sortie « par à-coups ». Ajoute un
IntersectionObserver (repaint + flushAck ciblé au retour-visible) et expose
Attachment.flushAck (factorisé avec flushAcks).
This commit is contained in:
2026-06-24 10:15:29 +02:00
parent b5236b41c8
commit 0f126cf911
3 changed files with 91 additions and 6 deletions

View File

@@ -38,6 +38,7 @@ const ended = ref(false);
let term: Terminal | null = null;
let attachment: Attachment | null = null;
let resizeObserver: ResizeObserver | null = null;
let intersectionObserver: IntersectionObserver | null = null;
let onVisible: (() => void) | null = null;
let disposed = false;
@@ -130,6 +131,20 @@ onMounted(async () => {
activeTerm.onData((data) => attachment?.sendStdin(data));
resizeObserver = new ResizeObserver(refit);
resizeObserver.observe(container.value);
// Re-révélation RÉELLE du conteneur : cellule de grille démontée/remontée (maximize↔grille), scroll
// hors/dans le viewport, ou navigation SPA — autant de cas que `visibilitychange` (onglet only) ne
// couvre PAS. Au retour visible, on repeint (le rendu était gelé hors-écran) ET on flushe l'ACK pour
// sortir d'une éventuelle pause serveur (le callback de xterm.write était throttlé tant que caché).
intersectionObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
// évite un fit() sur conteneur de taille 0 (remontage transitoire) → déforme le TUI
if (entry.intersectionRect.width === 0 || entry.intersectionRect.height === 0) continue;
refit();
attachment?.flushAck();
}
});
intersectionObserver.observe(container.value);
// au retour en avant-plan, forcer un repaint (le renderer WebGL peut avoir « gelé » l'écran pendant
// que l'onglet était caché) ; le ws-client flushe en parallèle les ACK pour relancer le flux.
onVisible = (): void => {
@@ -142,6 +157,7 @@ onMounted(async () => {
onBeforeUnmount(() => {
disposed = true;
resizeObserver?.disconnect();
intersectionObserver?.disconnect();
if (onVisible) document.removeEventListener('visibilitychange', onVisible);
attachment?.detach();
term?.dispose();