Skip to content

Commit 8fca912

Browse files
committed
feat: add quote selection feature to chat UI
- Add text selection detection with floating quote button in Markdown component - Implement quote preview above ChatTextArea with dismiss control - Add keyboard shortcut support (Cmd/Ctrl+Shift+Q) for quick quoting - Format quoted text with [context] wrapper for AI context - Add comprehensive tests for quote selection functionality - Add translation strings for quote feature Closes #8837
1 parent f5d7ba1 commit 8fca912

File tree

9 files changed

+639
-1176
lines changed

9 files changed

+639
-1176
lines changed

src/i18n/locales/en/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@
181181
"task_prompt": "What should Roo do?",
182182
"task_placeholder": "Type your task here"
183183
},
184+
"chat": {
185+
"quotePreview": "Quote",
186+
"quoteSelection": "Quote"
187+
},
184188
"customModes": {
185189
"errors": {
186190
"yamlParseError": "Invalid YAML in .roomodes file at line {{line}}. Please check for:\n• Proper indentation (use spaces, not tabs)\n• Matching quotes and brackets\n• Valid YAML syntax",

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface ExtensionMessage {
7070
| "theme"
7171
| "workspaceUpdated"
7272
| "invoke"
73+
| "addQuoteToComposer"
7374
| "messageUpdated"
7475
| "mcpServers"
7576
| "enhancedPrompt"

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface WebviewMessage {
3535
| "deleteApiConfiguration"
3636
| "loadApiConfiguration"
3737
| "loadApiConfigurationById"
38+
| "addQuoteToComposer"
3839
| "renameApiConfiguration"
3940
| "getListApiConfiguration"
4041
| "customInstructions"

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ export const ChatRowContent = ({
10921092
<span style={{ fontWeight: "bold" }}>{t("chat:text.rooSaid")}</span>
10931093
</div>
10941094
<div className="pl-6">
1095-
<Markdown markdown={message.text} partial={message.partial} />
1095+
<Markdown markdown={message.text} partial={message.partial} messageTs={message.ts} />
10961096
{message.images && message.images.length > 0 && (
10971097
<div style={{ marginTop: "10px" }}>
10981098
{message.images.map((image, index) => (
@@ -1201,7 +1201,7 @@ export const ChatRowContent = ({
12011201
{title}
12021202
</div>
12031203
<div className="border-l border-green-600/30 ml-2 pl-4 pb-1">
1204-
<Markdown markdown={message.text} />
1204+
<Markdown markdown={message.text} messageTs={message.ts} />
12051205
</div>
12061206
</>
12071207
)
@@ -1355,7 +1355,7 @@ export const ChatRowContent = ({
13551355
</div>
13561356
)}
13571357
<div style={{ paddingTop: 10 }}>
1358-
<Markdown markdown={message.text} partial={message.partial} />
1358+
<Markdown markdown={message.text} partial={message.partial} messageTs={message.ts} />
13591359
</div>
13601360
</>
13611361
)
@@ -1441,7 +1441,11 @@ export const ChatRowContent = ({
14411441
{title}
14421442
</div>
14431443
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
1444-
<Markdown markdown={message.text} partial={message.partial} />
1444+
<Markdown
1445+
markdown={message.text}
1446+
partial={message.partial}
1447+
messageTs={message.ts}
1448+
/>
14451449
</div>
14461450
</div>
14471451
)
@@ -1460,6 +1464,7 @@ export const ChatRowContent = ({
14601464
<div className="flex flex-col gap-2 ml-6">
14611465
<Markdown
14621466
markdown={message.partial === true ? message?.text : followUpData?.question}
1467+
messageTs={message.ts}
14631468
/>
14641469
<FollowUpSuggest
14651470
suggestions={followUpData?.suggest}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ interface ChatTextAreaProps {
5151
// Edit mode props
5252
isEditMode?: boolean
5353
onCancel?: () => void
54+
// Quote selection props
55+
quotedText?: string
56+
onClearQuote?: () => void
5457
}
5558

5659
export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
@@ -71,6 +74,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
7174
modeShortcutText,
7275
isEditMode = false,
7376
onCancel,
77+
quotedText,
78+
onClearQuote,
7479
},
7580
ref,
7681
) => {
@@ -913,6 +918,31 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
913918
"flex flex-col gap-1 bg-editor-background outline-none border border-none box-border",
914919
isEditMode ? "p-2 w-full" : "relative px-1.5 pb-1 w-[calc(100%-16px)] ml-auto mr-auto",
915920
)}>
921+
{/* Quote preview above text area */}
922+
{quotedText && !isEditMode && (
923+
<div className="mx-1.5 mb-1 p-2 bg-vscode-textCodeBlock-background rounded border border-vscode-widget-border relative">
924+
<button
925+
className="absolute top-1 right-1 p-1 hover:bg-vscode-toolbar-hoverBackground rounded"
926+
onClick={() => onClearQuote?.()}
927+
aria-label="Clear quote">
928+
<span className="codicon codicon-close text-xs" />
929+
</button>
930+
<div className="text-xs text-vscode-descriptionForeground mb-1">{t("chat:quotePreview")}</div>
931+
<div className="text-xs max-h-20 overflow-y-auto pr-6">
932+
{quotedText
933+
.split("\n")
934+
.slice(0, 3)
935+
.map((line, idx) => (
936+
<div key={idx} className="text-vscode-foreground opacity-75">
937+
{line}
938+
</div>
939+
))}
940+
{quotedText.split("\n").length > 3 && (
941+
<div className="text-vscode-descriptionForeground">...</div>
942+
)}
943+
</div>
944+
</div>
945+
)}
916946
<div className={cn(!isEditMode && "relative")}>
917947
<div
918948
className={cn("chat-text-area", !isEditMode && "relative", "flex", "flex-col", "outline-none")}

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
197197
>(undefined)
198198
const [isCondensing, setIsCondensing] = useState<boolean>(false)
199199
const [showAnnouncementModal, setShowAnnouncementModal] = useState(false)
200+
const [quotedText, setQuotedText] = useState<string>("")
200201
const everVisibleMessagesTsRef = useRef<LRUCache<number, boolean>>(
201202
new LRUCache({
202203
max: 100,
@@ -783,6 +784,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
783784
const message: ExtensionMessage = e.data
784785

785786
switch (message.type) {
787+
case "addQuoteToComposer":
788+
// Handle quote from message selection
789+
if (message.text) {
790+
// Format the quoted text with [context] wrapper
791+
const formattedQuote = `[context]\n${message.text
792+
.split("\n")
793+
.map((line) => `> ${line}`)
794+
.join("\n")}\n[/context]\n`
795+
setQuotedText(formattedQuote)
796+
}
797+
break
786798
case "action":
787799
switch (message.action!) {
788800
case "didBecomeVisible":
@@ -1720,6 +1732,28 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17201732
// Add keyboard event handler
17211733
const handleKeyDown = useCallback(
17221734
(event: KeyboardEvent) => {
1735+
// Check for Command/Ctrl + Shift + Q for quote selection
1736+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "q") {
1737+
event.preventDefault()
1738+
1739+
// Get current text selection
1740+
const selection = window.getSelection()
1741+
if (selection && !selection.isCollapsed) {
1742+
const selectedText = selection.toString().trim()
1743+
if (selectedText) {
1744+
// Format the quoted text with [context] wrapper
1745+
const formattedQuote = `[context]\n${selectedText
1746+
.split("\n")
1747+
.map((line) => `> ${line}`)
1748+
.join("\n")}\n[/context]\n`
1749+
setQuotedText(formattedQuote)
1750+
1751+
// Clear the selection
1752+
selection.removeAllRanges()
1753+
}
1754+
}
1755+
}
1756+
17231757
// Check for Command/Ctrl + Period (with or without Shift)
17241758
// Using event.key to respect keyboard layouts (e.g., Dvorak)
17251759
if ((event.metaKey || event.ctrlKey) && event.key === ".") {
@@ -1992,7 +2026,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
19922026
placeholderText={placeholderText}
19932027
selectedImages={selectedImages}
19942028
setSelectedImages={setSelectedImages}
1995-
onSend={() => handleSendMessage(inputValue, selectedImages)}
2029+
onSend={() => {
2030+
// Include quoted text when sending
2031+
const finalMessage = quotedText ? quotedText + inputValue : inputValue
2032+
handleSendMessage(finalMessage, selectedImages)
2033+
setQuotedText("") // Clear quote after sending
2034+
}}
19962035
onSelectImages={selectImages}
19972036
shouldDisableImages={shouldDisableImages}
19982037
onHeightChange={() => {
@@ -2003,6 +2042,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
20032042
mode={mode}
20042043
setMode={setMode}
20052044
modeShortcutText={modeShortcutText}
2045+
quotedText={quotedText}
2046+
onClearQuote={() => setQuotedText("")}
20062047
/>
20072048

20082049
{isProfileDisabled && (

0 commit comments

Comments
 (0)