Skip to content

Commit 92a04f5

Browse files
committed
feat(textarea): navigate through history
1 parent 5d408af commit 92a04f5

File tree

3 files changed

+353
-8
lines changed

3 files changed

+353
-8
lines changed

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

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import { EditorRefPlugin } from "@lexical/react/LexicalEditorRefPlugin"
1010
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
1111
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
1212
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"
13-
import { $getRoot, EditorState, LexicalEditor } from "lexical"
13+
import {
14+
$getRoot,
15+
EditorState,
16+
LexicalEditor,
17+
$getSelection,
18+
$isRangeSelection,
19+
$createParagraphNode,
20+
$createTextNode,
21+
} from "lexical"
1422

1523
import { cn } from "@/lib/utils"
1624
import { ModeSelector } from "./ModeSelector"
@@ -25,11 +33,13 @@ import { IndexingStatusBadge } from "./IndexingStatusBadge"
2533
import { MentionNode } from "./lexical/MentionNode"
2634
import { LexicalMentionPlugin, type MentionInfo, type LexicalMentionPluginRef } from "./lexical/LexicalMentionPlugin"
2735
import { LexicalSelectAllPlugin } from "./lexical/LexicalSelectAllPlugin"
36+
import { LexicalPromptHistoryPlugin } from "./lexical/LexicalPromptHistoryPlugin"
2837
import ContextMenu from "./ContextMenu"
2938
import { ContextMenuOptionType, getContextMenuOptions, SearchResult } from "@/utils/context-mentions"
3039
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
3140
import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
3241
import { ChatContextBar } from "./ChatContextBar"
42+
import { usePromptHistoryData } from "./hooks/usePromptHistoryData"
3343

3444
type ChatTextAreaProps = {
3545
inputValue: string
@@ -84,11 +94,11 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
8494
listApiConfigMeta,
8595
customModes,
8696
customModePrompts,
87-
// cwd,
97+
cwd,
8898
pinnedApiConfigs,
8999
togglePinnedApiConfig,
90-
// taskHistory,
91-
// clineMessages,
100+
taskHistory,
101+
clineMessages,
92102
commands,
93103
cloudUserInfo,
94104
} = useExtensionState()
@@ -106,6 +116,13 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
106116
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
107117
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
108118
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
119+
const editorRef = useRef<LexicalEditor | null>(null)
120+
121+
const { promptHistory } = usePromptHistoryData({
122+
clineMessages,
123+
taskHistory,
124+
cwd,
125+
})
109126

