---
title: "Hooks do React"
description: "Execute uma sessão do Beeuni no React com o hook useSession."
---

`@beeuni/sdk/react` adiciona hooks do React ao SDK. Esta página aborda
`useSession`, o hook que executa uma sessão de ponta a ponta, e os outros hooks
confirmados como estáveis para apps React.

## useSession(projectId, sessionId, options?)

`useSession` inicia a sessão, abre o transporte de eventos selecionado pelo servidor e
sincroniza mensagens, status e solicitações pendentes. Chame-o uma vez por visualização de sessão.

```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} />
    </>
  );
}
```

O servidor determina se o ambiente está pronto. O ambiente de execução está pronto quando `POST /start` retorna
`stage: 'ready'`. `useSession` não executa uma verificação de integridade separada no cliente.

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

### Retorno

| Campo | Tipo | O que contém |
|---|---|---|
| `phase` | `'starting' \| 'ready' \| 'error'` | Estado geral. Renderize uma tela de inicialização até `ready`. |
| `messages` | `{ info, parts }[]` | A lista de mensagens. As partes chegam em tempo real. |
| `savedTranscript` | `'loading' \| 'shown' \| 'none'` | Se a conversa salva pode ser exibida antes de o computador despertar. `loading`: uma cópia salva está a caminho. `shown`: as mensagens estão na lista. `none`: nada pode ser exibido até o ambiente de execução responder. |
| `conversationEmpty` | `boolean` | A cópia salva comprova que a conversa está vazia, nenhum turno terminou desde então e nada está aberto ou na fila. Renderize o campo de composição, não uma tela de inicialização. |
| `status` | `SessionStatus` | O status da sessão. |
| `isBusy` | `boolean` | O agente está gerando uma resposta. |
| `questions`, `permissions` | array | Perguntas pendentes do agente e solicitações de aprovação de ferramentas. Um `permission` aqui é uma aprovação de ferramenta do ambiente de execução, não uma permissão IAM. |
| `diffs`, `todos` | array | Diffs de arquivos e itens de tarefas em tempo real. |
| `sendError` | `BeeuniSendError \| null` | A última falha de `send`: `billing`, `runtime-not-ready` ou `runtime-error`. |
| `rewindMessageId` | `string \| null` | A mensagem do usuário selecionada enquanto um retrocesso reversível está preparado. |
| `rewindPending` | `boolean` | Uma solicitação de retrocesso ou restauração está em andamento. |
| `rewindError` | `BeeuniSendError \| null` | A última falha de retrocesso ou restauração. |
| `models`, `agents`, `defaultAgent`, `commands` | — | Modelos selecionáveis, agentes selecionáveis, o agente padrão e comandos de barra. Disponíveis antes de o ambiente de execução iniciar. |
| `retry` | `() => void` | Força uma nova verificação de `/start`. |

<a id="actions"></a>

### Ações

| Ação | O que faz |
|---|---|
| `send(text, override?)` | Envia um prompt. `override` define `{ model?, agent? }` apenas para esta mensagem. |
| `sendParts(parts, override?)` | Envia texto e partes de arquivos por URL do ambiente de execução pelo transporte selecionado. Identificadores privados preparados usam as rotas de prompt da plataforma. |
| `rewind(messageId)` | Retrocede esta sessão canônica até uma mensagem do usuário. A mensagem selecionada e o caminho posterior ficam ocultos e podem ser recuperados. |
| `restoreRewind()` | Restaura o caminho removido antes que outro prompt confirme sua substituição. |
| `cancel()` | Interrompe a execução atual e limpa perguntas e permissões pendentes. Primeiro retém os prompts na fila da sessão, para que um prompt na fila não inicie uma nova execução. |
| `runCommand(command, args)` | Executa um comando de barra do projeto. |
| `answerQuestion(id, answers)` | Responde a uma pergunta pendente do agente. |
| `rejectQuestion(id)` | Rejeita uma pergunta pendente do agente. |
| `answerPermission(id, reply, message?)` | Responde a uma solicitação de aprovação de ferramenta. `reply` é `'once'`, `'always'` ou `'reject'`. |

`useSession` também retorna `removeQuestion` e `removePermission`. Não os
use. Eles removem a solicitação do estado local, mas nunca notificam o agente, então
a execução continua bloqueada. Use `answerQuestion`, `rejectQuestion` ou
`answerPermission` em vez disso.

