multica/server/internal/util/mention.go
Bohan Jiang f353e8db59
feat(mentions): support @mentioning issues + server-side auto-expansion (#242)
* feat(mentions): support @mentioning issues in comments

- Extend MentionItem type to include "issue" alongside "member"/"agent"
- Add issue search (by identifier and title) to mention suggestion dropdown
- Render issue mentions with CircleDot icon in autocomplete popup
- Issue mentions serialize as [MUL-117 Title](mention://issue/id) (no @ prefix)
- Markdown renderer shows issue mentions as clickable links to /issues/:id
- Backend mentionRe regex updated to match issue mention type

* feat(mentions): auto-expand issue identifiers and add mention format to agent instructions

1. Path A — CLAUDE.md template (runtime_config.go):
   Add a "## Mentions" section teaching agents the mention serialization
   format for issues, members, and agents. All agents automatically
   receive this via the auto-generated CLAUDE.md.

2. Approach 2 — Server-side auto-conversion (internal/mention/):
   New ExpandIssueIdentifiers() utility that scans comment content for
   bare issue identifiers (e.g. MUL-117) and replaces them with
   [MUL-117](mention://issue/<uuid>) mention links. Skips code blocks,
   inline code, and existing markdown links. Integrated into both:
   - handler.CreateComment (HTTP API path)
   - service.createAgentComment (agent task output path)
2026-04-02 13:48:53 +08:00

44 lines
1.3 KiB
Go

package util
import "regexp"
// Mention represents a parsed @mention from markdown content.
type Mention struct {
Type string // "member", "agent", "issue", or "all"
ID string // user_id, agent_id, issue_id, or "all"
}
// MentionRe matches [@Label](mention://type/id) or [Label](mention://issue/id) in markdown.
// The @ prefix is optional to support issue mentions which use [MUL-123](mention://issue/...).
var MentionRe = regexp.MustCompile(`\[@?[^\]]*\]\(mention://(member|agent|issue|all)/([0-9a-fA-F-]+|all)\)`)
// IsMentionAll returns true if the mention is an @all mention.
func (m Mention) IsMentionAll() bool {
return m.Type == "all"
}
// ParseMentions extracts deduplicated mentions from markdown content.
func ParseMentions(content string) []Mention {
matches := MentionRe.FindAllStringSubmatch(content, -1)
seen := make(map[string]bool)
var result []Mention
for _, m := range matches {
key := m[1] + ":" + m[2]
if seen[key] {
continue
}
seen[key] = true
result = append(result, Mention{Type: m[1], ID: m[2]})
}
return result
}
// HasMentionAll returns true if any mention in the slice is an @all mention.
func HasMentionAll(mentions []Mention) bool {
for _, m := range mentions {
if m.IsMentionAll() {
return true
}
}
return false
}