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 = {...@@ -47,7 +47,8 @@ export type ComposerCitationCommentTarget = {
47export const ComposerCitationCommentContext = createContext<{47export const ComposerCitationCommentContext = createContext<{
48 openComment: ComposerCitationCommentTarget | null;48 openComment: ComposerCitationCommentTarget | null;
49 onOpenChange: (nodeKey: NodeKey, open: boolean) => void;49 onOpenChange: (nodeKey: NodeKey, open: boolean) => void;
50}>({ openComment: null, onOpenChange: () => {} });50 onSubmitAndSend: () => void;
51}>({ openComment: null, onOpenChange: () => {}, onSubmitAndSend: () => {} });
5152
52/** Consume a cite action once its controlled prompt has been committed to the editor. */53/** Consume a cite action once its controlled prompt has been committed to the editor. */
53export function $consumeComposerCitationCommentRequest(requestRef: {54export function $consumeComposerCitationCommentRequest(requestRef: {
...@@ -127,6 +128,11 @@ function ComposerCitationDecorator(props: { citation: AssistantCitation; nodeKey...@@ -127,6 +128,11 @@ function ComposerCitationDecorator(props: { citation: AssistantCitation; nodeKey
127 commentContext.onOpenChange(props.nodeKey, open);128 commentContext.onOpenChange(props.nodeKey, open);
128 },129 },
129 onSave: onSaveComment,130 onSave: onSaveComment,
131 onSaveAndSend: (comment) => {
132 if (!onSaveComment(comment)) return false;
133 commentContext.onSubmitAndSend();
134 return true;
135 },
130 }}136 }}
131 onRemove={onRemove}137 onRemove={onRemove}
132 />138 />
apps/web/src/components/ComposerPromptEditor.tsx+6-1
...@@ -924,6 +924,7 @@ interface ComposerPromptEditorProps {...@@ -924,6 +924,7 @@ interface ComposerPromptEditorProps {
924 onPageScrollKeyDown?: (key: "PageUp" | "PageDown") => void;924 onPageScrollKeyDown?: (key: "PageUp" | "PageDown") => void;
925 onPageScrollKeyUp?: (key: string) => void;925 onPageScrollKeyUp?: (key: string) => void;
926 onPageScrollRelease?: () => void;926 onPageScrollRelease?: () => void;
927 onCitationSubmitAndSend?: () => void;
927 onPaste: React.ClipboardEventHandler<HTMLElement>;928 onPaste: React.ClipboardEventHandler<HTMLElement>;
928 editorRef: React.RefObject<ComposerPromptEditorHandle | null>;929 editorRef: React.RefObject<ComposerPromptEditorHandle | null>;
929}930}
...@@ -1571,6 +1572,7 @@ function ComposerPromptEditorInner({...@@ -1571,6 +1572,7 @@ function ComposerPromptEditorInner({
1571 onPageScrollKeyDown,1572 onPageScrollKeyDown,
1572 onPageScrollKeyUp,1573 onPageScrollKeyUp,
1573 onPageScrollRelease,1574 onPageScrollRelease,
1575 onCitationSubmitAndSend,
1574 onPaste,1576 onPaste,
1575 editorRef,1577 editorRef,
1576}: ComposerPromptEditorProps) {1578}: ComposerPromptEditorProps) {
...@@ -1603,8 +1605,9 @@ function ComposerPromptEditorInner({...@@ -1603,8 +1605,9 @@ function ComposerPromptEditorInner({
1603 open ? { nodeKey } : current?.nodeKey === nodeKey ? null : current,1605 open ? { nodeKey } : current?.nodeKey === nodeKey ? null : current,
1604 );1606 );
1605 },1607 },
1608 onSubmitAndSend: onCitationSubmitAndSend ?? (() => {}),
1606 }),1609 }),
1607 [openCitationComment],1610 [onCitationSubmitAndSend, openCitationComment],
1608 );1611 );
1609 const terminalContextActions = useMemo(1612 const terminalContextActions = useMemo(
1610 () => ({ onRemoveTerminalContext }),1613 () => ({ onRemoveTerminalContext }),
...@@ -1969,6 +1972,7 @@ export function ComposerPromptEditor({...@@ -1969,6 +1972,7 @@ export function ComposerPromptEditor({
1969 onPageScrollKeyDown,1972 onPageScrollKeyDown,
1970 onPageScrollKeyUp,1973 onPageScrollKeyUp,
1971 onPageScrollRelease,1974 onPageScrollRelease,
1975 onCitationSubmitAndSend,
1972 onPaste,1976 onPaste,
1973 editorRef,1977 editorRef,
1974}: ComposerPromptEditorProps) {1978}: ComposerPromptEditorProps) {
...@@ -2013,6 +2017,7 @@ export function ComposerPromptEditor({...@@ -2013,6 +2017,7 @@ export function ComposerPromptEditor({
2013 onChange={onChange}2017 onChange={onChange}
2014 {...(onVisibleSelectionChange ? { onVisibleSelectionChange } : {})}2018 {...(onVisibleSelectionChange ? { onVisibleSelectionChange } : {})}
2015 onPaste={onPaste}2019 onPaste={onPaste}
2020 {...(onCitationSubmitAndSend ? { onCitationSubmitAndSend } : {})}
2016 editorRef={editorRef}2021 editorRef={editorRef}
2017 {...(onCommandKeyDown ? { onCommandKeyDown } : {})}2022 {...(onCommandKeyDown ? { onCommandKeyDown } : {})}
2018 {...(onPageScrollKeyDown ? { onPageScrollKeyDown } : {})}2023 {...(onPageScrollKeyDown ? { onPageScrollKeyDown } : {})}
apps/web/src/components/chat/AssistantCitationChip.tsx+10
...@@ -42,6 +42,7 @@ export function AssistantCitationChip({...@@ -42,6 +42,7 @@ export function AssistantCitationChip({
42 sourceAnchor?: AssistantCitationSourceAnchor | undefined;42 sourceAnchor?: AssistantCitationSourceAnchor | undefined;
43 onOpenChange: (open: boolean) => void;43 onOpenChange: (open: boolean) => void;
44 onSave: (comment: string) => boolean;44 onSave: (comment: string) => boolean;
45 onSaveAndSend?: (comment: string) => boolean;
45 };46 };
46}) {47}) {
47 const navigate = useNavigate();48 const navigate = useNavigate();
...@@ -158,6 +159,15 @@ export function AssistantCitationChip({...@@ -158,6 +159,15 @@ export function AssistantCitationChip({
158 commentEditor.onOpenChange(false);159 commentEditor.onOpenChange(false);
159 return true;160 return true;
160 }}161 }}
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 : {})}
161 onCancel={() => commentEditor.onOpenChange(false)}171 onCancel={() => commentEditor.onOpenChange(false)}
162 />172 />
163 </PopoverPopup>173 </PopoverPopup>
apps/web/src/components/chat/AssistantCitationCommentEditor.tsx+16-2
...@@ -7,11 +7,13 @@ export function AssistantCitationCommentEditor({...@@ -7,11 +7,13 @@ export function AssistantCitationCommentEditor({
7 citation,7 citation,
8 inputRef,8 inputRef,
9 onSubmit,9 onSubmit,
10 onSubmitAndSend,
10 onCancel,11 onCancel,
11}: {12}: {
12 citation: AssistantCitation;13 citation: AssistantCitation;
13 inputRef?: Ref<HTMLTextAreaElement>;14 inputRef?: Ref<HTMLTextAreaElement>;
14 onSubmit: (comment: string) => boolean;15 onSubmit: (comment: string) => boolean;
16 onSubmitAndSend?: (comment: string) => boolean;
15 onCancel: () => void;17 onCancel: () => void;
16}) {18}) {
17 const [comment, setComment] = useState(citation.comment ?? "");19 const [comment, setComment] = useState(citation.comment ?? "");
...@@ -19,6 +21,14 @@ export function AssistantCitationCommentEditor({...@@ -19,6 +21,14 @@ export function AssistantCitationCommentEditor({
19 const submit = () => {21 const submit = () => {
20 if (!commentTooLong) onSubmit(comment);22 if (!commentTooLong) onSubmit(comment);
21 };23 };
24 const submitAndSend = () => {
25 if (commentTooLong) return;
26 if (onSubmitAndSend) {
27 onSubmitAndSend(comment);
28 } else {
29 onSubmit(comment);
30 }
31 };
2232
23 return (33 return (
24 <div34 <div
...@@ -35,7 +45,7 @@ export function AssistantCitationCommentEditor({...@@ -35,7 +45,7 @@ export function AssistantCitationCommentEditor({
35 <textarea45 <textarea
36 ref={inputRef}46 ref={inputRef}
37 aria-label="Comment on selected text"47 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."
39 aria-invalid={commentTooLong || undefined}49 aria-invalid={commentTooLong || undefined}
40 placeholder="Add an optional comment..."50 placeholder="Add an optional comment..."
41 rows={2}51 rows={2}
...@@ -50,7 +60,11 @@ export function AssistantCitationCommentEditor({...@@ -50,7 +60,11 @@ export function AssistantCitationCommentEditor({
50 event.keyCode !== 22960 event.keyCode !== 229
51 ) {61 ) {
52 event.preventDefault();62 event.preventDefault();
53 submit();63 if (event.metaKey || event.ctrlKey) {
64 submitAndSend();
65 } else {
66 submit();
67 }
54 }68 }
55 }}69 }}
56 />70 />
apps/web/src/components/chat/ChatComposer.tsx+10
...@@ -2824,6 +2824,15 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2824,6 +2824,15 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2824 shouldBlurMobileComposerOnSubmit,2824 shouldBlurMobileComposerOnSubmit,
2825 ],2825 ],
2826 );2826 );
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]);
2827 const compactThreadContext = useCallback(() => {2836 const compactThreadContext = useCallback(() => {
2828 if (2837 if (
2829 compactDisabled ||2838 compactDisabled ||
...@@ -5246,6 +5255,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -5246,6 +5255,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
5246 onPageScrollKeyDown={onPageScrollKeyDown}5255 onPageScrollKeyDown={onPageScrollKeyDown}
5247 onPageScrollKeyUp={onPageScrollKeyUp}5256 onPageScrollKeyUp={onPageScrollKeyUp}
5248 onPageScrollRelease={onPageScrollRelease}5257 onPageScrollRelease={onPageScrollRelease}
5258 onCitationSubmitAndSend={submitCitationAndSend}
5249 onPaste={onComposerPaste}5259 onPaste={onComposerPaste}
5250 placeholder={5260 placeholder={
5251 isComposerApprovalState5261 isComposerApprovalState