* feat(android): settled permission verdict details Port the web PermissionFooter verdict classification so a settled permission row explains HOW a request was decided, not just that it was. Behavior vs web (web/src/components/ToolCard/PermissionFooter.tsx:11-63): - codex family sessions (flavor family + Codex/Gemini/OpenCode/Copilot/ Cursor tool prefixes): approved + decision approved_for_session shows "Approved for session"; denied + decision abort shows "Aborted" with the wire reason appended when present. - claude sessions: mode acceptEdits shows "Approved: allow all edits" (previously the raw mode id was appended); decision approved_for_session or an allowedTools hit — plain tool name or the `Bash(command)` qualifier built from the tool input command/cmd — shows "Approved for session" (previously indistinguishable from a plain approval). - canceled now appends the wire reason (previously dropped). - resolved/pending/denied unchanged in shape; denied keeps the error tone and aborted shares it. Deliberate divergence: web's own settled footer renders only a minimal red reason box (PermissionFooter.tsx:207-220) — its formatPermissionSummary classification is unreachable there. Android renders the classified verdict line in the existing subdued row style instead, as intended. Implementation: new pure JVM classifier feature/chat/permissions/PermissionVerdict.kt (exhaustive status -> PermissionVerdictLabel mapping, unit-tested in PermissionVerdictTest); ToolCallBlockView.PermissionStateRow becomes a thin renderer over it. Strings added in en/zh-rCN/ru (ru mirrors en key order; zh values from web locales). Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * feat(android): persistent permission-mode chip + inactive gating Item 2 — persistent permission-mode chip above the composer, mirroring the web StatusBar mode span (StatusBar.tsx:253-265, 389-393): - New PermissionModeChip composable + PermissionModeChipRule pure object: resolve the raw wire mode, keep it only when the flavor offers it, and hide `default` everywhere except copilot (model=auto sessions still surface the chip). Unknown wire ids hide (no raw fallback). - Tone mapping follows the web PERMISSION_TONE_CLASSES (StatusBar.tsx: 43-48): neutral=onSurfaceVariant, info=primary, warning=tertiary, danger=error. Labels stay en catalog strings — web mode labels are hardcoded English, not i18n. - The chip consults the unfiltered flavor catalog: the concurrent-clients Safe-Yolo carve-out only narrows the switcher, never the chip. - Deliberate divergence: the web span is not clickable (its switcher is the composer settings sheet / Shift+Tab). Android has no Shift+Tab, so tapping the chip opens the session config sheet that hosts the switcher — same surface as the top-bar gear, no new top-bar icon (ChatScreen keeps its two-icons-max rule). Item 4 — permission-mode gating aligned with web (SessionChat.tsx: 2079, 2138-2163; HappyComposer.tsx:536-537, 1544-1549, 1960-1995): - Inactive sessions stay switchable, matching web: SessionChat passes allowSendWhenInactive unconditionally, so configurationControlsDisabled never trips for inactive threads and the offline notice stays informational only (the send path auto-resumes). - The permission section now disappears for copilot sessions under local terminal control — web withholds onPermissionModeChange there and the whole section then hides (HappyComposer.tsx:1595). Other flavors keep switching while terminal-controlled. - ChatViewModel.setPermissionMode gains the matching belt-and-suspenders guard; the controlledByUser derivation is extracted into terminalControlled() (controlledByUser && !concurrentClients, same as SessionChat.tsx:996) and shared with buildConfigUi. Tests: PermissionModeChipTest (6 cases — default/codex/copilot/pi/unknown/ safe-yolo), ChatViewModelInteractionTest +3 (withheld for terminal copilot; survives terminal claude + shared-clients copilot; inactive stays switchable). No new user-visible strings (chip label is catalog en, contentDescription reuses chat_config_permission_mode). Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * test(android): derive the dsh chip expectation from the flavor catalog The hardcoded "dsh offers no modes" assumption breaks on trees where dsh gained the codex-style mode list — derive the expectation from the catalog so the test pins the actual contract (the chip mirrors the flavor's offered list). Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: HAPI <noreply@hapi.run>
HAPI
Run official Claude Code / Codex / Cursor Agent / Grok Build / OpenCode / Kimi / Copilot / Antigravity / Pi / DeepSeek Harness sessions and control them remotely through native iOS / Android apps, Web / PWA, or Telegram Mini App.
Why HAPI? HAPI is a local-first alternative to Happy. See Why Not Happy? for the key differences.
Features
- Seamless Handoff - Work locally, switch to remote when needed, switch back anytime. No context loss, no session restart.
- Shared Codex Sessions - Use Codex from your terminal and phone at the same time. Requires Codex 0.154.0+. Usage and limits.
- Native First - HAPI wraps your AI agent instead of replacing it. Same terminal, same experience, same muscle memory.
- AFK Without Stopping - Step away from your desk? Approve AI requests from your phone with one tap.
- Your AI, Your Choice - Claude Code, Codex, Cursor Agent, Grok Build, OpenCode, Kimi, Copilot, Antigravity, Pi, DeepSeek Harness—different agents, one unified workflow.
- Terminal Anywhere - Run commands from your phone's browser or desktop web app, directly connected to the working machine.
- Voice Control - Use dictation in native apps, or talk to your AI agent hands-free with the web voice assistant.
- Workspace Browser - Opt-in via one or more
hapi runner start --workspace-root <path>flags: browse scoped file trees from the web and start sessions in allowed subdirectories.
Demo
https://github.com/user-attachments/assets/38230353-94c6-4dbe-9c29-b2a2cc457546
Getting Started
npx @twsxtd/hapi hub --relay # start hub with E2E encrypted relay
npx @twsxtd/hapi # choose an agent and start a session
hapi server remains supported as an alias.
Use hapi <agent> [options] to start an agent directly, for example hapi claude
or hapi codex. Scripts must specify the agent explicitly. hapi --help shows
HAPI's commands and supported agents.
The hub displays a URL and two QR codes. Open the web URL in a browser, or pair a native app using the companion QR. See Native apps for build and pairing instructions.
The relay uses WireGuard + TLS for end-to-end encryption. Your data is encrypted from your device to your machine.
For self-hosted options (Cloudflare Tunnel, Tailscale), see Installation
Docs
Native apps (iOS / Android)
The repository includes SwiftUI/UIKit and Kotlin Compose clients with chat, approvals, session creation, files, dictation, and push notifications. See the native app guide for capabilities, platform differences and pairing. Build instructions: iOS and Android. Developer protocol: client contract.
Build from source
Requires Bun 1.4.0.
bun install
bun run build:single-exe
Credits
HAPI means "哈皮" a Chinese transliteration of Happy. Great credit to the original project.