Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
f8ed67b
feat(android): native phone app for Remote Access
BigWebstas Sep 29, 2026
9e3f57c
feat(android): widget card transparency and color
BigWebstas Sep 30, 2026
91bb1ba
feat(remote): add phone diff review and task lifecycle
BigWebstas Sep 30, 2026
5d73b0a
feat(android): merge tasks from the phone with readiness checks
BigWebstas Sep 30, 2026
6381b17
feat(android): ship the desktop's look presets on the phone
BigWebstas Sep 30, 2026
87adeac
Merge branch 'task/lets-look-at-the-android-app-and-8e5115' into feat…
BigWebstas Sep 30, 2026
c619091
fix(remote): explain a phone diff for a task with no branch
BigWebstas Sep 30, 2026
5eca4c8
feat(android): host quick-switch, composer upgrades, connection labels
BigWebstas Oct 1, 2026
5e7eb47
feat(android): add VPN check and wait-to-connect support
BigWebstas Oct 1, 2026
34ac767
test(mermaid): increase render error timeout to avoid flakiness under…
BigWebstas Oct 1, 2026
1fe608c
feat(android): skip waiting for VPN on home Wi-Fi
BigWebstas Oct 3, 2026
fbe7015
ci(android): build, test, and release the phone app
BigWebstas Oct 3, 2026
5bb8a03
perf(agents): cache checks and keep MCP role guidance visible
johannesjo Oct 4, 2026
9155ec2
fix(mcp): harden coordinator, MCP server and remote API
johannesjo Oct 4, 2026
064ea17
fix(ui): clarify subtask status and message actions
johannesjo Oct 4, 2026
b3cbd85
fix(theme): correct selected agent text contrast
johannesjo Oct 4, 2026
5481dba
fix(git): show only a subtask's own work in its diff
johannesjo Oct 4, 2026
3a0c4d4
Merge branch 'task/for-child-tasks-especially-sometimes-893236'
johannesjo Oct 4, 2026
4fc987b
feat(pr-checks): show merged PR state in purple
johannesjo Oct 4, 2026
2302a3f
Merge branch 'task/detect-if-a-remote-pr-was-merged-on-7a00c3'
johannesjo Oct 4, 2026
63e4deb
feat(android): commit, stage all and unstage all from the phone
BigWebstas Oct 4, 2026
b35391b
fix(remote): send phones the full terminal history
BigWebstas Oct 4, 2026
5c02518
feat(android): wrap long terminal lines instead of scrolling sideways
BigWebstas Oct 4, 2026
6e9a18c
Merge pull request #13 from BigWebstas/task/agents-terminal-history-d…
BigWebstas Oct 4, 2026
7a42fb8
fix(android): return to the task list after closing a task
BigWebstas Oct 4, 2026
6142119
feat(android): back up and restore phone data
BigWebstas Oct 4, 2026
5576d37
feat(android): widget totals across computers, with icon and status dot
BigWebstas Oct 4, 2026
61ce6d0
feat(github): add issue/PR tasks, CI fixes and merge flow
johannesjo Oct 4, 2026
0ac3242
feat(evidence): add evidence packages and a task finish dialog
johannesjo Oct 4, 2026
e0bfb9e
Merge branch 'task/concept-for-evidence-packages-4e2539'
johannesjo Oct 4, 2026
040057f
fix(android): draw the widget divider with an ImageView
BigWebstas Oct 4, 2026
32d5386
Merge pull request #14 from BigWebstas/task/agents-terminal-history-d…
BigWebstas Oct 4, 2026
c067003
Merge remote-tracking branch 'upstream/main' into feat/android-phone-app
BigWebstas Oct 4, 2026
554d5a8
fix(android): spell out "computers" in the widget headline
BigWebstas Oct 4, 2026
91eada8
fix(usage): show Claude extra-usage spend when there are no rate-limi…
BigWebstas Oct 5, 2026
84f7066
fix(android): show Claude extra-usage spend in the usage card and widget
BigWebstas Oct 5, 2026
92e9cdf
fix(android): recognize the home Wi-Fi after reopening the app
BigWebstas Oct 5, 2026
32640ec
refactor(usage): adopt the creditUsage shape from fork/extras
BigWebstas Oct 5, 2026
6e4c7e7
feat(android): show every saved computer's usage on the widget
BigWebstas Oct 5, 2026
cbb1cc2
feat(android): count idle agents in the widget headline
BigWebstas Oct 5, 2026
f328a75
Merge branch 'task/add-a-better-github-integration-taking-e223c6'
johannesjo Oct 7, 2026
3c209a4
fix(canvas): stop auto-opening ordinary markdown writes
johannesjo Oct 4, 2026
fe74dd4
fix(tour): keep simple change tours concise
johannesjo Oct 8, 2026
94eb68c
feat(tasks): integrate PR and CI into finish dialog
johannesjo Oct 8, 2026
f0eda2b
Merge branch 'task/tour-feature-should-not-create-many-cards-for-simp…
johannesjo Oct 8, 2026
ce7aeff
fix(github): recognize enterprise pull requests
johannesjo Oct 8, 2026
c256def
Merge branch 'task/fix-enterprise-github-prs-not-being-recognized-003…
johannesjo Oct 8, 2026
83f5631
Merge PR #297 (feat/android-phone-app) into main
johannesjo Oct 8, 2026
81ecb83
fix(remote): keep the desktop's close warnings on a refused close
johannesjo Oct 8, 2026
56912ee
fix(remote): word the phone close dialog from the branch setting
johannesjo Oct 8, 2026
d45cb03
fix(android): drop the commit pane until the desktop serves it
johannesjo Oct 8, 2026
eea2dc7
fix(remote): drop silent phone sockets with a ping heartbeat
johannesjo Oct 8, 2026
0dc63c4
ci(android): run on desktop look changes and harden the release
johannesjo Oct 8, 2026
631b74f
fix(usage): draw the credit meter at 0% used
johannesjo Oct 8, 2026
31f546b
perf(remote): keep long terminal history only while phones can connect
johannesjo Oct 8, 2026
b615788
fix(android): stop polling usage on desktops without the route
johannesjo Oct 8, 2026
8744669
fix(android): keep paired tokens out of unencrypted backups
johannesjo Oct 8, 2026
7a5d1ce
refactor(android): narrow network callback catches
johannesjo Oct 8, 2026
5c0a227
test(pty): cover remote history on running sessions
johannesjo Oct 8, 2026
f06936c
fix(android): survive a revoked notification permission
johannesjo Oct 8, 2026
9b500f0
test(android): cover the missing usage route rule
johannesjo Oct 8, 2026
bfb6068
feat(android): check GitHub releases for app updates
johannesjo Oct 8, 2026
47d8c54
feat(remote): point Android phones to the native app
johannesjo Oct 8, 2026
b5ee397
docs(android): explain installing and updating the app
johannesjo Oct 8, 2026
af193b8
test(store): allow a cold store import on loaded machines
johannesjo Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
99 changes: 99 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Android

