This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -5018,6 +5018,7 @@ dependencies = [
"tokio",
"urlencoding",
"uuid",
"windows-sys 0.61.2",
]
[[package]]

View File

@@ -27,3 +27,11 @@ serde_json = "1"
tokio = { version = "1", features = ["full"] }
urlencoding = "2"
uuid = { version = "1", features = ["v4"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_JobObjects",
"Win32_System_Threading",
] }

View File

@@ -1,69 +1,25 @@
; ─── Waggle NSIS Installer Template ──────────────────────────────────────────
; Waggle-specific extensions for Tauri's NSIS installer.
;
; Custom hooks for the Tauri NSIS installer:
; 1. Welcome message with Waggle branding
; 2. Desktop shortcut creation
; 3. Start Menu entry
; 4. "Launch Waggle" on finish
; 5. Uninstaller with optional ~/.waggle/ data removal
; Tauri owns install location, shortcuts, finish-page launch, silent /R launch,
; registry entries, and uninstaller cleanup. Do not duplicate those here: doing
; so double-launched normal installs and made silent repair nondeterministic.
; Autostart is handled by tauri-plugin-autostart at runtime.
; Personal data is always preserved by the package uninstaller. Tauri's base
; uninstaller exposes a generic "Delete app data" checkbox, so PREUNINSTALL
; explicitly neutralizes that state. Destructive erasure is available only
; through Waggle's authenticated, phrase-gated UI.
;
; Tauri injects NSIS defines: PRODUCT_NAME, PRODUCT_VERSION, MAINBINARYNAME,
; DEFAULT_INSTALL_DIR. Autostart is handled by tauri-plugin-autostart at
; runtime, not by the installer.
;
; Reference: https://tauri.app/distribute/windows-installer/#nsis
; ─────────────────────────────────────────────────────────────────────────────
InstallDir "${DEFAULT_INSTALL_DIR}"
; Reference: https://v2.tauri.app/distribute/windows-installer/#extending-the-installer
!macro NSIS_HOOK_PREINSTALL
DetailPrint "Installing ${PRODUCT_NAME} v${PRODUCT_VERSION}..."
DetailPrint "Your personal AI agent workspace powered by Waggle."
DetailPrint "Installing Waggle..."
DetailPrint "Your personal AI agent workspace - powered by Waggle."
!macroend
!macro NSIS_HOOK_POSTINSTALL
; ── Desktop shortcut ──────────────────────────────────────────────────────
CreateShortcut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" \
"" "$INSTDIR\${MAINBINARYNAME}.exe" 0
DetailPrint "Desktop shortcut created."
; ── Start Menu entry ──────────────────────────────────────────────────────
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" \
"$INSTDIR\${MAINBINARYNAME}.exe" "" "$INSTDIR\${MAINBINARYNAME}.exe" 0
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk" \
"$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0
DetailPrint "Start Menu entry created."
; ── Launch after install ──────────────────────────────────────────────────
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
DetailPrint "Launching ${PRODUCT_NAME}..."
!macroend
!macro NSIS_HOOK_POSTUNINSTALL
; ── Remove desktop shortcut ─────────────────────────────────────────────
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
; ── Remove Start Menu entries ───────────────────────────────────────────
Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk"
RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
; ── Ask about user data removal ─────────────────────────────────────────
MessageBox MB_YESNO|MB_ICONQUESTION \
"Waggle stores your data (agents, memories, configuration) in:$\r$\n$\r$\n\
$PROFILE\.waggle$\r$\n$\r$\n\
Do you want to remove this data as well?$\r$\n$\r$\n\
Choose $\"Yes$\" to delete all data, or $\"No$\" to keep it for future use." \
IDYES removeData IDNO skipData
removeData:
RMDir /r "$PROFILE\.waggle"
DetailPrint "User data removed: $PROFILE\.waggle"
Goto doneData
skipData:
DetailPrint "User data preserved: $PROFILE\.waggle"
doneData:
!macro NSIS_HOOK_PREUNINSTALL
StrCmp $DeleteAppDataCheckboxState "1" 0 +2
MessageBox MB_OK|MB_ICONINFORMATION \
"For safety, Waggle always preserves app data during uninstall. Data can only be erased from Settings > Data & Privacy while Waggle is installed."
StrCpy $DeleteAppDataCheckboxState 0
DetailPrint "Preserving Waggle app data."
!macroend

