Spin the Add remote user button; stop the search prefilter dropping typos

"Add remote user" was the one call site swapping its label instead of
using LoadingButton, so it alone had no spinner.

Global search's couldMatch was a strict subsequence test while cmdk's
scorer steps over one query character for a transposition, so a typo it
would have scored never reached it. The prefilter now carries that one
skip, which recovers every single-transposition query in a sweep of the
index shapes and admits nothing cmdk scores zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfgHWr7AQDiLsTQVu7hpKB
This commit is contained in:
2026-09-14 14:40:17 +02:00
co-authored by Claude Opus 5
parent aa3e2e8b81
commit dc08fa132b
2 changed files with 27 additions and 10 deletions
@@ -71,17 +71,32 @@ function normalise(text: string): string {
/**
* Whether cmdk could score this entry at all: it needs the query's characters
* in the value, in order. Asking the cheap half of that question here is what
* lets GROUP_CAP cap the candidates rather than the index.
* in the value, in order, bar the one its scorer steps over for a transposed
* or mistyped letter. Asking the cheap half of that question here is what lets
* GROUP_CAP cap the candidates rather than the index.
*
* Plain subsequence was the wrong half to ask: cmdk still scores "dahsboard"
* against "dashboard", and this dropped it before anything could. One skip is
* where it stops, because the scorer allows another for each further tenth of
* the score, and a budget that follows it all the way down is no filter at all.
*/
function couldMatch(value: string, query: string): boolean {
const text = normalise(value)
let from = 0
// Where the rest of the query can resume from with the skip still in hand,
// and where with it spent. Infinity is a path that has died — `indexOf` past
// the end of the string answers -1 by itself.
let intact = 0
let spent = Number.POSITIVE_INFINITY
for (const char of normalise(query)) {
from = text.indexOf(char, from) + 1
if (from === 0) return false
const resume = (from: number) => {
const at = text.indexOf(char, from)
return at < 0 ? Number.POSITIVE_INFINITY : at + 1
}
// Skipping this character leaves the intact path where it already stood.
spent = Math.min(intact, resume(spent))
intact = resume(intact)
}
return true
return Math.min(intact, spent) < Number.POSITIVE_INFINITY
}
/**
@@ -21,6 +21,7 @@ import {
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { LoadingButton } from "@/components/ui/loading-button"
import { Separator } from "@/components/ui/separator"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
@@ -242,14 +243,15 @@ export function RemoteAccess() {
</p>
</div>
<div>
<Button
<LoadingButton
className="rounded-full"
variant="outline"
disabled={!joinCode.trim() || addRemoteUser.isPending}
loading={addRemoteUser.isPending}
disabled={!joinCode.trim()}
onClick={() => addRemoteUser.mutate()}
>
{addRemoteUser.isPending ? "Adding…" : "Add remote user"}
</Button>
Add remote user
</LoadingButton>
</div>
</div>