Retour à la liste

Blog

Dans les coulisses du protocole : streaming de terminal en temps réel du Mac vers l'iPhone

Comment nous avons construit un protocole basé sur WebSocket qui transmet la sortie du terminal tmux vers l'iPhone en temps réel, avec des mises à jour différentielles, un rendu ANSI et des objectifs de latence ambitieux.

Publié le Tags: engineering / protocol / performance / terminal
Blog

Suivez les mises à jour produit et ingénierie de cette catégorie.

Voir la catégorie

Le streaming de terminal est le coeur de Tactic Remote. Chaque fonctionnalité — approbations, notifications, surveillance de session — repose sur la capacité de l'iPhone à afficher une représentation précise et réactive de ce qui se passe dans le terminal de votre Mac. Obtenir ce résultat a nécessité la résolution de plusieurs problèmes non évidents au niveau du protocole.

Cet article décrit le fonctionnement de notre protocole de streaming, de la capture tmux au rendu sur iPhone, ainsi que les compromis d'ingénierie que nous avons faits en cours de route.

Le pipeline

Les données du terminal traversent cinq étapes :

  1. Capture tmux — L'app Mac companion lit le contenu du terminal depuis les panneaux tmux.
  2. Calcul des différences — Nous comparons la capture courante avec le dernier état transmis.
  3. Sérialisation — Les régions modifiées sont empaquetées dans une trame binaire compacte.
  4. Transport WebSocket — Les trames sont envoyées à l'iPhone via une connexion WebSocket persistante.
  5. Rendu ANSI — Le client iPhone interprète les séquences d'échappement et affiche le texte mis en forme.

Chaque étape a son propre budget de latence. Notre objectif est une livraison de bout en bout en moins de 100ms sur un réseau local, avec une marge significative à chaque étape.

Capture de panneau tmux

Tactic Remote gère les sessions Claude Code via tmux. C'est une décision architecturale précoce qui porte ses fruits pour le streaming : tmux maintient un buffer de terminal virtuel que nous pouvons lire programmatiquement sans intercepter le flux PTY réel.

Nous utilisons tmux capture-pane avec les flags -p et -e pour extraire le contenu du panneau avec les séquences d'échappement ANSI préservées. Les captures s'exécutent à un intervalle de sondage de 50ms pendant le streaming actif — assez rapide pour donner une impression de temps réel, assez espacé pour éviter un overhead CPU significatif.

Une subtilité : capture-pane retourne le contenu visible du panneau plus l'historique de défilement. Nous ne capturons par défaut que le viewport visible et transmettons l'historique à la demande lorsque l'utilisateur fait défiler vers le haut sur l'iPhone. Cela maintient le payload par trame petit pour le cas courant.

Le processus de capture ajoute environ 3-8ms par trame sur un Mac moderne, mesuré sur du matériel M1 à M4.

Mises à jour différentielles

Envoyer l'intégralité du buffer terminal à chaque trame serait du gaspillage. Un panneau terminal typique de 80x24 contient environ 1 920 caractères. Avec les séquences ANSI, une trame complète peut atteindre 8-12 Ko. À 20 trames par seconde, cela représente 160-240 Ko/s de bande passante — acceptable en Wi-Fi local mais problématique pour les connexions cellulaires via Cloudflare Tunnel.

Au lieu de cela, nous calculons des différences au niveau des lignes entre les trames consécutives. La plupart des mises à jour de terminal n'affectent que quelques lignes — une nouvelle ligne de log apparaît, une barre de progression avance, un curseur se déplace. Notre algorithme de différenciation identifie les lignes modifiées par comparaison de hash et ne transmet que le contenu modifié.

En pratique, les mises à jour différentielles réduisent la taille moyenne des trames de 85-92%. Une trame différentielle typique pendant une sortie active de Claude Code fait 400-800 octets. Pendant les périodes d'inactivité (attente d'une saisie utilisateur ou de réponses API), aucune trame n'est envoyée — nous ne transmettons que lorsque le contenu change.

MétriqueTrame complèteDifférentielle
Taille moyenne de trame8-12 Ko400-800 octets
Bande passante à 20 fps160-240 Ko/s8-16 Ko/s
Bande passante au repos160-240 Ko/s0 Ko/s

Le format de transmission

Les trames différentielles sont sérialisées dans un format binaire compact plutôt qu'en JSON. Chaque trame contient :

  • Un en-tête de 4 octets : version du protocole (1 octet), type de trame (1 octet) et longueur du payload (2 octets).
  • Un numéro de séquence pour l'ordonnancement et la détection de pertes.
  • Un tableau d'enregistrements de mise à jour de lignes, chacun contenant l'index de ligne, le nouveau contenu (UTF-8 avec séquences ANSI intégrées) et une somme de contrôle CRC32 pour la vérification d'intégrité.

Nous avons choisi un format binaire personnalisé plutôt que Protocol Buffers ou MessagePack après que les benchmarks ont montré que l'overhead de sérialisation pour les petits messages fréquents était dominé par les coûts d'initialisation de la bibliothèque. Notre sérialiseur écrit manuellement ajoute moins de 1ms par trame.

