中文 | English
Positioning: Android Edition — the Android member of the Pi Desktop family. It ships no bundled browser kernel and no Electron/Chromium: the app hosts an in-process Node.js runtime (aosp-node-bridge + a provisioned
libnode.so) which serves the upstream Pi Web locally, and renders the UI with the system WebView pointed at that local service. The functional target is parity withpi-desktop-ark(the HarmonyOS pure-runtime edition), plus Android-specific increments (a foreground service, an in-process pnpm extension, and a Storage Access Framework bridge).
| Item | Value |
|---|---|
| Edition | Android Edition (pi-desktop-android) |
| Display name / application ID | Pi Desktop / org.fellow99.pi.PiDesktopAndroid (finalized 2026-10-24; the display name matches the pi-desktop / …Ark brand — only the package name carries "Android") |
| Version / versionCode | 0.15.0 / 1500 (v0.15.0 release; debug+release APK built this round and device-verified on MIX / Android 8.0.0: install, launch, WebView UI, version badge web v0.15.0 · pi v1.5.0, passwordless loopback auth, DeepSeek key, sandbox workspace, text chat, vision image chat (DeepSeek V4.1 Flash), tool chain — bash pwd/write/read/grep/ls plus built-in git init/add/commit/log) |
| Upstream baseline | pi v1.5.0 + pi-web v0.15.0 (six idempotent, reversible patches under patches/v0.15.0/) |
| Backend | io.github.fellow99:node-bridge 0.2.0 (JNI direct-call host bridge) + a provisioned aosp-node-runtime engine libnode.so v26.13.0 (--with-intl=small-icu; see Engine note below), in-process via dlopen + node::Start |
| Frontend | System WebView loading http://127.0.0.1:<random-port>/ (same-origin HTTP + SSE; loopback is a secure context) |
| ABI | arm64-v8a only (the engine ships a single ABI) |
| minSdk / targetSdk / compileSdk | 24 / 36 / 36 |
| Current status | Level-0 verification passed on a real device (Xiaomi M2105K81C, Android 11, A1–A8 all ✅); simulator is limited to lightweight probes because the ARM translation layer SIGSEGVs on the Next.js require graph |
| Planning doc | docs/000-工程规划.md (v0.2, the source of truth for planning) |
Engine note. The Maven artifact
io.github.fellow99:node-runtime:24.2.0is built--with-intl=none(noIntlat all, which makes Next 16's compiled output fail at parse time on Unicode property escapes). The project therefore provisions aosp-node-runtime v26.13.0 (small-ICU) intoapp/src/main/jniLibs/viascripts/provision-engine.mjs, with the SHA-256 locked inruntime.lock.json. The binary is deliberately not committed.
Pi is a minimal, extensible agent harness; Pi Web is its local web UI
(a Next.js app in which one Node process serves both the SPA and /api/*, and the agent runs in
that process). This project packages that stack into a native Android application shell:
- the system WebView (no bundled Chromium) renders the upstream UI 100% as-is — no frontend fork;
- a Node runtime inside the app's own process serves pi-web; the two talk over loopback only;
- the Kotlin shell supplies the things a local Node service cannot do on Android: folder/file pickers (SAF), runtime permissions, notifications, a keep-alive foreground service, and external link hand-off.
Unofficial notice. This project is not built by the Pi or Pi Web teams, has no affiliation with the upstream authors, and is not endorsed by them. Both
PiandPi Webare MIT-licensed upstream open-source projects; this project is merely a third-party port of their desktop form factor to Android.
Requirements are organized under specs/1NN-<name>/ (mirroring the ark numbering):
| Module | Spec directory | Deliverable |
|---|---|---|
101-runtime-integration |
specs/101-runtime-integration-launch-orchestration/ |
Maven bridge wiring, archive provisioning, launch/readiness/marker state machine |
102-payload-pipeline |
specs/102-payload-pipeline-assembly-crop/ |
Patch → build → crop → tar.gz payload pipeline (74.5 MiB archive) |
103-webview-shell |
specs/103-webview-shell-render-pages/ |
WebView config, boot/error pages, navigation fence, history clearing, Chrome-94 polyfills |
104-host-capabilities |
specs/104-host-capabilities-kotlin-impl/ |
Kotlin host bridge: facts, permissions, SAF pickers, notifications, external links, keep-alive, terminal.pty capability (node-pty 1.1.0) |
105-auth |
specs/105-auth-loopback-no-password/ |
Loopback-only bind + no password (WebView and Node share one process; unlike Electron there is no loopback isolation) |
106-tools-extensions |
specs/106-tools-extensions-same-name-overrides/ |
Same-name extension overrides (bash / find / grep) |
107-compliance |
specs/107-compliance-and-security/ |
Store-compliance checklist G1–G11 (manifest minimality, 16 KB alignment, debug-only diagnostics, AI-content label, privacy draft) |
The CLI capability increment is tracked in specs/112-cli-integration/:
five real cross-compiled CLIs — node (npm/npx/corepack), python (pip/pip3), git, uv,
and pnpm — provisioned from GitHub Releases into jniLibs/assets (binaries not committed) and
exposed on PATH through a per-launch $HOME/.profile plus login shell sh -l across the terminal and
the agent bash/ptyBash channels. The earlier pure-JS android-git (isomorphic-git) and in-process
android-pnpm extensions have been removed. Known limit: git commit segfaults in the app SELinux
domain because link(2) on app data is denied — confirmed on native arm64 real device (2026-11-07),
not just the emulator translation layer. See the 112 evidence directory and
node-bridge-workspace/docs/20261107-git问题报告.md.
Terminal command environment. The terminal panel and the agent bash/ptyBash channels run
mksh (/system/bin/sh, login shell sh -l). Pre-installed CLIs: git、node、npm、pnpm、python、pip、uv (real cross-compiled binaries). System built-ins: toybox (full text/file set), awk/sed/tr, tar/gzip, sha256sum, curl, ping, pm, getprop, logcat. No gcc/make/toolchain, so native node-gyp builds and pip sdist compilation are unavailable.
pi-desktop-android/
├── app/
│ ├── build.gradle.kts # applicationId / ABI arm64-v8a / signing (keystore not committed)
│ └── src/main/
│ ├── AndroidManifest.xml # 4 permissions; singleTask; dataSync foreground service
│ ├── assets/
│ │ ├── pi-web/pi-web.tar[.gz] # payload archive (built artifact, not committed)
│ │ └── webview/polyfill-webview.js # Chrome 94 (old system WebView) ES2022/dvh polyfills
│ ├── java/org/fellow99/pi/PiDesktopAndroid/
│ │ ├── MainActivity.kt # WebView shell: fence, file chooser, console forwarding, debug switch
│ │ ├── PiApplication.kt
│ │ ├── runtime/ # PiDesktopRuntime, PayloadArchive, EngineFacts
│ │ ├── host/HostCapabilities.kt # the host-capability bridge (10 capabilities)
│ │ ├── service/PiForegroundService.kt
│ │ └── payload/PayloadArchive.kt
│ ├── jniLibs/arm64-v8a/ # libnode.so + libc++_shared.so (provisioned, gitignored)
│ └── res/ # app name = "Pi Desktop", launcher icons
├── payload/ # shell overlay copied on top of pi-web at assembly
│ ├── pi-web-launch.js # in-process Next boot, readiness marker, jitless layer, materialization
│ ├── env.js # the environment contract (PI_* variables)
│ ├── extensions/
│ │ ├── android-exec.js # overrides built-in bash: resident mksh + sentinel line
│ │ ├── android-search.js # overrides find/grep: pure-JS walk, zero rg/fd/child processes
│ │ ├── android-runtime.js · android-workspace-roots.js
│ │ ├── android-saf.js # SAF bridge (content://, persistable permissions)
│ │ ├── android-pty/ # node-pty 1.1.0 (Maven AAR) + loader + ptyBash tool + /pty-test
│ │ └── (android-git/ android-pnpm/ removed in 112 — replaced by real git/pnpm CLIs via .profile)
│ ├── skills/android-runtime-capabilities/
│ ├── config/{settings.json,mcp.json}
│ └── jitless/ # pure-JS compatibility layer, activated only if WebAssembly is absent
├── patches/v0.15.0/ # six idempotent pi-web patches (0001–0006)
├── scripts/ # assemble-payload · crop-rules · provision-engine/pty · devtest · sync
├── docs/ # 000 plan · capability inventory · device records · privacy draft
├── specs/ # 1NN module specs
├── keystore.properties.sample # release signing template (keystore.properties is gitignored)
└── build.gradle.kts · settings.gradle.kts · gradle/
Prerequisites: a host Node.js for the payload pipeline, the Android SDK + JDK 17, and an
arm64-v8a device or emulator image (x86_64 emulator images run the engine only under the ARM
translation layer, which cannot boot the Next.js server — see Known limitations).
# 0) Engine: provision libnode.so into app/src/main/jniLibs (sha256-lock gated; not in git) node scripts/provision-engine.mjs --check # verify a previously provisioned engine node scripts/provision-engine.mjs # provision from the locked aosp-node-runtime build # 1) Assemble the payload on the host (dry run, then apply) node scripts/assemble-payload.mjs # report patches / crop stats / sizes node scripts/assemble-payload.mjs --apply # -> app/src/main/assets/pi-web/pi-web.tar.gz + .version # 2) Build the APK ./gradlew :app:assembleDebug # or :app:assembleRelease # Release signing: copy keystore.properties.sample to keystore.properties and fill it in; # without it the release APK is produced unsigned. Debug builds are signed with the debug key.
Output: app/build/outputs/apk/debug/app-debug.apk (~181 MB: engine 81.5 MB + payload 74.5 MiB).
The display name is Pi Desktop (res/values/strings.xml; only the package id
org.fellow99.pi.PiDesktopAndroid carries "Android"). All artwork comes from the
parent project's ../assets (logo.png, logo.svg, logo-1024-with-bg.png) — nothing is
drawn by hand, and a missing/invalid asset is a hard failure.
- Launcher / adaptive icons —
scripts/generate-icons.mjsrenders the five-density legacy square + round icons and the adaptive foreground from the parent assets (--checkverifies). - In-app web branding — during payload assembly
scripts/brand-assets.mjsrewrites the staged pi-web copy only (upstream../pi-webstays pristine): every user-visiblePi Webstring becomesPi Desktop(≈1000 sites), the page logo is replaced byte-for-byte withlogo.svg, and the favicon / PWA / apple-touch icons are regenerated. It then asserts zeroPi Webleftovers outsidenode_modules, the presence ofPi Desktopin pre-rendered HTML, and that technical identifiers (pi-web,@agegr/pi-web,PI_WEB_, …) are untouched. The step is idempotent and supports--check.
Note the payload stage is hard-linked for zero-copy speed, so branding replaces each staged entry
(temp-file + rename / unlink-before-write) rather than writing through the shared inode; the
build/pi-dist source is verified to remain upstream-branded. See
docs/安卓环境能力清单-0.14.1.md §H REV-1.
adb install -r app/build/outputs/apk/debug/app-debug.apk adb shell am start -n org.fellow99.pi.PiDesktopAndroid/.MainActivity
- First launch unpacks the payload (≈18 s measured for an 85 MB / 27k-file archive on the emulator; a SHA-256 marker makes every subsequent launch skip unpacking) and boots the in-process server; the boot page reports progress, and the WebView switches to the service once the HTTP readiness probe succeeds.
- The server binds
127.0.0.1on a random port (fixed ports caused hard EADDRINUSE failures during verification) with no password; loopback is a secure context (crypto.subtleavailable), andnetwork_security_configpermits cleartext only for loopback.
Device verification so far (see docs/002-真机验证记录.md):
- Real device Xiaomi M2105K81C (Android 11, arm64): A1–A8 (cold start, payload unpack, runtime
boot, host bridge, model/SSE conversation, image view, layout, file upload) all ✅, including
fixes for the device's factory WebView (Chrome 94): ES2022 + dynamic-viewport polyfills,
<input type=file>wiring, and full-height layout. - x86_64 emulator (berberis ARM translation): lightweight probes pass (JIT/WASM, Intl, SSE data plane, spawn semantics, host bridge, 18 s unpack); the Next.js require graph deterministically SIGSEGVs under translation in all tested JIT/jitless/memory configurations — a host-environment limitation, not an app defect. Emulator coverage is therefore limited to probes and diagnostics.
| Capability | Status | Note |
|---|---|---|
| Main UI (Pi Web), streaming SSE conversations, model/provider settings | ✅ on device | Same-origin local service; API keys stay in the app's internal storage |
| File read / write / edit / list | ✅ | Pure Node.js inside the app process |
Command execution (the bash tool) |
✅ on device | Replaced under the same name by android-exec.js: one resident non-PTY /system/bin/sh (mksh) + a random sentinel line; completion is judged from stdout alone, never from exit/close events |
File lookup / content search (find / grep) |
✅ on device | Replaced by android-search.js: pure-JS walk, zero fd/rg, zero child processes; 512 KiB per-file cap |
| Git operations | ✅ real CLI on device (emulator-verified) | Real git 2.56.0 arm64 binary on PATH via .profile; --version/init/add/hash-object/write-tree/log and HTTPS ls-remote/clone/fetch work (static curl+OpenSSL). Known limit: git commit segfaults (rc 139) because the app SELinux domain denies link(2) on app data and this build does not fall back to rename; native-arm64 ruling pending (112 evidence). No SSH remotes, SHA-256 repos, or worktree |
| Package management | ✅ real CLIs on device (emulator-verified) | Real pnpm 12.16.1 (Rust), node/npm/npx/corepack 26.13.0, python/pip 3.14.10, uv 0.13.6 on PATH; pnpm lifecycle scripts invoke the real node. No gcc/make/toolchain, so native node-gyp builds and pip sdist compilation are unavailable; pure-JS/pure-Python packages and prebuilt natives only |
| Terminal panel / PTY | ✅ on device (node 26.13.0) | node-pty 1.1.0 ships in the APK (libpty_node.so + vendored JS) and is dlopen'd by an always-on boot loader (pty.ready boot-fact). Upstream terminal-manager runs a real mksh PTY (/api/terminal + SSE); android-pty also registers a simplest ptyBash tool (onExit true exit codes) and /pty-test. Verified E2E on real device Xiaomi MIX: tty→/dev/pts/0, echo+ANSI, resize (COLUMNS/LINES), Ctrl-C (SIGINT rc=130, shell alive), exit-close; ptyBash echo ok;tty→rc0, exit 3→rc3 |
| Code-mode (QuickJS sandbox) | ⬜ not yet verified | Android runs JIT by default (no HarmonyOS-style W^X ACL gate) and WebAssembly validates on the probes, so code-mode is expected to work; it is simply not verified end-to-end yet. The jitless/ layer is a fallback installed only when WebAssembly is missing |
| MCP stdio servers | The spawn event-ordering blocker that existed on HarmonyOS is moot under Android (exit/close are delivered), but no real MCP server has been attached yet; shipped mcp.json is empty |
|
| Workspace / user files | ✅ app-specific external storage (no permission) + SAF | The mainline workspace lives under Android/data/<pkg>/files/workspace; additional trees are added via the Storage Access Framework (content:// URIs with persisted permissions), which are not POSIX paths and are exposed through the android-saf bridge |
| Notifications | 🔧 bridge implemented | notify.show goes through a notification channel; on Android 13+ it requests POST_NOTIFICATIONS at runtime and reports denials structurally |
| Background residency | ✅ implemented (device-verified start/stop) | runtime.keepAlive drives a dataSync foreground service with a permanent notification |
| External links | ✅ | Only main-frame non-local navigations leave the app, via ACTION_VIEW to the system browser; window.open is captured, subframes pass through |
| Auto-update / tray | ❌ / ➖ | No updater in v1 (sideload / domestic-channel APK first, Google Play later); tray is desktop-only |
codemode classifier WASM, image processing |
✅/🔧 | JIT+WASM available on the default engine; image resize uses the same pure-JS photon shim strategy as ark inside the jitless fallback layer |
See skills/android-runtime-capabilities/SKILL.md
(the capability digest presented to the model) and the authoritative as-built inventory
docs/安卓环境能力清单-0.14.1.md.
Six idempotent, reversible patches (auto-applied during payload assembly; the pi-web working tree is restored to pristine afterwards):
| Patch | Purpose |
|---|---|
0001-terminal-capability-unsupported |
Surface PTY as a structured capability flag — supported only once libpty_node.so is real-loaded at boot (no silent degradation into a fake terminal) |
0002-cwd-sandbox-root-from-android-roots |
Resolve the working directory / browser roots from PI_ANDROID_ROOTS |
0003-cwd-adb-path-alias-normalize |
Normalize adb/host-style path aliases to sandbox-relative paths |
0004-session-export-runtime-identity-guard |
Route session export through the in-process fallback when the runtime identity (app_process) cannot spawn Node |
0005-directory-picker-contract |
Workspace-root tabs + the directory-picker contract fed by host roots (no hardcoded paths in the UI) |
0006-directory-picker-permission-flow |
Split EACCES/EPERM from ENOENT → HTTP 403 + canRequest; POST /api/host/perm drives the system permission dialog |
- The app collects no accounts, runs no tracking/analytics/crash-SDK, and does no server-side
relay: your API keys (internal storage,
0600), sessions, and files leave the device only in the model-provider requests you configure. Draft (for channel review):docs/隐私政策与AI标识-G8G9.md. - The "AI-generated content" label is permanently shown at the bottom of the UI.
- The manifest requests four permissions only (
INTERNET,ACCESS_NETWORK_STATE,POST_NOTIFICATIONS,FOREGROUND_SERVICE+FOREGROUND_SERVICE_DATA_SYNC); WebView remote debugging is enabled in debug builds only.
| Document | Contents |
|---|---|
docs/000-工程规划.md |
Engineering plan, decisions, capability matrix, phases (planning source of truth, v0.2) |
docs/安卓环境能力清单-0.14.1.md |
Authoritative as-built capability inventory vs. ark, with evidence and root-cause layers |
docs/002-真机验证记录.md |
Phase 0 probe records, emulator translation-layer findings, real-device A1–A8 evidence |
docs/隐私政策与AI标识-G8G9.md |
Privacy-policy and AI-labeling draft for store submission |
specs/ |
Module specs (101–107), architecture, constitution, overall test cases |
MIT (see LICENSE). Upstream Pi and Pi Web are MIT-licensed. The five bundled CLIs
(node/python/git/uv/pnpm) keep their own licenses; each CLI's LICENSE/THIRD-PARTY-NOTICES ships in
its GitHub Release tarball and is registered in cli.lock.json.