authorgravatar for 118054752+extoci@users.noreply.github.comExotic <118054752+extoci@users.noreply.github.com> 2026-09-03 19:22:06+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 09:22:06-07:00
log77e35c561259733d880ab62a43aad0894d301d9b
tree6b4092239d75930a54de5cc65bbac3d5701c4301
parentfff33f9e851912363c5b1f3ac65598be35eb5f0d
signature Signed by PGP key B5690EEEBB952194

fix(web): send cited messages with Cmd+Enter (#9307)


5 files changed, 49 insertions(+), 4 deletions(-)

apps/web/src/components/ComposerCitationNode.tsx+7-1
......@@ -47,7 +47,8 @@ export type ComposerCitationCommentTarget = {
4747export const ComposerCitationCommentContext = createContext<{
4848 openComment: ComposerCitationCommentTarget | null;
4949 onOpenChange: (nodeKey: NodeKey, open: boolean) => void;
50}>({ openComment: null, onOpenChange: () => {} });
50 onSubmitAndSend: () => void;
51}>({ openComment: null, onOpenChange: () => {}, onSubmitAndSend: () => {} });
5152
5253/** Consume a cite action once its controlled prompt has been committed to the editor. */
5354export function $consumeComposerCitationCommentRequest(requestRef: {
......@@ -127,6 +128,11 @@ function ComposerCitationDecorator(props: { citation: AssistantCitation; nodeKey
127128 commentContext.onOpenChange(props.nodeKey, open);
128129 },
129130 onSave: onSaveComment,
131 onSaveAndSend: (comment) => {
132 if (!onSaveComment(comment)) return false;
133 commentContext.onSubmitAndSend();
134 return true;
135 },
130136 }}
131137 onRemove={onRemove}
132138 />
apps/web/src/components/ComposerPromptEditor.tsx+6-1
......@@ -924,6 +924,7 @@ interface ComposerPromptEditorProps {
924924 onPageScrollKeyDown?: (key: "PageUp" | "PageDown") => void;
925925 onPageScrollKeyUp?: (key: string) => void;
926926 onPageScrollRelease?: () => void;
927 onCitationSubmitAndSend?: () => void;
927928 onPaste: React.ClipboardEventHandler<HTMLElement>;
928929 editorRef: React.RefObject<ComposerPromptEditorHandle | null>;
929930}
......@@ -1571,6 +1572,7 @@ function ComposerPromptEditorInner({
15711572 onPageScrollKeyDown,
15721573 onPageScrollKeyUp,
15731574 onPageScrollRelease,
1575 onCitationSubmitAndSend,
15741576 onPaste,
15751577 editorRef,
15761578}: ComposerPromptEditorProps) {
......@@ -1603,8 +1605,9 @@ function ComposerPromptEditorInner({
16031605 open ? { nodeKey } : current?.nodeKey === nodeKey ? null : current,
16041606 );
16051607 },
1608 onSubmitAndSend: onCitationSubmitAndSend ?? (() => {}),
16061609 }),
1607 [openCitationComment],
1610 [onCitationSubmitAndSend, openCitationComment],
16081611 );
16091612 const terminalContextActions = useMemo(
16101613 () => ({ onRemoveTerminalContext }),
......@@ -1969,6 +1972,7 @@ export function ComposerPromptEditor({
19691972 onPageScrollKeyDown,
19701973 onPageScrollKeyUp,
19711974 onPageScrollRelease,
1975 onCitationSubmitAndSend,
19721976 onPaste,
19731977 editorRef,
19741978}: ComposerPromptEditorProps) {
......@@ -2013,6 +2017,7 @@ export function ComposerPromptEditor({
20132017 onChange={onChange}
20142018 {...(onVisibleSelectionChange ? { onVisibleSelectionChange } : {})}
20152019 onPaste={onPaste}
2020 {...(onCitationSubmitAndSend ? { onCitationSubmitAndSend } : {})}
20162021 editorRef={editorRef}
20172022 {...(onCommandKeyDown ? { onCommandKeyDown } : {})}
20182023 {...(onPageScrollKeyDown ? { onPageScrollKeyDown } : {})}
apps/web/src/components/chat/AssistantCitationChip.tsx+10
......@@ -42,6 +42,7 @@ export function AssistantCitationChip({
4242 sourceAnchor?: AssistantCitationSourceAnchor | undefined;
4343 onOpenChange: (open: boolean) => void;
4444 onSave: (comment: string) => boolean;
45 onSaveAndSend?: (comment: string) => boolean;
4546 };
4647}) {
4748 const navigate = useNavigate();
......@@ -158,6 +159,15 @@ export function AssistantCitationChip({
158159 commentEditor.onOpenChange(false);
159160 return true;
160161 }}
162 {...(commentEditor.onSaveAndSend
163 ? {
164 onSubmitAndSend: (comment: string) => {
165 if (!commentEditor.onSaveAndSend?.(comment)) return false;
166 commentEditor.onOpenChange(false);
167 return true;
168 },
169 }
170 : {})}
161171 onCancel={() => commentEditor.onOpenChange(false)}
162172 />
163173 </PopoverPopup>
apps/web/src/components/chat/AssistantCitationCommentEditor.tsx+16-2
......@@ -7,11 +7,13 @@ export function AssistantCitationCommentEditor({
77 citation,
88 inputRef,
99 onSubmit,
10 onSubmitAndSend,
1011 onCancel,
1112}: {
1213 citation: AssistantCitation;
1314 inputRef?: Ref<HTMLTextAreaElement>;
1415 onSubmit: (comment: string) => boolean;
16 onSubmitAndSend?: (comment: string) => boolean;
1517 onCancel: () => void;
1618}) {
1719 const [comment, setComment] = useState(citation.comment ?? "");
......@@ -19,6 +21,14 @@ export function AssistantCitationCommentEditor({
1921 const submit = () => {
2022 if (!commentTooLong) onSubmit(comment);
2123 };
24 const submitAndSend = () => {
25 if (commentTooLong) return;
26 if (onSubmitAndSend) {
27 onSubmitAndSend(comment);
28 } else {
29 onSubmit(comment);
30 }
31 };
2232
2333 return (
2434 <div
......@@ -35,7 +45,7 @@ export function AssistantCitationCommentEditor({
3545 <textarea
3646 ref={inputRef}
3747 aria-label="Comment on selected text"
38 aria-description="Enter to save the citation comment; Shift+Enter for a new line."
48 aria-description="Enter to save the citation comment; Command/Ctrl+Enter to save and send; Shift+Enter for a new line."
3949 aria-invalid={commentTooLong || undefined}
4050 placeholder="Add an optional comment..."
4151 rows={2}
......@@ -50,7 +60,11 @@ export function AssistantCitationCommentEditor({
5060 event.keyCode !== 229
5161 ) {
5262 event.preventDefault();
53 submit();
63 if (event.metaKey || event.ctrlKey) {
64 submitAndSend();
65 } else {
66 submit();
67 }
5468 }
5569 }}
5670 />
apps/web/src/components/chat/ChatComposer.tsx+10
......@@ -2824,6 +2824,15 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
28242824 shouldBlurMobileComposerOnSubmit,
28252825 ],
28262826 );
2827 const submitCitationAndSend = useCallback(() => {
2828 const intent = composerSubmissionIntentForEnter({
2829 isMobileViewport,
2830 shiftKey: false,
2831 modifierKey: true,
2832 isDraftThread: routeKind === "draft",
2833 });
2834 submitComposer(undefined, intent ?? "foreground");
2835 }, [isMobileViewport, routeKind, submitComposer]);
28272836 const compactThreadContext = useCallback(() => {
28282837 if (
28292838 compactDisabled ||
......@@ -5246,6 +5255,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
52465255 onPageScrollKeyDown={onPageScrollKeyDown}
52475256 onPageScrollKeyUp={onPageScrollKeyUp}
52485257 onPageScrollRelease={onPageScrollRelease}
5258 onCitationSubmitAndSend={submitCitationAndSend}
52495259 onPaste={onComposerPaste}
52505260 placeholder={
52515261 isComposerApprovalState