Files
tolaria/src-tauri/src/lib.rs

267 lines
8.3 KiB
Rust
Raw Normal View History

2026-04-13 19:37:59 +02:00
pub mod ai_agents;
pub mod app_updater;
feat: replace Anthropic API with Claude CLI for AI chat and agent panels (#159) * feat: add claude CLI subprocess backend for AI panels Add claude_cli.rs module that spawns the local `claude` CLI as a subprocess instead of calling the Anthropic API directly. This removes the need for users to configure an API key — the CLI uses the user's existing Claude authentication. - find_claude_binary(): discovers claude in PATH or common locations - check_cli(): returns install/version status for the frontend - run_chat_stream(): spawns claude -p with stream-json for chat panel - run_agent_stream(): spawns claude with MCP vault tools for agent panel - Parses stream-json NDJSON events and emits typed Tauri events - Remove http:default capability (no longer calling APIs from frontend) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: replace Anthropic API with Claude CLI for AI chat and agent panels Remove direct Anthropic API calls and the entire tool-use loop from the frontend. Both AI Chat and AI Agent panels now delegate to the `claude` CLI subprocess via Tauri commands, streaming NDJSON events back to the UI. Key changes: - ai-chat.ts / ai-agent.ts: replace SSE/API streaming with Tauri invoke + listen - useAIChat / useAiAgent hooks: simplified to use CLI streaming callbacks - AIChatPanel / AiPanel: remove API key dialogs, model selectors, undo support - SettingsPanel: remove Anthropic key field (no longer needed) - claude_cli.rs: add comprehensive tests (37 total, 90% coverage) - mock-handlers: replace ai_chat with check_claude_cli / stream_claude_* stubs - Net -432 lines across 17 files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add --verbose flag to claude CLI subprocess args * chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup) --------- Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:49:58 +01:00
pub mod claude_cli;
mod commands;
pub mod frontmatter;
pub mod git;
pub mod mcp;
#[cfg(desktop)]
pub mod menu;
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64) * feat: add search backend (qmd CLI) and design file - Add search.rs: qmd integration for keyword (BM25), semantic (vsearch), and hybrid search modes via CLI JSON output - Register search_vault Tauri command in lib.rs - Design file with 3 frames: empty, results, no-results states - Fix pre-existing test failures: install @tauri-apps/plugin-opener, add mock in test setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add SearchPanel UI with Cmd+Shift+F shortcut - SearchPanel component: full-text search overlay with keyword/semantic mode toggle, debounced search (200ms), arrow key navigation, result count + elapsed time display, note type badges from vault entries - Cmd+Shift+F shortcut registered in useAppKeyboard - Mock search_vault handler in mock-tauri for browser dev mode - SearchResult/SearchResponse types in types.ts - Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut - scrollIntoView mock in test setup for jsdom compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct SearchPanel imports for Tauri/browser dual-mode Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri with a searchCall wrapper, fixing the TypeScript build error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make test_detect_collection_fallback robust when qmd not installed * fix: reset localStorage between App tests to prevent view mode state leak --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
pub mod search;
pub mod settings;
pub mod telemetry;
pub mod vault;
pub mod vault_list;
#[cfg(desktop)]
use std::process::Child;
#[cfg(desktop)]
use std::sync::Mutex;
#[cfg(desktop)]
struct WsBridgeChild(Mutex<Option<Child>>);
#[cfg(desktop)]
fn log_startup_result(label: &str, result: Result<usize, String>) {
match result {
Ok(n) if n > 0 => log::info!("{}: {} files", label, n),
Err(e) => log::warn!("{}: {}", label, e),
_ => {}
}
}
/// Run startup housekeeping on the default vault (migrate legacy frontmatter, seed configs).
#[cfg(desktop)]
fn run_startup_tasks() {
let vault_path = dirs::home_dir()
.map(|h| h.join("Laputa"))
.unwrap_or_default();
if !vault_path.is_dir() {
return;
}
let vp_str = vault_path.to_str().unwrap_or_default();
log_startup_result(
"Migrated is_a to type on startup",
vault::migrate_is_a_to_type(vp_str),
);
// Migrate legacy config/agents.md → root AGENTS.md (one-time, idempotent)
vault::migrate_agents_md(vp_str);
// Seed AGENTS.md and config.md at vault root if missing
vault::seed_config_files(vp_str);
// Register Tolaria MCP server in Claude Code and Cursor configs
match mcp::register_mcp(vp_str) {
Ok(status) => log::info!("MCP registration: {status}"),
Err(e) => log::warn!("MCP registration failed: {e}"),
}
}
#[cfg(desktop)]
fn spawn_ws_bridge(app: &mut tauri::App) {
use tauri::Manager;
let vault_path = dirs::home_dir()
.map(|h| h.join("Laputa"))
.unwrap_or_default();
let vp_str = vault_path.to_string_lossy().to_string();
match mcp::spawn_ws_bridge(&vp_str) {
Ok(child) => {
let state: tauri::State<'_, WsBridgeChild> = app.state();
*state.0.lock().unwrap() = Some(child);
}
Err(e) => log::warn!("Failed to start ws-bridge: {}", e),
}
}
fn setup_common_plugins(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
app.handle().plugin(tauri_plugin_dialog::init())?;
Ok(())
}
#[cfg(desktop)]
fn setup_desktop_plugins(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
setup_macos_webview_shortcut_prevention(app)?;
app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
app.handle().plugin(tauri_plugin_process::init())?;
app.handle().plugin(tauri_plugin_opener::init())?;
menu::setup_menu(app)?;
Ok(())
}
const MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS: &[&str] = &["L"];
#[cfg(all(desktop, target_os = "macos"))]
fn setup_macos_webview_shortcut_prevention(
app: &mut tauri::App,
) -> Result<(), Box<dyn std::error::Error>> {
use tauri_plugin_prevent_default::ModifierKey::{MetaKey, ShiftKey};
use tauri_plugin_prevent_default::{Flags, KeyboardShortcut};
let mut builder = tauri_plugin_prevent_default::Builder::new().with_flags(Flags::empty());
// WKWebView can swallow some browser-reserved chords before our shared
// renderer shortcut handler sees them. Keep this list narrow and verify
// every addition with native QA.
for key in MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS {
builder = builder.shortcut(KeyboardShortcut::with_modifiers(key, &[MetaKey, ShiftKey]));
}
app.handle().plugin(builder.build())?;
Ok(())
}
#[cfg(not(all(desktop, target_os = "macos")))]
fn setup_macos_webview_shortcut_prevention(
_app: &mut tauri::App,
) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn setup_app(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
setup_common_plugins(app)?;
#[cfg(desktop)]
setup_desktop_plugins(app)?;
if telemetry::init_sentry_from_settings() {
log::info!("Sentry initialized (crash reporting enabled)");
}
#[cfg(desktop)]
{
run_startup_tasks();
spawn_ws_bridge(app);
}
Ok(())
}
2026-04-14 10:53:08 +02:00
macro_rules! app_invoke_handler {
() => {
tauri::generate_handler![
commands::list_vault,
commands::list_vault_folders,
commands::get_note_content,
commands::save_note_content,
commands::update_frontmatter,
commands::delete_frontmatter_property,
commands::rename_note,
commands::rename_note_filename,
commands::auto_rename_untitled,
commands::detect_renames,
commands::update_wikilinks_for_renames,
commands::get_file_history,
commands::get_modified_files,
commands::get_file_diff,
commands::get_file_diff_at_commit,
commands::get_vault_pulse,
commands::git_commit,
commands::get_build_number,
commands::get_last_commit_info,
commands::git_pull,
commands::git_push,
commands::git_remote_status,
commands::get_conflict_files,
commands::get_conflict_mode,
commands::git_resolve_conflict,
commands::git_commit_conflict_resolution,
commands::git_discard_file,
commands::is_git_repo,
commands::init_git_repo,
commands::check_claude_cli,
commands::get_ai_agents_status,
commands::get_vault_ai_guidance_status,
commands::restore_vault_ai_guidance,
2026-04-14 10:53:08 +02:00
commands::stream_claude_chat,
commands::stream_claude_agent,
commands::stream_ai_agent,
commands::reload_vault,
commands::reload_vault_entry,
commands::sync_note_title,
commands::save_image,
commands::copy_image_to_vault,
commands::delete_note,
commands::batch_delete_notes,
commands::batch_delete_notes_async,
commands::migrate_is_a_to_type,
commands::create_vault_folder,
commands::batch_archive_notes,
commands::get_settings,
commands::check_for_app_update,
commands::update_menu_state,
commands::trigger_menu_command,
commands::update_current_window_min_size,
2026-04-14 10:53:08 +02:00
commands::save_settings,
commands::download_and_install_app_update,
commands::load_vault_list,
commands::save_vault_list,
commands::clone_repo,
commands::search_vault,
commands::create_empty_vault,
commands::create_getting_started_vault,
commands::check_vault_exists,
commands::get_default_vault_path,
commands::register_mcp_tools,
commands::check_mcp_status,
commands::repair_vault,
commands::reinit_telemetry,
commands::list_views,
commands::save_view_cmd,
commands::delete_view_cmd
]
};
}
fn with_invoke_handler(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
2026-04-14 10:53:08 +02:00
builder.invoke_handler(app_invoke_handler!())
}
#[cfg(desktop)]
fn handle_run_event(app_handle: &tauri::AppHandle, event: &tauri::RunEvent) {
use tauri::Manager;
if let tauri::RunEvent::Exit = event {
let state: tauri::State<'_, WsBridgeChild> = app_handle.state();
let mut guard = state.0.lock().unwrap();
if let Some(ref mut child) = *guard {
let _ = child.kill();
log::info!("ws-bridge child process killed on exit");
}
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default();
#[cfg(desktop)]
let builder = builder.manage(WsBridgeChild(Mutex::new(None)));
with_invoke_handler(builder)
.setup(setup_app)
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| {
#[cfg(desktop)]
handle_run_event(app_handle, &event);
});
}
#[cfg(test)]
mod tests {
use super::MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS;
#[test]
fn macos_webview_shortcut_prevention_includes_ai_panel_shortcut() {
assert_eq!(MACOS_WEBVIEW_RESERVED_COMMAND_SHIFT_KEYS, ["L"]);
}
}