DisPatch: um Aplicativo de Chat Self-Hosted para Agentes de IA Locais

Publicado em
11 julho 2026
Atualizado em
11 julho 2026
Por
Jacob Lloyd — escrito com ajuda de IA, depois do projeto
Tempo de leitura
13 min de leitura

Em termos simples: O DisPatch é três coisas em uma, hospedadas inteiramente no seu próprio computador: um aplicativo de chat para conversar com assistentes de IA que também vivem ali, um servidor de arquivos e um terminal — disponíveis em todos os seus dispositivos, o tempo todo, sem nada indo para um serviço na nuvem. O servidor de arquivos move arquivos de qualquer dispositivo para o computador onde os assistentes rodam, para que eles possam trabalhar com o que você enviou. Em dispositivos compartilhados, um subconjunto seguro de assistentes fica disponível sem PIN; digitar o PIN libera os de poder total.

O DisPatch é o aplicativo que criei para parar de rodar uma instalação do Rocket.Chat com 30 containers só para trocar mensagens com meus próprios agentes de IA. Ele acabou virando três coisas em uma — um aplicativo de chat de LLM hospedado localmente, um servidor de arquivos e um terminal — disponível em todos os dispositivos que eu tenho, o tempo todo. É um zip de ~720KB, é gratuito, e é o único front-end de chat que eu uso hoje.

Resumo

  • O que é: Três coisas em uma, self-hosted: um chat web para um time de agentes de IA locais (um gateway OpenClaw), um servidor de arquivos com arrastar e soltar, e um terminal de verdade — em todos os dispositivos da sua rede local ou da sua rede Tailscale, o tempo todo.
  • O que custa: $0. Sem assinatura de SaaS, sem conta.
  • O que você precisa: Linux, Python 3.12+ e uv. Um gateway OpenClaw se você quiser respostas digitadas — todo o resto funciona sem ele.
  • O que você ganha: threads por bot, markdown em streaming, um Servidor de Arquivos com arrastar e soltar, um terminal de agente de codificação embutido no app, um "Modo Seguro" bloqueável por PIN, uma API de push para tarefas cron, e 187 testes que passam direto de dentro do zip.

O que você ganha

Antes de qualquer papo sobre configuração, é assim que ele se parece fazendo seu trabalho. As capturas de tela são de uma cópia em sandbox com dados fictícios — ninguém precisa ver os logs reais dos meus bots.

Layout do DisPatch no desktop em 1440px: uma barra de bots com avatares coloridos em bloco de letras à esquerda, uma lista de threads, e um painel de chat mostrando um resumo matinal em markdown com um bloco de código com destaque de sintaxe e um selo de modelo.
Desktop, 1440px: barra de bots, lista de threads, painel de chat. O resumo e o bloco de código são markdown renderizado, não capturas de tela coladas.
Layout do DisPatch no celular em 390px: a mesma conversa em um único painel, com uma barra de abas inferior para Bots, Chats e Messages, e ações de Copiar, Regenerar e Excluir em cada mensagem.
A mesma conversa em 390px. A barra de bots se recolhe em uma barra de abas inferior; cada mensagem ganha Copiar / Regenerar / Excluir.
DisPatch em Modo Seguro depois de definir um PIN: a barra de bots fica reduzida a três bots aprovados, com um botão de cadeado âmbar na parte de baixo para desbloquear o restante.
Modo Seguro com PIN definido: só os bots marcados como "safe" aparecem, com os mesmos avatares do lado desbloqueado. Não é um filtro de CSS — o servidor simplesmente nunca envia o restante.

O que ele realmente faz

  • Threads por bot, em todo lugar. Abra no celular no meio de uma conversa e é a mesma thread, o mesmo histórico, sem dança de relogin.
  • Um Servidor de Arquivos — arraste e solte arquivos entre qualquer dispositivo e a máquina. O recurso que mais uso; ele ganha uma seção própria mais abaixo.
  • Um terminal (Terminex) — um shell de verdade no navegador rodando um agente de codificação. Também tem sua própria seção mais abaixo.
  • Respostas em markdown com streaming, com código com destaque de sintaxe (botão de copiar incluído), imagens/vídeos com lightbox de zoom, instalável como PWA.
  • Um painel "trabalhando" ao vivo. Clique nos pontinhos de digitação para ver, em tempo real, as chamadas de ferramentas e o raciocínio do modelo — ele acompanha a transcrição do agente ao vivo, não é um spinner de mentirinha.
  • Bloqueio em duas camadas. Um PIN separa os agentes de poder total de um subconjunto seguro, só de informação geral — detalhes na seção de bloqueio em duas camadas mais abaixo.
  • Uma API de pushPOST /api/inject — para que uma tarefa cron ou outro agente possa colocar uma mensagem (ou uma imagem) em uma thread. É assim que um resumo diário chega no seu celular sem você pedir.
  • Geração de imagem local dentro da thread. Peça uma imagem a um agente e ele pode chamar uma instância local do ComfyUI; o resultado chega no chat como qualquer outra mídia, com lightbox e tudo. Sem API de imagem, sem upload para ninguém.
  • Opcional, desligado por padrão: um painel de ComfyUI para iniciar ou parar esse serviço de geração de imagem direto do chat.

