Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-ime-cursor-placement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix IME candidate placement and cursor rendering in the terminal editor.
25 changes: 24 additions & 1 deletion apps/kimi-code/src/tui/components/editor/custom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import {
Editor,
CURSOR_MARKER,
isKeyRelease,
matchesKey,
Key,
Expand All @@ -23,6 +24,8 @@ const ANSI_SGR = /\u001B\[[0-9;]*m/g;
const PASTE_MARKER_RE = /\[paste #(\d+)(?: (?:\+\d+ lines|\d+ chars))?\]/g;
const BRACKET_PASTE_START = '\u001B[200~';
const BRACKET_PASTE_END = '\u001B[201~';
const FAKE_CURSOR_START = '\u001B[7m';
const FAKE_CURSOR_END = '\u001B[0m';

// Kitty keyboard protocol CSI-u sequence: ESC [ keycode ; modifier[:eventType] u.
// We intentionally match only the simple two-field form — enough to rewrite
Expand Down Expand Up @@ -239,9 +242,10 @@ export class CustomEditor extends Editor {
// overwrite it (e.g. plan-mode / slash-context highlight via
// `editor.borderColor = chalk.hex(primary)`), so we route corners and
// side bars through the same hook to stay in sync.
return wrapWithSideBorders(lines, (s) => this.borderColor(s), {
const wrapped = wrapWithSideBorders(lines, (s) => this.borderColor(s), {
connectedAbove: this.connectedAbove && !this.borderHighlighted,
});
return wrapped.map(removeFakeCursorAtHardwareMarker);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip the fake cursor before slash highlighting

When the editor text starts with a slash command and the cursor is at the end of the command token (for example after typing / or /help), highlightFirstSlashToken runs before this post-processing and treats CURSOR_MARKER as part of the token, inserting its SGR reset between the marker and pi-tui's \u001B[7m fake cursor. removeFakeCursorAtHardwareMarker then no-ops because the marker is no longer immediately followed by \u001B[7m, so the inverse fake cursor remains exactly in the slash-command path where hardware cursor mode should remove it. Move the fake-cursor stripping before slash highlighting or make the highlighter ignore the marker.

Useful? React with 👍 / 👎.

}

override handleInput(data: string): void {
Expand Down Expand Up @@ -362,6 +366,25 @@ export class CustomEditor extends Editor {
}
}

export function removeFakeCursorAtHardwareMarker(line: string): string {
const markerStart = line.indexOf(CURSOR_MARKER);
if (markerStart < 0) return line;

const fakeCursorStart = markerStart + CURSOR_MARKER.length;
if (!line.startsWith(FAKE_CURSOR_START, fakeCursorStart)) return line;

const fakeCursorEnd = line.indexOf(
FAKE_CURSOR_END,
fakeCursorStart + FAKE_CURSOR_START.length,
);
if (fakeCursorEnd < 0) return line;

const cursorTextStart = fakeCursorStart + FAKE_CURSOR_START.length;
const cursorText = line.slice(cursorTextStart, fakeCursorEnd);
const afterFakeCursor = fakeCursorEnd + FAKE_CURSOR_END.length;
return line.slice(0, fakeCursorStart) + cursorText + line.slice(afterFakeCursor);
}

/**
* Return a copy of `line` with the first `/token` coloured using `hex`.
* For `/goal next manage`, also colour the command-path tokens.
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-code/src/tui/tui-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function createTUIState(options: KimiTUIOptions): TUIState {
const theme = currentTheme;

const terminal = new ProcessTerminal();
const ui = new TUI(terminal);
const ui = new TUI(terminal, true);

const transcriptContainer = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER);
const activityContainer = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER);
Expand Down
33 changes: 31 additions & 2 deletions apps/kimi-code/test/tui/components/editor/custom-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import type {
AutocompleteSuggestions,
TUI,
} from '@earendil-works/pi-tui';
import { CURSOR_MARKER } from '@earendil-works/pi-tui';
import { describe, expect, it, vi } from 'vitest';

import { CustomEditor } from '#/tui/components/editor/custom-editor';
import {
CustomEditor,
removeFakeCursorAtHardwareMarker,
} from '#/tui/components/editor/custom-editor';

function makeEditor(): CustomEditor {
const tui = {
Expand Down Expand Up @@ -132,6 +136,31 @@ describe('CustomEditor Kitty key release handling', () => {
});
});

describe('removeFakeCursorAtHardwareMarker', () => {
it('removes the rendered fake cursor and keeps the hardware cursor marker in place', () => {
const line = `before${CURSOR_MARKER}\u001B[7m \u001B[0mafter`;

expect(removeFakeCursorAtHardwareMarker(line)).toBe(`before${CURSOR_MARKER} after`);
});

it('renders a focused editor with hardware cursor positioning but no fake cursor block', () => {
const editor = makeEditor();
editor.focused = true;
editor.setText('test');

const output = editor.render(40).join('\n');

expect(output).toContain(CURSOR_MARKER);
expect(output).not.toContain('\u001B[7m');
});

it('leaves unrelated marker-like lines untouched', () => {
const line = `before${CURSOR_MARKER}after`;

expect(removeFakeCursorAtHardwareMarker(line)).toBe(line);
});
});

describe('CustomEditor paste marker expansion', () => {
const PASTE_START = '\x1b[200~';
const PASTE_END = '\x1b[201~';
Expand Down Expand Up @@ -199,7 +228,7 @@ describe('CustomEditor paste marker expansion', () => {

expect(editor.getText()).toMatch(/\[paste #1/);

editor.handleInput('\x16');
editor.handleInput(process.platform === 'win32' ? '\x1b[118;3u' : '\x16');

expect(editor.getText()).not.toContain('[paste #');
expect(editor.getText()).toContain(longText);
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/create-tui-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('createTUIState', () => {

// UI objects are created.
expect(state.ui).toBeDefined();
expect(state.ui.getShowHardwareCursor()).toBe(true);
expect(state.terminal).toBeDefined();
expect(state.transcriptContainer).toBeDefined();
expect(state.activityContainer).toBeDefined();
Expand Down