Transport WebSocket

La connexion de streaming utilise un unique WebSocket persistant. Nous avons évalué les Server-Sent Events (SSE) et le TCP brut, mais WebSocket offrait la meilleure combinaison de communication bidirectionnelle (nécessaire pour les requêtes de défilement et le contrôle de flux), de compatibilité réseau étendue et de framing intégré.

La gestion des connexions traite trois scénarios :

Réseau local — L'iPhone se connecte directement au serveur local du Mac companion. Le temps d'aller-retour est typiquement de 2-8ms. La latence totale de streaming (de la capture au rendu) est en moyenne de 40-60ms.

Cloudflare Tunnel — Le trafic transite par le réseau de bordure de Cloudflare. Le temps d'aller-retour augmente à 30-80ms selon la géographie. La latence totale de streaming est en moyenne de 120-200ms. Suffisamment réactif pour la surveillance, bien que les utilisateurs remarquent la différence lors de sorties défilant rapidement.

Transitions réseau — Lorsque l'iPhone bascule entre Wi-Fi et cellulaire (ou entre réseaux), la connexion WebSocket est interrompue. Notre logique de reconnexion rétablit la connexion en 1-2 secondes et demande une resynchronisation complète pour restaurer l'état. Pendant la reconnexion, un indicateur visuel sur l'iPhone affiche l'état de la connexion pour que les utilisateurs comprennent pourquoi la sortie est en pause.

Gestion des séquences d'échappement ANSI

La sortie terminal de Claude Code est riche en séquences d'échappement ANSI — des couleurs pour la coloration syntaxique, du gras pour l'emphase, du positionnement de curseur pour les indicateurs de progression. Préserver ces séquences avec précision sur l'iPhone est essentiel pour la lisibilité.

Notre moteur de rendu iOS gère les séquences SGR (Select Graphic Rendition) les plus courantes : 16 couleurs standard, palette de 256 couleurs, couleur vraie 24 bits, gras, italique, souligné et inversé. Nous ne supportons intentionnellement pas les séquences de mouvement de curseur (CSI H, CSI A/B/C/D) dans la vue rendue — celles-ci sont consommées lors de la capture tmux et reflétées dans le positionnement des lignes plutôt que transmises comme codes d'échappement bruts.

Un défi spécifique au mobile : le mode sombre. Les schémas de couleurs de terminal conçus pour des fonds sombres s'affichent mal quand iOS est en mode clair (et inversement). Nous détectons le mode d'apparence actuel de l'iPhone et appliquons un mappage de couleurs qui préserve les rapports de contraste. Ce n'est pas un thème — c'est un ajustement de luminance qui conserve l'intention de couleur originale tout en assurant la lisibilité.

Budget de performance

Nous suivons les performances de streaming sur trois percentiles :

MétriqueP50P95P99
Latence de capture4ms7ms12ms
Calcul des différences1ms3ms5ms
Sérialisation<1ms1ms2ms
Réseau (local)3ms8ms15ms
Réseau (tunnel)45ms90ms150ms
Rendu sur iPhone6ms12ms18ms
Total (local)15ms31ms52ms
Total (tunnel)57ms113ms187ms

Ces chiffres proviennent de la télémétrie de production de notre base d'utilisateurs bêta. La latence P50 locale de 15ms signifie que pour la plupart des interactions, l'écran de l'iPhone se met à jour plus vite que l'oeil humain ne peut percevoir le délai.

Ce que nous avons appris

La construction d'un protocole de streaming en temps réel pour la sortie terminal nous a enseigné plusieurs leçons :

Le sondage bat l'événementiel pour ce cas d'usage. Nous avons initialement essayé de nous brancher sur les événements internes de mise à jour de tmux, mais avons constaté que le sondage à 50ms était plus simple, plus fiable et avait un coût de performance négligeable.

Les différences au niveau des lignes sont suffisantes. Nous avons prototypé des différences au niveau des caractères, mais la complexité supplémentaire n'a produit qu'une réduction de bande passante additionnelle de 5-8%, ne justifiant pas la charge de maintenance.

Le framing binaire compte à haute fréquence. Pour des messages envoyés 20 fois par seconde, chaque octet d'overhead dans le format de sérialisation est amplifié. La verbosité du JSON avait un coût mesurable.

L'UX de reconnexion est aussi importante que la performance de connexion. Les utilisateurs tolèrent une reconnexion de 2 secondes s'ils comprennent ce qui se passe. Ils perçoivent une reconnexion de 500ms comme défaillante s'il n'y a pas de retour visuel.

Pour les détails d'implémentation, consultez notre documentation Architecture. Si vous construisez quelque chose de similaire, nous serons ravis de discuter de notre approche — contactez-nous via la page Support.

Essayez Tactic Remote

Pilotez vos agents de programmation depuis votre téléphone

Connectez-vous à Claude Code, Codex et aux autres agents sur votre Mac, PC Windows ou Linux. Consultez leur progression et envoyez la prochaine instruction depuis votre iPhone ou iPad.