O Servidor de Arquivos

De tudo que tem no DisPatch, o Servidor de Arquivos é a peça que mais uso. É uma área de arrastar e soltar arquivos, servida pela mesma máquina: abra o DisPatch em qualquer dispositivo, solte um arquivo nele, e o arquivo já está no servidor — agrupado por dia, com miniaturas para imagens e vídeos.

Visão do Servidor de Arquivos do DisPatch: uma área de soltar arquivos para upload no topo, seguida de arquivos agrupados por dia, com miniaturas de imagem e cartões por tipo de arquivo.
O Servidor de Arquivos: área de soltar arquivos no topo, uploads agrupados por dia abaixo. Pelo celular, esse é o fluxo de trabalho inteiro.

O que torna isso mais do que uma conveniência é onde os arquivos pousam: na mesma máquina onde os agentes vivem. Uma foto enviada do meu celular pode ser lida no disco por qualquer agente segundos depois — descrita, arquivada, colocada em um site, alimentada nas ferramentas de imagem. É a ponte entre "um arquivo em qualquer dispositivo que eu esteja segurando" e "um arquivo com o qual meus agentes podem trabalhar", sem nenhum drive na nuvem no meio e sem cabos.

As partes menos glamorosas estão cobertas: os uploads têm o tamanho verificado antes de serem colocados em buffer (um celular não consegue estourar a memória da máquina), existe um limite geral de armazenamento, as gravações são atômicas para que um upload interrompido nunca deixe um arquivo pela metade, e o armazenamento de arquivos é espelhado no mesmo esquema de backup rotativo do histórico de chat. Uma flag de configuração também pode transformar tudo isso em uma caixa de entrada só de upload, de mão única — os dispositivos podem adicionar arquivos, mas não navegar por eles — para configurações em que mais gente tem acesso ao app do que deveria poder ler o arquivo completo.

Terminex: um terminal dentro do chat

O outro recurso que se paga todo santo dia: o DisPatch consegue colocar um terminal de verdade no navegador. Ele aparece na barra de bots como qualquer outro bot, mas abri-lo te conecta a um PTY do lado do servidor, transmitido por WebSocket para dentro do xterm.js — uma sessão de shell de verdade na máquina, não uma simulação.

Visão do terminal do DisPatch: um painel de terminal xterm.js dentro do aplicativo de chat, com controles de Iniciar, Reiniciar e Parar, rodando uma CLI de agente de codificação.
Terminex: um PTY do lado do servidor dentro do aplicativo de chat, com controles de Iniciar / Reiniciar / Parar.

O meu abre direto no Reasonix, o agente de codificação no estilo Claude Code que rodo no DeepSeek — a configuração que a página central de assistentes chama de Terminex. Isso transforma uma aba de chat em um console completo de agente de codificação: entregue uma tarefa a ele, observe-o trabalhar, a partir de qualquer dispositivo que consiga abrir o aplicativo. Junto com o Servidor de Arquivos, dá para operar a máquina inteira a partir de um celular — solte um arquivo de onde você estiver, abra o terminal, diga ao agente o que fazer com ele.

  • Só no modo desbloqueado. O terminal não existe no Modo Seguro, e fica completamente desligado a menos que você o habilite (LOCAL_CHAT_TERMINAL).
  • A sessão fica só na memória. Iniciar / Reiniciar / Parar pelo app; reiniciar o servidor encerra a sessão. Não há nenhum shell persistente escondido em segundo plano.

Por que ele existe

Ele substituiu uma implantação completa do Rocket.Chat — um banco de dados e uma pilha de containers só para uma pessoa conversar com alguns modelos locais. Funcionava, mas é muita infraestrutura para "mandar texto, receber texto de volta, sincronizar em todo lugar".

