mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 18:03:50 +00:00
Add helper functions for key detection and update usage
- Add isCtrlA/C/E/K/O/P/T/U/W helper functions that check both raw and Kitty formats - Add isAltBackspace helper function - Refactor editor.ts, input.ts, select-list.ts to use helper functions - Refactor custom-editor.ts, session-selector.ts, user-message-selector.ts - Add CHANGELOG entry for the Shift+Enter fix
This commit is contained in:
parent
4a4531f887
commit
c3c2bffc68
9 changed files with 146 additions and 50 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Shift+Enter for newlines**: Fixed Shift+Enter not working for newlines in Ghostty, Kitty, WezTerm, and other terminals supporting the Kitty keyboard protocol. Also fixed Alt+Enter, Shift+Tab, and all Ctrl+key combinations (Ctrl+A/C/E/K/O/P/T/U/W). (by [@kim0](https://github.com/kim0))
|
||||
|
||||
## [0.23.4] - 2025-12-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Editor, isShiftTab, Keys } from "@mariozechner/pi-tui";
|
||||
import { Editor, isCtrlC, isCtrlO, isCtrlP, isCtrlT, isShiftTab } from "@mariozechner/pi-tui";
|
||||
|
||||
/**
|
||||
* Custom editor that handles Escape and Ctrl+C keys for coding-agent
|
||||
|
|
@ -12,25 +12,25 @@ export class CustomEditor extends Editor {
|
|||
public onCtrlT?: () => void;
|
||||
|
||||
handleInput(data: string): void {
|
||||
// Intercept Ctrl+T for thinking block visibility toggle (raw byte or Kitty protocol)
|
||||
if ((data === "\x14" || data === Keys.CTRL_T) && this.onCtrlT) {
|
||||
// Intercept Ctrl+T for thinking block visibility toggle
|
||||
if (isCtrlT(data) && this.onCtrlT) {
|
||||
this.onCtrlT();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+O for tool output expansion (raw byte or Kitty protocol)
|
||||
if ((data === "\x0f" || data === Keys.CTRL_O) && this.onCtrlO) {
|
||||
// Intercept Ctrl+O for tool output expansion
|
||||
if (isCtrlO(data) && this.onCtrlO) {
|
||||
this.onCtrlO();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+P for model cycling (raw byte or Kitty protocol)
|
||||
if ((data === "\x10" || data === Keys.CTRL_P) && this.onCtrlP) {
|
||||
// Intercept Ctrl+P for model cycling
|
||||
if (isCtrlP(data) && this.onCtrlP) {
|
||||
this.onCtrlP();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept Shift+Tab for thinking level cycling (legacy or Kitty protocol)
|
||||
// Intercept Shift+Tab for thinking level cycling
|
||||
if (isShiftTab(data) && this.onShiftTab) {
|
||||
this.onShiftTab();
|
||||
return;
|
||||
|
|
@ -43,8 +43,8 @@ export class CustomEditor extends Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
// Intercept Ctrl+C (raw byte or Kitty keyboard protocol)
|
||||
if ((data === "\x03" || data === Keys.CTRL_C) && this.onCtrlC) {
|
||||
// Intercept Ctrl+C
|
||||
if (isCtrlC(data) && this.onCtrlC) {
|
||||
this.onCtrlC();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type Component, Container, Input, Keys, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { type Component, Container, Input, isCtrlC, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import type { SessionManager } from "../../../core/session-manager.js";
|
||||
import { fuzzyFilter } from "../../../utils/fuzzy.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
|
@ -144,8 +144,8 @@ class SessionList implements Component {
|
|||
this.onCancel();
|
||||
}
|
||||
}
|
||||
// Ctrl+C - exit process (raw byte or Kitty keyboard protocol)
|
||||
else if (keyData === "\x03" || keyData === Keys.CTRL_C) {
|
||||
// Ctrl+C - exit process
|
||||
else if (isCtrlC(keyData)) {
|
||||
process.exit(0);
|
||||
}
|
||||
// Pass everything else to search input
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type Component, Container, Keys, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { type Component, Container, isCtrlC, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
|
|
@ -99,8 +99,8 @@ class UserMessageList implements Component {
|
|||
this.onCancel();
|
||||
}
|
||||
}
|
||||
// Ctrl+C - cancel (raw byte or Kitty keyboard protocol)
|
||||
else if (keyData === "\x03" || keyData === Keys.CTRL_C) {
|
||||
// Ctrl+C - cancel
|
||||
else if (isCtrlC(keyData)) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js";
|
||||
import { Keys } from "../keys.js";
|
||||
import { isAltBackspace, isCtrlA, isCtrlC, isCtrlE, isCtrlK, isCtrlU, isCtrlW, Keys } from "../keys.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { visibleWidth } from "../utils.js";
|
||||
import { SelectList, type SelectListTheme } from "./select-list.js";
|
||||
|
|
@ -260,8 +260,7 @@ export class Editor implements Component {
|
|||
// Handle special key combinations first
|
||||
|
||||
// Ctrl+C - Exit (let parent handle this)
|
||||
// Handle both raw byte (\x03) and Kitty keyboard protocol
|
||||
if (data.charCodeAt(0) === 3 || data === Keys.CTRL_C) {
|
||||
if (isCtrlC(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -360,28 +359,28 @@ export class Editor implements Component {
|
|||
}
|
||||
|
||||
// Continue with rest of input handling
|
||||
// Ctrl+K - Delete to end of line (raw byte or Kitty protocol)
|
||||
if (data.charCodeAt(0) === 11 || data === Keys.CTRL_K) {
|
||||
// Ctrl+K - Delete to end of line
|
||||
if (isCtrlK(data)) {
|
||||
this.deleteToEndOfLine();
|
||||
}
|
||||
// Ctrl+U - Delete to start of line (raw byte or Kitty protocol)
|
||||
else if (data.charCodeAt(0) === 21 || data === Keys.CTRL_U) {
|
||||
// Ctrl+U - Delete to start of line
|
||||
else if (isCtrlU(data)) {
|
||||
this.deleteToStartOfLine();
|
||||
}
|
||||
// Ctrl+W - Delete word backwards (raw byte or Kitty protocol)
|
||||
else if (data.charCodeAt(0) === 23 || data === Keys.CTRL_W) {
|
||||
// Ctrl+W - Delete word backwards
|
||||
else if (isCtrlW(data)) {
|
||||
this.deleteWordBackwards();
|
||||
}
|
||||
// Option/Alt+Backspace (e.g. Ghostty sends ESC + DEL, or Kitty protocol)
|
||||
else if (data === "\x1b\x7f" || data === Keys.ALT_BACKSPACE) {
|
||||
// Option/Alt+Backspace - Delete word backwards
|
||||
else if (isAltBackspace(data)) {
|
||||
this.deleteWordBackwards();
|
||||
}
|
||||
// Ctrl+A - Move to start of line (raw byte or Kitty protocol)
|
||||
else if (data.charCodeAt(0) === 1 || data === Keys.CTRL_A) {
|
||||
// Ctrl+A - Move to start of line
|
||||
else if (isCtrlA(data)) {
|
||||
this.moveToLineStart();
|
||||
}
|
||||
// Ctrl+E - Move to end of line (raw byte or Kitty protocol)
|
||||
else if (data.charCodeAt(0) === 5 || data === Keys.CTRL_E) {
|
||||
// Ctrl+E - Move to end of line
|
||||
else if (isCtrlE(data)) {
|
||||
this.moveToLineEnd();
|
||||
}
|
||||
// New line shortcuts (but not plain LF/CR which should be submit)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Keys } from "../keys.js";
|
||||
import { isAltBackspace, isCtrlA, isCtrlE, isCtrlK, isCtrlU, isCtrlW } from "../keys.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { visibleWidth } from "../utils.js";
|
||||
|
||||
|
|
@ -102,39 +102,39 @@ export class Input implements Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if (data === "\x01" || data === Keys.CTRL_A) {
|
||||
// Ctrl+A - beginning of line (raw byte or Kitty protocol)
|
||||
if (isCtrlA(data)) {
|
||||
// Ctrl+A - beginning of line
|
||||
this.cursor = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data === "\x05" || data === Keys.CTRL_E) {
|
||||
// Ctrl+E - end of line (raw byte or Kitty protocol)
|
||||
if (isCtrlE(data)) {
|
||||
// Ctrl+E - end of line
|
||||
this.cursor = this.value.length;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.charCodeAt(0) === 23 || data === Keys.CTRL_W) {
|
||||
// Ctrl+W - delete word backwards (raw byte or Kitty protocol)
|
||||
if (isCtrlW(data)) {
|
||||
// Ctrl+W - delete word backwards
|
||||
this.deleteWordBackwards();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data === "\x1b\x7f" || data === Keys.ALT_BACKSPACE) {
|
||||
// Option/Alt+Backspace - delete word backwards (legacy or Kitty protocol)
|
||||
if (isAltBackspace(data)) {
|
||||
// Option/Alt+Backspace - delete word backwards
|
||||
this.deleteWordBackwards();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.charCodeAt(0) === 21 || data === Keys.CTRL_U) {
|
||||
// Ctrl+U - delete from cursor to start of line (raw byte or Kitty protocol)
|
||||
if (isCtrlU(data)) {
|
||||
// Ctrl+U - delete from cursor to start of line
|
||||
this.value = this.value.slice(this.cursor);
|
||||
this.cursor = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.charCodeAt(0) === 11 || data === Keys.CTRL_K) {
|
||||
// Ctrl+K - delete from cursor to end of line (raw byte or Kitty protocol)
|
||||
if (isCtrlK(data)) {
|
||||
// Ctrl+K - delete from cursor to end of line
|
||||
this.value = this.value.slice(0, this.cursor);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Keys } from "../keys.js";
|
||||
import { isCtrlC } from "../keys.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { truncateToWidth } from "../utils.js";
|
||||
|
||||
|
|
@ -162,8 +162,8 @@ export class SelectList implements Component {
|
|||
this.onSelect(selectedItem);
|
||||
}
|
||||
}
|
||||
// Escape or Ctrl+C (raw byte or Kitty keyboard protocol)
|
||||
else if (keyData === "\x1b" || keyData === "\x03" || keyData === Keys.CTRL_C) {
|
||||
// Escape or Ctrl+C
|
||||
else if (keyData === "\x1b" || isCtrlC(keyData)) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,22 @@ export { Spacer } from "./components/spacer.js";
|
|||
export { Text } from "./components/text.js";
|
||||
export { TruncatedText } from "./components/truncated-text.js";
|
||||
// Kitty keyboard protocol helpers
|
||||
export { isCtrlC, isKittyCtrl, isKittyKey, isShiftTab, Keys } from "./keys.js";
|
||||
export {
|
||||
isAltBackspace,
|
||||
isCtrlA,
|
||||
isCtrlC,
|
||||
isCtrlE,
|
||||
isCtrlK,
|
||||
isCtrlO,
|
||||
isCtrlP,
|
||||
isCtrlT,
|
||||
isCtrlU,
|
||||
isCtrlW,
|
||||
isKittyCtrl,
|
||||
isKittyKey,
|
||||
isShiftTab,
|
||||
Keys,
|
||||
} from "./keys.js";
|
||||
// Terminal interface and implementations
|
||||
export { ProcessTerminal, type Terminal } from "./terminal.js";
|
||||
// Terminal image support
|
||||
|
|
|
|||
|
|
@ -93,16 +93,94 @@ export function isKittyKey(data: string, codepoint: number, modifier: number): b
|
|||
return data === kittySequence(codepoint, modifier);
|
||||
}
|
||||
|
||||
// Raw control character codes
|
||||
const RAW = {
|
||||
CTRL_A: "\x01",
|
||||
CTRL_C: "\x03",
|
||||
CTRL_E: "\x05",
|
||||
CTRL_K: "\x0b",
|
||||
CTRL_O: "\x0f",
|
||||
CTRL_P: "\x10",
|
||||
CTRL_T: "\x14",
|
||||
CTRL_U: "\x15",
|
||||
CTRL_W: "\x17",
|
||||
ALT_BACKSPACE: "\x1b\x7f",
|
||||
SHIFT_TAB: "\x1b[Z",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+A (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlA(data: string): boolean {
|
||||
return data === RAW.CTRL_A || data === Keys.CTRL_A;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+C (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlC(data: string): boolean {
|
||||
return data === "\x03" || data === Keys.CTRL_C;
|
||||
return data === RAW.CTRL_C || data === Keys.CTRL_C;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+E (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlE(data: string): boolean {
|
||||
return data === RAW.CTRL_E || data === Keys.CTRL_E;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+K (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlK(data: string): boolean {
|
||||
return data === RAW.CTRL_K || data === Keys.CTRL_K;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+O (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlO(data: string): boolean {
|
||||
return data === RAW.CTRL_O || data === Keys.CTRL_O;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+P (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlP(data: string): boolean {
|
||||
return data === RAW.CTRL_P || data === Keys.CTRL_P;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+T (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlT(data: string): boolean {
|
||||
return data === RAW.CTRL_T || data === Keys.CTRL_T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+U (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlU(data: string): boolean {
|
||||
return data === RAW.CTRL_U || data === Keys.CTRL_U;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+W (raw byte or Kitty protocol).
|
||||
*/
|
||||
export function isCtrlW(data: string): boolean {
|
||||
return data === RAW.CTRL_W || data === Keys.CTRL_W;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Alt+Backspace (legacy or Kitty protocol).
|
||||
*/
|
||||
export function isAltBackspace(data: string): boolean {
|
||||
return data === RAW.ALT_BACKSPACE || data === Keys.ALT_BACKSPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Shift+Tab (legacy or Kitty protocol).
|
||||
*/
|
||||
export function isShiftTab(data: string): boolean {
|
||||
return data === "\x1b[Z" || data === Keys.SHIFT_TAB;
|
||||
return data === RAW.SHIFT_TAB || data === Keys.SHIFT_TAB;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue