Skip to content

Commit 2481a51

Browse files
committed
fix(textarea): message couldn't be sent
1 parent 8c48c73 commit 2481a51

File tree

4 files changed

+147
-43
lines changed

4 files changed

+147
-43
lines changed

webview-ui/src/components/chat/ChatLexicalTextArea.tsx

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
$isRangeSelection,
1919
$createParagraphNode,
2020
$createTextNode,
21+
$isTextNode,
2122
} from "lexical"
2223

2324
import { cn } from "@/lib/utils"
@@ -30,7 +31,7 @@ import { AutoApproveDropdown } from "./AutoApproveDropdown"
3031
import { StandardTooltip } from "../ui"
3132
import { IndexingStatusBadge } from "./IndexingStatusBadge"
3233

33-
import { MentionNode } from "./lexical/MentionNode"
34+
import { MentionNode, $isMentionNode } from "./lexical/MentionNode"
3435
import { LexicalMentionPlugin, type MentionInfo, type LexicalMentionPluginRef } from "./lexical/LexicalMentionPlugin"
3536
import { LexicalSelectAllPlugin } from "./lexical/LexicalSelectAllPlugin"
3637
import { LexicalPromptHistoryPlugin } from "./lexical/LexicalPromptHistoryPlugin"
@@ -41,7 +42,6 @@ import { ContextMenuOptionType, SearchResult } from "@/utils/context-mentions"
4142
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
4243
import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
4344
import { ChatContextBar } from "./ChatContextBar"
44-
import { usePromptHistoryData } from "./hooks/usePromptHistoryData"
4545

