fix(web): expand skill mention picker (#2170)

This commit is contained in:
kami 2026-05-19 14:49:15 +09:00 committed by GitHub
parent 6e1ebb97dd
commit b838e94b88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 17 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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;