feat: event-driven parity, Windows shell detection, dirty build timestamps, and gatekeeper integration

This commit is contained in:
Riz Ashraf committed 2026-10-08 10:52:19 +01:00
1 parent 64857f9d5e
commit f4dcf3acbf
33 files changed
+3501 -143

No files matched your search

+3
View File
@@ -17,3 +17,6 @@ mcp-stdio = { version = "1.0.0", path = "../mcp-stdio" }
bytes = "1.12.1"
dashmap = "6.2.1"
[build-dependencies]
chrono = "0.4.45"
+20 -3
View File
@@ -1,22 +1,39 @@
use std::process::Command;
fn main() {
let git_hash = Command::new("git")
let mut git_hash = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())
.unwrap_or_else(|| "unknown".to_string());
git_hash = git_hash.trim().to_string();
let is_dirty = Command::new("git")
.args(["status", "--porcelain"])
.output()
.is_ok_and(|out| !out.stdout.is_empty());
if is_dirty {
let dirty_ts = chrono::Local::now().format("%y.%m.%d.%H%M%S").to_string();
git_hash.push_str(&format!("-dirty-{dirty_ts}"));
}
let git_date = Command::new("git")
.args(["log", "-1", "--format=%cd", "--date=format:%y.%m.%d.%H%M%S"])
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())
.unwrap_or_else(|| "unknown".to_string());
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| chrono::Local::now().format("%y.%m.%d.%H%M%S").to_string());
let version = format!("{} ({})", git_date.trim(), git_hash.trim());
let version = format!("v{git_date} ({git_hash})");
println!("cargo:rustc-env=APP_VERSION={version}");
println!("cargo:rerun-if-changed=../.git/HEAD");
println!("cargo:rerun-if-changed=../.git/index");
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-changed=build.rs");
}
+97 -6
View File
@@ -319,6 +319,7 @@ static PENDING_REQUESTS: LazyLock<
pub struct NvimState {
pub cursor: String,
pub active_buffer_id: u64,
pub active_buffer_name: String,
}
static NVIM_STATE: LazyLock<Arc<std::sync::Mutex<NvimState>>> =
@@ -341,8 +342,27 @@ fn handle_nvim_notification(params: &[rmpv::Value]) {
state.cursor = format!("Line: {}, Column: {}", row, col);
}
}
"TextChanged" => {
tracing::debug!("Shadow buffer text changed event received");
"TextChanged" | "BufEnter" => {
if params.len() > 1 {
let buf_id = match &params[1] {
rmpv::Value::Integer(i) => i.as_u64().unwrap_or(0),
_ => 0,
};
let buf_name = if params.len() > 2 {
match &params[2] {
rmpv::Value::String(s) => s.as_str().unwrap_or("").to_string(),
_ => String::new(),
}
} else {
String::new()
};
let mut state = NVIM_STATE.lock().unwrap_or_else(|e| e.into_inner());
state.active_buffer_id = buf_id;
if !buf_name.is_empty() {
state.active_buffer_name = buf_name;
}
}
tracing::debug!("Neovim buffer changed/entered event received");
}
_ => {}
}
@@ -591,7 +611,9 @@ async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
vim.api.nvim_create_autocmd({"TextChanged", "TextChangedI", "BufEnter"}, {
group = "MCP_Tracking",
callback = function()
pcall(vim.rpcnotify, channel, "mcp_event", "TextChanged", vim.api.nvim_get_current_buf())
local b = vim.api.nvim_get_current_buf()
local n = vim.api.nvim_buf_get_name(b)
pcall(vim.rpcnotify, channel, "mcp_event", "TextChanged", b, n)
end
})
"#;
@@ -1686,7 +1708,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
bufnr = 0
end
_G.gemini.preview_diff(diff_id, bufnr, start_line, end_line, lines)
return "Preview added"
return { status = "Preview added", diff_id = diff_id }
else
error("gemini UI not loaded")
end
@@ -1708,6 +1730,61 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
send_error(id, -32602, "Missing arguments: requires file_path, start_line, end_line, and preview_content").await;
}
}
"prompt" | "prompt_user" => {
let prompt_text = args
.get("prompt")
.or_else(|| args.get("prompt_text"))
.and_then(|v| v.as_str());
if let Some(prompt) = prompt_text {
use std::time::{SystemTime, UNIX_EPOCH};
let request_id = args
.get("request_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.to_string()
});
let options_arr = match args.get("options").and_then(|v| v.as_array()) {
Some(arr) => {
let rmp_arr: Vec<rmpv::Value> = arr
.iter()
.filter_map(|item| item.as_str())
.map(|s| rmpv::Value::String(s.into()))
.collect();
rmpv::Value::Array(rmp_arr)
}
None => rmpv::Value::Nil,
};
let lua_code = r#"
local prompt_text, options, request_id = ...
if _G.gemini and _G.gemini.prompt_user then
_G.gemini.prompt_user(prompt_text, options, request_id)
return { status = "Prompt displayed", request_id = request_id }
else
error("gemini UI not loaded")
end
"#;
handle_lua_result!(
id,
execute_nvim_lua_with_args(
lua_code,
vec![
rmpv::Value::String(prompt.into()),
options_arr,
rmpv::Value::String(request_id.into()),
]
)
);
} else {
send_error(id, -32602, "Missing 'prompt' or 'prompt_text'").await;
}
}
"extmark" => {
if let (Some(line), Some(text)) = (
args.get("line").and_then(serde_json::Value::as_i64),
@@ -1996,8 +2073,22 @@ mod tests {
]),
];
handle_nvim_notification(&params);
let state = NVIM_STATE.lock().unwrap();
assert_eq!(state.cursor, "Line: 15, Column: 4");
{
let state = NVIM_STATE.lock().unwrap();
assert_eq!(state.cursor, "Line: 15, Column: 4");
}
let text_params = vec![
rmpv::Value::String("TextChanged".into()),
rmpv::Value::Integer(157.into()),
rmpv::Value::String("server/src/dashboard.ts".into()),
];
handle_nvim_notification(&text_params);
{
let state = NVIM_STATE.lock().unwrap();
assert_eq!(state.active_buffer_id, 157);
assert_eq!(state.active_buffer_name, "server/src/dashboard.ts");
}
}
#[tokio::test]