# Hooks React

Exécutez une session Beeuni dans React avec le hook useSession.

Canonical page: https://beeuni.ai/docs/fr/sdk/react

`@beeuni/sdk/react` ajoute des hooks React au SDK. Cette page présente
`useSession`, le hook qui exécute une session de bout en bout, ainsi que les autres hooks
confirmés stables pour les applications React.

## useSession(projectId, sessionId, options?)

`useSession` démarre la session, ouvre le transport d’événements sélectionné par le serveur et
synchronise les messages, l’état et les demandes en attente. Appelez-le une fois par vue de session.

```tsx
import { useSession } from '@beeuni/sdk/react';

function Chat({ projectId, sessionId }: { projectId: string; sessionId: string }) {
  const s = useSession(projectId, sessionId);

  if (s.phase !== 'ready') return <Booting stage={s.stage} onRetry={s.retry} />;

  return (
    <>
      {s.messages.map(({ info, parts }) => (
        <Message key={info.id} info={info} parts={parts} />
      ))}
      <Composer busy={s.isBusy} onSend={s.send} onStop={s.cancel} />
    </>
  );
}
```

Le serveur fait autorité sur la disponibilité. L’environnement d’exécution est prêt lorsque `POST /start` renvoie
`stage: 'ready'`. `useSession` n’effectue pas de vérification de santé distincte côté client.

<a id="returns"></a>

### Valeurs renvoyées

| Champ | Type | Contenu |
|---|---|---|
| `phase` | `'starting' \| 'ready' \| 'error'` | État global. Affichez un écran de démarrage jusqu’à `ready`. |
| `messages` | `{ info, parts }[]` | La liste des messages. Les parties arrivent en direct. |
| `savedTranscript` | `'loading' \| 'shown' \| 'none'` | Indique si la conversation enregistrée peut s’afficher avant le réveil de l’ordinateur. `loading` : une copie enregistrée est en cours d’acheminement. `shown` : les messages sont dans la liste. `none` : rien ne peut s’afficher avant la réponse de l’environnement d’exécution. |
| `conversationEmpty` | `boolean` | La copie enregistrée confirme que la conversation est vide, qu’aucun tour ne s’est terminé depuis et que rien n’est ouvert ni en file d’attente. Affichez la zone de saisie, pas un écran de démarrage. |
| `status` | `SessionStatus` | L’état de la session. |
| `isBusy` | `boolean` | L’agent génère une réponse. |
| `questions`, `permissions` | array | Questions de l’agent et demandes d’approbation d’outils en attente. Ici, un `permission` est une approbation d’outil de l’environnement d’exécution, pas une autorisation IAM. |
| `diffs`, `todos` | array | Différences de fichiers et tâches à faire en direct. |
| `sendError` | `BeeuniSendError \| null` | Le dernier échec de `send` : `billing`, `runtime-not-ready` ou `runtime-error`. |
| `rewindMessageId` | `string \| null` | Le message utilisateur sélectionné pendant la préparation d’un retour en arrière réversible. |
| `rewindPending` | `boolean` | Une demande de retour en arrière ou de restauration est en cours. |
| `rewindError` | `BeeuniSendError \| null` | Le dernier échec de retour en arrière ou de restauration. |
| `models`, `agents`, `defaultAgent`, `commands` | — | Modèles sélectionnables, agents sélectionnables, agent par défaut et commandes slash. Disponibles avant le démarrage de l’environnement d’exécution. |
| `retry` | `() => void` | Force une nouvelle vérification de `/start`. |

### Actions

| Action | Fonction |
|---|---|
| `send(text, override?)` | Envoie un prompt. `override` définit `{ model?, agent? }` pour ce message uniquement. |
| `sendParts(parts, override?)` | Envoie du texte et des parties de fichiers avec URL de l’environnement d’exécution via le transport sélectionné. Les références privées préparées utilisent les routes de prompts de la plateforme. |
| `rewind(messageId)` | Ramène cette session canonique à un message utilisateur. Le message sélectionné et la suite du parcours deviennent masqués et récupérables. |
| `restoreRewind()` | Restaure le parcours supprimé avant qu’un autre prompt ne valide son remplacement. |
| `cancel()` | Arrête l’exécution en cours et efface les questions et autorisations en attente. Suspend d’abord les prompts en file d’attente de la session, afin qu’un prompt en attente ne démarre pas une nouvelle exécution. |
| `runCommand(command, args)` | Exécute une commande slash du projet. |
| `answerQuestion(id, answers)` | Répond à une question de l’agent en attente. |
| `rejectQuestion(id)` | Rejette une question de l’agent en attente. |
| `answerPermission(id, reply, message?)` | Répond à une demande d’approbation d’outil. `reply` vaut `'once'`, `'always'` ou `'reject'`. |

