Initial implementation of Claude desktop app (Tauri v2)
Standalone native desktop app wrapping claude.ai with: - Direct webview navigation to claude.ai (not iframe) - System tray with close-to-tray behavior - Native notification bridge (JS overrides browser Notification API) - Navigation filter allowing only Claude, Anthropic, and OAuth domains - External links open in system browser - Session persistence via WebKitGTK cookies/localStorage - Builds .deb and AppImage bundles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
c024717934
17 changed files with 5757 additions and 0 deletions
5455
src-tauri/Cargo.lock
generated
Normal file
5455
src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
20
src-tauri/Cargo.toml
Normal file
20
src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "claude-app"
|
||||
version = "1.0.0"
|
||||
description = "Claude desktop app"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
url = "2"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
20
src-tauri/capabilities/default.json
Normal file
20
src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default permissions for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:webview:default",
|
||||
"opener:default",
|
||||
"notification:default",
|
||||
"notification:allow-is-permission-granted",
|
||||
"notification:allow-request-permission",
|
||||
"notification:allow-notify"
|
||||
]
|
||||
}
|
||||
16
src-tauri/capabilities/remote.json
Normal file
16
src-tauri/capabilities/remote.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "remote",
|
||||
"description": "Grants claude.ai IPC access for notifications",
|
||||
"remote": {
|
||||
"urls": ["https://claude.ai/*", "https://*.claude.ai/*"]
|
||||
},
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:default",
|
||||
"notification:default",
|
||||
"notification:allow-is-permission-granted",
|
||||
"notification:allow-request-permission",
|
||||
"notification:allow-notify"
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
BIN
src-tauri/icons/128x128@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
BIN
src-tauri/icons/32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.icns
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/icon.ico
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
src-tauri/icons/icon.png
Normal file
BIN
src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
187
src-tauri/src/lib.rs
Normal file
187
src-tauri/src/lib.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use tauri::{
|
||||
image::Image,
|
||||
menu::{Menu, MenuItem},
|
||||
tray::TrayIconBuilder,
|
||||
webview::{NewWindowResponse, WebviewWindowBuilder},
|
||||
Manager, RunEvent, WebviewUrl, WindowEvent,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// JavaScript injected into every page load to bridge browser Notification API
|
||||
/// to native notifications via Tauri IPC.
|
||||
const NOTIFICATION_BRIDGE_JS: &str = r#"
|
||||
(function() {
|
||||
// Only override if __TAURI_INTERNALS__ is available
|
||||
if (!window.__TAURI_INTERNALS__) return;
|
||||
|
||||
const invoke = window.__TAURI_INTERNALS__.invoke;
|
||||
|
||||
class TauriNotification {
|
||||
constructor(title, options = {}) {
|
||||
this.title = title;
|
||||
this.body = options.body || '';
|
||||
invoke('send_notification', { title: this.title, body: this.body });
|
||||
}
|
||||
|
||||
static get permission() {
|
||||
return 'granted';
|
||||
}
|
||||
|
||||
static requestPermission() {
|
||||
return Promise.resolve('granted');
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the browser Notification with our bridge
|
||||
Object.defineProperty(window, 'Notification', {
|
||||
value: TauriNotification,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
});
|
||||
})();
|
||||
"#;
|
||||
|
||||
/// Tauri command invoked from JS to show a native notification.
|
||||
#[tauri::command]
|
||||
fn send_notification(app: tauri::AppHandle, title: String, body: String) {
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
let _ = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title(&title)
|
||||
.body(&body)
|
||||
.show();
|
||||
}
|
||||
|
||||
/// Returns true if the URL should be allowed to load inside the app webview.
|
||||
/// Allows claude.ai, Anthropic domains, and common OAuth providers.
|
||||
fn is_allowed_navigation(url: &Url) -> bool {
|
||||
let host = match url.host_str() {
|
||||
Some(h) => h.to_lowercase(),
|
||||
None => return false,
|
||||
};
|
||||
|
||||
// Claude / Anthropic
|
||||
if host == "claude.ai" || host.ends_with(".claude.ai") {
|
||||
return true;
|
||||
}
|
||||
if host == "anthropic.com" || host.ends_with(".anthropic.com") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Google OAuth
|
||||
if host == "accounts.google.com"
|
||||
|| host.ends_with(".google.com")
|
||||
|| host.ends_with(".googleapis.com")
|
||||
|| host.ends_with(".gstatic.com")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Apple OAuth
|
||||
if host == "appleid.apple.com" || host.ends_with(".apple.com") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow about:blank and similar
|
||||
if url.scheme() == "about" || url.scheme() == "data" || url.scheme() == "blob" {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
let app = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![send_notification])
|
||||
.setup(|app| {
|
||||
// -- Create main window navigating directly to claude.ai --
|
||||
//
|
||||
// on_navigation and on_new_window are builder methods, so they must
|
||||
// be chained before .build(). The close-to-tray handler is set on
|
||||
// the built window via on_window_event.
|
||||
let webview_window = WebviewWindowBuilder::new(
|
||||
app,
|
||||
"main",
|
||||
WebviewUrl::External("https://claude.ai".parse().unwrap()),
|
||||
)
|
||||
.title("Claude")
|
||||
.inner_size(1200.0, 800.0)
|
||||
.min_inner_size(400.0, 300.0)
|
||||
// Standard WebKit user agent so claude.ai serves the full experience
|
||||
.user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15")
|
||||
.initialization_script(NOTIFICATION_BRIDGE_JS)
|
||||
.zoom_hotkeys_enabled(true)
|
||||
.enable_clipboard_access()
|
||||
// Navigation filter: only allow approved domains in-app
|
||||
.on_navigation(|url| is_allowed_navigation(url))
|
||||
// External links (target=_blank) open in system browser
|
||||
.on_new_window(|url, _features| {
|
||||
let _ = tauri_plugin_opener::open_url(url.as_str(), None::<&str>);
|
||||
NewWindowResponse::Deny
|
||||
})
|
||||
.build()?;
|
||||
|
||||
// -- Close-to-tray: hide window instead of quitting --
|
||||
let win_clone = webview_window.clone();
|
||||
webview_window.on_window_event(move |event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = win_clone.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// -- System tray --
|
||||
let show_item =
|
||||
MenuItem::with_id(app, "show", "Show Claude", true, None::<&str>)?;
|
||||
let quit_item =
|
||||
MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
|
||||
|
||||
let tray_icon = Image::from_bytes(include_bytes!("../icons/32x32.png"))?;
|
||||
|
||||
let tray_handle = app.handle().clone();
|
||||
TrayIconBuilder::new()
|
||||
.icon(tray_icon)
|
||||
.tooltip("Claude")
|
||||
.menu(&menu)
|
||||
.on_menu_event(move |app_handle, event| match event.id().as_ref() {
|
||||
"show" => {
|
||||
if let Some(win) = app_handle.get_webview_window("main") {
|
||||
let _ = win.show();
|
||||
let _ = win.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
app_handle.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(move |_tray, event| {
|
||||
if let tauri::tray::TrayIconEvent::Click {
|
||||
button: tauri::tray::MouseButton::Left,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
if let Some(win) = tray_handle.get_webview_window("main") {
|
||||
let _ = win.show();
|
||||
let _ = win.set_focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error building tauri application");
|
||||
|
||||
// -- Keep app alive when all windows are hidden --
|
||||
app.run(|_app, event| {
|
||||
if let RunEvent::ExitRequested { api, .. } = event {
|
||||
api.prevent_exit();
|
||||
}
|
||||
});
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
6
src-tauri/src/main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Prevents additional console window on Windows in release.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
claude_app::run();
|
||||
}
|
||||
34
src-tauri/tauri.conf.json
Normal file
34
src-tauri/tauri.conf.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"productName": "Claude",
|
||||
"version": "1.0.0",
|
||||
"identifier": "no.naiv.claude-app",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [],
|
||||
"security": {
|
||||
"dangerousDisableAssetCspModification": true,
|
||||
"assetProtocol": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "appimage"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["libwebkit2gtk-4.1-0", "libayatana-appindicator3-1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue