mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(vscode): address chat search review findings from testing round
High: visible tool content (e.g. a todowrite checklist) could be
highlighted in the DOM while the counter still reported "No results"
and navigation was disabled. toolText() previously only indexed
state.title for non-bash tools, independently of what
transcript-search-highlight.ts actually scans in the DOM. Rewrote it
to recursively collect every string leaf from state.input/state.metadata
plus state.output for any tool, so matching and highlighting draw from
one canonical, much more comprehensive notion of "the tool's text"
instead of two divergent ones. read/glob/grep/list remain excluded --
kilo-ui's context-tool-results.tsx confirmed they never render raw
input/output text, even expanded, so including it there would
reintroduce the same class of mismatch for those tools.
High: search only covered the initially-loaded message page (80
messages), silently missing older history in long sessions. MessageList
now auto-requests older pages (session.loadOlderMessages()) while a
search is active with a non-empty query, looping via reactivity on
hasOlderMessages()/loadingOlderMessages() until history is exhausted.
A new searchingHistory state surfaces this to the widget, which shows
"Searching earlier messages..." and withholds a final "No results"
until the whole session has actually been searched (a live match count
still updates progressively as pages load).
Medium: whole-word matching used plain \b, which only treats ASCII
letters/digits/underscore as word characters and silently breaks for
Cyrillic, Arabic, CJK, and similar text. Replaced with Unicode-aware
boundary lookarounds using \p{L}/\p{M}/\p{N} property escapes, with the
`u` flag applied to every compiled pattern.
New chat.search.searchingHistory i18n key added across all 20 locales.
(cherry picked from commit 47d4ebd73b)
This commit is contained in:
committed by
marius-kilocode
parent
3d28bfc50e
commit
f96771d302
@@ -61,7 +61,7 @@ import {
|
||||
parseProviderAuthError,
|
||||
unwrapError,
|
||||
} from "../../utils/errorUtils"
|
||||
import type { Part, QuestionRequest, SuggestionRequest } from "../../types/messages"
|
||||
import type { Part, QuestionRequest, SuggestionRequest, ToolState } from "../../types/messages"
|
||||
|
||||
interface MessageListProps {
|
||||
onSelectSession?: (id: string) => void
|
||||
@@ -210,19 +210,37 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
|
||||
// Extracts only the text kilo-ui's tool renderers actually put on screen —
|
||||
// matched field-by-field rather than reading `state.title` generically.
|
||||
// The bash/shell renderer never shows `state.title` (its header is a
|
||||
// static "Shell" label); the visible command/description come from
|
||||
// `state.input` instead, and its output is shown as plain scrollable
|
||||
// text. Other tools' `state.output` is typically internal data (file
|
||||
// contents, JSON) that isn't rendered inline, so including it — or bash's
|
||||
// unused `title` — produces search matches with no corresponding
|
||||
// highlight, which is what made navigation appear to skip past matches.
|
||||
// Uses one canonical extraction for both counting/navigation (this
|
||||
// function) and highlighting (transcript-search-highlight.ts scans the
|
||||
// rendered DOM) rather than two independently maintained notions of "the
|
||||
// tool's text" — a hand-picked field list here previously missed content
|
||||
// that's genuinely always on screen (e.g. a todowrite checklist's item
|
||||
// text), so a visible match could be highlighted in the DOM while the
|
||||
// counter still reported "No results" and navigation was disabled.
|
||||
//
|
||||
// read/glob/grep/list are the one confirmed exception: kilo-ui always
|
||||
// collapses them into a context-group summary (context-tool-results.tsx)
|
||||
// that never renders raw input/output text, even expanded — including
|
||||
// that text here would count matches with no corresponding highlight,
|
||||
// the same class of bug this rewrite fixes for every other tool.
|
||||
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
|
||||
|
||||
function toolText(part: Part & { type: "tool" }): string[] {
|
||||
const state = part.state
|
||||
if (state.status === "running") return state.title ? [state.title] : []
|
||||
if (state.status === "error") return state.error ? [state.error] : []
|
||||
if (state.status !== "completed") return []
|
||||
if (part.tool !== "bash") return state.title ? [state.title] : []
|
||||
if (CONTEXT_GROUP_TOOLS.has(part.tool)) return state.title ? [state.title] : []
|
||||
if (part.tool === "bash") return bashText(state)
|
||||
const chunks: string[] = []
|
||||
if (state.title) chunks.push(state.title)
|
||||
collectStrings(state.input, chunks)
|
||||
collectStrings(state.metadata, chunks)
|
||||
if (typeof state.output === "string" && state.output) chunks.push(state.output)
|
||||
return chunks
|
||||
}
|
||||
|
||||
function bashText(state: Extract<ToolState, { status: "completed" }>) {
|
||||
const input = state.input as { command?: string; description?: string } | undefined
|
||||
const metadata = state.metadata as { command?: string; description?: string } | undefined
|
||||
const command = input?.command ?? metadata?.command
|
||||
@@ -238,6 +256,27 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
return chunks
|
||||
}
|
||||
|
||||
// Recursively collects every string leaf value from a tool's `input`/
|
||||
// `metadata` (JSON-like objects/arrays of unknown shape), so nested
|
||||
// rendered text — a todo item's `content`, a question's `question` text,
|
||||
// a skill's `name` — is included without hand-modeling each tool's shape.
|
||||
function collectStrings(value: unknown, out: string[], depth = 0): void {
|
||||
if (depth > 4 || value === undefined || value === null) return
|
||||
if (typeof value === "string") {
|
||||
if (value) out.push(value)
|
||||
return
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) collectStrings(item, out, depth + 1)
|
||||
return
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
for (const key of Object.keys(value as Record<string, unknown>)) {
|
||||
collectStrings((value as Record<string, unknown>)[key], out, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildPattern(query: string, matchCase: boolean, wholeWord: boolean, regex: boolean): RegExp | undefined {
|
||||
if (!query) return undefined
|
||||
try {
|
||||
@@ -246,9 +285,14 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
pattern = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
if (wholeWord) {
|
||||
pattern = `\\b(?:${pattern})\\b`
|
||||
// Unicode-aware boundary: plain `\b` only treats ASCII letters/
|
||||
// digits/underscore as "word" characters, so it silently breaks
|
||||
// whole-word matching for Cyrillic, Arabic, CJK, and other non-ASCII
|
||||
// text. `\p{L}`/`\p{M}`/`\p{N}` (letters/marks/numbers) require the
|
||||
// `u` flag, applied below for every pattern, not just this one.
|
||||
pattern = `(?<![\\p{L}\\p{M}\\p{N}_])(?:${pattern})(?![\\p{L}\\p{M}\\p{N}_])`
|
||||
}
|
||||
return new RegExp(pattern, matchCase ? "g" : "gi")
|
||||
return new RegExp(pattern, matchCase ? "gu" : "giu")
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
@@ -268,6 +312,24 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
search.setInvalid(search.active() && !!q && search.regex() && !pattern())
|
||||
})
|
||||
|
||||
// Sessions only load the most recent page (session.tsx's MESSAGE_PAGE_LIMIT)
|
||||
// up front; matches() only ever sees currently-loaded rows(). Without this,
|
||||
// an active search would silently miss everything in older, not-yet-loaded
|
||||
// history — undermining the main "find something in a long session" use
|
||||
// case. While a query is active, keep requesting older pages until there
|
||||
// either aren't any more or the search is no longer active; each
|
||||
// completed load feeds back into hasOlderMessages()/loadingOlderMessages(),
|
||||
// both tracked here, so this effect naturally re-fires and continues the
|
||||
// chain without an explicit loop. searchingHistory (surfaced to the
|
||||
// widget) stays true for that whole stretch, so "No results"/a final
|
||||
// count aren't shown until the whole session has actually been searched.
|
||||
createEffect(() => {
|
||||
const searching = search.active() && !!search.query() && session.hasOlderMessages()
|
||||
search.setSearchingHistory(searching)
|
||||
if (!searching || session.loadingOlderMessages()) return
|
||||
session.loadOlderMessages()
|
||||
})
|
||||
|
||||
const matches = createMemo(() => {
|
||||
const p = pattern()
|
||||
if (!p) return []
|
||||
|
||||
@@ -116,7 +116,12 @@ export const TranscriptSearch: Component = () => {
|
||||
<Show when={search.invalid()}>
|
||||
<span data-slot="transcript-search-error">{language.t("chat.search.invalidRegex")}</span>
|
||||
</Show>
|
||||
<Show when={!search.invalid() && search.query().length > 0 && search.count() === 0}>
|
||||
<Show when={!search.invalid() && search.searchingHistory() && search.count() === 0}>
|
||||
<span data-slot="transcript-search-empty">{language.t("chat.search.searchingHistory")}</span>
|
||||
</Show>
|
||||
<Show
|
||||
when={!search.invalid() && !search.searchingHistory() && search.query().length > 0 && search.count() === 0}
|
||||
>
|
||||
<span data-slot="transcript-search-empty">{language.t("chat.search.noResults")}</span>
|
||||
</Show>
|
||||
<Show when={!search.invalid() && search.count() > 0}>
|
||||
|
||||
@@ -32,6 +32,12 @@ interface TranscriptSearchContextValue {
|
||||
* indistinguishable from a plain "no matches". */
|
||||
invalid: Accessor<boolean>
|
||||
setInvalid: (value: boolean) => void
|
||||
/** True while MessageList is auto-loading older message pages to search
|
||||
* them too — the session only loads the most recent page by default, so
|
||||
* without this the widget would report "No results"/a final count while
|
||||
* older history hadn't been searched yet. */
|
||||
searchingHistory: Accessor<boolean>
|
||||
setSearchingHistory: (value: boolean) => void
|
||||
}
|
||||
|
||||
const TranscriptSearchContext = createContext<TranscriptSearchContextValue>()
|
||||
@@ -46,6 +52,7 @@ export const TranscriptSearchProvider: ParentComponent = (props) => {
|
||||
const [count, setCount] = createSignal(0)
|
||||
const [jump, setJump] = createSignal(0)
|
||||
const [invalid, setInvalid] = createSignal(false)
|
||||
const [searchingHistory, setSearchingHistory] = createSignal(false)
|
||||
|
||||
return (
|
||||
<TranscriptSearchContext.Provider
|
||||
@@ -68,6 +75,8 @@ export const TranscriptSearchProvider: ParentComponent = (props) => {
|
||||
requestJump: () => setJump((n) => n + 1),
|
||||
invalid,
|
||||
setInvalid,
|
||||
searchingHistory,
|
||||
setSearchingHistory,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
+1
@@ -1845,4 +1845,5 @@ export const dict = {
|
||||
"chat.search.close": "إغلاق البحث",
|
||||
"chat.search.invalidRegex": "تعبير عادي غير صالح",
|
||||
"chat.search.noResults": "لا توجد نتائج",
|
||||
"chat.search.searchingHistory": "جارٍ البحث في الرسائل السابقة…",
|
||||
}
|
||||
|
||||
+1
@@ -1894,4 +1894,5 @@ export const dict = {
|
||||
"chat.search.close": "Fechar pesquisa",
|
||||
"chat.search.invalidRegex": "Expressão regular inválida",
|
||||
"chat.search.noResults": "Nenhum resultado",
|
||||
"chat.search.searchingHistory": "Pesquisando mensagens anteriores…",
|
||||
}
|
||||
|
||||
+1
@@ -1886,4 +1886,5 @@ export const dict = {
|
||||
"chat.search.close": "Zatvori pretragu",
|
||||
"chat.search.invalidRegex": "Nevažeći regularni izraz",
|
||||
"chat.search.noResults": "Nema rezultata",
|
||||
"chat.search.searchingHistory": "Pretraživanje ranijih poruka…",
|
||||
}
|
||||
|
||||
+1
@@ -1878,4 +1878,5 @@ export const dict = {
|
||||
"chat.search.close": "Luk søgning",
|
||||
"chat.search.invalidRegex": "Ugyldigt regulært udtryk",
|
||||
"chat.search.noResults": "Ingen resultater",
|
||||
"chat.search.searchingHistory": "Søger i tidligere beskeder…",
|
||||
}
|
||||
|
||||
@@ -1916,4 +1916,5 @@ export const dict = {
|
||||
"chat.search.close": "Suche schließen",
|
||||
"chat.search.invalidRegex": "Ungültiger regulärer Ausdruck",
|
||||
"chat.search.noResults": "Keine Ergebnisse",
|
||||
"chat.search.searchingHistory": "Frühere Nachrichten werden durchsucht…",
|
||||
} satisfies Partial<Record<Keys, string>>
|
||||
|
||||
@@ -1873,4 +1873,5 @@ export const dict = {
|
||||
"chat.search.close": "Close search",
|
||||
"chat.search.invalidRegex": "Invalid regular expression",
|
||||
"chat.search.noResults": "No results",
|
||||
"chat.search.searchingHistory": "Searching earlier messages…",
|
||||
}
|
||||
|
||||
+1
@@ -1902,4 +1902,5 @@ export const dict = {
|
||||
"chat.search.close": "Cerrar búsqueda",
|
||||
"chat.search.invalidRegex": "Expresión regular no válida",
|
||||
"chat.search.noResults": "Sin resultados",
|
||||
"chat.search.searchingHistory": "Buscando en mensajes anteriores…",
|
||||
}
|
||||
|
||||
+1
@@ -1926,4 +1926,5 @@ export const dict = {
|
||||
"chat.search.close": "Fermer la recherche",
|
||||
"chat.search.invalidRegex": "Expression régulière non valide",
|
||||
"chat.search.noResults": "Aucun résultat",
|
||||
"chat.search.searchingHistory": "Recherche dans les messages précédents…",
|
||||
}
|
||||
|
||||
+1
@@ -1811,4 +1811,5 @@ export const dict = {
|
||||
"chat.search.close": "Chiudi ricerca",
|
||||
"chat.search.invalidRegex": "Espressione regolare non valida",
|
||||
"chat.search.noResults": "Nessun risultato",
|
||||
"chat.search.searchingHistory": "Ricerca nei messaggi precedenti…",
|
||||
} as const
|
||||
|
||||
+1
@@ -1871,4 +1871,5 @@ export const dict = {
|
||||
"chat.search.close": "検索を閉じる",
|
||||
"chat.search.invalidRegex": "正規表現が無効です",
|
||||
"chat.search.noResults": "見つかりませんでした",
|
||||
"chat.search.searchingHistory": "以前のメッセージを検索しています…",
|
||||
}
|
||||
|
||||
+1
@@ -1853,4 +1853,5 @@ export const dict = {
|
||||
"chat.search.close": "검색 닫기",
|
||||
"chat.search.invalidRegex": "정규식이 잘못되었습니다",
|
||||
"chat.search.noResults": "검색 결과 없음",
|
||||
"chat.search.searchingHistory": "이전 메시지를 검색하는 중…",
|
||||
}
|
||||
|
||||
+1
@@ -1913,4 +1913,5 @@ export const dict = {
|
||||
"chat.search.close": "Zoeken sluiten",
|
||||
"chat.search.invalidRegex": "Ongeldige reguliere expressie",
|
||||
"chat.search.noResults": "Geen resultaten",
|
||||
"chat.search.searchingHistory": "Eerdere berichten doorzoeken…",
|
||||
}
|
||||
|
||||
+1
@@ -1871,4 +1871,5 @@ export const dict = {
|
||||
"chat.search.close": "Lukk søk",
|
||||
"chat.search.invalidRegex": "Ugyldig regulært uttrykk",
|
||||
"chat.search.noResults": "Ingen resultater",
|
||||
"chat.search.searchingHistory": "Søker i tidligere meldinger…",
|
||||
} satisfies Partial<Record<Keys, string>>
|
||||
|
||||
+1
@@ -1884,4 +1884,5 @@ export const dict = {
|
||||
"chat.search.close": "Zamknij wyszukiwanie",
|
||||
"chat.search.invalidRegex": "Nieprawidłowe wyrażenie regularne",
|
||||
"chat.search.noResults": "Brak wyników",
|
||||
"chat.search.searchingHistory": "Wyszukiwanie we wcześniejszych wiadomościach…",
|
||||
}
|
||||
|
||||
+1
@@ -1882,4 +1882,5 @@ export const dict = {
|
||||
"chat.search.close": "Закрыть поиск",
|
||||
"chat.search.invalidRegex": "Недопустимое регулярное выражение",
|
||||
"chat.search.noResults": "Нет результатов",
|
||||
"chat.search.searchingHistory": "Поиск в более ранних сообщениях…",
|
||||
}
|
||||
|
||||
+1
@@ -1851,4 +1851,5 @@ export const dict = {
|
||||
"chat.search.close": "ปิดการค้นหา",
|
||||
"chat.search.invalidRegex": "นิพจน์ทั่วไปไม่ถูกต้อง",
|
||||
"chat.search.noResults": "ไม่มีผลลัพธ์",
|
||||
"chat.search.searchingHistory": "กำลังค้นหาข้อความก่อนหน้า…",
|
||||
}
|
||||
|
||||
+1
@@ -1900,4 +1900,5 @@ export const dict = {
|
||||
"chat.search.close": "Aramayı kapat",
|
||||
"chat.search.invalidRegex": "Geçersiz normal ifade",
|
||||
"chat.search.noResults": "Sonuç yok",
|
||||
"chat.search.searchingHistory": "Önceki mesajlarda aranıyor…",
|
||||
}
|
||||
|
||||
+1
@@ -1897,4 +1897,5 @@ export const dict = {
|
||||
"chat.search.close": "Закрити пошук",
|
||||
"chat.search.invalidRegex": "Недійсний регулярний вираз",
|
||||
"chat.search.noResults": "Немає результатів",
|
||||
"chat.search.searchingHistory": "Пошук у попередніх повідомленнях…",
|
||||
}
|
||||
|
||||
+1
@@ -1800,4 +1800,5 @@ export const dict = {
|
||||
"chat.search.close": "关闭搜索",
|
||||
"chat.search.invalidRegex": "正则表达式无效",
|
||||
"chat.search.noResults": "无结果",
|
||||
"chat.search.searchingHistory": "正在搜索更早的消息…",
|
||||
} satisfies Partial<Record<Keys, string>>
|
||||
|
||||
+1
@@ -1806,4 +1806,5 @@ export const dict = {
|
||||
"chat.search.close": "關閉搜尋",
|
||||
"chat.search.invalidRegex": "規則運算式無效",
|
||||
"chat.search.noResults": "無結果",
|
||||
"chat.search.searchingHistory": "正在搜尋較早的訊息…",
|
||||
} satisfies Partial<Record<Keys, string>>
|
||||
|
||||
Reference in New Issue
Block a user