* 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> * feat(android): permission input semantics parity (editor inputType, URL confirm, grok auto gate) Three behavior alignments with the web app's permission surfaces: 1. Editor inputType (web requestUserInput.ts:152-153, 191-195): a required text question with `inputType === 'editor'` is always answered and its note is pushed untrimmed (even the literal "user_note: "); non-editor notes stay trimmed and optional. Android: PermissionInputs.requestUserInputAnswerValues / isRequestUserInputAnswered now take the question, gate on the literal "editor" inputType; the footer's note field grows to minLines = 2 to invite multi-line answers. 2. request_user_input URL confirmation (web requestUserInput.ts:46-70, 129-141; RequestUserInputFooter.tsx:142-152): the sign-in url only survives when it parses as http/https; submitting requires the CLI-synthesized __mcp_url_confirmation option (description === url) to be selected, and submission opens the url before approving. Android: parseRequestUserInput + normalizeRequestUserInputUrl + isRequestUserInputUrlConfirmed; the footer enforces the confirm guard, then opens the url via ACTION_VIEW. Divergences: the raw wire string is compared (the CLI synthesizes the description from the same value web would re-normalize, so they are equivalent for every CLI-produced request), and the open-failure message is platform-adapted (no popup blocker on Android). 3. Grok Auto permission gate (web NewSessionForm index.tsx + grok models probe): when the machine reports `autoPermissionModeSupported === false` for the chosen cwd, the Auto option is disabled with an " (unavailable)" suffix, a hint line is shown, and an existing Auto selection resets to Default. The gate is grok-only (claude and other agents never gate) and tri-state — null (unknown, probe pending or failed) never disables. Android: GrokModelsResponse wire type (nullable field), HapiApi.getMachineGrokModelsForCwd, NewSessionViewModel probes on agent/cwd/machine changes (deduped, cancellable, existence-gated), OptionItem gains `enabled`, the permission dropdown honors it and renders the hint. In-session switching stays ungated, matching web. New strings in en / zh-rCN / ru (KEYSYNC: en and ru key order identical). Tests: RequestUserInputTest covers editor/untrimmed-note/url parsing/confirmation semantics; NewSessionViewModelTest covers gate disable+reset+hint, tri-state never-gate cases, and agent/cwd gating; HapiApiTest covers the cwd query param and nullable decoding. Co-Authored-By: Claude Code <noreply@anthropic.com> via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(android): accept browser-valid sign-in URLs --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: weishu <twsxtd@gmail.com>
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.