Initial Commit

This commit is contained in:
2026-09-07 15:16:52 -04:00
commit e8ce633e5e
54 changed files with 13079 additions and 0 deletions

14
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "cex"
version = "0.1.0"
edition = "2024"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
cex-core = { path = "../crates/cex-core" }
serde = { version = "1", features = ["derive"] }
tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tokio-util = { version = "0.7", features = ["rt"] }

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main",
"description": "Local folder selection for the code explorer",
"windows": ["main"],
"permissions": ["core:default", "dialog:allow-open", "dialog:allow-message"]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,301 @@
use crate::{AppState, blocking};
use cex_core::{
Result,
analysis::{Position, Range},
explanation::{
self, Completion, LlmSettings, ModelProvider, OpenAiCompatible, PreparedExplanation,
},
};
use serde::Serialize;
use std::{path::PathBuf, sync::Mutex};
use tauri::{State, ipc::Channel};
use tokio_util::sync::CancellationToken;
#[derive(Default)]
pub struct ExplainState {
inner: Mutex<Session>,
}
#[derive(Default)]
struct Session {
generation: u64,
cancellation: CancellationToken,
prepared: Option<(u64, PreparedExplanation)>,
// Credential is memory-only and bound to one workspace and final endpoint.
credential: Option<(PathBuf, String, String)>,
}
impl ExplainState {
pub fn invalidate(&self, clear_key: bool) {
let mut session = self.inner.lock().unwrap();
session.cancellation.cancel();
session.generation += 1;
session.prepared = None;
if clear_key {
session.credential = None;
}
}
}
#[derive(Serialize)]
pub struct ModelSettingsView {
settings: LlmSettings,
has_session_key: bool,
}
#[derive(Serialize)]
pub struct PreparedView {
id: u64,
#[serde(flatten)]
explanation: PreparedExplanation,
}
#[tauri::command]
pub fn get_llm_settings(root: PathBuf, state: State<'_, AppState>) -> Result<ModelSettingsView> {
let workspace = state.current()?;
if workspace.root != root {
return Err("Workspace changed.".into());
}
let endpoint = workspace.settings.llm.endpoint()?.to_string();
let session = state.explanations.inner.lock().unwrap();
let has_session_key = session
.credential
.as_ref()
.is_some_and(|(r, e, _)| *r == root && *e == endpoint);
Ok(ModelSettingsView {
settings: workspace.settings.llm,
has_session_key,
})
}
#[tauri::command]
pub async fn save_llm_settings(
root: PathBuf,
settings: LlmSettings,
api_key: Option<String>,
state: State<'_, AppState>,
) -> Result<()> {
let state = state.inner().clone();
blocking(move || {
let _lifecycle = state.lifecycle.lock().unwrap();
let mut guard = state.workspace.lock().unwrap();
let workspace = guard.as_mut().ok_or("Open a project first.")?;
if workspace.root != root {
return Err("Workspace changed.".into());
}
settings.validate()?;
if api_key
.as_ref()
.is_some_and(|k| k.len() > 4096 || k.contains(['\r', '\n']))
{
return Err("Invalid API key.".into());
}
let endpoint = settings.endpoint()?.to_string();
workspace.save_llm_settings(settings)?;
state.explanations.invalidate(false);
let mut session = state.explanations.inner.lock().unwrap();
if session
.credential
.as_ref()
.is_some_and(|(r, e, _)| *r != root || *e != endpoint)
{
session.credential = None;
}
if let Some(key) = api_key {
session.credential = if key.is_empty() {
None
} else {
Some((root, endpoint, key))
};
}
Ok(())
})
.await
}
#[tauri::command]
pub async fn prepare_explanation(
root: PathBuf,
path: String,
position: Position,
selection: Option<Range>,
question: String,
state: State<'_, AppState>,
) -> Result<PreparedView> {
let (workspace, server, id, cancellation) =
{
let _lifecycle = state.lifecycle.lock().unwrap();
let workspace = state.current()?;
if workspace.root != root {
return Err("Workspace changed.".into());
}
let server =
state.analysis.lock().unwrap().clone().ok_or(
"Start clangd with a build directory before preparing an explanation.",
)?;
let mut session = state.explanations.inner.lock().unwrap();
session.cancellation.cancel();
session.generation += 1;
session.cancellation = CancellationToken::new();
session.prepared = None;
(
workspace,
server,
session.generation,
session.cancellation.clone(),
)
};
let prepared = blocking(move || {
explanation::prepare(
&workspace,
server.as_ref(),
&path,
position,
selection,
&question,
&cancellation,
)
})
.await?;
let mut session = state.explanations.inner.lock().unwrap();
if session.generation != id || session.cancellation.is_cancelled() {
return Err("Context preparation cancelled or workspace changed.".into());
}
session.prepared = Some((id, prepared.clone()));
Ok(PreparedView {
id,
explanation: prepared,
})
}
#[tauri::command]
pub async fn send_explanation(
root: PathBuf,
id: u64,
on_delta: Channel<String>,
state: State<'_, AppState>,
) -> Result<Completion> {
let (prepared, key, cancellation) = {
let _lifecycle = state.lifecycle.lock().unwrap();
if state.current()?.root != root {
return Err("Workspace changed.".into());
}
let mut session = state.explanations.inner.lock().unwrap();
let prepared = session
.prepared
.as_ref()
.filter(|(prepared_id, _)| *prepared_id == id)
.map(|(_, p)| p.clone())
.ok_or("Context expired. Prepare context again before sending.")?;
let key = session
.credential
.as_ref()
.filter(|(r, e, _)| *r == root && *e == prepared.endpoint)
.map(|(_, _, key)| key.clone());
session.cancellation.cancel();
session.generation += 1;
session.cancellation = CancellationToken::new();
(prepared, key, session.cancellation.clone())
};
let key = match (key, prepared.settings.api_key_env.is_empty()) {
(Some(key), _) => Some(key),
(None, true) => None,
(None, false) => Some(std::env::var(&prepared.settings.api_key_env).map_err(|_| format!("Environment variable {} is not set for CEX. Set it before launching or enter a session API key.", prepared.settings.api_key_env))?),
};
let stream_cancel = cancellation.clone();
let emit = move |text: String| {
if on_delta.send(text).is_err() {
stream_cancel.cancel();
}
};
OpenAiCompatible
.explain(&prepared, key.as_deref(), &cancellation, &emit)
.await
}
#[tauri::command]
pub fn cancel_explanation(root: PathBuf, state: State<'_, AppState>) -> Result<()> {
if state.current()?.root != root {
return Ok(());
}
let mut session = state.explanations.inner.lock().unwrap();
session.cancellation.cancel();
session.generation += 1;
Ok(())
}
#[tauri::command]
pub fn update_explanation_question(
root: PathBuf,
id: u64,
question: String,
state: State<'_, AppState>,
) -> Result<PreparedView> {
let _lifecycle = state.lifecycle.lock().unwrap();
if state.current()?.root != root {
return Err("Workspace changed.".into());
}
let mut session = state.explanations.inner.lock().unwrap();
let (_, prepared) = session
.prepared
.as_mut()
.filter(|(stored, _)| *stored == id)
.ok_or("Context expired. Prepare again.")?;
explanation::update_question(prepared, &question)?;
Ok(PreparedView {
id,
explanation: prepared.clone(),
})
}
#[tauri::command]
pub async fn expand_explanation(
root: PathBuf,
id: u64,
selected: Vec<String>,
depth: u8,
include_standard: Option<bool>,
question: String,
state: State<'_, AppState>,
) -> Result<PreparedView> {
let (prepared, server, next_id, cancel) = {
let _lifecycle = state.lifecycle.lock().unwrap();
if state.current()?.root != root {
return Err("Workspace changed.".into());
}
let server = state
.analysis
.lock()
.unwrap()
.clone()
.ok_or("Start clangd first.")?;
let mut session = state.explanations.inner.lock().unwrap();
let prepared = session
.prepared
.as_ref()
.filter(|(stored, _)| *stored == id)
.map(|(_, p)| p.clone())
.ok_or("Context expired. Prepare again.")?;
session.cancellation.cancel();
session.generation += 1;
session.cancellation = CancellationToken::new();
(
prepared,
server,
session.generation,
session.cancellation.clone(),
)
};
let explanation = blocking(move || {
explanation::expand_with_standard(
&prepared,
server.as_ref(),
&selected,
depth,
include_standard.unwrap_or(false),
&question,
&cancel,
)
})
.await?;
let mut session = state.explanations.inner.lock().unwrap();
if session.generation != next_id || session.cancellation.is_cancelled() {
return Err("Expansion cancelled or workspace changed.".into());
}
session.prepared = Some((next_id, explanation.clone()));
Ok(PreparedView {
id: next_id,
explanation,
})
}

