-
Notifications
You must be signed in to change notification settings - Fork 0
Mirror: Better search UX (#5726) #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "kilo-code": patch | ||
| --- | ||
|
|
||
| Highlight matching characters in "Recommended models" and model search (thanks @bernaferrari!) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import React, { memo } from "react" | ||
| import { cn } from "@/lib/utils" | ||
|
|
||
| interface HighlightedTextProps { | ||
| text: string | ||
| matchingPositions?: Set<number> | ||
| className?: string | ||
| } | ||
|
|
||
| export const HighlightedText = memo(({ text, matchingPositions, className }: HighlightedTextProps) => { | ||
| if (!matchingPositions || matchingPositions.size === 0) { | ||
| return <span className={className}>{text}</span> | ||
| } | ||
|
|
||
| const parts: React.ReactNode[] = [] | ||
| let lastIndex = 0 | ||
|
|
||
| // Iterate through the string one character at a time or grouped | ||
| // Grouping is more efficient for React rendering | ||
| for (let i = 0; i < text.length; i++) { | ||
| const isMatch = matchingPositions.has(i) | ||
| const isLastMatch = i > 0 && matchingPositions.has(i - 1) | ||
|
|
||
| if (isMatch !== isLastMatch) { | ||
| // specific transition, push previous chunk | ||
| if (i > lastIndex) { | ||
| const chunk = text.slice(lastIndex, i) | ||
| parts.push( | ||
| isLastMatch ? ( | ||
| <span key={lastIndex} className="font-bold text-vscode-textLink-foreground"> | ||
| {chunk} | ||
| </span> | ||
| ) : ( | ||
| <span key={lastIndex}>{chunk}</span> | ||
| ), | ||
| ) | ||
| } | ||
| lastIndex = i | ||
| } | ||
| } | ||
|
|
||
| // Push trailing chunk | ||
| if (lastIndex < text.length) { | ||
| const chunk = text.slice(lastIndex) | ||
| const isMatch = matchingPositions.has(text.length - 1) | ||
| parts.push( | ||
| isMatch ? ( | ||
| <span key={lastIndex} className="font-bold text-vscode-textLink-foreground"> | ||
| {chunk} | ||
| </span> | ||
| ) : ( | ||
| <span key={lastIndex}>{chunk}</span> | ||
| ), | ||
| ) | ||
| } | ||
|
Comment on lines
+20
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for splitting the text into highlighted and non-highlighted parts can be simplified. The current implementation uses a |
||
|
|
||
| return <span className={cn("truncate", className)}>{parts}</span> | ||
| }) | ||
|
|
||
| HighlightedText.displayName = "HighlightedText" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { useRooPortal } from "./hooks/useRooPortal" | |
| import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui" | ||
| import { StandardTooltip } from "@/components/ui" | ||
| import { IconProps } from "@radix-ui/react-icons/dist/types" // kilocode_change | ||
| import { HighlightedText } from "./highlighted-text" // kilocode_change | ||
|
|
||
| export enum DropdownOptionType { | ||
| ITEM = "item", | ||
|
|
@@ -26,6 +27,7 @@ export interface DropdownOption { | |
| disabled?: boolean | ||
| type?: DropdownOptionType | ||
| pinned?: boolean | ||
| matchingPositions?: Set<number> // kilocode_change | ||
| } | ||
|
|
||
| export interface SelectDropdownProps { | ||
|
|
@@ -134,26 +136,31 @@ export const SelectDropdown = React.memo( | |
|
|
||
| // Filter options based on search value using memoized Fzf instance | ||
| const filteredOptions = React.useMemo(() => { | ||
| // If search is disabled or no search value, return all options without filtering | ||
| if (disableSearch || !searchValue) return options | ||
| // Get fuzzy matching items AND their positions | ||
| const matchingResults = fzfInstance.find(searchValue) | ||
| const matchMap = new Map(matchingResults.map((r) => [r.item.original.value, r.positions])) | ||
|
|
||
| // Get fuzzy matching items - only perform search if we have a search value | ||
| const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) | ||
|
|
||
| // Always include separators, shortcuts, and labels | ||
| return options.filter((option) => { | ||
| // Always include separators, shortcuts, and labels, and matched items | ||
| return options.reduce((acc, option) => { | ||
| if ( | ||
| option.type === DropdownOptionType.SEPARATOR || | ||
| option.type === DropdownOptionType.SHORTCUT || | ||
| option.type === DropdownOptionType.LABEL // kilocode_change: include LABEL in filtered results | ||
| ) { | ||
| return true | ||
| acc.push(option) | ||
| return acc | ||
| } | ||
|
|
||
| // Include if it's in the matching items | ||
| return matchingItems.some((item) => item.value === option.value) | ||
| }) | ||
| }, [options, searchValue, fzfInstance, disableSearch]) | ||
| const positions = matchMap.get(option.value) | ||
| if (positions) { | ||
| // Clone option to add matching positions without mutating original | ||
| acc.push({ ...option, matchingPositions: positions }) | ||
| } | ||
|
|
||
| return acc | ||
| }, [] as DropdownOption[]) | ||
|
Comment on lines
+139
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Highlight indices mismatch Match positions are computed against searchStr (label+value) but later used to highlight only option.label, so queries that match only the value (or later parts of searchStr) can produce no/incorrect highlighting. Agent Prompt
|
||
| }, [options, searchValue, fzfInstance]) | ||
|
Comment on lines
+139
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Unmarked filteredoptions logic change The new fuzzy-match/position mapping logic in select-dropdown.tsx is not wrapped/annotated with kilocode_change markers even though it modifies upstream-shared webview-ui/ code. This can cause ambiguity and merge conflicts when syncing from upstream. Agent Prompt
|
||
|
|
||
| // Group options by type and handle separators and labels | ||
| // kilocode_change start: improved handling for section labels | ||
|
|
@@ -374,7 +381,12 @@ export const SelectDropdown = React.memo( | |
| )} | ||
| /> | ||
| <div className="flex-1"> | ||
| <div>{option.label}</div> | ||
| <div className="flex items-center"> | ||
| <HighlightedText | ||
| text={option.label} | ||
| matchingPositions={option.matchingPositions} | ||
| /> | ||
| </div> | ||
| {option.description && ( | ||
| <div className="text-[11px] opacity-50 mt-0.5"> | ||
| {option.description} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. highlightedtext missing kilocode_change header
📘 Rule violation⛯ ReliabilityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools