Android UI Inspection (Koder Konsul-first)
Em sessão com Android conectado via ADB, preferir Koder Konsul (HTTP API em 127.0.0.1:9876 após `adb forward tcp:9876 tcp:9876`) a screenshots crus. ~10× menos tokens, bounds/IDs/estado exatos do OS. Cair pra screencap só em casos visuais (ícone, cor, canvas, FLAG_SECURE).
Em qualquer sessão com dispositivo Android conectado via ADB, preferir o Koder Konsul a screenshots crus para inspecionar a UI. Aplicável a qualquer agente de IA (Claude Code, Codex, Gemini CLI, etc.) que opere no monorepo Koder.
Por quê
Vision custa ~10× mais tokens que JSON da accessibility tree. Eye dá:
- Bounds exatos (
[x, y, w, h]) — screenshot força adivinhar coordenadas - View IDs (
dev.koder.konsul:id/start_btn) — invisíveis em pixel - Estado direto do OS:
enabled,clickable,checked,focused - Texto exato de TextViewButtonEditText/Switch — sem erros de OCR
- Diff estrutural entre snapshots — duas screenshots podem parecer idênticas e diferir em estado
- Long-poll de eventos (
/events?wait=10000) — sem polling de screencap
Custo por inspeção:
| Ação | Tokens |
|---|---|
| Screenshot enviado ao chat | ≈1500 (image) |
GET /screen/text |
50–500 (text) |
GET /screen/buttons |
100–400 (text) |
GET /screen/diff |
50–800 (text, ≈0 quando nada mudou) |
10 screenshots ≈ 15 000 tokens vs. 10 queries Eye ≈ 2 000 tokens.
Setup obrigatório por sessão
Fazer antes de declarar Eye indisponível:
adb -s <serial> forward tcp:9876 tcp:9876
curl -s localhost:9876/healthz # endpoint é /healthz, NÃO /health
# Esperado: {"ok":true,"version":"...","accessibility":true,"media_projection":false}Se /healthz responder, Eye está pronto.
Se conexão for recusada após forward, o servidor HTTP do Eye está parado — é o estado default. Eye é desenhado pra exigir consent explícito antes do servidor ouvir em :9876.
Eye ≥ v0.4 (post-eye#036): use o broadcast — um único comando, sem Activity launch:
adb -s <serial> shell am broadcast \
-a dev.koder.konsul.action.START_SERVER \
-p dev.koder.konsul
sleep 1
curl -s localhost:9876/healthz
# => {"ok":true,"version":"...","accessibility":true,"media_projection":false}O receiver é gated por android.permission.DUMP — disponível pra ADB shell por default, indisponível pra apps regulares. Sem auth bypass.
NUNCA use
monkeypara abrir/resumir um app (iris#057). O comandomonkey -p <pkg> -c android.intent.category.LAUNCHER 1religaaccelerometer_rotationdo device como efeito colateral (reproduzido no SM-S948B, 2026-06-08 e 2026-06-20). Para abrir/resumir um app use sempream start -n <pkg>/<activity>(oucmd activity start) — inócuo à rotação, assim comouiautomator dump/screencap. Se alguma ferramenta precisar mexer em rotação, salve e restaure ao fim:R=$(adb shell settings get system accelerometer_rotation); …; adb shell settings put system accelerometer_rotation $R.
Eye ≤ v0.3 (releases antigos sem o receiver): fallback é tap manual via uiautomator dump + input tap:
# 1. Launch
adb -s <serial> shell am start -n dev.koder.konsul/.MainActivity
sleep 2
# 2. Discover the "Iniciar Servidor" button bounds (dump + rm no mesmo comando — §Higiene de arquivo)
BOUNDS=$(adb -s <serial> shell 'uiautomator dump /sdcard/ui.xml >/dev/null && cat /sdcard/ui.xml; rm -f /sdcard/ui.xml' | grep -oE 'text="Iniciar Servidor"[^>]*bounds="\[[0-9,]+\]\[[0-9,]+\]"' | grep -oE '\[[0-9,]+\]\[[0-9,]+\]')
# BOUNDS looks like [395,1131][686,1191]
# Compute center: x = (395+686)/2 = 540, y = (1131+1191)/2 = 1161
# 3. Tap center
adb -s <serial> shell input tap 540 1161
# 4. Verify
sleep 2
curl -s localhost:9876/healthzDetect which path applies (broadcast vs tap) por probe:
# Try the broadcast first — fails silent on old Eye
adb -s <serial> shell am broadcast -a dev.koder.konsul.action.START_SERVER -p dev.koder.konsul 2>&1 | head -1
# If "Broadcast completed: result=0", it worked. Else fall back to the tap recipe.Se Eye não está habilitado (One UI Auto-blocker desativa apps recém-instalados; dumpsys package dev.koder.konsul | grep enabled= retorna enabled=0):
adb -s <serial> shell pm enable dev.koder.konsul
adb -s <serial> shell pm grant dev.koder.konsul android.permission.POST_NOTIFICATIONSSe accessibility service não está bound (/healthz retorna "accessibility":false):
EXISTING=$(adb -s <serial> shell settings get secure enabled_accessibility_services | tr -d '\r')
adb -s <serial> shell settings put secure enabled_accessibility_services \
"${EXISTING}:dev.koder.konsul/dev.koder.konsul.KonsulAccessibilityService"
adb -s <serial> shell settings put secure accessibility_enabled 1Classe correta =
KonsulAccessibilityService(nãoEyeAccessibilityService, resíduo do rename Eye→Iris — iris#053). A receita antiga bindava o componente errado.
Higiene de estado do device — snapshot → restore (OBRIGATÓRIO)
O fluxo de acesso ADB muta settings do device do owner (serviço de acessibilidade acima; e empiricamente a Rotação Automática accelerometer_rotation aparece habilitada após o acesso — owner reportou 2026-06-10). Regra: o device do owner deve voltar exatamente ao estado anterior ao acesso. Antes de tocar no device, fazer snapshot dos settings que o fluxo altera; ao terminar (mesmo em erro — use trap), restaurar.
# --- SNAPSHOT (antes de qualquer ação no device) ---
S=<serial>
ROT0=$(adb -s $S shell settings get system accelerometer_rotation | tr -d '\r')
TMO0=$(adb -s $S shell settings get system screen_off_timeout | tr -d '\r')
A11Y0=$(adb -s $S shell settings get secure enabled_accessibility_services | tr -d '\r')
A11YEN0=$(adb -s $S shell settings get secure accessibility_enabled | tr -d '\r')
# --- RESTORE (ao final — armar com trap pra cobrir falhas) ---
restore_device() {
[ "$ROT0" = null ] || adb -s $S shell settings put system accelerometer_rotation "$ROT0"
[ "$TMO0" = null ] || adb -s $S shell settings put system screen_off_timeout "$TMO0"
adb -s $S shell settings put secure enabled_accessibility_services "$A11Y0"
adb -s $S shell settings put secure accessibility_enabled "$A11YEN0"
# Higiene de ARQUIVO (R5 de scratch-location.kmd): remover qualquer screencap/
# dump de IA vazado pra raiz do /sdcard — scratch NUNCA fica no device do owner.
adb -s $S shell 'rm -f /sdcard