Imported from AlvaroBiano/bianinho-backup-1777760438 (
skills/devops/aionui/SKILL.md). Install upstream withnpx skills add AlvaroBiano/bianinho-backup-1777760438 --skill aionui. Copyright stays with the author.
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:
# 1. O que está a correr
ps aux | grep -E "aionui|AionUi|Xvfb" | grep -v grep
# 2. Portas activas
ss -tlnp | grep -E "8765|18743|9223"
# 3. Ficheiros no directorio
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.
- Repo upstream: https://github.com/iOfficeAI/AionUi (Apache-2.0)
- Fork Álvaro: https://github.com/AlvaroBiano/AionUi
- Repo local fork:
~/repos/aionui-custom - Repo local original:
~/repos/aionui(binário Electron + web viewer)
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
# Depois abre no browser: http://localhost:8765
Modo 2: DMG no Mac (Electron nativo)
O AionUI corre como app Electron nativa 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 tunnel do Mac para o servidor
ssh -L 8765:localhost:8765 alvarobiano@alvarobiano-linuxmint.taile2fd75.ts.net
# Abre no browser: http://localhost:8765
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
# Depois: curl http://localhost:8765 para confirmar
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
# Bun necessário (não vem pré-instalado)
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"
# Instalar deps
cd ~/repos/aionui-custom
npm install --prefer-offline
# Build TypeScript → out/ (USA SEMPRE ESTE)
bunx electron-vite build
# Saída: out/main/, out/preload/, out/renderer/
# Push para fork
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 usarbunx 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';
// handlers com ipcMain.handle('nome.metodo', async () => { ... })
- Importar em
src/index.tsdentro dehandleAppReady():
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
Preferências de Comunicação do Álvaro
- Respostas curtas e directas — sem explicações longas, sem "deixa-me ver", sem rodeios
- 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)
| Componente | Estado |
|---|---|
| BianinhoBridge HTTP Server | ✅ Funcional em 100.79.189.95:18743 |
| systemd service | ✅ activo |
| Tailscale | ✅ ambos dispositivos ligados |
| AionUI Xvfb + Web Viewer (8765) | ✅ Funcional no servidor |
| cloudflared named tunnel | ✅ d532ba47-1f57-4c2d-8622-011a259b5a90 |
| AionUI URL pública | ✅ https://aionui.masterclasslife.com.br |
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:
# Servidor: túnel a correr?
ps aux | grep cloudflared | grep tunnel | grep -v grep
# Servidor: AionUI a responder?
curl -s --connect-timeout 5 http://localhost:8765/ -o /dev/null -w "%{http_code}"
# MacBook: acesso externo?
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.devDNS 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 emhttps://*.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
# Confirma: /usr/bin/python3 -c "import websocket; print('OK')"
# O web viewer precisa de correr com /usr/bin/python3:
/usr/bin/python3 ~/repos/aionui/aionui-web-viewer.py
Regra de Ouro: Clarificar Objectivo Antes de Agir
Quando o Álvaro diz "vamos recomeçar do zero", significa que eu passei demasiado tempo numa abordagem errada sem confirmar o que ele realmente queria. Antes de gastar mais de 5 minutos numa tarefa, confirmar:
- "Qual é o resultado final que procuras?"
- "Estás a falar do AionUI no servidor ou do DMG no Mac?"
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:
# No servidor — ver se está a correr
ss -tlnp | grep 18743
systemctl --user status bianinho-bridge-server
# Logs
journalctl --user-unit bianinho-bridge-server -n 20
# Reiniciar
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');
// ... response handling ...
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
# Recebe: \x00\x00\x00\x1d{"ok":true,"pong":"test","platform":"linux"}
Erro comum: shutdown(SHUT_WR) quebra
# ERRADO
sock.send(payload)
sock.shutdown(socket.SHUT_WR) # ❌ — broken pipe
resp = sock.recv(...)
# CORRECTO — sem shutdown, length prefix determina quando parar
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 };
}
// React 19 — APENAS getDerivedStateFromError. componentDidCatch NÃO funciona.
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { hasError: true, error };
}
// NÃO USAR componentDidCatch em React 19 — está break. getDerivedStateFromError é suficiente.
// componentDidCatch foi deprecated em React 16 e não é lifecycle seguro em React 18+.
// O state update de errorInfo é opcional para debug — stack já está em error.stack.
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)retornafalseno DMG → app bloqueia emchecking- 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 resultdo IPC pode sernull→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
# gh CLI tem bug com Node 24 — usar API REST via Python urllib
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'}
# Obter workflow ID
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 (build-dmg.yml TEM APENAS workflow_dispatch — não corre no push)
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) # 204 = OK
# Aguardar ~8-12 min, depois:
# Artifact URL: https://github.com/{org}/{repo}/releases/download/{tag}/{filename}
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 # obter do API (gh api repos/.../actions/runs/.../artifacts --jq '.artifacts[0].id')
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
# Criar tag + release
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"
# Upload DMG
gh release upload v1.9.24-fix2 /tmp/AionUi-1.9.24-mac-arm64.dmg --repo AlvaroBiano/AionUi
# URL do asset: https://github.com/AlvaroBiano/AionUi/releases/download/v1.9.24-fix2/AionUi-1.9.24-mac-arm64.dmg
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:
// Substituir:
const isDesktopRuntime = typeof window !== 'undefined' && Boolean(window.electronAPI);
// Por:
const isDesktopRuntime = true; // Desktop nunca precisa de servidor externo
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:
// Handler em bianinhoBridge.ts — SEMPRE com fallback
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: [] } }; // fallback seguro
});
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: [] }; // fallback seguro
});
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 ?..
// fetchSkills — validar antes de guardar na state
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 ?. em vez de acesso directo
{ragStats?.categories?.length > 0 && ragStats.categories.map(cat => ...)}
{skillsInfo?.skills?.length ? skillsInfo.skills.map(...) : null}
// Em GuidPage.tsx — guard adicional
if (!agentSelection.availableAgents?.length) return [];
PASSO 3 — Push + Rebuild DMG
# 1. Commit
git add -A && git commit -m "fix: white screen" && git push alvaro main
# 2. Dispatch build via API REST (gh CLI tem bug com Node 24)
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'}
# Obter workflow ID
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 (204 = OK)
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)
# 3. Aguardar ~8-12 min, depois obter artifact
# Artifact URL: https://github.com/{org}/{repo}/releases/download/{tag}/{filename}
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):
# Obter artifact ID primeiro
gh api repos/AlvaroBiano/AionUi/actions/runs/25252230954/artifacts --jq '.artifacts[0] | "\(.id) \(.name)"'
# Output: 6763103414 AionUI-Bianinho-arm64.dmg
# Download — gh api stdout redirect é suficiente
ARTIFACT_ID=6763103414
gh api repos/AlvaroBiano/AionUi/actions/artifacts/$ARTIFACT_ID/zip > /tmp/AionUi.dmg.zip
# GitHub sempre zipa artifacts — o .zip contém o .dmg
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
# URL: https://github.com/AlvaroBiano/AionUi/releases/download/v1.9.24-fix2/AionUi-1.9.24-mac-arm64.dmg
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 + Ino 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):
// useTeamList.ts — fetcher defensivo NO SWR
const { data: teams = [], mutate } = useSWR<TTeam[]>(
`teams/${userId}`,
() => ipcBridge.team.list.invoke({ userId }).then((data) => {
// Defensive: IPC pode retornar {} ou null em vez de array
if (Array.isArray(data)) return data;
console.warn('[useTeamList] IPC returned non-array:', data);
return [];
}),
{ revalidateOnFocus: false }
);
// useSiderTeamBadges.ts — DOUBLE defence
export function useSiderTeamBadges(teams: TTeam[] | undefined): Map<string, number> {
// 1. Array.isArray é mais seguro que ?? — pega {} e 'string' também
const safeTeams: TTeam[] = Array.isArray(teams) ? teams : [];
const initCounts = (): Map<string, number> => {
for (const team of safeTeams) { ... } // safeTeams, não teams
};
useEffect(() => {
for (const team of safeTeams) {
for (const agent of team.agents ?? []) { ... } // ?? [] dentro do loop
}
}, [
// safeTeams em vez de teams — não optional chaining ?.
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未知):
# 1. Extrair DMG
cd /tmp && 7z x AionUi-1.9.24-mac-arm64.dmg -o/tmp/dmg-contents -y
# 2. Instalar asar (7z não consegue ler ASAR directamente)
cd /tmp && npm install asar --no-save
# 3. Extrair ASAR
node -e "require('asar').extractAll('/path/to/app.asar', '/tmp/asar-extracted')"
# 4. Encontrar bundle com erro (o hash do ficheiro aparece no stack trace do utilizador)
find /tmp/asar-extracted -name "index-*.js" | head -5
# 5. Analisar com Python — procurar .map calls na zona do crash
python3 - << 'PYEOF'
with open('/tmp/asar-extracted/out/renderer/assets/index-B7FSMILK.js') as f:
content = f.read()
lines = content.split('\n')
# Mostrar contexto da linha do crash (número vem do stack trace)
for i in range(7559, 7570):
print(f"{i+1}: {lines[i][:200]}")
# Procurar r.agents.map no bundle — identifica o hook exacto
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):
// ❌ ANTES — assume teams sempre array (SWR pode retornar undefined)
export function useSiderTeamBadges(teams: TTeam[]): Map<string, number> {
const initCounts = () => {
for (const team of teams) // CRASH: teams = undefined
map.set(team.id, readStorage(team.id));
return map;
};
useEffect(() => {
for (const team of teams) // CRASH: teams = undefined
for (const agent of team.agents) // CRASH: agents = undefined
...;
}, [teams.map(...)]) // CRASH: teams = undefined
}
// ✅ DEPOIS — defesa completa em 3 sítios
export function useSiderTeamBadges(teams: TTeam[] | undefined): Map<string, number> {
const safeTeams: TTeam[] = teams ?? []; // ← GARANTIA desde o início do hook
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:
// Substituir:
const isDesktopRuntime = typeof window !== 'undefined' && Boolean(window.electronAPI);
// Por:
const isDesktopRuntime = true; // Desktop nunca precisa de servidor externo
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:
// Handler em bianinhoBridge.ts — SEMPRE com fallback
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: [] } }; // fallback seguro
});
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: [] }; // fallback seguro
});
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:
// ANTES (crasha se bridge retornar null)
const result = await ipcBridge.bianinho.listSkills.invoke();
setSkillsInfo(result); // null → crash no .map()
// DEPOIS (defensivo)
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 => ...)}
//Também seguro
{skillsInfo?.skills?.length ? skillsInfo.skills.slice(0, 30).map(...) : null}
Em GuidPage.tsx:
// Guard extra para availableAgents ser array
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:
- O ecrã mostra algum conteúdo (mesmo que parcialmente)?
- Há algum log no Terminal se o app for aberto por linha de comando?
/Applications/AionUI.app/Contents/MacOS/AionUI --no-sandbox 2>&1 | head -50
Fluxo de debug cego (quando DevTools não está disponível)
isDesktopRuntime→ forçar atrue(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. extractBianinhoResources() — extracção de recursos no primeiro lançamento
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:
// src/index.ts — dentro de handleAppReady(), ANTES de registerBianinhoBridge()
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; // já extraiu
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);
}
15. gh CLI Broken com Node 24
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:
# gh auth login --with-token (requer token válido)
# gh workflow run (falha)
# gh run list (falha)
# Opção 1: API REST via curl
curl -s -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/AlvaroBiano/AionUi/actions/workflows"
# Opção 2: gh via nvm (trocar versão do Node)
nvm use 22 && gh workflow run build-dmg.yml --field arch=arm64
# Opção 3: Trigger manual na web
# https://github.com/AlvaroBiano/AionUi/actions/workflows/build-dmg.yml → Run workflow
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
// ERRADO
import { Tabs, TabPane } from '@arco-design/web-react';
<TabPane key='1' title='Tab 1'><Content /></TabPane>
// CORRECTO
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:
# Abordagem que funciona — subprocess + nc
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'),
// etc.
},
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
# ERRADO
class SkillsSandbox:
def __init__(self):
self.hermes_path = detect_hermes_path() # NameError!
skills_sandbox = SkillsSandbox()
def detect_hermes_path():
...
# CORRECTO
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:
# Usar background=true — sempre
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:
# 1. Verificar ficheiros criados
git status --short
# 2. Se Build falhar com erro de ícone, identificar e corrigir
grep -rn "Trash\|Settings\|Gear\|Database\|CheckCircle" src/
# Corrigir com sed, ex: sed -i 's/Trash/Block/g' src/file.tsx
# 3. Rebuild
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) oux64(Intel) - Aguardar ~10-15 min → artifact
.dmgdisponí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:
# 1. Dispatch build
gh workflow run build-dmg.yml --field arch=arm64 --repo AlvaroBiano/AionUi
# 2. Obter run ID + artifact ID (polling até status=completed)
gh api repos/AlvaroBiano/AionUi/actions/runs --jq '.workflow_runs[0].id' # run ID
gh api repos/AlvaroBiano/AionUi/actions/runs/$RUN_ID/artifacts --jq '.artifacts[0].id' # artifact ID
# 3. Download em background
curl -L -H "Authorization: token $TOKEN" \
-o /tmp/AionUi-new.dmg.zip \
"https://api.github.com/repos/.../actions/artifacts/$ARTIFACT_ID/zip" &
DOWNLOAD_PID=$!
# 4. wait do PID — NÃO fazer release upload até download terminar
wait $DOWNLOAD_PID
ls -lh /tmp/AionUi-new.dmg.zip # confirmar ~257MB
# 5. Só agora criar release e upload
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).
13. 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
*Truncated - read the full file at https://github.com/AlvaroBiano/bianinho-backup-1777760438/blob/3edfe9544d8d8bfbc664d5d3faeb310909cc9cf2/skills/devops/aionui/SKILL.md.*