mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(ui): unbind history recall from arrow keys; use Ctrl+P / Ctrl+N
Mouse wheel on Windows Terminal + ConPTY translates to up/down arrow key events for raw-mode child processes. Indistinguishable from keyboard arrows at the byte level. With history recall bound to up/down on empty buffer, every wheel-scroll on the prompt was clobbering it with a recalled prior message. Fix: rebind history recall to Ctrl+P (prev) / Ctrl+N (next), the bash readline convention. Up/down arrows now no-op on a single- line buffer and only move the cursor across lines in a multi-line draft. Wheel scrolls again belong entirely to the terminal. Tests updated to reflect the new binding. Placeholder hint mentions Ctrl+P/N alongside the Ctrl+J newline hint.
This commit is contained in:
1 parent
1d3b9e9c77
commit
9254d3ae87
4 files changed
+69
-46
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reasonix",
|
||||
"version": "0.7.9",
|
||||
"version": "0.7.10",
|
||||
"description": "DeepSeek-native coding agent: cache-first loop, flash-first cost control, tool-call repair.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -176,7 +176,7 @@ export function PromptInput({
|
||||
|
||||
const placeholderActive = narrow
|
||||
? "type a message, or /command"
|
||||
: "type a message, or /command · [Ctrl+J] newline (Shift+Enter where supported)";
|
||||
: "type a message, or /command · [Ctrl+J] newline · [Ctrl+P/N] history";
|
||||
const effectivePlaceholder = disabled
|
||||
? (placeholder ?? "…waiting for response…")
|
||||
: (placeholder ?? placeholderActive);
|
||||
|
||||
@@ -107,12 +107,21 @@ export function processMultilineKey(
|
||||
return cursor === value.length ? NOOP : { next: null, cursor: value.length, submit: false };
|
||||
}
|
||||
|
||||
// Empty buffer + ↑/↓ → parent handles history recall.
|
||||
if (value.length === 0 && (key.upArrow || key.downArrow)) {
|
||||
return { ...NOOP, historyHandoff: key.upArrow ? "prev" : "next" };
|
||||
// History recall is bound to Ctrl+P / Ctrl+N (bash readline
|
||||
// convention) rather than ↑/↓. Reason: Windows Terminal + ConPTY
|
||||
// translates mouse wheel events to ↑/↓ keystrokes for raw-mode
|
||||
// child processes — indistinguishable at the byte level. If ↑/↓
|
||||
// recalled history, every wheel-scroll would corrupt the prompt
|
||||
// with a prior message. Ctrl+P/N is wheel-immune.
|
||||
if (key.ctrl && key.input === "p") {
|
||||
return { ...NOOP, historyHandoff: "prev" };
|
||||
}
|
||||
if (key.ctrl && key.input === "n") {
|
||||
return { ...NOOP, historyHandoff: "next" };
|
||||
}
|
||||
|
||||
// Cursor motion.
|
||||
// Cursor motion. Empty single-line buffer + ↑/↓ is now a no-op
|
||||
// (used to recall history; see Ctrl+P/N comment above).
|
||||
if (key.leftArrow) {
|
||||
return { next: null, cursor: Math.max(0, cursor - 1), submit: false };
|
||||
}
|
||||
@@ -121,16 +130,11 @@ export function processMultilineKey(
|
||||
}
|
||||
if (key.upArrow) {
|
||||
const moved = moveCursorUp(value, cursor);
|
||||
// Cursor already on the first line — hand off to history recall
|
||||
// instead of sitting stuck. Users can now escape into history
|
||||
// from the middle of a multi-line draft.
|
||||
if (moved === cursor) return { ...NOOP, historyHandoff: "prev" };
|
||||
return { next: null, cursor: moved, submit: false };
|
||||
return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
|
||||
}
|
||||
if (key.downArrow) {
|
||||
const moved = moveCursorDown(value, cursor);
|
||||
if (moved === cursor) return { ...NOOP, historyHandoff: "next" };
|
||||
return { next: null, cursor: moved, submit: false };
|
||||
return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
|
||||
}
|
||||
|
||||
// Emacs-style line jumps (universal across terminals; Home/End aren't
|
||||
|
||||
@@ -148,64 +148,76 @@ describe("processMultilineKey — cursor motion", () => {
|
||||
expect(processMultilineKey("abc", 3, key({ rightArrow: true })).cursor).toBe(3);
|
||||
});
|
||||
|
||||
it("↑/↓ on a single-line non-empty buffer hands off to history recall (nowhere to move)", () => {
|
||||
// 0.7.1: previously this was a pure NOOP, which left the user
|
||||
// feeling stuck when they wanted to recall a prior prompt from
|
||||
// a non-empty buffer. Now we emit `historyHandoff` so the
|
||||
// parent can decide — typically swap the buffer for the
|
||||
// previous prompt.
|
||||
it("↑/↓ on a single-line non-empty buffer is a NOOP (no history recall)", () => {
|
||||
// 0.7.10: history recall moved to Ctrl+P/N because mouse wheel
|
||||
// on Windows ConPTY translates to ↑/↓ events, indistinguishable
|
||||
// from keyboard. Wheel scrolling was clobbering the prompt with
|
||||
// recalled history. Now ↑/↓ does nothing on a single-line
|
||||
// buffer; users press Ctrl+P / Ctrl+N for history.
|
||||
const up = processMultilineKey("hello", 3, key({ upArrow: true }));
|
||||
expect(up).toEqual({ next: null, cursor: null, submit: false, historyHandoff: "prev" });
|
||||
expect(up).toEqual({ next: null, cursor: null, submit: false });
|
||||
const down = processMultilineKey("hello", 3, key({ downArrow: true }));
|
||||
expect(down).toEqual({ next: null, cursor: null, submit: false, historyHandoff: "next" });
|
||||
expect(down).toEqual({ next: null, cursor: null, submit: false });
|
||||
});
|
||||
|
||||
it("↑/↓ on an empty buffer hands off to the parent (history recall)", () => {
|
||||
it("↑/↓ on an empty buffer is a NOOP", () => {
|
||||
expect(processMultilineKey("", 0, key({ upArrow: true }))).toEqual({
|
||||
next: null,
|
||||
cursor: null,
|
||||
submit: false,
|
||||
historyHandoff: "prev",
|
||||
});
|
||||
expect(processMultilineKey("", 0, key({ downArrow: true }))).toEqual({
|
||||
next: null,
|
||||
cursor: null,
|
||||
submit: false,
|
||||
historyHandoff: "next",
|
||||
});
|
||||
});
|
||||
|
||||
it("↑ at line 0 of a multi-line buffer hands off to history (escape hatch from a draft)", () => {
|
||||
// Cursor on first line, buffer has a second line below — ↑ has
|
||||
// nowhere to move, so we emit the handoff. Previously this was
|
||||
// a silent NOOP that trapped the user.
|
||||
const v = "first\nsecond";
|
||||
const handoff = processMultilineKey(v, 3, key({ upArrow: true }));
|
||||
expect(handoff).toEqual({
|
||||
it("Ctrl+P / Ctrl+N hand off to history recall (bash readline binding)", () => {
|
||||
expect(processMultilineKey("", 0, key({ ctrl: true, input: "p" }))).toEqual({
|
||||
next: null,
|
||||
cursor: null,
|
||||
submit: false,
|
||||
historyHandoff: "prev",
|
||||
});
|
||||
expect(processMultilineKey("", 0, key({ ctrl: true, input: "n" }))).toEqual({
|
||||
next: null,
|
||||
cursor: null,
|
||||
submit: false,
|
||||
historyHandoff: "next",
|
||||
});
|
||||
// Also from a non-empty buffer — Ctrl+P/N is unambiguous, no
|
||||
// need for buffer-state gating like the old ↑/↓ binding.
|
||||
expect(processMultilineKey("hello", 3, key({ ctrl: true, input: "p" })).historyHandoff).toBe(
|
||||
"prev",
|
||||
);
|
||||
});
|
||||
|
||||
it("↓ at last line hands off similarly", () => {
|
||||
// cursor at pos 8 = col 2 on line 1 ("second")
|
||||
it("↑ at line 0 of a multi-line buffer is a NOOP (no auto history handoff)", () => {
|
||||
const v = "first\nsecond";
|
||||
const handoff = processMultilineKey(v, 8, key({ downArrow: true }));
|
||||
expect(handoff.historyHandoff).toBe("next");
|
||||
const result = processMultilineKey(v, 3, key({ upArrow: true }));
|
||||
expect(result).toEqual({ next: null, cursor: null, submit: false });
|
||||
});
|
||||
|
||||
it("raw `\\x1b[A` escape sequence is rewritten into upArrow (Windows-terminal fallback)", () => {
|
||||
// Some Windows consoles don't set key.upArrow; the raw CSI leaks
|
||||
// through as `input` and would otherwise be inserted verbatim
|
||||
// into the buffer. Rewrite into structured key instead.
|
||||
const handoff = processMultilineKey("", 0, { input: "\x1b[A" });
|
||||
expect(handoff.historyHandoff).toBe("prev");
|
||||
it("↓ at last line is a NOOP", () => {
|
||||
const v = "first\nsecond";
|
||||
const result = processMultilineKey(v, 8, key({ downArrow: true }));
|
||||
expect(result).toEqual({ next: null, cursor: null, submit: false });
|
||||
});
|
||||
|
||||
it("raw `\\x1b[A` escape sequence is rewritten into upArrow (no history handoff)", () => {
|
||||
// Recovery still runs (so wheel-as-↑ doesn't insert literal
|
||||
// `[A` into the buffer), but ↑ now no-ops instead of recalling.
|
||||
const result = processMultilineKey("", 0, { input: "\x1b[A" });
|
||||
expect(result).toEqual({ next: null, cursor: null, submit: false });
|
||||
});
|
||||
|
||||
it("raw `\\x1b[B` becomes downArrow; `\\x1b[C` rightArrow; `\\x1b[D` leftArrow", () => {
|
||||
expect(processMultilineKey("", 0, { input: "\x1b[B" }).historyHandoff).toBe("next");
|
||||
expect(processMultilineKey("", 0, { input: "\x1b[B" })).toEqual({
|
||||
next: null,
|
||||
cursor: null,
|
||||
submit: false,
|
||||
});
|
||||
expect(processMultilineKey("abc", 0, { input: "\x1b[C" }).cursor).toBe(1);
|
||||
expect(processMultilineKey("abc", 2, { input: "\x1b[D" }).cursor).toBe(1);
|
||||
});
|
||||
@@ -215,12 +227,19 @@ describe("processMultilineKey — cursor motion", () => {
|
||||
// remaining `[C` through useInput as plain text. Without the
|
||||
// ESC-less fallback, pressing right-arrow at end of a line would
|
||||
// insert literal `[C` instead of moving the cursor across the
|
||||
// newline boundary — which surfaced as "right arrow can't reach
|
||||
// line 2" on Windows.
|
||||
// newline boundary.
|
||||
expect(processMultilineKey("ab\ncd", 2, { input: "[C" }).cursor).toBe(3);
|
||||
expect(processMultilineKey("ab\ncd", 3, { input: "[D" }).cursor).toBe(2);
|
||||
expect(processMultilineKey("", 0, { input: "[A" }).historyHandoff).toBe("prev");
|
||||
expect(processMultilineKey("", 0, { input: "[B" }).historyHandoff).toBe("next");
|
||||
expect(processMultilineKey("", 0, { input: "[A" })).toEqual({
|
||||
next: null,
|
||||
cursor: null,
|
||||
submit: false,
|
||||
});
|
||||
expect(processMultilineKey("", 0, { input: "[B" })).toEqual({
|
||||
next: null,
|
||||
cursor: null,
|
||||
submit: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("↑ moves cursor to the previous line, preserving column when possible", () => {
|
||||
|
||||
Reference in new issue
Block a user