diff --git a/extensions/inference-cortex-extension/src/index.ts b/extensions/inference-cortex-extension/src/index.ts
index c453281c5..7d3cf0c4d 100644
--- a/extensions/inference-cortex-extension/src/index.ts
+++ b/extensions/inference-cortex-extension/src/index.ts
@@ -90,7 +90,7 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
Authorization: `Bearer ${apiKey}`,
}
: {},
- retry: 4,
+ retry: 10,
})
return this.api
}
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index ef8932d4e..992460ff8 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -51,6 +51,7 @@ dirs = "6.0.0"
sysinfo = "0.34.2"
ash = "0.38.0"
nvml-wrapper = "0.10.0"
+tauri-plugin-deep-link = "2"
[target.'cfg(windows)'.dependencies]
libloading = "0.8.7"
@@ -59,3 +60,4 @@ libc = "0.2.172"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"
once_cell = "1.18"
+tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist
new file mode 100644
index 000000000..b77d0febd
--- /dev/null
+++ b/src-tauri/Info.plist
@@ -0,0 +1,19 @@
+
+
+
+
+ CFBundleIdentifier
+ jan.ai.app
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ jan.ai.app
+ CFBundleURLSchemes
+
+ jan
+
+
+
+
+
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 566118a7d..9c5b69d46 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -19,6 +19,7 @@
"log:default",
"updater:default",
"dialog:default",
+ "deep-link:default",
"core:webview:allow-create-webview-window",
{
"identifier": "http:default",
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 23343bc43..436e4c0ba 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -14,8 +14,16 @@ use reqwest::blocking::Client;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
- tauri::Builder::default()
- .plugin(tauri_plugin_os::init())
+ let mut builder = tauri::Builder::default();
+ #[cfg(desktop)]
+ {
+ builder = builder.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| {
+ println!("a new app instance was opened with {argv:?} and the deep link event was already triggered");
+ // when defining deep link schemes at runtime, you must also check `argv` here
+ }));
+ }
+ builder.plugin(tauri_plugin_os::init())
+ .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_http::init())
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 18108d34e..545873172 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -68,7 +68,8 @@
"windows": {
"installMode": "passive"
}
- }
+ },
+ "deep-link": { "schemes": ["jan"] }
},
"bundle": {
"active": true,
diff --git a/web-app/package.json b/web-app/package.json
index d0941dd2b..ad82e5688 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -30,6 +30,7 @@
"@tanstack/react-router": "^1.116.0",
"@tanstack/react-router-devtools": "^1.116.0",
"@tauri-apps/api": "^2.5.0",
+ "@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
diff --git a/web-app/src/providers/DataProvider.tsx b/web-app/src/providers/DataProvider.tsx
index 9e2f50d00..49d9dcf4b 100644
--- a/web-app/src/providers/DataProvider.tsx
+++ b/web-app/src/providers/DataProvider.tsx
@@ -13,6 +13,12 @@ import { getMCPConfig } from '@/services/mcp'
import { useAssistant } from '@/hooks/useAssistant'
import { getAssistants } from '@/services/assistants'
import { migrateData } from '@/utils/migration'
+import {
+ onOpenUrl,
+ getCurrent as getCurrentDeepLinkUrls,
+} from '@tauri-apps/plugin-deep-link'
+import { useNavigate } from '@tanstack/react-router'
+import { route } from '@/constants/routes'
export function DataProvider() {
const { setProviders } = useModelProvider()
@@ -21,6 +27,7 @@ export function DataProvider() {
const { checkForUpdate } = useAppUpdater()
const { setServers } = useMCPServers()
const { setAssistants } = useAssistant()
+ const navigate = useNavigate()
useEffect(() => {
fetchModels().then((models) => {
@@ -39,6 +46,8 @@ export function DataProvider() {
console.warn('Failed to load assistants, keeping default:', error)
})
migrateData()
+ getCurrentDeepLinkUrls().then(handleDeepLink)
+ onOpenUrl(handleDeepLink)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -58,5 +67,27 @@ export function DataProvider() {
checkForUpdate()
}, [checkForUpdate])
+ const handleDeepLink = (urls: string[] | null) => {
+ if (!urls) return
+ console.log('Received deeplink:', urls)
+ const deeplink = urls[0]
+ if (deeplink) {
+ const url = new URL(deeplink)
+ const params = url.pathname.split('/').filter((str) => str.length > 0)
+
+ if (params.length < 3) return undefined
+ // const action = params[0]
+ // const provider = params[1]
+ const resource = params.slice(1).join('/')
+ // return { action, provider, resource }
+ navigate({
+ to: route.hub,
+ search: {
+ repo: resource,
+ },
+ })
+ }
+ }
+
return null
}
diff --git a/web-app/src/routes/hub.tsx b/web-app/src/routes/hub.tsx
index ec58f9252..ecff4276d 100644
--- a/web-app/src/routes/hub.tsx
+++ b/web-app/src/routes/hub.tsx
@@ -1,4 +1,10 @@
-import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ createFileRoute,
+ Link,
+ useNavigate,
+ useSearch,
+} from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { useModelSources } from '@/hooks/useModelSources'
import { cn, fuzzySearch, toGigabytes } from '@/lib/utils'
@@ -37,10 +43,15 @@ type ModelProps = {
}[]
}
}
+type SearchParams = {
+ repo: string
+}
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.hub as any)({
component: Hub,
+ validateSearch: (search: Record): SearchParams => ({
+ repo: search.repo as SearchParams['repo'],
+ }),
})
const sortOptions = [
@@ -50,6 +61,7 @@ const sortOptions = [
function Hub() {
const { sources, fetchSources, loading } = useModelSources()
+ const search = useSearch({ from: route.hub as any })
const [searchValue, setSearchValue] = useState('')
const [sortSelected, setSortSelected] = useState('newest')
const [expandedModels, setExpandedModels] = useState>(
@@ -71,6 +83,22 @@ function Hub() {
}))
}
+ useEffect(() => {
+ if (search.repo) {
+ setSearchValue(search.repo || '')
+ setIsSearching(true)
+ addModelSourceTimeoutRef.current = setTimeout(() => {
+ addModelSource(search.repo)
+ .then(() => {
+ fetchSources()
+ })
+ .finally(() => {
+ setIsSearching(false)
+ })
+ }, 500)
+ }
+ }, [fetchSources, search])
+
// Sorting functionality
const sortedModels = useMemo(() => {
return [...sources].sort((a, b) => {