mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(webui): rank skill autocomplete results
This commit is contained in:
parent
ef9e687f19
commit
fa5d27696a
@ -309,6 +309,16 @@ interface SlashPaletteCommand {
|
|||||||
recent: boolean;
|
recent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skillMatchRank(skill: SkillSummary, query: string): number | null {
|
||||||
|
if (!query) return 0;
|
||||||
|
const name = skill.name.toLowerCase();
|
||||||
|
if (name === query) return 0;
|
||||||
|
if (name.startsWith(query)) return 1;
|
||||||
|
if (name.includes(query)) return 2;
|
||||||
|
if (skill.description.toLowerCase().includes(query)) return 3;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function slashCommandI18nKey(command: string): string {
|
function slashCommandI18nKey(command: string): string {
|
||||||
return command.replace(/^\//, "").replace(/-/g, "_");
|
return command.replace(/^\//, "").replace(/-/g, "_");
|
||||||
}
|
}
|
||||||
@ -1029,15 +1039,28 @@ export function ThreadComposer({
|
|||||||
const query = skillQuery.text;
|
const query = skillQuery.text;
|
||||||
return skills
|
return skills
|
||||||
.filter((skill) => skill.available)
|
.filter((skill) => skill.available)
|
||||||
.filter((skill) => {
|
.flatMap((skill) => {
|
||||||
const haystack = [
|
const matchRank = skillMatchRank(skill, query);
|
||||||
skill.name,
|
return matchRank === null
|
||||||
skill.description,
|
? []
|
||||||
].join(" ").toLowerCase();
|
: [{
|
||||||
return haystack.includes(query);
|
command: `$${skill.name}`,
|
||||||
|
matchRank,
|
||||||
|
skill,
|
||||||
|
}];
|
||||||
})
|
})
|
||||||
.map((skill) => {
|
.sort((a, b) => {
|
||||||
const command = `$${skill.name}`;
|
if (a.matchRank !== b.matchRank) return a.matchRank - b.matchRank;
|
||||||
|
if (query !== "") return 0;
|
||||||
|
const aRecent = recentSlashCommands.indexOf(a.command);
|
||||||
|
const bRecent = recentSlashCommands.indexOf(b.command);
|
||||||
|
if (aRecent === -1 && bRecent === -1) return 0;
|
||||||
|
if (aRecent === -1) return 1;
|
||||||
|
if (bRecent === -1) return -1;
|
||||||
|
return aRecent - bRecent;
|
||||||
|
})
|
||||||
|
.slice(0, 8)
|
||||||
|
.map(({ command, skill }) => {
|
||||||
const description = skill.description || skill.name;
|
const description = skill.description || skill.name;
|
||||||
return {
|
return {
|
||||||
command,
|
command,
|
||||||
@ -1048,8 +1071,7 @@ export function ThreadComposer({
|
|||||||
kind: "skill" as const,
|
kind: "skill" as const,
|
||||||
recent: recentSlashCommands.includes(command),
|
recent: recentSlashCommands.includes(command),
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.slice(0, 8);
|
|
||||||
}
|
}
|
||||||
if (slashQuery === null) return [];
|
if (slashQuery === null) return [];
|
||||||
const withDetails = visibleSlashCommands
|
const withDetails = visibleSlashCommands
|
||||||
|
|||||||
@ -1506,6 +1506,80 @@ describe("ThreadComposer", () => {
|
|||||||
expect(input).toHaveValue(`please use $${skillName} `);
|
expect(input).toHaveValue(`please use $${skillName} `);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ranks skill name matches ahead of earlier description matches", () => {
|
||||||
|
render(
|
||||||
|
<ThreadComposer
|
||||||
|
onSend={vi.fn()}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
skills={[
|
||||||
|
{
|
||||||
|
name: "skill-creator",
|
||||||
|
description: "Create or update AgentSkills",
|
||||||
|
source: "builtin",
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setup-update",
|
||||||
|
description: "Configure upgrades",
|
||||||
|
source: "builtin",
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update-setup",
|
||||||
|
description: "One-time setup wizard",
|
||||||
|
source: "builtin",
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "up",
|
||||||
|
description: "Exact match",
|
||||||
|
source: "workspace",
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText("Message input");
|
||||||
|
fireEvent.change(input, { target: { value: "$up", selectionStart: 3 } });
|
||||||
|
|
||||||
|
const options = within(screen.getByRole("listbox", { name: "Slash commands" }))
|
||||||
|
.getAllByRole("option");
|
||||||
|
expect(options.map((option) => option.textContent)).toEqual([
|
||||||
|
"upExact match",
|
||||||
|
"update-setupOne-time setup wizard",
|
||||||
|
"setup-updateConfigure upgrades",
|
||||||
|
"skill-creatorCreate or update AgentSkills",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a recently selected skill visible at the top of the blank skill menu", () => {
|
||||||
|
const skills = Array.from({ length: 9 }, (_, index) => ({
|
||||||
|
name: `skill-${index}`,
|
||||||
|
description: `Skill ${index}`,
|
||||||
|
source: "builtin",
|
||||||
|
available: true,
|
||||||
|
}));
|
||||||
|
render(
|
||||||
|
<ThreadComposer
|
||||||
|
onSend={vi.fn()}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
skills={skills}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByLabelText("Message input");
|
||||||
|
fireEvent.change(input, { target: { value: "$skill-8", selectionStart: 8 } });
|
||||||
|
fireEvent.keyDown(input, { key: "Tab" });
|
||||||
|
fireEvent.change(input, { target: { value: "$", selectionStart: 1 } });
|
||||||
|
|
||||||
|
const options = within(screen.getByRole("listbox", { name: "Slash commands" }))
|
||||||
|
.getAllByRole("option");
|
||||||
|
expect(options).toHaveLength(8);
|
||||||
|
expect(options[0]).toHaveTextContent("skill-8");
|
||||||
|
expect(options[0]).toHaveTextContent("Recent");
|
||||||
|
});
|
||||||
|
|
||||||
it("shows right-side source badges so users can distinguish CLI apps from MCP servers", () => {
|
it("shows right-side source badges so users can distinguish CLI apps from MCP servers", () => {
|
||||||
render(
|
render(
|
||||||
<ThreadComposer
|
<ThreadComposer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user