Skip to content

Commit f84e7ef

Browse files
authored
fix(prompt-input): prevent extra line break after send (#136)
1 parent 24cdaf4 commit f84e7ef

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {
2+
COMMAND_PRIORITY_CRITICAL,
3+
KEY_ENTER_COMMAND,
4+
type LexicalEditor,
5+
} from "lexical";
6+
7+
/**
8+
* The prompt composer handles plain Enter during React's capture phase.
9+
* Lexical's plain-text plugin does not honor `defaultPrevented` and would
10+
* otherwise insert a trailing line break after the prompt has been sent.
11+
*/
12+
export function registerPromptLexicalPreventedEnterCommand(
13+
editor: LexicalEditor,
14+
) {
15+
return editor.registerCommand(
16+
KEY_ENTER_COMMAND,
17+
(event) => event?.defaultPrevented === true,
18+
COMMAND_PRIORITY_CRITICAL,
19+
);
20+
}

src/components/ai-elements/prompt-lexical-editor.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ import {
5454
} from "@/lib/prompt-token-chips";
5555
import type { WorkspaceInformationReferenceOption } from "@/lib/workspace-information-references";
5656
import { cn } from "@/lib/utils";
57+
import {
58+
registerPromptLexicalPreventedEnterCommand,
59+
} from "./prompt-lexical-editor.commands";
5760
import { PromptTokenChip } from "./prompt-token-chip";
5861

5962
const PROMPT_SYNC_TAG = "stave-prompt-sync";
@@ -576,6 +579,17 @@ function PromptLexicalTokenDeletionPlugin() {
576579
return null;
577580
}
578581

582+
function PromptLexicalPreventedEnterPlugin() {
583+
const [editor] = useLexicalComposerContext();
584+
585+
useEffect(
586+
() => registerPromptLexicalPreventedEnterCommand(editor),
587+
[editor],
588+
);
589+
590+
return null;
591+
}
592+
579593
function PromptLexicalImperativePlugin(args: {
580594
forwardedRef: ForwardedRef<PromptLexicalEditorHandle>;
581595
}) {
@@ -783,6 +797,7 @@ export const PromptLexicalEditor = forwardRef<
783797
<HistoryPlugin />
784798
<PromptLexicalEditablePlugin disabled={props.disabled} />
785799
<PromptLexicalSelectionPlugin onSelectionChange={props.onSelectionChange} />
800+
<PromptLexicalPreventedEnterPlugin />
786801
<PromptLexicalTokenDeletionPlugin />
787802
<PromptLexicalImperativePlugin forwardedRef={ref} />
788803
<PromptLexicalExternalSyncPlugin
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, test } from "bun:test";
2+
import {
3+
COMMAND_PRIORITY_EDITOR,
4+
createEditor,
5+
KEY_ENTER_COMMAND,
6+
} from "lexical";
7+
import {
8+
registerPromptLexicalPreventedEnterCommand,
9+
} from "@/components/ai-elements/prompt-lexical-editor.commands";
10+
11+
describe("prompt Lexical Enter handling", () => {
12+
test("consumes Enter after the prompt composer prevents it", () => {
13+
const editor = createEditor({ namespace: "prompt-enter-prevented" });
14+
let plainTextHandlerCalls = 0;
15+
const unregisterGuard =
16+
registerPromptLexicalPreventedEnterCommand(editor);
17+
const unregisterPlainTextHandler = editor.registerCommand(
18+
KEY_ENTER_COMMAND,
19+
() => {
20+
plainTextHandlerCalls += 1;
21+
return true;
22+
},
23+
COMMAND_PRIORITY_EDITOR,
24+
);
25+
26+
const handled = editor.dispatchCommand(KEY_ENTER_COMMAND, {
27+
defaultPrevented: true,
28+
} as KeyboardEvent);
29+
30+
expect(handled).toBe(true);
31+
expect(plainTextHandlerCalls).toBe(0);
32+
unregisterPlainTextHandler();
33+
unregisterGuard();
34+
});
35+
36+
test("leaves multiline Enter behavior to Lexical when it is not prevented", () => {
37+
const editor = createEditor({ namespace: "prompt-enter-multiline" });
38+
let plainTextHandlerCalls = 0;
39+
const unregisterGuard =
40+
registerPromptLexicalPreventedEnterCommand(editor);
41+
const unregisterPlainTextHandler = editor.registerCommand(
42+
KEY_ENTER_COMMAND,
43+
() => {
44+
plainTextHandlerCalls += 1;
45+
return true;
46+
},
47+
COMMAND_PRIORITY_EDITOR,
48+
);
49+
50+
const handled = editor.dispatchCommand(KEY_ENTER_COMMAND, {
51+
defaultPrevented: false,
52+
} as KeyboardEvent);
53+
54+
expect(handled).toBe(true);
55+
expect(plainTextHandlerCalls).toBe(1);
56+
unregisterPlainTextHandler();
57+
unregisterGuard();
58+
});
59+
});

0 commit comments

Comments
 (0)