# Tests and builds the phone app when it changes. Pushing an android-v* tag (for example
# android-v0.2.0) also publishes a signed APK as a GitHub release, separate from desktop v* releases.
on:
push:
branches: [main]
tags: ['android-v*']
paths:
- 'android/**'
- '.github/workflows/android.yml'
# LookPresetsGeneratedTest checks the generated looks against these desktop sources.
- 'scripts/generate-android-looks.mjs'
- 'src/styles.css'
- 'src/lib/look.ts'
- 'src/lib/theme.ts'
pull_request:
paths:
- 'android/**'
- '.github/workflows/android.yml'
- 'scripts/generate-android-looks.mjs'
- 'src/styles.css'
- 'src/lib/look.ts'
- 'src/lib/theme.ts'

permissions:
contents: read

defaults:
run:
working-directory: android

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: '21'

- uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0

- name: Test and build
run: ./gradlew testDebugUnitTest assembleDebug

release:
if: startsWith(github.ref, 'refs/tags/android-v')
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: '21'

# A signed build must not restore caches that other workflow runs wrote.
- uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
with:
cache-disabled: true

- name: Decode signing key
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: echo -n "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/release.jks"

- name: Build signed APK
env:
ANDROID_KEYSTORE_PATH: ${{ runner.temp }}/release.jks
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
VERSION_CODE: ${{ github.run_number }}
run: |
VERSION="${GITHUB_REF_NAME#android-v}"
./gradlew assembleRelease -PversionName="$VERSION" -PversionCode="$VERSION_CODE"
mv app/build/outputs/apk/release/app-release.apk "$RUNNER_TEMP/parallel-code-phone-$VERSION.apk"

