Add electric-agents:// deep links to open sessions in desktop & mobile apps#4586
Add electric-agents:// deep links to open sessions in desktop & mobile apps#4586balegas wants to merge 22 commits into
Conversation
Adds sessionAppUrl, isSessionDeepLink, and parseSessionDeepLink to sessionLinks.ts, with a vitest config and expo-linking stub so that tests can run without react-native's Flow-typed source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4586 +/- ##
==========================================
+ Coverage 57.31% 58.87% +1.55%
==========================================
Files 341 388 +47
Lines 39765 42657 +2892
Branches 11559 12227 +668
==========================================
+ Hits 22791 25113 +2322
- Misses 16934 17469 +535
- Partials 40 75 +35
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
✅ Deploy Preview for electric-next ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Electric Agents Mobile BuildLocal mobile checks ran for commit The EAS Android preview build was skipped because the |
Claude Code ReviewSummary This PR adds What is Working Well
Issues Found Critical (Must Fix): None. Important (Should Fix): None. Suggestions (Nice to Have):
Issue Conformance Unchanged from iteration 1 and still good: the PR closes #4584 with one intentional, documented scheme/format deviation ( Previous Review Status All four items from iteration 1 resolved:
Plus the dispatch-layer tests and doc-removal suggestions were taken on. Review iteration: 2 | 2026-06-16 |
Resolve the review on PR #4586: - Desktop cold start no longer drops the deep link or routes it to the wrong server. The payload flows through a single pull (desktop:get-pending-session); the renderer pulls on mount and on an onOpenSession wake-up signal, and only navigates once `servers` has hydrated the target server. - Mobile refuses an unknown self-hosted server from a link instead of silently adding + switching to it, mirroring the desktop. Routing logic extracted into a pure, unit-tested decideOpenSession state machine. - Tighten the open-session host match to a boundary check so `open-sessionfoo` can't match. - Dedup the link format into one shared module (@electric-ax/agents-runtime/session-links) re-exported by desktop, mobile and web; collapse the three hostOf copies into serverHost.ts. - Add dispatch-layer tests (resolveOpenSessionPayload, decideOpenSession) and a shared round-trip suite. - Remove the large planning/spec docs from the tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review — addressed all of it in e565705. Important1. Desktop cold-start delivery + wrong-server routing 2. Mobile silently adding an unknown server Suggestions
Also updated the docs page so the mobile "unknown server" description matches the new refuse behavior. All four packages typecheck and lint clean, and the new/updated unit suites pass (48 tests). |
msfstef
left a comment
There was a problem hiding this comment.
Reviewed the deep-links work end to end — overall it's a careful, well-tested implementation. The pull-not-push IPC on desktop (stash in main, renderer pulls + clears via getPendingSession) correctly sidesteps the cold-start listener race, and extracting the pure helpers (resolveOpenSessionPayload, decideOpenSession, session-links) so the branchy parts are unit-testable is the right call.
Two things flagged inline — one should-fix (mobile Cloud links), one non-blocking discussion point (mobile share link format) — plus one housekeeping note:
Rebase + regenerate the lockfile. pnpm-lock.yaml is ~830 lines smaller and includes a batch of unrelated babel/transitive downgrades (e.g. @babel/compat-data 7.29.7 → 7.29.3, several @babel/plugin-* 7.29.7 → 7.27.1/7.28.x). That looks like a pnpm install against a stale tree rather than anything this feature needs. Could you rebase on main and regenerate the lockfile with a fresh pnpm install so the PR doesn't carry an accidental dependency rollback?
|
|
||
| if (activeMatches) return { kind: `route`, entityUrl: target.entityUrl } | ||
| // A Cloud server we're not already signed into can't be switched to silently. | ||
| if (isCloudServer(targetServer)) return { kind: `abandon` } |
There was a problem hiding this comment.
Should-fix: mobile silently drops links to saved Cloud servers, and diverges from desktop here.
Because isCloudServer is checked before isSavedServer, any non-active Cloud server returns abandon even when the user has already added and signed into it. And abandon redirects to / with no message (open-session.tsx) — strictly worse feedback than the explanatory refuse screen an unknown self-hosted server gets. Desktop, meanwhile, resolves saved Cloud servers via findSavedServerForUrl (no source filter, unlike findCloudServerForUrl) and does switch + open them. So the same shared link opens on desktop but silently no-ops on mobile.
The "Cloud needs interactive sign-in" rationale doesn't hold for an already-saved server: HomeMenu.selectServer switches to a Cloud server with just prepareServerHeaders(url) + saveServerUrl(url) — no prompt; the token is derived from the URL service-id + the stored dashboard JWT, which is exactly what the switch decision triggers. An expired token just 401s in-session, same as a manual switch.
Suggested reorder so the saved-server check handles cloud + self-hosted uniformly:
if (activeMatches) return { kind: `route`, entityUrl: target.entityUrl }
if (isSavedServer(targetServer)) return { kind: `switch`, serverUrl: targetServer }
// Only an *unsaved* Cloud server needs an interactive sign-in we can't do from a link.
if (isCloudServer(targetServer)) return { kind: `abandon` } // ideally surface a "sign in first" message instead of a silent redirect
return { kind: `refuse`, host: hostOf(targetServer) }This makes mobile match desktop. Note the cloud-abandon unit test (openSessionDecision.test.ts:38-45, which hardcodes isSavedServer: () => false) and the comments at openSessionDecision.ts:9 / :47 encode the old premise and would need updating alongside.
|
|
||
| const shareLink = async (): Promise<void> => { | ||
| const url = sessionWebUrl(serverUrl, entityUrl) | ||
| const url = sessionAppUrl(serverUrl, entityUrl) |
There was a problem hiding this comment.
Non-blocking — flagging for discussion, not requesting a change.
This swaps the mobile share link from an https web link (sessionWebUrl) to the custom-scheme electric-agents:// link. Worth being deliberate about the trade-off, because custom-scheme links are a known weak spot for sharing specifically: many clients won't make them clickable (Gmail strips them outright; Slack/Notion/etc. render them as inert text), and if the recipient doesn't have the app installed there's no fallback — vs. an https/universal link that can open a web page. Desktop's custom scheme is fine (that's the standard Electron pattern); it's the mobile share path where this narrows where a pasted link actually works.
Deferring full Universal/App Links (which need hosted apple-app-site-association / assetlinks.json + domain control) to a later pass is totally reasonable. The low-effort middle ground, if we want to keep paste-into-chat working now, is to have the share sheet emit an https link to a small redirect page that attempts the electric-agents:// handoff and falls back gracefully — and those same https links upgrade into seamless universal links later. Not blocking; just want it to be a conscious call rather than a side effect of "custom protocol only."
Closes #4584
Summary
Adds deep links that open a specific agent session directly in the Electric Agents desktop (Electron) and mobile (Expo/React Native) apps.
Link format (self-contained — a session is identified by server + entityUrl, so both are carried):
electric-agents(already registered for OAuth on mobile); desktop now registers it too.open-session(notsession) so expo-router doesn't auto-route it to the internal/sessionscreen — a dedicated landing route handles it. The same link works on both platforms.Desktop (Electron)
electric-agentsscheme (setAsDefaultProtocolClient+electron-builder.ymlprotocols).open-url(with a pre-ready queue), Windows/Linux cold-startargv, and warm-startsecond-instance.desktop:open-sessionIPC channel → controller resolves the link's server against saved servers → renderer switches to that server and navigates to the session. Unknown server → a toast ("you haven't added this server").Mobile (Expo/RN)
Linkinglistener) as apendingSessionLink.app/open-session.tsxlanding route switches to the link's server when needed (self-hosted; Cloud servers need sign-in and are not silently switched), then opens the session.Sharing (generate)
ShareSessionScreen: the Session link now emits theelectric-agents://open-sessionapp link (replacing the web link), per the issue's "custom protocol only".ShareEntityDialog: new Copy session link action.Docs
website/docs/agents/usage/sharing-and-deep-links.md(+ sidebar entry).Out of scope (v1)
Web/https session links & server-side redirects; browser fallback when the app isn't installed; auto-adding an unknown server from a link; deep links for non-session entity types (the format leaves room).
Design / plan
Committed under
docs/superpowers/specs/anddocs/superpowers/plans/for reviewers.Test plan
agents-mobile, vitest, in CI), desktop renderer builder (agents-server-ui, vitest, in CI), desktop main parser (agents-desktop, node:test — matches the package's existing convention; typechecked in CI).pnpm typecheckgreen acrossagents-mobile,agents-server-ui,agents-desktop; full mobile (92) and server-ui (104) suites pass.open "electric-agents://open-session?server=…&entity=…"(macOS)xcrun simctl openurl booted "electric-agents://open-session?…"(iOS)adb shell am start -a android.intent.action.VIEW -d "electric-agents://open-session?…"(Android)🤖 Generated with Claude Code