From 009889261a685f6c720aa70ae70f8ae14424c803 Mon Sep 17 00:00:00 2001 From: shobhit99 Date: Wed, 24 Jun 2026 14:11:44 +0530 Subject: [PATCH 1/4] fix: surface individual settings (e.g. "Color Filters") in search (#471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System Settings panes exposed only the top-level pane (e.g. "Accessibility"), so sub-settings findable in Spotlight — like "Color Filters" — could not be found in SuperCmd. discoverSettingsSearchTermCommands() already parsed each pane's .searchTerms file to extract these sub-settings, but it was never called. Wire it into the appex (Source 1) discovery loop so every pane's individual settings are added to the command list, searchable by name and keyword just like in Spotlight. Also bump COMMANDS_DISK_CACHE_VERSION to 2 so existing installs refresh their cached command list to include the new sub-settings on first launch. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/commands.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/commands.ts b/src/main/commands.ts index 4a89f301..7477b811 100644 --- a/src/main/commands.ts +++ b/src/main/commands.ts @@ -119,7 +119,9 @@ const STALE_REFRESH_COOLDOWN_MS = 15_000; // and are re-attached on load. Bump the version when CommandInfo shape changes // in a breaking way. -const COMMANDS_DISK_CACHE_VERSION = 1; +// v2: settings panes now also expose their individual settings (search terms), +// e.g. "Color Filters" inside Accessibility — refresh cached commands to include them. +const COMMANDS_DISK_CACHE_VERSION = 2; let commandsDiskCachePath: string | null = null; function getCommandsDiskCachePath(): string { @@ -1151,12 +1153,23 @@ async function discoverSystemSettings(): Promise { _bundlePath: extPath, }; - return paneCommand; + // Also surface the pane's individual settings (e.g. "Color Filters" + // inside Accessibility) by parsing the bundle's .searchTerms file, so + // they can be found by name just like in Spotlight. + const subItems = await discoverSettingsSearchTermCommands( + extPath, + paneCommand, + bundleId, + legacyBundleId, + searchTermsFileName + ); + + return [paneCommand, ...subItems]; }) ); for (const item of items) { - if (item) results.push(item); + if (item) results.push(...item); } } } From 7bfecbf14be314295edc03c1142cd2248e6df586 Mon Sep 17 00:00:00 2001 From: shobhit99 Date: Wed, 24 Jun 2026 14:18:29 +0530 Subject: [PATCH 2/4] fix: only surface human setting titles, not raw searchTerms keys Codex review (P2): discoverSettingsSearchTermCommands added each top-level .searchTerms key as its own visible command. Those keys are internal identifiers (e.g. "AX_ALT_MOUSE_ENABLE_SOUNDS"), so wiring the function in flooded the launcher with ~300+ junk commands per pane. Every key carries the real human label in localizableStrings[].title (verified: across 47 system panes, only 2 keys lack rows). Drop the section-key-as-title branch and its identifier-derived keywords; emit only row titles plus their curated `index` keywords. Accessibility sub-items drop 679 -> 342 clean entries; "Color Filters" still resolves. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/commands.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/commands.ts b/src/main/commands.ts index 7477b811..f4188007 100644 --- a/src/main/commands.ts +++ b/src/main/commands.ts @@ -929,26 +929,18 @@ async function discoverSettingsSearchTermCommands( }; for (const [sectionRaw, sectionValue] of Object.entries(data)) { - const sectionTitle = cleanPaneName(sectionRaw); - const sectionKey = sectionRaw.toLowerCase(); - const sectionKeywords: string[] = [sectionKey]; - - if (sectionTitle && sectionTitle.toLowerCase() !== paneTitleLower) { - addCommand(sectionTitle, sectionKeywords, `section:${sectionRaw}`); - } - + // Each top-level key in a .searchTerms file is an internal identifier + // (e.g. "AX_DISPLAY_FILTER_ENABLED"), NOT a user-facing label. The human + // titles live in localizableStrings[].title — only surface those, otherwise + // the launcher fills up with junk commands named after raw identifiers. const rows = Array.isArray((sectionValue as any)?.localizableStrings) ? (sectionValue as any).localizableStrings : []; for (const row of rows) { const rowTitle = String(row?.title || '').trim(); - if (!rowTitle) continue; - const keywords = [ - sectionKey, - sectionTitle.toLowerCase(), - ...splitSearchKeywords(String(row?.index || '')), - ].filter(Boolean); + if (!rowTitle || rowTitle.toLowerCase() === paneTitleLower) continue; + const keywords = splitSearchKeywords(String(row?.index || '')); addCommand(rowTitle, keywords, `${sectionRaw}:${rowTitle}`); } } From 08428aae6c4adb1469046549ab2bfd6e9f29cc34 Mon Sep 17 00:00:00 2001 From: shobhit99 Date: Wed, 24 Jun 2026 15:02:54 +0530 Subject: [PATCH 3/4] feat: rank individual settings as a search fallback, not first-class results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wiring in per-setting search (previous commits) meant a query could surface hundreds of granular toggles competing with apps, commands, and panes — the launcher root is a curated namespace, not an exhaustive index like Spotlight. Mark each setting sub-item (CommandInfo.settingsSubItem) and treat it as a low-priority fallback in both ranking paths: - Root-search path (useLauncherCommandModel): skip sub-items for queries under 3 chars; apply a noise penalty (200) so they sort below real results. Weak (contains/subsequence) sub-item matches fall under the promotion floor and drop out entirely; strong (exact/prefix) ones still appear, lower down. - Legacy filterCommands path: same 3-char gate, and hard-tier sub-items below all non-sub-item matches regardless of raw score. Net: "color filters" still finds Color Filters, but typing "co" no longer floods results with accessibility toggles, and sub-items never outrank the app or command you actually wanted. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/commands.ts | 8 ++++++++ .../src/hooks/useLauncherCommandModel.ts | 20 +++++++++++++++++-- src/renderer/src/utils/command-helpers.tsx | 16 +++++++++++++++ src/renderer/types/electron.d.ts | 4 ++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/commands.ts b/src/main/commands.ts index f4188007..ea6a78dc 100644 --- a/src/main/commands.ts +++ b/src/main/commands.ts @@ -99,6 +99,13 @@ export interface CommandInfo { }>; /** SuperCmd deeplink (e.g. `supercmd://extensions///`). Set for extension and script commands. */ deeplink?: string; + /** + * True for an individual setting parsed from a pane's .searchTerms file + * (e.g. "Color Filters" inside Accessibility), as opposed to the top-level + * pane itself. Used by search ranking to treat these as a low-priority + * fallback so they don't crowd out apps, commands, and panes. + */ + settingsSubItem?: boolean; /** Bundle path on disk (used for icon extraction) */ _bundlePath?: string; } @@ -924,6 +931,7 @@ async function discoverSettingsSearchTermCommands( iconDataUrl: pane.iconDataUrl, category: 'settings', path: pane.path, + settingsSubItem: true, _bundlePath: pane._bundlePath, }); }; diff --git a/src/renderer/src/hooks/useLauncherCommandModel.ts b/src/renderer/src/hooks/useLauncherCommandModel.ts index 44ca0fbc..962efa1e 100644 --- a/src/renderer/src/hooks/useLauncherCommandModel.ts +++ b/src/renderer/src/hooks/useLauncherCommandModel.ts @@ -130,6 +130,13 @@ export type UseLauncherCommandModelResult = { selectedFileResultPath: string | null; }; +// Individual settings parsed from a pane's .searchTerms file (e.g. "Color +// Filters") are a low-priority search fallback. They're only considered once +// the query is at least this many chars, and carry a ranking penalty so they +// sit below real apps/commands/panes. +const SETTINGS_SUBITEM_MIN_QUERY_LEN = 3; +const SETTINGS_SUBITEM_NOISE_PENALTY = 200; + function inferCommandSubtype(command: CommandInfo): RootSearchSubtype { if (command.category === 'app') return 'app'; if (getQuickLinkIdFromCommandId(command.id)) return 'quicklink'; @@ -522,9 +529,14 @@ export function useLauncherCommandModel({ const commandCandidates = useMemo(() => { if (!hasSearchQuery || aiMode || rootBangState.mode !== 'none') return []; + // Individual settings (e.g. "Color Filters") are a fallback tier: skip them + // for very short queries where they'd flood the list, and let them through + // only once the user has typed enough to have a specific setting in mind. + const allowSettingsSubItems = searchQuery.trim().length >= SETTINGS_SUBITEM_MIN_QUERY_LEN; const searchableCommands = contextualCommands.filter((cmd) => cmd.id !== WEB_SEARCH_COMMAND_ID && - (!hiddenListOnlyCommandIds.has(cmd.id) || hasSearchQuery) + (!hiddenListOnlyCommandIds.has(cmd.id) || hasSearchQuery) && + (allowSettingsSubItems || !cmd.settingsSubItem) ); return rankCommands(searchableCommands, searchQuery, commandAliases) .map(({ command }) => { @@ -556,7 +568,11 @@ export function useLauncherCommandModel({ sourceQualityBoost: command.alwaysOnTop ? 80 : 0, freshnessBoost: 0, pathLocationBoost: 0, - noisePenalty: 0, + // Demote individual settings below real apps/commands/panes so they + // only surface as a fallback when nothing better matches. Weak + // (contains/subsequence) sub-item matches fall under the promotion + // floor entirely; strong (exact/prefix) ones still appear, lower down. + noisePenalty: command.settingsSubItem ? SETTINGS_SUBITEM_NOISE_PENALTY : 0, depthPenalty: 0, }, searchQuery, rootSearchRanking); }) diff --git a/src/renderer/src/utils/command-helpers.tsx b/src/renderer/src/utils/command-helpers.tsx index dfa5bcbc..7386d3a5 100644 --- a/src/renderer/src/utils/command-helpers.tsx +++ b/src/renderer/src/utils/command-helpers.tsx @@ -30,6 +30,10 @@ import { renderQuickLinkIconGlyph } from './quicklink-icons'; import { scoreRootSearchFields } from './root-search-ranking'; import { getTranslitVariant } from './transliterate'; +// Minimum query length before individual settings (e.g. "Color Filters") are +// considered. Keeps short, broad queries from filling up with system settings. +const SETTINGS_SUBITEM_MIN_QUERY_LEN = 3; + export interface LauncherAction { id: string; title: string; @@ -312,6 +316,12 @@ export function filterCommands( return null; } + // Individual settings (e.g. "Color Filters") are a fallback tier: skip + // them for very short queries where they'd flood the list. + if (cmd.settingsSubItem && normalizedQuery.length < SETTINGS_SUBITEM_MIN_QUERY_LEN) { + return null; + } + const primaryScore = computeCommandScore(normalizedQuery, queryTerms, title, subtitle, normalizedAlias, candidates, keywordTokens); const translitScore = translitVariant.isVariant ? computeCommandScore(translitVariant.query, transliteratedTerms, title, subtitle, normalizedAlias, candidates, keywordTokens) @@ -330,6 +340,12 @@ export function filterCommands( if (a.hasExactAliasMatch !== b.hasExactAliasMatch) { return Number(b.hasExactAliasMatch) - Number(a.hasExactAliasMatch); } + // Settings sub-items are a fallback tier — always sort them below real + // apps/commands/panes regardless of raw score, so they only show up once + // genuine matches are exhausted. + const aSub = Boolean(a.cmd.settingsSubItem); + const bSub = Boolean(b.cmd.settingsSubItem); + if (aSub !== bSub) return Number(aSub) - Number(bSub); if (b.score !== a.score) return b.score - a.score; return a.title.localeCompare(b.title); }); diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index 203a7699..945ccc3b 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -18,6 +18,10 @@ export interface CommandInfo { needsConfirmation?: boolean; /** Always shown at the top of the list, even during search. */ alwaysOnTop?: boolean; + /** True for an individual setting parsed from a pane's .searchTerms file + * (e.g. "Color Filters" inside Accessibility). Ranked as a low-priority + * search fallback so it doesn't crowd out apps, commands, and panes. */ + settingsSubItem?: boolean; browserMatchKind?: 'open-tab' | 'history' | 'search'; browserResultKind?: 'open-tab' | 'bookmark' | 'history' | 'search'; browserFaviconUrl?: string; From 7df1ce605abd0016e5c22e6661d7ff5cfbc44d66 Mon Sep 17 00:00:00 2001 From: shobhit99 Date: Wed, 24 Jun 2026 15:32:24 +0530 Subject: [PATCH 4/4] fix: address subagent review of settings sub-item fallback ranking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings from an independent review of the fallback-ranking change: - P2 (exact sub-item leapfrog): compareRootSearchCandidates ranks exact matches before finalScore, so an exactly-named settings sub-item could still jump above a fuzzily-matched real command despite the noise penalty. Add a strict fallback tier in the comparator — settings sub-items always sort below any other internal result. Placed after the organic-browser partition (keeping "internal beats browser") and before the exact/intent tiers; it's a clean binary partition so the comparator stays transitive. This also unifies the root-search path with the legacy filterCommands hard-tier. - P2 (settings tab flood): the Settings -> Extensions "System Settings" group listed every settings command, so the sub-items now flood that management UI. Exclude settingsSubItem entries from both the lookup map and the displayed list — only top-level panes are assignable there. - Add regression tests: a sub-item ranks below a real command even on an exact match, and a weak (subsequence) sub-item match is dropped below the promotion floor while a strong (exact) one is promoted. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/test-root-search-ranking.mjs | 60 ++++++++++++++++++- src/renderer/src/settings/ExtensionsTab.tsx | 6 +- src/renderer/src/utils/root-search-ranking.ts | 11 ++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/scripts/test-root-search-ranking.mjs b/scripts/test-root-search-ranking.mjs index f052f8a1..8d72db05 100644 --- a/scripts/test-root-search-ranking.mjs +++ b/scripts/test-root-search-ranking.mjs @@ -70,7 +70,7 @@ const { scoreRootSearchCandidate, scoreRootSearchFields, } = ranking; -const { assembleRootSearchSections } = sections; +const { assembleRootSearchSections, isRootResultPromotionCandidate } = sections; const now = Date.UTC(2026, 4, 17); @@ -1001,6 +1001,64 @@ test('weak browser history does not autocomplete', () => { assert.equal(getSharedRootCompletion(query, [history]), null); }); +test('a settings sub-item ranks below a real command even on an exact match', () => { + const query = 'twf'; + // Real extension command that only fuzzily matches the query. + const realCommand = candidate({ + query, + id: 'toggle-window-float', + title: 'Toggle Window Float', + subtype: 'extension-command', + source: 'command', + fields: [{ value: 'Toggle Window Float', kind: 'label' }], + }); + // Settings sub-item whose title matches the query EXACTLY, with the fallback + // noise penalty applied (as the launcher does for settingsSubItem commands). + const subItem = candidate({ + query, + id: 'twf-setting', + title: 'TWF', + subtype: 'system-command', + source: 'command', + noisePenalty: 200, + commandExtra: { settingsSubItem: true }, + fields: [{ value: 'TWF', kind: 'label' }], + }); + // Even though the sub-item is an exact match (which normally wins the + // exact-match tiebreaker), it must rank below the fuzzily-matched real command. + const ranked = rankRootSearchCandidates([subItem, realCommand]); + assert.deepEqual(ranked.map((c) => c.command.id), ['toggle-window-float', 'twf-setting']); +}); + +test('a weak settings sub-item match is dropped below the promotion floor', () => { + // Strong (exact) sub-item match survives the penalty and is promoted. + const strong = candidate({ + query: 'color filters', + id: 'color-filters', + title: 'Color filters', + subtype: 'system-command', + source: 'command', + noisePenalty: 200, + commandExtra: { settingsSubItem: true }, + fields: [{ value: 'Color filters', kind: 'label' }], + }); + assert.equal(isRootResultPromotionCandidate(strong, 'color filters'), true); + + // Weak (subsequence) sub-item match falls under the 500 floor and is dropped, + // so partial/fuzzy queries don't flood results with settings toggles. + const weak = candidate({ + query: 'clf', + id: 'color-filters', + title: 'Color filters', + subtype: 'system-command', + source: 'command', + noisePenalty: 200, + commandExtra: { settingsSubItem: true }, + fields: [{ value: 'Color filters', kind: 'label' }], + }); + assert.equal(isRootResultPromotionCandidate(weak, 'clf'), false); +}); + // All tests passed if we reach this point without throwing an error. // Using a ✓ here for consistency with the node:test output style. console.log('✓ All root-search-ranking tests passed'); diff --git a/src/renderer/src/settings/ExtensionsTab.tsx b/src/renderer/src/settings/ExtensionsTab.tsx index fcd69e5b..76d57de6 100644 --- a/src/renderer/src/settings/ExtensionsTab.tsx +++ b/src/renderer/src/settings/ExtensionsTab.tsx @@ -259,7 +259,7 @@ const ExtensionsTab: React.FC<{ map.set(`${INSTALLED_APPLICATIONS_NAME}/${cmd.id}`, cmd); continue; } - if (cmd.category === 'settings') { + if (cmd.category === 'settings' && !cmd.settingsSubItem) { map.set(`${SYSTEM_SETTINGS_NAME}/${cmd.id}`, cmd); } } @@ -391,7 +391,9 @@ const ExtensionsTab: React.FC<{ } const systemSettingsCommands = commands - .filter((cmd) => cmd.category === 'settings') + // Only the top-level panes are assignable here; individual settings + // (settingsSubItem) are a search-only fallback and would flood this list. + .filter((cmd) => cmd.category === 'settings' && !cmd.settingsSubItem) .sort((a, b) => a.title.localeCompare(b.title)); if (systemSettingsCommands.length > 0) { byExt.set(SYSTEM_SETTINGS_NAME, { diff --git a/src/renderer/src/utils/root-search-ranking.ts b/src/renderer/src/utils/root-search-ranking.ts index 6eec2570..a248e8b5 100644 --- a/src/renderer/src/utils/root-search-ranking.ts +++ b/src/renderer/src/utils/root-search-ranking.ts @@ -378,6 +378,17 @@ export function compareRootSearchCandidates(a: RootSearchCandidate, b: RootSearc const browserOrder = compareOrganicBrowserCandidates(a, b); if (browserOrder !== 0) return browserOrder; + // Settings sub-items (e.g. "Color Filters" inside Accessibility) are a strict + // fallback tier: they always rank below any other internal result — even on an + // exact match — so they never displace the app/command/pane the user is after. + // Placed after the organic-browser partition so the "internal beats browser" + // rule still holds, and before the exact-match/intent tiers (which would + // otherwise let an exactly-named sub-item leapfrog a fuzzily-matched command). + // This is a clean binary partition, so the comparator stays transitive. + const aSettingsSubItem = Boolean(a.command.settingsSubItem); + const bSettingsSubItem = Boolean(b.command.settingsSubItem); + if (aSettingsSubItem !== bSettingsSubItem) return aSettingsSubItem ? 1 : -1; + if (a.isProtectedIntentMatch !== b.isProtectedIntentMatch) { return a.isProtectedIntentMatch ? -1 : 1; }