fix: recherche worktrees, sessions externes masquées, rendu/historique terminal & plein écran grille

- Recherche : le watcher débouncé du singleton useWorktreeView est créé dans un effectScope
  détaché → ne gèle plus après une navigation ; recherche aussi par label de dépôt ; accès à
  la palette depuis le MoreSheet mobile (sidebar/⌘K indisponibles sur mobile).
- Sessions : sessionsForCwd exclut désormais toujours les sessions masquées ; les sessions
  externes (discovered) sont masquées par défaut côté UI, avec puce de révélation par carte et
  case globale dans le dashboard.
- Terminal : ACK découplé du callback de rendu (armé à la réception + flush au retour de
  visibilité) → plus de pause PTY quand l'onglet passe en arrière-plan ; repaint forcé sur
  perte de contexte WebGL / resize / retour en avant-plan. Historique élargi : REPLAY_TAIL_BYTES
  1 Mo, RING_CAPACITY 4 Mo, scrollback xterm 20k.
- Grille de groupe : bouton « plein écran » sur chaque cellule ouvrant /sessions/:id.
This commit is contained in:
2026-06-23 17:42:49 +02:00
parent 5ebe3a0f37
commit 663ae7ace1
15 changed files with 192 additions and 30 deletions
+20 -3
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 onVisible: (() => void) | null = null;
let disposed = false;
onMounted(async () => {
@@ -47,7 +48,7 @@ onMounted(async () => {
cursorBlink: true,
fontSize: 13,
fontFamily: "ui-monospace, 'JetBrains Mono', 'Fira Code', Menlo, Consolas, monospace",
scrollback: 10_000,
scrollback: 20_000, // absorbe le replay élargi (REPLAY_TAIL_BYTES = 1 Mo) → on remonte plus loin
theme: {
background: '#09090b',
foreground: '#d4d4d8',
@@ -58,18 +59,24 @@ onMounted(async () => {
const fit = new FitAddon();
term.loadAddon(fit);
term.open(container.value);
const activeTerm = term;
try {
const { WebglAddon } = await import('@xterm/addon-webgl');
const webgl = new WebglAddon();
webgl.onContextLoss(() => webgl.dispose());
// perte de contexte GPU : on dispose (retour au renderer DOM) PUIS on repeint — sinon l'écran
// reste figé sur le dernier rendu WebGL jusqu'à la prochaine interaction (« texte qui ne s'affiche
// que si ça bouge »).
webgl.onContextLoss(() => {
webgl.dispose();
activeTerm.refresh(0, activeTerm.rows - 1);
});
term.loadAddon(webgl);
} catch {
// WebGL indisponible : xterm retombe sur le renderer DOM
}
fit.fit();
const activeTerm = term;
// Re-mesure après stabilisation du layout flex : au `onMounted`, le conteneur peut ne pas avoir sa
// taille finale (la hauteur flex se résout après le 1er paint) → un seul `fit()` donne trop peu de
@@ -82,6 +89,9 @@ onMounted(async () => {
/* conteneur sans dimension (démontage en cours) : ignoré */
}
attachment?.resize(activeTerm.cols, activeTerm.rows);
// repaint explicite : le renderer WebGL ne repeint pas toujours une zone non « dirty » (retour
// d'arrière-plan, resync) → on force un rafraîchissement pour éviter un écran figé.
activeTerm.refresh(0, activeTerm.rows - 1);
};
try {
@@ -120,12 +130,19 @@ onMounted(async () => {
activeTerm.onData((data) => attachment?.sendStdin(data));
resizeObserver = new ResizeObserver(refit);
resizeObserver.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 => {
if (document.visibilityState === 'visible') activeTerm.refresh(0, activeTerm.rows - 1);
};
document.addEventListener('visibilitychange', onVisible);
if (props.mode === 'interactive') activeTerm.focus();
});
onBeforeUnmount(() => {
disposed = true;
resizeObserver?.disconnect();
if (onVisible) document.removeEventListener('visibilitychange', onVisible);
attachment?.detach();
term?.dispose();
});