Skip to content

Commit 54a9951

Browse files
RettendBrendonovich
authored andcommitted
feat(desktop): Add desktop deep link (anomalyco#10072)
Co-authored-by: Brendan Allan <[email protected]>
1 parent 73e9fa0 commit 54a9951

10 files changed

Lines changed: 181 additions & 6 deletions

File tree

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app/src/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
4343

4444
declare global {
4545
interface Window {
46-
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
46+
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
4747
}
4848
}
4949

packages/app/src/pages/layout.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,46 @@ export default function Layout(props: ParentProps) {
11361136
if (navigate) navigateToProject(directory)
11371137
}
11381138

1139+
const deepLinkEvent = "opencode:deep-link"
1140+
1141+
const parseDeepLink = (input: string) => {
1142+
if (!input.startsWith("opencode://")) return
1143+
const url = new URL(input)
1144+
if (url.hostname !== "open-project") return
1145+
const directory = url.searchParams.get("directory")
1146+
if (!directory) return
1147+
return directory
1148+
}
1149+
1150+
const handleDeepLinks = (urls: string[]) => {
1151+
if (!server.isLocal()) return
1152+
for (const input of urls) {
1153+
const directory = parseDeepLink(input)
1154+
if (!directory) continue
1155+
openProject(directory)
1156+
}
1157+
}
1158+
1159+
const drainDeepLinks = () => {
1160+
const pending = window.__OPENCODE__?.deepLinks ?? []
1161+
if (pending.length === 0) return
1162+
if (window.__OPENCODE__) window.__OPENCODE__.deepLinks = []
1163+
handleDeepLinks(pending)
1164+
}
1165+
1166+
onMount(() => {
1167+
const handler = (event: Event) => {
1168+
const detail = (event as CustomEvent<{ urls: string[] }>).detail
1169+
const urls = detail?.urls ?? []
1170+
if (urls.length === 0) return
1171+
handleDeepLinks(urls)
1172+
}
1173+
1174+
drainDeepLinks()
1175+
window.addEventListener(deepLinkEvent, handler as EventListener)
1176+
onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener))
1177+
})
1178+
11391179
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
11401180

11411181
async function renameProject(project: LocalProject, next: string) {

packages/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@solid-primitives/i18n": "2.2.1",
1919
"@solid-primitives/storage": "catalog:",
2020
"@tauri-apps/api": "^2",
21+
"@tauri-apps/plugin-deep-link": "~2",
2122
"@tauri-apps/plugin-dialog": "~2",
2223
"@tauri-apps/plugin-opener": "^2",
2324
"@tauri-apps/plugin-os": "~2",

packages/desktop/src-tauri/Cargo.lock

Lines changed: 102 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/desktop/src-tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ tauri-build = { version = "2", features = [] }
2020
[dependencies]
2121
tauri = { version = "2", features = ["macos-private-api", "devtools"] }
2222
tauri-plugin-opener = "2"
23+
tauri-plugin-deep-link = "2.4.6"
2324
tauri-plugin-shell = "2"
2425
tauri-plugin-dialog = "2"
2526
tauri-plugin-updater = "2"
@@ -29,7 +30,7 @@ tauri-plugin-window-state = "2"
2930
tauri-plugin-clipboard-manager = "2"
3031
tauri-plugin-http = "2"
3132
tauri-plugin-notification = "2"
32-
tauri-plugin-single-instance = "2"
33+
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
3334

3435
serde = { version = "1", features = ["derive"] }
3536
serde_json = "1"

packages/desktop/src-tauri/capabilities/default.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"permissions": [
77
"core:default",
88
"opener:default",
9+
"deep-link:default",
910
"core:window:allow-start-dragging",
1011
"core:window:allow-set-theme",
1112
"core:webview:allow-set-webview-zoom",

packages/desktop/src-tauri/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use std::{
1616
time::{Duration, Instant},
1717
};
1818
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
19+
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
20+
use tauri_plugin_deep_link::DeepLinkExt;
1921
#[cfg(windows)]
2022
use tauri_plugin_decorum::WebviewWindowExt;
2123
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
@@ -263,6 +265,7 @@ pub fn run() {
263265
let _ = window.unminimize();
264266
}
265267
}))
268+
.plugin(tauri_plugin_deep_link::init())
266269
.plugin(tauri_plugin_os::init())
267270
.plugin(
268271
tauri_plugin_window_state::Builder::new()
@@ -291,6 +294,9 @@ pub fn run() {
291294
markdown::parse_markdown_command
292295
])
293296
.setup(move |app| {
297+
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
298+
app.deep_link().register_all().ok();
299+
294300
let app = app.handle().clone();
295301

296302
// Initialize log state

packages/desktop/src-tauri/tauri.conf.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,12 @@
5252
"sidebarImage": "assets/nsis-sidebar.bmp"
5353
}
5454
}
55+
},
56+
"plugins": {
57+
"deep-link": {
58+
"desktop": {
59+
"schemes": ["opencode"]
60+
}
61+
}
5562
}
5663
}

packages/desktop/src/index.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./webview-zoom"
33
import { render } from "solid-js/web"
44
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
55
import { open, save } from "@tauri-apps/plugin-dialog"
6+
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
67
import { open as shellOpen } from "@tauri-apps/plugin-shell"
78
import { type as ostype } from "@tauri-apps/plugin-os"
89
import { check, Update } from "@tauri-apps/plugin-updater"
@@ -42,6 +43,22 @@ window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
4243

4344
let update: Update | null = null
4445

46+
const deepLinkEvent = "opencode:deep-link"
47+
48+
const emitDeepLinks = (urls: string[]) => {
49+
if (urls.length === 0) return
50+
window.__OPENCODE__ ??= {}
51+
const pending = window.__OPENCODE__.deepLinks ?? []
52+
window.__OPENCODE__.deepLinks = [...pending, ...urls]
53+
window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } }))
54+
}
55+
56+
const listenForDeepLinks = async () => {
57+
const startUrls = await getCurrent().catch(() => null)
58+
if (startUrls?.length) emitDeepLinks(startUrls)
59+
await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
60+
}
61+
4562
const createPlatform = (password: Accessor<string | null>): Platform => ({
4663
platform: "desktop",
4764
os: (() => {
@@ -332,6 +349,7 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
332349
})
333350

334351
createMenu()
352+
void listenForDeepLinks()
335353

336354
render(() => {
337355
const [serverPassword, setServerPassword] = createSignal<string | null>(null)

0 commit comments

Comments
 (0)