Rank slash command autocomplete matches
This commit is contained in:
@@ -11,6 +11,7 @@ import { useAutocomplete } from "./use-autocomplete";
|
|||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import { CLIENT_SLASH_COMMANDS, type ClientSlashCommand } from "@/client-slash-commands";
|
import { CLIENT_SLASH_COMMANDS, type ClientSlashCommand } from "@/client-slash-commands";
|
||||||
|
import { filterAndRankCommandAutocompleteEntries } from "@/utils/agent-command-autocomplete";
|
||||||
import {
|
import {
|
||||||
applyFileMentionReplacement,
|
applyFileMentionReplacement,
|
||||||
findActiveFileMention,
|
findActiveFileMention,
|
||||||
@@ -314,7 +315,6 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "command") {
|
if (mode === "command") {
|
||||||
const filterLower = commandFilterQuery.toLowerCase();
|
|
||||||
const providerCommands = commands.map(
|
const providerCommands = commands.map(
|
||||||
(command): AvailableCommand => ({ source: "provider", command }),
|
(command): AvailableCommand => ({ source: "provider", command }),
|
||||||
);
|
);
|
||||||
@@ -326,13 +326,10 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut
|
|||||||
),
|
),
|
||||||
...providerCommands,
|
...providerCommands,
|
||||||
];
|
];
|
||||||
const matches = availableCommands.filter((entry) => {
|
const matches = filterAndRankCommandAutocompleteEntries(
|
||||||
if (entry.source === "provider") {
|
availableCommands,
|
||||||
return entry.command.name.toLowerCase().includes(filterLower);
|
commandFilterQuery,
|
||||||
}
|
);
|
||||||
const candidates = [entry.command.name, ...entry.command.aliases];
|
|
||||||
return candidates.some((candidate) => candidate.toLowerCase().includes(filterLower));
|
|
||||||
});
|
|
||||||
const orderedMatches = orderAutocompleteOptions(matches);
|
const orderedMatches = orderAutocompleteOptions(matches);
|
||||||
return orderedMatches.map(mapCommandToOption);
|
return orderedMatches.map(mapCommandToOption);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { filterAndRankCommandAutocompleteEntries } from "./agent-command-autocomplete";
|
||||||
|
|
||||||
|
describe("filterAndRankCommandAutocompleteEntries", () => {
|
||||||
|
const entries = [
|
||||||
|
{ source: "provider" as const, command: { name: "paseo-committee" } },
|
||||||
|
{ source: "provider" as const, command: { name: "commit" } },
|
||||||
|
{ source: "provider" as const, command: { name: "paseo-advisor" } },
|
||||||
|
];
|
||||||
|
|
||||||
|
it("ranks command-name prefixes above later word-boundary partial matches", () => {
|
||||||
|
const result = filterAndRankCommandAutocompleteEntries(entries, "comm");
|
||||||
|
|
||||||
|
expect(result.map((entry) => entry.command.name)).toEqual(["commit", "paseo-committee"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches client command aliases", () => {
|
||||||
|
const result = filterAndRankCommandAutocompleteEntries(
|
||||||
|
[
|
||||||
|
{ source: "client" as const, command: { name: "exit", aliases: ["quit", "q"] } },
|
||||||
|
{ source: "client" as const, command: { name: "clear", aliases: ["new"] } },
|
||||||
|
],
|
||||||
|
"q",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.map((entry) => entry.command.name)).toEqual(["exit"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { compareMatchScores, type MatchScore, scoreTextFields } from "@/utils/score-match";
|
||||||
|
|
||||||
|
interface CommandAutocompleteEntry {
|
||||||
|
command: {
|
||||||
|
name: string;
|
||||||
|
aliases?: readonly string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScoredCommandAutocompleteEntry<TEntry> {
|
||||||
|
entry: TEntry;
|
||||||
|
score: MatchScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreCommandAutocompleteEntry(
|
||||||
|
entry: CommandAutocompleteEntry,
|
||||||
|
query: string,
|
||||||
|
): MatchScore | null {
|
||||||
|
return scoreTextFields(query, [entry.command.name, ...(entry.command.aliases ?? [])]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterAndRankCommandAutocompleteEntries<TEntry extends CommandAutocompleteEntry>(
|
||||||
|
entries: readonly TEntry[],
|
||||||
|
query: string,
|
||||||
|
): TEntry[] {
|
||||||
|
const normalizedQuery = query.trim().toLowerCase();
|
||||||
|
if (normalizedQuery.length === 0) {
|
||||||
|
return [...entries];
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoredEntries: ScoredCommandAutocompleteEntry<TEntry>[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const score = scoreCommandAutocompleteEntry(entry, normalizedQuery);
|
||||||
|
if (score) {
|
||||||
|
scoredEntries.push({ entry, score });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scoredEntries.sort((a, b) => {
|
||||||
|
const scoreComparison = compareMatchScores(a.score, b.score);
|
||||||
|
if (scoreComparison !== 0) {
|
||||||
|
return scoreComparison;
|
||||||
|
}
|
||||||
|
return a.entry.command.name.localeCompare(b.entry.command.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return scoredEntries.map((scored) => scored.entry);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user