4646
type ChatTextAreaProps = {
4747
inputValue: string
@@ -51,7 +51,7 @@ type ChatTextAreaProps = {
5151
placeholderText: string
5252
selectedImages: string[]
5353
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>
54-
onSend: () => void
54+
onSend: (serializedContent: string) => void
5555
onSelectImages: () => void
5656
shouldDisableImages: boolean
5757
onHeightChange?: (height: number) => void
@@ -96,11 +96,8 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
9696
listApiConfigMeta,
9797
customModes,
9898
customModePrompts,
99-
cwd,
10099
pinnedApiConfigs,
101100
togglePinnedApiConfig,
102-
taskHistory,
103-
clineMessages,
104101
commands,
105102
cloudUserInfo,
106103
} = useExtensionState()
@@ -119,12 +116,6 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
119116
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
120117
const editorRef = useRef<LexicalEditor | null>(null)
121118

122-
const { promptHistory } = usePromptHistoryData({
123-
clineMessages,
124-
taskHistory,
125-
cwd,
126-
})
127-
128119
// Find the ID and display text for the currently selected API configuration.
129120
const { currentConfigId, displayName } = useMemo(() => {
130121
const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
@@ -493,15 +484,93 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
493484
setIsMouseDownOnMenu(true)
494485
}, [])
495486

496-
const handleEditorChange = useCallback(
497-
(editorState: EditorState) => {
498-
editorState.read(() => {
487+
const serializeEditorContent = useCallback((editorState: EditorState): string => {
488+
return editorState.read(() => {
489+
const root = $getRoot()
490+
let serializedText = ""
491+
492+
const traverse = (node: any) => {
493+
if ($isMentionNode(node)) {
494+
const trigger = node.getTrigger()
495+
const value = node.getValue()
496+
const data = node.getData()
497+
498+
if (trigger === "@") {
499+
const type = data?.type
500+
if (type === "problems") {
501+
serializedText += "@problems"
502+
} else if (type === "terminal") {
503+
serializedText += "@terminal"
504+
} else {
505+
// File or folder mention
506+
serializedText += `@${value}`
507+
}
508+
} else if (trigger === "/") {
509+
// Command mention
510+
serializedText += `/${value}`
511+
}
512+
} else if ($isTextNode(node)) {
513+
serializedText += node.getTextContent()
514+
} else {
515+
// Traverse children for other node types
516+
const children = node.getChildren?.() || []
517+
for (const child of children) {
518+
traverse(child)
519+
}
520+
}
521+
}
522+
523+
const children = root.getChildren()
524+
for (const child of children) {
525+
traverse(child)
526+
}
527+
528+
return serializedText
529+
})
530+
}, [])
531+
532+
const getDisplayText = useCallback((editorState: EditorState): string => {
533+
return editorState.read(() => {
534+
const root = $getRoot()
535+
return root.getTextContent()
536+
})
537+
}, [])
538+
539+
const getSerializedContent = useCallback((): string => {
540+
if (editorRef.current) {
541+
return serializeEditorContent(editorRef.current.getEditorState())
542+
}
543+
return inputValue
544+
}, [serializeEditorContent, inputValue])
545+
546+
// Function to clear the editor content
547+
const clearEditor = useCallback(() => {
548+
if (editorRef.current) {
549+
editorRef.current.update(() => {
499550
const root = $getRoot()
500-
const textContent = root.getTextContent()
501-
setInputValue(textContent)
551+
root.clear()
502552
})
553+
}
554+
setInputValue("")
555+
setSelectedImages([])
556+
}, [setInputValue, setSelectedImages])
557+
558+
// Wrapper function for onSend that resets the textarea after sending
559+
const handleSend = useCallback(
560+
(serializedContent: string) => {
561+
onSend(serializedContent)
562+
clearEditor()
503563
},
504-
[setInputValue],
564+
[onSend, clearEditor],
565+
)
566+
567+
const handleEditorChange = useCallback(
568+
(editorState: EditorState) => {
569+
// Update display text for UI
570+
const displayText = getDisplayText(editorState)
571+
setInputValue(displayText)
572+
},
573+
[setInputValue, getDisplayText],
505574
)
506575

507576
const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})`
@@ -600,10 +669,8 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
600669
{/* see: https://github.com/facebook/lexical/discussions/3590#discussioncomment-8955421 */}
601670
<EditorRefPlugin
602671
editorRef={(el) => {
603-
// Store editor ref locally
604672
editorRef.current = el
605673

606-
// Also forward to parent ref
607674
if (typeof ref === "function") {
608675
ref(el)
609676
} else if (ref) {
@@ -685,8 +752,11 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
685752
materialIconsBaseUri={materialIconsBaseUri}
686753
/>
687754
<LexicalPromptHistoryPlugin
688-
promptHistory={promptHistory}
689755
showContextMenu={showContextMenu}
756+
onSend={() => {
757+
handleSend(getSerializedContent())
758+
}}
759+
sendingDisabled={sendingDisabled || !hasInputContent}
690760
/>
691761
<LexicalContextMenuPlugin
692762
showContextMenu={showContextMenu}
@@ -778,7 +848,7 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
778848
<button
779849
aria-label={t("chat:sendMessage")}
780850
disabled={sendingDisabled || !hasInputContent}
781-
onClick={onSend}
851+
onClick={() => handleSend(getSerializedContent())}
782852
className={cn(
783853
"relative inline-flex items-center justify-center",
784854
"bg-transparent border-none p-1.5",

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,16 +176,19 @@ export const ChatRowContent = ({
176176
}, [message.text, message.images, mode])
177177

178178
// Handle save edit
179-
const handleSaveEdit = useCallback(() => {
180-
setIsEditing(false)
181-
// Send edited message to backend
182-
vscode.postMessage({
183-
type: "submitEditedMessage",
184-
value: message.ts,
185-
editedMessageContent: editedContent,
186-
images: editImages,
187-
})
188-
}, [message.ts, editedContent, editImages])
179+
const handleSaveEdit = useCallback(
180+
(serializedContent: string) => {
181+
setIsEditing(false)
182+
// Send edited message to backend
183+
vscode.postMessage({
184+
type: "submitEditedMessage",
185+
value: message.ts,
186+
editedMessageContent: serializedContent,
187+
images: editImages,
188+
})
189+
},
190+
[message.ts, editImages],
191+
)
189192

190193
// Handle image selection for editing
191194
const handleSelectImages = useCallback(() => {

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ import { QueuedMessages } from "./QueuedMessages"
5757
import DismissibleUpsell from "../common/DismissibleUpsell"
5858
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
5959
import { Cloud } from "lucide-react"
60-
import { ChatLexicalTextArea } from "./ChatLexicalTextArea"
6160
import { LexicalEditor } from "lexical"
61+
import { ChatLexicalTextArea } from "./ChatLexicalTextArea"
6262

6363
export interface ChatViewProps {
6464
isHidden: boolean
@@ -679,7 +679,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
679679
// Mark that user has responded
680680
userRespondedRef.current = true
681681

682-
const trimmedInput = text?.trim()
682+
// Use provided text or fallback to inputValue
683+
const trimmedInput = text?.trim() || inputValue.trim()
683684

684685
switch (clineAsk) {
685686
case "api_req_failed":
@@ -718,15 +719,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
718719
setClineAsk(undefined)
719720
setEnableButtons(false)
720721
},
721-
[clineAsk, startNewTask],
722+
[clineAsk, startNewTask, inputValue],
722723
)
723724

724725
const handleSecondaryButtonClick = useCallback(
725726
(text?: string, images?: string[]) => {
726727
// Mark that user has responded
727728
userRespondedRef.current = true
728729

729-
const trimmedInput = text?.trim()
730+
// Use provided text or fallback to inputValue
731+
const trimmedInput = text?.trim() || inputValue.trim()
730732

731733
if (isStreaming) {
732734
vscode.postMessage({ type: "cancelTask" })
@@ -768,7 +770,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
768770
setClineAsk(undefined)
769771
setEnableButtons(false)
770772
},
771-
[clineAsk, startNewTask, isStreaming],
773+
[clineAsk, startNewTask, isStreaming, inputValue],
772774
)
773775

774776
const { info: model } = useSelectedModel(apiConfiguration)
@@ -1757,6 +1759,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17571759
if (enableButtons && primaryButtonText) {
17581760
handlePrimaryButtonClick(inputValue, selectedImages)
17591761
} else if (!sendingDisabled && !isProfileDisabled && (inputValue.trim() || selectedImages.length > 0)) {
1762+
// For acceptInput, we'll use the display text since we don't have direct access to serialized content
17601763
handleSendMessage(inputValue, selectedImages)
17611764
}
17621765
},
@@ -1999,7 +2002,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
19992002
placeholderText={placeholderText}
20002003
selectedImages={selectedImages}
20012004
setSelectedImages={setSelectedImages}
2002-
onSend={() => handleSendMessage(inputValue, selectedImages)}
2005+
onSend={(serializedContent) => {
2006+
handleSendMessage(serializedContent, selectedImages)
2007+
}}
20032008
onSelectImages={selectImages}
20042009
shouldDisableImages={shouldDisableImages}
20052010
onHeightChange={() => {

webview-ui/src/components/chat/lexical/LexicalPromptHistoryPlugin.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,32 @@ import {
1111
KEY_ENTER_COMMAND,
1212
} from "lexical"
1313
import { useCallback, useEffect, useRef, useState } from "react"
14+
import { usePromptHistoryData } from "../hooks/usePromptHistoryData"
15+
import { useExtensionState } from "@/context/ExtensionStateContext"
1416

1517
interface LexicalPromptHistoryPluginProps {
16-
promptHistory: string[]
1718
showContextMenu: boolean
19+
onSend: () => void
20+
sendingDisabled: boolean
1821
}
1922

20-
export function LexicalPromptHistoryPlugin({ promptHistory, showContextMenu }: LexicalPromptHistoryPluginProps): null {
23+
export function LexicalPromptHistoryPlugin({
24+
showContextMenu,
25+
onSend,
26+
sendingDisabled,
27+
}: LexicalPromptHistoryPluginProps): null {
2128
const [editor] = useLexicalComposerContext()
2229
const [historyIndex, setHistoryIndex] = useState(-1)
2330
const [tempInput, setTempInput] = useState("")
2431
const isNavigatingRef = useRef(false)
2532

33+
const { clineMessages, taskHistory, cwd } = useExtensionState()
34+
const { promptHistory } = usePromptHistoryData({
35+
clineMessages,
36+
taskHistory,
37+
cwd,
38+
})
39+
2640
const resetHistoryNavigation = useCallback(() => {
2741
setHistoryIndex(-1)
2842
setTempInput("")
@@ -201,13 +215,23 @@ export function LexicalPromptHistoryPlugin({ promptHistory, showContextMenu }: L
201215
const removeEnterListener = editor.registerCommand(
202216
KEY_ENTER_COMMAND,
203217
(event) => {
204-
if (showContextMenu) {
218+
if (showContextMenu || sendingDisabled) {
219+
return false
220+
}
221+
222+
// Allow Shift+Enter to add a new line
223+
if (event?.shiftKey) {
205224
return false
206225
}
207226

208-
// If in history navigation mode, reset navigation and send
209227
event?.preventDefault()
210-
resetHistoryNavigation()
228+
229+
if (historyIndex !== -1) {
230+
resetHistoryNavigation()
231+
}
232+
233+
onSend()
234+
211235
return true
212236
},
213237
COMMAND_PRIORITY_HIGH,
@@ -226,6 +250,8 @@ export function LexicalPromptHistoryPlugin({ promptHistory, showContextMenu }: L
226250
navigateToHistory,
227251
returnToCurrentInput,
228252
resetHistoryNavigation,
253+
onSend,
254+
sendingDisabled,
229255
])
230256

231257
return null

0 commit comments

Comments
 (0)