chore: fix clippy warnings, add embedding/omni_patch, update telemetry, and ignore temp files
This commit is contained in:
1 parent
495304f3fa
commit
98a1716ac8
23 files changed
+2252
-74
No files matched your search
@@ -1,3 +1,9 @@
|
|||||||
/target
|
/target
|
||||||
cargo-llvm-cov.exe
|
cargo-llvm-cov.exe
|
||||||
llvm-cov.zip
|
llvm-cov.zip
|
||||||
|
|
||||||
|
.fastembed_cache/
|
||||||
|
*.py
|
||||||
|
temp.json
|
||||||
|
*.txt
|
||||||
|
summary.md
|
||||||
Generated
+1710
-13
File diff suppressed because it is too large.
Load diff
@@ -104,7 +104,7 @@ dofile("C:/Users/reazul.ashraf/workspace/rust/mcp-memory/gemini-integration.lua"
|
|||||||
When you use the gemini-integration.lua script, Neovim acts as an active telemetry broadcaster.
|
When you use the gemini-integration.lua script, Neovim acts as an active telemetry broadcaster.
|
||||||
Whenever you alt-tab into a Neovim window (FocusGained) or switch files (BufEnter):
|
Whenever you alt-tab into a Neovim window (FocusGained) or switch files (BufEnter):
|
||||||
1. **Fallback Sync:** Neovim instantly writes its unique Session ID (Named Pipe / Unix Socket) to ~/.gemini/active_nvim.txt.
|
1. **Fallback Sync:** Neovim instantly writes its unique Session ID (Named Pipe / Unix Socket) to ~/.gemini/active_nvim.txt.
|
||||||
2. **WebSocket Telemetry:** Neovim pushes a JSON payload containing the active filename, cursor row, and column to the Rust server's /nvim/telemetry webhook.
|
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!
|
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!
|
||||||
|
|
||||||
### Neovim MCP Tools
|
### Neovim MCP Tools
|
||||||
@@ -114,3 +114,4 @@ The LLM agent interacts with your active Neovim session using a dedicated set of
|
|||||||
* **vim_get_active_buffer**: Read live, unsaved buffer contents.
|
* **vim_get_active_buffer**: Read live, unsaved buffer contents.
|
||||||
* **vim_get_cursor**: Fetch precise line/column coordinates.
|
* **vim_get_cursor**: Fetch precise line/column coordinates.
|
||||||
* **vim_get_visual_selection**: Read highlighted code blocks.
|
* **vim_get_visual_selection**: Read highlighted code blocks.
|
||||||
|
|
||||||
@@ -25,3 +25,15 @@ Invoke these dynamically using the `invoke_subagent` tool. Use `send_message` to
|
|||||||
* **Permissions:** You MUST define this subagent with `enable_write_tools: true`.
|
* **Permissions:** You MUST define this subagent with `enable_write_tools: true`.
|
||||||
* **Trigger:** When facing complex architectural bugs, race conditions, or eventually consistent state issues that are hard to track down.
|
* **Trigger:** When facing complex architectural bugs, race conditions, or eventually consistent state issues that are hard to track down.
|
||||||
* **Action:** Instead of guessing or patching blindly, this subagent creates diagnostic scripts, enhances reproducers with detailed logs, and isolates the bug. Once root cause is definitively proven, it hands the diagnostic evidence back to you to implement the fix.
|
* **Action:** Instead of guessing or patching blindly, this subagent creates diagnostic scripts, enhances reproducers with detailed logs, and isolates the bug. Once root cause is definitively proven, it hands the diagnostic evidence back to you to implement the fix.
|
||||||
|
|
||||||
|
## 4. ScrumMaster
|
||||||
|
* **Role:** Project & Task Orchestrator
|
||||||
|
* **Permissions:** You MUST define this subagent with `enable_mcp_tools: true` and `enable_write_tools: true`.
|
||||||
|
* **Trigger:** When we start a new feature or finish a task.
|
||||||
|
* **Action:** Passively monitors the `memory://tasks/active` and `memory://milestones` MCP resources. Creates tasks, organizes milestones, and updates statuses when criteria are met using `add_task`, `update_task_status`, `add_milestone`, and `verify_acceptance_criteria`.
|
||||||
|
|
||||||
|
## 5. DevOpsSRE
|
||||||
|
* **Role:** Environment & Handoff Manager
|
||||||
|
* **Permissions:** You MUST define this subagent with `enable_mcp_tools: true` and `enable_write_tools: true`.
|
||||||
|
* **Trigger:** When dependencies change (e.g., you modify `Cargo.toml`, `package.json`, or `.env`) or at the end of a session.
|
||||||
|
* **Action:** Updates environment fingerprints when dependency files change. At the end of a session, it executes the `handoff_routine` MCP prompt to automatically generate standup reports and handoff memos.
|
||||||
@@ -25,7 +25,7 @@ The server hosts a rich Single Page Application (SPA) natively on the root route
|
|||||||
* `GET /api/stats`: Returns a JSON snapshot of current entity, relation, task, and tech debt counts.
|
* `GET /api/stats`: Returns a JSON snapshot of current entity, relation, task, and tech debt counts.
|
||||||
|
|
||||||
### Git & IDE Integration Endpoints
|
### Git & IDE Integration Endpoints
|
||||||
* `POST /nvim/telemetry`: A one-way webhook that Neovim instances hit on `FocusGained` or `BufEnter` to globally broadcast the user's active file to the UI and Agent.
|
* `UDP /nvim/telemetry`: A one-way connectionless datagram listener that Neovim instances hit on `FocusGained` or `BufEnter` to globally broadcast the user's active file to the UI and Agent.
|
||||||
* `GET /gate/verify`: A lightweight, deterministic endpoint used by external scripts (like a `git push` wrapper) to verify if an action is authorized based on the current state.
|
* `GET /gate/verify`: A lightweight, deterministic endpoint used by external scripts (like a `git push` wrapper) to verify if an action is authorized based on the current state.
|
||||||
|
|
||||||
## 3. Client Connections
|
## 3. Client Connections
|
||||||
@@ -127,7 +127,7 @@ eqwest and okio).
|
|||||||
To enable seamless pair-programming inside Neovim, the daemon integrates with Neovim using two complementary systems: a Webhook Telemetry pipeline and dedicated MCP binaries.
|
To enable seamless pair-programming inside Neovim, the daemon integrates with Neovim using two complementary systems: a Webhook Telemetry pipeline and dedicated MCP binaries.
|
||||||
|
|
||||||
### Telemetry Pipeline (Last Focused Wins)
|
### Telemetry Pipeline (Last Focused Wins)
|
||||||
A lightweight Lua script (gemini-integration.lua) is loaded into Neovim, which fires an asynchronous curl POST to http://127.0.0.1:3000/nvim/telemetry whenever the user focuses a buffer or moves the cursor. The server then writes this data (including session_id, ile, line, and col) to both the Windows and WSL ctive_nvim.txt files and broadcasts it over WebSockets.
|
A lightweight Lua script (`gemini-integration.lua`) is loaded into Neovim, which fires an asynchronous UDP datagram to `127.0.0.1:3002` (fire-and-forget via native libuv) whenever the user focuses a buffer or moves the cursor. The server then writes this data (including `session_id`, `file`, `line`, and `col`) to both the Windows and WSL `active_nvim.txt` files and broadcasts it over WebSockets.
|
||||||
|
|
||||||
### Native MCP Binaries (win-nvim & linux-nvim)
|
### Native MCP Binaries (win-nvim & linux-nvim)
|
||||||
The project compiles two standalone, highly-performant binaries that implement the MCP JSON-RPC protocol over Stdio and bridge it directly to Neovim's Msgpack-RPC Named Pipes/Sockets. These binaries avoid hardcoding infinite tools by utilizing a "God Mode" escape hatch.
|
The project compiles two standalone, highly-performant binaries that implement the MCP JSON-RPC protocol over Stdio and bridge it directly to Neovim's Msgpack-RPC Named Pipes/Sockets. These binaries avoid hardcoding infinite tools by utilizing a "God Mode" escape hatch.
|
||||||
@@ -163,3 +163,14 @@ Historically, the project relied on a complex Python testing suite (\pytest\ + \
|
|||||||
## 17. Automated Git Context Binding (git2)
|
## 17. Automated Git Context Binding (git2)
|
||||||
Instead of forcing the LLM client to manually run `git rev-parse HEAD` and pass `git_branch` / `git_commit` arguments for every single code change, the server integrates the native **`git2`** C bindings.
|
Instead of forcing the LLM client to manually run `git rev-parse HEAD` and pass `git_branch` / `git_commit` arguments for every single code change, the server integrates the native **`git2`** C bindings.
|
||||||
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.
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
+9
-3
@@ -92,9 +92,14 @@ When shifting context rapidly (e.g. from building a feature to fixing a prod bug
|
|||||||
- **load_context_workspace:** Retrieve a saved workspace to instantly restore your context when returning to that task.
|
- **load_context_workspace:** Retrieve a saved workspace to instantly restore your context when returning to that task.
|
||||||
- **list_context_workspaces:** List all saved workspaces in a project.
|
- **list_context_workspaces:** List all saved workspaces in a project.
|
||||||
|
|
||||||
## Omni-Search (Global Vault Search)
|
## Omni-Search (Hybrid Vector Search)
|
||||||
When you remember a vague keyword but don't know which specific vault it's stored in.
|
When you remember a vague concept, keyword, or architectural pattern but don't know which specific vault it's stored in.
|
||||||
- **omni_search:** Searches across the Knowledge Graph, Tasks, Snippets, ADRs, Tech Debt, Memos, and Error Fixes simultaneously.
|
- **omni_search:** Searches across the Knowledge Graph, Tasks, Snippets, ADRs, Tech Debt, Memos, and Error Fixes simultaneously.
|
||||||
|
- **Semantic Superpower:** This tool utilizes **Hybrid Vector Search** (BM25 + Vector Embeddings). You do not need exact keyword matches. You can query conceptually (e.g., "database lock issues" or "how is auth handled") and the engine will return semantically relevant context.
|
||||||
|
|
||||||
|
## Terminal History & Telemetry
|
||||||
|
To maintain an audit trail of shell commands and execution contexts.
|
||||||
|
- **Context:** The server actively tracks Terminal History. Commands executed in the user's shell are logged to the memory server and visible in the dashboard UI. While you don't call an MCP tool for this directly, be aware that complex shell actions and their outcomes are persisted for historical auditing and UI visibility.
|
||||||
|
|
||||||
## Project Health Dashboard
|
## Project Health Dashboard
|
||||||
When starting a new session, get a numerical aggregate of the project's current state.
|
When starting a new session, get a numerical aggregate of the project's current state.
|
||||||
@@ -112,4 +117,5 @@ The project contains two MCP binaries (win-nvim and linux-nvim) that bridge JSON
|
|||||||
- **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.
|
- **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)
|
## 16. Tool Schema Discovery (Lazy Loading)
|
||||||
Antigravity automatically caches all MCP tool schemas to your disk to save tokens. Do **NOT** grep or search the Rust source code to find tool schemas or arguments. To understand a tool`s arguments, directly read `~/.gemini/antigravity-cli/mcp/<server_name>/<tool_name>.json`. For simple tools, confidently guess the arguments instead of reading the schema to save time.
|
Antigravity automatically caches all MCP tool schemas to your disk to save tokens. Do **NOT** grep or search the Rust source code to find tool schemas or arguments. To understand a tool`s arguments, directly read `~/.gemini/antigravity-cli/mcp/<server_name>/<tool_name>.json`. Do NOT guess arguments. ALWAYS read the schema if you are unfamiliar with a tool to prevent invalid argument errors.
|
||||||
|
|
||||||
+18
-5
@@ -90,12 +90,20 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
||||||
let p = content.trim().to_string();
|
let p = content.trim().to_string();
|
||||||
if !p.is_empty() {
|
if !p.is_empty() {
|
||||||
if p.starts_with(r"\\.\pipe\") {
|
let candidate = if p.starts_with(r"\\.\pipe\") {
|
||||||
return Ok(p);
|
p
|
||||||
} else if p.starts_with("nvim.") {
|
} else if p.starts_with("nvim.") {
|
||||||
return Ok(format!(r"\\.\pipe\{p}"));
|
format!(r"\\.\pipe\{p}")
|
||||||
|
} else {
|
||||||
|
p
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_interactive(&candidate).await {
|
||||||
|
return Ok(candidate);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Stale active_nvim.txt detected on Windows. Deleting.");
|
||||||
|
let _ = tokio::fs::remove_file(&path).await;
|
||||||
}
|
}
|
||||||
return Ok(p);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +130,12 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
||||||
let p = content.trim().to_string();
|
let p = content.trim().to_string();
|
||||||
if !p.is_empty() {
|
if !p.is_empty() {
|
||||||
return Ok(p);
|
if is_interactive(&p).await {
|
||||||
|
return Ok(p);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Stale active_nvim.txt detected on Unix. Deleting.");
|
||||||
|
let _ = tokio::fs::remove_file(&path).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ tracing-appender = "0.2.5"
|
|||||||
rmcp = { version = "3.4.0", features = ["server"] }
|
rmcp = { version = "3.4.0", features = ["server"] }
|
||||||
thiserror = "2.0.20"
|
thiserror = "2.0.20"
|
||||||
tower = { version = "0.5.3", features = ["util"] }
|
tower = { version = "0.5.3", features = ["util"] }
|
||||||
|
fastembed = "7.1.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
|||||||
.route("/ws", get(ws_handler))
|
.route("/ws", get(ws_handler))
|
||||||
.route("/health", get(health_handler))
|
.route("/health", get(health_handler))
|
||||||
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
||||||
|
.route("/api/terminal/history", get(crate::api::telemetry::get_terminal_history_handler))
|
||||||
|
.route("/terminal/telemetry", post(crate::api::telemetry::terminal_telemetry_handler))
|
||||||
.route("/events/wait", get(crate::api::events::wait_for_event_handler))
|
.route("/events/wait", get(crate::api::events::wait_for_event_handler))
|
||||||
.route("/events", post(crate::api::events::post_event_handler))
|
.route("/events", post(crate::api::events::post_event_handler))
|
||||||
.route("/gate/verify", get(gate_verify_handler))
|
.route("/gate/verify", get(gate_verify_handler))
|
||||||
|
|||||||
@@ -65,3 +65,108 @@ pub async fn nvim_telemetry_handler(
|
|||||||
|
|
||||||
axum::Json(serde_json::json!({"status": "ok"}))
|
axum::Json(serde_json::json!({"status": "ok"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::models::TerminalHistory;
|
||||||
|
|
||||||
|
pub async fn get_terminal_history_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> impl axum::response::IntoResponse {
|
||||||
|
let history_json = state.handler.state.terminal_history.read_with(|h| serde_json::to_string(h).unwrap_or_else(|_| "[]".to_string()));
|
||||||
|
([(axum::http::header::CONTENT_TYPE, "application/json")], history_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn terminal_telemetry_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
axum::Json(payload): axum::Json<TerminalHistory>,
|
||||||
|
) -> impl axum::response::IntoResponse {
|
||||||
|
state.handler.state.terminal_history.modify(|history| {
|
||||||
|
history.push_front(payload.clone());
|
||||||
|
if history.len() > 100 {
|
||||||
|
history.pop_back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ws_msg = serde_json::json!({
|
||||||
|
"type": "terminal_telemetry",
|
||||||
|
"data": payload
|
||||||
|
});
|
||||||
|
|
||||||
|
let msg_str = ws_msg.to_string();
|
||||||
|
let senders: Vec<_> = state
|
||||||
|
.clients
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
for tx in senders {
|
||||||
|
let _ = tx.try_send(msg_str.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
axum::Json(serde_json::json!({"status": "ok"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::router::MemoryHandler;
|
||||||
|
use crate::state::MemoryState;
|
||||||
|
use axum::http::Request;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::atomic::AtomicUsize;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use axum::routing::post;
|
||||||
|
use axum::routing::get;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_terminal_history() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||||
|
let app_state = Arc::new(AppState {
|
||||||
|
handler: Arc::new(MemoryHandler::new(state.clone())),
|
||||||
|
clients: std::sync::RwLock::new(HashMap::new()),
|
||||||
|
next_id: AtomicUsize::new(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.route("/api/terminal/history", get(get_terminal_history_handler))
|
||||||
|
.route("/terminal/telemetry", post(terminal_telemetry_handler))
|
||||||
|
.with_state(app_state.clone());
|
||||||
|
|
||||||
|
// Create a fake terminal telemetry payload
|
||||||
|
let payload = TerminalHistory {
|
||||||
|
command: "echo test".to_string(),
|
||||||
|
exit_code: 0,
|
||||||
|
|
||||||
|
timestamp: 12345,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Post the telemetry
|
||||||
|
let req = Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri("/terminal/telemetry")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(axum::body::Body::from(serde_json::to_string(&payload).unwrap()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = app.clone().oneshot(req).await.unwrap();
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
|
||||||
|
// Get the telemetry
|
||||||
|
let req2 = Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.uri("/api/terminal/history")
|
||||||
|
.body(axum::body::Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response2 = app.oneshot(req2).await.unwrap();
|
||||||
|
assert_eq!(response2.status(), 200);
|
||||||
|
|
||||||
|
let body_bytes = axum::body::to_bytes(response2.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let history: Vec<TerminalHistory> = serde_json::from_slice(&body_bytes).unwrap();
|
||||||
|
assert_eq!(history.len(), 1);
|
||||||
|
assert_eq!(history[0].command, "echo test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -478,6 +478,7 @@
|
|||||||
<button class="tab-button" onclick="switchTab('search-tab', this)">Omni-Search</button>
|
<button class="tab-button" onclick="switchTab('search-tab', this)">Omni-Search</button>
|
||||||
<button class="tab-button" onclick="switchTab('task-tab', this)">Task Board</button>
|
<button class="tab-button" onclick="switchTab('task-tab', this)">Task Board</button>
|
||||||
<button class="tab-button" onclick="switchTab('sticky-tab', this)">Sticky Notes</button>
|
<button class="tab-button" onclick="switchTab('sticky-tab', this)">Sticky Notes</button>
|
||||||
|
<button class="tab-button" onclick="switchTab('preferences-tab', this)">Preferences</button>
|
||||||
<button class="tab-button" onclick="switchTab('techdebt-tab', this)">Tech Debt</button>
|
<button class="tab-button" onclick="switchTab('techdebt-tab', this)">Tech Debt</button>
|
||||||
<button class="tab-button" onclick="switchTab('adrs-tab', this)">Decisions (ADR)</button>
|
<button class="tab-button" onclick="switchTab('adrs-tab', this)">Decisions (ADR)</button>
|
||||||
<button class="tab-button" onclick="switchTab('workspaces-tab', this)">Workspaces</button>
|
<button class="tab-button" onclick="switchTab('workspaces-tab', this)">Workspaces</button>
|
||||||
@@ -485,6 +486,7 @@
|
|||||||
<button class="tab-button" onclick="switchTab('memos-tab', this)">Handoffs</button>
|
<button class="tab-button" onclick="switchTab('memos-tab', this)">Handoffs</button>
|
||||||
<button class="tab-button" onclick="switchTab('snippets-tab', this)">Snippets</button>
|
<button class="tab-button" onclick="switchTab('snippets-tab', this)">Snippets</button>
|
||||||
<button class="tab-button" onclick="switchTab('pr-tab', this)">PRs</button>
|
<button class="tab-button" onclick="switchTab('pr-tab', this)">PRs</button>
|
||||||
|
<button class="tab-button" onclick="switchTab('terminal-tab', this)">Terminal</button>
|
||||||
<button class="tab-button" onclick="switchTab('activity-tab', this)">Live Activity</button>
|
<button class="tab-button" onclick="switchTab('activity-tab', this)">Live Activity</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -529,6 +531,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="preferences-tab" class="tab-content">
|
||||||
|
<div class="panel" style="flex:1; display:flex; flex-direction:column; overflow-y:auto;">
|
||||||
|
<h2>Global Preferences</h2>
|
||||||
|
<div id="preferences-container" style="display:flex; flex-direction:column; gap:15px; font-size: 0.9em; line-height: 1.4;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="sticky-tab" class="tab-content">
|
<div id="sticky-tab" class="tab-content">
|
||||||
<div class="panel sticky-panel" style="flex:1; display:flex; flex-direction:column;">
|
<div class="panel sticky-panel" style="flex:1; display:flex; flex-direction:column;">
|
||||||
<h2>Sticky Notes</h2>
|
<h2>Sticky Notes</h2>
|
||||||
@@ -589,6 +598,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="terminal-tab" class="tab-content">
|
||||||
|
<div class="panel" style="flex:1; display:flex; flex-direction:column; overflow-y:auto;">
|
||||||
|
<h2>Terminal History</h2>
|
||||||
|
<div id="terminal-container" style="display:flex; flex-direction:column; gap:15px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="activity-tab" class="tab-content">
|
<div id="activity-tab" class="tab-content">
|
||||||
<div class="panel activity-panel" style="flex:1; display:flex; flex-direction:column;">
|
<div class="panel activity-panel" style="flex:1; display:flex; flex-direction:column;">
|
||||||
<div id="activity-feed" style="flex:1;"></div>
|
<div id="activity-feed" style="flex:1;"></div>
|
||||||
@@ -615,6 +631,8 @@
|
|||||||
feed.scrollTop = 0;
|
feed.scrollTop = 0;
|
||||||
} else if (tabId === 'task-tab') {
|
} else if (tabId === 'task-tab') {
|
||||||
loadTasks();
|
loadTasks();
|
||||||
|
} else if (tabId === 'preferences-tab') {
|
||||||
|
loadPreferences();
|
||||||
} else if (tabId === 'sticky-tab') {
|
} else if (tabId === 'sticky-tab') {
|
||||||
loadStickyNotes();
|
loadStickyNotes();
|
||||||
} else {
|
} else {
|
||||||
@@ -1029,6 +1047,33 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadPreferences() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/preferences');
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const container = document.getElementById('preferences-container');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
container.innerHTML = '<div style="color:var(--text-secondary); text-align:center; padding:20px;">No global preferences found.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, pref] of Object.entries(data)) {
|
||||||
|
const date = new Date(pref.updated_at * 1000).toLocaleString();
|
||||||
|
container.innerHTML += `
|
||||||
|
<div style="background:var(--card-bg); border:1px solid var(--border-color); border-radius:6px; padding:15px; box-shadow:0 1px 3px rgba(0,0,0,0.1);">
|
||||||
|
<strong style="color:#3498db; display:block; margin-bottom:5px; font-size:1.1em;">${key}</strong>
|
||||||
|
<div style="margin-bottom:10px;">${pref.value}</div>
|
||||||
|
<div style="font-size:0.8em; color:var(--text-secondary); text-align:right;">Last Updated: ${date}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading preferences:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStickyNotes() {
|
async function loadStickyNotes() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/sticky');
|
const res = await fetch('/api/sticky');
|
||||||
@@ -1078,7 +1123,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadAllExtras() {
|
function loadAllExtras() {
|
||||||
|
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>
|
||||||
|
`);
|
||||||
|
|
||||||
loadGenericList('/api/tech_debts', 'techdebt-container', item => `
|
loadGenericList('/api/tech_debts', 'techdebt-container', item => `
|
||||||
<strong>${item.id}</strong> ${item.is_resolved ? '<span style="color:var(--success-color);">(Resolved)</span>' : '<span style="color:var(--error-color);">(Open)</span>'}
|
<strong>${item.id}</strong> ${item.is_resolved ? '<span style="color:var(--success-color);">(Resolved)</span>' : '<span style="color:var(--error-color);">(Open)</span>'}
|
||||||
@@ -1091,9 +1139,9 @@
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
loadGenericList('/api/error_fixes', 'errorfixes-container', item => `
|
loadGenericList('/api/error_fixes', 'errorfixes-container', item => `
|
||||||
<strong>${item.error_message || 'Error'}</strong>
|
<strong>${item.signature || 'Error'}</strong>
|
||||||
<div style="font-size:0.9em; margin-bottom:8px;"><em>Fix:</em> ${item.fix_description || ''}</div>
|
<div style="font-size:0.9em; margin-bottom:8px;"><em>Solution:</em> ${item.solution || ''}</div>
|
||||||
<div style="font-family:monospace; font-size:0.85em; color:#e74c3c;">Files: ${(item.files_changed || []).join(', ')}</div>
|
<div style="font-family:monospace; font-size:0.85em; color:#e74c3c;">Commit: ${item.git_commit || 'None'}</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
loadGenericList('/api/adrs', 'adrs-container', item => `
|
loadGenericList('/api/adrs', 'adrs-container', item => `
|
||||||
@@ -1169,4 +1217,3 @@
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#[allow(deprecated)]
|
||||||
|
use fastembed::{TextEmbedding, EmbeddingModel};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
static EMBEDDING_MODEL: OnceLock<Mutex<TextEmbedding>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
pub fn get_embedding_model() -> Result<&'static Mutex<TextEmbedding>, String> {
|
||||||
|
if let Some(model) = EMBEDDING_MODEL.get() {
|
||||||
|
return Ok(model);
|
||||||
|
}
|
||||||
|
let options = fastembed::InitOptions::new(EmbeddingModel::AllMiniLML6V2)
|
||||||
|
.with_show_download_progress(true);
|
||||||
|
|
||||||
|
let model = TextEmbedding::try_new(options).map_err(|e| e.to_string())?;
|
||||||
|
let _ = EMBEDDING_MODEL.set(Mutex::new(model));
|
||||||
|
Ok(EMBEDDING_MODEL.get().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_embedding_async(text: String) -> Result<Vec<f32>, String> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let model_mutex = get_embedding_model()?;
|
||||||
|
let mut model = model_mutex.lock().map_err(|e| e.to_string())?;
|
||||||
|
let embeddings = model.embed(vec![text], None).map_err(|e| e.to_string())?;
|
||||||
|
Ok(embeddings.into_iter().next().unwrap_or_default())
|
||||||
|
}).await.map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
||||||
|
if a.is_empty() || b.is_empty() || a.len() != b.len() {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
|
||||||
|
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||||
|
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||||
|
if norm_a == 0.0 || norm_b == 0.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
dot_product / (norm_a * norm_b)
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-40
@@ -138,6 +138,8 @@ impl McpTool for LogErrorFixHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
|
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
|
||||||
|
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
|
||||||
state.error_fixes.modify(|fixes| {
|
state.error_fixes.modify(|fixes| {
|
||||||
fixes.push(crate::models::ErrorFix {
|
fixes.push(crate::models::ErrorFix {
|
||||||
signature: req.signature,
|
signature: req.signature,
|
||||||
@@ -145,6 +147,7 @@ impl McpTool for LogErrorFixHandler {
|
|||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
|
embedding,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Error fix logged".to_string())
|
Ok("Error fix logged".to_string())
|
||||||
@@ -294,6 +297,8 @@ impl McpTool for LogTechDebtHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
|
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", req.description, req.ideal_solution);
|
||||||
|
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
|
||||||
state.tech_debts.modify(|debts| {
|
state.tech_debts.modify(|debts| {
|
||||||
debts.push(crate::models::TechDebt {
|
debts.push(crate::models::TechDebt {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
@@ -304,6 +309,7 @@ impl McpTool for LogTechDebtHandler {
|
|||||||
created_at: crate::handlers::utils::now_secs(),
|
created_at: crate::handlers::utils::now_secs(),
|
||||||
git_commit: req.git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: req.git_branch,
|
||||||
|
embedding,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Tech debt logged".to_string())
|
Ok("Tech debt logged".to_string())
|
||||||
@@ -404,6 +410,8 @@ impl McpTool for OmniSearchHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
println!("OMNI SEARCH MATCHES: {:?}", matches);
|
println!("OMNI SEARCH MATCHES: {:?}", matches);
|
||||||
|
let q = req.query.clone();
|
||||||
|
let query_emb = crate::embedding::generate_embedding_async(q.clone()).await.unwrap_or_default();
|
||||||
|
|
||||||
let kg_json = state.read_graph(|full| {
|
let kg_json = state.read_graph(|full| {
|
||||||
let mut kg_entities = std::collections::HashMap::new();
|
let mut kg_entities = std::collections::HashMap::new();
|
||||||
@@ -466,20 +474,27 @@ impl McpTool for OmniSearchHandler {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let snippets_json = state.snippets.read_with(|all_snippets| {
|
let snippets_json = state.snippets.read_with(|all_snippets| {
|
||||||
let filtered: Vec<_> = all_snippets
|
let mut scored: Vec<_> = all_snippets.iter().map(|s| {
|
||||||
.iter()
|
let mut score = 0.0;
|
||||||
.filter(|s| matched_snippets.contains(s.name.as_str()))
|
if matched_snippets.contains(s.name.as_str()) || contains_ignore_ascii_case(&s.name, &q) || contains_ignore_ascii_case(&s.description, &q) {
|
||||||
.take(limit)
|
score += 1.0;
|
||||||
.map(|s| {
|
}
|
||||||
if !include_body {
|
if let Some(emb) = &s.embedding {
|
||||||
let mut summary = s.clone();
|
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||||
summary.code = "".to_string();
|
}
|
||||||
summary
|
(s, score)
|
||||||
} else {
|
}).filter(|(_, score)| *score > 0.4).collect();
|
||||||
s.clone()
|
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
}
|
|
||||||
})
|
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(s, _)| {
|
||||||
.collect();
|
if !include_body {
|
||||||
|
let mut summary = s.clone();
|
||||||
|
summary.code = "".to_string();
|
||||||
|
summary
|
||||||
|
} else {
|
||||||
|
s.clone()
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -505,25 +520,31 @@ impl McpTool for OmniSearchHandler {
|
|||||||
|
|
||||||
let q = req.query;
|
let q = req.query;
|
||||||
let tech_debts_json = state.tech_debts.read_with(|debts| {
|
let tech_debts_json = state.tech_debts.read_with(|debts| {
|
||||||
let filtered: Vec<_> = debts
|
let mut scored: Vec<_> = debts.iter().map(|d| {
|
||||||
.iter()
|
if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) {
|
||||||
.filter(|d| {
|
return (d, 0.0);
|
||||||
req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns)
|
}
|
||||||
&& (contains_ignore_ascii_case(&d.description, &q)
|
let mut score = 0.0;
|
||||||
|| contains_ignore_ascii_case(&d.ideal_solution, &q))
|
if contains_ignore_ascii_case(&d.description, &q) || contains_ignore_ascii_case(&d.ideal_solution, &q) {
|
||||||
})
|
score += 1.0;
|
||||||
.take(limit)
|
}
|
||||||
.map(|d| {
|
if let Some(emb) = &d.embedding {
|
||||||
if !include_body {
|
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||||
let mut summary = d.clone();
|
}
|
||||||
summary.description = "".to_string();
|
(d, score)
|
||||||
summary.ideal_solution = "".to_string();
|
}).filter(|(_, score)| *score > 0.4).collect();
|
||||||
summary
|
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
} else {
|
|
||||||
d.clone()
|
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(d, _)| {
|
||||||
}
|
if !include_body {
|
||||||
})
|
let mut summary = d.clone();
|
||||||
.collect();
|
summary.description = "".to_string();
|
||||||
|
summary.ideal_solution = "".to_string();
|
||||||
|
summary
|
||||||
|
} else {
|
||||||
|
d.clone()
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -549,13 +570,19 @@ impl McpTool for OmniSearchHandler {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let error_fixes_json = state.error_fixes.read_with(|fixes| {
|
let error_fixes_json = state.error_fixes.read_with(|fixes| {
|
||||||
let filtered: Vec<_> = fixes
|
let mut scored: Vec<_> = fixes.iter().map(|f| {
|
||||||
.iter()
|
let mut score = 0.0;
|
||||||
.filter(|f| {
|
if contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) {
|
||||||
contains_ignore_ascii_case(&f.signature, &q)
|
score += 1.0;
|
||||||
|| contains_ignore_ascii_case(&f.solution, &q)
|
}
|
||||||
})
|
if let Some(emb) = &f.embedding {
|
||||||
.collect();
|
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||||
|
}
|
||||||
|
(f, score)
|
||||||
|
}).filter(|(_, score)| *score > 0.4).collect();
|
||||||
|
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
|
||||||
|
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(f, _)| f.clone()).collect();
|
||||||
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
serde_json::to_value(&filtered).map_err(|e| e.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -800,6 +827,7 @@ mod tests {
|
|||||||
updated_at: 0,
|
updated_at: 0,
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
expires_at: None,
|
||||||
dependencies: vec![],
|
dependencies: vec![],
|
||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ impl McpTool for AddStickyNoteHandler {
|
|||||||
notes.push(StickyNote {
|
notes.push(StickyNote {
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
content: req.content,
|
content: req.content,
|
||||||
|
expires_at: None,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Ok("Sticky note added.".to_string())
|
Ok("Sticky note added.".to_string())
|
||||||
@@ -133,6 +134,7 @@ impl McpTool for LeaveHandoffMemoHandler {
|
|||||||
content: req.content,
|
content: req.content,
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
|
expires_at: None,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Handoff memo left".to_string())
|
Ok("Handoff memo left".to_string())
|
||||||
@@ -220,6 +222,7 @@ impl McpTool for AddSessionSummaryHandler {
|
|||||||
summary: req.summary,
|
summary: req.summary,
|
||||||
namespace: req.namespace,
|
namespace: req.namespace,
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
|
expires_at: None,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok("Session summary added".to_string())
|
Ok("Session summary added".to_string())
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ impl McpTool for AddTaskHandler {
|
|||||||
parent_id: req.parent_id,
|
parent_id: req.parent_id,
|
||||||
dependencies: deps,
|
dependencies: deps,
|
||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
|
expires_at: None,
|
||||||
};
|
};
|
||||||
let idx = state.get_search_index();
|
let idx = state.get_search_index();
|
||||||
drop(idx.index_task(&task));
|
drop(idx.index_task(&task));
|
||||||
|
|||||||
@@ -110,12 +110,15 @@ impl McpTool for StoreSnippetHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
let req: StoreSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: StoreSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let req_name = req.name.clone(); // Keep for the OK message and retain closure
|
let req_name = req.name.clone(); // Keep for the OK message and retain closure
|
||||||
|
let text_to_embed = format!("Name: {}\nLanguage: {}\nDescription: {}\nCode: {}", req.name, req.language, req.description, req.code);
|
||||||
|
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
|
||||||
let snippet = Snippet {
|
let snippet = Snippet {
|
||||||
name: req.name,
|
name: req.name,
|
||||||
language: req.language,
|
language: req.language,
|
||||||
code: req.code,
|
code: req.code,
|
||||||
description: req.description,
|
description: req.description,
|
||||||
updated_at: crate::handlers::utils::now_secs(),
|
updated_at: crate::handlers::utils::now_secs(),
|
||||||
|
embedding,
|
||||||
};
|
};
|
||||||
|
|
||||||
let idx = state.get_search_index();
|
let idx = state.get_search_index();
|
||||||
|
|||||||
@@ -3,21 +3,22 @@
|
|||||||
You are Antigravity, connected to the mcp-memory persistence layer. This server provides a persistent knowledge graph, task management, and environment state tracking.
|
You are Antigravity, connected to the mcp-memory persistence layer. This server provides a persistent knowledge graph, task management, and environment state tracking.
|
||||||
|
|
||||||
## Core Directives
|
## Core Directives
|
||||||
1. **Always Log Code Changes**: Before completing any coding task or pull request, you MUST call log_code_change with your git branch and git hash to log what you modified.
|
1. **Always Log Code Changes**: Before completing any coding task or pull request, you MUST invoke the `MemoryLibrarian` subagent to execute `log_code_change`. The server uses `git2` to automatically detect your branch and hash.
|
||||||
2. **Always Log Error Fixes**: If you spend more than one turn fixing an error or bug, call log_error_fix with the stack trace and the exact solution you discovered.
|
2. **Always Log Error Fixes**: If you spend more than one turn fixing an error or bug, call log_error_fix with the stack trace and the exact solution you discovered.
|
||||||
3. **Omni Search**: When starting a session or looking for context, use omni_search to query the knowledge graph, tasks, snippets, ADRs, and tech debt.
|
3. **Omni Search (Hybrid Vector)**: When starting a session or looking for context, use omni_search. It uses Vector Embeddings for semantic matching, so you can query conceptually (e.g., "how does auth work") without needing exact keyword matches. It searches the knowledge graph, tasks, snippets, ADRs, and tech debt.
|
||||||
4. **Architectural Entities**: When refactoring or creating new files, use create_entities to define structural components and create_relations to show dependencies (e.g. depends_on, implements).
|
4. **Architectural Entities**: When refactoring or creating new files, delegate to the `MemoryLibrarian` to define structural components via `create_entities` and `create_relations`.
|
||||||
5. **Tech Debt**: Do NOT ignore tech debt. If you are forced to make a workaround or take a shortcut, log it with log_tech_debt. When you fix it later, use resolve_tech_debt.
|
5. **Tech Debt**: Do NOT ignore tech debt. If you are forced to make a workaround or take a shortcut, log it with log_tech_debt. When you fix it later, use resolve_tech_debt.
|
||||||
6. **Decisions**: Use log_decision when you make an architectural choice (e.g. choosing a specific library or pattern).
|
6. **Decisions**: Use log_decision when you make an architectural choice (e.g. choosing a specific library or pattern).
|
||||||
7. **Workspaces & Context**: When pausing work or shifting focus, use `save_context_workspace` to snapshot your active tasks and pinned files. When resuming, use `list_context_workspaces` and `load_context_workspace`. Keep your working files pinned (`pin_file`)!
|
7. **Workspaces & Context**: When pausing work or shifting focus, use `save_context_workspace` to snapshot your active tasks and pinned files. When resuming, use `list_context_workspaces` and `load_context_workspace`. Keep your working files pinned (`pin_file`)!
|
||||||
8. **Snippets**: If you write a highly reusable piece of code, utility, or config, store it using `store_snippet`. Before writing boilerplate, try `search_snippets`. If outdated, use `delete_snippet`.
|
8. **Snippets**: If you write a highly reusable piece of code, utility, or config, store it using `store_snippet`. Before writing boilerplate, try `search_snippets`. If outdated, use `delete_snippet`.
|
||||||
9. **PR Checklists**: Aggressively use `add_pr_checklist_item` to build up a list of manual verification steps. Once the PR is merged, use `clear_pr_checklist`.
|
9. **PR Checklists**: Aggressively use `add_pr_checklist_item` to build up a list of manual verification steps. Once the PR is merged, use `clear_pr_checklist`.
|
||||||
10. **Tasks & Milestones**: Always track the user's larger goals! Use `add_task` to break down work, `update_task_status` as you progress, and `set_acceptance_criteria` to know when you are done. Use `list_active_tasks` to check what's next.
|
10. **Tasks & Milestones**: Always track the user's larger goals! Invoke the `ScrumMaster` subagent to manage the board (`add_task`, `update_task_status`, `set_acceptance_criteria`). Use `list_active_tasks` to check what's next.
|
||||||
11. **Handoff Memos**: If you need to stop your session or hand off work to a subagent, use `leave_handoff_memo`. When starting, use `read_handoff_memos` and `clear_handoff_memos` once read.
|
11. **Handoff Memos**: If you need to stop your session or hand off work to a subagent, use `leave_handoff_memo`. When starting, use `read_handoff_memos` and `clear_handoff_memos` once read.
|
||||||
12. **Preferences**: If the user tells you how they like things done (e.g., "always use fastify", "never use sed"), use `learn_preference`.
|
12. **Preferences**: If the user tells you how they like things done (e.g., "always use fastify", "never use sed"), use `learn_preference`.
|
||||||
13. **Sticky Notes**: Use `add_sticky_note` for ephemeral, temporary scratchpad info (like IP addresses, temporary URLs, or pending command outputs).
|
13. **Sticky Notes**: Use `add_sticky_note` for ephemeral, temporary scratchpad info (like IP addresses, temporary URLs, or pending command outputs).
|
||||||
|
|
||||||
Be aggressive about logging state changes in the background! Use the MemoryLibrarian subagent to do this off-thread if needed.
|
Be aggressive about logging state changes in the background! You MUST delegate this heavy lifting to the `MemoryLibrarian`, `ScrumMaster`, and `DevOpsSRE` subagents in the background.
|
||||||
|
|
||||||
## Tool Schema Discovery
|
## Tool Schema Discovery
|
||||||
Do **NOT** grep or search the Rust source code to find tool schemas or arguments. All lazy-loaded MCP tool schemas are automatically cached as JSON files on your disk. To understand a tool`s arguments, directly read `~/.gemini/antigravity-cli/mcp/mcp-memory/<tool_name>.json`. Do not waste tokens inspecting the Rust server code for schemas.
|
Do **NOT** grep or search the Rust source code to find tool schemas or arguments. All lazy-loaded MCP tool schemas are automatically cached as JSON files on your disk. To understand a tool`s arguments, directly read `~/.gemini/antigravity-cli/mcp/mcp-memory/<tool_name>.json`. Do not waste tokens inspecting the Rust server code for schemas.
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ mod mcp;
|
|||||||
mod models;
|
mod models;
|
||||||
mod router;
|
mod router;
|
||||||
mod search;
|
mod search;
|
||||||
|
pub mod embedding;
|
||||||
mod state;
|
mod state;
|
||||||
mod store;
|
mod store;
|
||||||
mod tools;
|
mod tools;
|
||||||
@@ -89,6 +90,28 @@ pub struct AppState {
|
|||||||
next_id: AtomicUsize,
|
next_id: AtomicUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(Duration::from_secs(3600)).await;
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
state.tasks.modify(|tasks| {
|
||||||
|
tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now));
|
||||||
|
});
|
||||||
|
state.sticky.modify(|notes| {
|
||||||
|
notes.retain(|n| n.expires_at.is_none_or(|exp| exp > now));
|
||||||
|
});
|
||||||
|
state.handoff_memos.modify(|memos| {
|
||||||
|
memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now));
|
||||||
|
});
|
||||||
|
state.session_summaries.modify(|summaries| {
|
||||||
|
summaries.retain(|s| s.expires_at.is_none_or(|exp| exp > now));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
async fn index_committer_worker(state: Arc<MemoryState>) {
|
async fn index_committer_worker(state: Arc<MemoryState>) {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
@@ -103,6 +126,7 @@ async fn index_committer_worker(state: Arc<MemoryState>) {
|
|||||||
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
state.rebuild_index().await;
|
state.rebuild_index().await;
|
||||||
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
||||||
|
tokio::spawn(ttl_sweeper_worker(Arc::clone(&state)));
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
handler: Arc::new(MemoryHandler::new(Arc::clone(&state))),
|
handler: Arc::new(MemoryHandler::new(Arc::clone(&state))),
|
||||||
clients: RwLock::new(HashMap::new()),
|
clients: RwLock::new(HashMap::new()),
|
||||||
@@ -132,6 +156,102 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// UDP Telemetry Listener on port 3001
|
||||||
|
let udp_state = Arc::clone(&app_state);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3001").await {
|
||||||
|
let mut buf = [0; 4096];
|
||||||
|
loop {
|
||||||
|
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
|
||||||
|
&& let Ok(payload) = serde_json::from_slice::<crate::models::TerminalHistory>(&buf[..len])
|
||||||
|
{
|
||||||
|
udp_state.handler.state.terminal_history.modify(|history| {
|
||||||
|
history.push_front(payload.clone());
|
||||||
|
if history.len() > 100 {
|
||||||
|
history.pop_back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ws_msg = serde_json::json!({
|
||||||
|
"type": "terminal_telemetry",
|
||||||
|
"data": payload
|
||||||
|
});
|
||||||
|
let msg_str = ws_msg.to_string();
|
||||||
|
|
||||||
|
let senders: Vec<_> = udp_state
|
||||||
|
.clients
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
for tx in senders {
|
||||||
|
let _ = tx.try_send(msg_str.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// UDP Neovim Telemetry Listener on port 3002
|
||||||
|
let nvim_udp_state = Arc::clone(&app_state);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3002").await {
|
||||||
|
let mut buf = [0; 4096];
|
||||||
|
loop {
|
||||||
|
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
|
||||||
|
&& let Ok(payload) = serde_json::from_slice::<crate::api::telemetry::NvimTelemetry>(&buf[..len])
|
||||||
|
{
|
||||||
|
// 1. Legacy disk write for active_nvim.txt
|
||||||
|
if payload.event == "FocusGained" || payload.event == "BufEnter" || payload.event == "VimEnter" {
|
||||||
|
let session = &payload.session_id;
|
||||||
|
let is_unix_socket = session.starts_with('/') || session.starts_with('~');
|
||||||
|
if is_unix_socket {
|
||||||
|
let wsl_path = "\\\\wsl.localhost\\Ubuntu\\home\\riz\\.gemini\\active_nvim.txt";
|
||||||
|
let _ = tokio::fs::write(wsl_path, session).await;
|
||||||
|
} else {
|
||||||
|
let profile = std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
|
||||||
|
let win_path = format!("{}\\.gemini\\active_nvim.txt", profile);
|
||||||
|
let _ = tokio::fs::write(&win_path, session).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Broadcast to UI
|
||||||
|
let ws_msg = serde_json::json!({
|
||||||
|
"type": "nvim_telemetry",
|
||||||
|
"data": payload
|
||||||
|
});
|
||||||
|
let msg_str = ws_msg.to_string();
|
||||||
|
|
||||||
|
let senders: Vec<_> = nvim_udp_state
|
||||||
|
.clients
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
for tx in senders {
|
||||||
|
let _ = tx.try_send(msg_str.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Event bus trigger for auto-save hook
|
||||||
|
if payload.event == "BufWritePost"
|
||||||
|
&& let Some(ref file_path) = payload.file
|
||||||
|
{
|
||||||
|
let normalized_file = file_path.replace("\\", "/");
|
||||||
|
let topic = format!("nvim:save:{}", normalized_file);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let app = api::setup::create_router(app_state);
|
let app = api::setup::create_router(app_state);
|
||||||
|
|
||||||
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/ws");
|
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/ws");
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ pub struct CodeChange {
|
|||||||
pub struct StickyNote {
|
pub struct StickyNote {
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub expires_at: Option<u64>,
|
||||||
}
|
}
|
||||||
pub fn default_namespace() -> String {
|
pub fn default_namespace() -> String {
|
||||||
"global".to_string()
|
"global".to_string()
|
||||||
@@ -63,6 +65,8 @@ pub struct Task {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub expires_at: Option<u64>,
|
||||||
|
#[serde(default)]
|
||||||
pub dependencies: Vec<String>,
|
pub dependencies: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub acceptance_criteria: Vec<AcceptanceCriteria>,
|
pub acceptance_criteria: Vec<AcceptanceCriteria>,
|
||||||
@@ -74,6 +78,8 @@ pub struct Snippet {
|
|||||||
pub code: String,
|
pub code: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub updated_at: u64,
|
pub updated_at: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub embedding: Option<Vec<f32>>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct Adr {
|
pub struct Adr {
|
||||||
@@ -105,6 +111,8 @@ pub struct ErrorFix {
|
|||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
pub git_commit: Option<String>,
|
pub git_commit: Option<String>,
|
||||||
pub git_branch: Option<String>,
|
pub git_branch: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub embedding: Option<Vec<f32>>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct PinnedFile {
|
pub struct PinnedFile {
|
||||||
@@ -118,12 +126,16 @@ pub struct SessionSummary {
|
|||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub expires_at: Option<u64>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct HandoffMemo {
|
pub struct HandoffMemo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub expires_at: Option<u64>,
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
}
|
}
|
||||||
@@ -175,6 +187,8 @@ pub struct TechDebt {
|
|||||||
pub created_at: u64,
|
pub created_at: u64,
|
||||||
pub git_commit: Option<String>,
|
pub git_commit: Option<String>,
|
||||||
pub git_branch: Option<String>,
|
pub git_branch: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub embedding: Option<Vec<f32>>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct ContextWorkspace {
|
pub struct ContextWorkspace {
|
||||||
@@ -196,3 +210,11 @@ pub struct GateRecord {
|
|||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct TerminalHistory {
|
||||||
|
pub command: String,
|
||||||
|
pub exit_code: i32,
|
||||||
|
|
||||||
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub fn patch() {}
|
||||||
@@ -178,6 +178,28 @@ impl MemoryHandler {
|
|||||||
register_resource!(TasksActiveResource);
|
register_resource!(TasksActiveResource);
|
||||||
|
|
||||||
register_prompt!(AnalyzeTechDebtPrompt);
|
register_prompt!(AnalyzeTechDebtPrompt);
|
||||||
|
struct TerminalHistoryResource;
|
||||||
|
#[async_trait]
|
||||||
|
impl McpResource for TerminalHistoryResource {
|
||||||
|
fn uri(&self) -> &'static str {
|
||||||
|
"memory://terminal/recent"
|
||||||
|
}
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Terminal History"
|
||||||
|
}
|
||||||
|
fn description(&self) -> Option<&'static str> {
|
||||||
|
Some("Recent terminal execution history and exit codes")
|
||||||
|
}
|
||||||
|
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> {
|
||||||
|
let state_clone = Arc::clone(&state);
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let items = state_clone.terminal_history.cache.read().unwrap();
|
||||||
|
serde_json::to_string_pretty(&*items).map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
struct PinnedFilesResource;
|
struct PinnedFilesResource;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl McpResource for PinnedFilesResource {
|
impl McpResource for PinnedFilesResource {
|
||||||
@@ -252,9 +274,38 @@ impl MemoryHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register_resource!(TerminalHistoryResource);
|
||||||
register_resource!(PinnedFilesResource);
|
register_resource!(PinnedFilesResource);
|
||||||
register_resource!(MilestonesResource);
|
register_resource!(MilestonesResource);
|
||||||
|
struct ArchiveRoutinePrompt;
|
||||||
|
#[async_trait]
|
||||||
|
impl McpPrompt for ArchiveRoutinePrompt {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"archive_routine"
|
||||||
|
}
|
||||||
|
fn description(&self) -> Option<&'static str> {
|
||||||
|
Some("Compress old session summaries into a milestone retrospective")
|
||||||
|
}
|
||||||
|
async fn get(
|
||||||
|
&self,
|
||||||
|
_args: Value,
|
||||||
|
_state: Arc<MemoryState>,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
Ok(serde_json::json!({"
|
||||||
|
messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": {
|
||||||
|
"type": "text",
|
||||||
|
"text": "Please read the old session summaries, synthesize them into a dense Milestone Retrospective entity, and then use the appropriate tools to delete the old session summaries."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
register_prompt!(HandoffRoutinePrompt);
|
register_prompt!(HandoffRoutinePrompt);
|
||||||
|
register_prompt!(ArchiveRoutinePrompt);
|
||||||
|
|
||||||
macro_rules! register {
|
macro_rules! register {
|
||||||
($module:ident::$handler:ident) => {
|
($module:ident::$handler:ident) => {
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ mod tests {
|
|||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
dependencies: vec![],
|
dependencies: vec![],
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
expires_at: None,
|
||||||
};
|
};
|
||||||
let _ = index.index_task(&task).await.unwrap();
|
let _ = index.index_task(&task).await.unwrap();
|
||||||
|
|
||||||
@@ -344,6 +345,7 @@ mod tests {
|
|||||||
language: "rust".to_string(),
|
language: "rust".to_string(),
|
||||||
description: "A test snippet".to_string(),
|
description: "A test snippet".to_string(),
|
||||||
updated_at: 0,
|
updated_at: 0,
|
||||||
|
embedding: None,
|
||||||
};
|
};
|
||||||
let _ = index.index_snippet(&snippet).await.unwrap();
|
let _ = index.index_snippet(&snippet).await.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ pub struct MemoryState {
|
|||||||
pub gates: Store<Vec<GateRecord>>,
|
pub gates: Store<Vec<GateRecord>>,
|
||||||
pub context_workspaces: Store<Vec<ContextWorkspace>>,
|
pub context_workspaces: Store<Vec<ContextWorkspace>>,
|
||||||
pub recent_activities: Store<std::collections::VecDeque<serde_json::Value>>,
|
pub recent_activities: Store<std::collections::VecDeque<serde_json::Value>>,
|
||||||
|
pub terminal_history: Store<std::collections::VecDeque<TerminalHistory>>,
|
||||||
pub activity_tx: tokio::sync::broadcast::Sender<String>,
|
pub activity_tx: tokio::sync::broadcast::Sender<String>,
|
||||||
pub event_bus_tx: tokio::sync::broadcast::Sender<GenericEvent>,
|
pub event_bus_tx: tokio::sync::broadcast::Sender<GenericEvent>,
|
||||||
}
|
}
|
||||||
@@ -79,6 +80,7 @@ impl MemoryState {
|
|||||||
gates: Store::new("gates", db.clone()),
|
gates: Store::new("gates", db.clone()),
|
||||||
context_workspaces: Store::new("context_workspaces", db.clone()),
|
context_workspaces: Store::new("context_workspaces", db.clone()),
|
||||||
recent_activities: Store::new("recent_activities", db.clone()),
|
recent_activities: Store::new("recent_activities", db.clone()),
|
||||||
|
terminal_history: Store::new("terminal_history", db.clone()),
|
||||||
activity_tx: tokio::sync::broadcast::channel(100).0,
|
activity_tx: tokio::sync::broadcast::channel(100).0,
|
||||||
event_bus_tx: tokio::sync::broadcast::channel(1000).0,
|
event_bus_tx: tokio::sync::broadcast::channel(1000).0,
|
||||||
}
|
}
|
||||||
@@ -200,6 +202,7 @@ mod tests {
|
|||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
expires_at: None,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user