mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): expand skill mention picker (#2170)
This commit is contained in:
parent
6e1ebb97dd
commit
b838e94b88
3 changed files with 72 additions and 17 deletions
|
|
@ -1029,21 +1029,8 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
|
|||
const filteredSkills = mention
|
||||
? skills
|
||||
.filter((s) => !stagedSkillIds.has(s.id))
|
||||
.filter((s) => {
|
||||
if (!mentionQuery) return true;
|
||||
return [
|
||||
s.id,
|
||||
s.name,
|
||||
s.description,
|
||||
s.mode,
|
||||
s.surface ?? '',
|
||||
...s.triggers,
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(mentionQuery);
|
||||
})
|
||||
.slice(0, 8)
|
||||
.filter((s) => skillMatchesQuery(s, mentionQuery))
|
||||
.sort((a, b) => skillMentionRank(a, mentionQuery) - skillMentionRank(b, mentionQuery))
|
||||
: [];
|
||||
|
||||
return (
|
||||
|
|
@ -2079,6 +2066,15 @@ function skillMatchesQuery(skill: SkillSummary, query: string): boolean {
|
|||
.includes(q);
|
||||
}
|
||||
|
||||
function skillMentionRank(skill: SkillSummary, query: string): number {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return 1;
|
||||
const id = skill.id.toLowerCase();
|
||||
const name = skill.name.toLowerCase();
|
||||
if (id.startsWith(q) || name.startsWith(q)) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function mcpServerMatchesQuery(server: McpServerConfig, query: string): boolean {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
|
|
|
|||
|
|
@ -2283,9 +2283,9 @@ a.avatar-item:visited {
|
|||
/* -------- Mention popover ------------------------------------------- */
|
||||
.mention-popover {
|
||||
order: -1;
|
||||
flex: 0 0 clamp(248px, 38vh, 320px);
|
||||
flex: 0 0 clamp(300px, 52vh, 480px);
|
||||
min-height: 248px;
|
||||
max-height: 320px;
|
||||
max-height: min(480px, 72vh);
|
||||
margin: 0 0 6px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,24 @@ const SKILL = {
|
|||
aggregatesExamples: false,
|
||||
};
|
||||
|
||||
function makeSkill(overrides: Partial<typeof SKILL>): typeof SKILL {
|
||||
return {
|
||||
...SKILL,
|
||||
id: overrides.id ?? SKILL.id,
|
||||
name: overrides.name ?? SKILL.name,
|
||||
description: overrides.description ?? SKILL.description,
|
||||
triggers: overrides.triggers ?? SKILL.triggers,
|
||||
mode: overrides.mode ?? SKILL.mode,
|
||||
previewType: overrides.previewType ?? SKILL.previewType,
|
||||
designSystemRequired: overrides.designSystemRequired ?? SKILL.designSystemRequired,
|
||||
defaultFor: overrides.defaultFor ?? SKILL.defaultFor,
|
||||
upstream: overrides.upstream ?? SKILL.upstream,
|
||||
hasBody: overrides.hasBody ?? SKILL.hasBody,
|
||||
examplePrompt: overrides.examplePrompt ?? SKILL.examplePrompt,
|
||||
aggregatesExamples: overrides.aggregatesExamples ?? SKILL.aggregatesExamples,
|
||||
};
|
||||
}
|
||||
|
||||
const MCP_SERVER = {
|
||||
id: 'slack',
|
||||
label: 'Slack MCP',
|
||||
|
|
@ -209,6 +227,47 @@ describe('ChatComposer context pickers', () => {
|
|||
expect(screen.getByTestId('chat-composer-mention-overlay').textContent).toContain('@Deck Builder');
|
||||
});
|
||||
|
||||
it('shows all matching skills and ranks exact prefix matches first', async () => {
|
||||
skills = [
|
||||
makeSkill({
|
||||
id: 'story-brief',
|
||||
name: 'Story Brief',
|
||||
description: 'Use when planning audit work.',
|
||||
triggers: ['writing'],
|
||||
}),
|
||||
...Array.from({ length: 9 }, (_, index) =>
|
||||
makeSkill({
|
||||
id: `audit-helper-${index + 1}`,
|
||||
name: `Audit Helper ${index + 1}`,
|
||||
description: `Audit support workflow ${index + 1}.`,
|
||||
triggers: [`audit-${index + 1}`],
|
||||
}),
|
||||
),
|
||||
makeSkill({
|
||||
id: 'accessibility-review',
|
||||
name: 'Accessibility Review',
|
||||
description: 'Audit accessible interaction details.',
|
||||
triggers: ['a11y-audit'],
|
||||
}),
|
||||
];
|
||||
renderComposer();
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '@audit', selectionStart: 6 },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Audit Helper 9')).toBeTruthy());
|
||||
const skillNames = Array.from(
|
||||
screen.getByTestId('mention-popover').querySelectorAll('.mention-item strong'),
|
||||
(node) => node.textContent,
|
||||
);
|
||||
|
||||
expect(skillNames).toContain('Audit Helper 9');
|
||||
expect(skillNames.indexOf('Audit Helper 1')).toBeLessThan(skillNames.indexOf('Story Brief'));
|
||||
expect(skillNames.indexOf('Audit Helper 9')).toBeLessThan(skillNames.indexOf('Accessibility Review'));
|
||||
});
|
||||
|
||||
it('applies a plugin from @ search and keeps the plugin token inline', async () => {
|
||||
renderComposer();
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
|
|
|||
Loading…
Reference in a new issue