mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: edit/delete user message
This commit is contained in:
parent
c792662ac9
commit
7922443e94
7 changed files with 290 additions and 556 deletions
|
|
@ -317,7 +317,6 @@ describe("Checkpoint functionality", () => {
|
|||
},
|
||||
]
|
||||
mockCheckpointService.getDiff.mockResolvedValue(mockChanges)
|
||||
mockCheckpointService.getCheckpoints = vi.fn(() => ["commit1", "commit2"])
|
||||
await checkpointDiff(mockTask, {
|
||||
ts: 4,
|
||||
commitHash: "commit1",
|
||||
|
|
@ -343,7 +342,6 @@ describe("Checkpoint functionality", () => {
|
|||
},
|
||||
]
|
||||
mockCheckpointService.getDiff.mockResolvedValue(mockChanges)
|
||||
mockCheckpointService.getCheckpoints = vi.fn(() => ["commit1", "commit2"])
|
||||
|
||||
await checkpointDiff(mockTask, {
|
||||
ts: 4,
|
||||
|
|
|
|||
|
|
@ -271,15 +271,16 @@ export async function checkpointDiff(task: Task, { ts, previousCommitHash, commi
|
|||
TelemetryService.instance.captureCheckpointDiffed(task.taskId)
|
||||
|
||||
let prevHash = commitHash
|
||||
let nextHash: string | undefined
|
||||
let nextHash: string | undefined = undefined
|
||||
|
||||
const checkpoints = typeof service.getCheckpoints === "function" ? service.getCheckpoints() : []
|
||||
const idx = checkpoints.indexOf(commitHash)
|
||||
|
||||
if (idx !== -1 && idx < checkpoints.length - 1) {
|
||||
nextHash = checkpoints[idx + 1]
|
||||
} else {
|
||||
nextHash = undefined
|
||||
if (mode !== "full") {
|
||||
const checkpoints = task.clineMessages.filter(({ say }) => say === "checkpoint_saved").map(({ text }) => text!)
|
||||
const idx = checkpoints.indexOf(commitHash)
|
||||
if (idx !== -1 && idx < checkpoints.length - 1) {
|
||||
nextHash = checkpoints[idx + 1]
|
||||
} else {
|
||||
nextHash = undefined
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ export const webviewMessageHandler = async (
|
|||
const { messageIndex } = findMessageIndices(messageTs, currentCline)
|
||||
if (messageIndex !== -1) {
|
||||
// Find the last checkpoint before this message
|
||||
const checkpoints = currentCline.clineMessages
|
||||
.filter((msg) => msg.say === "checkpoint_saved" && msg.ts < messageTs)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
const checkpoints = currentCline.clineMessages.filter(
|
||||
(msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
|
||||
)
|
||||
|
||||
hasCheckpoint = checkpoints.length > 0
|
||||
} else {
|
||||
|
|
@ -153,19 +153,19 @@ export const webviewMessageHandler = async (
|
|||
// If checkpoint restoration is requested, find and restore to the last checkpoint before this message
|
||||
if (restoreCheckpoint) {
|
||||
// Find the last checkpoint before this message
|
||||
const checkpoints = currentCline.clineMessages
|
||||
.filter((msg) => msg.say === "checkpoint_saved" && msg.ts < messageTs)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
const checkpoints = currentCline.clineMessages.filter(
|
||||
(msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
|
||||
)
|
||||
|
||||
const lastCheckpoint = checkpoints[0]
|
||||
const nextCheckpoint = checkpoints[0]
|
||||
|
||||
if (lastCheckpoint && lastCheckpoint.text) {
|
||||
if (nextCheckpoint && nextCheckpoint.text) {
|
||||
await handleCheckpointRestoreOperation({
|
||||
provider,
|
||||
currentCline,
|
||||
messageTs: targetMessage.ts!,
|
||||
messageIndex,
|
||||
checkpoint: { hash: lastCheckpoint.text },
|
||||
checkpoint: { hash: nextCheckpoint.text },
|
||||
operation: "delete",
|
||||
})
|
||||
} else {
|
||||
|
|
@ -221,9 +221,9 @@ export const webviewMessageHandler = async (
|
|||
const { messageIndex } = findMessageIndices(messageTs, currentCline)
|
||||
if (messageIndex !== -1) {
|
||||
// Find the last checkpoint before this message
|
||||
const checkpoints = currentCline.clineMessages
|
||||
.filter((msg) => msg.say === "checkpoint_saved" && msg.ts < messageTs)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
const checkpoints = currentCline.clineMessages.filter(
|
||||
(msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
|
||||
)
|
||||
|
||||
hasCheckpoint = checkpoints.length > 0
|
||||
} else {
|
||||
|
|
@ -274,19 +274,19 @@ export const webviewMessageHandler = async (
|
|||
// If checkpoint restoration is requested, find and restore to the last checkpoint before this message
|
||||
if (restoreCheckpoint) {
|
||||
// Find the last checkpoint before this message
|
||||
const checkpoints = currentCline.clineMessages
|
||||
.filter((msg) => msg.say === "checkpoint_saved" && msg.ts < messageTs)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
const checkpoints = currentCline.clineMessages.filter(
|
||||
(msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs,
|
||||
)
|
||||
|
||||
const lastCheckpoint = checkpoints[0]
|
||||
const nextCheckpoint = checkpoints[0]
|
||||
|
||||
if (lastCheckpoint && lastCheckpoint.text) {
|
||||
if (nextCheckpoint && nextCheckpoint.text) {
|
||||
await handleCheckpointRestoreOperation({
|
||||
provider,
|
||||
currentCline,
|
||||
messageTs: targetMessage.ts!,
|
||||
messageIndex,
|
||||
checkpoint: { hash: lastCheckpoint.text },
|
||||
checkpoint: { hash: nextCheckpoint.text },
|
||||
operation: "edit",
|
||||
editData: {
|
||||
editedContent,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { appendImages } from "@src/utils/imageUtils"
|
|||
import { McpExecution } from "./McpExecution"
|
||||
import { ChatTextArea } from "./ChatTextArea"
|
||||
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
||||
import { useSelectedModel } from "../ui/hooks/useSelectedModel"
|
||||
|
||||
interface ChatRowProps {
|
||||
message: ClineMessage
|
||||
|
|
@ -115,8 +116,8 @@ export const ChatRowContent = ({
|
|||
}: ChatRowContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
|
||||
|
||||
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
|
||||
const { info: model } = useSelectedModel(apiConfiguration)
|
||||
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
|
||||
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
||||
|
|
@ -1184,7 +1185,7 @@ export const ChatRowContent = ({
|
|||
setSelectedImages={setEditImages}
|
||||
onSend={handleSaveEdit}
|
||||
onSelectImages={handleSelectImages}
|
||||
shouldDisableImages={false}
|
||||
shouldDisableImages={!model?.supportsImages}
|
||||
mode={editMode}
|
||||
setMode={setEditMode}
|
||||
modeShortcutText=""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react"
|
||||
import { VolumeX, Image, WandSparkles, SendHorizontal, MessageSquareX } from "lucide-react"
|
||||
|
||||
import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions"
|
||||
import { WebviewMessage } from "@roo/WebviewMessage"
|
||||
|
|
@ -31,7 +31,6 @@ import ContextMenu from "./ContextMenu"
|
|||
import { IndexingStatusBadge } from "./IndexingStatusBadge"
|
||||
import { SlashCommandsPopover } from "./SlashCommandsPopover"
|
||||
import { usePromptHistory } from "./hooks/usePromptHistory"
|
||||
import { EditModeControls } from "./EditModeControls"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
|
|
@ -58,7 +57,6 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
{
|
||||
inputValue,
|
||||
setInputValue,
|
||||
sendingDisabled,
|
||||
selectApiConfigDisabled,
|
||||
placeholderText,
|
||||
selectedImages,
|
||||
|
|
@ -897,261 +895,11 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
[setMode],
|
||||
)
|
||||
|
||||
// Helper function to render mode selector
|
||||
const renderModeSelector = () => (
|
||||
<ModeSelector
|
||||
value={mode}
|
||||
title={t("chat:selectMode")}
|
||||
onChange={handleModeChange}
|
||||
triggerClassName="w-full"
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
/>
|
||||
)
|
||||
|
||||
// Helper function to handle API config change
|
||||
const handleApiConfigChange = useCallback((value: string) => {
|
||||
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
|
||||
}, [])
|
||||
|
||||
// Helper function to render non-edit mode controls
|
||||
const renderNonEditModeControls = () => (
|
||||
<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
|
||||
<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
|
||||
<div className="shrink-0">{renderModeSelector()}</div>
|
||||
|
||||
<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
|
||||
<ApiConfigSelector
|
||||
value={currentConfigId}
|
||||
displayName={displayName}
|
||||
disabled={selectApiConfigDisabled}
|
||||
title={t("chat:selectApiConfig")}
|
||||
onChange={handleApiConfigChange}
|
||||
triggerClassName="w-full text-ellipsis overflow-hidden"
|
||||
listApiConfigMeta={listApiConfigMeta || []}
|
||||
pinnedApiConfigs={pinnedApiConfigs}
|
||||
togglePinnedApiConfig={togglePinnedApiConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
|
||||
{isTtsPlaying && (
|
||||
<StandardTooltip content={t("chat:stopTts")}>
|
||||
<button
|
||||
aria-label={t("chat:stopTts")}
|
||||
onClick={() => vscode.postMessage({ type: "stopTts" })}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<VolumeX className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
<SlashCommandsPopover />
|
||||
<IndexingStatusBadge />
|
||||
<StandardTooltip content={t("chat:addImages")}>
|
||||
<button
|
||||
aria-label={t("chat:addImages")}
|
||||
disabled={shouldDisableImages}
|
||||
onClick={!shouldDisableImages ? onSelectImages : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!shouldDisableImages && "cursor-pointer",
|
||||
shouldDisableImages &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
"mr-1",
|
||||
)}>
|
||||
<Image className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Helper function to render the text area section
|
||||
const renderTextAreaSection = () => (
|
||||
<div
|
||||
className={cn(
|
||||
"relative",
|
||||
"flex-1",
|
||||
"flex",
|
||||
"flex-col-reverse",
|
||||
"min-h-0",
|
||||
"overflow-hidden",
|
||||
"rounded",
|
||||
)}>
|
||||
<div
|
||||
ref={highlightLayerRef}
|
||||
data-testid="highlight-layer"
|
||||
className={cn(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"pointer-events-none",
|
||||
"whitespace-pre-wrap",
|
||||
"break-words",
|
||||
"text-transparent",
|
||||
"overflow-hidden",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
isFocused
|
||||
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
|
||||
: isDraggingOver
|
||||
? "border-2 border-dashed border-vscode-focusBorder"
|
||||
: "border border-transparent",
|
||||
isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2",
|
||||
"px-[8px]",
|
||||
"pr-9",
|
||||
"z-10",
|
||||
"forced-color-adjust-none",
|
||||
)}
|
||||
style={{
|
||||
color: "transparent",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
ref={(el) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(el)
|
||||
} else if (ref) {
|
||||
ref.current = el
|
||||
}
|
||||
textAreaRef.current = el
|
||||
}}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
onSelect={updateCursorPosition}
|
||||
onMouseUp={updateCursorPosition}
|
||||
onHeightChange={(height) => {
|
||||
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
||||
setTextAreaBaseHeight(height)
|
||||
}
|
||||
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={3}
|
||||
maxRows={15}
|
||||
autoFocus={true}
|
||||
className={cn(
|
||||
"w-full",
|
||||
"text-vscode-input-foreground",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
"cursor-text",
|
||||
isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2",
|
||||
isFocused
|
||||
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
|
||||
: isDraggingOver
|
||||
? "border-2 border-dashed border-vscode-focusBorder"
|
||||
: "border border-transparent",
|
||||
isDraggingOver
|
||||
? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
|
||||
: "bg-vscode-input-background",
|
||||
"transition-background-color duration-150 ease-in-out",
|
||||
"will-change-background-color",
|
||||
"min-h-[90px]",
|
||||
"box-border",
|
||||
"rounded",
|
||||
"resize-none",
|
||||
"overflow-x-hidden",
|
||||
"overflow-y-auto",
|
||||
"pr-9",
|
||||
"flex-none flex-grow",
|
||||
"z-[2]",
|
||||
"scrollbar-none",
|
||||
"scrollbar-hide",
|
||||
)}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
|
||||
<div className="absolute top-1 right-1 z-30">
|
||||
<StandardTooltip content={t("chat:enhancePrompt")}>
|
||||
<button
|
||||
aria-label={t("chat:enhancePrompt")}
|
||||
disabled={false}
|
||||
onClick={handleEnhancePrompt}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
|
||||
{!isEditMode && (
|
||||
<div className="absolute bottom-1 right-1 z-30">
|
||||
<StandardTooltip content={t("chat:sendMessage")}>
|
||||
<button
|
||||
aria-label={t("chat:sendMessage")}
|
||||
disabled={false}
|
||||
onClick={onSend}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<SendHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!inputValue && !isEditMode && (
|
||||
<div
|
||||
className="absolute left-2 z-30 pr-9 flex items-center h-8 font-vscode-font-family text-vscode-editor-font-size leading-vscode-editor-line-height"
|
||||
style={{
|
||||
bottom: "0.25rem",
|
||||
color: "color-mix(in oklab, var(--vscode-input-foreground) 50%, transparent)",
|
||||
userSelect: "none",
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
{placeholderBottomText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -1160,12 +908,12 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
"flex-col",
|
||||
"gap-1",
|
||||
"bg-editor-background",
|
||||
isEditMode ? "px-0" : "px-1.5",
|
||||
"px-1.5",
|
||||
"pb-1",
|
||||
"outline-none",
|
||||
"border",
|
||||
"border-none",
|
||||
isEditMode ? "w-full" : "w-[calc(100%-16px)]",
|
||||
"w-[calc(100%-16px)]",
|
||||
"ml-auto",
|
||||
"mr-auto",
|
||||
"box-border",
|
||||
|
|
@ -1228,23 +976,189 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
</div>
|
||||
)}
|
||||
|
||||
{renderTextAreaSection()}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"relative",
|
||||
"flex-1",
|
||||
"flex",
|
||||
"flex-col-reverse",
|
||||
"min-h-0",
|
||||
"overflow-hidden",
|
||||
"rounded",
|
||||
)}>
|
||||
<div
|
||||
ref={highlightLayerRef}
|
||||
data-testid="highlight-layer"
|
||||
className={cn(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"pointer-events-none",
|
||||
"whitespace-pre-wrap",
|
||||
"break-words",
|
||||
"text-transparent",
|
||||
"overflow-hidden",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
isFocused
|
||||
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
|
||||
: isDraggingOver
|
||||
? "border-2 border-dashed border-vscode-focusBorder"
|
||||
: "border border-transparent",
|
||||
"px-[8px]",
|
||||
"py-1.5",
|
||||
"pr-9",
|
||||
"z-10",
|
||||
"forced-color-adjust-none",
|
||||
)}
|
||||
style={{
|
||||
color: "transparent",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
ref={(el) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(el)
|
||||
} else if (ref) {
|
||||
ref.current = el
|
||||
}
|
||||
textAreaRef.current = el
|
||||
}}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
onSelect={updateCursorPosition}
|
||||
onMouseUp={updateCursorPosition}
|
||||
onHeightChange={(height) => {
|
||||
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
||||
setTextAreaBaseHeight(height)
|
||||
}
|
||||
|
||||
{isEditMode && (
|
||||
<EditModeControls
|
||||
mode={mode}
|
||||
onModeChange={handleModeChange}
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
onCancel={onCancel}
|
||||
onSend={onSend}
|
||||
onSelectImages={onSelectImages}
|
||||
sendingDisabled={sendingDisabled}
|
||||
shouldDisableImages={shouldDisableImages}
|
||||
/>
|
||||
)}
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={3}
|
||||
maxRows={15}
|
||||
autoFocus={true}
|
||||
className={cn(
|
||||
"w-full",
|
||||
"text-vscode-input-foreground",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
"cursor-text",
|
||||
"py-1.5 px-2",
|
||||
isFocused
|
||||
? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
|
||||
: isDraggingOver
|
||||
? "border-2 border-dashed border-vscode-focusBorder"
|
||||
: "border border-transparent",
|
||||
isDraggingOver
|
||||
? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
|
||||
: "bg-vscode-input-background",
|
||||
"transition-background-color duration-150 ease-in-out",
|
||||
"will-change-background-color",
|
||||
"min-h-[90px]",
|
||||
"box-border",
|
||||
"rounded",
|
||||
"resize-none",
|
||||
"overflow-x-hidden",
|
||||
"overflow-y-auto",
|
||||
"pr-9",
|
||||
"flex-none flex-grow",
|
||||
"z-[2]",
|
||||
"scrollbar-none",
|
||||
"scrollbar-hide",
|
||||
)}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
|
||||
<div className="absolute top-1 right-1 z-30">
|
||||
<StandardTooltip content={t("chat:enhancePrompt")}>
|
||||
<button
|
||||
aria-label={t("chat:enhancePrompt")}
|
||||
disabled={false}
|
||||
onClick={handleEnhancePrompt}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-1 right-1 z-30">
|
||||
{isEditMode && (
|
||||
<StandardTooltip content={t("chat:cancel.title")}>
|
||||
<button
|
||||
aria-label={t("chat:cancel.title")}
|
||||
disabled={false}
|
||||
onClick={onCancel}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<MessageSquareX className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
<StandardTooltip content={t("chat:sendMessage")}>
|
||||
<button
|
||||
aria-label={t("chat:sendMessage")}
|
||||
disabled={false}
|
||||
onClick={onSend}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<SendHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
|
||||
{!inputValue && (
|
||||
<div
|
||||
className="absolute left-2 z-30 pr-9 flex items-center h-8 font-vscode-font-family text-vscode-editor-font-size leading-vscode-editor-line-height"
|
||||
style={{
|
||||
bottom: "0.25rem",
|
||||
color: "color-mix(in oklab, var(--vscode-input-foreground) 50%, transparent)",
|
||||
userSelect: "none",
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
{placeholderBottomText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedImages.length > 0 && (
|
||||
|
|
@ -1259,7 +1173,80 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
/>
|
||||
)}
|
||||
|
||||
{!isEditMode && renderNonEditModeControls()}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="max-w-32">
|
||||
<ModeSelector
|
||||
value={mode}
|
||||
title={t("chat:selectMode")}
|
||||
onChange={handleModeChange}
|
||||
triggerClassName="w-full"
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-32">
|
||||
<ApiConfigSelector
|
||||
value={currentConfigId}
|
||||
displayName={displayName}
|
||||
disabled={selectApiConfigDisabled}
|
||||
title={t("chat:selectApiConfig")}
|
||||
onChange={handleApiConfigChange}
|
||||
triggerClassName="w-full text-ellipsis overflow-hidden"
|
||||
listApiConfigMeta={listApiConfigMeta || []}
|
||||
pinnedApiConfigs={pinnedApiConfigs}
|
||||
togglePinnedApiConfig={togglePinnedApiConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
{isTtsPlaying && (
|
||||
<StandardTooltip content={t("chat:stopTts")}>
|
||||
<button
|
||||
aria-label={t("chat:stopTts")}
|
||||
onClick={() => vscode.postMessage({ type: "stopTts" })}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
"cursor-pointer",
|
||||
)}>
|
||||
<VolumeX className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{!isEditMode ? <SlashCommandsPopover /> : null}
|
||||
{!isEditMode ? <IndexingStatusBadge /> : null}
|
||||
<StandardTooltip content={t("chat:addImages")}>
|
||||
<button
|
||||
aria-label={t("chat:addImages")}
|
||||
disabled={shouldDisableImages}
|
||||
onClick={!shouldDisableImages ? onSelectImages : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"text-vscode-foreground opacity-85",
|
||||
"transition-all duration-150",
|
||||
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!shouldDisableImages && "cursor-pointer",
|
||||
shouldDisableImages &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
"mr-1",
|
||||
)}>
|
||||
<Image className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
import React from "react"
|
||||
import { Mode } from "@roo/modes"
|
||||
import { Button, StandardTooltip } from "@/components/ui"
|
||||
import { Image, SendHorizontal } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ModeSelector } from "./ModeSelector"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
|
||||
interface EditModeControlsProps {
|
||||
mode: Mode
|
||||
onModeChange: (value: Mode) => void
|
||||
modeShortcutText: string
|
||||
customModes: any
|
||||
customModePrompts: any
|
||||
onCancel?: () => void
|
||||
onSend: () => void
|
||||
onSelectImages: () => void
|
||||
sendingDisabled: boolean
|
||||
shouldDisableImages: boolean
|
||||
}
|
||||
|
||||
export const EditModeControls: React.FC<EditModeControlsProps> = ({
|
||||
mode,
|
||||
onModeChange,
|
||||
modeShortcutText,
|
||||
customModes,
|
||||
customModePrompts,
|
||||
onCancel,
|
||||
onSend,
|
||||
onSelectImages,
|
||||
sendingDisabled,
|
||||
shouldDisableImages,
|
||||
}) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"absolute",
|
||||
"bottom-2",
|
||||
"left-2",
|
||||
"right-2",
|
||||
"z-30",
|
||||
)}>
|
||||
<div className={cn("flex", "items-center", "gap-1", "flex-1", "min-w-0")}>
|
||||
<div className="shrink-0">
|
||||
<ModeSelector
|
||||
value={mode}
|
||||
title={t("chat:selectMode")}
|
||||
onChange={onModeChange}
|
||||
triggerClassName="w-full"
|
||||
modeShortcutText={modeShortcutText}
|
||||
customModes={customModes}
|
||||
customModePrompts={customModePrompts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0", "ml-2")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
disabled={sendingDisabled}
|
||||
className="text-xs bg-vscode-toolbar-hoverBackground hover:bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground">
|
||||
Cancel
|
||||
</Button>
|
||||
<StandardTooltip content={t("chat:addImages")}>
|
||||
<button
|
||||
aria-label={t("chat:addImages")}
|
||||
disabled={shouldDisableImages}
|
||||
onClick={!shouldDisableImages ? onSelectImages : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!shouldDisableImages && "cursor-pointer",
|
||||
shouldDisableImages &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<Image className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("chat:save.tooltip")}>
|
||||
<button
|
||||
aria-label={t("chat:save.tooltip")}
|
||||
disabled={sendingDisabled}
|
||||
onClick={!sendingDisabled ? onSend : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center",
|
||||
"bg-transparent border-none p-1.5",
|
||||
"rounded-md min-w-[28px] min-h-[28px]",
|
||||
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
|
||||
"transition-all duration-150",
|
||||
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
|
||||
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
|
||||
"active:bg-[rgba(255,255,255,0.1)]",
|
||||
!sendingDisabled && "cursor-pointer",
|
||||
sendingDisabled &&
|
||||
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
|
||||
)}>
|
||||
<SendHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { EditModeControls } from "../EditModeControls"
|
||||
import { Mode } from "@roo/modes"
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock the UI components
|
||||
vi.mock("@/components/ui", () => ({
|
||||
Button: ({ children, onClick, disabled, ...props }: any) => (
|
||||
<button onClick={onClick} disabled={disabled} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
|
||||
}))
|
||||
|
||||
// Mock ModeSelector
|
||||
vi.mock("../ModeSelector", () => ({
|
||||
default: ({ value, onChange, title }: any) => (
|
||||
<select value={value} onChange={(e) => onChange(e.target.value)} title={title}>
|
||||
<option value="code">Code</option>
|
||||
<option value="architect">Architect</option>
|
||||
</select>
|
||||
),
|
||||
}))
|
||||
|
||||
describe("EditModeControls", () => {
|
||||
const defaultProps = {
|
||||
mode: "code" as Mode,
|
||||
onModeChange: vi.fn(),
|
||||
modeShortcutText: "Ctrl+M",
|
||||
customModes: [],
|
||||
customModePrompts: {},
|
||||
onCancel: vi.fn(),
|
||||
onSend: vi.fn(),
|
||||
onSelectImages: vi.fn(),
|
||||
sendingDisabled: false,
|
||||
shouldDisableImages: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders all controls correctly", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
// Check for mode selector
|
||||
expect(screen.getByTitle("chat:selectMode")).toBeInTheDocument()
|
||||
|
||||
// Check for Cancel button
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument()
|
||||
|
||||
// Check for image button
|
||||
expect(screen.getByTitle("chat:addImages")).toBeInTheDocument()
|
||||
|
||||
// Check for send button
|
||||
expect(screen.getByTitle("chat:save.tooltip")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onCancel when Cancel button is clicked", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const cancelButton = screen.getByText("Cancel")
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("calls onSend when send button is clicked", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const sendButton = screen.getByLabelText("chat:save.tooltip")
|
||||
fireEvent.click(sendButton)
|
||||
|
||||
expect(defaultProps.onSend).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("calls onSelectImages when image button is clicked", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const imageButton = screen.getByLabelText("chat:addImages")
|
||||
fireEvent.click(imageButton)
|
||||
|
||||
expect(defaultProps.onSelectImages).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("disables buttons when sendingDisabled is true", () => {
|
||||
render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
|
||||
|
||||
const cancelButton = screen.getByText("Cancel")
|
||||
const sendButton = screen.getByLabelText("chat:save.tooltip")
|
||||
|
||||
expect(cancelButton).toBeDisabled()
|
||||
expect(sendButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("disables image button when shouldDisableImages is true", () => {
|
||||
render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
|
||||
|
||||
const imageButton = screen.getByLabelText("chat:addImages")
|
||||
expect(imageButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it("does not call onSelectImages when image button is disabled", () => {
|
||||
render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
|
||||
|
||||
const imageButton = screen.getByLabelText("chat:addImages")
|
||||
fireEvent.click(imageButton)
|
||||
|
||||
expect(defaultProps.onSelectImages).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does not call onSend when send button is disabled", () => {
|
||||
render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
|
||||
|
||||
const sendButton = screen.getByLabelText("chat:save.tooltip")
|
||||
fireEvent.click(sendButton)
|
||||
|
||||
expect(defaultProps.onSend).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("calls onModeChange when mode is changed", () => {
|
||||
render(<EditModeControls {...defaultProps} />)
|
||||
|
||||
const modeSelector = screen.getByTitle("chat:selectMode")
|
||||
fireEvent.change(modeSelector, { target: { value: "architect" } })
|
||||
|
||||
expect(defaultProps.onModeChange).toHaveBeenCalledWith("architect")
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue