| name | aionui |
| description | AionUI — plataforma multi-agent cowork (frontend Electron). Arquitectura real, modos de acesso (Xvfb, web viewer, noVNC), e integração com Bianinho/Hermes. NUNCA inventar arquitectura sem verificar primeiro. |
| triggers | ["aionui","AionUI access","aionrs bridge","aionrs_bridge","AionUI Mac","AionUI remote access","AionUI interactivity","AionUI browser access","como funciona o AionUI","arquitectura AionUI","Hermes desapareceu","agent selector","New Chat agente","Teams","team agente","sub agente fixo","especialist team","AionHub","aionext","extensions AionUI","install agent","Scheduled Tasks","cron job error","testesque","Scheduled Tasks panel","go","goose cli","openclaw","goose","opencode"] |
AionUI — Complete Guide
REGRA DE OURO: VERIFICAR ANTES DE DESCREVER
NUNCA descrever arquitectura sem primeiro verificar o que está a correr. O Álvaro清清楚楚 que eu estava a inventar coisas sobre o AionUI sem verificar. Antes de dizer como funciona, confirmar com:
ps aux | grep -E "aionui|AionUi|Xvfb" | grep -v grep
ss -tlnp | grep -E "8765|18743|9223"
ls ~/repos/aionui/
ls ~/repos/aionui-custom/scripts/
O Que É
AionUI é um desktop de multi-agent cowork — Gemini CLI, Claude Code, Codex, e mais agentes que trabalham juntos 24/7. Não é um chatbot normal. O Álvaro tem um fork (AlvaroBiano/AionUi) com integração Bianinho/Hermes.
Modos de Operação
Modo 1: Servidor Linux + Xvfb (headless)
O AionUI corre no servidor Linux via Xvfb + Electron. Acesso por web viewer (porta 8765) via SSH tunneling Tailscale.
BINÁRIO: ~/repos/aionui/AionUi (200MB, Electron)
Scripts de controlo:
~/repos/aionui/aionui-start.sh — inicia Xvfb + AionUi
~/repos/aionui/aionui-stop.sh — para tudo
~/repos/aionui/aionui-web-viewer.py — Flask viewer (porta 8765), screenshot streaming via CDP
~/repos/aionui/start-web-access.sh — script de acesso
Acesso via SSH Tailscale (do Mac para o servidor):
ssh -L 8765:localhost:8765 alvarobiano@alvarobiano-linuxmint.taile2fd75.ts.net
Modo 2: DMG no Mac — Local (100% offline, 02/05/2026)
O AionUI corre como app Electron nativa no Mac. Liga-se ao BianinhoBridge HTTP em localhost:18743 — sem dependência do servidor.
Arquitectura:
MacBook local
├── Hermes Agent ( ~/.hermes/venv/bin/hermes )
├── RAG Server (localhost:3101) → ~/KnowledgeBase/knowledge_db/ (1.9GB)
├── BianinhoBridge HTTP (localhost:18743) → ~/.hermes/scripts/bianinho_bridge_server.py
└── AionUI app (/Applications/AionUI.app)
Setup (02/05/2026):
- BianinhoBridge:
~/.hermes/scripts/bianinho_bridge_server.py com BIND_HOST = "127.0.0.1"
- RAG Server:
~/KnowledgeBase/rag_server.py
- AionUI: build local com IP hardcoded
100.79.189.95 → 127.0.0.1 patcheado
- launchd:
~/Library/LaunchAgents/com.bianinho.bridge.plist + com.bianinho.rag.plist
⚠️ launchd Restart Loop: KeepAlive=true + loop interno do bridge causa reinícios infinitos. NÃO usar KeepAlive. Em vez disso, o bridge já tem Restart=on-failure internamente.
⚠️ curl hangs no macOS: curl do terminal macOS não funciona para localhost:18743 (mesmo com --connect-timeout). Usar Python:
import urllib.request
resp = urllib.request.urlopen('http://127.0.0.1:18743/ping', timeout=5)
Modo 3: DMG no Mac → Servidor (Tailscale)
O AionUI no Mac liga-se ao BianinhoBridge HTTP no servidor via Tailscale (100.79.189.95:18743).
DMG custom: ~/repos/aionui-custom/out/ após build
Bridge server: ~/repos/aionui-custom/scripts/bianinho_bridge_server.py (HTTP, porta 18743, systemd service)
Modo 3: noVNC (interactivo)
x11vnc + websockify para interatividade total via browser.
Ficheiros e Scripts Chave
| Ficheiro | Descrição |
|---|
~/repos/aionui/AionUi | Binário Electron (200MB) — só existe aqui, NÃO é o fork |
~/repos/aionui/aionui-web-viewer.py | Flask viewer (porta 8765) |
~/repos/aionui/aionui-start.sh | Script iniciar Xvfb + AionUI |
~/repos/aionui/aionui-stop.sh | Script parar |
~/repos/aionui-custom/ | Fork do Álvaro — código fonte, scripts, bridge |
~/repos/aionui-custom/scripts/bianinho_bridge_server.py | HTTP bridge (porta 18743) |
~/.config/systemd/user/bianinho-bridge-server.service | systemd service para bridge |
Estado Actual (verificado com ps aux)
alvarob+ 424406 python3 /home/alvarobiano/repos/aionui-custom/scripts/bianinho_bridge_server.py (porta 18743)
O AionUI (Xvfb + Electron) NÃO está a correr no servidor. Só o BianinhoBridge HTTP está activo.
Acesso Remoto — Verificar Primeiro
NÃO ASSUMIR sem verificar. O AionUI pode estar a correr de formas diferentes conforme o contexto. Antes de dar instruções de acesso, confirmar sempre com:
ps aux | grep -E "aionui|AionUi|Xvfb|web-viewer" | grep -v grep
ss -tlnp | grep -E "8765|18743|9223"
curl -s http://localhost:8765 >/dev/null 2>&1 && echo "Web viewer ON" || echo "Web viewer OFF"
curl -s http://localhost:18743/ping >/dev/null 2>&1 && echo "Bridge ON" || echo "Bridge OFF"
Opção 1: Web Viewer (porta 8765)
Se o web viewer estiver activo:
ssh -L 8765:localhost:8765 alvarobiano@alvarobiano-linuxmint.taile2fd75.ts.net
Limitação: screenshot streaming — vês mas não interages.
Opção 2: Iniciar o AionUI no Servidor + Web Viewer
Se quiseres ter o AionUI a correr no servidor:
~/repos/aionui/aionui-start.sh
Opção 3: DMG no Mac
O DMG do fork AlvaroBiano/AionUi corre nativamente no Mac e liga-se ao BianinhoBridge HTTP (100.79.189.95:18743) via Tailscale.
Estado Actual (sempre verificar — não confiar em valores antigos)
| Componente | Estado | Como verificar |
|---|
| BianinhoBridge HTTP | ps aux | grep bianinho_bridge_server | curl http://100.79.189.95:18743/ping |
| AionUI Xvfb | ps aux | grep Xvfb | ss -tlnp | grep 8765 |
| Tailscale | tailscale status | curl http://100.79.189.95:18743/ping |
Verificado em 02/05/2026: Só o BianinhoBridge HTTP está activo. O AionUI via Xvfb NÃO está a correr.
Ficheiros e Scripts Chave
| Ficheiro | Descrição |
|---|
~/repos/aionui/AionUi | Binário Electron |
~/repos/aionui/aionui-web-viewer.py | Flask viewer com captura CDP (porta 8765) |
~/repos/aionui-hermes-ten/scripts/aionrs-bridge/aionrs_bridge.py | Bridge para Hermes |
~/repos/aionui-custom/scripts/bianinho_bridge.py | BianinhoBridge Python (TCP porta 18743) |
/tmp/aionui.log | Log do AionUi |
Estados Conhecidos (01/05/2026)
| Processo | PID | Porta/Display | Estado |
|---|
| Xvfb | - | :99 | activo |
| BianinhoBridge | 39346 | 18743 | activo |
| cloudflared (noVNC) | - | - | activo |
Resolução de Problemas
AionUI não inicia
ps aux | grep Xvfb | grep -v grep
cat /tmp/aionui.log
CDP screenshot não funciona
curl -s http://localhost:9223/json | python3 -c "import json,sys; pages=json.load(sys.stdin); print(f'{len(pages)} pages')"
noVNC conecta mas mostra ecrã negro
ps aux | grep x11vnc | grep -v grep
x11vnc -display :99 -rfbport 5900 -shared -forever -nopw &
websockify --daemon --web /tmp/novnc --file-only 6080 localhost:5900 &
Fork Workflow (AlvaroBiano/AionUi)
O Álvaro tem um fork próprio em github.com/AlvaroBiano/AionUi. O repo local vive em ~/repos/aionui-custom.
Dual remote
origin → https://github.com/iOfficeAI/AionUi.git (upstream, só leitura)
alvaro → https://github.com/AlvaroBiano/AionUi.git (fork, push aqui)
Build
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
cd ~/repos/aionui-custom
npm install --prefer-offline
bunx electron-vite build
git add -A && git commit -m "descricao" && git push alvaro main
Importante:
npm run build (electron-builder) FALHA com Node 24 + dmg-builder — não usar
bunx electron-vite build é o comando correcto
- Build deve correr em background (
background=true) — o comando demora ~40s e o foreground timeout é 60s
Adicionar uma nova página
- Criar directorio:
src/renderer/pages/<nome>/
- Criar
NomePage.tsx + index.tsx + index.module.css
- Importar no
Router.tsx:
const NomePage = React.lazy(() => import('@renderer/pages/<nome>'));
<Route path='/nome' element={withRouteFallback(NomePage)} />
Adicionar handlers IPC (bridge)
- Criar
src/process/bridge/<nome>Bridge.ts:
import { ipcMain } from 'electron';
- Importar em
src/index.ts dentro de handleAppReady():
import './process/bridge/<nome>Bridge';
- Adicionar providers em
src/common/adapter/ipcBridge.ts:
nome: {
metodo: () => invoke('nome.metodo'),
},
Estrutura de directórios relevante
src/
common/chat/bianinho/ ← módulos BianinhoBridge (TypeScript)
renderer/pages/bianinho/ ← página UI do Bianinho
renderer/pages/skill-studio/ ← Skill Studio (Fase 3)
renderer/pages/memory/ ← Memory Visualizer (Fase 3)
process/bridge/ ← handlers IPC
scripts/
install.sh ← instalador Mac/Linux
uninstall.sh ← desinstalador
bianinho_bridge.py ← bridge Python (TCP porta 18743)
bianinho-venv/ ← venv Python (Python 3.14)
bianinho-bridge/ ← docs (README.md, SPEC.md)
benchmark_bridge.py ← benchmark latência bridge
benchmark_memory.py ← benchmark memória bridge
Teams MCP — EACCES Permission Denied (CRÍTICO — 03/05/2026)
Sintoma: Hermes aparece como Team Leader mas team_spawn_agent, team_members, team_task_list não estão disponíveis. Erro no log:
[error] [Storage] Failed to persist /Users/alvarobiano/.aionui-config/aionui-config.txt: Error: EACCES: permission denied
[warn] [mcpReadiness] Timed out waiting for MCP ready: slot-xxxxx
Causa: O ficheiro ~/.aionui-config/aionui-config.txt tem permissões só de leitura (-r--r--r--). Quando o Hermes inicia como Team Leader, o AionUI tenta cachear o resultado de inicialização neste ficheiro — como não consegue escrever, o hermes_team_mcp_stdio.py nunca chega a iniciar e as tools team_* ficam indisponíveis.
Fix (sempre executar primeiro quando Teams MCP falha):
chmod u+w ~/.aionui-config/aionui-config.txt
Verificação:
ls -la ~/.aionui-config/aionui-config.txt
Fluxo de debugging Teams MCP (sempre nesta ordem):
chmod u+w ~/.aionui-config/aionui-config.txt —权限
grep -i "TeamGuide\|team.*mcp\|EACCES" ~/Library/Logs/AionUi/$(date +%Y-%m-%d).log | tail -20 — logs
ps aux | grep -E "team-mcp|hermes_team" — processos
- Se
hermes_team_mcp_stdio.py não está a correr, o AionUI não está a injetar o Teams MCP no Hermes
Arquitectura Teams MCP (03/05/2026):
team-mcp-stdio.js → para aionrs, gemini, claude (Node.js)
hermes_team_mcp_stdio.py → para Hermes (Python, TCP 4-byte length prefix + JSON)
- Ambos comunicam com o
TeamGuideMcpServer interno do AionUI via TCP (porta dinâmica)
- O AionUI passa
TEAM_MCP_PORT + TEAM_MCP_TOKEN como variáveis de ambiente
Preferências de Comunicação do Álvaro
- Respostas curtas e directas — sem explicações longas, sem "deixa-me ver", sem rodeios
- Quando algo não aparece na UI, PERGUNTAR primeiro — não vasculhar código antes de confirmar o que o utilizador vê. Exemplo: "Hermes não aparece no Teams" → primeiro perguntar "o que aparece no dropdown? Algum agente?" antes de extrair ASARs e greppar código.
- Quando pedir algo (link, ficheiro, acção): executar imediatamente, não perguntar sedeve fazer
- "manda o arquivo" = anexar via MEDIA:path no Telegram — não é só mencionar o link
- Em incidentes críticos (WiFi, servidores): tentar resolver sozinho antes de perguntar
- Se não sabe algo: dizer "não sei" — não inventar
- Ligar para serviço: primeiro verificar se está a funcionar, depois informar o resultado
Estado Actual (02/05/2026)
MacBook Local (100% offline)
| Componente | Estado | Porta |
|---|
| Hermes Agent | ✅ Funcional | — |
| BianinhoBridge HTTP | ✅ Funcional | localhost:18743 |
| RAG HTTP Server | ✅ Funcional | localhost:3101 |
| AionUI DMG | ✅ Instalado | /Applications/AionUI.app |
| Knowledge Base | ✅ 1.9GB synced | ~/KnowledgeBase/ |
Servidor Linux
| Componente | Estado | Porta |
|---|
| BianinhoBridge HTTP | ✅ Funcional | 100.79.189.95:18743 |
| AionUI Xvfb + Web Viewer | ✅ Funcional | localhost:8765 |
| cloudflared tunnel | ✅ activo | aionui.masterclasslife.com.br |
Nota: Ambos os ambientes coexistem. MacBook é 100% local; servidor é para acesso remoto via web viewer.
Acesso ao AionUI do MacBook (PRIMÁRIO — 02/05/2026)
O Álvaro pode aceder ao AionUI que corre no servidor Linux Mint a partir do MacBook Pro através de:
👉 https://aionui.masterclasslife.com.br
Arquitectura:
Servidor (Linux Mint)
├── Xvfb :99
├── AionUi (Electron, porta CDP 9223)
├── aionui-web-viewer.py (Flask, porta 8765, screenshots via CDP)
└── cloudflared tunnel → aionui.masterclasslife.com.br
│
└── Internet (HTTPS)
│
MacBook Pro ──────┘
└── Browser → https://aionui.masterclasslife.com.br
Estado verificado (02/05/2026):
curl https://aionui.masterclasslife.com.br → HTTP 200 ✅
- AionUI binary:
~/repos/aionui/AionUi (v1.9.23, ELF 64-bit x86_64)
- Web viewer em
localhost:8765 → 200 OK, screenshot 79KB
- DNS CNAME:
aionui.masterclasslife.com.br → d532ba47-...cfargotunnel.com (proxied=true)
- Túnel cloudflared: processo activo com PID
Verificação rápida:
ps aux | grep cloudflared | grep tunnel | grep -v grep
curl -s --connect-timeout 5 http://localhost:8765/ -o /dev/null -w "%{http_code}"
curl -s --connect-timeout 8 https://aionui.masterclasslife.com.br -o /dev/null -w "%{http_code}"
Se ERRO 1033 ("Cloudflare Tunnel error — Cloudflare is currently unable to resolve it"):
→ O túnel cloudflared NÃO está a correr. Verificar com ps aux | grep cloudflared. Se não estiver, iniciar com:
cloudflared tunnel run >> /tmp/cloudflared.log 2>&1 &
sleep 8
curl -s --connect-timeout 8 https://aionui.masterclasslife.com.br -o /dev/null -w "%{http_code}"
Acesso ao AionUI do MacBook (PRIMÁRIO — 02/05/2026)
O Álvaro quer aceder ao AionUI que corre no servidor Linux Mint a partir do MacBook Pro. Este é o caso de uso principal, não o DMG.
Arquitectura:
Servidor (Linux Mint)
├── Xvfb :99
├── AionUi (Electron, porta CDP 9223)
├── aionui-web-viewer.py (Flask, porta 8765, screenshots via CDP)
└── cloudflared (túnel nomeado → aionui.trycloudflare.dev)
│
└── Internet (HTTPS)
│
MacBook Pro ──────┘
└── Browser → https://aionui.trycloudflare.dev/
Estado verificado (02/05/2026):
- AionUI binary:
~/repos/aionui/AionUi (v1.9.23, ELF 64-bit x86_64)
- AionUI a correr com Xvfb :99 +
--remote-debugging-port=9223
- Web viewer a servir em
localhost:8765 (Flask/Werkzeug)
- Screenshot funciona: 79KB por frame, 2fps,
curl localhost:8765/screenshot.png = 200 OK
- cloudflared tunnel connected mas
aionui.trycloudflare.dev DNS não existe
Para continuar (needs Cloudflare API token):
Criar o DNS record via cloudflared tunnel route dns d532ba47-1f57-4c2d-8622-011a259b5a90 aionui.trycloudflare.dev. Requer API token da Cloudflare com permissão DNS.
SEM Cloudflare API token — alternativas:
- Túnel rápido (instável, URL muda a cada reinício):
cloudflared tunnel --url http://localhost:8765 → URL disponível em https://*.trycloudflare.com
- SSH tunnel (funciona mas requer SSH abierto):
ssh -L 8765:localhost:8765 alvarobiano@alvarobiano-linuxmint.taile2fd75.ts.net
Problema: cloudflared quick tunnel dá sempre 404
Sintoma: curl https://*.trycloudflare.com/screenshot.png = HTTP 404, mas curl http://localhost:8765/screenshot.png = 200 OK.
Causa: O quick tunnel (--url) cria um túnel "account-less" que faz routing básico. paths como /screenshot.png são rejeitados — o túnel só funciona para o root / ou não faz o forwarding completo dos headers HTTP.
Solução: Não usar --url para produção. Usar túnel nomeado com DNS record criado via API, ou SSH tunnel.
Problema: websocket-client não disponível no Python do aionui-web-viewer
Sintoma: aionui-web-viewer.py falha com ModuleNotFoundError: No module named 'websocket'.
Causa: O script usa #!/usr/bin/env python3 que aponta para /home/alvarobiano/.local/bin/python3 (uv-managed Python 3.14). O módulo websocket-client está instalado em /usr/lib/python3/dist-packages/ (sistema) e no venv do Hermes (Python 3.11), mas não no Python 3.14.
Solução:
/usr/bin/python3 -m pip install websocket-client --break-system-packages
/usr/bin/python3 ~/repos/aionui/aionui-web-viewer.py
Regra de Ouro: EXECUTAR primeiro, confirmar depois
Quando o Álvaro diz "configure X", "faça Y", "instale Z" — executar imediatamente, não perguntar se deve fazer ou qual modo prefere. Perguntas de clarificação só se ele pedir para escolher entre opções claramente diferentes. Se ele diz "configura como fallback", isso é uma instrução, não uma pergunta.
Excepção: Só perguntar se as opções tiverem consequências muito diferentes (ex: "isto apaga dados?" ou "qual é o IP do servidor?").
Exemplo de violação (02/05/2026): O Álvaro disse "configura o Anything LLM como fallback" e eu respondi com uma pergunta "qual preferes?" em vez de simplesmente configurar. Corrigido — executou-se e confirmou-se depois.
AVISO CRÍTICO: Web Viewer (porta 8765) é Screenshot-Only
O web viewer em localhost:8765 é um fluxo de screenshots via Flask/CDP — NÃO é o AionUI interactivo. É um espelho VNC, não uma sessão real.
curl localhost:8765/screenshot.png → 200 OK, imagem PNG (screenshot)
curl localhost:8765/ → 200 OK, HTML com iframe do screenshot
- Não é possível interagir com o AionUI através do web viewer
- Não deve ser exposto como "AionUI acessível" — é enganoso
Se o Álvaro pedir para expor o AionUI na internet, JAMAIS expor a porta 8765 como se fosse acesso interactivo. Explicar que é apenas um espelho read-only.
Para acesso real e interactivo, as opções são:
- SSH X11 forwarding (XQuartz no Mac) — sessão X11 real
- DMG local — AionUI compilado para macOS, corre nativamente
- noVNC — VNC interactivo via browser (x11vnc + websockify)
- OpenClaw Remote Agent — WebSocket, mas requer OpenClaw Gateway no servidor (não existe ainda)
Arquitectura de Ligação AionUI ↔ Bianinho/Hermes (Cenários Reais)
| Cenário | Como funciona | Estado |
|---|
| DMG Mac → BianinhoBridge HTTP (servidor) | AionUI Electron no Mac liga-se a 100.79.189.95:18743 via Tailscale | ✅ Produção |
| DMG Mac → Hermes via ACP stdio | Hermes CLI local no Mac (não existe ainda) | ❌ Não existe |
| AionUI no servidor (Xvfb) → BianinhoBridge | Electron no servidor + bridge HTTP local | ⚠️ Apenas leitura (web viewer) |
| AionUI Mac → Hermes remoto via OpenClaw | Remote Agent WebSocket → OpenClaw Gateway no servidor | ❌ Requer setup OpenClaw |
| SSH X11 forwarding | Mac XQuartz → servidor Xvfb | ⚠️ Funciona mas lento e sem som |
O cenário que o Álvaro mais provavelmente quer: DMG local no Mac conectado ao BianinhoBridge HTTP no servidor via Tailscale. Este é o fluxo documentado em "BianinhoBridge (HTTP via Tailscale)" e está em produção.
O que NÃO é: expor a porta 8765 do web viewer como "AionUI na internet" — isso é apenas um screenshot read-only.
Esta regra aplica-se a TODAS as tarefas, especialmente quando há várias abordagens possíveis.
O BianinhoBridge tem dois modos, conforme o caso de uso:
Modo 1: TCP Local (default no packager DMG)
O bridge corre como subprocesso Python local no Mac, binds a 127.0.0.1:18743.
Usado quando Hermes + Bianinho estão instalados localmente no Mac.
Scripts: scripts/bianinho_bridge.py (TCP, 4-byte length prefix + JSON)
Estado: Em desuso para o Mac (precisa Hermes local)
Modo 2: HTTP via Tailscale (cloud — 02/05/2026)
O bridge corre no servidor Linux, binds a 100.79.189.95:18743 (IP Tailscale).
O AionUI no MacLiga-se ao servidor via rede Tailscale ponto-a-ponto.
Não precisa de SSH, nem de portas abertas no firewall, nem de cloudflared.
Scripts: scripts/bianinho_bridge_server.py (HTTP server, porta 18743)
Estado: ✅ Produção — activo em 100.79.189.95:18743
Endpoints HTTP (todos respondem GET ou POST):
| Método | Path | Descrição |
|---|
| GET | /ping | Health check |
| GET | /status | Uptime, msgs, errors |
| GET | /platform_info | OS, machine, python version |
| GET | /check_hermes | Verifica 5 checks do Hermes |
| GET | /hermes_path | Path do Hermes |
| GET | /list_skills | Lista 71 skills |
| GET | /rag_stats | Estatísticas RAG |
| POST | /rag_search | Pesquisa: {query, category, topK} |
| POST | /rag_backup | Backup: {label?} |
| GET | /inbox_list | Lista inbox |
| POST | /inbox_add | Adiciona: {content, priority, tags, source} |
| POST | /inbox_done | Toggle done: {id} |
| POST | /inbox_delete | Remove: {id} |
| GET | /cycle_status | Estado do ciclo autónomo |
| POST | /cycle_trigger | Força ciclo |
| GET | /memory?key=X | Lê chave |
| POST | /memory_set | Escreve: {key, value} |
| GET | /config?key=X | Lê config |
| POST | /config_set | Escreve: {key, value} |
| GET | /sync_status | Estado sync |
Verificação rápida (servidor):
curl http://100.79.189.95:18743/ping
curl http://100.79.189.95:18743/rag_stats
curl http://100.79.189.95:18743/list_skills
Verificação rápida (Mac — via Tailscale):
curl http://100.79.189.95:18743/ping
Se não responder — debugging:
ss -tlnp | grep 18743
systemctl --user status bianinho-bridge-server
journalctl --user-unit bianinho-bridge-server -n 20
systemctl --user restart bianinho-bridge-server
systemd Service (bianinho-bridge-server)
Ficheiro: ~/.config/systemd/user/bianinho-bridge-server.service
[Unit]
Description=BianinhoBridge HTTP Server
After=network.target tailscaled.service
[Service]
ExecStart=/usr/bin/python3 /home/alvarobiano/repos/aionui-custom/scripts/bianinho_bridge_server.py
WorkingDirectory=/home/alvarobiano/repos/aionui-custom
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable bianinho-bridge-server
systemctl --user start bianinho-bridge-server
Nota: O bind a 100.79.189.95 (Tailscale IP) só funciona se o Tailscale estiver activo. Se o Tailscale cair, o servidor recusa o bind e o serviço entra em crash-loop — o RestartSec=10 dá tempo ao Tailscale para recuperar.
Código do Bridge (bianinhoBridge.ts — Electron main process)
O src/process/bridge/bianinhoBridge.ts usa Electron net.request (não fetch) para comunicar com o servidor HTTP via Tailscale:
import { ipcMain, net } from 'electron';
const BRIDGE_HOST = '100.79.189.95';
const BRIDGE_PORT = 18743;
async function httpSend(method: 'GET' | 'POST', path: string, body?: Record<string, unknown>): Promise<unknown> {
const url = `http://${BRIDGE_HOST}:${BRIDGE_PORT}${path}`;
return new Promise((resolve) => {
const req = net.request({ method, url });
req.setHeader('Content-Type', 'application/json');
req.end();
});
}
Os handlers IPC (ex: bianinho.ragSearch) fazem httpSend('POST', '/rag_search', args).
BianinhoBridge (Python Sidecar)
O BianinhoBridge é o layer Python que integra o Bianinho autónomo no AionUI. Corre como processo sidecar em scripts/bianinho_bridge.py (Python 3.14, venv bianinho-venv).
Estado: Em produção (porta 18743, pids activos 39346/39528/81794)
Arquitectura
AionUI (Electron)
│ TCP 127.0.0.1:18743 (4-byte length prefix + JSON)
▼
BianinhoBridge (Python)
├── RAG: ~/KnowledgeBase/knowledge_db/
├── Hermes: ~/.hermes/
├── Skills: ~/.hermes/skills/ (72 skills)
├── Backup: ~/.hermes/backups/
└── Inbox/Memory: ~/.hermes/inbox.json, memory.json
Protocolo TCP Bridge
Formato
[4 bytes: big-endian length][JSON payload]
Teste rápido com nc
printf '\x00\x00\x00\x1d{"cmd":"ping","args":{"echo":"test"}}' | nc -N 127.0.0.1 18743
Erro comum: shutdown(SHUT_WR) quebra
sock.send(payload)
sock.shutdown(socket.SHUT_WR)
resp = sock.recv(...)
sock.sendall(len(payload).to_bytes(4, 'big') + payload)
Comandos Disponíveis (22)
Sistema
| Comando | Args | Descrição |
|---|
ping | echo | Teste de conectividade |
status | — | Uptime, msgs, errors, rate_limit_hits |
platform_info | — | system, release, machine, python version |
check_hermes | — | Verifica paths do Hermes (5/6 checks) |
RAG
| Comando | Args | Descrição |
|---|
rag_search | query, category?, topK? | Pesquisa com isolation |
rag_stats | — | Estatísticas: total chunks, categorias |
rag_backup | label? | Backup pre-write |
rag_restore | backup_name | Restore |
rag_list_backups | — | Lista backups |
Inbox / Skills / Ciclo / Memória / Snapshots
| Comando | Args | Descrição |
|---|
inbox_list/add/done/delete | — | CRUD tarefas |
list_skills, skill_execute, skill_validate | — | Skills |
cycle_status, cycle_trigger | — | Ciclo autónomo |
memory_get, memory_set | — | Memória factual |
snapshot_export, snapshot_import | — | Backup encriptado |
Segurança
| Mecanismo | Implementação |
|---|
| HMAC Auth | Token timestamp.signature, TTL 24h |
| Rate Limiting | 100 req/min por cliente TCP |
| Skills Sandbox | CPU 60s, RAM 500MB, timeout 30s |
| RAG Access Levels | full, read_sac, read_personal |
| Payload Validation | schemas para todos os comandos |
Build Testado
| Comando | Resultado | Nota |
|---|
bunx electron-vite build | ✅ OK (~36s) | Compila 3 alvos |
npm run package | ✅ OK | electron-vite build via package.json |
node scripts/build-with-builder.js --linux --x64 --publish=never | ✅ OK | Produz .deb (252MB) com todos extraResources |
node scripts/build-with-builder.js --mac | ❌ SÓ macOS | dmg-license não instala em Linux |
| Build em foreground | ⚠️ TIMEOUT | Corre em background (60s limit) |
White Screen — Debugging Sem DevTools (RESOLVIDO — 02/05/2026)
O white screen com e.map is not a function foi diagnosed e resolvido sem acesso a DevTools — o Álvaro não conseguiu abrir DevTools no Mac. As 4 causas raiz foram identificadas e corrigidas.
Fluxo de debug cego (sempre aplicar na ordem)
1. Adicionar ErrorBoundary Global (PRIMEIRO)
Sem ErrorBoundary, crashes de renderização mostram só e.map is not a function sem contexto — impossível saber que componente crashou. Com ErrorBoundary, o browser mostra o stack trace completo.
Criar src/renderer/components/ErrorBoundary.tsx:
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
export class ErrorBoundary extends React.Component<React.PropsWithChildren<object>, ErrorBoundaryState> {
constructor(props: React.PropsWithChildren<object>) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { hasError: true, error };
}
render(): React.ReactNode {
if (this.state.hasError) {
return (
<div style={{ padding: '40px', maxWidth: '600px', margin: '40px auto', fontFamily: 'monospace', fontSize: '13px' }}>
<h2 style={{ color: '#c00' }}>Erro de renderização</h2>
<p><strong>Mensagem:</strong> {this.state.error?.message}</p>
<details style={{ marginTop: '16px', whiteSpace: 'pre-wrap' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>Stack trace</summary>
<pre style={{ fontSize: '11px', background: '#fff', padding: '8px' }}>
{this.state.error?.stack}
</pre>
</details>
{this.state.errorInfo?.componentStack && (
<details style={{ marginTop: '12px', whiteSpace: 'pre-wrap' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>Component stack</summary>
<pre style={{ fontSize: '11px', background: '#fff', padding: '8px' }}>
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
</div>
);
}
return this.props.children;
}
}
Nota crítica: componentDidCatch como lifecycle method não funciona em React 18+. Em React 19 está completamente broken. O único método que funciona é getDerivedStateFromError. Se o ErrorBoundary mostrar ecrã branco em vez do UI de erro, é porque componentDidCatch está a话了 o render — remover imediatamente.
Em src/renderer/main.tsx:
import { ErrorBoundary } from './components/ErrorBoundary';
root.render(
React.createElement(
ErrorBoundary, null,
React.createElement(AppProviders, null, React.createElement(App))
)
);
2. Debug cego — 3 causas mais comuns
Após o ErrorBoundary, se o utilizador reportar erro visível com stack trace, o diagnóstico é directo. Se ainda branco total, aplicar cegamente (resolvem ~90% dos casos):
Causa 1 — isDesktopRuntime = false (mais crítica)
- Ficheiro:
src/renderer/hooks/context/AuthContext.tsx
Boolean(window.electronAPI) retorna false no DMG → app bloqueia em checking
- Fix:
const isDesktopRuntime = true;
Causa 2 — IPC handlers em falta
- Ficheiro:
src/process/bridge/bianinhoBridge.ts
- Handlers ausentes:
ragStats, inboxList, cycleStatus, memoryGet/Set, ragSearch, ragBackup, inboxAdd/Done/Delete
- Todos devem ter fallback:
return { count: 0, items: [] } etc.
Causa 3 — .map() em null no renderer
- Ficheiros:
BianinhoPage.tsx, GuidPage.tsx
result do IPC pode ser null → setSkillsInfo(null) → crash no .map()
- Validar:
if (result && typeof result === 'object' && Array.isArray((result as any).skills))
- Renderização:
ragStats?.categories?.length > 0 (usar ?.)
3. Push + Rebuild DMG
import urllib.request, json, re
token = re.search(r'password\s+(\S+)', open('~/.netrc').read()).group(1)
headers = {'Authorization': f'token {token}', 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/vnd.github.v3+json'}
req = urllib.request.Request('https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows', headers=headers)
with urllib.request.urlopen(req) as r:
wf_id = next(w['id'] for w in json.loads(r.read())['workflows'] if 'dmg' in w['name'].lower())
dispatch_req = urllib.request.Request(
f'https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows/{wf_id}/dispatches',
method='POST', data=json.dumps({'ref': 'main'}).encode(), headers=headers
)
urllib.request.urlopen(dispatch_req)
Nota: O build-dmg.yml só tem trigger workflow_dispatch — não corre automaticamente no push. Após cada commit, é preciso dispatchar manualmente.
PASSO 4 — Download do artifact
ARTIFACT_ID=6762494110
gh api repos/AlvaroBiano/AionUi/actions/artifacts/$ARTIFACT_ID/zip > /tmp/AionUi.dmg.zip
unzip -o /tmp/AionUi.dmg.zip && ls *.dmg
5. Criar Release com Asset
gh release create v1.9.24-fix2 \
--repo AlvaroBiano/AionUi \
--title "AionUI v1.9.24 (fix white screen)" \
--notes "Fix white screen on Mac DMG"
gh release upload v1.9.24-fix2 /tmp/AionUi-1.9.24-mac-arm64.dmg --repo AlvaroBiano/AionUi
Atalhos úteis para debug
Cmd + Alt + I (Mac) → abre DevTools directamente, mesmo sem menu
- Linha de comando:
/Applications/AionUI.app/Contents/MacOS/AionUI --no-sandbox 2>&1 | head -50
- Log do macOS:
~/Library/Logs/AionUi/
O white screen com e.map is not a function foi diagnosed e resolvido sem acesso a DevTools — o Álvaro não conseguiu abrir DevTools no Mac. O fluxo de debug começa SEMPRE com o ErrorBoundary.
PASSO 1 — Adicionar ErrorBoundary Global (primeiro fix a aplicar)
Quando o utilizador reporta white screen, o primeiro passo é adicionar um ErrorBoundary ao main.tsx. Sem ele, crashes de renderização mostram SOMENTE e.map is not a function sem contexto. Com ele, o browser mostra o stack trace exacto do componente que crashou.
Ficheiro: src/renderer/components/ErrorBoundary.tsx (criar):
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
export class ErrorBoundary extends React.Component<React.PropsWithChildren<object>, ErrorBoundaryState> {
constructor(props: React.PropsWithChildren<object>) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
this.setState({ errorInfo });
(window as unknown as Record<string, unknown>).__RENDER_ERROR__ = { error, errorInfo };
console.error('[ErrorBoundary] Render error:', error.message);
console.error('[ErrorBoundary] Stack:', error.stack);
console.error('[ErrorBoundary] Component stack:', errorInfo.componentStack);
}
render(): React.ReactNode {
if (this.state.hasError) {
return (
<div style={{ padding: '40px', maxWidth: '600px', margin: '40px auto', fontFamily: 'monospace', fontSize: '13px' }}>
<h2 style={{ color: '#c00' }}>Erro de renderização</h2>
<p><strong>Mensagem:</strong> {this.state.error?.message}</p>
<details style={{ marginTop: '16px', whiteSpace: 'pre-wrap' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>Stack trace</summary>
<pre style={{ fontSize: '11px', background: '#fff', padding: '8px' }}>
{this.state.error?.stack}
</pre>
</details>
<details style={{ marginTop: '12px', whiteSpace: 'pre-wrap' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>Component stack</summary>
<pre style={{ fontSize: '11px', background: '#fff', padding: '8px' }}>
{this.state.errorInfo?.componentStack}
</pre>
</details>
</div>
);
}
return this.props.children;
}
}
Em src/renderer/main.tsx:
import { ErrorBoundary } from './components/ErrorBoundary';
root.render(
React.createElement(
ErrorBoundary,
null,
React.createElement(AppProviders, null, React.createElement(App))
)
);
Nota: O ErrorBoundary DEVE usar getDerivedStateFromError — componentDidCatch não funciona em React 18+.
PASSO 2 — Debug cego (3 causas mais comuns)
Após adicionar ErrorBoundary, se o utilizador报告ar um erro visível, seguir o fluxo. Se ainda branco total, aplicar os 3 fixes cegos (causam ~90% dos casos):
Causa 1 — isDesktopRuntime é false no packaged app (MAIS CRÍTICA)
O AuthContext.tsx lia Boolean(window.electronAPI) para detectar se está no desktop. No packaged DMG, o preload pode não expor o electronAPI correctamente → isDesktopRuntime = false → app tenta ir à rede buscar utilizador → status === 'checking' para sempre → Router bloqueado → ecrã branco.
Fix em src/renderer/hooks/context/AuthContext.tsx:
const isDesktopRuntime = typeof window !== 'undefined' && Boolean(window.electronAPI);
const isDesktopRuntime = true;
Causa 2 — IPC handlers em falta (BianinhoBridge)
Os handlers ragStats, inboxList, cycleStatus, memoryGet, memorySet, ragSearch, ragBackup, inboxAdd, inboxDone, inboxDelete não existiam em src/process/bridge/bianinhoBridge.ts. Quando o renderer chamava estes, o IPC retornava undefined.
Padrão de fix — todos os handlers devem ter fallback defensivo:
ipcMain.handle('bianinho.ragStats', async () => {
const result = await tcpSend('rag_stats');
if (result && typeof result === 'object' && 'stats' in result) {
return result;
}
return { stats: { total_chunks: 0, categories: [] } };
});
ipcMain.handle('bianinho.inboxList', async () => {
const result = await tcpSend('inbox_list');
if (result && typeof result === 'object' && 'items' in result) {
return result;
}
return { count: 0, items: [] };
});
ipcMain.handle('bianinho.listSkills', async () => {
const result = await tcpSend('list_skills');
if (result && typeof result === 'object' && Array.isArray((result as { skills?: unknown }).skills)) {
return result;
}
return { count: 0, skills: [] };
});
Causa 3 — .map() chamado em null no renderer
Em BianinhoPage.tsx, o resultado do IPC era guardado directamente na state sem validação. Também na renderização, .map() era chamado sem guarda ?..
const result = await ipcBridge.bianinho.listSkills.invoke();
if (result && typeof result === 'object' && Array.isArray((result as { skills?: unknown }).skills)) {
setSkillsInfo(result as SkillsInfo);
} else {
setSkillsInfo({ count: 0, skills: [] });
}
{ragStats?.categories?.length > 0 && ragStats.categories.map(cat => ...)}
{skillsInfo?.skills?.length ? skillsInfo.skills.map(...) : null}
if (!agentSelection.availableAgents?.length) return [];
PASSO 3 — Push + Rebuild DMG
git add -A && git commit -m "fix: white screen" && git push alvaro main
import urllib.request, json, re
token = re.search(r'password\s+(\S+)', open('~/.netrc').read()).group(1)
headers = {'Authorization': f'token {token}', 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json'}
req = urllib.request.Request('https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows', headers=headers)
with urllib.request.urlopen(req) as r:
wf_id = next(w['id'] for w in json.loads(r.read())['workflows'] if 'dmg' in w['name'].lower())
dispatch_req = urllib.request.Request(
f'https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows/{wf_id}/dispatches',
method='POST', data=json.dumps({'ref': 'main'}).encode(), headers=headers
)
urllib.request.urlopen(dispatch_req)
Nota importante: O workflow build-dmg.yml tem APENAS workflow_dispatch — não corre automaticamente no push. É preciso dispatchar manualmente após cada commit.
Download do artifact (gh CLI OK para download):
gh api repos/AlvaroBiano/AionUi/actions/runs/25252230954/artifacts --jq '.artifacts[0] | "\(.id) \(.name)"'
ARTIFACT_ID=6763103414
gh api repos/AlvaroBiano/AionUi/actions/artifacts/$ARTIFACT_ID/zip > /tmp/AionUi.dmg.zip
unzip -o /tmp/AionUi.dmg.zip && ls *.dmg
Upload para release:
gh release create v1.9.24-fix2 --repo AlvaroBiano/AionUi --notes "Build c6c3dca6b"
gh release upload v1.9.24-fix2 /tmp/AionUi.dmg.zip --repo AlvaroBiano/AionUi
PASSO 5 — Informar o utilizador
Link: https://github.com/AlvaroBiano/AionUi/releases/download/v1.9.24-fix2/AionUi-1.9.24-mac-arm64.dmg
Instruções:
1. Apaga o AionUI antigo em Applications
2. Descarrega o .dmg pelo link
3. Duplo-click para montar > Arrasta para Applications
4. Terminal: sudo xattr -rd com.apple.quarantine /Applications/AionUI.app
5. Abre AionUI
Tag de release: criar com gh release create v1.9.24-fix2 --repo AlvaroBiano/AionUi --title "..." --notes "..." e depois fazer upload do DMG.
atalhos Úteis para Debug
Cmd + Alt + I no Mac → abre DevTools directamente (mesmo sem menu)
- Linha de comando:
/Applications/AionUI.app/Contents/MacOS/AionUI --no-sandbox 2>&1 | head -50
- Log do macOS:
~/Library/Logs/AionUi/
(RESOLVIDO — 02/05/2026)
13. White Screen — Root Cause: useSiderTeamBadges + SWR teams=undefined + useTeamList fetcher
Data: 02/05/2026 — 5 DMG builds needed to resolve.
Sintoma: TypeError: e.map is not a function na linha 7560 do bundle. Hash muda entre builds (B7FSMILK → CBvrP838).
Metodologia de debug (crítica para futuras ocorrências):
O problema não estava nos sítios óbvios (AuthContext, BianinhoPage, GuidPage). O crash era num hook aparentemente unrelated — useSiderTeamBadges(teams). SWR pode retornar undefined durante o primeiro render antes do fetch resolver, mesmo com = [] default no destructuring.
Lição aprendida (02/05/2026): O ?? [] fallback NO DESTRUCTURING não protege contra undefined durante a execução do useState(initCounts). A defence const safe = teams ?? [] tem de ser a primeira linha do hook, antes de qualquer uso de teams.
Os 2 fixes em useTeamList.ts e useSiderTeamBadges.ts (commit 95e4a7404):
const { data: teams = [], mutate } = useSWR<TTeam[]>(
`teams/${userId}`,
() => ipcBridge.team.list.invoke({ userId }).then((data) => {
if (Array.isArray(data)) return data;
console.warn('[useTeamList] IPC returned non-array:', data);
return [];
}),
{ revalidateOnFocus: false }
);
export function useSiderTeamBadges(teams: TTeam[] | undefined): Map<string, number> {
const safeTeams: TTeam[] = Array.isArray(teams) ? teams : [];
const initCounts = (): Map<string, number> => {
for (const team of safeTeams) { ... }
};
useEffect(() => {
for (const team of safeTeams) {
for (const agent of team.agents ?? []) { ... }
}
}, [
safeTeams.map((t) => `${t.id}:${(t.agents ?? []).map((a) => a.conversationId || '').join(',')}`).join('|')
]);
}
Nota: Optional chaining no dependency array (teams?.map(...)) NÃO previne crash no corpo da função — o crash era em initCounts() que é chamado DURANTE o render, antes do useEffect correr.
Extração do ASAR para análise (sempre fazer em builds未知):
cd /tmp && 7z x AionUi-1.9.24-mac-arm64.dmg -o/tmp/dmg-contents -y
cd /tmp && npm install asar --no-save
node -e "require('asar').extractAll('/path/to/app.asar', '/tmp/asar-extracted')"
find /tmp/asar-extracted -name "index-*.js" | head -5
python3 - << 'PYEOF'
with open('/tmp/asar-extracted/out/renderer/assets/index-B7FSMILK.js') as f:
content = f.read()
lines = content.split('\n')
for i in range(7559, 7570):
print(f"{i+1}: {lines[i][:200]}")
import re
for i, line in enumerate(lines):
if 'r.agents.map' in line and 'conversationId' in line:
print(f"FOUND at line {i+1}: {line[:300]}")
PYEOF
O bug real (useSiderTeamBadges):
export function useSiderTeamBadges(teams: TTeam[]): Map<string, number> {
const initCounts = () => {
for (const team of teams)
map.set(team.id, readStorage(team.id));
return map;
};
useEffect(() => {
for (const team of teams)
for (const agent of team.agents)
...;
}, [teams.map(...)])
}
export function useSiderTeamBadges(teams: TTeam[] | undefined): Map<string, number> {
const safeTeams: TTeam[] = teams ?? [];
const initCounts = () => {
for (const team of safeTeams) { ... }
};
useEffect(() => {
for (const team of safeTeams) {
for (const agent of team.agents ?? []) { ... }
}
}, [safeTeams.map((t) => `${t.id}:${(t.agents ?? []).map(...).join(',')}`).join('|')]);
}
Padrão geral: qualquer hook que recebe dados de SWR/fetch como prop/argumento DEVE defender contra undefined no primeiro render. O default = [] no destructuring NÃO protege durante a execução de initCounts() ou useEffect quando teams é passado como parâmetro. A defesa const safe = teams ?? [] deve ser a primeira linha do hook.
(RESOLVIDO — 02/05/2026)
Quando o utilizador não consegue abrir DevTools no Mac (não sabe como, ou está frustrado), aplicar o fluxo de debug cego: as 3 causas mais comuns são isDesktopRuntime = false, handlers IPC em falta, e .map() sem guarda. Push + rebuild DMG resolve ~90% dos casos. Ver secção completa em references/dmg-build.md → "White Screen — Debugging Sem DevTools". (RESOLVIDO — 02/05/2026)
O app compila e abre, mas fica com ecrã branco. Erro no Console DevTools:
Uncaught TypeError: e.map is not a function
at N1t (index-BRdc99Ma.js:7560)
Três causas raiz descobertas e corrigidas:
Causa 1 — isDesktopRuntime é false no packaged app (MAIS CRÍTICA)
O AuthContext.tsx lia Boolean(window.electronAPI) para detectar se está no desktop. No packaged DMG, o preload pode não expor o electronAPI correctamente, então isDesktopRuntime = false → o app tenta ir à rede buscar utilizador → fica em loading eterno → ecrã branco.
Fix em src/renderer/hooks/context/AuthContext.tsx:
const isDesktopRuntime = typeof window !== 'undefined' && Boolean(window.electronAPI);
const isDesktopRuntime = true;
Isto é a causa mais provável quando o ecrã é branco desde o primeiro frame — o Router fica bloqueado no status === 'checking' para sempre.
Causa 2 — IPC handlers em falta (BianinhoBridge)
Os handlers ragStats, inboxList, cycleStatus, memoryGet, memorySet, ragSearch, ragBackup, inboxAdd, inboxDone, inboxDelete não existiam em src/process/bridge/bianinhoBridge.ts. Quando o renderer chamava estes, o IPC retornava undefined ou erro.
Padrão de fix — todos os handlers devem ter fallback defensivo:
ipcMain.handle('bianinho.ragStats', async () => {
const result = await tcpSend('rag_stats');
if (result && typeof result === 'object' && 'stats' in result) {
return result;
}
return { stats: { total_chunks: 0, categories: [] } };
});
ipcMain.handle('bianinho.inboxList', async () => {
const result = await tcpSend('inbox_list');
if (result && typeof result === 'object' && 'items' in result) {
return result;
}
return { count: 0, items: [] };
});
ipcMain.handle('bianinho.listSkills', async () => {
const result = await tcpSend('list_skills');
if (result && typeof result === 'object' && Array.isArray((result as { skills?: unknown }).skills)) {
return result;
}
return { count: 0, skills: [] };
});
Causa 3 — .map() chamado em null no renderer
Em BianinhoPage.tsx, o resultado do IPC era guardado directamente na state sem validação:
const result = await ipcBridge.bianinho.listSkills.invoke();
setSkillsInfo(result);
const result = await ipcBridge.bianinho.listSkills.invoke();
if (result && typeof result === 'object' && Array.isArray((result as { skills?: unknown }).skills)) {
setSkillsInfo(result as SkillsInfo);
} else {
setSkillsInfo({ count: 0, skills: [] });
}
Na renderização, usar ?. para proteger todos os .map():
{ragStats.categories.map(cat => ...)}
{ragStats?.categories?.length > 0 && ragStats.categories.map(cat => ...)}
{skillsInfo?.skills?.length ? skillsInfo.skills.slice(0, 30).map(...) : null}
Em GuidPage.tsx:
if (!agentSelection.availableAgents?.length) return [];
Padrão geral: qualquer dado que venha do IPC bridge deve ser tratado como potencialmente null/undefined/wrong-shape — nunca confiar que vem no formato esperado.
Debug sem DevTools
Quando o utilizador não consegue abrir DevTools, aplicar todos os fixes acima cegamente (são as 3 causas mais comuns). Depois fazer push e rebuild — o DMG novo resolve o problema na esmagadora maioria dos casos.
Se ainda não resolver, perguntar ao utilizador:
Fluxo de debug cego (quando DevTools não está disponível)
isDesktopRuntime → forçar a true (causa mais provável de branco total)
- IPC handlers em falta → adicionar todos com fallback seguro
.map() sem guarda → adicionar ?. em todos os .map() de dados IPC
- Push → rebuild DMG via GitHub Actions workflow dispatch
- Download e teste
Este fluxo resolve ~90% dos casos de white screen em DMG.
14. launchd plist + KeepAlive causa restart loop
Sintoma: O BianinhoBridge ou RAG Server são iniciados múltiplas vezes por segundo via launchd, com logs "Iniciando BianinhoBridge HTTP Server" repetidos.
Causa: KeepAlive: true no plist + loop interno do Python server (que também tenta re-iciar em caso de falha). O launchd inicia novo processo, o processo anterior ainda está a correr, ambos competem pela porta.
Solução: NÃO usar KeepAlive: true com launchd para serviços que já têm restart logic interno. O BianinhoBridge já tem Restart=on-failure no código. Usar apenas RunAtLoad: true e deixar o serviço gerir os seus próprios restarts.
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
Nota: O RestartSec=10 em systemd (servidor) é diferente — lá o processo não tem restart logic interno, por isso o systemd faz o restart. No Mac com launchd, se o processo já tem restart intern, não adicionar KeepAlive.
15. GitHub clone lento/timeout → usar rsync do servidor
Sintoma: git clone --depth=1 do GitHub demora >60s e dá timeout no terminal.
Solução: Se o código já existir no servidor Linux, usar rsync direto:
rsync -avz --exclude='node_modules' --exclude='.git' --exclude='out' \
alvarobiano@100.79.189.95:~/repos/aionui-custom/ /tmp/aionui-mac/
16. bun em novo shell — path não persistido
Sintoma: bun: command not found após instalar.
Causa: curl -fsSL https://bun.sh/install | bash instala em ~/.bun/bin/ mas o PATH não é persistido entre sessões shell.
Solução: Sempre definir path manualmente:
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
19. Teams — Hermes não aparece como Team Leader (03/05/2026)
REGRA DE OURO: PERGUNTAR PRIMEIRO, INVESTIGAR DEPOIS
Quando algo não aparece na UI, SEMPRE perguntar ao utilizador o que ele vê ANTES de vasculhar código ou extrair ASARs.
O problema tinha DUAS causas raiz em localizações diferentes (03/05/2026):
- Main process (
out/main/index.js): KNOWN_TEAM_CAPABLE_BACKENDS já tinha 'hermes' ✓
- Renderer bundle (
out/renderer/assets/index-*.js): aAt = new Set([...]) NÃO tinha 'hermes' ✗
O Teams Leader dropdown usa o Set do renderer, não do main process. Por isso Hermes aparecia em New Chat (que usa main process data) mas não em Teams Leader.
Sintoma: Hermes detectado (3 agents), Hermes aparece em New Chat, mas Hermes NÃO aparece no Teams Leader dropdown.
Fix — duas localizações precisam de patch:
npx asar extract "/Applications/AionUi.app/Contents/Resources/app.asar" /tmp/aionui-patch
sed -i 's/gemini", "claude", "codex", "aionrs"/gemini", "claude", "codex", "aionrs", "hermes"/g' \
/tmp/aionui-patch/out/main/index.js
RENDERER_BUNDLE=$(ls /tmp/aionui-patch/out/renderer/assets/index-*.js | head -1)
sed -i 's/aAt=new Set(\["gemini","claude","codex","aionrs"\])/aAt=new Set(["gemini","claude","codex","aionrs","hermes"])/g' \
"$RENDERER_BUNDLE"
npx asar pack /tmp/aionui-patch "/Applications/AionUi.app/Contents/Resources/app.asar"
codesign -f -s - /Applications/AionUi.app
Nota: O aAt = new Set(...) no renderer é hardcoded no bundle compilado — não existe correspondência directa no source TypeScript. O KNOWN_TEAM_CAPABLE_BACKENDS em teamTypes.ts é para o main process, não para o renderer. O patch ao bundle é sempre necessário mesmo que o source TypeScript já tenha 'hermes'.
ALTERNATIVA: Patch à função isTeamCapableBackend (03/05/2026 noite)
Se o patch ao Set não funcionar, modificar directamente a função para forçar 'hermes' a passar:
npx asar extract "/Applications/AionUi.app/Contents/Resources/app.asar" /tmp/aion_hermes_fix
sed -i '' 's/if (KNOWN_TEAM_CAPABLE_BACKENDS.has(backend)) return true;/if (KNOWN_TEAM_CAPABLE_BACKENDS.has(backend) || backend === "hermes") return true;/g' \
/tmp/aion_hermes_fix/out/main/index.js
cd /tmp/aion_hermes_fix
npx asar pack out "/Applications/AionUi.app/Contents/Resources/app.asar"
codesign -f -s - /Applications/AionUi.app
Blindagem permanente: ver secção 23 (Auto-restore do patch após actualizações).
Blindagem permanente: ver secção 23 (Auto-restore do patch após actualizações).
Sessão: 03/05/2026
20. DMG app.asar patching — NUNCA rebuild, usar asar extract/pack
Problema: Para adicionar features ao AionUI DMG, a tentação é fazer bunx electron-vite build e re-pack. ISSO QUEBRA TUDO.
O que acontece:
- Rebuild cria
app.asar.unpacked/ com módulos nativos
better-sqlite3 rebuild falha com Node API incompatibility (Node v136 vs Electron v136)
- Mesmo que rebuild passe, o
app.asar.unpacked/ quebra a code signature do macOS
- App fica sem código válido → não abre
Solução CORRECTA — patch directo no ASAR:
npx asar extract "/Applications/AionUi.app/Contents/Resources/app.asar" /tmp/app-patched
sed -i 's/gemini", "claude", "codex", "aionrs"/gemini", "claude", "codex", "aionrs", "hermes"/' \
/tmp/app-patched/out/main/index.js
npx asar pack /tmp/app-patched "/Applications/AionUi.app/Contents/Resources/app.asar"
codesign -f -s - /Applications/AionUi.app
codesign -vv /Applications/AionUi.app
ATENÇÃO: app.asar.unpacked/ NUNCA deve existir no packaged app — cria-se durante o rebuild e quebra a assinatura. Se existir, apagar:
rm -rf "/Applications/AionUi.app/Contents/Resources/app.asar.unpacked"
Backup do ASAR original — SEMPRE fazer antes de patchar:
cp "/Applications/AionUi.app/Contents/Resources/app.asar" \
"/Applications/AionUi.app/Contents/Resources/app.asar.backup_before_$(date +%Y%m%d)"
Sessão: 03/05/2026
21. Code signing broken after app modification
Sintoma: App não abre após modificação — desaparece do Dock imediatamente.
Causa: Qualquer modificação no .app contents quebra a code signature do macOS (mesmo trocar um ficheiro dentro do .app).
Fix:
codesign -f -s - /Applications/AionUi.app
Verificação:
codesign -vv /Applications/AionUi.app
Sessão: 03/05/2026
22. Hermes symlink — PATH do Electron não inclui ~/.hermes/venv/bin
Problema: O Electron (AionUI) detecta agentes via command -v. O ~/.hermes/venv/bin não está no PATH do Electron.
Solução — symlink em ~/.local/bin (que já está no PATH do Electron):
mkdir -p ~/.local/bin
ln -sf ~/.hermes/venv/bin/hermes ~/.local/bin/hermes
zsh -c 'source /etc/zshrc; command -v hermes'
Sessão: 03/05/2026
Sintoma: App não abre após instalação via DMG — "não é possível abrir porque berasal de um developer não identificado".
Solução:
sudo xattr -rd com.apple.quarantine /Applications/AionUI.app
xattr -rd com.apple.quarantine /Applications/AionUI.app
Quando o app é instalado via DMG, os extraResources ficam dentro do .app:
/Applications/AionUI.app/Contents/Resources/
├── bianinho/ ← BianinhoBridge + scripts
├── hermes-source/ ← Hermes Agent (42MB)
└── app.asar
O main process precisa de os copiar para ~/Library/ApplicationSupport/AionUI/ no primeiro lançamento:
async function extractBianinhoResources(): Promise<void> {
if (process.platform !== 'darwin') return;
if (!app.isPackaged) return;
const appSupport = path.join(app.getPath('home'), 'Library/ApplicationSupport/AionUI');
const bianinhoDest = path.join(appSupport, 'bianinho');
const hermesDest = path.join(appSupport, 'hermes');
const resourceBianinho = path.join(process.resourcesPath!, 'bianinho');
const resourceHermes = path.join(process.resourcesPath!, 'hermes-source');
const markerFile = path.join(bianinhoDest, '.extracted');
if (fs.existsSync(markerFile)) return;
const copyDir = (src: string, dest: string) => {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src)) {
const srcPath = path.join(src, entry);
const destPath = path.join(dest, entry);
const stat = fs.statSync(srcPath);
if (stat.isDirectory()) copyDir(srcPath, destPath);
else fs.copyFileSync(srcPath, destPath);
}
};
copyDir(resourceBianinho, bianinhoDest);
copyDir(resourceHermes, hermesDest);
fs.writeFileSync(markerFile, new Date().toISOString());
if (fs.existsSync(path.join(bianinhoDest, 'hermes-launcher.sh')))
fs.chmodSync(path.join(bianinhoDest, 'hermes-launcher.sh'), 0o755);
}
19. extractBianinhoResources() — extracção de recursos no primeiro lançamento
gh CLI tem bug com Node 24 — qualquer comando dá:
TypeError: Cannot read properties of undefined (reading 'options')
at getAvailableArgsOnCmd
Alternativas quando gh está partido:
curl -s -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows"
nvm use 22 && gh workflow run build-dmg.yml --field arch=arm64
Token GitHub para API: está em ~/.netrc (machine github.com, password = token <GITHUB_PAT>). Pode estar inválido — "Bad credentials" se expirou.
armadilhas Descobertas (01/05/2026)
1. Ícones @icon-park/react — VERIFIED LIST v1.9.8
O build FALHA com "X does not exist" para ícones inexistentes. SEMPRE verificar antes de usar.
Verificar:
ls ~/repos/aionui-custom/node_modules/@icon-park/react/es/icons/ | grep -i "^NomeProcurado"
ÍCONES VERIFICADOS (usar estes):
| Categoria | Ícones |
|---|
| Acção | Plus, PlayOne, SaveOne, Delete, Download, Refresh, Edit, Block |
| UI | Check, Error, Signal, Timer, Trend, Power, Sync, Mute, Sound, HardDisk |
| Docs | FileText, FileAddition, FolderOpen, Tag, TagOne |
| Agent | Robot, Brain, MindMapping, Code, Terminal |
| Misc | Book, Info, PreviewOpen, TestTube, ManualGear, Flashlamp, Lightning, AlarmClock |
ÍCONES INEXISTENTES (não usar):
| NÃO usar | Substituto |
|---|
Trash | Block |
Settings / Gear | ManualGear |
Database | HardDisk |
CheckCircle | Check |
XCircle | Error |
Clock | AlarmClock ou Timer |
Flash | Flashlamp |
Energy | Lightning |
Notification | Signal |
Eye | PreviewOpen |
Edit2 | Edit |
FilePlus | FileAddition |
Play | PlayOne |
Save | SaveOne |
Rebuild IMEDIATAMENTE após adicionar ícone — erros de ícone param o build inteiro (10585 módulos).
2. Arco Design — TabPane não é export directo
import { Tabs, TabPane } from '@arco-design/web-react';
<TabPane key='1' title='Tab 1'><Content /></TabPane>
import { Tabs } from '@arco-design/web-react';
<Tabs.TabPane key='1' title='Tab 1'><Content /></Tabs.TabPane>
3. TCP Bridge —nc é mais fiável que socket Python
Socket Python directo com length-prefix tem problemas de buffering no recv(). O nc funciona sempre:
import subprocess, json
def send_cmd(cmd, host='127.0.0.1', port=18743):
payload = json.dumps(cmd)
length_bytes = len(payload).to_bytes(4, 'big')
p = subprocess.run(
f'echo -n "{length_bytes.decode("latin-1")}{payload}" | nc -N {host} {port}',
shell=True, capture_output=True, timeout=5
)
return p.stdout.decode()
4. IPC Bridge providers — não basta handlers IPC
Além dos handlers ipcMain.handle() em bianinhoBridge.ts, adicionar providers em src/common/adapter/ipcBridge.ts:
bianinho: {
ping: () => invoke('bianinho.ping'),
status: () => invoke('bianinho.status'),
checkHermes: () => invoke('bianinho.checkHermes'),
listSkills: () => invoke('bianinho.listSkills'),
},
5. registerBianinhoBridge() dentro de handleAppReady()
Importar ao nível do módulo funciona para handlers auto-registados, mas registerBianinhoBridge() precisa de ser chamado dentro de handleAppReady() no src/index.ts.
6. Python — funções definidas ANTES de classes que as usam
class SkillsSandbox:
def __init__(self):
self.hermes_path = detect_hermes_path()
skills_sandbox = SkillsSandbox()
def detect_hermes_path():
...
def detect_hermes_path():
...
class SkillsSandbox:
def __init__(self):
self.hermes_path = detect_hermes_path()
skills_sandbox = SkillsSandbox()
7. Subagentes — Gap UI vs Python Bridge
A implementacao TypeScript (BianinhoPage tab "Subagentes") cria interfaces e mock data em memória, mas os comandos Python correspondentes (subagent_list, subagent_create, etc.) não foram adicionados ao scripts/bianinho_bridge.py.
Para completar a feature: adicionar handlers em scripts/bianinho_bridge.py que persistem em ~/.hermes/config/subagents.json.
8. Build — corre SEMPRE em background
bunx electron-vite build demora ~40s e o foreground timeout é 60s — às vezes passa, às vezes não. Para evitar surpresas:
terminal(command="bunx electron-vite build", background=true, notify_on_complete=true)
process(action="wait", session_id="...procid...", timeout=90)
Iterações typicas até build OK: 3-5 (cada vez um ícone diferente corrigido).
9. Delegação — max 3 concurrent + verificação de outputs
delegate_task com max_concurrent_children=3. Se enviar 5 tasks, 2 ficam pendentes. Dividir em batches de 3.
IMPORANTE — Verificar outputs após subagente terminar:
Subagentes podem completar mas criar ficheiros com erros (ícones inexistentes, imports wrong) ou criar ficheiros fora do path esperado. SEMPRE fazer após cada batch:
git status --short
grep -rn "Trash\|Settings\|Gear\|Database\|CheckCircle" src/
Iterações típicas até build OK após trabalho de subagente: 3-5 (cada vez um ícone diferente corrigido).
10. Ícones — VERIFICAR ANTES de usar no prompt do subagente
Ao delegar para subagente, incluir na lista de contexto os ícones VERIFICADOS e os que NÃO existem. Erro comum: subagente usa Edit2, Trash, Eye sem saber que não existem.
ÍCONES VERIFICADOS (usar):
Plus, PlayOne, SaveOne, Delete, Download, Refresh, Edit, Block, Check, Error,
Signal, Timer, Trend, Power, Sync, Mute, Sound, HardDisk, FileText,
FileAddition, FolderOpen, Tag, TagOne, Robot, Brain, MindMapping, Code,
Terminal, Book, Info, PreviewOpen, TestTube, ManualGear, Flashlamp,
Lightning, AlarmClock
NÃO USAR (vão quebrar o build):
Trash→Block, Settings/Gear→ManualGear, Database→HardDisk, CheckCircle→Check,
XCircle→Error, Clock→AlarmClock/Timer, Flash→Flashlamp, Energy→Lightning,
Notification→Signal, Eye→PreviewOpen, Edit2→Edit, FilePlus→FileAddition,
Play→PlayOne, Save→SaveOne
Se o subagente usar ícone wrong, corrigir com sed após, mas idealmente evitar o ciclo.
11. GitHub Actions — Build DMG manual via workflow_dispatch
O repo tem .github/workflows/build-dmg.yml que pode ser accionado manualmente para gerar .dmg para Mac:
- Ir a https://github.com/AlvaroBiano/AionUi/actions → "Build DMG (macOS)"
- Clicar "Run workflow" → escolher
arm64 (Apple Silicon) ou x64 (Intel)
- Aguardar ~10-15 min → artifact
.dmg disponível para download (7 dias)
Architecture choices no workflow: arm64, x64, ou universal (ambos).
12. gh release upload — nunca fazer ANTES do download do artifact terminar
O gh api .../zip > file.zip demora ~40s para um artifact de 257MB. Se faz gh release upload antes do download terminar, o zip fica incompleto (4KB em vez de 257MB) e o utilizador recebe um ficheirocorrupto.
Sequência CORRECTA:
gh workflow run build-dmg.yml --field arch=arm64 --repo AlvaroBiano/AionUi
gh api repos/AlvaroBiano/AionUi/actions/runs --jq '.workflow_runs[0].id'
gh api repos/AlvaroBiano/AionUi/actions/runs/$RUN_ID/artifacts --jq '.artifacts[0].id'
curl -L -H "Authorization: token $TOKEN" \
-o /tmp/AionUi-new.dmg.zip \
"https://api.github.com/repos/.../actions/artifacts/$ARTIFACT_ID/zip" &
DOWNLOAD_PID=$!
wait $DOWNLOAD_PID
ls -lh /tmp/AionUi-new.dmg.zip
gh release create v1.9.24-fix4 --repo AlvaroBiano/AionUi --notes "..."
gh release upload v1.9.24-fix4 /tmp/AionUi-new.dmg.zip --repo AlvaroBiano/AionUi
O erro clássico: criar o release, fazer upload, e só depois perceber que o zip está mal. Remedy: verificar du -h do ficheiro antes de fazer upload — tem de ter o tamanho correcto (≈257MB para DMG).
14. AcpDetector — regex filtra paths absolutos (Hermes nunca detectado)
Sintoma: Hermes Agent nunca aparece no picker do AionUI, sem erro visível.
Causa: O AcpDetector.ts usa regex /^[a-zA-Z0-9_.-]+$/ para filtrar comandos antes de os enviar para command -v. Esta regex rejeita qualquer string contendo / — logo, paths absolutos como /Users/alvarobiano/.hermes/venv/bin/hermes são silenciosamente descartados.
Fix em src/process/agent/acp/AcpDetector.ts (linha ~56):
cmd => /^[a-zA-Z0-9_.-]+$/
cmd => /^[a-zA-Z0-9_./-]+$/
Nota: O cliCommand em acpTypes.ts configura apenas o nome do comando (hermes), não o path absoluto. O AcpDetector faz command -v com base no PATH do sistema — por isso o fix à regex é suficiente para permitir paths absolutos descobertos via PATH.
Sessão: 03/05/2026 — Hermes Agent露出了 no picker após patch.
15. better-sqlite3 rebuild para Electron (Node v136)
Sintoma: App não inicia após install — native module not compiled for this Electron version.
Causa: better-sqlite3 é um módulo nativo. O build de npm não sabe que vai correr dentro do Electron (Node v136), não do Node do sistema.
Fix:
npm rebuild better-sqlite3 --runtime=electron --target=37.10.3
Quando aplicar: Sempre que o app tem módulos nativos e o build é para packaged Electron (DMG/appImage). O target version deve coincidir com a versão do Electron usada no package.json.
Sessão: 03/05/2026 — rebuild necessário para Electron 37.10.3 no AionUI DMG.
16. gh CLI broken com Node 24 — alternativas
Sintoma: gh workflow run ou qualquer comando gh dá:
TypeError: Cannot read properties of undefined (reading 'options')
at getAvailableArgsOnCmd
Causa: gh CLI no sistema usa Node 24 internamente, que tem breaking changes.
Alternativas (por ordem de preferência):
- API REST via Python urllib — sempre funciona, não depende de gh CLI
- NVM —
nvm use 22 && gh workflow run ...
- UI web — accionar manualmente em https://github.com/AlvaroBiano/AionUi/actions
import urllib.request, json, re
with open(os.path.expanduser('~/.netrc')) as f:
token = re.search(r'password\s+(\S+)', f.read()).group(1)
headers = {'Authorization': f'token {token}', 'Accept': 'application/vnd.github.v3+json'}
req = urllib.request.Request(
'https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows',
headers=headers
)
with urllib.request.urlopen(req) as r:
wf_id = next(w['id'] for w in json.loads(r.read())['workflows'] if 'dmg' in w['name'].lower())
dispatch_req = urllib.request.Request(
f'https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows/{wf_id}/dispatches',
method='POST', data=json.dumps({'ref': 'main'}).encode(), headers=headers
)
urllib.request.urlopen(dispatch_req)
Sessão: 03/05/2026 — gh broken, dispatch via Python API REST.
17. Bundle — lancedb é o package mais pesado do venv
Na Opção Completa (bundle para Mac), o lancedb sozinho pesa ~362 MB dentro do venv. Isto é 95% do peso do venv:
| venv | Tamanho |
|---|
| Sem lancedb | 19 MB |
| Com lancedb | 382 MB |
Se o bundle precisar de ser mais pequeno, remover lancedb do pip install em export-lean-bundle.sh. Sem lancedb, o bundle fica ~350 MB mas o RAG local não funciona — pesquisas vão ao servidor.
Opção A — TUDO Local no Mac (Opção Completa)
O fork AlvaroBiano/AionUi suporta instalação 100% local no Mac — sem dependência do servidor Linux.
Arquitectura
DMG (~200MB) ──first-run──> Lean Bundle (~900MB) ──> TUDO LOCAL
│
┌────────────────────┴────────────────────┐
│ │
Hermes Agent Knowledge Base
+ BianinhoBridge (~1.1GB)
+ Skills + 65k chunks
+ Lean venv
(~382MB)
Tamanhos Reais (medidos em 01/05/2026)
| Componente | Tamanho real | Comprimido |
|---|
| Hermes source | 1.1 MB | 1 MB |
| Lean venv (c/ lancedb) | 382 MB | ~100 MB |
| Lean venv (s/ lancedb) | 19 MB | 5 MB |
| Knowledge Base | 1.1 GB | ~300 MB |
| Skills | 16 MB | 5 MB |
| Bundle COMPLETO (c/ lancedb) | ~1.5 GB | 724 MB |
| Bundle LEAN (s/ lancedb) | ~1.1 GB | ~350 MB |
Nota: O package mais pesado do venv é lancedb (362 MB). Sem lancedb, o venv é só 19 MB — mas sem lancedb não há RAG local.
Modos de Bundle
Lean (~350MB): UI + Hermes + Bridge + Skills. RAG vai ao servidor (precisa de rede). Mais rápido de transferir.
Completo (~900MB): UI + Hermes + KB + Skills + lancedb. 100% offline. Sem dependência do servidor.
Fluxo Completo — Opção Completa
1. No servidor — criar e servir bundle:
cd ~/repos/aionui-custom
bash scripts/export-lean-bundle.sh both
2. No MacBook — instalar AionUI:
curl -fsSL https://raw.githubusercontent.com/AlvaroBiano/AionUi/main/scripts/install.sh | bash
3. No MacBook — descarregar bundle (~724MB):
curl -o ~/Downloads/bianinho-lean.tar.gz http://IP_SERVIDOR:8878/download
4. No MacBook — extrair e setup completo:
mkdir -p ~/Library/ApplicationSupport/AionUI
tar -xzf ~/Downloads/bianinho-lean.tar.gz \
-C ~/Library/ApplicationSupport/
bash ~/AionUI-Bianinho/scripts/setup-complete.sh
Scripts de Transferência
| Script | Corre em | Função |
|---|
export-lean-bundle.sh | Servidor | Cria bundle + serve HTTP (porta 8878) |
serve-kb.sh | Servidor | Serve só a KB (~600MB) via HTTP |
export-mac-bundle.sh | Servidor | Bundle antigo (full venv 1.8GB) |
setup-complete.sh | Mac | Setup completo (API key + start services) |
setup-mac.sh | Mac | Setup interactivo (KB, API key, servidor) |
install.sh | Mac | Instalador (clone + deps + build) |
Nota Importante
O bundle (~724MB) não cabe num release GitHub como asset. O fluxo é sempre:
- DMG pequeno instala o app (~200MB)
- First-run descarga bundle (~724MB) do servidor
Se o Mac e servidor estiverem na mesma rede local, a transferência demora ~3-5 min (100Mbps). Por USB é mais rápido.
DMG build: O workflow build-dmg.yml acciona-se manualmente em:
https://github.com/AlvaroBiano/AionUi/actions/workflows/build-dmg.yml → Run workflow → escolher arm64.
O gh CLI tem bug com Node 24 (gh workflow run fails) — usar a UI web ou API REST se necessário.
23. Hermes blindagem permanente — auto-restore do patch após actualizações (03/05/2026)
O problema: O patch ao app.asar é perdido quando o AionUI é actualizado via novo DMG/instalador.
Arquitectura da solução:
~/Library/Application Support/hermes/aionui-hermes-patch/
├── app_backup.asar ← app.asar JÁ PATCHED (backup permanente)
├── renderer/assets/
│ └── index-CVd-wlNn.js ← renderer bundle PATCHED (copia permanente)
├── main/teamTypes.ts ← source TypeScript PATCHED (para rebuilds futuros)
└── restore_hermes_patch.sh ← script de reparação automática
LaunchAgent (~/Library/LaunchAgents/com.bianinho.aionui-hermes-patch.plist):
- Corre a cada 5 minutos (StartInterval: 300)
- Também corre ao fazer login (RunAtLoad: true)
- Detecta se o
app.asar perdeu o patch e re-aplica automaticamente
Como funciona o restore script:
grep -q 'aAt=new Set(\["gemini","claude","codex","aionrs","hermes"\])' bundle.js
npx asar extract "$ASAR_PATH" /tmp/restore$$
sed -i 's/aAt=new Set(\["gemini","claude","codex","aionrs"\])/aAt=new Set(["gemini","claude","codex","aionrs","hermes"])/g' \
/tmp/restore$$/out/renderer/assets/index-*.js
npx asar pack /tmp/restore$$ "$ASAR_PATH"
codesign -f -s - /Applications/AionUi.app
Verificação do LaunchAgent:
launchctl print gui/$UID/com.bianinho.aionui-hermes-patch
Se o patch precisar de ser forçado:
bash ~/Library/Application\ Support/hermes/aionui-hermes-patch/restore_hermes_patch.sh
launchctl start com.bianinho.aionui-hermes-patch
Sessão: 03/05/2026
Referências
references/aionui-hermes-blindagem.md — Patch blindado do Hermes: dual-location fix (main+renderer) + launchd auto-restore + restore script completo (03/05/2026)
references/aionhub-ecosystem.md — AionHub ecosystem: extensions, agents marketplace, CLI tools gratuitas (Goose + OpenClaw) (03/05/2026)
references/aionui-dmg-patch-03052026.md — DMG patching: rebuild breaks native modules + signing, extract/pack works
references/teams-mcp-architecture.md — Teams MCP: arquitectura, troubleshooting EACCES permission, debugging
references/aionui-teams-hermes-limitation.md — Teams: Hermes não aparece como Team Leader — root cause, workarounds