View File

@@ -42,7 +42,7 @@ pub async fn run_agent_query(
session: Option<String>,
) -> Result<String, String> {
let request_id = format!("agent-{}", Uuid::new_v4());
let port = state.port;
let port = state.verified_port()?;
let app_clone = app.clone();
let req_id_clone = request_id.clone();

View File

@@ -31,9 +31,10 @@ pub async fn recall_memory(
limit: Option<u32>,
workspace_id: Option<String>,
) -> Result<Value, String> {
let port = state.verified_port()?;
let mut url = format!(
"{}?q={}",
sidecar_url(state.port, "/api/memory/search"),
sidecar_url(port, "/api/memory/search"),
urlencoding::encode(&query)
);
if let Some(s) = scope {
@@ -61,6 +62,7 @@ pub async fn save_memory(
importance: Option<String>,
source: Option<String>,
) -> Result<Value, String> {
let port = state.verified_port()?;
let mut body = json!({ "content": content });
if let Some(ws) = workspace_id {
body["workspace"] = json!(ws);
@@ -72,7 +74,7 @@ pub async fn save_memory(
body["source"] = json!(src);
}
let url = sidecar_url(state.port, "/api/memory/frames");
let url = sidecar_url(port, "/api/memory/frames");
let resp = http_post(&url, &body).await?;
parse_json(resp).await
}
@@ -85,7 +87,8 @@ pub async fn search_entities(
workspace_id: Option<String>,
scope: Option<String>,
) -> Result<Value, String> {
let mut url = sidecar_url(state.port, "/api/memory/graph").to_string();
let port = state.verified_port()?;
let mut url = sidecar_url(port, "/api/memory/graph").to_string();
let mut params: Vec<String> = Vec::new();
if let Some(ws) = workspace_id {
params.push(format!("workspace={}", urlencoding::encode(&ws)));
@@ -111,7 +114,7 @@ pub async fn search_entities(
/// pre-A1.1 placeholders and now only fire on hard sidecar outages.
#[tauri::command]
pub async fn get_identity(state: State<'_, ServiceState>) -> Result<Value, String> {
let url = sidecar_url(state.port, "/api/identity");
let url = sidecar_url(state.verified_port()?, "/api/identity");
match http_get(&url).await {
Ok(resp) if resp.status().as_u16() == 404 => Ok(identity_placeholder(
"sidecar route 404 (unexpected post-A1.1)",

View File

@@ -26,7 +26,7 @@ use crate::service::ServiceState;
/// index (slugs + titles + metadata); call get_wiki_page_content for the body.
#[tauri::command]
pub async fn get_wiki_pages(state: State<'_, ServiceState>) -> Result<Value, String> {
let url = sidecar_url(state.port, "/api/wiki/pages");
let url = sidecar_url(state.verified_port()?, "/api/wiki/pages");
let resp = http_get(&url).await?;
parse_json(resp).await
}
@@ -36,7 +36,7 @@ pub async fn get_wiki_pages(state: State<'_, ServiceState>) -> Result<Value, Str
#[tauri::command]
pub async fn get_wiki_page(state: State<'_, ServiceState>, slug: String) -> Result<Value, String> {
let url = sidecar_url(
state.port,
state.verified_port()?,
&format!("/api/wiki/pages/{}", urlencoding::encode(&slug)),
);
let resp = http_get(&url).await?;
@@ -51,7 +51,7 @@ pub async fn get_wiki_page_content(
slug: String,
) -> Result<Value, String> {
let url = sidecar_url(
state.port,
state.verified_port()?,
&format!("/api/wiki/pages/{}/content", urlencoding::encode(&slug)),
);
let resp = http_get(&url).await?;
@@ -69,7 +69,7 @@ pub async fn compile_wiki_section(
if let Some(ws) = workspace_id {
body["workspace"] = json!(ws);
}
let url = sidecar_url(state.port, "/api/wiki/compile");
let url = sidecar_url(state.verified_port()?, "/api/wiki/compile");
let resp = http_post(&url, &body).await?;
parse_json(resp).await
}

View File

@@ -59,6 +59,52 @@ pub fn run() {
commands::onboarding::reset_first_launch,
])
.setup(|app| {
// Create the configured window here so the Windows certifier can
// opt into a loopback-only WebView CDP port without shipping
// remote debugging enabled for normal launches.
let main_window_config = app
.config()
.app
.windows
.iter()
.find(|window| window.label == "main")
.cloned()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"configured main window is missing",
)
})?;
let mut main_window = tauri::WebviewWindowBuilder::from_config(
app.handle(),
&main_window_config,
)?;
#[cfg(windows)]
if let Some(raw_port) = std::env::var_os("WAGGLE_CERTIFIER_WEBVIEW_DEBUG_PORT") {
let raw_port = raw_port.to_string_lossy();
let port = raw_port.parse::<u16>().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"WAGGLE_CERTIFIER_WEBVIEW_DEBUG_PORT must be an integer",
)
})?;
if port < 1024 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"WAGGLE_CERTIFIER_WEBVIEW_DEBUG_PORT must be >= 1024",
)
.into());
}
let browser_args = format!(
"--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --remote-debugging-port={port}"
);
main_window = main_window.additional_browser_args(&browser_args);
eprintln!(
"[waggle] WebView certifier debug endpoint enabled on 127.0.0.1:{port}"
);
}
main_window.build()?;
tray::setup_tray(app.handle())?;
// Register global hotkey: Ctrl+Shift+W to toggle window visibility
@@ -90,18 +136,17 @@ pub fn run() {
);
}
// Auto-start the sidecar service before the webview loads so the
// React app finds it already healthy on localhost:3333.
// Auto-start an owned sidecar launch before the webview loads.
// Its verified endpoint may differ from the preferred port.
let service_state = app.state::<ServiceState>();
let port = service_state.port;
match service::spawn_service_sync(port, &service_state.process) {
Ok(()) => eprintln!("[waggle] Sidecar spawn initiated on port {}", port),
match service::spawn_service_sync(&service_state) {
Ok(()) => eprintln!("[waggle] Owned sidecar spawn initiated"),
Err(e) => eprintln!("[waggle] Failed to auto-start sidecar: {}", e),
}
// Start service watchdog
let app_handle_watchdog = app.handle().clone();
service::start_watchdog(app_handle_watchdog, port);
service::start_watchdog(app_handle_watchdog);
Ok(())
})
@@ -115,15 +160,10 @@ pub fn run() {
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| {
// R7-002: kill the sidecar on app exit so it doesn't orphan and hold port 3333.
// R7-002: kill only the owned sidecar launch on app exit.
if let tauri::RunEvent::Exit = event {
if let Some(state) = app_handle.try_state::<ServiceState>() {
if let Ok(mut proc) = state.process.lock() {
if let Some(mut child) = proc.take() {
let _ = child.kill();
let _ = child.wait();
}
}
let _ = service::stop_service_sync(&state);
}
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
use tauri::{
image::Image,
menu::{MenuBuilder, MenuItemBuilder},
tray::TrayIconBuilder,
tray::{MouseButton, MouseButtonState, TrayIconBuilder},
AppHandle, Emitter, Manager,
};
@@ -50,6 +50,7 @@ pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
TrayIconBuilder::new()
.icon(icon)
.menu(&menu)
.show_menu_on_left_click(false)
.tooltip("Waggle Agent Service")
.on_menu_event(|app, event| match event.id().as_ref() {
"show" => {
@@ -65,7 +66,12 @@ pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let tauri::tray::TrayIconEvent::Click { .. } = event {
if let tauri::tray::TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
show_main_window(tray.app_handle());
}
})

View File

@@ -28,21 +28,19 @@
"windows": [
{
"title": "Waggle",
"create": false,
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"dataDirectory": "webview",
"resizable": true,
"fullscreen": false,
"decorations": true
}
],
"security": {
"csp": "default-src 'self'; connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https://us.i.posthog.com; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:"
},
"trayIcon": {
"iconPath": "icons/icon.png",
"tooltip": "Waggle - AI Agent Swarm"
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https://us.i.posthog.com; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:"
}
},
"plugins": {