Deeplinks are the shared URL contract that lets every ADE surface — desktop, ADE Code TUI, iOS, the marketing site, and external tools (Linear, GitHub PR descriptions, chat apps) — point at the same lane, work session, branch, PR, or Linear issue. Two forms carry identical semantics:
ade://lane/<uuid>
ade://session/<id>[?lane=<lane-uuid>]
ade://repo/<owner>/<repo>/branch/<branch>[?pr=<n>]
ade://pr/<owner>/<repo>/<number>
ade://linear-issue/<ADE-123>[?branch=<branch>]
https://ade-app.dev/open?type=lane&id=<uuid>
https://ade-app.dev/open?type=session&id=<id>[&lane=<lane-uuid>]
https://ade-app.dev/open?type=branch&repo=<owner>/<repo>&branch=<branch>[&pr=<n>]
https://ade-app.dev/open?type=pr&repo=<owner>/<repo>&number=<n>
https://ade-app.dev/open?type=linear-issue&issue=<ADE-123>[&branch=<branch>]
The HTTPS form lives on apps/web (Vercel) and acts as a marketing landing
page plus an OS-level upgrade into the ade:// form when an ADE client is
registered. The ade:// form routes directly through the OS to the running
desktop process (or starts it cold). Both forms parse to the same
AppNavigationTarget shape and dispatch through IPC.appNavigate.
Shared contract:
apps/desktop/src/shared/deeplinks.ts— builder + parser shared across main, renderer, ADE CLI, and the web/openAPI route. Validates UUIDs, GitHub owner/repo, Linear issue identifiers, and branch refs (rejects traversal, control chars, trailing.lock). ExportsbuildDeeplink,parseDeeplink,looksLikeAdeDeeplink, anddescribeTargetplus theDeeplinkTargetunion (lane | session | branch | pr | linear-issue).apps/desktop/src/shared/adeDeeplinkFooter.ts— renders the branded "Open in ADE" footer block (markdown + small HTML subset) appended to GitHub PR descriptions and reused as Linear attachment subtitle. Idempotent: the block is wrapped in<!-- ade:link v=1 ... -->/<!-- /ade:link -->markers so subsequent renders replace in place instead of appending duplicates. Used byprService.tswhen creating or updating PRs.apps/desktop/src/shared/types/core.ts—AppNavigationTarget/AppNavigationRequest/AppNavigationResultcarry the parsed deeplink payload across IPC. Targets coverlane,chat/work,pr(with optional repoOwner/repoName for not-yet-local PRs),branch(cross-machine send-to-mac payload),linear-issue, and the genericrouteshape.
Desktop main process — protocol handler:
apps/desktop/src/main/services/deeplinks/protocolHandler.ts— registers ADE as the OS handler for theade://scheme, acquires the single-instance lock so a secondopen ade://...reuses the running window, listens foropen-url(macOS) andsecond-instance(Win/Linux), buffers URLs received beforeapp.whenReady(), and on dispatch parses the URL, maps it to anAppNavigationTarget, and forwards through the caller-supplied dispatcher.main.tswires the dispatcher to focus the most-suitableBrowserWindowandwebContents.send(IPC.appNavigate, …).handleDeeplinkUrlis also re-used by the iOS Send-to-Mac sync command (syncRemoteCommandService.ts'sdeeplinks.open).apps/desktop/src/main/services/deeplinks/projectNavigationWindowSelection.ts— pure selection helper for project-scoped navigation. It first prefers a window whose active project already matches the target root, then a window with the target root open in a background project tab, asking main to activate that project before dispatching. Only when no existing window/tab can own the project does main open a new ADE window.apps/desktop/src/main/main.ts— callsregisterAdeProtocolHandler({...})beforeapp.whenReady()so cold-start URLs aren't lost. The iOSdeeplinks.openpath calls the project-scoped dispatcher for the sync host's project root so Send-to-Mac targets the paired project's window/tab instead of whichever ADE window is focused locally.
Desktop main process — PR footer integration:
apps/desktop/src/main/services/prs/prService.tscallsensureAdeDeeplinkFooterwhen creating, updating, and re-rendering PR descriptions so the branded block always reflects the current branch + PR number. Re-render fires a follow-up PATCH once the PR number is known.apps/desktop/src/main/services/cto/linearLaneCardService.tsbuilds the same deeplink target when posting Linear attachments so Linear cards can open ADE lanes, PRs, Work sessions, or the Linear pane via the cross-machinehttps://ade-app.dev/open?...URL instead of a Mac-only path.
ADE CLI — outbound + inbound:
apps/ade-cli/src/commands/deeplinks.ts—ade open,ade link, andade linear installsubcommands.ade open <url>invokes the OS opener (open/xdg-open/start) on a validatedade://orhttps://ade-app.dev/open?...URL, which routes back through the registered protocol handler. The--linear-issue <id> --branch <branch>form is what Linear's "Open issue in coding tool" entry passes; the receiving install opens the Linear pane to that issue, or shows a setup state if the project has not connected Linear yet.ade link …builds a deeplink for a lane / work session / branch / PR / Linear issue and copies it to the clipboard.--adeemits the custom scheme; the default is the HTTPS form. Round-trip form (ade link <url>) re-emits a parsed URL in the chosen form.ade linear installwrites~/.linear/coding-tools.jsonso Linear's "Open issue in coding tool" dropdown can launch ADE. Backs up the previous file alongside.
apps/ade-cli/src/cli.tsandapps/ade-cli/src/adeRpcServer.ts— PR creation formatters/tools include the ADE HTTPS PR URL next to the GitHub URL when the PR number and repo are known. Agents should use thatadeUrlin final handoffs; if a PR was adopted through another path,ade link pr <owner/repo> <number> --no-clipboardmints the same URL.apps/ade-cli/src/tuiClient/deeplinkRow.ts— pure helper used by the TUI'sCtrl+Ykeybinding. Resolves the focused row (lane / PR) to a canonicalade://URL, including parsing GitHub PR URLs to lift owner/repo/number when the right-pane only carries the URL.apps/ade-cli/src/tuiClient/keybindings/index.ts— registerscopy_deeplinksoCtrl+Yover a highlighted lane or PR row builds and copies the link viatuiClient/app.tsx.
Apps/web — landing page + OG unfurl:
apps/web/src/app/pages/OpenPage.tsx— React SPA page mounted at/open. Reads the same query shape as the parser, attempts theade://upgrade, and falls back to a download / install card with the parsed target described inline.apps/web/api/open.ts— Vercel serverless function for/open. Self- fetches/index.html, rewrites<title>,og:*, andtwitter:*meta tags from query params so chat-app unfurlers (Slack, Discord, iMessage, Gmail, Linear) show a rich card without executing JavaScript. Cache:public, max-age=600, stale-while-revalidate=86400.apps/web/vercel.json— adds/open → /api/openrewrite ahead of the catch-all SPA rewrite.
iOS — inbound deeplinks, outbound link minting, and Send-to-Mac:
apps/ios/ADE/Views/Lanes/LaneDeeplinkHelpers.swift— outbound link minting on the phone: buildsade://lane/<id>and percent-encodedade://repo/<owner>/<repo>/branch/<branch>strings for the lane detail's "Copy ADE lane link" / "Copy branch link" menu actions (branch links resolve owner/repo from a linked PR; with no GitHub remote the lane link is copied instead, with a notice).apps/ios/ADE/App/DeepLinkRouter.swift— parses inboundade://URLs.ade://session/<id>andade://pr/<n>(and the longerade://pr/<owner>/<repo>/<number>form) flip the active tab via.adeDeepLinkRequested.ade://lane/<uuid>andade://repo/<owner>/<repo>/branch/<branch>are local-only desktop concepts and instead post.adeSendToMacRequestedso the parent view shows the "Send to your Mac" confirmation card.apps/ios/ADE/Views/Deeplinks/SendToMacCard.swift— SwiftUI sheet bound to.adeSendToMacRequested. Parses the URL into aSendToMacTarget(lane,repoBranch,other) for a human-readable headline / detail and forwards the URL to a paired desktop through the sync command surface.apps/ade-cli/src/services/sync/syncRemoteCommandService.ts— receives the iOS "Send to your Mac" payload as thedeeplinks.opensync command and feeds the URL throughhandleDeeplinkUrlso the desktop dispatches it through the same parser as an OS-routedopen ade://..., with main scoping delivery to the paired sync service's project root.apps/ade-cli/src/services/sync/syncHostService.ts— exposes the samedeeplinks.opencommand from the ADE runtime so a phone paired directly to a headless runtime can still bounce a URL out through that runtime.
| Form | Target | Notes |
|---|---|---|
ade://lane/<uuid> |
{ kind: "lane", laneId } |
UUID v4 required. |
ade://session/<id>[?lane=<uuid>] |
{ kind: "session", sessionId, laneId? } |
Local Work tab session link. Routes to /work with the selected session. |
ade://repo/<owner>/<repo>/branch/<branch>[?pr=<n>] |
{ kind: "branch", repoOwner, repoName, branch, prNumber? } |
Cross-machine. Renderer routes to the lane that already owns the branch; otherwise it opens a create/import modal. PR-backed links use PR preflight, branch-only links fetch the remote branch and import it as a local lane. If no ADE project is open, the modal asks the user to open the matching project first. |
ade://pr/<owner>/<repo>/<number> |
{ kind: "pr", repoOwner, repoName, prNumber } |
If the PR isn't yet local, the renderer jumps to the PRs tab pre-filtered or falls back to the create-lane-from-branch flow. |
ade://linear-issue/<ADE-123>[?branch=<branch>] |
{ kind: "linear-issue", issueIdentifier, branch? } |
Linear hand-off. Opens ADE's Linear pane focused to the issue. If no project is open or this project is not connected to Linear, ADE shows a setup modal with the next action. |
Validation lives in one place (shared/deeplinks.ts) so the parser, the
TUI builders, and the web /open handler agree on what counts as
malformed.
┌────────────────────────────────────────────┐
│ Producer │
│ - ade link … (CLI clipboard) │
│ - Ctrl+Y in ade code (TUI copy) │
│ - PR description footer (auto-rendered) │
│ - Linear attachment (post on lane) │
│ - iOS share / chat app │
└────────────────────────────────────────────┘
│
ade:// URL or https://ade-app.dev/open?...
│
┌────────────────────────────────┼─────────────────────────────────┐
│ │ │
▼ ▼ ▼
Browser / chat unfurler Desktop OS handler iOS / paired phone
hits apps/web/api/open.ts (registerAdeProtocolHandler) (DeepLinkRouter.swift)
→ rich OG card + SPA → handleDeeplinkUrl → adeDeepLinkRequested
→ IPC.appNavigate → adeSendToMacRequested
→ sync deeplinks.open
on paired host
→ handleDeeplinkUrl
→ IPC.appNavigate
The parser and AppNavigationTarget mapping are the same in every case.
Delivery is project-aware: OS-routed desktop URLs use the active-window
dispatcher, while sync-originated Send-to-Mac URLs select the paired
project's existing window or background tab before IPC.appNavigate is
sent. The renderer's window.ade.app.onNavigate listener owns the routing
decision (open lane tab, jump to PR, prompt to create lane from branch,
resolve Linear issue to lane).
<!-- ade:link v=1 type=pr repo=<owner>/<repo> branch=<branch> num=<n> -->
<p>
<img src="https://ade-app.dev/logo.png" height="18" align="left" alt="ADE">
<strong>Open in ADE</strong>
· <a href="…">branch link</a>
· <a href="…">PR link</a>
</p>
<!-- /ade:link -->prService.ts calls ensureAdeDeeplinkFooter on PR create and update.
Re-renders are idempotent — the marker comments mark the block boundary
so the next call replaces it in place rather than appending. When the PR
is first created, the footer initially carries the branch link only;
once the PR number is known, a follow-up patch re-renders the block
with the PR link included.
Agent closeout still includes explicit links. ade prs create and the
private create_pr_from_lane action return both githubUrl and
adeUrl when available, and the text formatter prints them as separate
rows. The GitHub PR body footer is automatic, but the final chat or TUI
handoff should still include the GitHub URL and the ADE HTTPS PR URL so
the user can jump directly to either GitHub or the ADE PRs tab.
Only the Stable channel claims ade:// as the OS-default handler.
Beta and Alpha builds still install the single-instance lock and the
open-url / second-instance listeners (so a manual duti binding
still routes deeplinks to them), but they skip
app.setAsDefaultProtocolClient so they don't fight Stable for the
binding on machines where multiple channels are installed.
Source builds default to the same skip behavior and do not expose an env
override to claim the system binding. They can still dispatch URLs that are
explicitly delivered to that process, but the OS-default ade:// handler is
reserved for the real packaged Stable desktop build. The gate lives in
apps/desktop/src/main/main.ts (packaged Stable channel detection) and the
registration mechanics live in protocolHandler.ts behind the
claimAsDefault option.
- Register the protocol handler before
app.whenReady(). Otherwise the cold-start URL on macOS / Windows gets dropped before the listener attaches.protocolHandler.tsbuffers URLs that arrive between registration andwhenReadyso they aren't lost. - Validate before dispatch. The parser rejects malformed inputs (non-UUID lane ids, traversal in branch refs, non-positive PR numbers, unknown hosts). Don't bypass it for "trusted" callers — Linear's template substitution can produce empty strings.
- The HTTPS form is the social form. When linking from chat apps,
emails, or anywhere a preview matters, prefer the
https://ade-app.dev/openshape so the unfurl works. Useade://when the target is guaranteed to be a machine with ADE installed (TUI copy, terminal share). - Linear hand-off doesn't carry the GitHub repo. The
linear-issueshape only has the identifier (ADE-123) and optionally Linear's generated branch name. The desktop opens the Linear pane to that issue inside the active project. From there the user can create a lane, start a chat, or connect Linear if this project has not been authorized yet. If no project is open, ADE shows a setup modal because the Linear issue identifier alone is not enough to choose a local repo. - Branch links are the portable lane form. Lane UUID links are local, but branch links can be shared across machines. If the receiver does not already have that lane, ADE fetches remotes and imports the branch as a local worktree-backed lane. Without an active project, the import modal stays read-only and asks the user to open the matching ADE project first.
- The PR footer is GitHub-flavored markdown, not full HTML. Only the
<p>,<img>,<a>,<strong>subset renders. Linear accepts the same subset.
- Pull requests — PR description rendering, footer integration.
- Sync and multi-device — the iOS Send-to-Mac sync command bounces deeplinks to a paired host.
- ADE Code —
ade open/ade link/ade linear installsubcommands,Ctrl+Ycopy. - System overview —
IPC.appNavigate, web /open route placement.