355
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,355 @@
mod explanations;
use cex_core::analysis::{AnalysisResult, AnalysisStatus, Clangd, CodeAnalysis, Position, Query};
use cex_core::{Document, Entry, Inspection, SearchResults, Workspace};
use std::{
path::PathBuf,
sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
},
};
use tauri::{Manager, State};
#[derive(Clone, Default)]
struct AppState {
workspace: Arc<Mutex<Option<Workspace>>>,
search_generation: Arc<AtomicU64>,
analysis: Arc<Mutex<Option<Arc<dyn CodeAnalysis>>>>,
analysis_status: Arc<Mutex<AnalysisStatus>>,
lifecycle: Arc<Mutex<()>>,
explanations: Arc<explanations::ExplainState>,
}
impl AppState {
fn current(&self) -> Result<Workspace, String> {
self.workspace
.lock()
.map_err(|_| "Workspace lock failed.")?
.clone()
.ok_or("Open a project first.".into())
}
}
async fn blocking<T: Send + 'static>(
f: impl FnOnce() -> Result<T, String> + Send + 'static,
) -> Result<T, String> {
tauri::async_runtime::spawn_blocking(f)
.await
.map_err(|e| e.to_string())?
}
#[tauri::command]
async fn inspect_workspace(root: PathBuf) -> Result<Inspection, String> {
blocking(move || cex_core::inspect(&root)).await
}
#[tauri::command]
async fn open_workspace(
root: PathBuf,
build: Option<PathBuf>,
reset: bool,
exclusions: Option<Vec<String>>,
clangd_path: Option<String>,
state: State<'_, AppState>,
) -> Result<Workspace, String> {
let state = state.inner().clone();
state.search_generation.fetch_add(1, Ordering::Relaxed);
blocking(move || {
let _lifecycle = state.lifecycle.lock().unwrap();
state.explanations.invalidate(true);
if let Some(server) = state.analysis.lock().unwrap().take() {
server.stop();
}
*state.analysis_status.lock().unwrap() =
AnalysisStatus::unavailable("Select a build directory and start analysis.");
let mut current = state
.workspace
.lock()
.map_err(|_| "Workspace lock failed.")?;
let mut workspace = cex_core::open(&root, build.as_deref(), reset)?;
workspace.save_options(
exclusions.unwrap_or_else(|| workspace.settings.search_exclusions.clone()),
clangd_path.unwrap_or_else(|| workspace.settings.clangd_path.clone()),
)?;
*current = Some(workspace.clone());
Ok(workspace)
})
.await
}
#[tauri::command]
async fn list_directory(path: String, state: State<'_, AppState>) -> Result<Vec<Entry>, String> {
let workspace = state.current()?;
blocking(move || workspace.list(&path)).await
}
#[tauri::command]
async fn read_file(path: String, state: State<'_, AppState>) -> Result<Document, String> {
let workspace = state.current()?;
let analysis = state.analysis.lock().unwrap().clone();
blocking(move || {
workspace.read(&path).or_else(|error| {
analysis
.as_ref()
.map_or(Err(error), |server| server.read_target(&path))
})
})
.await
}
#[tauri::command]
async fn remember_file(
path: String,
root: PathBuf,
state: State<'_, AppState>,
) -> Result<(), String> {
let state = state.inner().clone();
blocking(move || {
let mut guard = state
.workspace
.lock()
.map_err(|_| "Workspace lock failed.")?;
let workspace = guard.as_mut().ok_or("Open a project first.")?;
if workspace.root != root {
return Ok(());
}
workspace.remember_file(&path)
})
.await
}
#[tauri::command]
async fn search(query: String, state: State<'_, AppState>) -> Result<SearchResults, String> {
let workspace = state.current()?;
let generation = state.search_generation.clone();
let ticket = generation.fetch_add(1, Ordering::Relaxed) + 1;
blocking(move || workspace.search(&query, &generation, ticket)).await
}
#[tauri::command]
fn cancel_search(state: State<'_, AppState>) {
state.search_generation.fetch_add(1, Ordering::Relaxed);
}
#[tauri::command]
async fn save_search_exclusions(
root: PathBuf,
exclusions: Vec<String>,
state: State<'_, AppState>,
) -> Result<(), String> {
let state = state.inner().clone();
state.search_generation.fetch_add(1, Ordering::Relaxed);
blocking(move || {
let mut guard = state.workspace.lock().unwrap();
let workspace = guard.as_mut().ok_or("Open a project first.")?;
if workspace.root != root {
return Err("Workspace changed; retry the operation.".into());
}
workspace.save_options(exclusions, workspace.settings.clangd_path.clone())
})
.await
}
#[tauri::command]
async fn start_analysis(
root: PathBuf,
state: State<'_, AppState>,
) -> Result<AnalysisStatus, String> {
let state = state.inner().clone();
blocking(move || {
let _lifecycle = state.lifecycle.lock().unwrap();
state.explanations.invalidate(false);
let workspace = state.current()?;
if workspace.root != root {
return Err("Workspace changed; retry the operation.".into());
}
if let Some(server) = state.analysis.lock().unwrap().take() {
server.stop();
}
*state.analysis_status.lock().unwrap() = AnalysisStatus {
phase: "starting".into(),
message: "Starting clangd…".into(),
..AnalysisStatus::default()
};
match Clangd::start(workspace) {
Ok(server) => {
let status = server.status();
*state.analysis.lock().unwrap() = Some(server);
Ok(status)
}
Err(error) => {
let status = AnalysisStatus::unavailable(error);
*state.analysis_status.lock().unwrap() = status.clone();
Ok(status)
}
}
})
.await
}
#[tauri::command]
fn analysis_status(root: PathBuf, state: State<'_, AppState>) -> Result<AnalysisStatus, String> {
if state.current()?.root != root {
return Err("Workspace changed.".into());
}
let server = state.analysis.lock().unwrap().clone();
Ok(server.map_or_else(
|| state.analysis_status.lock().unwrap().clone(),
|s| s.status(),
))
}
#[tauri::command]
async fn analysis_request(
root: PathBuf,
path: String,
position: Position,
kind: Query,
state: State<'_, AppState>,
) -> Result<AnalysisResult, String> {
if state.current()?.root != root {
return Err("Workspace changed.".into());
}
let server = state
.analysis
.lock()
.unwrap()
.clone()
.ok_or_else(|| state.analysis_status.lock().unwrap().message.clone())?;
blocking(move || server.query(&path, position, kind)).await
}
#[tauri::command]
async fn graph_request(
root: PathBuf,
path: String,
position: Position,
options: cex_core::analysis::GraphOptions,
state: State<'_, AppState>,
) -> Result<cex_core::analysis::ConceptGraph, String> {
if state.current()?.root != root {
return Err("Workspace changed.".into());
}
let server = state
.analysis
.lock()
.unwrap()
.clone()
.ok_or("Start analysis first.")?;
blocking(move || server.graph(&path, position, &options)).await
}
#[tauri::command]
async fn save_graph_options(
root: PathBuf,
options: cex_core::analysis::GraphOptions,
state: State<'_, AppState>,
) -> Result<(), String> {
let state = state.inner().clone();
blocking(move || {
let mut guard = state.workspace.lock().unwrap();
let workspace = guard.as_mut().ok_or("Open a project first.")?;
if workspace.root != root {
return Err("Workspace changed.".into());
}
workspace.save_graph_options(options)
})
.await
}
#[tauri::command]
async fn load_theme(app: tauri::AppHandle) -> Result<cex_core::appearance::Theme, String> {
let directory = app
.path()
.home_dir()
.map_err(|e| e.to_string())?
.join(".cex");
blocking(move || cex_core::appearance::load(&directory)).await
}
#[tauri::command]
async fn save_theme(
app: tauri::AppHandle,
theme: cex_core::appearance::Theme,
) -> Result<(), String> {
let directory = app
.path()
.home_dir()
.map_err(|e| e.to_string())?
.join(".cex");
blocking(move || cex_core::appearance::save(&directory, theme)).await
}
#[tauri::command]
async fn recent_projects(
app: tauri::AppHandle,
) -> Result<Vec<cex_core::recents::RecentProject>, String> {
let directory = app
.path()
.home_dir()
.map_err(|e| e.to_string())?
.join(".cex");
blocking(move || cex_core::recents::list(&directory)).await
}
#[tauri::command]
async fn remember_recent(
app: tauri::AppHandle,
state: State<'_, AppState>,
root: PathBuf,
) -> Result<(), String> {
if state.current()?.root != root {
return Err("Workspace changed.".into());
}
let directory = app
.path()
.home_dir()
.map_err(|e| e.to_string())?
.join(".cex");
blocking(move || cex_core::recents::remember(&directory, &root)).await
}
#[tauri::command]
async fn remove_recent(app: tauri::AppHandle, root: PathBuf) -> Result<(), String> {
let directory = app
.path()
.home_dir()
.map_err(|e| e.to_string())?
.join(".cex");
blocking(move || cex_core::recents::remove(&directory, &root)).await
}
fn main() {
// WebKitGTK's DMA-BUF renderer can produce a blank window when GBM buffer
// allocation fails (notably with proprietary NVIDIA drivers on X11).
// Use its compatibility rendering path by default, retaining an explicit
// environment override for machines where DMA-BUF works well.
#[cfg(target_os = "linux")]
if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
// SAFETY: this is the first operation in main, before Tauri, GTK, or
// the async runtime is initialized and before we create any threads.
unsafe { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1") };
}
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(AppState::default())
.invoke_handler(tauri::generate_handler![
graph_request,
save_graph_options,
load_theme,
save_theme,
recent_projects,
remember_recent,
remove_recent,
inspect_workspace,
open_workspace,
list_directory,
read_file,
remember_file,
search,
cancel_search,
save_search_exclusions,
start_analysis,
analysis_status,
analysis_request,
explanations::get_llm_settings,
explanations::save_llm_settings,
explanations::prepare_explanation,
explanations::send_explanation,
explanations::expand_explanation,
explanations::update_explanation_question,
explanations::cancel_explanation
])
.build(tauri::generate_context!())
.expect("Could not start CEX")
.run(|app, event| {
if matches!(event, tauri::RunEvent::Exit) {
use tauri::Manager;
let state = app.state::<AppState>();
state.explanations.invalidate(true);
let _lifecycle = state.lifecycle.lock().unwrap();
if let Some(server) = state.analysis.lock().unwrap().take() {
server.stop();
}
}
});
}

17
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CEX",
"version": "0.1.0",
"identifier": "dev.cex.explorer",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [{ "title": "CEX — Code Explorer", "incognito": true, "width": 1280, "height": 850, "minWidth": 800, "minHeight": 600 }],
"security": { "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data:; font-src 'self' data:; connect-src ipc: http://ipc.localhost" }
},
"bundle": { "active": false, "icon": ["icons/128x128.png"] }
}