Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

- feature: oauth deeplink flow #561

Merged
merged 12 commits into from
Dec 20, 2024
425 changes: 282 additions & 143 deletions apps/shinkai-desktop/src-tauri/Cargo.lock

Large diffs are not rendered by default.

23 changes: 12 additions & 11 deletions apps/shinkai-desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
tauri-build = { version = "2.0.1", features = [] }
tauri-build = { version = "2.0.3", features = [] }

[dependencies]
tauri = { version = "2.0.2", features = [ "macos-private-api", "tray-icon", "image-png"] }
tauri = { version = "2.1.1", features = [ "macos-private-api", "tray-icon", "image-png"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
whisper-rs = "0.8.0"
Expand All @@ -40,15 +40,16 @@ log = "0.4"
anyhow = "1.0.89"
uuid = "1.10.0"

tauri-plugin-global-shortcut = "2.0.1"
tauri-plugin-shell = "2.0.1"
tauri-plugin-single-instance = "2.0.1"
tauri-plugin-updater = "2.0.2"
tauri-plugin-dialog = "2.0.1"
tauri-plugin-fs="2.0.1"
tauri-plugin-os = "2.0.1"
tauri-plugin-process = "2.0.1"
tauri-plugin-log = "2.0.1"
tauri-plugin-global-shortcut = "2.2"
tauri-plugin-shell = "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"
tauri-plugin-os = "2.2"
tauri-plugin-process = "2.2"
tauri-plugin-log = "2.2"
tauri-plugin-deep-link = "2.2"

[features]
# this feature is used for production builds or when `devPath` points to the filesystem
Expand Down
56 changes: 56 additions & 0 deletions apps/shinkai-desktop/src-tauri/src/deep_links.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use tauri::Emitter;
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 query_pairs = url.query_pairs().collect::<Vec<_>>();
let state = query_pairs
.iter()
.find(|(key, _)| key == "state")
.map(|(_, value)| value.to_string())
.unwrap_or_default();
let code = query_pairs
.iter()
.find(|(key, _)| key == "code")
.map(|(_, value)| value.to_string())
.unwrap_or_default();
let payload = OAuthDeepLinkPayload { state, code };
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(
Window::Coordinator.as_str(),
"oauth-deep-link",
payload,
);
}
}
}
});
Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
21 changes: 13 additions & 8 deletions apps/shinkai-desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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;
Expand All @@ -33,6 +33,7 @@ mod hardware;
mod local_shinkai_node;
mod tray;
mod windows;
mod deep_links;

#[derive(Clone, serde::Serialize)]
struct Payload {
Expand All @@ -42,16 +43,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()
Expand All @@ -71,6 +72,7 @@ fn main() {
)
.build(),
)
.plugin(tauri_plugin_deep_link::init())
.invoke_handler(tauri::generate_handler![
hide_spotlight_window_app,
show_spotlight_window_app,
Expand Down Expand Up @@ -104,6 +106,7 @@ fn main() {
}

create_tray(app.handle())?;
setup_deep_links(app.handle())?;

/*
This is the initialization pipeline
Expand All @@ -118,9 +121,9 @@ fn main() {
shinkai_node_manager_guard.kill().await;
drop(shinkai_node_manager_guard);

recreate_window(app_handle.clone(), Window::Coordinator, false);
recreate_window(app_handle.clone(), Window::Spotlight, false);
recreate_window(app_handle.clone(), Window::Main, true);
let _ = recreate_window(app_handle.clone(), Window::Coordinator, false);
let _ = recreate_window(app_handle.clone(), Window::Spotlight, false);
let _ = recreate_window(app_handle.clone(), Window::Main, true);
}
});

Expand All @@ -141,6 +144,8 @@ fn main() {
}
});



Ok(())
})
.build(tauri::generate_context!())
Expand Down
14 changes: 10 additions & 4 deletions apps/shinkai-desktop/src-tauri/src/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use tauri::{AppHandle, Manager, WebviewWindowBuilder};
use tauri::{AppHandle, Manager, WebviewWindow, WebviewWindowBuilder};

#[derive(Debug, Clone, Copy)]
pub enum Window {
Expand All @@ -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<WebviewWindow> {
let label = window_name.as_str();
if let Some(window) = app_handle.get_webview_window(label) {
log::info!("window {} found, bringing to front", label);
Expand All @@ -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
Expand All @@ -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)
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions apps/shinkai-desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"windows": {
"installMode": "basicUi"
}
},
"deep-link": {
"desktop": {
"schemes": ["shinkai"]
}
}
},
"app": {
Expand Down
2 changes: 2 additions & 0 deletions apps/shinkai-desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import { BrowserRouter as Router } from 'react-router-dom';

import FullPageErrorFallback from './components/error-boundary';
import { OAuthConnect } from './components/oauth/oauth-connect';
import { AnalyticsProvider } from './lib/posthog-provider';
import AppRoutes from './routes';
import { useSyncStorageSecondary } from './store/sync-utils';
Expand All @@ -21,6 +22,7 @@ function App() {
<ErrorBoundary FallbackComponent={FullPageErrorFallback}>
<AnalyticsProvider>
<QueryProvider>
<OAuthConnect />
<Router>
<AppRoutes />
</Router>
Expand Down
43 changes: 42 additions & 1 deletion apps/shinkai-desktop/src/components/chat/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,21 @@ import { formatText } from '@shinkai_network/shinkai-ui/helpers';
import { cn } from '@shinkai_network/shinkai-ui/utils';
import { format } from 'date-fns';
import { AnimatePresence, motion } from 'framer-motion';
import { Edit3, InfoIcon, Loader2, RotateCcw, XCircle } from 'lucide-react';
import {
Edit3,
InfoIcon,
Loader2,
RotateCcw,
Unplug,
XCircle,
} from 'lucide-react';
import React, { Fragment, memo, useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Link } from 'react-router-dom';
import { z } from 'zod';

import { useOAuth } from '../../../store/oauth';
import { oauthUrlMatcherFromErrorMessage } from '../../../utils/oauth';
import { useChatStore } from '../context/chat-context';

export const extractErrorPropertyOrContent = (
Expand Down Expand Up @@ -196,6 +205,12 @@ export const MessageBase = ({
return null;
}, [message.content]);

const oauthUrl = useMemo(() => {
return oauthUrlMatcherFromErrorMessage(message.content);
}, [message.content]);

const { setOauthModalVisible } = useOAuth();

return (
<motion.div
animate="rest"
Expand Down Expand Up @@ -397,6 +412,32 @@ export const MessageBase = ({
</div>
)}
{pythonCode && <PythonCodeRunner code={pythonCode} />}

{oauthUrl && (
<div className="mt-4 flex flex-col items-start rounded-lg bg-gray-900 p-4 shadow-md">
<p className="mb-2 text-lg font-semibold text-white">
<div className="flex items-center">
<Unplug className="mr-2 h-5 w-5" />
{t('oauth.connectionRequired')}
</div>
</p>
<p className="mb-4 text-sm text-white">
{t('oauth.connectionRequiredDescription', {
provider: new URL(oauthUrl).hostname,
})}
</p>
<Button
className="rounded-lg px-4 py-2 text-white transition duration-300"
onClick={() =>
setOauthModalVisible({ visible: true, url: oauthUrl })
}
variant="outline"
>
{t('oauth.connectNow')}
</Button>
</div>
)}

{message.role === 'user' && !!message.attachments.length && (
<FileList
className="mt-2 min-w-[200px] max-w-[70vw]"
Expand Down
Loading