From 29315f3962c2f4c7ab2757e88d15e173269211c7 Mon Sep 17 00:00:00 2001 From: Yi-Fan Wang Date: Sat, 29 Aug 2026 16:02:51 +0200 Subject: [PATCH] fix(cli): standard text-editing keys in AI question prompt Add cursor-aware editing to the AskUserQuestion "Other" answer box (arrows, Ctrl/Alt+arrow word movement, Home/End, Ctrl+A/E/B/F/W/U/K, Alt+B/F/D, Backspace, Delete), stop arrow keys from inserting escape-sequence garbage, recognize ESCOD/ESCOC and Ctrl+Home/End in the parser, and document per-platform key mappings in the READMEs. Co-authored-by: deepseek-v4-pro --- README-en.md | 22 +++ README-zh_CN.md | 22 +++ README.md | 22 +++ .../cli/src/tests/ask-user-question.test.ts | 63 ++++++- .../cli/src/tests/prompt-input-keys.test.ts | 20 +++ packages/cli/src/ui/hooks/useTerminalInput.ts | 21 ++- packages/cli/src/ui/index.ts | 2 +- .../src/ui/views/AskUserQuestionPrompt.tsx | 166 +++++++++++++++--- 8 files changed, 306 insertions(+), 32 deletions(-) diff --git a/README-en.md b/README-en.md index 70fc105f..f55b9d99 100644 --- a/README-en.md +++ b/README-en.md @@ -92,6 +92,28 @@ Skills are discovered from these locations, in priority order: | `Ctrl+V` | Paste an image from the clipboard | | `Esc` | Interrupt the current model turn | | `Ctrl+D` twice | Quit Deep Code | +| `←` / `→` | Move the cursor one character | +| `Alt+←` / `Alt+→` (or `Ctrl+←` / `Ctrl+→`) | Move by word | +| `Home` / `End` (or `Ctrl+A` / `Ctrl+E`) | Jump to line start / end | +| `Ctrl+B` / `Ctrl+F` | Move left / right one character | +| `Alt+B` / `Alt+F` | Move back / forward by word | +| `Ctrl+W` | Delete the word before the cursor | +| `Alt+Backspace` | Delete the word before the cursor (macOS) | +| `Alt+D` | Delete the word after the cursor | +| `Ctrl+K` | Delete to the end of the line | +| `Ctrl+U` | Clear the input | +| `Backspace` / `Delete` | Delete the previous / next character | + +These editing keys also apply to the "Other" free-text answer in AI question prompts. + +### Terminal key mapping notes + +`Cmd` (macOS) and `Win` (Windows) key combinations are reserved by the operating system and are never delivered to terminal apps — map them in your terminal emulator instead: + +- **Linux + Bash**: use `Ctrl+A` / `Ctrl+E` or `Home` / `End` for line start/end; `Alt+B` / `Alt+F` or `Ctrl+←` / `Ctrl+→` for words. +- **macOS Terminal.app**: use `Ctrl+A` / `Ctrl+E` for line start/end and `Fn+←` / `Fn+→` as Home/End. Enable *Settings → Profiles → Keyboard → Use Option as Meta key* so `Option+B` / `Option+F` move by word. +- **macOS iTerm2**: map `Cmd+←` / `Cmd+→` under *Settings → Profiles → Keys* to send the Home/End escape sequences (`ESC[H` / `ESC[F`) or hex `0x01` / `0x05` (`Ctrl+A` / `Ctrl+E`). +- **Windows Terminal (PowerShell / WSL)**: `Win+←` / `Win+→` is OS window snapping and cannot be used. Use `Home` / `End` or `Ctrl+A` / `Ctrl+E` for line start/end, and `Ctrl+←` / `Ctrl+→` for word movement. ## Supported Models diff --git a/README-zh_CN.md b/README-zh_CN.md index 5cda5663..933b6faf 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -91,6 +91,28 @@ Skills 会按以下优先级扫描: | `Ctrl+V` | 从剪贴板粘贴图片 | | `Esc` | 中断当前模型回复 | | 连续 `Ctrl+D` | 退出 | +| `←` / `→` | 光标左右移动一个字符 | +| `Alt+←` / `Alt+→`(或 `Ctrl+←` / `Ctrl+→`) | 按词移动 | +| `Home` / `End`(或 `Ctrl+A` / `Ctrl+E`) | 跳到行首 / 行尾 | +| `Ctrl+B` / `Ctrl+F` | 光标左移 / 右移一个字符 | +| `Alt+B` / `Alt+F` | 按词后退 / 前进 | +| `Ctrl+W` | 删除光标前的单词 | +| `Alt+Backspace` | 删除光标前的单词(macOS) | +| `Alt+D` | 删除光标后的单词 | +| `Ctrl+K` | 删除到行尾 | +| `Ctrl+U` | 清空输入 | +| `Backspace` / `Delete` | 删除前一个 / 后一个字符 | + +以上编辑键同样适用于 AI 提问框中 "Other" 自由文本回答。 + +### 终端按键映射说明 + +`Cmd`(macOS)与 `Win`(Windows)组合键被操作系统保留,终端应用无法接收,请在终端模拟器中自行映射: + +- **Linux + Bash**:行首/行尾用 `Ctrl+A` / `Ctrl+E` 或 `Home` / `End`;按词移动用 `Alt+B` / `Alt+F` 或 `Ctrl+←` / `Ctrl+→`。 +- **macOS 终端(Terminal.app)**:行首/行尾用 `Ctrl+A` / `Ctrl+E`,`Fn+←` / `Fn+→` 等同于 Home/End。在 *设置 → 描述文件 → 键盘 → 将 Option 用作 Meta 键* 中启用后,`Option+B` / `Option+F` 可按词移动。 +- **macOS iTerm2**:在 *设置 → Profiles → Keys* 中将 `Cmd+←` / `Cmd+→` 映射为发送 Home/End 转义序列(`ESC[H` / `ESC[F`)或十六进制 `0x01` / `0x05`(`Ctrl+A` / `Ctrl+E`)。 +- **Windows Terminal(PowerShell / WSL)**:`Win+←` / `Win+→` 为系统窗口贴靠,无法使用。行首/行尾请用 `Home` / `End` 或 `Ctrl+A` / `Ctrl+E`,按词移动用 `Ctrl+←` / `Ctrl+→`。 ## 支持的模型 diff --git a/README.md b/README.md index 21a7311d..0a7515e5 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,28 @@ Skills 会按以下优先级扫描: | `Ctrl+V` | 从剪贴板粘贴图片 | | `Esc` | 中断当前模型回复 | | 连续 `Ctrl+D` | 退出 | +| `←` / `→` | 光标左右移动一个字符 | +| `Alt+←` / `Alt+→`(或 `Ctrl+←` / `Ctrl+→`) | 按词移动 | +| `Home` / `End`(或 `Ctrl+A` / `Ctrl+E`) | 跳到行首 / 行尾 | +| `Ctrl+B` / `Ctrl+F` | 光标左移 / 右移一个字符 | +| `Alt+B` / `Alt+F` | 按词后退 / 前进 | +| `Ctrl+W` | 删除光标前的单词 | +| `Alt+Backspace` | 删除光标前的单词(macOS) | +| `Alt+D` | 删除光标后的单词 | +| `Ctrl+K` | 删除到行尾 | +| `Ctrl+U` | 清空输入 | +| `Backspace` / `Delete` | 删除前一个 / 后一个字符 | + +以上编辑键同样适用于 AI 提问框中 "Other" 自由文本回答。 + +### 终端按键映射说明 + +`Cmd`(macOS)与 `Win`(Windows)组合键被操作系统保留,终端应用无法接收,请在终端模拟器中自行映射: + +- **Linux + Bash**:行首/行尾用 `Ctrl+A` / `Ctrl+E` 或 `Home` / `End`;按词移动用 `Alt+B` / `Alt+F` 或 `Ctrl+←` / `Ctrl+→`。 +- **macOS 终端(Terminal.app)**:行首/行尾用 `Ctrl+A` / `Ctrl+E`,`Fn+←` / `Fn+→` 等同于 Home/End。在 *设置 → 描述文件 → 键盘 → 将 Option 用作 Meta 键* 中启用后,`Option+B` / `Option+F` 可按词移动。 +- **macOS iTerm2**:在 *设置 → Profiles → Keys* 中将 `Cmd+←` / `Cmd+→` 映射为发送 Home/End 转义序列(`ESC[H` / `ESC[F`)或十六进制 `0x01` / `0x05`(`Ctrl+A` / `Ctrl+E`)。 +- **Windows Terminal(PowerShell / WSL)**:`Win+←` / `Win+→` 为系统窗口贴靠,无法使用。行首/行尾请用 `Home` / `End` 或 `Ctrl+A` / `Ctrl+E`,按词移动用 `Ctrl+←` / `Ctrl+→`。 ## 支持的模型 diff --git a/packages/cli/src/tests/ask-user-question.test.ts b/packages/cli/src/tests/ask-user-question.test.ts index cf1980d7..0e544942 100644 --- a/packages/cli/src/tests/ask-user-question.test.ts +++ b/packages/cli/src/tests/ask-user-question.test.ts @@ -1,6 +1,13 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; +import { + applyOtherAnswerEdit, + findPendingAskUserQuestion, + formatAskUserQuestionAnswers, + formatAskUserQuestionDecline, +} from "../ui"; +import type { PromptBufferState } from "../ui"; +import { parseTerminalInput } from "../ui/hooks"; import type { SessionMessage } from "@vegamo/deepcode-core"; function message(content: unknown): SessionMessage { @@ -122,3 +129,57 @@ test("formatAskUserQuestionAnswers normalizes multiline questions and answers", test("formatAskUserQuestionDecline creates decline text", () => { assert.match(formatAskUserQuestionDecline(), /declined to answer/); }); + +function editWith(state: PromptBufferState, sequence: string): PromptBufferState | null { + const { input, key } = parseTerminalInput(sequence); + return applyOtherAnswerEdit(state, input, key); +} + +function state(text: string, cursor: number): PromptBufferState { + return { text, cursor }; +} + +test("applyOtherAnswerEdit inserts typed text at the cursor", () => { + assert.deepEqual(editWith(state("ac", 1), "b"), { text: "abc", cursor: 2 }); +}); + +test("applyOtherAnswerEdit moves the cursor with plain arrow keys and never inserts residue", () => { + const { input, key } = parseTerminalInput("\u001B[D"); + assert.equal(input, "[D"); // escape-sequence residue that used to be typed + assert.deepEqual(applyOtherAnswerEdit(state("abc", 2), input, key), { text: "abc", cursor: 1 }); + assert.deepEqual(editWith(state("abc", 1), "\u001B[C"), { text: "abc", cursor: 2 }); + assert.deepEqual(editWith(state("abc", 2), "\u001B[C"), { text: "abc", cursor: 3 }); +}); + +test("applyOtherAnswerEdit moves by word with ctrl/meta arrows", () => { + const text = state("one two three", 7); + assert.deepEqual(editWith(text, "\u001B[1;5D"), { text: "one two three", cursor: 4 }); + assert.deepEqual(editWith(text, "\u001Bb"), { text: "one two three", cursor: 4 }); + assert.deepEqual(editWith(state("one two", 0), "\u001B[1;5C"), { text: "one two", cursor: 3 }); + assert.deepEqual(editWith(state("one two", 0), "\u001Bf"), { text: "one two", cursor: 3 }); +}); + +test("applyOtherAnswerEdit moves to line start/end with home/end and ctrl+a/e", () => { + assert.deepEqual(editWith(state("abc", 1), "\u001B[H"), { text: "abc", cursor: 0 }); + assert.deepEqual(editWith(state("abc", 1), "\u001B[F"), { text: "abc", cursor: 3 }); + assert.deepEqual(editWith(state("abc", 1), "\u0001"), { text: "abc", cursor: 0 }); + assert.deepEqual(editWith(state("abc", 1), "\u0005"), { text: "abc", cursor: 3 }); + assert.deepEqual(editWith(state("abc", 1), "\u001B[1;5H"), { text: "abc", cursor: 0 }); + assert.deepEqual(editWith(state("abc", 1), "\u001B[1;5F"), { text: "abc", cursor: 3 }); +}); + +test("applyOtherAnswerEdit deletes around the cursor", () => { + assert.deepEqual(editWith(state("abcd", 2), "\u007F"), { text: "acd", cursor: 1 }); + assert.deepEqual(editWith(state("abcd", 2), "\u001B[3~"), { text: "abd", cursor: 2 }); + assert.deepEqual(editWith(state("one two", 7), "\u0017"), { text: "one ", cursor: 4 }); + assert.deepEqual(editWith(state("abc", 1), "\u0015"), { text: "", cursor: 0 }); + assert.deepEqual(editWith(state("ab\ncd", 1), "\u000B"), { text: "a\ncd", cursor: 1 }); + assert.deepEqual(editWith(state("one two", 3), "\u001Bd"), { text: "one", cursor: 3 }); + assert.deepEqual(editWith(state("one two", 4), "\u001B\u007F"), { text: "two", cursor: 0 }); +}); + +test("applyOtherAnswerEdit ignores unhandled escape sequences instead of typing them", () => { + for (const sequence of ["\u001B[A", "\u001B[B", "\u001B[1;2D", "\u001B[11~", "\u001B[Z"]) { + assert.equal(editWith(state("abc", 1), sequence), null, `sequence ${JSON.stringify(sequence)}`); + } +}); diff --git a/packages/cli/src/tests/prompt-input-keys.test.ts b/packages/cli/src/tests/prompt-input-keys.test.ts index 8f0ea47e..07e03acb 100644 --- a/packages/cli/src/tests/prompt-input-keys.test.ts +++ b/packages/cli/src/tests/prompt-input-keys.test.ts @@ -82,6 +82,26 @@ test("parseTerminalInput recognizes word navigation modifiers", () => { assert.equal(metaRight.key.meta, true); }); +test("parseTerminalInput recognizes application cursor mode arrows", () => { + const left = parseTerminalInput("\u001BOD"); + const right = parseTerminalInput("\u001BOC"); + assert.equal(left.key.leftArrow, true); + assert.equal(left.key.meta, false); + assert.equal(right.key.rightArrow, true); + assert.equal(right.key.meta, false); +}); + +test("parseTerminalInput recognizes ctrl+home and ctrl+end", () => { + const home = parseTerminalInput("\u001B[1;5H"); + const end = parseTerminalInput("\u001B[1;5F"); + assert.equal(home.key.home, true); + assert.equal(home.key.ctrl, true); + assert.equal(home.key.meta, false); + assert.equal(end.key.end, true); + assert.equal(end.key.ctrl, true); + assert.equal(end.key.meta, false); +}); + test("parseTerminalInput keeps DEL payload for meta+backspace", () => { const { input, key } = parseTerminalInput("\u001B\u007F"); assert.equal(input, "\u007F"); diff --git a/packages/cli/src/ui/hooks/useTerminalInput.ts b/packages/cli/src/ui/hooks/useTerminalInput.ts index 9e985fa9..f0b3603e 100644 --- a/packages/cli/src/ui/hooks/useTerminalInput.ts +++ b/packages/cli/src/ui/hooks/useTerminalInput.ts @@ -34,6 +34,11 @@ const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]); const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]); const META_LEFT_SEQUENCES = new Set(["\u001B[1;3D", "\u001B[3D", "\u001Bb"]); const META_RIGHT_SEQUENCES = new Set(["\u001B[1;3C", "\u001B[3C", "\u001Bf"]); +// Application cursor keys mode (DECCKM) arrows. +const APPLICATION_LEFT = "\u001BOD"; +const APPLICATION_RIGHT = "\u001BOC"; +const CTRL_HOME_SEQUENCES = new Set(["\u001B[1;5H", "\u001B[5H"]); +const CTRL_END_SEQUENCES = new Set(["\u001B[1;5F", "\u001B[5F"]); const TERMINAL_FOCUS_IN = "\u001B[I"; const TERMINAL_FOCUS_OUT = "\u001B[O"; @@ -118,15 +123,21 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: const key: InputKey = { upArrow: raw === "\u001B[A", downArrow: raw === "\u001B[B", - leftArrow: raw === "\u001B[D" || CTRL_LEFT_SEQUENCES.has(raw) || META_LEFT_SEQUENCES.has(raw), - rightArrow: raw === "\u001B[C" || CTRL_RIGHT_SEQUENCES.has(raw) || META_RIGHT_SEQUENCES.has(raw), - home: HOME_SEQUENCES.has(raw), - end: END_SEQUENCES.has(raw), + leftArrow: + raw === "\u001B[D" || raw === APPLICATION_LEFT || CTRL_LEFT_SEQUENCES.has(raw) || META_LEFT_SEQUENCES.has(raw), + rightArrow: + raw === "\u001B[C" || raw === APPLICATION_RIGHT || CTRL_RIGHT_SEQUENCES.has(raw) || META_RIGHT_SEQUENCES.has(raw), + home: HOME_SEQUENCES.has(raw) || CTRL_HOME_SEQUENCES.has(raw), + end: END_SEQUENCES.has(raw) || CTRL_END_SEQUENCES.has(raw), pageDown: raw === "\u001B[6~", pageUp: raw === "\u001B[5~", return: raw === "\r" || SHIFT_RETURN_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw), escape: raw === "\u001B", - ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw), + ctrl: + CTRL_LEFT_SEQUENCES.has(raw) || + CTRL_RIGHT_SEQUENCES.has(raw) || + CTRL_HOME_SEQUENCES.has(raw) || + CTRL_END_SEQUENCES.has(raw), shift: SHIFT_RETURN_SEQUENCES.has(raw), tab: raw === "\t" || raw === "\u001B[Z", backspace: BACKSPACE_BYTES.has(raw), diff --git a/packages/cli/src/ui/index.ts b/packages/cli/src/ui/index.ts index a975efd0..bfb98f3b 100644 --- a/packages/cli/src/ui/index.ts +++ b/packages/cli/src/ui/index.ts @@ -14,7 +14,7 @@ export { resolvePromptTerminalCursorPosition, } from "./hooks/cursor"; export { default as AppContainer } from "./views/AppContainer"; -export { AskUserQuestionPrompt } from "./views/AskUserQuestionPrompt"; +export { AskUserQuestionPrompt, applyOtherAnswerEdit } from "./views/AskUserQuestionPrompt"; export { PlanImplementationPrompt, extractProposedPlan, diff --git a/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx b/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx index ccce5f7e..926a2e84 100644 --- a/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx +++ b/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx @@ -1,7 +1,25 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; import type { AskUserQuestionAnswers, AskUserQuestionItem } from "../core/ask-user-question"; +import { + EMPTY_BUFFER, + backspace, + deleteForward, + deleteWordAfter, + deleteWordBefore, + insertText, + killLine, + moveLeft, + moveLineEnd, + moveLineStart, + moveRight, + moveWordLeft, + moveWordRight, +} from "../core/prompt-buffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; import { useTerminalInput } from "../hooks"; +import type { InputKey } from "../hooks"; +import { renderBufferWithCursor } from "./PromptInput"; type Props = { questions: AskUserQuestionItem[]; @@ -23,13 +41,14 @@ export function AskUserQuestionPrompt({ questions, onSubmit, onCancel }: Props): const [cursorIndex, setCursorIndex] = useState(0); const [answers, setAnswers] = useState({}); const [selectedValues, setSelectedValues] = useState>({}); - const [otherTexts, setOtherTexts] = useState>({}); + const [otherTexts, setOtherTexts] = useState>({}); const [statusMessage, setStatusMessage] = useState(null); const question = questions[questionIndex]; const options = useMemo(() => buildOptions(question), [question]); const selectedForQuestion = selectedValues[questionIndex] ?? []; - const otherText = otherTexts[questionIndex] ?? ""; + const otherState = otherTexts[questionIndex] ?? EMPTY_BUFFER; + const otherText = otherState.text; const isCurrentOther = options[cursorIndex]?.isOther === true; useEffect(() => { @@ -80,28 +99,36 @@ export function AskUserQuestionPrompt({ questions, onSubmit, onCancel }: Props): return; } - if (key.backspace && isCurrentOther) { - setOtherTexts((prev) => ({ - ...prev, - [questionIndex]: (prev[questionIndex] ?? "").slice(0, -1), - })); - return; - } - if (key.return) { commitCurrentQuestion(); return; } - if (isCurrentOther && input && !key.ctrl && !key.meta && !input.startsWith("\u001B")) { - const sanitized = input.replace(/\r/g, ""); - if (sanitized) { - setOtherTexts((prev) => ({ - ...prev, - [questionIndex]: `${prev[questionIndex] ?? ""}${sanitized}`, - })); + if (isCurrentOther) { + const next = applyOtherAnswerEdit(otherTexts[questionIndex] ?? EMPTY_BUFFER, input, key); + if (next) { + setOtherTexts((prev) => ({ ...prev, [questionIndex]: next })); + return; + } + // Consume unhandled control/escape sequences so they are never typed. + if ( + key.ctrl || + key.meta || + key.tab || + key.backspace || + key.delete || + key.leftArrow || + key.rightArrow || + key.home || + key.end || + key.pageUp || + key.pageDown || + key.focusIn || + key.focusOut || + input.startsWith("\u001B") + ) { + return; } - return; } if (question.multiSelect && input === " " && !key.ctrl && !key.meta) { @@ -197,10 +224,7 @@ export function AskUserQuestionPrompt({ questions, onSubmit, onCancel }: Props): width={64} > {otherText ? ( - - {otherText} - {isCursor ? : null} - + {renderBufferWithCursor(otherState, isCursor)} ) : ( {isCursor ? "type your answer here" : "type a custom answer"} )} @@ -219,16 +243,108 @@ export function AskUserQuestionPrompt({ questions, onSubmit, onCancel }: Props): {statusMessage ?? (isCurrentOther - ? "Type your answer · Backspace edit · Enter submit/next · ↑ choose presets · Esc type manually" + ? "Type your answer · ←/→ move · Alt+←/→ word · Home/End · Enter submit/next · ↑ choose presets · Esc cancel" : question.multiSelect - ? "↑/↓ move · Space toggle · Enter submit/next · Esc type manually" - : "↑/↓ move · Enter select/next · Esc type manually")} + ? "↑/↓ move · Space toggle · Enter submit/next · Esc cancel" + : "↑/↓ move · Enter select/next · Esc cancel")} ); } +/** + * Apply one keystroke to the single-line "Other" answer buffer. + * Returns the next state, or `null` when the keystroke is not an edit + * (e.g. option navigation or an unhandled escape sequence). + */ +export function applyOtherAnswerEdit(state: PromptBufferState, input: string, key: InputKey): PromptBufferState | null { + if ((key.ctrl || key.meta) && key.leftArrow) { + return moveWordLeft(state); + } + if ((key.ctrl || key.meta) && key.rightArrow) { + return moveWordRight(state); + } + if (key.leftArrow) { + return moveLeft(state); + } + if (key.rightArrow) { + return moveRight(state); + } + if (key.home) { + return moveLineStart(state); + } + if (key.end) { + return moveLineEnd(state); + } + if (key.ctrl && (input === "a" || input === "A")) { + return moveLineStart(state); + } + if (key.ctrl && (input === "e" || input === "E")) { + return moveLineEnd(state); + } + if (key.ctrl && (input === "b" || input === "B")) { + return moveLeft(state); + } + if (key.ctrl && (input === "f" || input === "F")) { + return moveRight(state); + } + if (key.meta && (input === "b" || input === "B")) { + return moveWordLeft(state); + } + if (key.meta && (input === "f" || input === "F")) { + return moveWordRight(state); + } + if (key.delete) { + return deleteForward(state); + } + if (key.backspace) { + return backspace(state); + } + if (key.ctrl && (input === "w" || input === "W")) { + return deleteWordBefore(state); + } + if (key.ctrl && (input === "u" || input === "U")) { + return { ...EMPTY_BUFFER }; + } + if (key.ctrl && (input === "k" || input === "K")) { + return killLine(state); + } + if (key.meta && (input === "d" || input === "D")) { + return deleteWordAfter(state); + } + if (key.meta && (input === "\u007F" || input === "\b")) { + return deleteWordBefore(state); + } + + const isPlainText = + input.length > 0 && + !key.ctrl && + !key.meta && + !key.tab && + !key.backspace && + !key.delete && + !key.return && + !key.escape && + !key.upArrow && + !key.downArrow && + !key.leftArrow && + !key.rightArrow && + !key.home && + !key.end && + !key.pageUp && + !key.pageDown && + !key.focusIn && + !key.focusOut && + !input.startsWith("\u001B"); + + if (isPlainText) { + return insertText(state, input.replace(/\r/g, "")); + } + + return null; +} + function buildOptions(question: AskUserQuestionItem | undefined): OptionEntry[] { if (!question) { return [];