summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorHegyi Áron Ferenc <[email protected]>2026-01-29 08:09:53 +0100
committerGitHub <[email protected]>2026-01-29 15:09:53 +0800
commit2af326606c936380c303bf56506e3c8bed04b0eb (patch)
tree2c1fe35b7f5f77f88869291168e01e7ab220b566
parent7c0067d59d318bfd6ecd473c36a9e673a4f68ff9 (diff)
downloadopencode-2af326606c936380c303bf56506e3c8bed04b0eb.tar.gz
opencode-2af326606c936380c303bf56506e3c8bed04b0eb.zip
feat(desktop): Add desktop deep link (#10072)
Co-authored-by: Brendan Allan <[email protected]>
-rw-r--r--bun.lock3
-rw-r--r--packages/app/src/app.tsx2
-rw-r--r--packages/app/src/pages/layout.tsx40
-rw-r--r--packages/desktop/package.json1
-rw-r--r--packages/desktop/src-tauri/Cargo.lock106
-rw-r--r--packages/desktop/src-tauri/Cargo.toml3
-rw-r--r--packages/desktop/src-tauri/capabilities/default.json1
-rw-r--r--packages/desktop/src-tauri/src/lib.rs6
-rw-r--r--packages/desktop/src-tauri/tauri.conf.json7
-rw-r--r--packages/desktop/src/index.tsx18
10 files changed, 181 insertions, 6 deletions
diff --git a/bun.lock b/bun.lock
index a8c14f386..30b0cecb0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -189,6 +189,7 @@
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "~2",
@@ -1748,6 +1749,8 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
+ "@tauri-apps/plugin-deep-link": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
+
"@tauri-apps/plugin-dialog": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
"@tauri-apps/plugin-http": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index ba0d1e7aa..11fdb5743 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
declare global {
interface Window {
- __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
+ __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
}
}
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index afef14c84..73480e8f2 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -1136,6 +1136,46 @@ export default function Layout(props: ParentProps) {
if (navigate) navigateToProject(directory)
}
+ const deepLinkEvent = "opencode:deep-link"
+
+ const parseDeepLink = (input: string) => {
+ if (!input.startsWith("opencode://")) return
+ const url = new URL(input)
+ if (url.hostname !== "open-project") return
+ const directory = url.searchParams.get("directory")
+ if (!directory) return
+ return directory
+ }
+
+ const handleDeepLinks = (urls: string[]) => {
+ if (!server.isLocal()) return
+ for (const input of urls) {
+ const directory = parseDeepLink(input)
+ if (!directory) continue
+ openProject(directory)
+ }
+ }
+
+ const drainDeepLinks = () => {
+ const pending = window.__OPENCODE__?.deepLinks ?? []
+ if (pending.length === 0) return
+ if (window.__OPENCODE__) window.__OPENCODE__.deepLinks = []
+ handleDeepLinks(pending)
+ }
+
+ onMount(() => {
+ const handler = (event: Event) => {
+ const detail = (event as CustomEvent<{ urls: string[] }>).detail
+ const urls = detail?.urls ?? []
+ if (urls.length === 0) return
+ handleDeepLinks(urls)
+ }
+
+ drainDeepLinks()
+ window.addEventListener(deepLinkEvent, handler as EventListener)
+ onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener))
+ })
+
const displayName = (project: LocalProject) => project.name || getFilename(project.worktree)
async function renameProject(project: LocalProject, next: string) {
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 5ba2ec347..0047cde20 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -18,6 +18,7 @@
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2",
diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock
index 294d7ad6c..8e7e9af0d 100644
--- a/packages/desktop/src-tauri/Cargo.lock
+++ b/packages/desktop/src-tauri/Cargo.lock
@@ -610,6 +610,26 @@ dependencies = [
]
[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -981,6 +1001,15 @@ dependencies = [
]
[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1779,6 +1808,12 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
@@ -1930,7 +1965,7 @@ dependencies = [
"tokio",
"tower-service",
"tracing",
- "windows-registry",
+ "windows-registry 0.6.1",
]
[[package]]
@@ -2345,7 +2380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80"
dependencies = [
"freedesktop_entry_parser",
- "rust-ini",
+ "rust-ini 0.17.0",
]
[[package]]
@@ -3038,6 +3073,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
+ "tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
@@ -3067,11 +3103,21 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
dependencies = [
- "dlv-list",
+ "dlv-list 0.2.3",
"hashbrown 0.9.1",
]
[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list 0.5.2",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3947,7 +3993,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
dependencies = [
"cfg-if",
- "ordered-multimap",
+ "ordered-multimap 0.3.1",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap 0.7.3",
]
[[package]]
@@ -4818,6 +4874,27 @@ dependencies = [
]
[[package]]
+name = "tauri-plugin-deep-link"
+version = "2.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "444b091f24f2f6bdb4a305b54d3961f629c11861c685aceeea9a1972f89e43d5"
+dependencies = [
+ "dunce",
+ "plist",
+ "rust-ini 0.21.3",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.17",
+ "tracing",
+ "url",
+ "windows-registry 0.5.3",
+ "windows-result 0.3.4",
+]
+
+[[package]]
name = "tauri-plugin-dialog"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4980,6 +5057,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
+ "tauri-plugin-deep-link",
"thiserror 2.0.17",
"tracing",
"windows-sys 0.60.2",
@@ -5272,6 +5350,15 @@ dependencies = [
]
[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
name = "tinystr"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6210,6 +6297,17 @@ dependencies = [
[[package]]
name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml
index b875f928b..6a0219ae4 100644
--- a/packages/desktop/src-tauri/Cargo.toml
+++ b/packages/desktop/src-tauri/Cargo.toml
@@ -20,6 +20,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["macos-private-api", "devtools"] }
tauri-plugin-opener = "2"
+tauri-plugin-deep-link = "2.4.6"
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
@@ -29,7 +30,7 @@ tauri-plugin-window-state = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-http = "2"
tauri-plugin-notification = "2"
-tauri-plugin-single-instance = "2"
+tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
diff --git a/packages/desktop/src-tauri/capabilities/default.json b/packages/desktop/src-tauri/capabilities/default.json
index 12de32931..66f068af8 100644
--- a/packages/desktop/src-tauri/capabilities/default.json
+++ b/packages/desktop/src-tauri/capabilities/default.json
@@ -6,6 +6,7 @@
"permissions": [
"core:default",
"opener:default",
+ "deep-link:default",
"core:window:allow-start-dragging",
"core:window:allow-set-theme",
"core:webview:allow-set-webview-zoom",
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index dab98f4a0..29ac86f29 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -16,6 +16,8 @@ use std::{
time::{Duration, Instant},
};
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
+#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
+use tauri_plugin_deep_link::DeepLinkExt;
#[cfg(windows)]
use tauri_plugin_decorum::WebviewWindowExt;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
@@ -263,6 +265,7 @@ pub fn run() {
let _ = window.unminimize();
}
}))
+ .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_os::init())
.plugin(
tauri_plugin_window_state::Builder::new()
@@ -291,6 +294,9 @@ pub fn run() {
markdown::parse_markdown_command
])
.setup(move |app| {
+ #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
+ app.deep_link().register_all().ok();
+
let app = app.handle().clone();
// Initialize log state
diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json
index f8df151bd..5f76d510b 100644
--- a/packages/desktop/src-tauri/tauri.conf.json
+++ b/packages/desktop/src-tauri/tauri.conf.json
@@ -52,5 +52,12 @@
"sidebarImage": "assets/nsis-sidebar.bmp"
}
}
+ },
+ "plugins": {
+ "deep-link": {
+ "desktop": {
+ "schemes": ["opencode"]
+ }
+ }
}
}
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index 344c6be8d..2e7ca136a 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -3,6 +3,7 @@ import "./webview-zoom"
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
+import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { check, Update } from "@tauri-apps/plugin-updater"
@@ -42,6 +43,22 @@ window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
let update: Update | null = null
+const deepLinkEvent = "opencode:deep-link"
+
+const emitDeepLinks = (urls: string[]) => {
+ if (urls.length === 0) return
+ window.__OPENCODE__ ??= {}
+ const pending = window.__OPENCODE__.deepLinks ?? []
+ window.__OPENCODE__.deepLinks = [...pending, ...urls]
+ window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } }))
+}
+
+const listenForDeepLinks = async () => {
+ const startUrls = await getCurrent().catch(() => null)
+ if (startUrls?.length) emitDeepLinks(startUrls)
+ await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined)
+}
+
const createPlatform = (password: Accessor<string | null>): Platform => ({
platform: "desktop",
os: (() => {
@@ -332,6 +349,7 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
})
createMenu()
+void listenForDeepLinks()
render(() => {
const [serverPassword, setServerPassword] = createSignal<string | null>(null)