diff --git a/apps/shinkai-desktop/src-tauri/Cargo.lock b/apps/shinkai-desktop/src-tauri/Cargo.lock index 2b082dff0..6ed2573f0 100644 --- a/apps/shinkai-desktop/src-tauri/Cargo.lock +++ b/apps/shinkai-desktop/src-tauri/Cargo.lock @@ -5617,6 +5617,7 @@ dependencies = [ "serde", "serde_json", "tauri", + "tauri-plugin-deep-link", "thiserror 2.0.6", "tracing", "windows-sys 0.59.0", diff --git a/apps/shinkai-desktop/src-tauri/Cargo.toml b/apps/shinkai-desktop/src-tauri/Cargo.toml index d371ade1f..c386aee76 100644 --- a/apps/shinkai-desktop/src-tauri/Cargo.toml +++ b/apps/shinkai-desktop/src-tauri/Cargo.toml @@ -42,7 +42,7 @@ uuid = "1.10.0" tauri-plugin-global-shortcut = "2.2" tauri-plugin-shell = "2.2" -tauri-plugin-single-instance = "2.2" +tauri-plugin-single-instance = { version = "2.2", features = ["deep-link"] } tauri-plugin-updater = "2.3" tauri-plugin-dialog = "2.2" tauri-plugin-fs ="2.2" diff --git a/apps/shinkai-desktop/src-tauri/src/deep_links.rs b/apps/shinkai-desktop/src-tauri/src/deep_links.rs new file mode 100644 index 000000000..7bcf84386 --- /dev/null +++ b/apps/shinkai-desktop/src-tauri/src/deep_links.rs @@ -0,0 +1,45 @@ +use tauri::{Emitter, EventTarget}; +use tauri_plugin_deep_link::DeepLinkExt; + +use crate::windows::{recreate_window, Window}; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct OAuthDeepLinkPayload { + pub state: String, + pub code: String, +} + +pub fn setup_deep_links(app: &tauri::AppHandle) -> tauri::Result<()> { + #[cfg(any(windows, target_os = "linux"))] + { + use tauri_plugin_deep_link::DeepLinkExt; + app.deep_link() + .register_all() + .map_err(|e| tauri::Error::Anyhow(e.into()))?; + } + let app_handle = app.clone(); + app.deep_link().on_open_url(move |event| { + let urls: Vec<_> = event.urls().into_iter().collect(); + log::debug!("deep link URLs: {:?}", urls); + for url in urls { + log::debug!("handling deep link: {:?}", url); + if let Some(host) = url.host() { + if host.to_string() == "oauth" { + log::debug!("oauth deep link: {:?}", url); + let payload = OAuthDeepLinkPayload { + state: url.query().unwrap_or_default().to_string(), + code: url.query().unwrap_or_default().to_string(), + }; + log::debug!("emitting oauth-deep-link event to {}", Window::Coordinator.as_str()); + let _ = recreate_window(app_handle.clone(), Window::Main, true); + let _ = app_handle.emit_to( + EventTarget::webview_window(Window::Coordinator.as_str()), + "oauth-deep-link", + payload, + ); + } + } + } + }); + Ok(()) +} diff --git a/apps/shinkai-desktop/src-tauri/src/global_shortcuts/toggle_spotlight.rs b/apps/shinkai-desktop/src-tauri/src/global_shortcuts/toggle_spotlight.rs index 505839c78..0b08dcac9 100644 --- a/apps/shinkai-desktop/src-tauri/src/global_shortcuts/toggle_spotlight.rs +++ b/apps/shinkai-desktop/src-tauri/src/global_shortcuts/toggle_spotlight.rs @@ -11,5 +11,5 @@ pub fn toggle_spotlight(app: &tauri::AppHandle, _: Shortcut, _: ShortcutEvent) { return; } } - recreate_window(app.clone(), Window::Spotlight, true) + recreate_window(app.clone(), Window::Spotlight, true); } diff --git a/apps/shinkai-desktop/src-tauri/src/main.rs b/apps/shinkai-desktop/src-tauri/src/main.rs index 923efb09e..be247a9e7 100644 --- a/apps/shinkai-desktop/src-tauri/src/main.rs +++ b/apps/shinkai-desktop/src-tauri/src/main.rs @@ -22,7 +22,7 @@ use tauri::{Manager, RunEvent}; use tokio::sync::Mutex; use tray::create_tray; use windows::{recreate_window, Window}; - +use deep_links::setup_deep_links; mod audio; mod commands; mod galxe; @@ -32,6 +32,7 @@ mod hardware; mod local_shinkai_node; mod tray; mod windows; +mod deep_links; #[derive(Clone, serde::Serialize)] struct Payload { @@ -41,16 +42,16 @@ struct Payload { fn main() { tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { + app.emit("single-instance", Payload { args: argv, cwd }) + .unwrap(); + })) .plugin(tauri_plugin_log::Builder::new().build()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { - app.emit("single-instance", Payload { args: argv, cwd }) - .unwrap(); - })) .plugin(tauri_plugin_dialog::init()) .plugin( tauri_plugin_global_shortcut::Builder::new() @@ -70,6 +71,7 @@ fn main() { ) .build(), ) + .plugin(tauri_plugin_deep_link::init()) .invoke_handler(tauri::generate_handler![ hide_spotlight_window_app, show_spotlight_window_app, @@ -102,6 +104,7 @@ fn main() { } create_tray(app.handle())?; + setup_deep_links(app.handle())?; /* This is the initialization pipeline @@ -139,6 +142,8 @@ fn main() { } }); + + Ok(()) }) .build(tauri::generate_context!()) diff --git a/apps/shinkai-desktop/src-tauri/src/windows/mod.rs b/apps/shinkai-desktop/src-tauri/src/windows/mod.rs index e1d9fa3d8..ca9812725 100644 --- a/apps/shinkai-desktop/src-tauri/src/windows/mod.rs +++ b/apps/shinkai-desktop/src-tauri/src/windows/mod.rs @@ -1,4 +1,4 @@ -use tauri::{AppHandle, Manager, WebviewWindowBuilder}; +use tauri::{AppHandle, Manager, WebviewWindow, WebviewWindowBuilder}; #[derive(Debug, Clone, Copy)] pub enum Window { @@ -19,7 +19,7 @@ impl Window { } } -pub fn recreate_window(app_handle: AppHandle, window_name: Window, focus: bool) { +pub fn recreate_window(app_handle: AppHandle, window_name: Window, focus: bool) -> tauri::Result { let label = window_name.as_str(); if let Some(window) = app_handle.get_webview_window(label) { log::info!("window {} found, bringing to front", label); @@ -32,6 +32,7 @@ pub fn recreate_window(app_handle: AppHandle, window_name: Window, focus: bool) // window.center().unwrap(); let _ = window.set_focus(); } + return Ok(window); } else { log::info!("window {} not found, recreating...", label); let window_config = app_handle @@ -44,13 +45,18 @@ pub fn recreate_window(app_handle: AppHandle, window_name: Window, focus: bool) .clone(); match WebviewWindowBuilder::from_config(&app_handle, &window_config) { Ok(builder) => match builder.build() { - Ok(_) => { + Ok(window) => { log::info!("window {} created", label); + return Ok(window) } - Err(e) => log::error!("failed to recreate window: {}", e), + Err(e) => { + log::error!("failed to recreate window: {}", e); + Err(e) + }, }, Err(e) => { log::error!("failed to recreate window from config: {}", e); + Err(e) } } } diff --git a/apps/shinkai-desktop/src-tauri/tauri.conf.json b/apps/shinkai-desktop/src-tauri/tauri.conf.json index 25edb7bd9..c3c57768f 100644 --- a/apps/shinkai-desktop/src-tauri/tauri.conf.json +++ b/apps/shinkai-desktop/src-tauri/tauri.conf.json @@ -38,6 +38,11 @@ "windows": { "installMode": "basicUi" } + }, + "deep-link": { + "desktop": { + "schemes": ["shinkai"] + } } }, "app": { diff --git a/apps/shinkai-desktop/src/hooks/use-oauth-deep-link.ts b/apps/shinkai-desktop/src/hooks/use-oauth-deep-link.ts new file mode 100644 index 000000000..8d33674a5 --- /dev/null +++ b/apps/shinkai-desktop/src/hooks/use-oauth-deep-link.ts @@ -0,0 +1,34 @@ +import { useSetOAuthToken } from '@shinkai_network/shinkai-node-state/v2/mutations/setOAuthToken/index'; +import { emit, listen } from '@tauri-apps/api/event'; +import { useEffect } from 'react'; + +import { useAuth } from '../store/auth'; + +export const useOAuthDeepLink = () => { + const { mutateAsync: setOAuthToken } = useSetOAuthToken({ + onSuccess: (data) => { + console.log('oauth-success', data); + emit('oauth-success', data); + }, + }); + const auth = useAuth((s) => s.auth); + + useEffect(() => { + if (!auth) return; + const unlisten = listen('oauth-deep-link', (event) => { + console.log('useOAuthDeepLink'); + + const payload = event.payload as { state: string; code: string }; + setOAuthToken({ + state: payload.state, + code: payload.code, + nodeAddress: auth.node_address ?? '', + token: auth.api_v2_key ?? '', + }); + }); + + return () => { + unlisten.then((fn) => fn()); + }; + }, [setOAuthToken, auth]); +}; diff --git a/apps/shinkai-desktop/src/windows/coordinator/main.tsx b/apps/shinkai-desktop/src/windows/coordinator/main.tsx index fe2886a7e..b2eaafa11 100644 --- a/apps/shinkai-desktop/src/windows/coordinator/main.tsx +++ b/apps/shinkai-desktop/src/windows/coordinator/main.tsx @@ -2,6 +2,7 @@ import { info } from '@tauri-apps/plugin-log'; import React, { useEffect } from 'react'; import ReactDOM from 'react-dom/client'; +import { useOAuthDeepLink } from '../../hooks/use-oauth-deep-link'; import { useSyncStorageMain, useSyncStorageSecondary, @@ -15,6 +16,8 @@ const App = () => { useSyncStorageMain(); useSyncStorageSecondary(); useSyncStorageSideEffects(); + useOAuthDeepLink(); + return null; }; diff --git a/libs/shinkai-message-ts/src/api/tools/index.ts b/libs/shinkai-message-ts/src/api/tools/index.ts index b48a0fab3..274ef2355 100644 --- a/libs/shinkai-message-ts/src/api/tools/index.ts +++ b/libs/shinkai-message-ts/src/api/tools/index.ts @@ -26,6 +26,8 @@ import { SaveToolCodeRequest, SaveToolCodeResponse, SearchPromptsResponse, + SetOAuthTokenRequest, + SetOAuthTokenResponse, UndoToolImplementationRequest, UndoToolImplementationResponse, UpdatePromptRequest, @@ -273,6 +275,7 @@ export const saveToolCode = async ( ); return response.data as SaveToolCodeResponse; }; + export const getPlaygroundTools = async ( nodeAddress: string, bearerToken: string, @@ -366,3 +369,19 @@ export const exportTool = async ( ); return response.data as ExportToolResponse; }; + +export const setOAuthToken = async ( + nodeAddress: string, + bearerToken: string, + payload: SetOAuthTokenRequest, +): Promise => { + const response = await httpClient.post( + urlJoin(nodeAddress, '/v2/set_oauth_token'), + payload, + { + headers: { Authorization: `Bearer ${bearerToken}` }, + responseType: 'json', + }, + ); + return response.data as SetOAuthTokenResponse; +}; diff --git a/libs/shinkai-message-ts/src/api/tools/types.ts b/libs/shinkai-message-ts/src/api/tools/types.ts index 09d920d8d..f77b137e3 100644 --- a/libs/shinkai-message-ts/src/api/tools/types.ts +++ b/libs/shinkai-message-ts/src/api/tools/types.ts @@ -292,3 +292,13 @@ export type ExportToolRequest = { }; export type ExportToolResponse = Blob; + +export type SetOAuthTokenRequest = { + code: string; + state: string; +}; + +export type SetOAuthTokenResponse = { + message: string; + status: string; +}; diff --git a/libs/shinkai-node-state/src/v2/mutations/setOAuthToken/index.ts b/libs/shinkai-node-state/src/v2/mutations/setOAuthToken/index.ts new file mode 100644 index 000000000..97ea74c7a --- /dev/null +++ b/libs/shinkai-node-state/src/v2/mutations/setOAuthToken/index.ts @@ -0,0 +1,36 @@ +import { setOAuthToken } from '@shinkai_network/shinkai-message-ts/api/tools/index'; +import { useMutation, UseMutationOptions } from '@tanstack/react-query'; + +import { APIError } from '../../types'; + +export type SetOAuthTokenInput = { + nodeAddress: string; + token: string; + state: string; + code: string; +}; + +export type SetOAuthTokenOutput = { + state: string; + code: string; +}; + +type Options = UseMutationOptions< + SetOAuthTokenOutput, + APIError, + SetOAuthTokenInput +>; + +export const useSetOAuthToken = (options?: Options) => { + return useMutation({ + mutationFn: async (variables) => { + const { nodeAddress, token, state, code } = variables; + await setOAuthToken(nodeAddress, token, { + code, + state, + }); + return { code, state }; + }, + ...options, + }); +};