From b31d7758a323f7551486d6d58f678faf80656b20 Mon Sep 17 00:00:00 2001 From: Christoffer Artmann Date: Wed, 28 Oct 2026 10:17:08 +0100 Subject: [PATCH 1/2] feat(client): add-project folder search matches substrings and fuzzy subsequences The add-project folder browser only matched folder names by prefix. It now ranks entries with the shared palette scorer: exact, prefix, word start, substring, then fuzzy subsequence. --- .../src/state/filesystem.test.ts | 29 +++++++++++++++++++ .../client-runtime/src/state/filesystem.ts | 29 +++++++++++++++---- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/client-runtime/src/state/filesystem.test.ts b/packages/client-runtime/src/state/filesystem.test.ts index 6d1c22bb856d3..6fe6323ac4a1a 100644 --- a/packages/client-runtime/src/state/filesystem.test.ts +++ b/packages/client-runtime/src/state/filesystem.test.ts @@ -3,6 +3,7 @@ import { AuthFilesystemReadScope, AuthOrchestrationReadScope } from "@t3tools/co import { createBrowseNavigationCoordinator, + filterFilesystemBrowseEntries, getFilesystemBrowsePath, resolveFilesystemReadAccess, } from "./filesystem.ts"; @@ -105,3 +106,31 @@ describe("browse navigation", () => { expect(commits).toEqual(["second"]); }); }); + +describe("browse entry filtering", () => { + const entries = ["my-t3code", "t3code", "cotton", "T3Code-old", ".t3", "docs"].map((name) => ({ + name, + fullPath: `~/code/${name}`, + })); + const names = (query: string) => + filterFilesystemBrowseEntries(entries, query).visibleEntries.map((entry) => entry.name); + + it("keeps every visible entry in order for an empty query", () => { + expect(names("")).toEqual(["my-t3code", "t3code", "cotton", "T3Code-old", "docs"]); + }); + + it("matches substrings and subsequences, best match first", () => { + expect(names("code")).toEqual(["t3code", "T3Code-old", "my-t3code"]); + expect(names("t3c")).toEqual(["t3code", "T3Code-old", "my-t3code"]); + expect(names("tcd")).toEqual(["t3code", "T3Code-old", "my-t3code"]); + expect(names("xyz")).toEqual([]); + }); + + it("shows dot folders only when the query starts with a dot", () => { + expect(names("t3")).not.toContain(".t3"); + expect(filterFilesystemBrowseEntries(entries, ".t3")).toEqual({ + visibleEntries: [entries[4]], + exactEntry: entries[4], + }); + }); +}); diff --git a/packages/client-runtime/src/state/filesystem.ts b/packages/client-runtime/src/state/filesystem.ts index 90276fe21661e..54a22c745ca04 100644 --- a/packages/client-runtime/src/state/filesystem.ts +++ b/packages/client-runtime/src/state/filesystem.ts @@ -5,6 +5,7 @@ import { sessionGrantsScope, type SessionGrantInput, } from "@t3tools/contracts"; +import { scoreQueryMatch } from "@t3tools/shared/searchRanking"; import { Atom } from "effect/reactivity"; import type { @@ -74,11 +75,29 @@ export function filterFilesystemBrowseEntries( ) { const lowerQuery = query.toLowerCase(); const showHidden = query.startsWith("."); - const visibleEntries = entries.filter( - (entry) => - entry.name.toLowerCase().startsWith(lowerQuery) && - (showHidden || !entry.name.startsWith(".")), - ); + const candidates = entries.filter((entry) => showHidden || !entry.name.startsWith(".")); + const visibleEntries = + lowerQuery.length === 0 + ? candidates + : candidates + .map((entry) => ({ + entry, + score: scoreQueryMatch({ + value: entry.name.toLowerCase(), + query: lowerQuery, + exactBase: 0, + prefixBase: 100, + boundaryBase: 200, + includesBase: 300, + fuzzyBase: 400, + boundaryMarkers: [" ", "-", "_", "."], + }), + })) + .filter((match): match is { entry: FilesystemBrowseEntry; score: number } => { + return match.score !== null; + }) + .sort((left, right) => left.score - right.score) + .map((match) => match.entry); const exactEntry = query.length > 0 ? (visibleEntries.find((entry) => entry.name === query) ?? null) : null; From 6d4e66389b8482b0758ee0711d2b8d535cf489bd Mon Sep 17 00:00:00 2001 From: Christoffer Artmann Date: Wed, 28 Oct 2026 10:48:20 +0100 Subject: [PATCH 2/2] fix(client): folder search match tiers no longer overlap for long names --- packages/client-runtime/src/state/filesystem.test.ts | 11 +++++++++++ packages/client-runtime/src/state/filesystem.ts | 9 +++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/client-runtime/src/state/filesystem.test.ts b/packages/client-runtime/src/state/filesystem.test.ts index 6fe6323ac4a1a..c6af4d6f64c6f 100644 --- a/packages/client-runtime/src/state/filesystem.test.ts +++ b/packages/client-runtime/src/state/filesystem.test.ts @@ -126,6 +126,17 @@ describe("browse entry filtering", () => { expect(names("xyz")).toEqual([]); }); + it("ranks a word-start match in a long name above any substring match", () => { + const longName = `${"a".repeat(240)}-foo`; + const folders = [ + { name: "xfoo", fullPath: "~/xfoo" }, + { name: longName, fullPath: "~/long" }, + ]; + expect( + filterFilesystemBrowseEntries(folders, "foo").visibleEntries.map((entry) => entry.name), + ).toEqual([longName, "xfoo"]); + }); + it("shows dot folders only when the query starts with a dot", () => { expect(names("t3")).not.toContain(".t3"); expect(filterFilesystemBrowseEntries(entries, ".t3")).toEqual({ diff --git a/packages/client-runtime/src/state/filesystem.ts b/packages/client-runtime/src/state/filesystem.ts index 54a22c745ca04..3e1d03db7fdef 100644 --- a/packages/client-runtime/src/state/filesystem.ts +++ b/packages/client-runtime/src/state/filesystem.ts @@ -82,14 +82,15 @@ export function filterFilesystemBrowseEntries( : candidates .map((entry) => ({ entry, + // Position penalties stay under 1000 for 255-character names, so tiers never overlap. score: scoreQueryMatch({ value: entry.name.toLowerCase(), query: lowerQuery, exactBase: 0, - prefixBase: 100, - boundaryBase: 200, - includesBase: 300, - fuzzyBase: 400, + prefixBase: 1000, + boundaryBase: 2000, + includesBase: 3000, + fuzzyBase: 4000, boundaryMarkers: [" ", "-", "_", "."], }), }))