Android: cold-launch deep-link + vision doc (CI-built APK)#16
Open
Android: cold-launch deep-link + vision doc (CI-built APK)#16
Conversation
Personal API keys (agor_sk_*) exist so external orchestrators (Hermes, etc.) can call Agor MCP without binding to a specific session. The recently-added session-context gate forced every API-key request to supply ?sessionId=/X-Agor-Session-Id, which broke tool discovery and any sessionless workflow. Drop the hard gate; tools that genuinely need a current session will surface their own error when ctx.sessionId is undefined. Also fix the agor-live build ordering: daemon must build before the CLI because apps/agor-cli dynamically imports @agor/daemon, which tsup DTS cannot resolve otherwise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ui): full-feature PWA with phone-friendly drawer layout
Restores phone usability and delivers desktop feature parity on mobile,
and fixes PWA behavior when the UI is hosted under /ui.
Routing parity
--------------
* Remove the device-detection redirect that forced phones onto the
trimmed-down /m shell. The full Agor UI now renders on every device.
* /m/* is preserved as a legacy redirect (MobileLegacyRedirect) so
existing PWA installs and bookmarks resolve to canonical
/b/:board/:session/ URLs, preserving session context when available.
Feature parity on phone
-----------------------
* useIsCompactViewport (matchMedia; max-width <768 OR coarse-pointer
tablet) drives a layout switch in components/App/App.tsx.
* On compact viewports the React Flow canvas is replaced by
MobileBoardView — a Segmented control over Worktrees/Sessions/Comments
lists with a floating action button for new sessions.
* SessionPanel, CommentsPanel, and EventStreamPanel open as full-screen
Drawers on mobile instead of side panels. All desktop modals, dialogs,
and tool flows remain available.
/ui PWA correctness
-------------------
* manifest.webmanifest uses relative start_url/scope/icons ("./") so it
resolves correctly whether hosted at / or /ui/.
* index.html manifest link uses Vite's %BASE_URL% so deep SPA cold loads
still resolve the manifest to an absolute path.
* Service worker rewritten: derives basePath from self.registration.scope,
network-first for navigation (no more stale shells), stale-while-
revalidate for static assets, hard bypass for API/socket.io/MCP/
EventSource/WebSocket traffic, and a SKIP_WAITING channel so users
reload into new deploys without clearing caches.
* usePWAUpdate hook + <PWAShell /> banner surface install/update/offline
state on every route.
* getDaemonUrl() now uses window.location.origin in production, fixing
cross-origin WSS blocks on mobile networks/Brave/Firefox when served
through an HTTPS reverse proxy at / or /ui/.
Viewport/keyboard usability
---------------------------
* index.html ships apple-touch-icon, mask-icon, mobile-web-app-capable,
safe-area padding (env(safe-area-inset-*)), 100dvh root, dynamic
viewport, overscroll-behavior:none, and format-detection.
* Responsive CSS overrides shrink the antd header, full-bleed modals,
hide unusable react-resizable-panels handles, enlarge tap targets,
and respect iOS safe-area insets in standalone display mode.
Device detection hardening
--------------------------
* isMobileDevice/isCompactViewport documented as UX-affordance only --
must never control route ownership.
* isOnMobileRoute renamed to isCompactViewport (width-based) since the
old pathname check is now meaningless.
Cleanup
-------
* Delete dead /m-only shell: MobileApp, MobileCommentsPage, MobileHeader,
MobileNavTree, MobilePromptInput, SessionPage.
* Drop write-only promptDrafts state in App.tsx -- SessionPanel already
persists per-session drafts via localStorage.
Tests
-----
* pwa-base-path.test.ts: manifest relative paths, %BASE_URL% link,
service worker scope/basePath/NO_CACHE_PREFIXES/SKIP_WAITING.
* MobileLegacyRedirect.test.tsx: /m, /m/comments/:boardId,
/m/session/:id with known and missing session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(api-keys): register patch/remove on base service so revoke works
Feathers dispatches methods on the service name, so calling
`client.service('api/v1/user/api-keys').remove(id)` requires the
`remove` method to exist on that exact service. The previous
registration split the CRUD methods across two services
(`api-keys` for find/create and `api-keys/:id` for patch/remove),
causing "Method 'remove' not allowed on service
'api/v1/user/api-keys'" when users tried to revoke a key from the
Personal API Keys settings tab.
Consolidate all four methods on the single base service. Feathers'
REST provider maps `DELETE /api-keys/:id` to `service.remove(id, params)`
and `PATCH /api-keys/:id` to `service.patch(id, data, params)`
automatically, so no separate `/:id` route is needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pwa): bypass cache for /api/v1/* and /opencode/* in service worker NO_CACHE_PREFIXES matches the first path segment after basePath, so /api/v1/user/api-keys (personal API keys) and /opencode/health|models were falling through to the stale-while-revalidate asset cache. Add both prefixes and extend the base-path regression test to assert they stay listed. * chore(ui): drop unused deviceDetection utility After the drawer-layout rewrite, useIsCompactViewport fully replaces the old screen-size helpers. No module imports isMobileDevice or the legacy isCompactViewport function anywhere in apps/agor-ui. * feat(ui): close mobile drawers on browser back gesture Full-screen session/comments/event-stream drawers on compact viewports now push a marker onto the history stack when they open, so the Android system back gesture (or browser back button in any PWA) closes the drawer instead of navigating away from the current board. The hook is inert when `enabled` is false so desktop routing is unaffected. Each drawer passes a stable id so overlapping drawers can't close one another. --------- Co-authored-by: Claude <noreply@anthropic.com>
* perf(mcp): slash token cost of Agor MCP server
External orchestrators (Hermes, etc.) were triggering rapid context
compaction because the MCP surface was bloated at multiple layers:
- Tool responses were pretty-printed with 2-space indentation
(JSON.stringify(_, null, 2)), inflating every payload 30-40%.
- SERVER_INSTRUCTIONS carried ~1.8 KB of domain lists and workflow
examples on every initialize.
- agor_artifacts_publish embedded ~1.6 KB of CONFIG CONVENTION docs
in its description; useLocalBundler and artifacts_status carried
hundreds more chars each.
- Session tools had prose-heavy enum/parameter descriptions
(sessionType parentheticals, spawn/prompt narratives,
get_current_context field list).
- Worktree tools defaulted _include_sessions:true on every get,
scaling response size with session count, and othersCan/
othersFsAccess fields repeated tier documentation.
- Search/execute meta-tools had long self-descriptions and returned
unused hint + domains blobs on every call.
Trim every layer. No tool names or schemas removed — only description
length, default includes, and response formatting. DOMAIN_DESCRIPTIONS
collapsed. agor_worktrees_get now takes an opt-in includeSessions
flag (default: false).
Expected ≥50% reduction in per-call token cost for typical workflows.
* fix(mcp): enforce list limits + default-exclude archived everywhere
Two related bugs found while reviewing the MCP surface:
1. Pagination was unbounded. `PAGINATION.DEFAULT_LIMIT = 10_000` in
packages/core — every MCP list tool whose description claimed
"default: 50" actually returned up to 10,000 rows because nothing
set $limit when the agent omitted it. Affected: sessions, worktrees,
boards, tasks, repos, users. Set an explicit default of 50 in each
tool handler so the schema's stated default is actually enforced.
2. Archived items leaked into several tools by default:
- agor_cards_list: three code paths (findByZoneId, findByCardTypeId,
unfiltered find) never passed archived=false. Unified on the
includeArchived/archived opt-in pattern (same as sessions/boards)
and added post-filter for the paths where the repo doesn't accept
an archived arg.
- agor_tasks_list: tasks have no archived column and the tool didn't
join to sessions, so tasks from archived sessions were returned.
When called without sessionId, scope to active-session IDs. Added
an includeArchived opt-in.
- agor_messages_list: unscoped search bled into archived sessions.
When no sessionId/taskId is given, restrict via inArray on active
session IDs. Added an includeArchived opt-in.
These are semantic (correctness) fixes as much as token fixes — once a
user archives something, it should stay out of agent context by
default.
* fix(mcp): restore workflow hints in SERVER_INSTRUCTIONS
Review feedback: the previous trim dropped the workflow recipes which
genuinely save agents discovery round-trips. Keep the three common
workflows (orient, create-and-start, delegate, continue/fork) and a
one-line product description — agents still get the domain listing
from agor_search_tools itself, so that part stays out.
~900 bytes vs the original ~1,800 and the over-trimmed ~300.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add apps/agor-android/ — a Kotlin + Jetpack Compose native Android app that talks to the existing FeathersJS daemon via REST + Socket.IO, mirroring the SwiftUI iPhone app on the maroun2/agor add-iphone-native-app branch. Bundled features: - Auth: smart URL probe (auto :3030 + http/https fallback, /health validation), email/password login, JWT + refresh stored in EncryptedSharedPreferences, ServerProfile persistence in DataStore. - Sidebar: boards → worktrees → sessions tree, Important Sessions, Needs Attention, 1h-TTL JSON cache restored on launch, 45s polling. - Chat: streaming markdown via multiplatform-markdown-renderer, collapsible Tool/ToolResult/Thinking/Image blocks, inline Permission and InputRequest cards (radio/checkbox/free-text), task-grouped messages with dividers, prompt input with draft persistence, pagination. - Files: virtual directory tree built from the daemon's flat /file API, text + image viewer (base64 + URL). - Voice: full pipeline ported from iOS — VoiceActivityDetector with asymmetric EMA, M-of-N confirmation, hysteresis, calibration phase, and noise-floor freeze, plus VadConfig (Codable, runtime-tunable). TTS via Android TextToSpeech with queue + interruption rules. Continuous voice runs in a foreground service (microphone|mediaPlayback) with wake-lock. - Transcription: whisper.cpp via NDK/JNI when vendored (scripts/sync-whisper.sh + scripts/fetch-whisper-model.sh), Android SpeechRecognizer fallback. - Notifications: local notifications on favorited-session-finished using deterministic per-session IDs to dedupe. The Android module sits next to apps/agor-ios/ (on the iPhone branch) and apps/agor-ui/. pnpm-workspace.yaml's apps/* glob is harmless because the directory has no package.json. CI does not yet build the Android app — the README documents local Gradle build (./gradlew :app:assembleDebug) and ./deploy.sh for adb installs.
Pre-existing format violations in apps/agor-daemon/src/mcp/tools/* (collapse multi-line zod chains onto single lines) flagged by the pre-commit hook. Auto-fix only — no behavior change.
- .github/workflows/build-android-apk.yml: builds debug APK on push/PR for apps/agor-android/**, uploads agor-android-debug-<sha>.apk as an artifact so reviewers can install before merge. Optional whisper.cpp vendoring via workflow_dispatch input; gradle, NDK CMake build, and whisper.cpp checkout are all cached. - apps/agor-android/gradlew + gradlew.bat + gradle-wrapper.jar: complete the Gradle wrapper so CI (and any fresh checkout) can run ./gradlew without a system Gradle install. - flake.nix: add packages.build-agor-android-apk and apps.build-agor-android-apk one-shot builders, plus a devShells.android shell with all toolchains (JDK 17, Android SDK 35, build-tools 35.0.0, NDK 27.1.12297006, CMake 3.22.1) sourced from nixpkgs via androidenv.composeAndroidPackages. NixOS users can run \`nix run .#build-agor-android-apk\` to produce the APK. - apps/agor-android/README.md: document the CI artifact and Nix entry points.
Pre-existing formatter drift unblocking the pre-commit lint hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the existing notification PendingIntent through to the Compose router so a tap actually opens the session, both on warm resume (onNewIntent) and from a cold launch. - AgorNotificationManager attaches the full session id as an Intent extra (URL was kept for deep-link compatibility but couldnt be reversed back to a full session id since URLs use 8-char short ids). - AppContainer holds a pendingSessionId StateFlow so MainActivity can push the id before the AppViewModel exists. - AppViewModel re-exports it; MainScreen routes to ChatScreen on first non-null emission and consumes immediately so the same id cant re-fire on recomposition. Also lands VISION.md describing the eventual scope for this app: Hermes-orchestrator-as-Agor-session driving Agor via MCP, with AnythingLLM as the long-term RAG memory tier. Phased so each step is shippable on its own. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setup-android@v3 was passing the multiline packages input as one quoted argument to sdkmanager, which then failed with "Failed to find package platform-tools\nplatforms;android-35\n…". Skip the action s package install and run sdkmanager ourselves with each package as a separate arg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`yes | sdkmanager --licenses` was failing under `set -o pipefail` because `yes` gets SIGPIPE once sdkmanager finishes reading and exits non-zero. Wrap just that pipeline in a subshell that disables pipefail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build.gradle.kts gitShortSha() shells out to git at configuration time, which Gradles configuration cache forbids. The Nix flake script already disables it; mirror that on CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
onNewIntent) and cold start. The notification carries the full session id as anIntentextra (URLs use 8-char short ids that can't be reversed to a full id without reverse-lookup).packages/executor/src/sdk-handlers/codex/prompt-service.tsso the pre-commit hook passes.How CI builds the APK
.github/workflows/build-android-apk.ymlruns on PRs targeting `main` whenever `apps/agor-android/**` changes. It produces an artifact named `agor-android-debug-`.To install on a phone:
Test plan
🤖 Generated with Claude Code