mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 04:00:36 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
parent
096fabb5d6
commit
eb95c131be
644 changed files with 70389 additions and 23709 deletions
|
|
@ -29,6 +29,11 @@ import {
|
|||
KITTY_KEYCODE_NUMPAD_ENTER,
|
||||
KITTY_KEYCODE_TAB,
|
||||
MAX_KITTY_SEQUENCE_LENGTH,
|
||||
KITTY_MODIFIER_BASE,
|
||||
KITTY_MODIFIER_EVENT_TYPES_OFFSET,
|
||||
MODIFIER_SHIFT_BIT,
|
||||
MODIFIER_ALT_BIT,
|
||||
MODIFIER_CTRL_BIT,
|
||||
} from '../utils/platformConstants.js';
|
||||
|
||||
import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
|
||||
|
|
@ -36,6 +41,9 @@ import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
|
|||
const ESC = '\u001B';
|
||||
export const PASTE_MODE_PREFIX = `${ESC}[200~`;
|
||||
export const PASTE_MODE_SUFFIX = `${ESC}[201~`;
|
||||
export const DRAG_COMPLETION_TIMEOUT_MS = 100; // Broadcast full path after 100ms if no more input
|
||||
export const SINGLE_QUOTE = "'";
|
||||
export const DOUBLE_QUOTE = '"';
|
||||
|
||||
export interface Key {
|
||||
name: string;
|
||||
|
|
@ -83,6 +91,9 @@ export function KeypressProvider({
|
|||
}) {
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
const subscribers = useRef<Set<KeypressHandler>>(new Set()).current;
|
||||
const isDraggingRef = useRef(false);
|
||||
const dragBufferRef = useRef('');
|
||||
const draggingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const subscribe = useCallback(
|
||||
(handler: KeypressHandler) => {
|
||||
|
|
@ -99,7 +110,17 @@ export function KeypressProvider({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRawMode(true);
|
||||
const clearDraggingTimer = () => {
|
||||
if (draggingTimerRef.current) {
|
||||
clearTimeout(draggingTimerRef.current);
|
||||
draggingTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const wasRaw = stdin.isRaw;
|
||||
if (wasRaw === false) {
|
||||
setRawMode(true);
|
||||
}
|
||||
|
||||
const keypressStream = new PassThrough();
|
||||
let usePassthrough = false;
|
||||
|
|
@ -116,48 +137,244 @@ export function KeypressProvider({
|
|||
let rawDataBuffer = Buffer.alloc(0);
|
||||
let rawFlushTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const parseKittySequence = (sequence: string): Key | null => {
|
||||
const kittyPattern = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])$`);
|
||||
const match = sequence.match(kittyPattern);
|
||||
if (!match) return null;
|
||||
|
||||
const keyCode = parseInt(match[1], 10);
|
||||
const modifiers = match[3] ? parseInt(match[3], 10) : 1;
|
||||
const modifierBits = modifiers - 1;
|
||||
const shift = (modifierBits & 1) === 1;
|
||||
const alt = (modifierBits & 2) === 2;
|
||||
const ctrl = (modifierBits & 4) === 4;
|
||||
|
||||
const keyNameMap: Record<number, string> = {
|
||||
[CHAR_CODE_ESC]: 'escape',
|
||||
[KITTY_KEYCODE_TAB]: 'tab',
|
||||
[KITTY_KEYCODE_BACKSPACE]: 'backspace',
|
||||
[KITTY_KEYCODE_ENTER]: 'return',
|
||||
[KITTY_KEYCODE_NUMPAD_ENTER]: 'return',
|
||||
};
|
||||
|
||||
if (keyCode in keyNameMap) {
|
||||
// Parse a single complete kitty sequence from the start (prefix) of the
|
||||
// buffer and return both the Key and the number of characters consumed.
|
||||
// This lets us "peel off" one complete event when multiple sequences arrive
|
||||
// in a single chunk, preventing buffer overflow and fragmentation.
|
||||
// Parse a single complete kitty/parameterized/legacy sequence from the start
|
||||
// of the buffer and return both the parsed Key and the number of characters
|
||||
// consumed. This enables peel-and-continue parsing for batched input.
|
||||
const parseKittyPrefix = (
|
||||
buffer: string,
|
||||
): { key: Key; length: number } | null => {
|
||||
// In older terminals ESC [ Z was used as Cursor Backward Tabulation (CBT)
|
||||
// In newer terminals the same functionality of key combination for moving
|
||||
// backward through focusable elements is Shift+Tab, hence we will
|
||||
// map ESC [ Z to Shift+Tab
|
||||
// 0) Reverse Tab (legacy): ESC [ Z
|
||||
// Treat as Shift+Tab for UI purposes.
|
||||
// Regex parts:
|
||||
// ^ - start of buffer
|
||||
// ESC [ - CSI introducer
|
||||
// Z - legacy reverse tab
|
||||
const revTabLegacy = new RegExp(`^${ESC}\\[Z`);
|
||||
let m = buffer.match(revTabLegacy);
|
||||
if (m) {
|
||||
return {
|
||||
name: keyNameMap[keyCode],
|
||||
ctrl,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence,
|
||||
kittyProtocol: true,
|
||||
key: {
|
||||
name: 'tab',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: true,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
if (keyCode >= 97 && keyCode <= 122 && ctrl) {
|
||||
const letter = String.fromCharCode(keyCode);
|
||||
// 1) Reverse Tab (parameterized): ESC [ 1 ; <mods> Z
|
||||
// Parameterized reverse Tab: ESC [ 1 ; <mods> Z
|
||||
const revTabParam = new RegExp(`^${ESC}\\[1;(\\d+)Z`);
|
||||
m = buffer.match(revTabParam);
|
||||
if (m) {
|
||||
let mods = parseInt(m[1], 10);
|
||||
if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
|
||||
mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
|
||||
}
|
||||
const bits = mods - KITTY_MODIFIER_BASE;
|
||||
const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
|
||||
const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
|
||||
return {
|
||||
name: letter,
|
||||
ctrl: true,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence,
|
||||
kittyProtocol: true,
|
||||
key: {
|
||||
name: 'tab',
|
||||
ctrl,
|
||||
meta: alt,
|
||||
// Reverse tab implies Shift behavior; force shift regardless of mods
|
||||
shift: true,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
// 2) Parameterized functional: ESC [ 1 ; <mods> (A|B|C|D|H|F|P|Q|R|S)
|
||||
// 2) Parameterized functional: ESC [ 1 ; <mods> (A|B|C|D|H|F|P|Q|R|S)
|
||||
// Arrows, Home/End, F1–F4 with modifiers encoded in <mods>.
|
||||
const arrowPrefix = new RegExp(`^${ESC}\\[1;(\\d+)([ABCDHFPQSR])`);
|
||||
m = buffer.match(arrowPrefix);
|
||||
if (m) {
|
||||
let mods = parseInt(m[1], 10);
|
||||
if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
|
||||
mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
|
||||
}
|
||||
const bits = mods - KITTY_MODIFIER_BASE;
|
||||
const shift = (bits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT;
|
||||
const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
|
||||
const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
|
||||
const sym = m[2];
|
||||
const symbolToName: { [k: string]: string } = {
|
||||
A: 'up',
|
||||
B: 'down',
|
||||
C: 'right',
|
||||
D: 'left',
|
||||
H: 'home',
|
||||
F: 'end',
|
||||
P: 'f1',
|
||||
Q: 'f2',
|
||||
R: 'f3',
|
||||
S: 'f4',
|
||||
};
|
||||
const name = symbolToName[sym] || '';
|
||||
if (!name) return null;
|
||||
return {
|
||||
key: {
|
||||
name,
|
||||
ctrl,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
// 3) CSI-u form: ESC [ <code> ; <mods> (u|~)
|
||||
// 3) CSI-u and tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
|
||||
// 'u' terminator: Kitty CSI-u; '~' terminator: tilde-coded function keys.
|
||||
const csiUPrefix = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])`);
|
||||
m = buffer.match(csiUPrefix);
|
||||
if (m) {
|
||||
const keyCode = parseInt(m[1], 10);
|
||||
let modifiers = m[3] ? parseInt(m[3], 10) : KITTY_MODIFIER_BASE;
|
||||
if (modifiers >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
|
||||
modifiers -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
|
||||
}
|
||||
const modifierBits = modifiers - KITTY_MODIFIER_BASE;
|
||||
const shift =
|
||||
(modifierBits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT;
|
||||
const alt = (modifierBits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
|
||||
const ctrl = (modifierBits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
|
||||
const terminator = m[4];
|
||||
|
||||
// Tilde-coded functional keys (Delete, Insert, PageUp/Down, Home/End)
|
||||
if (terminator === '~') {
|
||||
let name: string | null = null;
|
||||
switch (keyCode) {
|
||||
case 1:
|
||||
name = 'home';
|
||||
break;
|
||||
case 2:
|
||||
name = 'insert';
|
||||
break;
|
||||
case 3:
|
||||
name = 'delete';
|
||||
break;
|
||||
case 4:
|
||||
name = 'end';
|
||||
break;
|
||||
case 5:
|
||||
name = 'pageup';
|
||||
break;
|
||||
case 6:
|
||||
name = 'pagedown';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (name) {
|
||||
return {
|
||||
key: {
|
||||
name,
|
||||
ctrl,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const kittyKeyCodeToName: { [key: number]: string } = {
|
||||
[CHAR_CODE_ESC]: 'escape',
|
||||
[KITTY_KEYCODE_TAB]: 'tab',
|
||||
[KITTY_KEYCODE_BACKSPACE]: 'backspace',
|
||||
[KITTY_KEYCODE_ENTER]: 'return',
|
||||
[KITTY_KEYCODE_NUMPAD_ENTER]: 'return',
|
||||
};
|
||||
|
||||
const name = kittyKeyCodeToName[keyCode];
|
||||
if (name) {
|
||||
return {
|
||||
key: {
|
||||
name,
|
||||
ctrl,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
// Ctrl+letters
|
||||
if (
|
||||
ctrl &&
|
||||
keyCode >= 'a'.charCodeAt(0) &&
|
||||
keyCode <= 'z'.charCodeAt(0)
|
||||
) {
|
||||
const letter = String.fromCharCode(keyCode);
|
||||
return {
|
||||
key: {
|
||||
name: letter,
|
||||
ctrl: true,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Legacy function keys (no parameters): ESC [ (A|B|C|D|H|F)
|
||||
// Arrows + Home/End without modifiers.
|
||||
const legacyFuncKey = new RegExp(`^${ESC}\\[([ABCDHF])`);
|
||||
m = buffer.match(legacyFuncKey);
|
||||
if (m) {
|
||||
const sym = m[1];
|
||||
const nameMap: { [key: string]: string } = {
|
||||
A: 'up',
|
||||
B: 'down',
|
||||
C: 'right',
|
||||
D: 'left',
|
||||
H: 'home',
|
||||
F: 'end',
|
||||
};
|
||||
const name = nameMap[sym]!;
|
||||
return {
|
||||
key: {
|
||||
name,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -171,6 +388,9 @@ export function KeypressProvider({
|
|||
};
|
||||
|
||||
const handleKeypress = (_: unknown, key: Key) => {
|
||||
if (key.sequence === FOCUS_IN || key.sequence === FOCUS_OUT) {
|
||||
return;
|
||||
}
|
||||
if (key.name === 'paste-start') {
|
||||
isPaste = true;
|
||||
return;
|
||||
|
|
@ -194,6 +414,27 @@ export function KeypressProvider({
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
key.sequence === SINGLE_QUOTE ||
|
||||
key.sequence === DOUBLE_QUOTE ||
|
||||
isDraggingRef.current
|
||||
) {
|
||||
isDraggingRef.current = true;
|
||||
dragBufferRef.current += key.sequence;
|
||||
|
||||
clearDraggingTimer();
|
||||
draggingTimerRef.current = setTimeout(() => {
|
||||
isDraggingRef.current = false;
|
||||
const seq = dragBufferRef.current;
|
||||
dragBufferRef.current = '';
|
||||
if (seq) {
|
||||
broadcast({ ...key, name: '', paste: true, sequence: seq });
|
||||
}
|
||||
}, DRAG_COMPLETION_TIMEOUT_MS);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.name === 'return' && waitingForEnterAfterBackslash) {
|
||||
if (backslashTimeout) {
|
||||
clearTimeout(backslashTimeout);
|
||||
|
|
@ -285,18 +526,51 @@ export function KeypressProvider({
|
|||
);
|
||||
}
|
||||
|
||||
const kittyKey = parseKittySequence(kittySequenceBuffer);
|
||||
if (kittyKey) {
|
||||
if (debugKeystrokeLogging) {
|
||||
console.log(
|
||||
'[DEBUG] Kitty sequence parsed successfully:',
|
||||
kittySequenceBuffer,
|
||||
);
|
||||
// Try to peel off as many complete sequences as are available at the
|
||||
// start of the buffer. This handles batched inputs cleanly. If the
|
||||
// prefix is incomplete or invalid, skip to the next CSI introducer
|
||||
// (ESC[) so that a following valid sequence can still be parsed.
|
||||
let parsedAny = false;
|
||||
while (kittySequenceBuffer) {
|
||||
const parsed = parseKittyPrefix(kittySequenceBuffer);
|
||||
if (!parsed) {
|
||||
// Look for the next potential CSI start beyond index 0
|
||||
const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1);
|
||||
if (nextStart > 0) {
|
||||
if (debugKeystrokeLogging) {
|
||||
console.log(
|
||||
'[DEBUG] Skipping incomplete/invalid CSI prefix:',
|
||||
kittySequenceBuffer.slice(0, nextStart),
|
||||
);
|
||||
}
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(nextStart);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
kittySequenceBuffer = '';
|
||||
broadcast(kittyKey);
|
||||
return;
|
||||
if (debugKeystrokeLogging) {
|
||||
const parsedSequence = kittySequenceBuffer.slice(
|
||||
0,
|
||||
parsed.length,
|
||||
);
|
||||
if (kittySequenceBuffer.length > parsed.length) {
|
||||
console.log(
|
||||
'[DEBUG] Kitty sequence parsed successfully (prefix):',
|
||||
parsedSequence,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'[DEBUG] Kitty sequence parsed successfully:',
|
||||
parsedSequence,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Consume the parsed prefix and broadcast it.
|
||||
kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length);
|
||||
broadcast(parsed.key);
|
||||
parsedAny = true;
|
||||
}
|
||||
if (parsedAny) return;
|
||||
|
||||
if (config?.getDebugMode() || debugKeystrokeLogging) {
|
||||
const codes = Array.from(kittySequenceBuffer).map((ch) =>
|
||||
|
|
@ -470,7 +744,9 @@ export function KeypressProvider({
|
|||
rl.close();
|
||||
|
||||
// Restore the terminal to its original state.
|
||||
setRawMode(false);
|
||||
if (wasRaw === false) {
|
||||
setRawMode(false);
|
||||
}
|
||||
|
||||
if (backslashTimeout) {
|
||||
clearTimeout(backslashTimeout);
|
||||
|
|
@ -494,6 +770,23 @@ export function KeypressProvider({
|
|||
});
|
||||
pasteBuffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
if (draggingTimerRef.current) {
|
||||
clearTimeout(draggingTimerRef.current);
|
||||
draggingTimerRef.current = null;
|
||||
}
|
||||
if (isDraggingRef.current && dragBufferRef.current) {
|
||||
broadcast({
|
||||
name: '',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: true,
|
||||
sequence: dragBufferRef.current,
|
||||
});
|
||||
isDraggingRef.current = false;
|
||||
dragBufferRef.current = '';
|
||||
}
|
||||
};
|
||||
}, [
|
||||
stdin,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue