From 0d7a01b450ca356a2b5a868237c0dc54fa61bb5f Mon Sep 17 00:00:00 2001 From: Shaenn <22753401+Shaenn@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:14:53 +0200 Subject: [PATCH 1/9] =?UTF-8?q?Un=20flux=20coup=C3=A9=20ne=20laisse=20plus?= =?UTF-8?q?=20l'=C3=A9cran=20affirmer=20ce=20qu'il=20ne=20sait=20plus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `EventSource` se reconnaît seul, et le `snapshot` qui suit remet tout d'aplomb : la coupure n'est pas une panne. Mais tant qu'elle dure, la dernière phase reçue restait affichée avec son chrono qui montait — il tourne côté client. Une session coupée pendant un compactage annonçait ainsi un compactage qui n'avait plus lieu, sous un statut « Au travail » figé à la dernière trame. `connected` existait et n'était pas déstructuré par la page. Il ne suffisait pas non plus : faux entre l'ouverture et la première trame, il aurait annoncé une perte là où il n'y avait qu'une attente. D'où `lost`, qui ne se lève qu'après un flux qui a vécu. Les outils en vol s'arrêtent de battre du même geste : ils viennent d'`activity`, que la coupure remet à `IDLE_ACTIVITY`. Au passage : une erreur appartient au statut qui la porte. Sans cette remise à zéro, celle d'un tour ancien restait sous le composeur pour le reste de la session, y compris après une reprise réussie. Co-Authored-By: Claude Opus 5 (1M context) --- src/composables/useLiveSession.ts | 32 ++++++++++++++++++++++++++++--- src/i18n/en/pages/atelier.ts | 1 + src/i18n/fr/pages/atelier.ts | 3 +++ src/pages/AtelierPage.vue | 18 +++++++++++++++-- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/composables/useLiveSession.ts b/src/composables/useLiveSession.ts index 31b0340..0a7bf68 100644 --- a/src/composables/useLiveSession.ts +++ b/src/composables/useLiveSession.ts @@ -36,6 +36,15 @@ export function useLiveSession() { const permissions = ref([]) const asks = ref([]) const connected = ref(false) + /** + * Le lien a été établi, puis rompu — et pas encore rétabli. + * + * `connected` seul ne suffit pas à le dire : il est faux entre l'ouverture de + * la page et la première trame, où il n'y a pourtant rien d'anormal à + * signaler. Ce drapeau ne se lève qu'après un flux qui a vécu, et retombe dès + * qu'il reprend. + */ + const lost = ref(false) /** * Ce que l'agent fait à l'instant. Un état à part de la timeline : il ne * s'ajoute à rien, il se remplace — et il n'a pas de passé. @@ -69,6 +78,8 @@ export function useLiveSession() { let source: EventSource | null = null let attached = '' + /** Le flux a-t-il déjà porté quelque chose ? Voir `lost`. */ + let opened = false let commandsAsked = false let filesAsked = false const index = new Map() @@ -150,7 +161,10 @@ export function useLiveSession() { case 'status': status.value = upsert.status - if (upsert.error) error.value = upsert.error + // Une erreur appartient au statut qui la porte : sans cette remise à + // zéro, celle d'un tour ancien restait sous le composeur pour le reste + // de la session, y compris après une reprise réussie. + error.value = upsert.error ?? '' if (session.value) session.value.status = upsert.status return @@ -235,16 +249,25 @@ export function useLiveSession() { filesTruncated.value = false filesAsked = false attached = runId + opened = false index.clear() source = new EventSource(streamUrl(runId)) source.onopen = () => { connected.value = true + lost.value = false + opened = true } source.onerror = () => { - // `EventSource` se reconnecte seul ; le `snapshot` qui suivra remettra - // l'état d'aplomb. On ne signale donc qu'une coupure visible, pas une panne. + // `EventSource` se reconnecte seul, et le `snapshot` qui suivra remettra + // l'état d'aplomb : la coupure n'est donc pas une panne. Mais tant qu'elle + // dure, on ne sait plus ce que fait l'agent — et la dernière phase reçue + // resterait à l'écran, son chrono montant à vide, parce qu'il tourne ici. + // Une session coupée pendant un compactage annonçait ainsi un compactage + // qui n'avait plus lieu. connected.value = false + lost.value = opened + activity.value = IDLE_ACTIVITY } // Le serveur nomme chaque trame de son `kind` : on écoute par nom plutôt que // `onmessage`, qui ne reçoit que les trames sans nom. @@ -276,6 +299,8 @@ export function useLiveSession() { source = null attached = '' connected.value = false + lost.value = false + opened = false // Débranché, on ne sait plus ce qui se passe : laisser la dernière phase // affichée ferait croire à une session qu'on regarde encore. activity.value = IDLE_ACTIVITY @@ -291,6 +316,7 @@ export function useLiveSession() { permissions, asks, connected, + lost, activity, commands, commandsLoading, diff --git a/src/i18n/en/pages/atelier.ts b/src/i18n/en/pages/atelier.ts index 5141948..d59f262 100644 --- a/src/i18n/en/pages/atelier.ts +++ b/src/i18n/en/pages/atelier.ts @@ -68,6 +68,7 @@ const atelier: MessageSchema['pages']['atelier'] = { failed: 'Stopped on error', }, resumed: 'resumed', + lost: 'I’ve lost contact', autoScroll: 'Auto-scroll', followLive: 'Follow live', fullReplay: 'Full replay', diff --git a/src/i18n/fr/pages/atelier.ts b/src/i18n/fr/pages/atelier.ts index c2cb787..949fb6a 100644 --- a/src/i18n/fr/pages/atelier.ts +++ b/src/i18n/fr/pages/atelier.ts @@ -83,6 +83,9 @@ export default { failed: 'Arrêtée sur erreur', }, resumed: 'reprise', + // Ce qui est rompu est le flux, pas la session : elle travaille peut-être + // encore. La phrase dit donc ce qu'AURA ne voit plus, jamais ce qui a échoué. + lost: 'J’ai perdu le contact', autoScroll: 'Auto-défilement', followLive: 'Suivre le direct', fullReplay: 'Rejeu complet', diff --git a/src/pages/AtelierPage.vue b/src/pages/AtelierPage.vue index aea6979..61fbccd 100644 --- a/src/pages/AtelierPage.vue +++ b/src/pages/AtelierPage.vue @@ -236,8 +236,15 @@ -