`useSession` renvoie aussi `removeQuestion` et `removePermission`. Ne les utilisez
pas. Ils effacent la demande de l’état local sans jamais avertir l’agent ;
l’exécution reste donc bloquée. Utilisez plutôt `answerQuestion`, `rejectQuestion` ou
`answerPermission`.

### Options

| Option | Valeur par défaut | Fonction |
|---|---|---|
| `waitMs` | `15000` | Le budget de long polling envoyé à `/start`. |
| `replayStartStash` | `true` | Rejoue un prompt enregistré avant la création de la session, une fois la session prête. |
| `enabled` | `true` | Définissez `false` pour différer le hook, par exemple jusqu’à la validation d’une vérification de facturation. |
| `chatEngine` | `true` | Définissez `false` si votre application monte sa propre interface de chat pour cette session, afin d’éviter de synchroniser les messages deux fois. |
| `initialRuntimeSessionId` | `null` | Une référence fixe de session de l’environnement d’exécution que votre serveur détient déjà pour cette session. La transcription s’affiche à partir de cette référence pendant l’exécution de `/start` ; la réponse de `/start` reste la référence faisant autorité. `initialOpenCodeSessionId` est son ancien nom, désormais obsolète. |
| `browserPresence` | `false` | Maintient le bail de présence de cet onglet pendant qu’une personne connectée utilise la page (saisie au cours des 10 dernières minutes). Le bail maintient l’ordinateur de la session éveillé. |
| `presenceAlerts` | `false` | Cet onglet affiche ses propres notifications. Transmis via le bail de présence : le serveur n’envoie pas de notifications au téléphone ni via Web Push uniquement lorsqu’un onglet émettant des alertes est utilisé. Toute modification est envoyée immédiatement. Passez `true` uniquement lorsque l’indicateur `notification_center` du projet est activé ; sinon, le serveur l’ignore. Nécessite `browserPresence`. |
| `presencePageExit` | `false` | Met fin au bail de présence lorsque la page se ferme ou entre dans le cache de navigation précédent/suivant, afin qu’un tour se terminant juste après la fermeture de l’onglet déclenche quand même une notification. Si activé, `pagehide` signale l’absence, et chaque signalement d’absence est envoyé avec `keepalive` pour survivre à la page. Si désactivé : aucun signalement `pagehide` et aucun `keepalive`. Un onglet qui se ferme devient tout de même masqué et signale l’absence, mais le navigateur peut annuler cette requête lors du déchargement de la page ; le bail reste alors actif jusqu’à son expiration après 90 s. Passez l’indicateur `notification_center` du projet. Toute modification s’applique immédiatement, sans nouveau signalement de présence. Nécessite `browserPresence`. |
| `repositoryMode` | — | Télémétrie uniquement. Une session créée avant le remplacement d’un dépôt démarre et utilise la configuration actuelle du projet sans cette option. Seul son clone `/workspace` provient encore de l’ancien dépôt ; un push depuis ce clone nécessite donc d’abord un rebase. |

L’envoi est optimiste. `send` affiche votre message immédiatement, puis les événements
du flux complètent la réponse de l’agent.

`rewind(messageId)` ne crée jamais de session. Il utilise la session canonique de
`POST /start`. L’environnement d’exécution restaure l’état des fichiers et conserve le parcours supprimé de la transcription
récupérable. Le prochain prompt accepté valide le parcours de remplacement.

<a id="other-stable-hooks"></a>

## Autres hooks stables

`@beeuni/sdk/react` exporte également des hooks React Query pour les données qui ne
nécessitent pas de session en cours d’exécution. Chacun correspond à une méthode du
[client](/docs/fr/sdk/reference) et ne nécessite aucun fournisseur.

