Fix Shift+Enter for multiline input in agent terminals

xterm.js sends plain \r for Enter regardless of modifiers. Intercept
modified Enter in the custom key handler so it routes through CSI u
encoding (Kitty keyboard protocol), which Claude Code uses for
Shift+Enter newlines.
This commit is contained in:
Mohamed Boudra
2026-04-12 22:10:16 +07:00
parent 609d11ee8d
commit 0095f61121
3 changed files with 81 additions and 1 deletions

View File

@@ -317,7 +317,9 @@ export class TerminalEmulatorRuntime {
!shouldInterceptDomTerminalKey({
key: normalizedKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
metaKey: event.metaKey,
pendingModifiers: this.pendingModifiers,
})
) {

View File

@@ -57,7 +57,9 @@ describe("terminal key helpers", () => {
shouldInterceptDomTerminalKey({
key: "Escape",
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false },
}),
).toBe(false);
@@ -65,7 +67,9 @@ describe("terminal key helpers", () => {
shouldInterceptDomTerminalKey({
key: "c",
ctrlKey: true,
shiftKey: false,
altKey: false,
metaKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false },
}),
).toBe(false);
@@ -73,7 +77,9 @@ describe("terminal key helpers", () => {
shouldInterceptDomTerminalKey({
key: "c",
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
pendingModifiers: { ctrl: true, shift: false, alt: false },
}),
).toBe(true);
@@ -81,12 +87,73 @@ describe("terminal key helpers", () => {
shouldInterceptDomTerminalKey({
key: "Escape",
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: true },
}),
).toBe(true);
});
it("intercepts Enter with DOM shift modifier for CSI u encoding", () => {
expect(
shouldInterceptDomTerminalKey({
key: "Enter",
ctrlKey: false,
shiftKey: true,
altKey: false,
metaKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false },
}),
).toBe(true);
});
it("intercepts Enter with any DOM modifier for CSI u encoding", () => {
expect(
shouldInterceptDomTerminalKey({
key: "Enter",
ctrlKey: true,
shiftKey: false,
altKey: false,
metaKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false },
}),
).toBe(true);
expect(
shouldInterceptDomTerminalKey({
key: "Enter",
ctrlKey: false,
shiftKey: false,
altKey: true,
metaKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false },
}),
).toBe(true);
expect(
shouldInterceptDomTerminalKey({
key: "Enter",
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: true,
pendingModifiers: { ctrl: false, shift: false, alt: false },
}),
).toBe(true);
});
it("does not intercept plain Enter without modifiers", () => {
expect(
shouldInterceptDomTerminalKey({
key: "Enter",
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false },
}),
).toBe(false);
});
it("detects pending modifier state", () => {
expect(hasPendingTerminalModifiers({ ctrl: false, shift: false, alt: false })).toBe(false);
expect(hasPendingTerminalModifiers({ ctrl: true, shift: false, alt: false })).toBe(true);

View File

@@ -85,10 +85,21 @@ export function hasPendingTerminalModifiers(modifiers: PendingTerminalModifiers)
export function shouldInterceptDomTerminalKey(args: {
key: string;
ctrlKey: boolean;
shiftKey: boolean;
altKey: boolean;
metaKey: boolean;
pendingModifiers: PendingTerminalModifiers;
}): boolean {
return hasPendingTerminalModifiers(args.pendingModifiers);
if (hasPendingTerminalModifiers(args.pendingModifiers)) {
return true;
}
// xterm.js sends plain \r for Enter regardless of modifiers.
// Intercept modified Enter so it gets CSI u encoding (Kitty keyboard protocol),
// which Claude Code and other TUI apps use for Shift+Enter newlines.
if (args.key === "Enter" && (args.shiftKey || args.ctrlKey || args.altKey || args.metaKey)) {
return true;
}
return false;
}
export function mergeTerminalModifiers(args: {