Skip to content

feat: implement @mention feature (@I/@A/@search inline refs) - #262

Merged
getappz merged 4 commits into
masterfrom
feat/mention-syntax
Jul 19, 2026
Merged

getappz merged 4 commits into
masterfrom
feat/mention-syntax

Conversation

@getappz

@getappz getappz commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Implements the @mention design 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.
  • New module 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.
  • Wired into hook.rs::prompt_submit, after the s.active gate — so it only runs when agentflare is ACTIVE, and never opens a DB connection otherwise.

Review fixes applied (from #115)

  • Quoted syntax for multi-word search: @search:"bug login timeout" (unquoted still works for single-word values).
  • Correct hook placement: after the s.active gate, not before it.
  • Opens its own backend-db connection + AgentflareMcp::default().resolve_project(), mirroring the pattern session_start_message's pending-items feature already uses (the hook has no live AgentflareMcp instance).
  • regex was 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 existing item get/asset get MCP tools (src/mcp_server/asset.rs) resolve by bare id with no project/workspace scoping either — only deleted_at IS NULL, which item::get/asset::get already 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:path stayed deferred per the review's recommendation.

Test plan

  • 15 new unit tests across parse/resolve/format (regex edge cases, project-scoped search, asset truncation, not-found handling)
  • 1 new hook.rs integration test (prompt_submit_expands_mentions_for_the_resolved_project) exercising the full mentions::expand() path against a resolved project
  • cargo fmt --check clean
  • cargo clippy --all-targets -- -D warnings -A unsafe_code -A clippy::pedantic clean on the new/touched files (2 unrelated pre-existing errors on master — agent_launch.rs unused import, components.rs dead_code — block a full workspace run; confirmed present on master before this change)
  • cargo test --bin agentflare mentions:: and cargo test --bin agentflare hook:: both pass

Summary by CodeRabbit

  • New Features
    • Added support for inline @I:, @A:, and @search: references in prompts, including quoted multi-word searches.
    • Resolved items, assets, and search hits are injected into the prompt’s additional context; missing references are clearly marked as not found.
    • Asset content is included when available and truncated for long content; unreadable or missing content is noted.
    • Search results list matches (or indicate when none are found), scoped to the same project.

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.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds inline @I, @A, and @search mention parsing, project-scoped backend resolution, formatted context generation, and prompt hook integration with unit tests.

Changes

Inline mention expansion

Layer / File(s) Summary
Mention parsing contract
src/mentions/parse.rs
Parses item, asset, and search references, including quoted searches, punctuation trimming, case-insensitive prefixes, and a five-mention limit.
Project-scoped mention resolution
src/mentions/resolve.rs
Resolves items, assets, and searches against the linked project, checks asset ownership, handles missing records, truncates asset content, and tests project scoping and search results.
Resolved context formatting
src/mentions/format.rs
Formats resolved items, assets, search results, and missing mentions into context blocks.
Prompt hook integration
src/mentions/mod.rs, src/main.rs, src/hook.rs
Registers the mentions module, expands prompt mentions through the backend database, and appends formatted output to additionalContext when available.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding inline @mention support for I, A, and search refs.
Description check ✅ Passed The PR description covers the summary, test plan, and reviewer notes, with enough detail to match the template overall.
Docstring Coverage ✅ Passed Docstring coverage is 80.65% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mention-syntax

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/hook.rs (1)

738-783: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test 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 through prompt_submit() (which is where the real prompt — post extract_prompt lowercasing — flows). It validates expand()'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

📥 Commits

Reviewing files that changed from the base of the PR and between 3e2c400 and 2a869ad.

📒 Files selected for processing (6)
  • src/hook.rs
  • src/main.rs
  • src/mentions/format.rs
  • src/mentions/mod.rs
  • src/mentions/parse.rs
  • src/mentions/resolve.rs

Comment thread src/mentions/resolve.rs Outdated
Comment on lines +86 to +108
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,
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 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.

Suggested change
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.
@getappz

getappz commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Re: the "deliberate deviation from review" note in the PR description — verified against the backend code and it's incorrect. item::get/asset::get don't filter by project_id at the SQL level (only deleted_at IS NULL), but that's not the codebase's actual scoping mechanism — items are scoped to projects via project_id, enforced by list_by_project, item::search (already used by @search: in this same file), and the sequence-id path of resolve_id, each with dedicated cross-project isolation tests. resolve.rs already threads project_id through for @search:; it just wasn't applied to @I:/@A:, which was an inconsistency within this file rather than consistency with the rest of the codebase.

Pushed a fix (5c40f7b):

  • resolve_item now rejects when item.project_id != project_id (item already carries project_id, no new backend call needed).
  • resolve_asset resolves the owning project via entity_type (item_attachment -> look up that item's project_id; project_attachment -> entity_id is the project id directly) and compares.
  • Added item_mention_is_scoped_to_project and asset_mention_is_scoped_to_project tests proving cross-project references now resolve to NotFound.
  • Fixed existing asset test that was passing a bogus "unused" project id (only worked because scoping didn't exist yet).

cargo test --bin agentflare mentions:: (17/17) and hook:: (23/23) pass; fmt clean; clippy clean on touched files (workspace-wide clippy has pre-existing unrelated failures on this branch from before #260 landed, same 2 exceptions already noted in the PR description).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/mentions/resolve.rs (1)

123-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct coverage for project-attached assets.

The new project_attachment branch is security-sensitive, but the added tests only exercise item_attachment. Add in-project and cross-project project_attachment cases 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2a869ad and 5c40f7b.

📒 Files selected for processing (1)
  • src/mentions/resolve.rs

@getappz
getappz merged commit d9cdcdd into master Jul 19, 2026
16 checks passed
@getappz
getappz deleted the feat/mention-syntax branch July 19, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant