From dc08fa132b95801cb580945dbed6eaaac1418e9e Mon Sep 17 00:00:00 2001
From: stroblme
Date: Mon, 14 Sep 2026 14:40:17 +0200
Subject: [PATCH] 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)
Claude-Session: https://claude.ai/code/session_01VfgHWr7AQDiLsTQVu7hpKB
---
.../src/components/Common/GlobalSearch.tsx | 27 ++++++++++++++-----
.../components/UserSettings/RemoteAccess.tsx | 10 ++++---
2 files changed, 27 insertions(+), 10 deletions(-)
diff --git a/frontend/src/components/Common/GlobalSearch.tsx b/frontend/src/components/Common/GlobalSearch.tsx
index 41d9c5a..f05874c 100644
--- a/frontend/src/components/Common/GlobalSearch.tsx
+++ b/frontend/src/components/Common/GlobalSearch.tsx
@@ -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
}
/**
diff --git a/frontend/src/components/UserSettings/RemoteAccess.tsx b/frontend/src/components/UserSettings/RemoteAccess.tsx
index 9c1ef26..1b4ec3f 100644
--- a/frontend/src/components/UserSettings/RemoteAccess.tsx
+++ b/frontend/src/components/UserSettings/RemoteAccess.tsx
@@ -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() {