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:
@@ -108,6 +108,7 @@ export class WsClient {
|
||||
private socket: WebSocket | null = null;
|
||||
private ready = false;
|
||||
private stopped = true;
|
||||
private visibilityBound = false;
|
||||
private backoffMs = BACKOFF_MIN_MS;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
/** terminaux ouverts, ré-attachés à chaque reconnexion (ordre d'insertion = ordre FIFO) */
|
||||
@@ -121,6 +122,14 @@ export class WsClient {
|
||||
|
||||
connect(): void {
|
||||
this.stopped = false;
|
||||
// Au retour en avant-plan, le rendu se débloque : on flushe les ACK pour ne pas attendre le
|
||||
// débounce si le serveur s'était mis en pause pendant que l'onglet/PWA était caché.
|
||||
if (!this.visibilityBound && typeof document !== 'undefined') {
|
||||
this.visibilityBound = true;
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') this.flushAcks();
|
||||
});
|
||||
}
|
||||
if (this.socket || this.reconnectTimer) return;
|
||||
if (this.status.value === 'idle') this.status.value = 'connecting';
|
||||
this.openSocket();
|
||||
@@ -301,6 +310,9 @@ export class WsClient {
|
||||
att.lastAckBytes = 0;
|
||||
att.sink.reset();
|
||||
if (frame.payload.byteLength > 0) att.sink.write(frame.payload);
|
||||
// filet anti-stall : le replay ne passe PAS de callback de write → on arme quand même l'ACK
|
||||
// traînant pour que le flux suivant ne puisse pas rester gelé (serveur en pause).
|
||||
this.scheduleTrailingAck(att, att.epoch);
|
||||
return;
|
||||
}
|
||||
if (frame.type !== BINARY_FRAME.OUTPUT) return;
|
||||
@@ -315,18 +327,43 @@ export class WsClient {
|
||||
att.lastAckBytes = att.processedBytes;
|
||||
this.sendControl({ type: 'ack', channel: att.channel, bytes: att.processedBytes });
|
||||
}
|
||||
// ACK traînant débouncé : si le flux s'arrête avec un reliquat non-ACKé,
|
||||
// le serveur ne doit jamais rester en pause faute d'ACK suivant.
|
||||
if (att.trailingAckTimer) clearTimeout(att.trailingAckTimer);
|
||||
att.trailingAckTimer = setTimeout(() => {
|
||||
att.trailingAckTimer = null;
|
||||
if (att.closed || att.epoch !== epoch || att.channel < 0) return;
|
||||
if (att.processedBytes > att.lastAckBytes) {
|
||||
att.lastAckBytes = att.processedBytes;
|
||||
this.sendControl({ type: 'ack', channel: att.channel, bytes: att.processedBytes });
|
||||
}
|
||||
}, 200);
|
||||
this.scheduleTrailingAck(att, epoch);
|
||||
});
|
||||
// Filet anti-stall, INDÉPENDANT du callback de xterm.write : ce callback est throttlé quand
|
||||
// l'onglet/PWA passe en arrière-plan (rAF + setTimeout ralentis) → sans cela l'ACK ne partait
|
||||
// plus, le serveur dépassait HIGH_WATERMARK et mettait le PTY en pause, et la sortie « chargeait
|
||||
// par à-coups » jusqu'à ce que l'utilisateur bouge. On arme donc l'ACK traînant dès la réception.
|
||||
this.scheduleTrailingAck(att, epoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* (Ré)arme l'ACK traînant débouncé : si rien d'autre n'arrive, il envoie le reliquat d'octets
|
||||
* réellement traités par xterm (`processedBytes`) pour que le serveur sorte toujours de pause.
|
||||
* Appelé à la fois dans le callback de write ET à la réception d'une frame (le callback peut être
|
||||
* indéfiniment retardé quand le rendu est gelé). Sémantique inchangée : on n'ACK que du traité.
|
||||
*/
|
||||
private scheduleTrailingAck(att: Attachment, epoch: number): void {
|
||||
if (att.trailingAckTimer) clearTimeout(att.trailingAckTimer);
|
||||
att.trailingAckTimer = setTimeout(() => {
|
||||
att.trailingAckTimer = null;
|
||||
if (att.closed || att.epoch !== epoch || att.channel < 0) return;
|
||||
if (att.processedBytes > att.lastAckBytes) {
|
||||
att.lastAckBytes = att.processedBytes;
|
||||
this.sendControl({ type: 'ack', channel: att.channel, bytes: att.processedBytes });
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** ACK immédiat du reliquat traité pour tous les terminaux ouverts — au retour en avant-plan,
|
||||
* pour rattraper sans attendre le débounce un serveur éventuellement en pause. */
|
||||
private flushAcks(): void {
|
||||
for (const att of this.attachments) {
|
||||
if (att.closed || att.channel < 0) continue;
|
||||
if (att.processedBytes > att.lastAckBytes) {
|
||||
att.lastAckBytes = att.processedBytes;
|
||||
this.sendControl({ type: 'ack', channel: att.channel, bytes: att.processedBytes });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleServerMessage(msg: ServerMessage): void {
|
||||
|
||||
Reference in New Issue
Block a user