110127
// Find the ID and display text for the currently selected API configuration.
111128
const { currentConfigId, displayName } = useMemo(() => {
@@ -310,10 +327,49 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
310327
} else if (message.type === "ttsStop") {
311328
setIsTtsPlaying(false)
312329
} else if (message.type === "enhancedPrompt") {
313-
if (message.text) {
330+
if (message.text && editorRef.current) {
331+
editorRef.current.update(() => {
332+
const root = $getRoot()
333+
const selection = $getSelection()
334+
335+
// Clear current content
336+
root.clear()
337+
338+
// Insert new text
339+
const paragraphNode = $createParagraphNode()
340+
const textNode = $createTextNode(message.text)
341+
paragraphNode.append(textNode)
342+
root.append(paragraphNode)
343+
344+
// Move cursor to end
345+
if ($isRangeSelection(selection)) {
346+
root.selectEnd()
347+
}
348+
})
314349
setInputValue(message.text)
315350
}
316351
setIsEnhancingPrompt(false)
352+
} else if (message.type === "insertTextIntoTextarea") {
353+
if (message.text && editorRef.current) {
354+
// Insert text at cursor position
355+
editorRef.current.update(() => {
356+
const selection = $getSelection()
357+
if ($isRangeSelection(selection)) {
358+
const anchor = selection.anchor
359+
const textContent = anchor.getNode().getTextContent()
360+
const offset = anchor.offset
361+
362+
// Check if we need to add a space before
363+
const textBefore = textContent.slice(0, offset)
364+
const needsSpaceBefore = textBefore.length > 0 && !textBefore.endsWith(" ")
365+
const prefix = needsSpaceBefore ? " " : ""
366+
367+
// Insert the text with space after
368+
const insertText = prefix + message.text + " "
369+
selection.insertText(insertText)
370+
}
371+
})
372+
}
317373
} else if (message.type === "commitSearchResults") {
318374
const commits = message.commits.map((commit: any) => ({
319375
type: ContextMenuOptionType.Git,
@@ -329,9 +385,6 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
329385
if (message.requestId === searchRequestId) {
330386
setFileSearchResults(message.results || [])
331387
}
332-
} else if (message.type === "insertTextIntoTextarea") {
333-
// Lexical editor handles inserts differently. Future improvement.
334-
console.log("Insert text requested for Lexical, but not yet implemented:", message.text)
335388
}
336389
}
337390

@@ -698,6 +751,10 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
698751
{/* see: https://github.com/facebook/lexical/discussions/3590#discussioncomment-8955421 */}
699752
<EditorRefPlugin
700753
editorRef={(el) => {
754+
// Store editor ref locally
755+
editorRef.current = el
756+
757+
// Also forward to parent ref
701758
if (typeof ref === "function") {
702759
ref(el)
703760
} else if (ref) {
@@ -776,6 +833,10 @@ export const ChatLexicalTextArea = forwardRef<LexicalEditor, ChatTextAreaProps>(
776833
onMentionUpdate={handleMentionUpdate}
777834
materialIconsBaseUri={materialIconsBaseUri}
778835
/>
836+
<LexicalPromptHistoryPlugin
837+
promptHistory={promptHistory}
838+
showContextMenu={showContextMenu}
839+
/>
779840
<LexicalSelectAllPlugin />
780841
</LexicalComposer>
781842

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ClineMessage, HistoryItem } from "@roo-code/types"
2+
import { useMemo } from "react"
3+
4+
interface UsePromptHistoryDataProps {
5+
clineMessages: ClineMessage[] | undefined
6+
taskHistory: HistoryItem[] | undefined
7+
cwd: string | undefined
8+
}
9+
10+
export interface UsePromptHistoryDataReturn {
11+
promptHistory: string[]
12+
}
13+
14+
const MAX_PROMPT_HISTORY_SIZE = 100
15+
16+
export const usePromptHistoryData = ({
17+
clineMessages,
18+
taskHistory,
19+
cwd,
20+
}: UsePromptHistoryDataProps): UsePromptHistoryDataReturn => {
21+
const promptHistory = useMemo(() => {
22+
const conversationPrompts = clineMessages
23+
?.filter((message) => message.type === "say" && message.say === "user_feedback" && message.text?.trim())
24+
.map((message) => message.text!)
25+
26+
// If we have conversation messages, use those (newest first when navigating up)
27+
if (conversationPrompts?.length) {
28+
return conversationPrompts.slice(-MAX_PROMPT_HISTORY_SIZE).reverse()
29+
}
30+
31+
// If we have clineMessages array (meaning we're in an active task), don't fall back to task history
32+
// Only use task history when starting fresh (no active conversation)
33+
if (clineMessages?.length) {
34+
return []
35+
}
36+
37+
// Fall back to task history only when starting fresh (no active conversation)
38+
if (!taskHistory?.length || !cwd) {
39+
return []
40+
}
41+
42+
// Extract user prompts from task history for the current workspace only
43+
return taskHistory
44+
.filter((item) => item.task?.trim() && (!item.workspace || item.workspace === cwd))
45+
.map((item) => item.task)
46+
.slice(0, MAX_PROMPT_HISTORY_SIZE)
47+
}, [clineMessages, taskHistory, cwd])
48+
49+
return {
50+
promptHistory,
51+
}
52+
}

0 commit comments

Comments
 (0)