From 28122f9b160ef7a873bbeb2a4915128e483165c3 Mon Sep 17 00:00:00 2001 From: Michael Morahan Date: Sat, 31 Oct 2026 12:25:07 -0600 Subject: [PATCH 1/4] fix(web): Tab completes folder names in the add-project browser Pressing Tab while typing a path in the Local folder browser moved focus out of the input instead of completing. Tab now completes shell-style: a single match (or the highlighted row) enters that directory, and several matches extend to their common prefix. Focus stays in the input. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/CommandPalette.tsx | 20 +++++++++++++++++++ .../src/state/filesystem.test.ts | 14 +++++++++++++ .../client-runtime/src/state/filesystem.ts | 17 ++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 486d80a988666..8e30c2cb2182c 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -32,6 +32,7 @@ import { createBrowseNavigationCoordinator, filterFilesystemBrowseEntries, getFilesystemBrowsePath, + getFilesystemBrowseTabCompletion, } from "@t3tools/client-runtime/state/filesystem"; import { isAtomCommandInterrupted, @@ -2492,6 +2493,25 @@ function OpenCommandPaletteDialog(props: { } if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return; + + if (event.key === "Tab" && isBrowsing && !relativePathNeedsActiveProject) { + (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); + event.preventDefault(); + const highlightedEntry = visibleBrowseEntries.find( + (entry) => `browse:${entry.fullPath}` === highlightedItemValue, + ); + const completion = highlightedEntry + ? { name: highlightedEntry.name, isComplete: true } + : getFilesystemBrowseTabCompletion(visibleBrowseEntries, browsePath.filterQuery); + if (completion?.isComplete) { + void browseTo(completion.name); + } else if (completion && !pinnedCloneDirectoryName) { + setHighlightedItemValue(null); + setQuery(`${getBrowseDirectoryPath(query)}${completion.name}`); + } + return; + } + const firstItemValue = autoHighlightsFirstRow ? resultRows.itemValues[0] : undefined; if ( event.key === "ArrowDown" && diff --git a/packages/client-runtime/src/state/filesystem.test.ts b/packages/client-runtime/src/state/filesystem.test.ts index 6d1c22bb856d3..249b9c344e878 100644 --- a/packages/client-runtime/src/state/filesystem.test.ts +++ b/packages/client-runtime/src/state/filesystem.test.ts @@ -4,6 +4,7 @@ import { AuthFilesystemReadScope, AuthOrchestrationReadScope } from "@t3tools/co import { createBrowseNavigationCoordinator, getFilesystemBrowsePath, + getFilesystemBrowseTabCompletion, resolveFilesystemReadAccess, } from "./filesystem.ts"; @@ -79,6 +80,19 @@ describe("filesystem browse model", () => { expect(getFilesystemBrowsePath("C:\\Users\\test", "MacIntel").isBrowsing).toBe(false); expect(getFilesystemBrowsePath("~/projects/", "", false).isBrowsing).toBe(false); }); + + it("completes Tab like a shell", () => { + const entry = (name: string) => ({ name, fullPath: `/Users/me/${name}` }); + expect(getFilesystemBrowseTabCompletion([entry("Creations")], "Creation")).toEqual({ + name: "Creations", + isComplete: true, + }); + expect(getFilesystemBrowseTabCompletion([entry("OpenSourced"), entry("OpenAI")], "op")).toEqual( + { name: "Open", isComplete: false }, + ); + expect(getFilesystemBrowseTabCompletion([entry("Open"), entry("Opal")], "Op")).toBeNull(); + expect(getFilesystemBrowseTabCompletion([], "x")).toBeNull(); + }); }); describe("browse navigation", () => { diff --git a/packages/client-runtime/src/state/filesystem.ts b/packages/client-runtime/src/state/filesystem.ts index 90276fe21661e..759bf8e40e23a 100644 --- a/packages/client-runtime/src/state/filesystem.ts +++ b/packages/client-runtime/src/state/filesystem.ts @@ -85,6 +85,23 @@ export function filterFilesystemBrowseEntries( return { visibleEntries, exactEntry }; } +/** Shell-style Tab: a single match enters the directory, several extend to their common prefix. */ +export function getFilesystemBrowseTabCompletion( + visibleEntries: ReadonlyArray, + filterQuery: string, +): { readonly name: string; readonly isComplete: boolean } | null { + const [first, ...rest] = visibleEntries; + if (first === undefined) return null; + if (rest.length === 0) return { name: first.name, isComplete: true }; + let prefix = first.name; + for (const entry of rest) { + let length = 0; + while (length < prefix.length && prefix[length] === entry.name[length]) length += 1; + prefix = prefix.slice(0, length); + } + return prefix.length > filterQuery.length ? { name: prefix, isComplete: false } : null; +} + export function createBrowseNavigationCoordinator() { let generation = 0; From 0a9b40ba151417b81921bcd84c76f504d26df1e9 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 23:39:35 +0000 Subject: [PATCH 2/4] fix(web): preserve folder completion edge cases --- apps/web/src/components/CommandPalette.tsx | 5 +++++ packages/client-runtime/src/state/filesystem.test.ts | 9 +++++++++ packages/client-runtime/src/state/filesystem.ts | 12 +++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 8e30c2cb2182c..81a5ac4922881 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -2495,8 +2495,13 @@ function OpenCommandPaletteDialog(props: { if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return; if (event.key === "Tab" && isBrowsing && !relativePathNeedsActiveProject) { + if (event.nativeEvent.isComposing || event.keyCode === 229) return; (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); event.preventDefault(); + if (highlightedItemValue === "browse:up") { + void browseUp(); + return; + } const highlightedEntry = visibleBrowseEntries.find( (entry) => `browse:${entry.fullPath}` === highlightedItemValue, ); diff --git a/packages/client-runtime/src/state/filesystem.test.ts b/packages/client-runtime/src/state/filesystem.test.ts index 249b9c344e878..680fe34c44cc3 100644 --- a/packages/client-runtime/src/state/filesystem.test.ts +++ b/packages/client-runtime/src/state/filesystem.test.ts @@ -91,6 +91,15 @@ describe("filesystem browse model", () => { { name: "Open", isComplete: false }, ); expect(getFilesystemBrowseTabCompletion([entry("Open"), entry("Opal")], "Op")).toBeNull(); + expect(getFilesystemBrowseTabCompletion([entry("openAI"), entry("OpenSource")], "o")).toEqual({ + name: "open", + isComplete: false, + }); + expect(getFilesystemBrowseTabCompletion([entry("😀one"), entry("😁two")], "")).toBeNull(); + expect(getFilesystemBrowseTabCompletion([entry("😀one"), entry("😀other")], "😀")).toEqual({ + name: "😀o", + isComplete: false, + }); expect(getFilesystemBrowseTabCompletion([], "x")).toBeNull(); }); }); diff --git a/packages/client-runtime/src/state/filesystem.ts b/packages/client-runtime/src/state/filesystem.ts index 759bf8e40e23a..ab1bfdfbb4f4e 100644 --- a/packages/client-runtime/src/state/filesystem.ts +++ b/packages/client-runtime/src/state/filesystem.ts @@ -93,13 +93,19 @@ export function getFilesystemBrowseTabCompletion( const [first, ...rest] = visibleEntries; if (first === undefined) return null; if (rest.length === 0) return { name: first.name, isComplete: true }; - let prefix = first.name; + let prefix = Array.from(first.name); for (const entry of rest) { + const characters = Array.from(entry.name); let length = 0; - while (length < prefix.length && prefix[length] === entry.name[length]) length += 1; + while ( + length < prefix.length && + prefix[length]?.toLowerCase() === characters[length]?.toLowerCase() + ) + length += 1; prefix = prefix.slice(0, length); } - return prefix.length > filterQuery.length ? { name: prefix, isComplete: false } : null; + const name = prefix.join(""); + return name.length > filterQuery.length ? { name, isComplete: false } : null; } export function createBrowseNavigationCoordinator() { From b40c7e6d91dcb6641297a99900f12fe0942041c9 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 23:51:54 +0000 Subject: [PATCH 3/4] fix(web): keep completed prefixes compatible with filtering --- packages/client-runtime/src/state/filesystem.test.ts | 1 + packages/client-runtime/src/state/filesystem.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/filesystem.test.ts b/packages/client-runtime/src/state/filesystem.test.ts index 680fe34c44cc3..be169b2ab9529 100644 --- a/packages/client-runtime/src/state/filesystem.test.ts +++ b/packages/client-runtime/src/state/filesystem.test.ts @@ -96,6 +96,7 @@ describe("filesystem browse model", () => { isComplete: false, }); expect(getFilesystemBrowseTabCompletion([entry("😀one"), entry("😁two")], "")).toBeNull(); + expect(getFilesystemBrowseTabCompletion([entry("ΟΣα"), entry("οσβ")], "ο")).toBeNull(); expect(getFilesystemBrowseTabCompletion([entry("😀one"), entry("😀other")], "😀")).toEqual({ name: "😀o", isComplete: false, diff --git a/packages/client-runtime/src/state/filesystem.ts b/packages/client-runtime/src/state/filesystem.ts index ab1bfdfbb4f4e..f8315eb3c6623 100644 --- a/packages/client-runtime/src/state/filesystem.ts +++ b/packages/client-runtime/src/state/filesystem.ts @@ -105,7 +105,11 @@ export function getFilesystemBrowseTabCompletion( prefix = prefix.slice(0, length); } const name = prefix.join(""); - return name.length > filterQuery.length ? { name, isComplete: false } : null; + const lowerName = name.toLowerCase(); + return name.length > filterQuery.length && + visibleEntries.every((entry) => entry.name.toLowerCase().startsWith(lowerName)) + ? { name, isComplete: false } + : null; } export function createBrowseNavigationCoordinator() { From a070dceaf8321f8a9402296c8ebae658e9fe94b6 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 1 Nov 2026 00:00:48 +0000 Subject: [PATCH 4/4] fix(web): compare completion progress after case folding --- packages/client-runtime/src/state/filesystem.test.ts | 4 ++++ packages/client-runtime/src/state/filesystem.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/filesystem.test.ts b/packages/client-runtime/src/state/filesystem.test.ts index be169b2ab9529..6c21c5d926919 100644 --- a/packages/client-runtime/src/state/filesystem.test.ts +++ b/packages/client-runtime/src/state/filesystem.test.ts @@ -97,6 +97,10 @@ describe("filesystem browse model", () => { }); expect(getFilesystemBrowseTabCompletion([entry("😀one"), entry("😁two")], "")).toBeNull(); expect(getFilesystemBrowseTabCompletion([entry("ΟΣα"), entry("οσβ")], "ο")).toBeNull(); + expect(getFilesystemBrowseTabCompletion([entry("İfoo"), entry("İfar")], "i\u0307")).toEqual({ + name: "İf", + isComplete: false, + }); expect(getFilesystemBrowseTabCompletion([entry("😀one"), entry("😀other")], "😀")).toEqual({ name: "😀o", isComplete: false, diff --git a/packages/client-runtime/src/state/filesystem.ts b/packages/client-runtime/src/state/filesystem.ts index f8315eb3c6623..03d9b27af57cd 100644 --- a/packages/client-runtime/src/state/filesystem.ts +++ b/packages/client-runtime/src/state/filesystem.ts @@ -106,7 +106,7 @@ export function getFilesystemBrowseTabCompletion( } const name = prefix.join(""); const lowerName = name.toLowerCase(); - return name.length > filterQuery.length && + return lowerName.length > filterQuery.toLowerCase().length && visibleEntries.every((entry) => entry.name.toLowerCase().startsWith(lowerName)) ? { name, isComplete: false } : null;