E a resposta mais profunda para "por que sequer hospedar um app de chat em casa": uma vez que seus modelos rodam localmente, a interface de chat é a última peça ainda te tentando na direção de um serviço na nuvem. Hospedá-la você mesmo significa que seus prompts, a saída dos seus agentes e as mensagens da sua família nunca saem de casa, e o ciclo inteiro — interface, gateway, modelos, geração de imagem — continua funcionando com a internet desligada. Se você já se comprometeu com LLMs locais, um front-end de chat hospedado é o único elemento que destoa.

Uma escolha de design que vale destacar: o DisPatch não tem etapa de build, de propósito. O frontend é JavaScript puro (módulos ES) e CSS/HTML simples. Sem bundler, sem npm run build. Meu requisito real desde o início era "fácil para um agente de IA modificar depois". Editar um arquivo JS, atualizar a página, pronto. Eu tomaria a mesma decisão de novo.

A stack

CamadaO que é
BackendPython 3.12+, FastAPI + uvicorn, aiosqlite (um único arquivo SQLite, modo WAL), Pillow, gerenciado com uv
Tamanho do backend~7.200 linhas em backend/app/ (só o main.py já tem ~3.600), mais ~2.700 linhas de testes
FrontendJS puro sem build + HTML/CSS — ~4.900 linhas de JS, ~2.100 de CSS/HTML/service worker
Bibliotecas embutidasmarked, highlight.js, DOMPurify, xterm.js — nenhuma chamada a CDN
DadosFora do código, em ~/.local/share/local-chat/: BD, config.yaml, security.yaml, media/, files/, backups rotacionados do BD

Configurando

Resumido a partir do README-SETUP.md dentro do zip (caixa de download abaixo).

unzip dispatch-2026-07.zip
cd dispatch
./install.sh
./run.sh

Isso o inicia em http://0.0.0.0:8765 e imprime as URLs da rede local e do Tailscale, para você tocar em uma delas pelo celular. A partir daí:

  • Edite ~/.local/share/local-chat/config.yaml (criado automaticamente, documentado em config.yaml.example) para que os ids dos bots correspondam aos ids dos seus agentes OpenClaw. Ele recarrega a quente.
  • Opcional: ./install.sh --systemd gera uma unidade systemd de usuário.
  • Opcional: defina um PIN pelo ícone de engrenagem → Security. O security.yaml.example documenta o arquivo, incluindo o api_token para quem for acessar /api/inject de fora da máquina.
  • Ajustes de ambiente: LOCAL_CHAT_HOST/PORT/DATA_DIR/AGENT_TIMEOUT/MAX_CONCURRENCY, além de LOCAL_CHAT_COMFY=0 / LOCAL_CHAT_TERMINAL=0 para manter os painéis opcionais desligados.

A própria CLI openclaw não vem incluída — o DisPatch espera que um gateway OpenClaw já esteja rodando em algum lugar acessível. Ainda não tem gateway? O app inicia mesmo assim, avisa nos logs, e tudo funciona, exceto as respostas digitadas: threads, a API de inject, o servidor de arquivos, PIN e Modo Seguro. Foi assim que fiz o teste de fumaça deste zip — descompactação nova, uv sync, porta livre, sem gateway — e a suíte de testes empacotada rodou 187 passed in 8.4s.

O que realmente acontece quando você digita uma mensagem

Você digita algo e aperta enviar. Este é o caminho que isso percorre:

Esse é o caminho feliz. A engenharia de verdade foi nos caminhos infelizes: a CLI expira, trava, ou um subagente responde dez minutos depois. Quatro fontes independentes — o observador ao vivo, o payload JSON da CLI, um reconciliador pós-turno que reescaneia a transcrição, e um "seguidor" que continua acompanhando por mais ~30 minutos — alimentam um único funil de entrega com deduplicação por turno (texto canonicalizado mais impressão digital da mídia). Qualquer um desses caminhos pode falhar e a resposta ainda assim chega exatamente uma vez. Mesmo em um erro de CLI, o reconciliador roda primeiro, porque o modelo muitas vezes já gravou sua resposta na transcrição antes de morrer.

O bloqueio em duas camadas: modo normal vs. Modo Seguro

A distinção importa porque o lado desbloqueado não é um brinquedo. Esses agentes conseguem realmente fazer coisas: editar código-fonte, publicar mudanças no meu site em produção, rodar ferramentas, gerenciar a máquina onde vivem. O lado bloqueado é só informação geral — ainda genuinamente útil, dá para fazer perguntas, bater papo e receber ajuda de verdade — mas deliberadamente cercado, longe de todo esse poder. O motivo é simples: tenho um filho de três anos, e o celular da família nem sempre está nas mãos de um adulto. Imagine o que uma criança de três anos conseguiria fazer com acesso total pelo meu celular — o site inteiro viraria vídeos da Patrulha Canina. Então o Modo Seguro é uma trava para crianças, aplicada no servidor. Um pequeno toque de qualidade de vida: a tela inicial bloqueada agora mostra os mesmos avatares de bot do lado desbloqueado, em vez dos antigos placeholders em bloco de letras. Veja o que acontece quando chega uma requisição sem PIN ainda digitado:

Se você se trancar para fora, existe um arquivo de código de recuperação mais um YAML editável, não um chamado de suporte. E se o security.yaml corromper no meio de uma gravação, o modo de falha é "continua bloqueado", não "serve tudo para qualquer um na rede local". Essa foi uma decisão deliberada.

Pegadinhas

  • O campo da API é content, não text. POST /api/inject espera {"bot_id": "...", "content": "..."}. O erro de integração mais comum de todos, incluindo alguns que eu mesmo cometi.
  • Não passe mensagens longas como argumento de linha de comando. O kernel limita o argv a 128KB (MAX_ARG_STRLEN); uma resposta longa derruba o spawn com E2BIG. O código usa --message-file <tmpfile> — não "simplifique" isso de volta para um argumento.
  • Os ids de bot diferenciam maiúsculas de minúsculas em um ponto específico. O gateway do OpenClaw transforma as chaves de sessão em minúsculas no seu índice, mas seus ids têm caixa mista (Doxy, DS_Brain). Uma busca sensível a maiúsculas e minúsculas mata silenciosamente o painel "trabalhando" e o seguidor de respostas tardias, enquanto tudo o mais continua funcionando — difícil de perceber. Primeiro tenta correspondência exata, depois cai para minúsculas.
  • A CLI só retorna o bloco final. Qualquer coisa que um agente diga entre chamadas de ferramentas existe só na sua transcrição, não no valor de retorno da CLI — daí o observador e o reconciliador, e o tratamento especial para o "aqui está o que encontrei até agora" de um agente enquanto ele espera por um subagente.
  • Sequestro de WebSocket entre sites é bloqueado por uma verificação de Origin. Ambas as rotas de WS rejeitam um handshake cujo host de Origin não bate com o Host; clientes que não são navegadores não enviam Origin e passam sem problema.
  • SVGs deliberadamente não podem ser servidos como mídia. Um navegador trata SVG inline como script da mesma origem — servir um de volta como imagem é uma brecha de XSS armazenado. As respostas de mídia recebem, em vez disso, um CSP de sandbox e nosniff.
  • Atualizações de service worker deixam abas antigas presas no JS antigo. Parece exatamente uma regressão até você perceber que a aba nunca pegou o novo worker. Resolvido com um recarregamento automático no controllerchange.

Replicando a ideia

Mesmo que você nunca rode o meu zip, a arquitetura vale a pena copiar. O todo é feito de quatro peças independentes, cada uma substituível:

  • Um backend de chat que é dono do histórico: um servidor web pequeno (aqui, FastAPI + um único arquivo SQLite em modo WAL) com um WebSocket para atualizações ao vivo e um endpoint HTTP simples para empurrar mensagens para dentro. É essa a substituição inteira do "banco de dados e pilha de containers".
  • Um gateway de agentes que o backend chama por subprocesso ou por HTTP — aqui, o OpenClaw, mas qualquer coisa que aceite "id de sessão + mensagem entra, texto sai" funciona. Mantê-lo como um processo separado significa que o app de chat sobrevive se a stack de agentes travar, for atualizada ou for trocada por completo.
  • Servidores de modelo locais por trás do gateway (LM Studio, vLLM, Ollama — problema do gateway, não do app de chat).
  • Serviços de mídia opcionais, como um ComfyUI local para geração de imagem, que os agentes chamam como ferramentas e cuja saída o backend simplesmente armazena como mídia.

As lições que valem para qualquer stack: aplique seu modo seguro/bloqueado no servidor, nunca em CSS; trate "a resposta do modelo chegou ao usuário exatamente uma vez" como um problema real de sistemas distribuídos, não como uma suposição de caminho feliz; e mantenha o frontend sem etapa de build se você espera que agentes de IA sejam quem vai mantê-lo.

Downloads

Gratuito para uso pessoal. Se isso te economizar uma tarde, o botão do café está logo ali.


← Mais de IA e LLM Local