<a id="options"></a>

### Opções

| Opção | Padrão | O que faz |
|---|---|---|
| `waitMs` | `15000` | O limite de tempo de long polling enviado a `/start`. |
| `replayStartStash` | `true` | Reenvia um prompt salvo antes de a sessão existir, assim que a sessão estiver pronta. |
| `enabled` | `true` | Defina `false` para adiar o hook, por exemplo, até que uma verificação de faturamento seja aprovada. |
| `chatEngine` | `true` | Defina `false` se seu app montar sua própria interface de chat para esta sessão, para evitar sincronizar mensagens duas vezes. |
| `initialRuntimeSessionId` | `null` | Uma referência fixa à sessão do ambiente de execução que seu servidor já mantém para esta sessão. A transcrição é renderizada a partir dela enquanto `/start` é executado; a resposta de `/start` continua sendo a fonte de verdade. `initialOpenCodeSessionId` é seu nome obsoleto. |
| `browserPresence` | `false` | Mantém a concessão de presença desta aba enquanto uma pessoa conectada usa a página (entrada nos últimos 10 minutos). A concessão mantém o computador da sessão ativo. |
| `presenceAlerts` | `false` | Esta aba exibe suas próprias notificações. Enviado na concessão de presença: o servidor deixa de enviar ao telefone e via Web Push apenas enquanto uma aba que emite alertas está em uso. Uma alteração é enviada imediatamente. Passe `true` apenas quando a flag `notification_center` do projeto estiver ativada; caso contrário, o servidor a ignora. Requer `browserPresence`. |
| `presencePageExit` | `false` | Encerra a concessão de presença quando a página fecha ou entra no cache de voltar/avançar, para que um turno que termine logo após o fechamento da aba ainda gere uma notificação. Ativada: `pagehide` informa ausência, e cada informe de ausência é enviado com `keepalive`, para sobreviver à página. Desativada: sem informe de `pagehide` e sem `keepalive`. Uma aba sendo fechada ainda fica oculta e informa ausência, mas o navegador pode cancelar essa solicitação ao descarregar a página; nesse caso, a concessão permanece até expirar em 90 s. Passe a flag `notification_center` do projeto. Uma alteração é aplicada imediatamente, sem um novo informe de presença. Requer `browserPresence`. |
| `repositoryMode` | — | Apenas telemetria. Uma sessão criada antes da substituição de um repositório inicia e executa a configuração atual do projeto sem ela. Apenas seu clone `/workspace` ainda vem do repositório antigo, então um push a partir dele exige um rebase primeiro. |

O envio é otimista. `send` mostra sua mensagem imediatamente, e os eventos
do fluxo preenchem a resposta do agente.

`rewind(messageId)` nunca cria uma sessão. Ele usa a sessão canônica de
`POST /start`. O ambiente de execução restaura o estado dos arquivos e mantém o caminho removido da transcrição
recuperável. O próximo prompt aceito confirma o caminho substituto.

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

## Outros hooks estáveis

`@beeuni/sdk/react` também exporta hooks do React Query para dados que não
precisam de uma sessão em execução. Cada um corresponde a um método no
[cliente](/docs/pt/sdk/reference) e não precisa de provider.

