mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add collapsed code snippet display in chat input
Implements a feature to show code context as compact chips in the chat input box rather than displaying full code text. When selecting code and adding to context, it now appears as a collapsed chip showing the file path and line range. Features: - CodeSnippet type for structured code references - CodeSnippetChip component for compact display of code references - Multiple code snippets can be added and displayed as chips - Full code content is expanded when sending to AI - Remove button on each chip to remove snippets Relates to #10085
This commit is contained in:
parent
e3b90fb182
commit
9a05952af7
7 changed files with 221 additions and 8 deletions
51
packages/types/src/code-snippet.ts
Normal file
51
packages/types/src/code-snippet.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Represents a code snippet reference that can be added to the chat input.
|
||||
* The snippet is displayed in a collapsed/compressed form in the UI,
|
||||
* but the full code content is sent to the AI.
|
||||
*/
|
||||
export interface CodeSnippet {
|
||||
/** Unique identifier for the snippet */
|
||||
id: string
|
||||
/** File path relative to workspace */
|
||||
filePath: string
|
||||
/** Start line number (1-indexed) */
|
||||
startLine: number
|
||||
/** End line number (1-indexed) */
|
||||
endLine: number
|
||||
/** The actual code content */
|
||||
content: string
|
||||
/** Timestamp when the snippet was added */
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique ID for a code snippet
|
||||
*/
|
||||
export function createCodeSnippetId(): string {
|
||||
return `snippet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a code snippet for display in a collapsed chip/pill format
|
||||
*/
|
||||
export function formatCodeSnippetLabel(snippet: CodeSnippet): string {
|
||||
const fileName = snippet.filePath.split("/").pop() || snippet.filePath
|
||||
return `${fileName}:${snippet.startLine}-${snippet.endLine}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a code snippet into the full text format to be sent to the AI
|
||||
*/
|
||||
export function expandCodeSnippet(snippet: CodeSnippet): string {
|
||||
return `${snippet.filePath}:${snippet.startLine}-${snippet.endLine}
|
||||
\`\`\`
|
||||
${snippet.content}
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands multiple code snippets and joins them with spacing
|
||||
*/
|
||||
export function expandCodeSnippets(snippets: CodeSnippet[]): string {
|
||||
return snippets.map(expandCodeSnippet).join("\n\n")
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
export * from "./api.js"
|
||||
export * from "./cloud.js"
|
||||
export * from "./code-snippet.js"
|
||||
export * from "./codebase-index.js"
|
||||
export * from "./context-management.js"
|
||||
export * from "./cookie-consent.js"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
type CreateTaskOptions,
|
||||
type TokenUsage,
|
||||
type ToolUsage,
|
||||
type CodeSnippet,
|
||||
RooCodeEventName,
|
||||
requestyDefaultModelId,
|
||||
openRouterDefaultModelId,
|
||||
|
|
@ -43,6 +44,7 @@ import {
|
|||
DEFAULT_MODES,
|
||||
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
|
||||
getModelId,
|
||||
createCodeSnippetId,
|
||||
} from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud"
|
||||
|
|
@ -682,10 +684,19 @@ export class ClineProvider
|
|||
const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
|
||||
|
||||
if (command === "addToContext") {
|
||||
// Create a code snippet for collapsed display in the input
|
||||
const codeSnippet: CodeSnippet = {
|
||||
id: createCodeSnippetId(),
|
||||
filePath: params.filePath as string,
|
||||
startLine: params.startLine as unknown as number,
|
||||
endLine: params.endLine as unknown as number,
|
||||
content: params.selectedText as string,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
await visibleProvider.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "setChatBoxMessage",
|
||||
text: `${prompt}\n\n`,
|
||||
invoke: "addCodeSnippet",
|
||||
codeSnippet,
|
||||
})
|
||||
await visibleProvider.postMessageToWebview({ type: "action", action: "focusInput" })
|
||||
return
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {
|
|||
ShareVisibility,
|
||||
QueuedMessage,
|
||||
SerializedCustomToolDefinition,
|
||||
CodeSnippet,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { GitCommit } from "../utils/git"
|
||||
|
|
@ -149,7 +150,13 @@ export interface ExtensionMessage {
|
|||
| "focusInput"
|
||||
| "switchTab"
|
||||
| "toggleAutoApprove"
|
||||
invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
|
||||
invoke?:
|
||||
| "newChat"
|
||||
| "sendMessage"
|
||||
| "primaryButtonClick"
|
||||
| "secondaryButtonClick"
|
||||
| "setChatBoxMessage"
|
||||
| "addCodeSnippet"
|
||||
state?: ExtensionState
|
||||
images?: string[]
|
||||
filePaths?: string[]
|
||||
|
|
@ -218,6 +225,7 @@ export interface ExtensionMessage {
|
|||
isBrowserSessionActive?: boolean // For browser session panel updates
|
||||
stepIndex?: number // For browserSessionNavigate: the target step index to display
|
||||
tools?: SerializedCustomToolDefinition[] // For customToolsResult
|
||||
codeSnippet?: CodeSnippet // For addCodeSnippet: the code snippet to add
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces }
|
|||
import { WebviewMessage } from "@roo/WebviewMessage"
|
||||
import { Mode, getAllModes } from "@roo/modes"
|
||||
import { ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
import type { CodeSnippet } from "@roo-code/types"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
|
|
@ -32,6 +33,7 @@ import ContextMenu from "./ContextMenu"
|
|||
import { IndexingStatusBadge } from "./IndexingStatusBadge"
|
||||
import { usePromptHistory } from "./hooks/usePromptHistory"
|
||||
import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
|
||||
import { CodeSnippetChips } from "./CodeSnippetChip"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
|
|
@ -41,6 +43,8 @@ interface ChatTextAreaProps {
|
|||
placeholderText: string
|
||||
selectedImages: string[]
|
||||
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>
|
||||
codeSnippets?: CodeSnippet[]
|
||||
onRemoveCodeSnippet?: (id: string) => void
|
||||
onSend: () => void
|
||||
onSelectImages: () => void
|
||||
shouldDisableImages: boolean
|
||||
|
|
@ -65,6 +69,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
placeholderText,
|
||||
selectedImages,
|
||||
setSelectedImages,
|
||||
codeSnippets = [],
|
||||
onRemoveCodeSnippet,
|
||||
onSend,
|
||||
onSelectImages,
|
||||
shouldDisableImages,
|
||||
|
|
@ -253,10 +259,10 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
|
||||
const allModes = useMemo(() => getAllModes(customModes), [customModes])
|
||||
|
||||
// Memoized check for whether the input has content (text or images)
|
||||
// Memoized check for whether the input has content (text, images, or code snippets)
|
||||
const hasInputContent = useMemo(() => {
|
||||
return inputValue.trim().length > 0 || selectedImages.length > 0
|
||||
}, [inputValue, selectedImages])
|
||||
return inputValue.trim().length > 0 || selectedImages.length > 0 || codeSnippets.length > 0
|
||||
}, [inputValue, selectedImages, codeSnippets])
|
||||
|
||||
// Compute the key combination text for the send button tooltip based on enterBehavior
|
||||
const sendKeyCombination = useMemo(() => {
|
||||
|
|
@ -1229,6 +1235,14 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{codeSnippets.length > 0 && onRemoveCodeSnippet && (
|
||||
<CodeSnippetChips
|
||||
snippets={codeSnippets}
|
||||
onRemove={onRemoveCodeSnippet}
|
||||
className="bg-vscode-input-background border-b border-vscode-input-border"
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedImages.length > 0 && (
|
||||
<Thumbnails
|
||||
images={selectedImages}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { Trans } from "react-i18next"
|
|||
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
|
||||
import { appendImages } from "@src/utils/imageUtils"
|
||||
|
||||
import type { ClineAsk, ClineMessage } from "@roo-code/types"
|
||||
import type { ClineAsk, ClineMessage, CodeSnippet } from "@roo-code/types"
|
||||
import { expandCodeSnippets } from "@roo-code/types"
|
||||
|
||||
import { ClineSayTool, ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
import { findLast } from "@roo/array"
|
||||
|
|
@ -135,6 +136,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [sendingDisabled, setSendingDisabled] = useState(false)
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([])
|
||||
const [codeSnippets, setCodeSnippets] = useState<CodeSnippet[]>([])
|
||||
|
||||
// We need to hold on to the ask because useEffect > lastMessage will always
|
||||
// let us know when an ask comes in and handle it, but by the time
|
||||
|
|
@ -566,6 +568,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
setInputValue("")
|
||||
setSendingDisabled(true)
|
||||
setSelectedImages([])
|
||||
setCodeSnippets([])
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// Do not reset mode here as it should persist.
|
||||
|
|
@ -582,6 +585,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
(text: string, images: string[]) => {
|
||||
text = text.trim()
|
||||
|
||||
// Expand code snippets and prepend to the message
|
||||
if (codeSnippets.length > 0) {
|
||||
const expandedSnippets = expandCodeSnippets(codeSnippets)
|
||||
text = expandedSnippets + (text ? "\n\n" + text : "")
|
||||
}
|
||||
|
||||
if (text || images.length > 0) {
|
||||
// Queue message if:
|
||||
// - Task is busy (sendingDisabled)
|
||||
|
|
@ -643,7 +652,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
handleChatReset()
|
||||
}
|
||||
},
|
||||
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length], // messagesRef and clineAskRef are stable
|
||||
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length, codeSnippets], // messagesRef and clineAskRef are stable
|
||||
)
|
||||
|
||||
const handleSetChatBoxMessage = useCallback(
|
||||
|
|
@ -661,6 +670,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
[inputValue, selectedImages],
|
||||
)
|
||||
|
||||
const handleAddCodeSnippet = useCallback((snippet: CodeSnippet) => {
|
||||
setCodeSnippets((prev) => {
|
||||
// Avoid duplicates by checking if snippet with same file/lines already exists
|
||||
const isDuplicate = prev.some(
|
||||
(s) =>
|
||||
s.filePath === snippet.filePath &&
|
||||
s.startLine === snippet.startLine &&
|
||||
s.endLine === snippet.endLine,
|
||||
)
|
||||
if (isDuplicate) {
|
||||
return prev
|
||||
}
|
||||
return [...prev, snippet]
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleRemoveCodeSnippet = useCallback((id: string) => {
|
||||
setCodeSnippets((prev) => prev.filter((s) => s.id !== id))
|
||||
}, [])
|
||||
|
||||
const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), [])
|
||||
|
||||
// This logic depends on the useEffect[messages] above to set clineAsk,
|
||||
|
|
@ -838,6 +867,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "secondaryButtonClick":
|
||||
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "addCodeSnippet":
|
||||
if (message.codeSnippet) {
|
||||
handleAddCodeSnippet(message.codeSnippet)
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "condenseTaskContextStarted":
|
||||
|
|
@ -884,6 +918,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
handleSecondaryButtonClick,
|
||||
setCheckpointWarning,
|
||||
playSound,
|
||||
handleAddCodeSnippet,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -1601,6 +1636,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
placeholderText={placeholderText}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
codeSnippets={codeSnippets}
|
||||
onRemoveCodeSnippet={handleRemoveCodeSnippet}
|
||||
onSend={() => handleSendMessage(inputValue, selectedImages)}
|
||||
onSelectImages={selectImages}
|
||||
shouldDisableImages={shouldDisableImages}
|
||||
|
|
|
|||
91
webview-ui/src/components/chat/CodeSnippetChip.tsx
Normal file
91
webview-ui/src/components/chat/CodeSnippetChip.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useMemo } from "react"
|
||||
import { X, FileCode } from "lucide-react"
|
||||
|
||||
import type { CodeSnippet } from "@roo-code/types"
|
||||
import { formatCodeSnippetLabel } from "@roo-code/types"
|
||||
|
||||
import { cn } from "@src/lib/utils"
|
||||
import { StandardTooltip } from "@src/components/ui"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
|
||||
interface CodeSnippetChipProps {
|
||||
snippet: CodeSnippet
|
||||
onRemove: (id: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A compact chip component that displays a collapsed code snippet reference.
|
||||
* Shows the file name and line range in a pill format.
|
||||
* Users can click the X to remove the snippet.
|
||||
*/
|
||||
export const CodeSnippetChip: React.FC<CodeSnippetChipProps> = ({ snippet, onRemove, className }) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const label = useMemo(() => formatCodeSnippetLabel(snippet), [snippet])
|
||||
|
||||
const tooltipContent = useMemo(() => {
|
||||
const lineCount = snippet.endLine - snippet.startLine + 1
|
||||
return t("chat:codeSnippetTooltip", {
|
||||
lineCount,
|
||||
defaultValue: `${lineCount} line${lineCount > 1 ? "s" : ""} of code`,
|
||||
})
|
||||
}, [snippet, t])
|
||||
|
||||
return (
|
||||
<StandardTooltip content={tooltipContent}>
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-2 py-0.5",
|
||||
"bg-vscode-badge-background text-vscode-badge-foreground",
|
||||
"rounded-full text-xs font-medium",
|
||||
"border border-vscode-contrastBorder",
|
||||
"max-w-[200px] truncate",
|
||||
className,
|
||||
)}>
|
||||
<FileCode className="w-3 h-3 flex-shrink-0" />
|
||||
<span className="truncate">{label}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onRemove(snippet.id)
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center",
|
||||
"w-3.5 h-3.5 ml-0.5 flex-shrink-0",
|
||||
"rounded-full",
|
||||
"hover:bg-vscode-toolbar-hoverBackground",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
aria-label={t("chat:removeCodeSnippet", { defaultValue: "Remove code snippet" })}>
|
||||
<X className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
</StandardTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
interface CodeSnippetChipsProps {
|
||||
snippets: CodeSnippet[]
|
||||
onRemove: (id: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Container component for displaying multiple code snippet chips.
|
||||
*/
|
||||
export const CodeSnippetChips: React.FC<CodeSnippetChipsProps> = ({ snippets, onRemove, className }) => {
|
||||
if (snippets.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-wrap gap-1 px-2 py-1.5", className)}>
|
||||
{snippets.map((snippet) => (
|
||||
<CodeSnippetChip key={snippet.id} snippet={snippet} onRemove={onRemove} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue