From 5316e8b6a2c82bbe3847c2de60342f4e6418f73a Mon Sep 17 00:00:00 2001 From: wenyuan <49969025+wenyuanw@users.noreply.github.com> Date: Fri, 29 May 2026 00:24:28 +0800 Subject: [PATCH] feat(tui): support j/k navigation in non-search pickers --- .changeset/tidy-platform-navigation.md | 5 +++++ apps/kimi-code/src/tui/utils/searchable-list.ts | 13 +++++++++++-- .../test/tui/utils/searchable-list.test.ts | 13 +++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-platform-navigation.md diff --git a/.changeset/tidy-platform-navigation.md b/.changeset/tidy-platform-navigation.md new file mode 100644 index 00000000..d49887d3 --- /dev/null +++ b/.changeset/tidy-platform-navigation.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Add Vim-style j/k navigation to non-search TUI pickers. diff --git a/apps/kimi-code/src/tui/utils/searchable-list.ts b/apps/kimi-code/src/tui/utils/searchable-list.ts index b5b7343a..22f3b10b 100644 --- a/apps/kimi-code/src/tui/utils/searchable-list.ts +++ b/apps/kimi-code/src/tui/utils/searchable-list.ts @@ -5,7 +5,8 @@ * The component owns presentation and the keys that carry component-specific * meaning — Enter (submit), Esc (cancel), and ←/→ (paging in one picker, a * thinking toggle in another). This unit owns the keys that behave identically - * everywhere: ↑/↓, PgUp/PgDn, and search editing. + * everywhere: ↑/↓, PgUp/PgDn, Vim-style j/k navigation for non-search + * pickers, and search editing. */ import { fuzzyFilter, Key, matchesKey } from '@earendil-works/pi-tui'; @@ -105,6 +106,7 @@ export class SearchableList { * Enter, Esc, and ←/→ are intentionally left to the component. */ handleKey(data: string): boolean { + const ch = printableChar(data); if (matchesKey(data, Key.up)) { this.moveUp(); return true; @@ -121,6 +123,14 @@ export class SearchableList { this.pageDown(); return true; } + if (!this.searchable && ch === 'k') { + this.moveUp(); + return true; + } + if (!this.searchable && ch === 'j') { + this.moveDown(); + return true; + } if (!this.searchable) return false; if (matchesKey(data, Key.backspace)) { if (this.query.length > 0) { @@ -129,7 +139,6 @@ export class SearchableList { } return true; } - const ch = printableChar(data); if (isPrintableChar(ch)) { this.query += ch; this.cursor = 0; diff --git a/apps/kimi-code/test/tui/utils/searchable-list.test.ts b/apps/kimi-code/test/tui/utils/searchable-list.test.ts index 170b8993..0354cbcb 100644 --- a/apps/kimi-code/test/tui/utils/searchable-list.test.ts +++ b/apps/kimi-code/test/tui/utils/searchable-list.test.ts @@ -97,4 +97,17 @@ describe('SearchableList', () => { expect(search.handleKey(BACKSPACE)).toBe(true); expect(search.view().query).toBe(''); }); + + it('supports j/k navigation for non-search pickers without stealing searchable input', () => { + const nav = make({ searchable: false, initialIndex: 1 }); + expect(nav.handleKey('j')).toBe(true); + expect(nav.selected()).toBe('item02'); + expect(nav.handleKey('k')).toBe(true); + expect(nav.selected()).toBe('item01'); + + const search = make({ searchable: true }); + expect(search.handleKey('j')).toBe(true); + expect(search.view().query).toBe('j'); + expect(search.selected()).toBe(search.view().items[0]); + }); });