| Hook | Lê |
|---|---|
| `useProjectModels(projectId)` | Modelos selecionáveis para o projeto. |
| `useProjectModelPickerCatalog(projectId)` | O registro bruto `/model-picker` por modelo no protocolo (`reasoning_options`, `temperature`, `limit`) e estimativas de preço para o cliente `managedPricingRoutes` para cada rota gerenciada elegível. A rota que atende à solicitação determina a cobrança final de créditos. |
| `useProjectSessions(projectId, { parent, startedBy, q, limit })` | As sessões de um projeto, paginadas. `parent: 'root'` retorna sessões de nível superior com `child_count`; `startedBy: 'me' \| 'others' \| 'automated'` filtra por quem iniciou a execução; `q` pesquisa todas as sessões que o usuário pode abrir, não apenas as páginas carregadas. Cada conjunto de filtros tem sua própria entrada de cache. |
| `useSessionChildren(projectId, parentSessionId, { q, limit, enabled })` | As sessões criadas por uma sessão pai (`parent=<id>`), das mais recentes para as mais antigas, paginadas. Defina `enabled: false` até que a sessão pai seja aberta. |
| `useVisibleAgents({ projectId })` | Os agentes visíveis do projeto. |
| `useProjectConfig(projectId)` | A configuração do runtime do projeto: agente padrão, comandos. |
| `useProjectSecrets(projectId)` | Segredos: listar, adicionar, remover e substituições pessoais. |
| `useAccountSecretResources(accountId)` | Metadados dos segredos de provedores da conta e concessões de uso. Os valores nunca são retornados. |
| `useSessionProviderSecretPools(projectId, sessionId)` | Pools configurados, incluindo seleções vazias; `setPool` atualiza um provedor e atualiza o cache da sessão. |
| `useProjectTriggers(projectId)` | Gatilhos: listar, criar, atualizar, remover, disparar. |
| `useProjectTriggerEventTypes(projectId, connector)` | Os eventos do app que podem acionar um conector. Fica inativo até que `connector` seja definido. |
| `useChangeRequests(projectId, status?)` | Solicitações de alteração: listar, abrir, mesclar, fechar, solicitar alterações. |
| `useRuntimeSupports(capability)` | Se o runtime da sessão aberta oferece um recurso (`session.rewind`, `session.compact`, …), com base nas respostas de `/beeuni/health` lidas pelo mecanismo de consulta periódica de reconexão do runtime. `true` até a primeira resposta. Veja [O que o runtime oferece](/docs/pt/sdk/sessions#what-the-runtime-supports). |
| `useNotificationInbox({ userId, limit?, enabled? })` | A página mais recente da caixa de entrada de quem faz a chamada (`limit` com padrão 20), `unreadCount` e `markRead(ids)`, `markAllRead()`, `markSessionRead(sessionId)`. Consulta a cada 60 s enquanto a página está visível e busca novamente ao receber foco. Uma gravação altera o cache imediatamente, restaura-o em caso de falha e depois busca novamente cada entrada da caixa de entrada. Nenhuma solicitação sem `userId`. |
| `useNotificationPreferences({ userId, enabled? })` | A escolha de push e e-mail de quem faz a chamada por tipo, `email_available` e `update(patch)` (otimista). |
| `useSessionWatch({ userId, projectId, sessionId, enabled? })` | `{ watching }` para uma sessão e `setWatching(watching)` (otimista) para silenciá-la ou acompanhá-la. |
| `resetRuntimeQueries(queryClient)` | Não é um hook. Descarta todas as consultas de runtime em cache (sessões, mensagens, agentes, comandos, diffs). Chame-o depois que o runtime da sessão for substituído por uma reinicialização ou recarga da configuração, para que a próxima leitura venha do novo runtime. |

Os três hooks de notificação usam `userId` (`qk.notifications`) como chave de cache,
para que uma troca de usuário na mesma aba nunca mostre a caixa de entrada do usuário anterior. Passe
o ID do usuário conectado.

As notificações dependem da `notification_center`
[flag de recurso](/docs/pt/feature-flags) por projeto, desativada por padrão. Condicione cada hook a
`enabled: useFeatureFlag(projectId, 'notification_center').enabled`, para que um
projeto com a flag desativada não envie solicitações de notificação.

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

## Links de configuração de conectores

Um agente pode gerar um link de conexão com 1 clique (`/connect/<token>`). Os dados do fluxo
e a política do runtime ficam no SDK; o host renderiza seu próprio diálogo sobre eles e
injeta seus adaptadores.

| Exportação | Função |
|---|---|
| `useConnectorSetup(token, options)` | O fluxo completo: carregar o que o link identifica, `POST /start`, abrir a página hospedada do provedor por meio de `openPopup` injetado, consultar `POST /finalize` no cronograma de 3 s / 5 s / 5 minutos e informar `phase` (`loading`, `ready`, `starting`, `opened`, `connected`, `error`), as informações do app, o erro e a identidade com que a conta foi autorizada. `options` contém `backendUrl`, o adaptador `storage` e `openPopup` do host; `onOpened` é acionado após cada abertura de popup. |
| `useConnectorLinkInfo(token, options)` | Um cache compartilhado, apoiado em armazenamento, por token de link, para que todos os cards e o modal compartilhem um único GET da rota pública com limite de requisições e exibam o logo do app no primeiro quadro. As mesmas opções `backendUrl`/`storage`. |
| `createConnectorLinkInfoCache(fetchInfo, { storage })` | A própria fábrica de cache (sem framework, com armazenamento injetado) para um host que queira sua própria instância. |
| `resolveConnectorStart({ start, finalize })` | A regra de resultado de `/start`: URL hospedada → popup, já conectado → finalizar uma vez, caso contrário, a mensagem de erro. |
| `nextConnectorPollDelay(attempt, elapsedMs)` | O cronograma limitado de consultas periódicas (3 s primeiro, 5 s depois, para aos 5 minutos). |

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

## Dados de conectores

`useConnectorQuery(projectId, slug, action, args, { account, enabled, staleTime })`
lê a saída de uma ação de conector como uma consulta em cache. `error` é um
`ConnectorCallError`: renderize um botão de conexão a partir de `error.connectUrl` ou um
seletor de contas a partir de `error.availableAccounts`. Use-o apenas para uma ação que
faça leitura; cada busca executa a ação e grava um evento de auditoria. A saída permanece
atualizada por 30 s, o foco nunca causa uma nova busca e apenas um `429` ou `503` de upstream gera
uma nova tentativa. Veja [Conectores por código](/docs/pt/sdk/reference#pconnectorslug--one-connector).

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

## Inicie a leitura de abertura antecipadamente

`prefetchSessionOpen(queryClient, projectId, sessionId)` inicia a leitura de abertura da sessão
antes que a visualização da sessão seja montada. Ele emite uma solicitação `GET .../snapshot`
(registro da sessão, turno, fila e as primeiras 40 mensagens da transcrição) e preenche o
cache do registro da sessão com ela. `useSession` então usa essa solicitação em vez de
iniciar a sua própria.

Chame-o quando uma rota identificar uma sessão ou ao passar o cursor, dar foco ou tocar em um
link de sessão. É somente leitura: nunca chama `/start` e nunca desperta um computador. Ele
nunca rejeita. Uma chamada repetida para a mesma sessão em até 30 segundos não envia
solicitação.

```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>

## Renderize a partir do cache ao recarregar

Dois armazenamentos mantêm o que o usuário já viu, para que uma recarga ou uma inicialização a frio do app renderize esse conteúdo
no primeiro quadro e o atualize no lugar. Ambos são por usuário e limitados, e o
host limpa ambos ao sair da conta.

- `createPersistedQueryCache` mantém os resultados do React Query usados pelo usuário para navegar:
  contas, projetos, detalhes do projeto, a lista paginada de sessões e registros de sessões
  (`isPersistableQueryKey`). Uma entrada restaurada mantém sua idade original, portanto é buscada novamente
  na montagem. Os limites padrão são 1.000.000 de caracteres e 7 dias.
- `createSavedCopyStore` mantém as últimas cópias salvas das transcrições enviadas pelo
  servidor. Registre-o com `setSavedCopyStore`, e `useSession` exibe a cópia mantida
  antes do primeiro quadro. A cópia do servidor é então reconciliada com ela pelo ID da mensagem, e
  o SDK a lê novamente 3 segundos após o fim de cada turno. Os limites padrão são 12
  sessões, 1.500.000 de caracteres e 14 dias. Uma cópia com mais de 300.000 caracteres mantém
  as mensagens mais recentes que couberem; as mensagens mais antigas são carregadas do servidor quando o usuário
  rola para cima.

```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 }));
}
```

No React Native, passe AsyncStorage como `storage`. `restore` então retorna uma promise.

O armazenamento de cópias salvas mantém apenas capturas do servidor: o espelho da transcrição que a API
grava quando um turno termina. A transcrição ao vivo nunca é gravada no dispositivo, para que um
turno interrompido não volte como um turno em execução. Uma leitura do runtime que não retorna mensagens
mantém as mensagens salvas na tela: isso significa que o computador perdeu seu estado, não que a
conversa está vazia. A primeira leitura do runtime com mensagens as reconcilia.

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

## Saída da conta e troca de contas

O SDK mantém em memória o estado das sessões por usuário: transcrições, perguntas
e permissões pendentes, recibos de turnos e escolhas de modelos. Chame `resetIdentityState()` a
cada mudança de identidade: saída da conta e entrada de um usuário diferente. Isso inclui
uma entrada em outra aba que altere o usuário sem carregar a página.

Chame este manipulador quando seu fluxo de autenticação conectar ou desconectar um usuário.

```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()` também remove a entrada `localStorage` do armazenamento de modelos. Ele
não limpa os caches do próprio host, como o React Query, nem a fonte de tokens
passada para `createBeeuni`. Redefina-os no mesmo handler. Ele nunca lança exceções.

Com os armazenamentos de [Renderizar a partir do cache ao recarregar](#render-from-cache-on-a-reload),
limpe-os no mesmo handler: `await cache.clear()`, depois
`await savedCopies.clear()` e `setSavedCopyStore(null)`. Nenhum dos armazenamentos jamais
restaura dados de outro usuário, mas um dispositivo desconectado da conta não deve mantê-los.

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

## A seguir

- [Sessões](/docs/pt/sdk/sessions) — o identificador de sessão encapsulado por `useSession` e os tipos de `BeeuniSendError`.
- [Referência](/docs/pt/sdk/reference) — toda a interface REST que esses hooks consultam.


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

## Histórico salvo durante a inicialização

`useSession(projectId, sessionId)` mostra o histórico de chat salvo enquanto seu computador inicia.
O SDK lê o banco de dados independentemente de o runtime estar pronto. As mensagens ao vivo são então reconciliadas
pelos seus IDs originais. Os hosts não precisam de uma solicitação separada de histórico nem de um armazenamento de mensagens.

O histórico salvo é renderizado exatamente como a transcrição ao vivo: texto, raciocínio e cada chamada de ferramenta
com sua entrada, saída e erro. A visualização completa de um subagente lê a própria
transcrição salva do subagente: passe o escopo do agente pai e `savedChild: true` para
`useSessionSync(subAgentSessionId, { beeuniSessionScope, savedChild: true })`. A janela inicial contém as 40 mensagens mais recentes, com
no máximo 1.000.000 de caracteres. Rolar para cima carrega janelas mais antigas do histórico salvo, mesmo enquanto
o computador está desligado. Anexos que exigem o computador ficam disponíveis quando o
runtime está pronto.

`savedTranscript` informa ao host o que renderizar enquanto o computador inicia. Seu valor é `loading` enquanto
uma cópia salva ainda pode chegar, `shown` quando há mensagens em `messages` e `none` quando nada
pode ser exibido antes de o runtime responder. Um estado desconhecido é `loading`, nunca `none`.

`conversationEmpty` é true quando não há nada a aguardar. A cópia salva comprova que a
conversa está vazia: uma leitura completa do runtime da sessão não encontrou mensagens. Nenhum turno terminou
desde então, e nada está aberto ou na fila. Registros ausentes nunca tornam esse valor true, portanto uma sessão antiga
com histórico nunca é exibida como vazia. Uma sessão obtém essa comprovação na primeira inicialização com o histórico
salvo ativado.

```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 />;
}
```

Chamadas de ferramentas salvas antes de 2026-09-28 perderam suas entradas e saídas. O servidor fornece cada uma com
a entrada comprovada pelo título ou pelos metadados preservados: o comando e a saída de um comando, o caminho de uma chamada de arquivo,
o padrão de uma busca, a descrição de um subagente, a URL de uma busca de conteúdo, uma lista de tarefas, o nome de uma skill. A
próxima inicialização da sessão salva todo o histórico novamente, 1:1.
<a id="pending-prompts"></a>

### Prompts pendentes

`useSessionPrompts(projectId, sessionId)` envia para a caixa de entrada persistente da sessão.
O servidor executa os prompts pendentes automaticamente, cada um em seu próprio turno.

Passe `placement: 'transcript'` para exibir uma mensagem pendente na conversa.
Ela é executada antes de cada entrada `composer` e encerra a resposta ativa depois que a
chamada de ferramenta atual termina.
Passe `placement: 'composer'` para mantê-la na lista editável acima do campo de entrada.
Ela aguarda a resposta ativa terminar. Cada posicionamento mantém a ordem de envio.
Passe `delivery: 'steer' | 'queue' | 'interrupt'` para indicar como o prompt chega a um
turno em execução. `steer` o entrega ao turno na próxima etapa; o turno não
para. `queue` aguarda o turno terminar. `interrupt` encerra o turno após a
ferramenta em execução. Com apenas `delivery`, o SDK envia o posicionamento implícito:
`interrupt` é `transcript`, os demais são `composer`. Uma linha de direcionamento que não consegue
direcionar apresenta `delivery: 'queue'` com `steer_fallback` definido como `unsupported`,
`not_prompter` ou `turn_ended`. `interrupt(promptId)` ("Stop and send") transforma uma
linha em espera em uma entrada da transcrição; uma linha já em transmissão responde `409`.

O editor web envia Enter como `steer` enquanto um turno está em execução e como um envio para a transcrição
quando a sessão está ociosa. Command+Enter (Control+Enter no Windows/Linux)
usa o posicionamento no editor. Shift+Enter insere uma quebra de linha.

As linhas da lista incluem `full_text` para prévias completas após recarregar. `text` continua sendo
a prévia limitada para clientes antigos. Os bytes dos anexos são excluídos das respostas
de listagem. A exclusão retorna o prompt completo para permitir desfazer sem perdas.

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

### Entrega pendente e respostas ativas

`useSessionWorking()` mantém `state: 'working'` enquanto um prompt aguarda na caixa de entrada
persistente. Isso mantém Stop disponível. `pendingDelivery: true` marca um prompt ainda a
caminho do runtime; o app web mostra um indicador de atividade sempre que `state`
é `working`, independentemente do que esse sinalizador indique. Turnos ativos do runtime e atividade recente do runtime
limpam esse estado. Um cancelamento que excedeu o tempo limite não confirma que a execução parou.

A reivindicação por um worker apenas verifica a admissão e mantém o prompt em espera. A entrega começa
após a admissão ser bem-sucedida. Um turno ativo confirmado limpa a apresentação de pendência
mesmo que o snapshot anterior da caixa de entrada ainda liste esse prompt. A atividade do runtime
preserva o ID da mensagem do turno ativo durante essa transição.

O editor web chama Command/Ctrl+Enter de **Queue List** e "Stop and
send" de uma linha de **Quick Queue**. As entradas de Quick Queue são executadas primeiro. Uma entrada de Quick Queue em espera é um balão colorido após
os turnos entregues, sem texto de status. As entradas de Queue List permanecem editáveis até o início da entrega.
O servidor recupera eventos de conclusão perdidos a partir de evidências exatas do runtime, inclusive
após recarregar, sem tratar um runtime inacessível como um turno concluído.

Cada Enter distinto exibe seu prompt pendente imediatamente, mesmo enquanto uma solicitação anterior
de aceitação ainda não foi resolvida. Apenas uma falha de entrega adiciona texto: sua causa,
Retry e Remove. A caixa de entrada do servidor continua controlando a ordem de execução.

O hook lê as alterações da fila pelo fluxo de controle da sessão
(`GET .../sessions/:id/events?channels=control`), com uma conexão por sessão,
independentemente de quantos componentes o montem. Cada gravação de uma linha da caixa de entrada chega como um
frame `beeuni.control.queue`. A consulta periódica `GET .../prompts` só é executada enquanto esse
fluxo não está conectado.

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

### Por que um turno terminou

A transcrição de um turno interrompido contém apenas `MessageAbortedError`. O
plano de controle mantém o que sabe sobre como cada turno terminou: a causa indicada pelo
sandbox, por exemplo `SandboxMemoryGuard`, e quais turnos terminaram sem
uma causa indicada. Uma parada solicitada por alguém — o botão Stop, `abort()`, um prompt
enviado para uma sessão ocupada — é registrada como uma solicitação e nunca é relatada como
falha.

`useSessionTurnOutcome(projectId, sessionId)` retorna `last_ended`,
`recent_failures` e o horário da leitura `atMs` do cache que `useSessionWorking()`
mantém atualizado. Não faz nenhuma solicitação. Passe o resultado para `turnEndNotice`, junto com o que
a transcrição informa sobre o turno:

```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
```

O host mapeia cada `kind` para seu próprio texto e nunca interpreta a mensagem do
sandbox. `null` significa "mostrar o que a transcrição mostra": um erro real permanece, um
cancelamento não renderiza nada. Uma falha sem explicação só é relatada quando a leitura
ocorre alguns segundos após o fim do turno, porque a causa costuma chegar um frame
depois do cancelamento. `turnEndCause(outcome, messageId)` retorna a causa
registrada bruta ou `null`.
