Repository navigation
feat: implement @mention feature (@I/@A/@search inline refs) - #262
Conversation
Implements the design from item #116, applying the changes-requested fixes from its review (#115) that opencode never came back to: quoted syntax for multi-word @search queries, and hooking in after the s.active gate instead of before it. Item-get/asset-get scoping is left as-is (unscoped by id), matching how the existing item/asset MCP tools already behave.
📝 WalkthroughWalkthroughAdds inline ChangesInline mention expansion
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant prompt_submit
participant mentions_expand
participant backend_DB
participant format_context
prompt_submit->>mentions_expand: expand(prompt)
mentions_expand->>backend_DB: resolve linked project and mentions
backend_DB-->>mentions_expand: resolved mention data
mentions_expand->>format_context: format_context(resolved)
format_context-->>mentions_expand: formatted context
mentions_expand-->>prompt_submit: append additionalContext
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/hook.rs (1)
738-783: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest bypasses
prompt_submit's prompt handling, so it can't catch the lowercasing issue above.This test calls
crate::mentions::expand(...)directly with a hand-built, already-correct-case string, rather than going throughprompt_submit()(which is where the real prompt — postextract_promptlowercasing — flows). It validatesexpand()'s DB/project-resolution wiring, but provides no coverage for the actual hook integration point at Lines 376-378, including the case-sensitivity interaction flagged there.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hook.rs` around lines 738 - 783, The test prompt_submit_expands_mentions_for_the_resolved_project currently bypasses prompt_submit and does not cover prompt extraction or lowercasing. Update it to invoke prompt_submit with a mention in the user-facing prompt, then assert the submitted/expanded result contains “Fix login timeout”; retain the no-mention behavior only if it can be verified through the same hook path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/mentions/resolve.rs`:
- Around line 86-108: Update resolve_asset to check the known asset.size against
MAX_ASSET_CHARS before calling read_file, returning the existing ResolvedAsset
with no content (or the established oversized-file behavior) when the limit is
exceeded. Keep the current read, UTF-8 decoding, and character truncation flow
only for assets within the permitted size.
---
Nitpick comments:
In `@src/hook.rs`:
- Around line 738-783: The test
prompt_submit_expands_mentions_for_the_resolved_project currently bypasses
prompt_submit and does not cover prompt extraction or lowercasing. Update it to
invoke prompt_submit with a mention in the user-facing prompt, then assert the
submitted/expanded result contains “Fix login timeout”; retain the no-mention
behavior only if it can be verified through the same hook path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 97fb675b-f372-409e-93da-044cc84f8b86
📒 Files selected for processing (6)
src/hook.rssrc/main.rssrc/mentions/format.rssrc/mentions/mod.rssrc/mentions/parse.rssrc/mentions/resolve.rs
| fn resolve_asset(conn: &Connection, id: &str) -> ResolvedContent { | ||
| let Ok(asset) = agentflare_backend::asset::get(conn, id) else { | ||
| return ResolvedContent::NotFound; | ||
| }; | ||
| let base_path = crate::paths::home().join(".agentflare"); | ||
| let content = agentflare_backend::asset::read_file(&base_path, &asset.storage_path) | ||
| .ok() | ||
| .and_then(|bytes| String::from_utf8(bytes).ok()) | ||
| .map(|text| { | ||
| if text.chars().count() > MAX_ASSET_CHARS { | ||
| let mut truncated: String = text.chars().take(MAX_ASSET_CHARS).collect(); | ||
| truncated.push_str("… [truncated]"); | ||
| truncated | ||
| } else { | ||
| text | ||
| } | ||
| }); | ||
| ResolvedContent::Asset(ResolvedAsset { | ||
| filename: asset.filename, | ||
| entity_id: asset.entity_id, | ||
| content, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Unbounded file read before size check in resolve_asset.
The full attachment is read into memory and UTF-8-decoded before any size limit is applied — only the resulting string is truncated to MAX_ASSET_CHARS. A large attachment (binary or text) is fully loaded into memory synchronously inside the prompt hook regardless of MAX_ASSET_CHARS, since asset.size (already known from the DB row) isn't checked first.
⚡ Proposed fix: check size before reading
+/// Skip reading attachments larger than this outright — avoids loading a
+/// huge file into memory just to truncate it down to MAX_ASSET_CHARS.
+const MAX_ASSET_READ_BYTES: i64 = 1_000_000;
+
fn resolve_asset(conn: &Connection, id: &str) -> ResolvedContent {
let Ok(asset) = agentflare_backend::asset::get(conn, id) else {
return ResolvedContent::NotFound;
};
let base_path = crate::paths::home().join(".agentflare");
- let content = agentflare_backend::asset::read_file(&base_path, &asset.storage_path)
- .ok()
- .and_then(|bytes| String::from_utf8(bytes).ok())
- .map(|text| {
- if text.chars().count() > MAX_ASSET_CHARS {
- let mut truncated: String = text.chars().take(MAX_ASSET_CHARS).collect();
- truncated.push_str("… [truncated]");
- truncated
- } else {
- text
- }
- });
+ let content = if asset.size > MAX_ASSET_READ_BYTES {
+ None
+ } else {
+ agentflare_backend::asset::read_file(&base_path, &asset.storage_path)
+ .ok()
+ .and_then(|bytes| String::from_utf8(bytes).ok())
+ .map(|text| {
+ if text.chars().count() > MAX_ASSET_CHARS {
+ let mut truncated: String = text.chars().take(MAX_ASSET_CHARS).collect();
+ truncated.push_str("… [truncated]");
+ truncated
+ } else {
+ text
+ }
+ })
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn resolve_asset(conn: &Connection, id: &str) -> ResolvedContent { | |
| let Ok(asset) = agentflare_backend::asset::get(conn, id) else { | |
| return ResolvedContent::NotFound; | |
| }; | |
| let base_path = crate::paths::home().join(".agentflare"); | |
| let content = agentflare_backend::asset::read_file(&base_path, &asset.storage_path) | |
| .ok() | |
| .and_then(|bytes| String::from_utf8(bytes).ok()) | |
| .map(|text| { | |
| if text.chars().count() > MAX_ASSET_CHARS { | |
| let mut truncated: String = text.chars().take(MAX_ASSET_CHARS).collect(); | |
| truncated.push_str("… [truncated]"); | |
| truncated | |
| } else { | |
| text | |
| } | |
| }); | |
| ResolvedContent::Asset(ResolvedAsset { | |
| filename: asset.filename, | |
| entity_id: asset.entity_id, | |
| content, | |
| }) | |
| } | |
| /// Skip reading attachments larger than this outright — avoids loading a | |
| /// huge file into memory just to truncate it down to MAX_ASSET_CHARS. | |
| const MAX_ASSET_READ_BYTES: i64 = 1_000_000; | |
| fn resolve_asset(conn: &Connection, id: &str) -> ResolvedContent { | |
| let Ok(asset) = agentflare_backend::asset::get(conn, id) else { | |
| return ResolvedContent::NotFound; | |
| }; | |
| let base_path = crate::paths::home().join(".agentflare"); | |
| let content = if asset.size > MAX_ASSET_READ_BYTES { | |
| None | |
| } else { | |
| agentflare_backend::asset::read_file(&base_path, &asset.storage_path) | |
| .ok() | |
| .and_then(|bytes| String::from_utf8(bytes).ok()) | |
| .map(|text| { | |
| if text.chars().count() > MAX_ASSET_CHARS { | |
| let mut truncated: String = text.chars().take(MAX_ASSET_CHARS).collect(); | |
| truncated.push_str("… [truncated]"); | |
| truncated | |
| } else { | |
| text | |
| } | |
| }) | |
| }; | |
| ResolvedContent::Asset(ResolvedAsset { | |
| filename: asset.filename, | |
| entity_id: asset.entity_id, | |
| content, | |
| }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mentions/resolve.rs` around lines 86 - 108, Update resolve_asset to check
the known asset.size against MAX_ASSET_CHARS before calling read_file, returning
the existing ResolvedAsset with no content (or the established oversized-file
behavior) when the limit is exceeded. Keep the current read, UTF-8 decoding, and
character truncation flow only for assets within the permitted size.
Items are project-scoped in this codebase (project_id column, list_by_project/search/resolve_id all filter on it) — @i:/@A: mention resolution was the one lookup path that didn't, letting a prompt reference an item or asset belonging to a different project. resolve_item now rejects a project_id mismatch; resolve_asset resolves the owning project through entity_type (item_attachment -> the item's project_id, project_attachment -> entity_id itself) and does the same.
|
Re: the "deliberate deviation from review" note in the PR description — verified against the backend code and it's incorrect. Pushed a fix (5c40f7b):
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/mentions/resolve.rs (1)
123-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd direct coverage for project-attached assets.
The new
project_attachmentbranch is security-sensitive, but the added tests only exerciseitem_attachment. Add in-project and cross-projectproject_attachmentcases to verify both allowed and rejected resolution paths.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mentions/resolve.rs` around lines 123 - 128, Add direct tests for the project_attachment branch in the asset resolution logic, covering an asset whose entity_id matches the requested project_id and one from a different project. Assert that the matching project_attachment resolves successfully while the cross-project attachment is rejected, alongside the existing item_attachment coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/mentions/resolve.rs`:
- Around line 123-128: Add direct tests for the project_attachment branch in the
asset resolution logic, covering an asset whose entity_id matches the requested
project_id and one from a different project. Assert that the matching
project_attachment resolves successfully while the cross-project attachment is
rejected, alongside the existing item_attachment coverage.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e71dff30-b9ae-4119-9db9-1d72c98a7b9f
📒 Files selected for processing (1)
src/mentions/resolve.rs
Summary
@mentiondesign from item chore: add scripts/discussions.sh for managing GitHub Discussions #116, which had review changes-requested (item #115) but was never followed up on — no code had landed.src/mentions/{mod,parse,resolve,format}.rs: regex-parses@I:uuid/@A:uuid/@search:"query"refs out of a prompt, resolves them against the backend DB, and formats a context block.hook.rs::prompt_submit, after thes.activegate — so it only runs when agentflare is ACTIVE, and never opens a DB connection otherwise.Review fixes applied (from #115)
@search:"bug login timeout"(unquoted still works for single-word values).s.activegate, not before it.AgentflareMcp::default().resolve_project(), mirroring the patternsession_start_message's pending-items feature already uses (the hook has no liveAgentflareMcpinstance).regexwas already a Cargo.toml dependency — no new dependency added.Deliberate deviation from the review
Did not add extra workspace/project scoping to
@I:/@A:resolution. The existingitem get/asset getMCP tools (src/mcp_server/asset.rs) resolve by bare id with no project/workspace scoping either — onlydeleted_at IS NULL, whichitem::get/asset::getalready enforce at the SQL level. Scoping just the mention path would be inconsistent with every other id-based lookup in the codebase, and isn't a reachable attack surface (single local user/trust domain). Flagging in case this judgment call should be revisited.@file:pathstayed deferred per the review's recommendation.Test plan
parse/resolve/format(regex edge cases, project-scoped search, asset truncation, not-found handling)hook.rsintegration test (prompt_submit_expands_mentions_for_the_resolved_project) exercising the fullmentions::expand()path against a resolved projectcargo fmt --checkcleancargo clippy --all-targets -- -D warnings -A unsafe_code -A clippy::pedanticclean on the new/touched files (2 unrelated pre-existing errors on master —agent_launch.rsunused import,components.rsdead_code — block a full workspace run; confirmed present on master before this change)cargo test --bin agentflare mentions::andcargo test --bin agentflare hook::both passSummary by CodeRabbit
@I:,@A:, and@search:references in prompts, including quoted multi-word searches.