diff --git a/CHANGELOG.md b/CHANGELOG.md index ae41135f5ca..fd527a4a0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai - Memory/FTS: add configurable trigram tokenization plus short-CJK substring fallback so memory search can find Chinese, Japanese, and Korean text without breaking mixed long-and-short queries. Thanks @carrotRakko. - Hooks/config: accept runtime channel plugin ids in `hooks.mappings[].channel` (for example `feishu`) instead of rejecting non-core channels during config validation. (#56226) Thanks @AiKrai001. - TUI/chat: keep optimistic outbound user messages visible during active runs by deferring local-run binding until the first gateway chat event reveals the real run id, preventing premature history reloads from wiping pending local sends. (#54722) Thanks @seanturner001. +- TUI/model picker: keep searchable `/model` and `/models` input mode from hijacking `j`/`k` as navigation keys, and harden width bounds under `m`-filtered model lists so search no longer crashes on long rows. (#30156) Thanks @briannicholls. ## 2026.3.28 diff --git a/src/tui/components/searchable-select-list.test.ts b/src/tui/components/searchable-select-list.test.ts index 76c6f39243a..35f767af0b9 100644 --- a/src/tui/components/searchable-select-list.test.ts +++ b/src/tui/components/searchable-select-list.test.ts @@ -124,6 +124,34 @@ describe("SearchableSelectList", () => { } }); + it("keeps model-search rows within width when filtering by m", () => { + const items = [ + { value: "minimax-cn/MiniMax-M2", label: "minimax-cn/MiniMax-M2", description: "MiniMax M2" }, + { + value: "minimax-cn/MiniMax-M2.1", + label: "minimax-cn/MiniMax-M2.1", + description: "MiniMax M2.1", + }, + { + value: "mistral/codestral-latest", + label: "mistral/codestral-latest", + description: "Codestral", + }, + { + value: "mistral/devstral-medium-latest", + label: "mistral/devstral-medium-latest", + description: "Devstral Medium", + }, + ]; + const list = new SearchableSelectList(items, 9, ansiHighlightTheme); + typeInput(list, "m"); + + const width = 209; + for (const line of list.render(width)) { + expect(visibleWidth(line)).toBeLessThanOrEqual(width); + } + }); + it("ignores ANSI escape codes in search matching", () => { const items = [ { value: "styled", label: "\u001b[32mopenai/gpt-4\u001b[0m", description: "Styled label" }, @@ -266,6 +294,24 @@ describe("SearchableSelectList", () => { expect(list.getSelectedItem()?.value).toBe("anthropic/claude-3-sonnet"); }); + it("types j and k into search input instead of intercepting as vim navigation", () => { + const items = [ + { value: "alpha", label: "alpha" }, + { value: "kilo", label: "kilo" }, + { value: "juliet", label: "juliet" }, + ]; + + const jList = new SearchableSelectList(items, 5, mockTheme); + jList.handleInput("j"); + expect(jList.getSelectedItem()?.value).toBe("juliet"); + expect(stripAnsi(jList.render(80)[0] ?? "")).toContain("j"); + + const kList = new SearchableSelectList(items, 5, mockTheme); + kList.handleInput("k"); + expect(kList.getSelectedItem()?.value).toBe("kilo"); + expect(stripAnsi(kList.render(80)[0] ?? "")).toContain("k"); + }); + it("calls onSelect when enter is pressed", () => { const list = new SearchableSelectList(testItems, 5, mockTheme); let selectedValue: string | undefined; diff --git a/src/tui/components/searchable-select-list.ts b/src/tui/components/searchable-select-list.ts index e640e3c39d5..43db6d4d990 100644 --- a/src/tui/components/searchable-select-list.ts +++ b/src/tui/components/searchable-select-list.ts @@ -330,24 +330,14 @@ export class SearchableSelectList implements Component { return; } - const allowVimNav = !this.searchInput.getValue().trim(); - // Navigation keys - if ( - matchesKey(keyData, "up") || - matchesKey(keyData, "ctrl+p") || - (allowVimNav && keyData === "k") - ) { + if (matchesKey(keyData, "up") || matchesKey(keyData, "ctrl+p")) { this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.notifySelectionChange(); return; } - if ( - matchesKey(keyData, "down") || - matchesKey(keyData, "ctrl+n") || - (allowVimNav && keyData === "j") - ) { + if (matchesKey(keyData, "down") || matchesKey(keyData, "ctrl+n")) { this.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1); this.notifySelectionChange(); return;