@@ -578,19 +578,32 @@ export function scoreSectionItems<T>(
578578}
579579
580580/**
581- * Rank offset added to every matched action. Actions are the palette's few
581+ * Rank offset added to a matched action. Actions are the palette's few
582582 * runnable verbs, so a matched action outranks entity rows of the same match
583583 * quality — a name-matched action beats name-matched entities, a
584584 * keyword-matched action beats other secondary-text matches — while the
585585 * half-tier offset deliberately cannot bridge into the next tier up
586- * ({@link SECTION_MATCH_TIER}, {@link PAGE_MATCH_TIER}).
586+ * ({@link SECTION_MATCH_TIER}, {@link PAGE_MATCH_TIER}). A name hit that
587+ * starts mid-word ("h" in "New chat") is NOT the same quality as the
588+ * word-start matches the offset would leapfrog, so it forgoes the bias.
587589 */
588590export const ACTION_MATCH_BIAS = 500_000
589591
592+ /**
593+ * Whether a match begins where a word begins — the string start, right after a
594+ * separator, or at a camelCase hump. The empty query (no positions) counts as
595+ * a word start.
596+ */
597+ function isWordStartMatch ( text : string , positions : readonly number [ ] ) : boolean {
598+ if ( positions . length === 0 ) return true
599+ return isHardBoundary ( text . toLowerCase ( ) , positions [ 0 ] ) || isCamelBoundary ( text , positions [ 0 ] )
600+ }
601+
590602/**
591603 * Scores actions by visible name before falling back to their keywords.
592- * Every match is lifted by {@link ACTION_MATCH_BIAS}; a query listed in the
593- * action's `exactQueries` ranks it like a page row instead.
604+ * Word-start matches are lifted by {@link ACTION_MATCH_BIAS}; a mid-word name
605+ * hit keeps its honest score so word-start entity matches outrank it; a query
606+ * listed in the action's `exactQueries` ranks it like a page row instead.
594607 */
595608export function scoreActions (
596609 actions : ActionItem [ ] ,
@@ -606,10 +619,15 @@ export function scoreActions(
606619 search ,
607620 ( action ) => `${ toSearchToken ( action . name ) } ${ action . keywords ?? '' } ` ,
608621 maxResults
609- ) . map ( ( { item, score } ) => ( {
610- item,
611- score : item . exactQueries ?. includes ( query ) ? PAGE_MATCH_TIER : score + ACTION_MATCH_BIAS ,
612- } ) )
622+ ) . map ( ( { item, score } ) => {
623+ if ( item . exactQueries ?. includes ( query ) ) return { item, score : PAGE_MATCH_TIER }
624+ /* Section-lifted rows (the query IS the group label) keep the bias
625+ wholesale — only plain name-tier scores are quality-checked. */
626+ const byName = fuzzyMatch ( item . name , query )
627+ const midWordNameMatch =
628+ score < SECTION_MATCH_TIER && byName . matched && ! isWordStartMatch ( item . name , byName . positions )
629+ return { item, score : midWordNameMatch ? score : score + ACTION_MATCH_BIAS }
630+ } )
613631}
614632
615633/**
0 commit comments