diff --git a/.changeset/fix-session-picker-narrow-terminal.md b/.changeset/fix-session-picker-narrow-terminal.md new file mode 100644 index 00000000..8376138e --- /dev/null +++ b/.changeset/fix-session-picker-narrow-terminal.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix a crash in the `/sessions` picker on very narrow terminals (`Rendered line exceeds terminal width`). Long session ids, the inline time / `(current)` badge, and long prompts could be drawn past the terminal edge; every rendered line is now clamped to the terminal width so the picker degrades gracefully instead of crashing. diff --git a/apps/kimi-code/src/tui/components/dialogs/session-picker.ts b/apps/kimi-code/src/tui/components/dialogs/session-picker.ts index da1a139e..29053e64 100644 --- a/apps/kimi-code/src/tui/components/dialogs/session-picker.ts +++ b/apps/kimi-code/src/tui/components/dialogs/session-picker.ts @@ -126,6 +126,16 @@ export class SessionPickerComponent extends Container implements Focusable { } override render(width: number): string[] { + return this.renderLines(width).map((line) => truncateToWidth(line, width, ELLIPSIS)); + } + + // Builds the raw lines; `render()` applies a final width clamp so no line + // can ever exceed the terminal width. The per-line budgets below keep the + // layout tidy at normal widths, but on a very narrow terminal those budgets + // floor at a minimum and the trailing time/badge are appended in full, so + // the clamp in `render()` is what guarantees the renderer's invariant and + // prevents the "Rendered line exceeds terminal width" crash (issue #240). + private renderLines(width: number): string[] { const colors = this.colors; const lines: string[] = [chalk.hex(colors.primary)('─'.repeat(width))]; @@ -222,8 +232,9 @@ export class SessionPickerComponent extends Container implements Focusable { if (badge.length > 0) header += ' ' + chalk.hex(colors.success)(badge); const card: string[] = [header]; - // Session id is rendered in full (no truncation). The directory wraps to - // its own line if it would push past the terminal edge. + // Session id is rendered in full at normal widths (the final clamp in + // `render()` truncates it only when the terminal is narrower than the id). + // The directory wraps to its own line if it would push past the edge. const fullId = session.id; const idWidth = visibleWidth(fullId); const metaGap = ' '; diff --git a/apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts b/apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts index c8e91963..c2608d5c 100644 --- a/apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts @@ -261,4 +261,41 @@ describe('SessionPickerComponent', () => { } } }); + + // Regression for #240: a long session id, the inline time + "(current)" + // badge, and a long prompt all used to be appended past the terminal edge, + // which crashed the renderer with "Rendered line exceeds terminal width" on + // very narrow terminals. + it('never renders a line wider than the terminal, even on tiny widths (#240)', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const id = 'ses_fbe574f3-572d-487f-9fa0-d09694f599d4'; + const component = new SessionPickerComponent({ + sessions: [ + { + id, + title: '现在要重构一下 sessions 列表,让 UI 更好看一些', + last_prompt: 'please redesign the picker UI to be much nicer than before', + work_dir: '/Users/getlong/Development/cesiumdb', + updated_at: now - 5 * 60 * 1000, + metadata: { imported_from_kimi_cli: true }, + }, + ], + loading: false, + currentSessionId: id, + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + for (let width = 10; width <= 60; width++) { + const lines = component.render(width); + for (const [idx, line] of lines.entries()) { + expect(visibleWidth(line), `width=${String(width)} line#${String(idx)}`).toBeLessThanOrEqual( + width, + ); + } + } + }); });