diff --git a/ide/Cargo.lock b/ide/Cargo.lock index 545dea0..c222798 100644 --- a/ide/Cargo.lock +++ b/ide/Cargo.lock @@ -65,6 +65,7 @@ dependencies = [ "async-trait", "axum-core 0.4.5", "axum-macros", + "base64", "bytes", "futures-util", "http", @@ -83,8 +84,10 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper", "tokio", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -196,6 +199,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -263,6 +272,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "digest" version = "0.10.7" @@ -298,12 +313,13 @@ dependencies = [ "axum 0.7.9", "el-lsp", "el-plugin-host", + "futures-util", "mime_guess", "reqwest", "rust-embed", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-stream", "tower", @@ -318,7 +334,7 @@ dependencies = [ name = "el-lexer" version = "0.1.0" dependencies = [ - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -330,7 +346,7 @@ dependencies = [ "el-types", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -338,7 +354,7 @@ name = "el-parser" version = "0.1.0" dependencies = [ "el-lexer", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -347,7 +363,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -356,7 +372,7 @@ version = "0.1.0" dependencies = [ "el-lexer", "el-parser", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -447,6 +463,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -466,6 +493,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-macro", + "futures-sink", "futures-task", "pin-project-lite", "slab", @@ -1069,6 +1098,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1103,6 +1141,36 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1408,6 +1476,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1543,13 +1622,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1641,6 +1740,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -1793,6 +1904,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "typenum" version = "1.20.0" @@ -1841,6 +1970,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2283,6 +2418,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.7" diff --git a/ide/crates/el-ide-server/src/api/lsp.rs b/ide/crates/el-ide-server/src/api/lsp.rs index 707c7e4..218e30f 100644 --- a/ide/crates/el-ide-server/src/api/lsp.rs +++ b/ide/crates/el-ide-server/src/api/lsp.rs @@ -1,14 +1,16 @@ -//! LSP API endpoints — completions, hover, errors. +//! LSP API endpoints — completions, hover, errors, activate-preview. use axum::{ - extract::Query, + extract::{Query, State}, http::StatusCode, Json, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use el_lsp::{Completion, Diagnostic, HoverInfo, LanguageServer}; +use crate::AppState; + type ApiResult = Result, (StatusCode, Json)>; // ── Query types ─────────────────────────────────────────────────────────────── @@ -19,6 +21,27 @@ pub struct SourceQuery { pub pos: Option, } +#[derive(Debug, Deserialize)] +pub struct ActivatePreviewQuery { + pub type_name: String, + pub query: String, + pub source: Option, +} + +#[derive(Debug, Serialize)] +pub struct ActivatePreviewResponse { + pub count: usize, + pub nodes: Vec, + pub connected: bool, +} + +#[derive(Debug, Serialize)] +pub struct ActivateNode { + pub id: String, + pub label: String, + pub score: f64, +} + // ── Handlers ────────────────────────────────────────────────────────────────── /// GET /api/lsp/complete?source=...&pos=... @@ -49,3 +72,64 @@ pub async fn errors( let diags = lsp.diagnostics(&q.source); Ok(Json(diags)) } + +/// GET /api/lsp/activate-preview?type_name=...&query=...&source=... +/// +/// Returns a live preview of what nodes would activate for the given +/// `activate TypeName where "query"` expression. +pub async fn activate_preview( + State(state): State, + Query(q): Query, +) -> ApiResult { + let engram_url = &state.config.engram_url; + + // Try to query the Engram DB for matching nodes + let client = reqwest::Client::new(); + let url = format!("{engram_url}/api/activate-preview"); + let body = serde_json::json!({ + "type_name": q.type_name, + "query": q.query, + "limit": 5, + }); + + match client + .post(&url) + .json(&body) + .timeout(std::time::Duration::from_secs(5)) + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + let json: serde_json::Value = resp.json().await.unwrap_or_default(); + + let nodes: Vec = json["nodes"] + .as_array() + .map(|arr| { + arr.iter() + .map(|n| ActivateNode { + id: n["id"].as_str().unwrap_or("").to_string(), + label: n["label"].as_str().unwrap_or("").to_string(), + score: n["score"].as_f64().unwrap_or(0.0), + }) + .collect() + }) + .unwrap_or_default(); + + let count = json["count"].as_u64().unwrap_or(nodes.len() as u64) as usize; + + Ok(Json(ActivatePreviewResponse { + count, + nodes, + connected: true, + })) + } + _ => { + // Engram not connected — return stub indicating disconnected state + Ok(Json(ActivatePreviewResponse { + count: 0, + nodes: vec![], + connected: false, + })) + } + } +} diff --git a/ide/crates/el-ide-server/src/api/reason.rs b/ide/crates/el-ide-server/src/api/reason.rs index e88d337..6953cd9 100644 --- a/ide/crates/el-ide-server/src/api/reason.rs +++ b/ide/crates/el-ide-server/src/api/reason.rs @@ -1,4 +1,4 @@ -//! Reasoning API — proxy to engram-server for hypothesis evaluation. +//! Reasoning API — pair programming with Neuron + hypothesis evaluation. use axum::{ extract::State, @@ -15,18 +15,38 @@ fn api_err(code: StatusCode, msg: impl Into) -> (StatusCode, Json, pub context: Option, + // Pair programming fields + pub message: Option, + pub selection: Option, + pub conversation_history: Option>, + pub mode: Option, // "hypothesis" | "pair" | "knowledge-search" + pub query: Option, // for knowledge-search mode +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConversationMessage { + pub role: String, + pub content: String, } #[derive(Debug, Serialize)] pub struct ReasonResponse { - pub verdict: String, - pub confidence: f64, + pub verdict: Option, + pub confidence: Option, pub evidence: Vec, pub source: String, + // Pair programming fields + pub message: Option, + pub code_blocks: Vec, + pub suggestions: Vec, + // Knowledge search fields + pub nodes: Vec, } #[derive(Debug, Serialize)] @@ -35,18 +55,51 @@ pub struct EvidenceItem { pub weight: f64, } -/// POST /api/reason — proxy to engram-server reasoning endpoint. +#[derive(Debug, Serialize)] +pub struct CodeBlock { + pub language: String, + pub code: String, + pub description: Option, +} + +#[derive(Debug, Serialize)] +pub struct KnowledgeNode { + pub id: String, + pub label: String, + pub node_type: String, + pub score: f64, +} + +/// POST /api/reason — multi-mode endpoint: +/// - mode "hypothesis" (default): proxy to engram-server for hypothesis evaluation +/// - mode "pair": pair programming with Neuron (proxies to soma chat endpoint) +/// - mode "knowledge-search": search Engram knowledge graph pub async fn reason( State(state): State, Json(req): Json, ) -> ApiResult { + let mode = req.mode.as_deref().unwrap_or("hypothesis"); + + match mode { + "pair" => handle_pair_programming(state, req).await, + "knowledge-search" => handle_knowledge_search(state, req).await, + _ => handle_hypothesis(state, req).await, + } +} + +// ── Hypothesis mode ─────────────────────────────────────────────────────────── + +async fn handle_hypothesis( + state: AppState, + req: ReasonRequest, +) -> ApiResult { + let hypothesis = req.hypothesis.clone().unwrap_or_default(); let engram_url = &state.config.engram_url; let url = format!("{engram_url}/api/reason"); - // Attempt to proxy to engram-server; fall back to stub if unavailable. let client = reqwest::Client::new(); let body = serde_json::json!({ - "hypothesis": req.hypothesis, + "hypothesis": hypothesis, "context": req.context, }); @@ -55,8 +108,8 @@ pub async fn reason( let json: serde_json::Value = resp.json().await .map_err(|e| api_err(StatusCode::BAD_GATEWAY, format!("invalid response: {e}")))?; Ok(Json(ReasonResponse { - verdict: json["verdict"].as_str().unwrap_or("unknown").to_string(), - confidence: json["confidence"].as_f64().unwrap_or(0.0), + verdict: Some(json["verdict"].as_str().unwrap_or("unknown").to_string()), + confidence: Some(json["confidence"].as_f64().unwrap_or(0.0)), evidence: json["evidence"].as_array().map(|arr| { arr.iter().map(|e| EvidenceItem { text: e["text"].as_str().unwrap_or("").to_string(), @@ -64,18 +117,292 @@ pub async fn reason( }).collect() }).unwrap_or_default(), source: "engram-server".into(), + message: None, + code_blocks: vec![], + suggestions: vec![], + nodes: vec![], })) } _ => { - // Stub response when engram-server is unavailable Ok(Json(ReasonResponse { - verdict: "unresolved".into(), - confidence: 0.0, + verdict: Some("unresolved".into()), + confidence: Some(0.0), evidence: vec![EvidenceItem { text: "engram-server is not reachable; connect an Engram instance for live reasoning.".into(), weight: 0.0, }], source: "stub".into(), + message: None, + code_blocks: vec![], + suggestions: vec![], + nodes: vec![], + })) + } + } +} + +// ── Pair programming mode ───────────────────────────────────────────────────── + +async fn handle_pair_programming( + state: AppState, + req: ReasonRequest, +) -> ApiResult { + let user_message = req.message.clone().unwrap_or_default(); + let context = req.context.clone().unwrap_or_default(); + let selection = req.selection.clone().unwrap_or_default(); + let history = req.conversation_history.clone().unwrap_or_default(); + + let engram_url = &state.config.engram_url; + + // Build system prompt + let system_prompt = build_pair_system_prompt(&context, &selection); + + // Try to proxy to soma/engram chat endpoint + let client = reqwest::Client::new(); + + // Build messages array for chat completions format + let mut messages: Vec = vec![ + serde_json::json!({ "role": "system", "content": system_prompt }), + ]; + + // Add conversation history + for msg in &history { + messages.push(serde_json::json!({ + "role": msg.role, + "content": msg.content, + })); + } + + // Add current user message + messages.push(serde_json::json!({ "role": "user", "content": user_message })); + + let chat_body = serde_json::json!({ + "model": "neuron", + "messages": messages, + "stream": false, + }); + + // Try soma chat endpoint + let soma_url = format!("{engram_url}/v1/chat/completions"); + let response_text = match client + .post(&soma_url) + .json(&chat_body) + .timeout(std::time::Duration::from_secs(30)) + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + let json: serde_json::Value = resp.json().await + .map_err(|e| api_err(StatusCode::BAD_GATEWAY, format!("invalid response: {e}")))?; + json["choices"][0]["message"]["content"] + .as_str() + .unwrap_or("No response from Neuron.") + .to_string() + } + _ => { + // Fallback stub response when soma is unavailable + build_stub_pair_response(&user_message, &context, &selection) + } + }; + + // Extract code blocks from the response + let code_blocks = extract_code_blocks(&response_text); + + // Extract suggestions (lines starting with "- " or "• ") + let suggestions = extract_suggestions(&response_text); + + Ok(Json(ReasonResponse { + verdict: None, + confidence: None, + evidence: vec![], + source: "neuron".into(), + message: Some(response_text), + code_blocks, + suggestions, + nodes: vec![], + })) +} + +fn build_pair_system_prompt(context: &str, selection: &str) -> String { + let mut prompt = String::from( + "You are Neuron, an AI pair programmer for the Engram language. \ + You are deeply integrated into the IDE — you can see the current file, understand types, \ + and help write, debug, and reason about Engram programs.\n\n\ + The Engram language features:\n\ + - `activate TypeName where \"query\"` — spreading activation over the knowledge graph\n\ + - `sealed { ... }` — quantum-sealed sensitive blocks\n\ + - Types map to semantic nodes in the Engram knowledge graph\n\ + - Structural typing with protocols and impl blocks\n\n" + ); + + if !context.is_empty() { + let truncated = if context.len() > 3000 { + format!("{}...(truncated)", &context[..3000]) + } else { + context.to_string() + }; + prompt.push_str(&format!( + "Current file content:\n```engram\n{truncated}\n```\n\n" + )); + } + + if !selection.is_empty() { + prompt.push_str(&format!( + "User has selected this code:\n```engram\n{selection}\n```\n\n" + )); + } + + prompt.push_str( + "When suggesting code, wrap it in ```engram code blocks. \ + Be concise and practical. If you see opportunities to use spreading activation \ + (`activate X where \"...\"`), suggest them. \ + Focus on the semantic relationships between types and knowledge graph nodes." + ); + + prompt +} + +fn build_stub_pair_response(user_message: &str, context: &str, selection: &str) -> String { + let has_context = !context.is_empty(); + let has_selection = !selection.is_empty(); + let msg_lower = user_message.to_lowercase(); + + if msg_lower.contains("explain") && has_selection { + format!( + "I can see the selected code. To get a real AI analysis, \ + connect an Engram instance at the configured endpoint.\n\n\ + Based on the structure, this code appears to define types and functions \ + in the Engram type system.\n\n\ + Connect Neuron for semantic analysis of knowledge graph relationships." + ) + } else if msg_lower.contains("activate") || msg_lower.contains("semantic") { + "The `activate` keyword performs spreading activation over the Engram knowledge graph. \ + Example:\n\n\ + ```engram\nlet results: [User] = activate User where \"premium subscribers active this week\"\n```\n\n\ + This returns all User nodes semantically matching the query. \ + Connect an Engram instance for live knowledge graph queries.".to_string() + } else if has_context { + format!( + "I see your current file. Neuron pair programming is ready — \ + connect an Engram instance to enable live AI responses.\n\n\ + Your file has {} characters of Engram code. \ + Once connected, I can analyze types, suggest activations, and help debug.", + context.len() + ) + } else { + "Neuron pair programming panel is active. \ + Connect an Engram instance (set EL_ENGRAM_URL) to enable live AI responses. \ + I can help with:\n\ + - Explaining and debugging Engram code\n\ + - Suggesting `activate` queries for your data types\n\ + - Writing functions and type definitions\n\ + - Analyzing semantic relationships in your knowledge graph".to_string() + } +} + +fn extract_code_blocks(text: &str) -> Vec { + let mut blocks = Vec::new(); + let mut remaining = text; + + while let Some(start) = remaining.find("```") { + remaining = &remaining[start + 3..]; + // Find language (optional, on same line as ```) + let lang_end = remaining.find('\n').unwrap_or(remaining.len()); + let lang = remaining[..lang_end].trim().to_string(); + let lang = if lang.is_empty() { "engram".to_string() } else { lang }; + + remaining = &remaining[lang_end..]; + if remaining.starts_with('\n') { + remaining = &remaining[1..]; + } + + if let Some(end) = remaining.find("```") { + let code = remaining[..end].trim_end_matches('\n').to_string(); + remaining = &remaining[end + 3..]; + if !code.is_empty() { + blocks.push(CodeBlock { + language: lang, + code, + description: None, + }); + } + } else { + break; + } + } + + blocks +} + +fn extract_suggestions(text: &str) -> Vec { + text.lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with("- ") || trimmed.starts_with("• ") || trimmed.starts_with("* ") { + Some(trimmed[2..].trim().to_string()) + } else { + None + } + }) + .filter(|s| !s.is_empty()) + .take(8) + .collect() +} + +// ── Knowledge search mode ───────────────────────────────────────────────────── + +async fn handle_knowledge_search( + state: AppState, + req: ReasonRequest, +) -> ApiResult { + let query = req.query.clone().unwrap_or_else(|| req.message.clone().unwrap_or_default()); + let engram_url = &state.config.engram_url; + + let client = reqwest::Client::new(); + let url = format!("{engram_url}/api/search"); + let body = serde_json::json!({ "query": query, "limit": 20 }); + + match client.post(&url).json(&body).timeout(std::time::Duration::from_secs(10)).send().await { + Ok(resp) if resp.status().is_success() => { + let json: serde_json::Value = resp.json().await + .map_err(|e| api_err(StatusCode::BAD_GATEWAY, format!("invalid response: {e}")))?; + + let nodes = json.as_array() + .map(|arr| { + arr.iter().map(|n| KnowledgeNode { + id: n["id"].as_str().unwrap_or("").to_string(), + label: n["label"].as_str().unwrap_or("").to_string(), + node_type: n["type"].as_str().unwrap_or("").to_string(), + score: n["score"].as_f64().unwrap_or(0.0), + }).collect() + }) + .unwrap_or_default(); + + Ok(Json(ReasonResponse { + verdict: None, + confidence: None, + evidence: vec![], + source: "engram-db".into(), + message: None, + code_blocks: vec![], + suggestions: vec![], + nodes, + })) + } + _ => { + // Stub: return empty nodes when engram is unavailable + Ok(Json(ReasonResponse { + verdict: None, + confidence: None, + evidence: vec![EvidenceItem { + text: "Engram DB not reachable — connect an Engram instance for knowledge graph search.".into(), + weight: 0.0, + }], + source: "stub".into(), + message: Some("Connect an Engram instance to search the knowledge graph.".into()), + code_blocks: vec![], + suggestions: vec![], + nodes: vec![], })) } } diff --git a/ide/crates/el-ide-server/src/main.rs b/ide/crates/el-ide-server/src/main.rs index eb2efe0..fb04740 100644 --- a/ide/crates/el-ide-server/src/main.rs +++ b/ide/crates/el-ide-server/src/main.rs @@ -13,8 +13,8 @@ mod tests; use std::sync::Arc; -use axum::{Router, routing::{delete, get, post}}; -use tokio::sync::Mutex; +use axum::{Router, routing::{get, post, any}}; +use tokio::sync::{Mutex, RwLock}; use tower_http::cors::{Any, CorsLayer}; use tracing::info; use tracing_subscriber::EnvFilter; @@ -29,6 +29,7 @@ use crate::config::Config; pub struct AppState { pub config: Arc, pub plugins: Arc>, + pub active_theme: Arc>, } // ── Entry point ─────────────────────────────────────────────────────────────── @@ -44,6 +45,7 @@ async fn main() { let state = AppState { config: Arc::new(config), plugins: Arc::new(Mutex::new(PluginHost::new())), + active_theme: Arc::new(RwLock::new("dark".into())), }; let app = build_router(state); @@ -63,10 +65,21 @@ pub fn build_router(state: AppState) -> Router { Router::new() // IDE HTML .route("/", get(embed::serve_index)) - .route("/*path", get(embed::serve_asset)) + // Config + .route("/api/config", get(api::config::get_config)) + // Status + .route("/api/status", get(api::status::status)) // File API .route("/api/files", get(api::files::list_files)) - .route("/api/file", get(api::files::read_file).post(api::files::write_file)) + .route("/api/file", + get(api::files::read_file) + .post(api::files::write_file) + .delete(api::files::delete_file) + ) + .route("/api/files/mkdir", post(api::files::mkdir)) + .route("/api/files/rename", post(api::files::rename_file)) + // Search + .route("/api/search", get(api::search::search)) // Build/run (SSE) .route("/api/build", post(api::build::build_handler)) .route("/api/run", post(api::build::run_handler)) @@ -74,14 +87,36 @@ pub fn build_router(state: AppState) -> Router { .route("/api/lsp/complete", get(api::lsp::complete)) .route("/api/lsp/hover", get(api::lsp::hover)) .route("/api/lsp/errors", get(api::lsp::errors)) + .route("/api/lsp/activate-preview", get(api::lsp::activate_preview)) // Type graph .route("/api/type-graph", get(api::type_graph::type_graph)) + // Themes + .route("/api/themes", get(api::themes::list_themes)) + .route("/api/themes/active", post(api::themes::set_theme)) // Plugins .route("/api/plugins", get(api::plugins::list_plugins)) .route("/api/plugins/install", post(api::plugins::install_plugin)) - .route("/api/plugins/{name}", delete(api::plugins::remove_plugin)) + .route("/api/plugins/remove", post(api::plugins::remove_plugin)) + .route("/api/plugins/enable", post(api::plugins::enable_plugin)) + .route("/api/plugins/disable", post(api::plugins::disable_plugin)) + // Format + .route("/api/format", post(api::format::format)) + // Outline + .route("/api/outline", get(api::outline::outline)) + // Definition and references + .route("/api/definition", get(api::references::definition)) + .route("/api/references", get(api::references::references)) + // Snippets + .route("/api/completions/snippet", get(api::snippets::snippets)) + // Git + .route("/api/git/status", get(api::git::git_status)) + .route("/api/git/diff", get(api::git::git_diff)) + // Terminal WebSocket + .route("/api/terminal", any(api::terminal::terminal_ws)) // Reasoning (proxy to engram-server) .route("/api/reason", post(api::reason::reason)) .layer(cors) .with_state(state) + // Fallback for SPA routing — must come after with_state/layer + .fallback(embed::fallback) } diff --git a/ide/ide/index.html b/ide/ide/index.html index 566db1b..3e0167a 100644 --- a/ide/ide/index.html +++ b/ide/ide/index.html @@ -7,6 +7,7 @@ @@ -564,7 +1649,7 @@ body {
- project / hello + project /
+ + +
+
+ + 0 + nodes for + +
+
+
+
+ +
Build Output
Problems
+
Search
+
Terminal
+
Outline
+
Git Diff
Reasoning
+
Knowledge Graph
Plugins
+
Settings
@@ -650,6 +1834,34 @@ body {
No problems detected.
+ + + + +
+
+
+ + +
+
+ No file selected + +
+
Select a file and click Refresh to see its diff.
+
+ + +
+
Open a file to see its outline.
+
+
@@ -661,51 +1873,141 @@ body {
+ +
+
+ + +
+
Search the Engram knowledge graph to discover available data concepts.
+
+
Loading...
+ +
+
+
Editor
+
+ + + 13px +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Theme
+
+ + + + +
+
+
+
Keyboard Shortcuts
+ + + + + + + + + + + + + + +
Save fileCmd + S
Close tabCmd + W or middle-click
Cycle tabsCmd + Tab
Switch to tab 1-9Cmd + 19
Find / ReplaceCmd + F / H
Search projectCmd + Shift + F
Quick open filesCmd + P
Command paletteCmd + Shift + P or > in open
Go to definitionF12
Find referencesShift + F12
BuildCmd + B
Fold / UnfoldCmd + [ / ]
Close menu / modalEsc
+
+
+
+ + + + + + + +
+