From ab798d80b2fdad773626ff06e45d5d10d33d6762 Mon Sep 17 00:00:00 2001 From: Scott Jones Date: Sun, 26 Jul 2026 14:25:31 -0400 Subject: [PATCH 1/2] Rank installed apps above equally matching menu entries Searching the menu for an installed app buried it: "brave" listed Setup > Defaults > Browser, Install > Browser and Remove > Browser ahead of the Brave app itself. All four are exact label matches scoring 0, so the tiebreak falls to declaration order, and mergeAppRows appends app rows after every static item. Bias app rows ahead of menu entries that match equally well. The bias is smaller than the gap between match tiers, so a menu entry that matches the query better still sorts first. Co-Authored-By: Claude Opus 5 (1M context) --- shell/plugins/menu/MenuModel.js | 6 ++++++ test/shell.d/menu-test.sh | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/shell/plugins/menu/MenuModel.js b/shell/plugins/menu/MenuModel.js index 12ef3c0c..3de9b8ee 100644 --- a/shell/plugins/menu/MenuModel.js +++ b/shell/plugins/menu/MenuModel.js @@ -309,6 +309,12 @@ function searchScore(items, entry, query) { else if (descriptionTextMatches(needle, descriptionText)) score = 60 if (entry.kind === "menu" || entry.kind === "link") score -= 2 + // mergeAppRows appends app rows after every menu item, so an app always + // loses the order tiebreak below to a menu entry that matches just as well. + // Searching an installed app's name should launch it, not offer to install + // or remove it. The bias stays under the 10-point gap between match tiers, + // so a menu entry that matches better still sorts first. + if (entry.kind === "app") score -= 5 return score * 1000 + depthFor(items, entry.id) * 25 + entry.order } diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index 52cedb92..8f838702 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -109,6 +109,26 @@ assertDeepEqual( const defaultItems = menu.parseMenuJsonc(defaultMenuJsonc) const defaultById = Object.fromEntries(defaultItems.map(item => [item.id, item])) + +// App rows land after every static item, so ranking has to survive the real +// menu's item count: with hundreds of entries ahead of them, the order +// tiebreak alone buries an installed app under Install and Remove. +const rankBase = menu.mergeMenuSources(defaultItems, []) +const ranked = menu.mergeAppRows(rankBase.items, rankBase.itemOrder, [ + { id: 'apps.brave', parent: 'apps', kind: 'app', label: 'Brave', description: '', aliases: [] }, + { id: 'apps.fontforge', parent: 'apps', kind: 'app', label: 'FontForge', description: '', aliases: [] } +]) +const rankScore = (id, query) => menu.searchScore(ranked.items, ranked.items[id], query) +assert( + ['install.browser.brave', 'remove.browser.brave', 'setup.default.browser.brave'].every( + id => rankScore('apps.brave', 'brave') < rankScore(id, 'brave') + ), + 'menu ranks an installed app above menu entries matching the query equally well' +) +assert( + rankScore('style.font', 'font') < rankScore('apps.fontforge', 'font'), + 'menu keeps a better-matching menu entry above a weaker app match' +) const triggerItems = defaultItems.filter(item => item.parent === 'trigger') assertEqual( triggerItems[0].id, From 0c6b07e2c492fad4a22d05cf1b634dbd9f0d450b Mon Sep 17 00:00:00 2001 From: Scott Jones Date: Sun, 26 Jul 2026 14:34:43 -0400 Subject: [PATCH 2/2] Trim ranking comments Say it once and match mergeAppRows: app rows sort after all menu items, not interleaved among them. Co-Authored-By: Claude Opus 5 (1M context) --- shell/plugins/menu/MenuModel.js | 7 ++----- test/shell.d/menu-test.sh | 5 ++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/shell/plugins/menu/MenuModel.js b/shell/plugins/menu/MenuModel.js index 3de9b8ee..556c1d77 100644 --- a/shell/plugins/menu/MenuModel.js +++ b/shell/plugins/menu/MenuModel.js @@ -309,11 +309,8 @@ function searchScore(items, entry, query) { else if (descriptionTextMatches(needle, descriptionText)) score = 60 if (entry.kind === "menu" || entry.kind === "link") score -= 2 - // mergeAppRows appends app rows after every menu item, so an app always - // loses the order tiebreak below to a menu entry that matches just as well. - // Searching an installed app's name should launch it, not offer to install - // or remove it. The bias stays under the 10-point gap between match tiers, - // so a menu entry that matches better still sorts first. + // App rows sort after all menu items, so they lose the tiebreak below to an + // equal match. Outrank those, but stay inside the tier so better ones win. if (entry.kind === "app") score -= 5 return score * 1000 + depthFor(items, entry.id) * 25 + entry.order diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index 8f838702..1035f9b6 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -110,9 +110,8 @@ assertDeepEqual( const defaultItems = menu.parseMenuJsonc(defaultMenuJsonc) const defaultById = Object.fromEntries(defaultItems.map(item => [item.id, item])) -// App rows land after every static item, so ranking has to survive the real -// menu's item count: with hundreds of entries ahead of them, the order -// tiebreak alone buries an installed app under Install and Remove. +// Needs the real menu: app rows sort after all menu items, and only at that +// item count does the order tiebreak alone bury an installed app. const rankBase = menu.mergeMenuSources(defaultItems, []) const ranked = menu.mergeAppRows(rankBase.items, rankBase.itemOrder, [ { id: 'apps.brave', parent: 'apps', kind: 'app', label: 'Brave', description: '', aliases: [] },