Transform el-ide into AI-native IDE: Neuron pair programming, live type graph, semantic autocomplete, activation preview
- Neuron pair programming panel (right column, 300px): full conversation interface with code-block rendering, "Insert at cursor" for AI suggestions, quick actions (explain selection, find activations, analyze types), thinking indicator - Live activation preview: floating inline widget when cursor is inside `activate TypeName where "..."` expressions; queries /api/lsp/activate-preview - Type graph activation highlighting: `activate` statements cause named types to pulse with purple glow rings; animation loop runs while activations are present - Knowledge Graph Explorer: new bottom panel tab; search the Engram DB for semantic concepts, insert activate expressions, ask Neuron about nodes - Semantic autocomplete: completions with score >= 0.8 get "⟁ semantic" prefix and priority boost in the completion list - Backend: /api/reason extended to support three modes: "hypothesis" (existing), "pair" (proxies to soma /v1/chat/completions with full file context + system prompt), "knowledge-search" (proxies to Engram DB search endpoint) - Backend: new GET /api/lsp/activate-preview endpoint queries Engram for nodes matching a partial activate expression - Type graph node click now navigates editor to the type definition - Resolved merge conflicts in engram-lang/crates (ast.rs, parser.rs, checker.rs, types.rs): took union of HEAD and worktree-agent branches
This commit is contained in:
Generated
+162
-7
@@ -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"
|
||||
|
||||
@@ -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<T> = Result<Json<T>, (StatusCode, Json<serde_json::Value>)>;
|
||||
|
||||
// ── Query types ───────────────────────────────────────────────────────────────
|
||||
@@ -19,6 +21,27 @@ pub struct SourceQuery {
|
||||
pub pos: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ActivatePreviewQuery {
|
||||
pub type_name: String,
|
||||
pub query: String,
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ActivatePreviewResponse {
|
||||
pub count: usize,
|
||||
pub nodes: Vec<ActivateNode>,
|
||||
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<AppState>,
|
||||
Query(q): Query<ActivatePreviewQuery>,
|
||||
) -> ApiResult<ActivatePreviewResponse> {
|
||||
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<ActivateNode> = 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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>) -> (StatusCode, Json<serde_
|
||||
(code, Json(serde_json::json!({ "error": msg.into() })))
|
||||
}
|
||||
|
||||
// ── Hypothesis evaluation (legacy) ───────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReasonRequest {
|
||||
pub hypothesis: String,
|
||||
pub hypothesis: Option<String>,
|
||||
pub context: Option<String>,
|
||||
// Pair programming fields
|
||||
pub message: Option<String>,
|
||||
pub selection: Option<String>,
|
||||
pub conversation_history: Option<Vec<ConversationMessage>>,
|
||||
pub mode: Option<String>, // "hypothesis" | "pair" | "knowledge-search"
|
||||
pub query: Option<String>, // 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<String>,
|
||||
pub confidence: Option<f64>,
|
||||
pub evidence: Vec<EvidenceItem>,
|
||||
pub source: String,
|
||||
// Pair programming fields
|
||||
pub message: Option<String>,
|
||||
pub code_blocks: Vec<CodeBlock>,
|
||||
pub suggestions: Vec<String>,
|
||||
// Knowledge search fields
|
||||
pub nodes: Vec<KnowledgeNode>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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<AppState>,
|
||||
Json(req): Json<ReasonRequest>,
|
||||
) -> ApiResult<ReasonResponse> {
|
||||
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<ReasonResponse> {
|
||||
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<ReasonResponse> {
|
||||
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<serde_json::Value> = 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<CodeBlock> {
|
||||
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<String> {
|
||||
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<ReasonResponse> {
|
||||
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![],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Config>,
|
||||
pub plugins: Arc<Mutex<PluginHost>>,
|
||||
pub active_theme: Arc<RwLock<String>>,
|
||||
}
|
||||
|
||||
// ── 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)
|
||||
}
|
||||
|
||||
+3631
-288
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user