# Not marked latest, so the desktop auto-updater keeps finding the desktop release.
- name: Publish release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
name: Android ${{ github.ref_name }}
files: ${{ runner.temp }}/parallel-code-phone-*.apk
make_latest: 'false'

- name: Remove signing key
if: always()
run: rm -f "$RUNNER_TEMP/release.jks"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:

- run: npm ci

- name: Check (compile + typecheck + lint + format)
- name: Check (typecheck + lint + format)
run: npm run check

- name: Check static (typecheck + lint + dead code + architecture)
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ docs/*
!docs/guided-understanding-plan.md
!docs/runtime-separation-plan.md
!docs/agent-coordination-plan.md
!docs/evidence-packages.md

# Sandbox bind-mount artifacts from user home (not project files).
# Root-anchored so legitimate nested files with these names are still tracked.
Expand Down
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ package-lock.json
*.AppImage
*.deb
*.dmg

# Android build outputs
android/.gradle/
android/**/build/
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ Electron desktop app for running coding agents in isolated Git worktrees. Deskto

- `npm run dev` starts the Electron development app.
- `npm run build` builds and packages the app; `npm run build:remote` rebuilds the phone UI.
- `npm run typecheck` checks frontend types and their imports; it does **not** cover the entire Electron backend. `npm run compile` checks backend TypeScript and emits `dist-electron/`.
- `npm run typecheck` checks frontend types and their imports; it does **not** cover the entire Electron backend. `npm run typecheck:electron` checks backend TypeScript; `npm run compile` also emits `dist-electron/`.
- Run focused tests while developing:
- `npm run test:unit -- path/to/file.test.ts` for logic/backend tests (Node environment).
- `npm run test:client -- path/to/file.client.test.tsx` for DOM/component tests (happy-dom).
- For code changes, run `npm run check` and relevant tests before handoff. It runs backend compilation, frontend type checking, lint, and a formatting check. For documentation-only changes, check the changed files with Prettier.
- `npm run test:changed` runs the tests of both suites that uncommitted changes affect; run it before committing.
- For code changes, run `npm run check` and relevant tests before handoff. It runs backend and frontend type checking, lint, and a formatting check; caches in `node_modules/.cache` make repeat runs fast. For documentation-only changes, check the changed files with Prettier.
- `npm test` runs both test suites. `npm run test:ci` adds unit coverage thresholds. `npm run check:static` includes dead-code and architecture checks as well as type checking and lint; use it when changing exports or module dependencies.
- See `.github/workflows/ci.yml` for the complete CI sequence, including security-rule fixture tests and the real-PTY coordinator test. Ordinary test runs skip opt-in PTY, Docker, and real-agent suites; real-agent tests can launch paid services. Report skipped or unavailable verification.
- CI tests that Semgrep rules work on fixtures; it does not scan the repository with Semgrep. `npm run lint:security` and `npm run lint:secrets` run separate scans and require Semgrep and Gitleaks respectively.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ Phone access uses port `8777` in development (`npm run dev`) and `7777` in the i

</details>

## Android app

The phone UI works in any mobile browser. On Android, the native app adds agent notifications that need no HTTPS setup, a home-screen widget, and voice input.