| Hook | Données lues |
|---|---|
| `useProjectModels(projectId)` | Modèles sélectionnables pour le projet. |
| `useProjectModelPickerCatalog(projectId)` | L’enregistrement brut `/model-picker` par modèle au format de transport (`reasoning_options`, `temperature`, `limit`) et les estimations de prix client `managedPricingRoutes` pour chaque route gérée éligible. La route qui traite la requête détermine le montant final facturé en crédits. |
| `useProjectSessions(projectId, { parent, startedBy, q, limit })` | Les sessions d’un projet, paginées. `parent: 'root'` renvoie les sessions de premier niveau avec `child_count` ; `startedBy: 'me' \| 'others' \| 'automated'` filtre selon la personne qui a lancé l’exécution ; `q` recherche dans toutes les sessions que l’utilisateur peut ouvrir, pas seulement dans les pages chargées. Chaque ensemble de filtres possède sa propre entrée de cache. |
| `useSessionChildren(projectId, parentSessionId, { q, limit, enabled })` | Les sessions créées par un même parent (`parent=<id>`), de la plus récente à la plus ancienne, paginées. Définissez `enabled: false` jusqu’à l’ouverture du parent. |
| `useVisibleAgents({ projectId })` | Les agents visibles du projet. |
| `useProjectConfig(projectId)` | La configuration du runtime du projet : agent par défaut, commandes. |
| `useProjectSecrets(projectId)` | Secrets : liste, ajout, suppression et substitutions personnelles. |
| `useAccountSecretResources(accountId)` | Métadonnées des secrets de fournisseur du compte et autorisations d’utilisation. Les valeurs ne sont jamais renvoyées. |
| `useSessionProviderSecretPools(projectId, sessionId)` | Pools configurés, y compris les sélections vides ; `setPool` met à jour un fournisseur et actualise le cache des sessions. |
| `useProjectTriggers(projectId)` | Déclencheurs : liste, création, mise à jour, suppression, déclenchement. |
| `useProjectTriggerEventTypes(projectId, connector)` | Les événements d’application sur lesquels un connecteur peut se déclencher. Inactif tant que `connector` n’est pas défini. |
| `useChangeRequests(projectId, status?)` | Demandes de modification : liste, ouverture, fusion, fermeture, demande de changements. |
| `useRuntimeSupports(capability)` | Indique si le runtime de la session ouverte prend en charge une fonctionnalité (`session.rewind`, `session.compact`, …), d’après les réponses `/beeuni/health` lues par le mécanisme d’interrogation de reconnexion au runtime. `true` jusqu’à la première réponse. Voir [Ce que le runtime prend en charge](/docs/fr/sdk/sessions#what-the-runtime-supports). |
| `useNotificationInbox({ userId, limit?, enabled? })` | La page la plus récente de la boîte de réception de l’appelant (`limit` vaut 20 par défaut), `unreadCount`, ainsi que `markRead(ids)`, `markAllRead()`, `markSessionRead(sessionId)`. Interroge toutes les 60 s tant que la page est visible et recharge les données à la prise de focus. Une écriture modifie immédiatement le cache, le restaure en cas d’échec, puis recharge chaque entrée de la boîte de réception. Aucune requête sans `userId`. |
| `useNotificationPreferences({ userId, enabled? })` | Le choix de l’appelant pour les notifications push et par e-mail, par type, `email_available` et `update(patch)` (optimiste). |
| `useSessionWatch({ userId, projectId, sessionId, enabled? })` | `{ watching }` pour une session, et `setWatching(watching)` (optimiste) pour la mettre en sourdine ou la suivre. |
| `resetRuntimeQueries(queryClient)` | Ce n’est pas un hook. Supprime toutes les requêtes de runtime mises en cache (sessions, messages, agents, commandes, diffs). Appelez-le après le remplacement du runtime de la session par un redémarrage ou un rechargement de configuration, afin que la prochaine lecture provienne du nouveau runtime. |

Les trois hooks de notification utilisent `userId` (`qk.notifications`) comme clé de cache,
afin qu’un changement d’utilisateur dans le même onglet n’affiche jamais la boîte de réception de l’utilisateur précédent. Passez
l’identifiant de l’utilisateur connecté.

Les notifications sont contrôlées par le [feature flag](/docs/fr/feature-flags)
`notification_center` propre à chaque projet, désactivé par défaut. Conditionnez chaque hook avec
`enabled: useFeatureFlag(projectId, 'notification_center').enabled`, afin qu’un
projet où ce flag est désactivé n’envoie aucune requête de notification.

<a id="connector-setup-links"></a>

## Liens de configuration des connecteurs

Un agent peut générer un lien de connexion en un clic (`/connect/<token>`). Les données du parcours
et la politique du runtime résident dans le SDK ; l’hôte affiche sa propre boîte de dialogue à partir de ces éléments et
y injecte ses adaptateurs.

| Export | Rôle |
|---|---|
| `useConnectorSetup(token, options)` | Le parcours complet : charger ce que le lien désigne, `POST /start`, ouvrir la page hébergée du fournisseur via le `openPopup` injecté, interroger `POST /finalize` selon le calendrier de 3 s / 5 s / 5 minutes, et communiquer `phase` (`loading`, `ready`, `starting`, `opened`, `connected`, `error`), les informations de l’application, l’erreur et l’identité sous laquelle le compte a été autorisé. `options` contient le `backendUrl` de l’hôte, l’adaptateur `storage` et `openPopup` ; `onOpened` se déclenche après chaque ouverture de fenêtre contextuelle. |
| `useConnectorLinkInfo(token, options)` | Un cache partagé, adossé au stockage, par jeton de lien, afin que toutes les cartes et la fenêtre modale partagent une seule requête GET vers la route publique soumise à une limite de débit et affichent le logo de l’application dès la première image. Les mêmes options `backendUrl`/`storage`. |
| `createConnectorLinkInfoCache(fetchInfo, { storage })` | La fabrique de cache elle-même (sans framework, stockage injecté) pour un hôte qui souhaite sa propre instance. |
| `resolveConnectorStart({ start, finalize })` | La règle de résultat de `/start` : URL hébergée → fenêtre contextuelle, déjà connecté → finaliser une seule fois, sinon le message d’erreur. |
| `nextConnectorPollDelay(attempt, elapsedMs)` | Le calendrier d’interrogation borné (3 s au départ, puis 5 s, arrêt à 5 minutes). |

<a id="connector-data"></a>

## Données des connecteurs

`useConnectorQuery(projectId, slug, action, args, { account, enabled, staleTime })`
lit la sortie d’une action de connecteur sous forme de requête mise en cache. `error` est un
`ConnectorCallError` : affichez un bouton de connexion à partir de `error.connectUrl`, ou un
sélecteur de compte à partir de `error.availableAccounts`. Utilisez-le uniquement pour une action de
lecture ; chaque récupération exécute l’action et écrit un événement d’audit. La sortie reste
à jour pendant 30 s, la prise de focus ne recharge jamais les données, et seul un `429` ou `503` en amont fait
l’objet d’une nouvelle tentative. Voir [Connecteurs depuis le code](/docs/fr/sdk/reference#pconnectorslug--one-connector).

<a id="start-the-open-read-early"></a>

## Anticiper la lecture d’ouverture

`prefetchSessionOpen(queryClient, projectId, sessionId)` lance la lecture d’ouverture de session
avant le montage de la vue de session. Il émet une requête `GET .../snapshot`
(enregistrement de session, tour, file d’attente et les 40 premiers messages de la transcription) et initialise le
cache de l’enregistrement de session à partir de celle-ci. `useSession` utilise ensuite cette requête au lieu
d’en lancer une autre.

Appelez-le lorsqu’une route désigne une session, ou au survol, à la prise de focus ou au toucher d’un lien de
session. Il est en lecture seule : il n’appelle jamais `/start` et ne réveille jamais un ordinateur. Il
ne rejette jamais sa promesse. Un nouvel appel pour la même session dans les 30 secondes n’envoie aucune
requête.

```tsx
import { prefetchSessionOpen } from '@beeuni/sdk/react';

<a
  href={`/projects/${projectId}/sessions/${sessionId}`}
  onFocus={() => void prefetchSessionOpen(queryClient, projectId, sessionId)}
/>;
```

<a id="render-from-cache-on-a-reload"></a>

## Afficher depuis le cache lors d’un rechargement

Deux magasins conservent ce qu’un utilisateur a déjà vu, afin qu’un rechargement ou un démarrage à froid de l’application l’affiche
dès la première image et l’actualise sur place. Tous deux sont propres à chaque utilisateur et bornés, et
l’hôte les efface tous deux à la déconnexion.

- `createPersistedQueryCache` conserve les résultats React Query utilisés par l’utilisateur pour naviguer :
  comptes, projets, détail du projet, liste paginée des sessions et enregistrements de session
  (`isPersistableQueryKey`). Une entrée restaurée conserve son ancienneté d’origine, et ses données sont donc rechargées
  au montage. Les limites par défaut sont de 1 000 000 caractères et 7 jours.
- `createSavedCopyStore` conserve les dernières copies enregistrées des transcriptions envoyées par le
  serveur. Enregistrez-le avec `setSavedCopyStore`, et `useSession` affiche la copie conservée
  avant la première image. La copie du serveur est ensuite réconciliée avec elle par identifiant de message, et
  le SDK la relit 3 secondes après la fin de chaque tour. Les limites par défaut sont de 12
  sessions, 1 500 000 caractères et 14 jours. Une copie de plus de 300 000 caractères conserve
  ses messages les plus récents qui tiennent dans cette limite ; les messages plus anciens sont chargés depuis le serveur lorsque l’utilisateur
  fait défiler vers le haut.

```tsx
import {
  browserKeyValueStorage,
  createPersistedQueryCache,
  createSavedCopyStore,
  isPersistableQueryKey,
  setSavedCopyStore,
} from '@beeuni/sdk';

const storage = browserKeyValueStorage(); // null where there is no localStorage
if (storage) {
  const cache = createPersistedQueryCache({ storage, userId, shouldPersist: isPersistableQueryKey });
  cache.restore(queryClient); // synchronous with localStorage: no pending frame
  const stop = cache.persist(queryClient);
  setSavedCopyStore(createSavedCopyStore({ storage, userId }));
}
```

Sur React Native, passez AsyncStorage comme `storage`. `restore` renvoie alors une promesse.

Le magasin de copies enregistrées ne conserve que les captures du serveur : le miroir de la transcription que l’API
écrit à la fin d’un tour. La transcription en direct n’est jamais écrite sur l’appareil, afin qu’un
tour arrêté ne puisse pas réapparaître comme étant en cours. Une lecture du runtime qui ne renvoie aucun message
conserve les messages enregistrés à l’écran : cela signifie que l’ordinateur a perdu son état, pas que la
conversation est vide. La première lecture du runtime contenant des messages les réconcilie.

<a id="sign-out-and-account-switching"></a>

## Déconnexion et changement de compte

Le SDK conserve en mémoire l’état des sessions par utilisateur : transcriptions, questions
et autorisations en attente, reçus de tours et choix de modèles. Appelez `resetIdentityState()` à
chaque changement d’identité : déconnexion et connexion d’un autre utilisateur. Cela inclut
une connexion dans un autre onglet qui change l’utilisateur sans rechargement de page.

Appelez ce gestionnaire lorsque votre flux d’authentification connecte ou déconnecte un utilisateur.

```tsx
import { resetIdentityState } from '@beeuni/sdk/react';

let previousUserId: string | null = null;

function onIdentityChange(userId: string | null) {
  if (userId !== previousUserId) {
    queryClient.clear();
    resetIdentityState();
  }
  previousUserId = userId;
}
```

`resetIdentityState()` supprime également l’entrée `localStorage` du magasin de modèles. Il
ne vide pas les caches propres à l’hôte, comme React Query, ni la source de jetons
transmise à `createBeeuni`. Réinitialisez-les dans le même gestionnaire. Il ne lève jamais d’exception.

Avec les magasins de [Afficher depuis le cache lors d’un rechargement](#render-from-cache-on-a-reload),
videz-les dans le même gestionnaire : `await cache.clear()`, puis
`await savedCopies.clear()` et `setSavedCopyStore(null)`. Aucun des deux magasins ne
restaure de données pour un autre utilisateur, mais un appareil déconnecté ne doit pas les conserver.

<a id="next"></a>

## Suite

- [Sessions](/docs/fr/sdk/sessions) — le handle de session encapsulé par `useSession` et les types `BeeuniSendError`.
- [Référence](/docs/fr/sdk/reference) — l’ensemble de l’interface REST utilisée en lecture par ces hooks.

<a id="saved-history-during-startup"></a>

## Historique enregistré pendant le démarrage

`useSession(projectId, sessionId)` affiche l’historique de discussion enregistré pendant le démarrage de son ordinateur.
Le SDK lit la base de données indépendamment de la disponibilité de l’environnement d’exécution. Les messages en direct sont ensuite rapprochés
à l’aide de leurs identifiants d’origine. Les hôtes n’ont pas besoin d’une requête d’historique ni d’un magasin de messages distincts.

L’historique enregistré s’affiche exactement comme la transcription en direct : texte, raisonnement et chaque appel d’outil
avec son entrée, sa sortie et son erreur. La vue complète d’un sous-agent lit la transcription enregistrée propre à ce sous-agent : transmettez la portée du parent et `savedChild: true` à
`useSessionSync(subAgentSessionId, { beeuniSessionScope, savedChild: true })`. La fenêtre initiale contient les 40 messages les plus récents, dans
la limite de 1 000 000 de caractères. Faire défiler vers le haut charge des fenêtres plus anciennes de l’historique enregistré, même lorsque
l’ordinateur est éteint. Les pièces jointes qui nécessitent l’ordinateur deviennent disponibles lorsque
l’environnement d’exécution est prêt.

`savedTranscript` indique à l’hôte quoi afficher pendant le démarrage de l’ordinateur. Il vaut `loading` tant
qu’une copie enregistrée peut encore arriver, `shown` une fois que les messages sont dans `messages`, et `none` lorsque rien
ne peut s’afficher avant la réponse de l’environnement d’exécution. Un état inconnu vaut `loading`, jamais `none`.

`conversationEmpty` vaut true lorsqu’il n’y a rien à attendre. La copie enregistrée prouve que la
conversation est vide : une lecture complète de l’environnement d’exécution de la session n’a trouvé aucun message. Aucun tour ne s’est terminé
depuis, et rien n’est ouvert ni en file d’attente. Des enregistrements manquants ne le font jamais passer à true ; une ancienne session
avec un historique n’est donc jamais affichée comme vide. Une session obtient cette preuve à son premier démarrage avec l’historique
enregistré activé.

```tsx
if (s.messages.length === 0) {
  // Nothing saved and nothing to wait for: the user can type now.
  if (s.conversationEmpty) return <Composer />;
  // One round trip for a saved copy; the computer can take minutes.
  return s.savedTranscript === 'none' ? <Booting stage={s.stage} /> : <MessageSkeleton />;
}
```

Les appels d’outils enregistrés avant le 2026-09-28 ont perdu leurs entrées et sorties. Le serveur fournit chacun d’eux avec
l’entrée attestée par le titre ou les métadonnées conservés : la commande et la sortie d’une commande, le chemin d’un appel de fichier,
le motif d’une recherche, la description d’un sous-agent, l’URL d’une récupération, une liste de tâches, le nom d’une compétence. Au
prochain démarrage de la session, l’historique entier est à nouveau enregistré, à l’identique.
<a id="pending-prompts"></a>

### Prompts en attente

`useSessionPrompts(projectId, sessionId)` soumet les prompts à la boîte de réception persistante de la session.
Le serveur exécute automatiquement les prompts en attente, chacun dans son propre tour.

Transmettez `placement: 'transcript'` pour afficher un message en attente dans la conversation.
Il s’exécute avant chaque entrée `composer` et met fin à la réponse active après la
fin de l’appel d’outil en cours.
Transmettez `placement: 'composer'` pour le conserver dans la liste modifiable au-dessus du champ de saisie.
Il attend que la réponse active se termine. Chaque emplacement conserve l’ordre de soumission.
Transmettez `delivery: 'steer' | 'queue' | 'interrupt'` pour indiquer comment le prompt parvient à un
tour en cours. `steer` le transmet au tour à son étape suivante ; le tour ne
s’arrête pas. `queue` attend la fin du tour. `interrupt` met fin au tour après
l’outil en cours. Avec seulement `delivery`, le SDK envoie l’emplacement qu’il implique :
`interrupt` correspond à `transcript`, les autres à `composer`. Une ligne de réorientation qui ne peut pas
réorienter affiche `delivery: 'queue'` avec `steer_fallback` défini sur `unsupported`,
`not_prompter` ou `turn_ended`. `interrupt(promptId)` (« Stop and send ») transforme une
ligne en attente en entrée de transcription ; une ligne déjà en cours de transmission renvoie `409`.

L’éditeur web envoie avec Entrée sous la forme `steer` lorsqu’un tour est en cours, et comme un envoi dans la transcription
lorsque la session est inactive. Commande+Entrée (Ctrl+Entrée sous Windows/Linux)
utilise l’emplacement de l’éditeur. Maj+Entrée insère un saut de ligne.

Les lignes de la liste incluent `full_text` pour des aperçus complets après rechargement. `text` reste
l’aperçu limité destiné aux anciens clients. Les octets des pièces jointes sont exclus des réponses
de liste. La suppression renvoie le prompt complet pour permettre une annulation sans perte.

<a id="pending-delivery-and-active-responses"></a>

### Transmission en attente et réponses actives

`useSessionWorking()` conserve `state: 'working'` tant qu’un prompt attend dans la boîte de réception
persistante. Cela maintient Stop disponible. `pendingDelivery: true` signale un prompt encore en
cours de transmission vers l’environnement d’exécution ; l’application web affiche un indicateur d’activité dès que `state`
vaut `working`, quelle que soit la valeur de cet indicateur. Les tours actifs et l’activité récente de l’environnement
d’exécution l’effacent. Une annulation ayant expiré ne confirme pas l’arrêt de l’exécution.

La prise en charge par un worker vérifie uniquement l’admission et laisse le prompt en attente. La transmission commence
après une admission réussie. Un tour actif confirmé efface la présentation en attente,
même si l’instantané précédent de la boîte de réception répertorie encore ce prompt. L’activité de l’environnement d’exécution
préserve l’identifiant de message du tour actif pendant ce transfert.

L’éditeur web nomme Commande/Ctrl+Entrée **Queue List** et l’action « Stop and
send » d’une ligne **Quick Queue**. Les entrées Quick Queue s’exécutent en premier. Une entrée Quick Queue en attente est une bulle teintée après
les tours transmis, sans texte d’état. Les entrées Queue List restent modifiables jusqu’au début de la transmission.
Le serveur récupère les événements de fin manqués à partir de preuves exactes de l’environnement d’exécution, y compris
après rechargement, sans considérer un environnement d’exécution inaccessible comme un tour terminé.

Chaque pression distincte sur Entrée affiche immédiatement son prompt en attente, même si une demande
d’acceptation précédente reste sans réponse. Seul un échec de transmission ajoute du texte : sa cause,
Retry et Remove. La boîte de réception du serveur reste responsable de l’ordre d’exécution.

Le hook lit les modifications de la file d’attente depuis le flux de contrôle de la session
(`GET .../sessions/:id/events?channels=control`), avec une connexion par session,
quel que soit le nombre de composants qui le montent. Chaque écriture d’une ligne de la boîte de réception arrive sous forme de
trame `beeuni.control.queue`. L’interrogation périodique `GET .../prompts` ne s’exécute que lorsque ce
flux n’est pas connecté.

<a id="why-a-turn-ended"></a>

### Pourquoi un tour s’est terminé

La transcription d’un tour interrompu ne contient que `MessageAbortedError`. Le
plan de contrôle conserve ce qu’il sait de la fin de chaque tour : la cause indiquée par la
sandbox, par exemple `SandboxMemoryGuard`, et les tours qui se sont arrêtés sans
cause indiquée. Un arrêt demandé par quelqu’un — le bouton Stop, `abort()`, un prompt
envoyé dans une session occupée — est enregistré comme une demande et n’est jamais signalé comme un
échec.

`useSessionTurnOutcome(projectId, sessionId)` renvoie `last_ended`,
`recent_failures` et l’heure de lecture `atMs` depuis le cache que `useSessionWorking()`
maintient à jour. Il n’effectue aucune requête. Transmettez le résultat à `turnEndNotice`, avec ce que
la transcription indique sur le tour :

```tsx
const outcome = useSessionTurnOutcome(projectId, sessionId);
const notice = turnEndNotice(outcome, turn.userMessage.info.id, {
  hasError: Boolean(turnError),
  isAbort: turnErrorIsAbort,
});
// notice: { kind: 'sandbox-memory', usedPct: 97, detail: 'sandbox memory at 97% …' }
//       | { kind: 'cause', name: 'SomeGuard', message: '…' }
//       | { kind: 'unexplained' }
//       | null
```

L’hôte associe chaque `kind` à son propre texte et n’analyse jamais le
message de la sandbox. `null` signifie « afficher ce que la transcription affiche » : une véritable erreur reste affichée, une
interruption n’affiche rien. Un échec inexpliqué n’est signalé que lorsque la lecture
survient quelques secondes après la fin du tour, car la cause arrive souvent une trame
après l’interruption. `turnEndCause(outcome, messageId)` renvoie la cause brute
enregistrée, ou `null`.
