mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-15 14:19:13 +02:00
* feat(editor): add / slash-command palette for invoking agent skills Adds a `/` trigger in the chat box that opens a popover listing the active agent's skills. Selecting an item inserts a `[/label](slash://skill/<id>)` token; the daemon extracts those IDs in `buildChatPrompt` and emits an "Explicitly selected skills:" block using the canonical names from the agent's skill registry — labels are display-only and never trusted. Built on Tiptap's `Mention` extension so the suggestion lifecycle, keyboard routing, and IME handling mirror the existing `@` mention UX. Item list is sourced from the React Query workspace cache (no per-keystroke fetch). Gated behind a new `enableSlashCommands` prop so only `chat-input` opts in; other `ContentEditor` consumers (issue editor, comments) are unaffected. Read-only markdown surfaces render the token as a `.slash-command` pill via a custom link renderer + sanitize-schema/url-transform allowlists. Closes #3108 * fix(i18n): add slash_command editor copy for ko/ja The PR added slash_command popover empty-state keys to en + zh-Hans only; locales/parity.test.ts requires every locale to cover every EN key, so ko and ja failed CI. Add the two keys (no_skills_configured, no_results) matching existing skill terminology (스킬 / スキル). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.0 KiB
Go
70 lines
2.0 KiB
Go
package daemon
|
|
|
|
import "testing"
|
|
|
|
func TestExtractSlashSkills(t *testing.T) {
|
|
t.Run("parses basic link", func(t *testing.T) {
|
|
refs := ExtractSlashSkills("please [/deploy](slash://skill/abc-123) this")
|
|
if len(refs) != 1 {
|
|
t.Fatalf("expected 1 ref, got %d", len(refs))
|
|
}
|
|
if refs[0].Label != "deploy" || refs[0].ID != "abc-123" {
|
|
t.Fatalf("unexpected ref: %+v", refs[0])
|
|
}
|
|
})
|
|
|
|
t.Run("parses escaped brackets", func(t *testing.T) {
|
|
refs := ExtractSlashSkills(`[/deploy\[prod\]](slash://skill/x)`)
|
|
if len(refs) != 1 || refs[0].Label != "deploy[prod]" {
|
|
t.Fatalf("unexpected refs: %+v", refs)
|
|
}
|
|
})
|
|
|
|
t.Run("deduplicates by ID", func(t *testing.T) {
|
|
refs := ExtractSlashSkills("[/a](slash://skill/same) and [/b](slash://skill/same)")
|
|
if len(refs) != 1 {
|
|
t.Fatalf("expected 1 ref after dedupe, got %d", len(refs))
|
|
}
|
|
})
|
|
|
|
t.Run("ignores slash action links", func(t *testing.T) {
|
|
refs := ExtractSlashSkills("[/x](slash://action/y)")
|
|
if len(refs) != 0 {
|
|
t.Fatalf("expected 0 refs for action link, got %d", len(refs))
|
|
}
|
|
})
|
|
|
|
t.Run("ignores normal markdown links", func(t *testing.T) {
|
|
refs := ExtractSlashSkills("[docs](https://example.com)")
|
|
if len(refs) != 0 {
|
|
t.Fatalf("expected 0 refs for normal link, got %d", len(refs))
|
|
}
|
|
})
|
|
|
|
t.Run("ignores mention links", func(t *testing.T) {
|
|
refs := ExtractSlashSkills("[@user](mention://member/id)")
|
|
if len(refs) != 0 {
|
|
t.Fatalf("expected 0 refs for mention link, got %d", len(refs))
|
|
}
|
|
})
|
|
|
|
t.Run("extracts multiple distinct skills", func(t *testing.T) {
|
|
refs := ExtractSlashSkills("[/a](slash://skill/id-1) and [/b](slash://skill/id-2)")
|
|
if len(refs) != 2 {
|
|
t.Fatalf("expected 2 refs, got %d", len(refs))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExtractSlashSkillsDoesNotMatchPartialProtocol(t *testing.T) {
|
|
for _, md := range []string{
|
|
"[/x](slash://y)",
|
|
"[/x](slash://skills/y)",
|
|
"[/x](slash://skill-extra/y)",
|
|
} {
|
|
if refs := ExtractSlashSkills(md); len(refs) != 0 {
|
|
t.Errorf("expected 0 refs for %q, got %d", md, len(refs))
|
|
}
|
|
}
|
|
}
|