1. On your phone, open the [Android releases](https://github.com/johannesjo/parallel-code/releases?q=android-v&expanded=true) and download the newest `parallel-code-phone-*.apk`.
2. Open the downloaded file. Android asks you to allow installs from your browser the first time.
3. In the app, scan the QR code from **Connect Phone** or paste its link, then enter the desktop PIN to enable replies.

The app is not in the Play Store yet. It checks GitHub for a newer release once a day and offers the download; you can turn that off under **Settings → About**. [Obtainium](https://github.com/ImranR98/Obtainium) can also install and update it: add `https://github.com/johannesjo/parallel-code` and filter release titles by `Android`.

Its notifications come from your computer over the same connection as the app, so the phone has to be able to reach your computer, on the same Wi-Fi or through Tailscale. Turning them on keeps a quiet notification visible while the app watches in the background.

## Phone notifications

Paired phones can receive a notification when a running task changes to **Needs input**, including while the phone is locked or the phone app is closed. Tap the notification to open that task. Parallel Code must remain running on your computer with phone access enabled, and both devices need internet access for push delivery.
Expand Down
6 changes: 6 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.gradle/
.kotlin/
.idea/
build/
local.properties
*.iml
89 changes: 89 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Parallel Code for Android

Native companion app for the desktop's **Connect Phone** (Remote Access) feature. It talks to the same HTTP/WebSocket API as the phone web UI in `src/remote/`.

## What it does

- **Connect:** scan the QR code in Connect Phone, or paste the link under it. This gives a view-only token.
- **Pair:** enter the six-digit code from Connect Phone to get a paired token, which may type into terminals. "Keep this phone authorized" asks the desktop to remember the phone across restarts.
- **Several computers:** link more than one desktop (for example the installed app and a dev build, or two machines) and switch between them in Settings → Computers; each keeps its own pairing.
- **Agents:** live list with each agent's status and last line, under the desktop's Claude, Codex, and Antigravity 5-hour and weekly usage meters (hidden on desktops without `/api/mobile/usage`).
- **Minimized tasks:** tasks minimized on the desktop are pinned below the live list; a setting hides them.
- **Looks:** the same 15 themes as the desktop, in Settings → Appearance. Follow system / always dark / always light picks the tone, and a separate dark and light look is remembered, so switching your phone's theme switches the look with it. Each look is drawn with a live swatch, and every color and corner radius comes from the desktop's own stylesheet. See [Looks](#looks).
- **Settings:** theme and looks, keep the screen on, widget background transparency and card color, connection status, wait for VPN (skipped on your home Wi-Fi, which needs location access to read the network name, and "Allow all the time" for agent notifications in the background), and forget this computer.
- **Swipe between tasks:** with a task open, swipe sideways to the previous or next one in the list; the header shows its position ("2 of 5").
- **Terminal:** an agent's terminal in the colors of the look you picked, matching the desktop. Once paired: a reply box and keys a phone keyboard lacks (Enter, Esc, Tab, arrows, Ctrl+C). With "Fit the terminal to this phone" on (Settings, off by default), the terminal takes the phone's size while open so full-screen agents such as Claude Code fill it; the computer's own terminal shifts meanwhile and gets its size back when you leave.
- **Changes:** the task's diff against its base branch, file by file with added and removed lines.
- **Quick replies and voice:** saved replies above the reply box (edit them in Settings) and a mic button that dictates with Android's speech recognizer.
- **Widget:** a home-screen widget with the agents that need you and the usage meters, updated while the app is connected. Settings → Widget sets its background transparency (opaque, 75%, 50% or 25%; the border fades with the card, so your wallpaper shows through) and its card color (Obsidian, Slate or Light, each with text colors that stay readable).
- **Notes:** read a task's notes panel; edit and save it once paired.
- **New task:** pick a project and describe the work; needs pairing.
- **Notifications:** optional, in Settings. A foreground service keeps the connection open in the background and notifies when an agent needs input, hits an error, or finishes (each can be turned off); tapping one opens that agent.
- **Close task:** from an agent's screen; needs pairing. Like the desktop, it warns before losing uncommitted or unmerged work.

- **Built-in chat:** read the conversation, send messages, stop the agent, and answer its approvals and questions once paired. Choosing the model and attaching images stay on the computer.

## Looks

The phone uses the desktop's look presets, not its own. `LookPalettes.kt` is generated from the files the desktop already keeps its looks in:

| Desktop source | What it contributes |
| ------------------ | ------------------------------------------------------------ |
| `src/lib/look.ts` | Preset ids, labels, descriptions, order, and light/dark tone |
| `src/styles.css` | The colors and the corner radius scale |
| `src/lib/theme.ts` | The terminal ANSI palettes and which look pairs with which |

```sh
npm run generate:android-looks # rewrite LookPalettes.kt after a desktop theme change
npm run check:android-looks # fail if it is out of date (also run by the Kotlin tests)
```

Three things are worth knowing about the mapping:

- **The cascade is resolved, not copied.** Each desktop theme sets only the variables it changes and inherits the rest from `:root`, so the generator resolves the full palette per preset. The phone has no fallback values of its own.
- **Gradients are flattened.** Several desktop backgrounds are `radial-gradient`s. The phone draws flat surfaces, so a gradient becomes its middle stop, which keeps the look recognizable. Everything else is the exact value.
- **Terminals follow the look.** A terminal is drawn over the look's `--task-panel-bg` with the ANSI set the desktop pairs with that look, so Midnight gets a pure-black panel and Noir gets Noir's ANSI colors. Dark looks with no set of their own on the desktop fall back to the muted Noir set, because the desktop's fallback there is xterm's own defaults.

Obsidian in both tones is the default, and its values are pinned by `LookPalettesTest`, so adding a theme cannot quietly change what the app looks like out of the box.

## Build

Needs JDK 17+ and the Android SDK (compile SDK 37). Set `ANDROID_HOME` or add `sdk.dir` to `android/local.properties`.

```sh
cd android
./gradlew testDebugUnitTest # unit tests
./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk
./gradlew installDebug # install on a connected device
```

### Releases

`.github/workflows/android.yml` tests and builds the app whenever `android/` changes. Pushing a tag such as `android-v0.2.0` also publishes a signed APK as a GitHub release, kept separate from the desktop's `v*` releases. Signing reads `ANDROID_KEYSTORE_PATH`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, and `ANDROID_KEY_PASSWORD` from the environment; CI fills them from repository secrets of the same names, with the keystore stored base64-encoded as `ANDROID_KEYSTORE_BASE64`. Every update must be signed with the same key, so keep a backup of it.

The release title starts with `Android`, which Obtainium filters on, and the APK name must keep ending in `.apk`: the app's update check (`AppReleases.kt`) looks for `android-v*` releases that are not drafts or prereleases and have an APK attached, and compares their dot-separated version numbers with its own `versionName`. Installs from an app store skip the check. User install steps are in the main [README](../README.md#android-app).

QR scanning uses the Google Play services code scanner, so the app needs no camera permission. On phones without Play services, paste the link instead.

## How it maps to the server

See `electron/remote/server.ts` and `electron/remote/protocol.ts`.

| Step | Request |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| Pair | `POST /api/pair/verify` with `Authorization: Bearer <token>` and `{ pin, remember }`; returns `{ token }` |
| Connect | WebSocket `/ws`; first message `{ type: "auth", token }`. The paired token is used when present |
| Watch | `subscribe` / `unsubscribe`; the server sends `scrollback`, then `output` (base64 PTY bytes) |
| View size | `view-size` with `{ cols, rows }` (paired) while a terminal is open; without them, or on disconnect, the desktop size returns |
| Projects | `GET /api/mobile/projects` (paired) |
| New task | `POST /api/mobile/tasks` with `{ projectId, name, prompt }` (paired); returns `{ taskId }` |
| Usage | `GET /api/mobile/usage`; the desktop status bar's snapshot, readable view-only |
| Notes | `GET` / `PUT /api/mobile/notes/<taskId>` with `{ notes }`; reading works view-only, saving needs pairing |
| Close task | `POST /api/mobile/tasks/<taskId>/close` with `{ force }` (paired); `409` with `{ warnings }` when work would be lost |
| Changes | `GET /api/mobile/tasks/<taskId>/diff` → `{ diff, truncated, unsupported }`; readable view-only |
| Reply | `input` with `submit: true` and a `requestId`; confirmed by `input-result` |
| Close `4001` | Paired token rejected: drop it and reconnect view-only. QR token rejected: scan again |
| Close `4003` | Typing rights lost: drop the paired token |
| HTTP 401 | On a paired-token request: drop the paired token and reconnect view-only |

Remote Access serves plain HTTP on the LAN or Tailscale address, so the app allows cleartext traffic. Credentials live in app-private storage and are excluded from backups and device transfer.
Loading
Loading