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/mouse-select-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/pi-tui": minor
---

Add text selection, replacement, deletion, rendering, and mouse-position mapping to the editor component.
5 changes: 5 additions & 0 deletions .changeset/terminal-mouse-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add experimental mouse selection to the prompt editor. Set `KIMI_CODE_EXPERIMENTAL_TERMINAL_MOUSE_INPUT=1` to enable it.
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export interface SlashCommandHost {
restoreEditor(): void;
restoreInputText(text: string): void;
refreshSlashCommandAutocomplete(): void;
refreshTerminalMouseTracking(): void;

// Session
requireSession(): Session;
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/commands/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise<void>
const config = await host.harness.getConfig({ reload: true });
setExperimentalFeatures(await host.harness.getExperimentalFeatures());
host.refreshSlashCommandAutocomplete();
host.refreshTerminalMouseTracking();
applyRuntimeConfig(host, config);
await applyReloadedTuiConfig(host, tuiConfig);

Expand Down
10 changes: 10 additions & 0 deletions apps/kimi-code/src/tui/components/editor/custom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class CustomEditor extends Editor {
public onNonEscapeInput?: () => void;
public onCtrlD?: () => void;
public onCtrlC?: () => void;
public onCopySelection?: (text: string) => void;
public onToggleToolExpand?: () => void;
public onOpenExternalEditor?: () => void;
public onCtrlS?: () => void;
Expand Down Expand Up @@ -424,6 +425,11 @@ export class CustomEditor extends Editor {
}

if (matchesKey(normalized, Key.ctrl('c'))) {
const selectedText = this.getSelectedText();
if (selectedText !== undefined) {
this.onCopySelection?.(selectedText);
return;
}
this.onCtrlC?.();
return;
}
Expand Down Expand Up @@ -496,6 +502,10 @@ export class CustomEditor extends Editor {
this.cancelAutocompleteActivity();
return;
}
if (this.hasSelection()) {
this.clearSelection();
return;
}
this.onEscape?.();
return;
}
Expand Down
6 changes: 6 additions & 0 deletions apps/kimi-code/src/tui/constant/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export const TERMINAL_FOCUS_OUT = `${ESC}[O`;
export const ENABLE_TERMINAL_FOCUS_REPORTING = `${ESC}[?1004h`;
export const DISABLE_TERMINAL_FOCUS_REPORTING = `${ESC}[?1004l`;

// Xterm SGR mouse reporting. Button-event tracking reports drag motion while
// a button is held; SGR mode keeps coordinates unambiguous in wide terminals.
export const ENABLE_TERMINAL_MOUSE_REPORTING = `${ESC}[?1002h${ESC}[?1006h`;
export const DISABLE_TERMINAL_MOUSE_REPORTING =
`${ESC}[?1006l${ESC}[?1003l${ESC}[?1002l${ESC}[?1000l`;

// Standard OSC 11 background-color query. The response regex intentionally
// allows a missing leading ESC because terminals can echo replies alongside
// other raw input, but it requires an OSC terminator so fragmented color
Expand Down
9 changes: 9 additions & 0 deletions apps/kimi-code/src/tui/controllers/editor-keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { KimiHarness, Session } from '@moonshot-ai/kimi-code-sdk';
import { compressImageForModel, persistOriginalImage, sessionMediaOriginalsDir } from '@moonshot-ai/kimi-code-sdk';

import { ClipboardMediaError, readClipboardMedia } from '#/utils/clipboard/clipboard-image';
import { copyTextToClipboard } from '#/utils/clipboard/clipboard-text';
import { parseImageMeta } from '#/utils/image/image-mime';
import { editInExternalEditor, resolveEditorCommand } from '#/utils/process/external-editor';

Expand Down Expand Up @@ -55,6 +56,8 @@ export interface EditorKeyboardHost {
handleInputModeChange(mode: 'prompt' | 'bash'): void;
clearQueuedMessages(): void;
setExternalEditorRunning(running: boolean): void;
suspendTerminalMouseTracking(): void;
refreshTerminalMouseTracking(): void;
}

export class EditorKeyboardController {
Expand Down Expand Up @@ -120,6 +123,10 @@ export class EditorKeyboardController {
this.clearPendingUndoEsc();
};

editor.onCopySelection = (text: string) => {
void copyTextToClipboard(text).catch(() => undefined);
};

editor.onCtrlC = () => {
if (host.cancelInFlight !== undefined) {
const cancel = host.cancelInFlight;
Expand Down Expand Up @@ -512,6 +519,7 @@ export class EditorKeyboardController {
}
this.host.setExternalEditorRunning(true);
const seed = state.editor.getExpandedText?.() ?? state.editor.getText();
this.host.suspendTerminalMouseTracking();
state.ui.stop();
await new Promise<void>((resolve) => {
setImmediate(resolve);
Expand All @@ -529,6 +537,7 @@ export class EditorKeyboardController {
process.stdin.pause();
}
state.ui.start();
this.host.refreshTerminalMouseTracking();
state.ui.setFocus(state.editor);
state.ui.requestRender(true);
this.host.setExternalEditorRunning(false);
Expand Down
20 changes: 20 additions & 0 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ import { sessionRowsForPicker } from './utils/session-picker-rows';
import { formatBashOutputForDisplay } from './utils/shell-output';
import { combineStartupNotice, isOAuthLoginRequiredError } from './utils/startup';
import { installTerminalFocusTracking } from './utils/terminal-focus';
import { installEditorMouseTracking } from './utils/editor-mouse';
import { notifyTerminalOnce } from './utils/terminal-notification';
import { installTerminalThemeTracking } from './utils/terminal-theme';
import { detectTmuxKeyboardWarning } from './utils/tmux-keyboard';
Expand Down Expand Up @@ -313,6 +314,7 @@ export class KimiTUI {
aborted = false;
private terminalFocusTrackingDispose: (() => void) | undefined;
private terminalThemeTrackingDispose: (() => void) | undefined;
private terminalMouseTrackingDispose: (() => void) | undefined;
private clipboardImageHintController: ClipboardImageHintController | undefined;
private uninstallRainbowDance: () => void;
private signalCleanupHandlers: Array<() => void> = [];
Expand Down Expand Up @@ -539,6 +541,7 @@ export class KimiTUI {
return;
}
const shouldReplayHistory = await this.initMainTui();
this.refreshTerminalMouseTracking();
this.startBackgroundFdAutocomplete();
await this.finishStartup(shouldReplayHistory);
} catch (error) {
Expand Down Expand Up @@ -635,6 +638,7 @@ export class KimiTUI {
this.startClipboardImageHintController();
this.terminalFocusTrackingDispose = installTerminalFocusTracking(this.state);
this.refreshTerminalThemeTracking();
this.refreshTerminalMouseTracking();
}

private startClipboardImageHintController(): void {
Expand Down Expand Up @@ -940,6 +944,7 @@ export class KimiTUI {

private disposeTerminalTracking(): void {
this.stopTerminalThemeTracking();
this.suspendTerminalMouseTracking();
this.clipboardImageHintController?.stop();
this.clipboardImageHintController = undefined;
this.terminalFocusTrackingDispose?.();
Expand Down Expand Up @@ -2694,6 +2699,18 @@ export class KimiTUI {
this.terminalThemeTrackingDispose = undefined;
}

suspendTerminalMouseTracking(): void {
this.terminalMouseTrackingDispose?.();
this.terminalMouseTrackingDispose = undefined;
}

refreshTerminalMouseTracking(): void {
this.suspendTerminalMouseTracking();
if (!isExperimentalFlagEnabled('terminal_mouse_input')) return;
if (!this.state.editorContainer.children.includes(this.state.editor)) return;
this.terminalMouseTrackingDispose = installEditorMouseTracking(this.state);
}

private async applyResolvedAutoTheme(resolved: ResolvedTheme): Promise<void> {
if (this.state.appState.theme !== 'auto') return;
const palette = getBuiltInPalette(resolved);
Expand Down Expand Up @@ -2770,6 +2787,8 @@ export class KimiTUI {
// =========================================================================

mountEditorReplacement(panel: Component & Focusable): void {
this.suspendTerminalMouseTracking();
this.state.editor.clearSelection();
this.state.editorContainer.clear();
this.state.editorContainer.addChild(panel);
this.state.ui.setFocus(panel);
Expand All @@ -2780,6 +2799,7 @@ export class KimiTUI {
this.state.editorContainer.clear();
this.state.editorContainer.addChild(this.state.editor);
this.state.ui.setFocus(this.state.editor);
this.refreshTerminalMouseTracking();
// Measure overflow against the restored tree (editor mounted), not the tall
// panel just removed — otherwise a short session with a tall panel looks like
// it overflows and we take a full clear/home that yanks the editor to the top.
Expand Down
Loading