Compare commits

...
Author SHA1 Message Date
Saoud Rizwan 97147614e1 fix(cli): restore fuzzy file mention ranking 2026-05-19 15:15:44 -07:00
5 changed files with 144 additions and 38 deletions
+6 -5
View File
@@ -68,26 +68,27 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"@clack/prompts": "^1.2.0",
"@chat-adapter/discord": "^4.23.0",
"@chat-adapter/gchat": "^4.23.0",
"@chat-adapter/linear": "^4.23.0",
"@chat-adapter/slack": "^4.23.0",
"@chat-adapter/telegram": "^4.23.0",
"@chat-adapter/whatsapp": "^4.23.0",
"@clack/prompts": "^1.2.0",
"@gramio/format": "^0.7.0",
"chat": "^4.23.0",
"commander": "^14.0.3",
"@opentui-ui/dialog": "^0.1.2",
"@opentui/core": "0.1.102",
"@opentui/react": "0.1.102",
"@opentui-ui/dialog": "^0.1.2",
"chat": "^4.23.0",
"commander": "^14.0.3",
"fzf": "^0.5.2",
"marked": "^15.0.12",
"open": "^10.2.0",
"opentui-spinner": "^0.0.6",
"pino": "^10.3.1",
"react": "19.2.4",
"react-reconciler": "0.32.0",
"react-devtools-core": "^7.0.1",
"react-reconciler": "0.32.0",
"yaml": "^2.8.2",
"zod": "^4.1.11"
},
@@ -198,13 +198,8 @@ export function useAutocomplete(opts: {
);
const getFilteredMentionOptions = useCallback(
(query: string): AutocompleteOption[] => {
const q = query.toLowerCase();
let filtered = mentionResults;
if (q) {
filtered = mentionResults.filter((f) => f.toLowerCase().includes(q));
}
return filtered.slice(0, MAX_COMPLETION_RESULTS).map((f) => ({
(_query: string): AutocompleteOption[] => {
return mentionResults.slice(0, MAX_COMPLETION_RESULTS).map((f) => ({
display: f,
value: formatMentionAutocompleteValue(f),
}));
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { rankMentionPaths } from "./interactive-welcome";
describe("TUI file mention search ranking", () => {
it("keeps initialism matches for compact and hyphenated input", () => {
const paths = [
"src/components/Button.tsx",
"src/domain/MyAmazingClassDefinition.ts",
"docs/MACD.md",
"packages/core/src/runtime/manager.ts",
];
expect(rankMentionPaths(paths, "MACD", 10)).toEqual([
"docs/MACD.md",
"src/domain/MyAmazingClassDefinition.ts",
]);
expect(rankMentionPaths(paths, "M-A-C-D", 10)).toEqual([
"docs/MACD.md",
"src/domain/MyAmazingClassDefinition.ts",
]);
});
it("normalizes common mention prefixes before matching workspace paths", () => {
const paths = [
"docs/architecture.md",
"src/tui/interactive-welcome.ts",
"src/tui/hooks/use-autocomplete.ts",
];
expect(rankMentionPaths(paths, "./src/tui", 10)).toEqual([
"src/tui/hooks/use-autocomplete.ts",
"src/tui/interactive-welcome.ts",
]);
expect(rankMentionPaths(paths, "/docs", 10)).toEqual([
"docs/architecture.md",
]);
});
it("ranks filename matches ahead of path-only fuzzy matches", () => {
const paths = [
"src/migrations/add-column.ts",
"src/domain/MyAmazingClassDefinition.ts",
"docs/classes.md",
];
expect(rankMentionPaths(paths, "class", 10)[0]).toBe("docs/classes.md");
});
});
+84 -25
View File
@@ -3,6 +3,7 @@ import {
type ProviderSettings,
type UserInstructionConfigService,
} from "@cline/core";
import { byLengthAsc, Fzf, type FzfResultItem } from "fzf";
import type { Config } from "../utils/types";
import { formatClineCredits, loadClineAccountSnapshot } from "./cline-account";
@@ -17,24 +18,94 @@ function normalizeLimit(limit: number | undefined): number {
if (typeof limit !== "number" || Number.isNaN(limit)) {
return 10;
}
return Math.min(50, Math.max(1, Math.trunc(limit)));
return Math.min(200, Math.max(1, Math.trunc(limit)));
}
function rankPath(path: string, query: string): number {
if (query.length === 0) {
return 3;
function getPathLabel(filePath: string): string {
const parts = filePath.split("/");
return parts[parts.length - 1] ?? filePath;
}
function normalizeMentionQuery(query: string): string {
const trimmed = query.trim().replace(/^["']/, "");
if (trimmed.startsWith("./")) {
return trimmed.slice(2);
}
const lowerPath = path.toLowerCase();
if (lowerPath.startsWith(query)) {
return 0;
if (trimmed.startsWith("/")) {
return trimmed.slice(1);
}
if (lowerPath.includes(`/${query}`)) {
return 1;
return trimmed;
}
function compactSearchText(text: string): string {
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
}
interface MentionPathItem {
path: string;
label: string;
searchText: string;
}
function countGaps(positions: Iterable<number>): number {
let gaps = 0;
let previous = Number.NEGATIVE_INFINITY;
for (const position of positions) {
if (previous !== Number.NEGATIVE_INFINITY && position - previous > 1) {
gaps++;
}
previous = position;
}
if (lowerPath.includes(query)) {
return 2;
return gaps;
}
function orderByMatchScore(
left: FzfResultItem<MentionPathItem>,
right: FzfResultItem<MentionPathItem>,
): number {
return countGaps(left.positions) - countGaps(right.positions);
}
export function rankMentionPaths(
paths: Iterable<string>,
query: string,
limit: number,
): string[] {
const items = Array.from(paths, (path): MentionPathItem => {
const label = getPathLabel(path);
return {
path,
label,
searchText: [
label,
label,
path,
compactSearchText(label),
compactSearchText(path),
].join(" "),
};
});
const normalizedQuery = normalizeMentionQuery(query);
if (!normalizedQuery) {
return items
.sort((left, right) => left.path.localeCompare(right.path))
.slice(0, limit)
.map((item) => item.path);
}
return Number.POSITIVE_INFINITY;
const fzf = new Fzf(items, {
selector: (item) => item.searchText,
tiebreakers: [orderByMatchScore, byLengthAsc],
limit,
});
const rawResults = fzf.find(normalizedQuery);
const results =
rawResults.length > 0
? rawResults
: fzf.find(compactSearchText(normalizedQuery));
return results.map((result) => result.item.path);
}
export function listInteractiveSlashCommands(
@@ -90,21 +161,9 @@ export async function searchWorkspaceFilesForMention(input: {
if (!workspaceRoot) {
return [];
}
const query = input.query.trim().toLowerCase();
const limit = normalizeLimit(input.limit);
const index = await getFileIndex(workspaceRoot);
const allPaths = Array.from(index).sort((a, b) => a.localeCompare(b));
return allPaths
.map((path) => ({ path, rank: rankPath(path, query) }))
.filter((item) => Number.isFinite(item.rank))
.sort((left, right) => {
if (left.rank !== right.rank) {
return left.rank - right.rank;
}
return left.path.localeCompare(right.path);
})
.slice(0, limit)
.map((item) => item.path);
return rankMentionPaths(index, input.query, limit);
}
export async function resolveClineWelcomeLine(input: {
+4 -1
View File
@@ -19,7 +19,7 @@
},
"apps/cli": {
"name": "@cline/cli",
"version": "3.0.3",
"version": "3.0.8",
"bin": {
"cline": "src/index.ts",
},
@@ -38,6 +38,7 @@
"@opentui/react": "0.1.102",
"chat": "^4.23.0",
"commander": "^14.0.3",
"fzf": "^0.5.2",
"marked": "^15.0.12",
"open": "^10.2.0",
"opentui-spinner": "^0.0.6",
@@ -2087,6 +2088,8 @@
"fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
"fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="],
"gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],