Compare commits
3
Commits
98a1716ac8
...
f59602a3e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59602a3e2 | ||
|
|
4e23043f5d | ||
|
|
b3c64c1ad8 |
No files matched your search
@@ -107,6 +107,14 @@ Whenever you alt-tab into a Neovim window (FocusGained) or switch files (BufEnte
|
||||
2. **WebSocket Telemetry:** Neovim pushes a JSON payload containing the active filename, cursor row, and column via a connectionless UDP datagram to the Rust server's port 3002 listener.
|
||||
3. **UI Broadcast:** The Rust server updates the global state and broadcasts this over WebSockets (/ws) so that the Brain Monitor Dashboard can animate your active file live in the UI!
|
||||
|
||||
### Interactive UDP UI (New)
|
||||
The UI script (`gemini-ui.lua`) provides a deeply integrated, non-blocking pair-programming experience using zero-latency UDP:
|
||||
* **Interactive Prompts:** The agent can trigger native `vim.ui.select` or `vim.ui.input` dialogs in your editor. Your responses are instantly routed back to the agent via UDP.
|
||||
* **Smart Context (`<leader>ai`):** Highlighting code and pressing `<leader>ai` will package your prompt, file, and exact cursor/selection coordinates into a UDP packet and send it directly to the agent without spawning any subprocesses.
|
||||
* **Ghost Text Diffs (Non-Destructive Review):** Instead of modifying your buffers directly, the agent uses `extmarks` to overlay proposed code changes as grayed-out "Ghost Text".
|
||||
* Press `<leader>aa` (**A**gent **A**ccept) to apply the change and notify the agent.
|
||||
* Press `<leader>ar` (**A**gent **R**eject) to dismiss the change and notify the agent.
|
||||
|
||||
### Neovim MCP Tools
|
||||
The LLM agent interacts with your active Neovim session using a dedicated set of MCP tools. *(Note: /nvim/telemetry is strictly a one-way webhook for Neovim; the LLM uses the tools below to interact).*
|
||||
* **vim_goto_line**: Open files and jump cursors directly from the LLM.
|
||||
|
||||
@@ -64,12 +64,17 @@ Currently implemented persistent stores include:
|
||||
* **Environment State:** Pinned Files, Env Fingerprints, Milestones, and Environments.
|
||||
* **Safety Gates:** Authorized execution gates (Push Safety).
|
||||
|
||||
## 7. Full-Text Search Engine (Tantivy)
|
||||
To support blazing-fast, intelligent semantic retrieval across the sprawling knowledge graph, the daemon embeds **Tantivy**, a full-text search engine (inspired by Apache Lucene).
|
||||
* **The `MemoryIndex`:** Whenever the graph or auxiliary stores mutate, a background thread dynamically rebuilds the Tantivy index (`tantivy_index/` dir).
|
||||
* **Global Omni-Search:** This architecture powers the `omni_search` tool, allowing subagents to instantly fuzzy-search and rank documents across Entities, Tasks, Snippets, Error Fixes, and ADRs simultaneously in milliseconds, without loading massive JSON arrays into RAM.
|
||||
## 7. Full-Text & Semantic Search Engine (Tantivy + FastEmbed)
|
||||
To support blazing-fast, intelligent semantic retrieval across the sprawling knowledge graph, the daemon embeds **Tantivy** (a full-text search engine inspired by Apache Lucene) alongside **FastEmbed** (a local ONNX runtime for vector embeddings).
|
||||
* **The `MemoryIndex`:** Whenever the graph or auxiliary stores mutate, a background thread dynamically rebuilds the Tantivy index (`tantivy_index/` dir) and computes semantic vectors.
|
||||
* **Global Omni-Search:** This architecture powers the `omni_search` tool, allowing subagents to instantly fuzzy-search and semantically rank documents across Entities, Tasks, Snippets, Error Fixes, and ADRs simultaneously in milliseconds, without loading massive JSON arrays into RAM.
|
||||
|
||||
## 8. The Gate System (Push Safety Verification)
|
||||
## 8. Webhook Telemetry & Passive Ingestion
|
||||
The server features a suite of webhook listeners that passively ingest development activity to build context without requiring human copy-pasting:
|
||||
* **Terminal Ingestion (`/terminal/telemetry`):** Shell hooks (PowerShell/Nushell) silently POST command execution history and exit codes, allowing agents to read recent stack traces via the `memory://terminal/recent` resource.
|
||||
* **IDE Telemetry (`/nvim/telemetry`):** A connectionless UDP datagram listener receives focus events from Neovim, broadcasting the user's active file to the UI and Agent instantly.
|
||||
|
||||
## 9. The Gate System (Push Safety Verification)
|
||||
The binary supports a flexible safety gate authorization system, accessible both via the HTTP API and CLI subcommands.
|
||||
* **API / CLI set**: Records an authorization status (`authorized`, `blocked`, or `pending`) for a specific target and namespace. Can be invoked via `POST /gate/set` (JSON) or `mcp-memory-stub gate set`.
|
||||
* **API / CLI verify**: Evaluates a pending action against the gate store. It returns standard HTTP status codes (`200 OK`, `403 Forbidden`, `404 Not Found`) via `GET /gate/verify?action=...` or POSIX exit codes (0, 1, 2) via `mcp-memory-stub gate verify`. Both support a `consume` parameter/flag to immediately revoke the authorization after a successful check.
|
||||
@@ -165,12 +170,10 @@ Instead of forcing the LLM client to manually run `git rev-parse HEAD` and pass
|
||||
When engineering endpoints (`log_code_change`, `log_error_fix`, `log_tech_debt`) are invoked, the server asynchronously discovers the surrounding Git repository, peels the HEAD reference, and automatically injects the current commit hash, commit message, and branch name directly into the stored entities and audit logs. This guarantees airtight VCS traceability without wasting LLM tokens or relying on the agent's memory.
|
||||
|
||||
## 18. Upcoming Enhancements
|
||||
### A. Vector Embeddings for Semantic Search
|
||||
The `omni_search` will be upgraded from purely lexical `tantivy` indexing to include a local ONNX embedding model (via `fastembed`). This allows semantic retrieval of `ErrorFix` and `TechDebt` entries even when exact vocabulary does not match.
|
||||
|
||||
### B. Passive Terminal Ingestion
|
||||
A shell hook system (PowerShell/Nushell) will silently POST terminal execution history and exit codes to `/terminal/history`. LLMs can passively read this via the `memory://terminal/recent` resource, eliminating the need for the user to manually copy/paste stack traces.
|
||||
|
||||
### C. Graph Summarization & Decay
|
||||
### A. Graph Summarization & Decay
|
||||
To prevent context window bloat, the Redb engine will enforce TTLs on ephemeral nodes (`StickyNotes`, minor `Tasks`). Furthermore, an `archive_routine` prompt will allow subagents to automatically compress older `SessionSummaries` into dense milestone retrospectives.
|
||||
|
||||
### B. Proactive Subagent Triggers
|
||||
The daemon will eventually gain the ability to autonomously spawn background subagents (like the `BugDiagnostician`) when specific events are detected in the webhook telemetry, rather than relying strictly on the human user to initiate the agent.
|
||||
|
||||
+117
-25
@@ -55,35 +55,118 @@ function M.close_progress()
|
||||
end
|
||||
end
|
||||
|
||||
local function execute_agent(prompt)
|
||||
local cmd = { "agy", "--new-project", "-i", prompt }
|
||||
|
||||
M.show_progress("Starting Antigravity...")
|
||||
|
||||
-- termopen requires an empty and unmodified buffer
|
||||
vim.api.nvim_buf_set_lines(progress_buf, 0, -1, false, {})
|
||||
vim.bo[progress_buf].modified = false
|
||||
|
||||
-- termopen will attach to the current buffer (which show_progress just focused)
|
||||
job_id = vim.fn.termopen(cmd, {
|
||||
on_exit = function(_, code)
|
||||
job_id = nil
|
||||
local function send_udp(payload)
|
||||
local uv = vim.uv or vim.loop
|
||||
local client = uv.new_udp()
|
||||
if client then
|
||||
local ok, data = pcall(vim.fn.json_encode, payload)
|
||||
if ok then
|
||||
client:send(data, "127.0.0.1", 3002, function(err)
|
||||
client:close()
|
||||
end)
|
||||
else
|
||||
client:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.prompt_user(prompt_text, options, request_id)
|
||||
if options and #options > 0 then
|
||||
vim.ui.select(options, { prompt = prompt_text }, function(choice, idx)
|
||||
send_udp({
|
||||
event = "agent_prompt_response",
|
||||
request_id = request_id,
|
||||
response = choice,
|
||||
index = idx,
|
||||
session_id = vim.v.servername
|
||||
})
|
||||
end)
|
||||
else
|
||||
vim.ui.input({ prompt = prompt_text }, function(input)
|
||||
send_udp({
|
||||
event = "agent_prompt_response",
|
||||
request_id = request_id,
|
||||
response = input,
|
||||
session_id = vim.v.servername
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local ns_id = vim.api.nvim_create_namespace("gemini_ghost_text")
|
||||
local active_diffs = {}
|
||||
|
||||
function M.preview_diff(diff_id, bufnr, start_row, end_row, new_lines)
|
||||
bufnr = (bufnr == 0 or bufnr == nil) and vim.api.nvim_get_current_buf() or bufnr
|
||||
local virt_lines = {}
|
||||
for _, line in ipairs(new_lines) do
|
||||
table.insert(virt_lines, {{line, "DiffAdd"}})
|
||||
end
|
||||
|
||||
local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, ns_id, start_row, 0, {
|
||||
virt_lines = virt_lines,
|
||||
virt_lines_above = true,
|
||||
})
|
||||
vim.cmd('startinsert')
|
||||
|
||||
active_diffs[diff_id] = {
|
||||
bufnr = bufnr,
|
||||
start_row = start_row,
|
||||
end_row = end_row,
|
||||
new_lines = new_lines,
|
||||
extmark_id = extmark_id
|
||||
}
|
||||
|
||||
if pcall(require, "notify") then
|
||||
require("notify")("Ghost text diff added. Use <leader>aa to accept or <leader>ar to reject.", "info", { title = "Antigravity" })
|
||||
else
|
||||
print("Ghost text diff added. Use <leader>aa to accept or <leader>ar to reject.")
|
||||
end
|
||||
end
|
||||
|
||||
function M.accept_diff()
|
||||
local diff_id, diff = next(active_diffs)
|
||||
if not diff then
|
||||
print("No active diffs to accept.")
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_row, diff.end_row, false, diff.new_lines)
|
||||
vim.api.nvim_buf_del_extmark(diff.bufnr, ns_id, diff.extmark_id)
|
||||
active_diffs[diff_id] = nil
|
||||
|
||||
send_udp({ event = "agent_diff_accepted", diff_id = diff_id, session_id = vim.v.servername })
|
||||
print("Diff accepted.")
|
||||
end
|
||||
|
||||
function M.reject_diff()
|
||||
local diff_id, diff = next(active_diffs)
|
||||
if not diff then
|
||||
print("No active diffs to reject.")
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_del_extmark(diff.bufnr, ns_id, diff.extmark_id)
|
||||
active_diffs[diff_id] = nil
|
||||
|
||||
send_udp({ event = "agent_diff_rejected", diff_id = diff_id, session_id = vim.v.servername })
|
||||
print("Diff rejected.")
|
||||
end
|
||||
|
||||
function M.ask_agent()
|
||||
local file = vim.api.nvim_buf_get_name(0)
|
||||
vim.ui.input({ prompt = 'Ask Agent: ' }, function(input)
|
||||
if not input or input == '' then return end
|
||||
|
||||
local final_input = input
|
||||
if file and file ~= "" then
|
||||
final_input = input .. string.format(" [Context file: %s]", file)
|
||||
end
|
||||
|
||||
execute_agent(final_input)
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
send_udp({
|
||||
event = "agent_ask",
|
||||
prompt = input,
|
||||
file = file,
|
||||
line = cursor[1],
|
||||
col = cursor[2],
|
||||
session_id = vim.v.servername
|
||||
})
|
||||
M.show_progress("Sent to Antigravity via UDP...")
|
||||
vim.defer_fn(M.close_progress, 1500)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -97,12 +180,21 @@ function M.setup()
|
||||
if not input or input == '' then return end
|
||||
|
||||
local file = vim.api.nvim_buf_get_name(0)
|
||||
local context = string.format(" [Context: %s lines %d-%d]", file, csrow, cerow)
|
||||
local final_input = input .. context
|
||||
|
||||
execute_agent(final_input)
|
||||
send_udp({
|
||||
event = "agent_ask_selection",
|
||||
prompt = input,
|
||||
file = file,
|
||||
start_row = csrow,
|
||||
end_row = cerow,
|
||||
session_id = vim.v.servername
|
||||
})
|
||||
M.show_progress("Sent selection to Antigravity via UDP...")
|
||||
vim.defer_fn(M.close_progress, 1500)
|
||||
end)
|
||||
end, { desc = "Ask Antigravity Agent about selection", noremap = true, silent = true })
|
||||
|
||||
vim.keymap.set('n', '<leader>aa', M.accept_diff, { desc = "Accept Agent Diff", noremap = true, silent = true })
|
||||
vim.keymap.set('n', '<leader>ar', M.reject_diff, { desc = "Reject Agent Diff", noremap = true, silent = true })
|
||||
end
|
||||
|
||||
return M
|
||||
+3
-2
@@ -112,8 +112,9 @@ To maintain absolute traceability, we link memory items directly to the exact gi
|
||||
|
||||
|
||||
## 15. Neovim Integration & God Mode
|
||||
The project contains two MCP binaries (win-nvim and linux-nvim) that bridge JSON-RPC over stdio directly to the active Neovim instance (using `active_nvim.txt` for Last Focused Wins telemetry). If no interactive instance is open, they will automatically spawn and connect to a persistent headless Neovim background instance. **Note on Visual Engagement:** While the headless instance is perfect for autonomous/background tasks, if the task involves collaborative "pair programming" or requires the user's visual review, you should pause and request the user to open a Neovim UI before proceeding.
|
||||
- These binaries expose basic tools (`nvim_get_cursor`, `nvim_get_active_buffer`, `nvim_list_buffers`, etc.).
|
||||
The project contains two MCP binaries (win-nvim and linux-nvim) that bridge JSON-RPC over stdio directly to the active Neovim instance.
|
||||
- **Interactive UI (UDP):** The active editor connects to the backend via UDP (port 3002). The UI script (`gemini-ui.lua`) supports non-destructive **Ghost Text Diff Reviews** and native `vim.ui` prompts without spawning subprocesses. You can trigger these interactively using the backend server.
|
||||
- **Tools:** These binaries expose basic tools (`nvim_get_cursor`, `nvim_get_active_buffer`, `nvim_list_buffers`, etc.).
|
||||
- **God Mode**: They also expose `nvim_execute_lua`. This is the ultimate fallback tool. If you need to access *any* Neovim API that does not have a dedicated Rust tool (e.g., getting LSP diagnostics, evaluating a visual selection block based on modes, setting registers), you MUST write a short Lua script and pass it to `nvim_execute_lua`. Do not attempt to recompile the Rust server to add new basic tools; use the Lua escape hatch dynamically.
|
||||
|
||||
## 16. Tool Schema Discovery (Lazy Loading)
|
||||
|
||||
@@ -9,6 +9,14 @@ pub struct NvimTelemetry {
|
||||
pub file: Option<String>,
|
||||
pub line: Option<i64>,
|
||||
pub col: Option<i64>,
|
||||
// New interactive fields
|
||||
pub prompt: Option<String>,
|
||||
pub response: Option<String>,
|
||||
pub index: Option<i64>,
|
||||
pub request_id: Option<String>,
|
||||
pub diff_id: Option<String>,
|
||||
pub start_row: Option<i64>,
|
||||
pub end_row: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn nvim_telemetry_handler(
|
||||
@@ -138,7 +146,8 @@ mod tests {
|
||||
let payload = TerminalHistory {
|
||||
command: "echo test".to_string(),
|
||||
exit_code: 0,
|
||||
|
||||
cwd: "C:/Users/reazul.ashraf/workspace".to_string(),
|
||||
os: "windows".to_string(),
|
||||
timestamp: 12345,
|
||||
};
|
||||
|
||||
|
||||
@@ -1126,6 +1126,10 @@
|
||||
loadGenericList('/api/terminal/history', 'terminal-container', item => `
|
||||
<span style="font-weight:bold; font-size:1.05em; color:var(--text-primary);">${item.command}</span>
|
||||
<span style="margin-left:15px; font-size:0.85em;"><span style="font-weight:bold; color:var(--text-secondary);">Exit Code:</span> <span style="color:${item.exit_code === 0 ? 'var(--success-color)' : 'var(--error-color)'}; font-weight:bold;">${item.exit_code}</span></span>
|
||||
<div style="margin-top:10px; font-size:0.85em;">
|
||||
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; margin-right:8px; border:1px solid var(--border-color);">OS: ${item.os || 'Unknown'}</span>
|
||||
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">CWD: ${item.cwd || 'Unknown'}</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
loadGenericList('/api/tech_debts', 'techdebt-container', item => `
|
||||
|
||||
@@ -247,6 +247,17 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
||||
};
|
||||
let _ = nvim_udp_state.handler.state.event_bus_tx.send(event);
|
||||
}
|
||||
|
||||
// 4. Interactive Agent UI Events
|
||||
if payload.event.starts_with("agent_") {
|
||||
let topic = format!("nvim:ui:{}", payload.event);
|
||||
let event = crate::state::GenericEvent {
|
||||
topic,
|
||||
session_id: Some(payload.session_id.clone()),
|
||||
payload: serde_json::json!(&payload),
|
||||
};
|
||||
let _ = nvim_udp_state.handler.state.event_bus_tx.send(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,8 @@ pub struct GateRecord {
|
||||
pub struct TerminalHistory {
|
||||
pub command: String,
|
||||
pub exit_code: i32,
|
||||
|
||||
pub cwd: String,
|
||||
pub os: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user