Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-boxes-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": minor
---

Redesign chat window, update start screen, fix chat disabling.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
{
"command": "roo-cline.mcpButtonClicked",
"title": "MCP Servers",
"icon": "$(server)"
"icon": "$(extensions)"
},
{
"command": "roo-cline.promptsButtonClicked",
Expand Down
23 changes: 2 additions & 21 deletions src/core/webview/__tests__/ClineProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import { ClineProvider } from "../ClineProvider"
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
import { GlobalStateKey, SecretKey } from "../../../shared/globalState"
import { setSoundEnabled } from "../../../utils/sound"
import { defaultModeSlug } from "../../../shared/modes"
import { experimentDefault } from "../../../shared/experiments"
import { Cline } from "../../Cline"
import { defaultExtensionState } from "../../../shared/ExtensionMessage"

// Mock setup must come before imports
jest.mock("../../prompts/sections/custom-instructions")
Expand Down Expand Up @@ -417,11 +416,7 @@ describe("ClineProvider", () => {
await provider.resolveWebviewView(mockWebviewView)

const mockState: ExtensionState = {
version: "1.0.0",
preferredLanguage: "English",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
...defaultExtensionState,
apiConfiguration: {
apiProvider: "openrouter",
},
Expand All @@ -432,24 +427,10 @@ describe("ClineProvider", () => {
alwaysAllowBrowser: false,
alwaysAllowMcp: false,
uriScheme: "vscode",
soundEnabled: false,
diffEnabled: false,
enableCheckpoints: false,
checkpointStorage: "task",
writeDelayMs: 1000,
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
mcpEnabled: true,
enableMcpServerCreation: false,
requestDelaySeconds: 5,
rateLimitSeconds: 0,
mode: defaultModeSlug,
customModes: [],
experiments: experimentDefault,
maxOpenTabsContext: 20,
browserToolEnabled: true,
telemetrySetting: "unset",
showRooIgnoredFiles: true,
}

const message: ExtensionMessage = {
Expand Down
52 changes: 48 additions & 4 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api"
import { HistoryItem } from "./HistoryItem"
import { McpServer } from "./mcp"
import { GitCommit } from "../utils/git"
import { Mode, CustomModePrompts, ModeConfig } from "./modes"
import { Mode, CustomModePrompts, ModeConfig, defaultModeSlug, defaultPrompts } from "./modes"
import { CustomSupportPrompts } from "./support-prompt"
import { ExperimentId } from "./experiments"
import { experimentDefault, ExperimentId } from "./experiments"
import { TERMINAL_OUTPUT_LIMIT } from "./terminal"
import { ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code"
import { CheckpointStorage } from "./checkpoints"
import { TelemetrySetting } from "./TelemetrySetting"
import { ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code"

export interface LanguageModelChatSelector {
vendor?: string
Expand Down Expand Up @@ -97,6 +98,11 @@ export interface ApiConfigMeta {
}

export interface ExtensionState {
glamaModels?: Record<string, ModelInfo>
requestyModels?: Record<string, ModelInfo>
openRouterModels?: Record<string, ModelInfo>
unboundModels?: Record<string, ModelInfo>
openAiModels?: string[]
version: string
clineMessages: ClineMessage[]
taskHistory: HistoryItem[]
Expand Down Expand Up @@ -231,8 +237,46 @@ export interface HumanRelayCancelMessage {
}

export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"

export type ToolProgressStatus = {
icon?: string
text?: string
}

export const defaultExtensionState: ExtensionState = {
version: "",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
allowedCommands: [],
soundEnabled: false,
soundVolume: 0.5,
diffEnabled: false,
enableCheckpoints: true,
checkpointStorage: "task",
fuzzyMatchThreshold: 1.0,
preferredLanguage: "English",
enableCustomModeCreation: true,
writeDelayMs: 1000,
browserViewportSize: "900x600",
screenshotQuality: 75,
terminalOutputLimit: TERMINAL_OUTPUT_LIMIT,
mcpEnabled: true,
enableMcpServerCreation: true,
alwaysApproveResubmit: false,
requestDelaySeconds: 5,
rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
currentApiConfigName: "default",
listApiConfigMeta: [],
mode: defaultModeSlug,
customModePrompts: defaultPrompts,
customSupportPrompts: {},
experiments: experimentDefault,
enhancementApiConfigId: "",
autoApprovalEnabled: false,
customModes: [],
maxOpenTabsContext: 20,
cwd: "",
browserToolEnabled: true,
telemetrySetting: "unset",
showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior)
}
136 changes: 136 additions & 0 deletions webview-ui/src/components/chat/ChatTextArea/ChatTextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { forwardRef, useEffect, useState } from "react"
import ChatTextAreaLayout from "./ChatTextAreaLayout"
import ChatTextAreaInput from "./ChatTextAreaInput"
import ChatTextAreaSelections from "./ChatTextAreaSelections"
import ChatTextAreaActions from "./ChatTextAreaActions"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ContextMenuOptionType } from "@/utils/context-mentions"
import { Mode } from "../../../../../src/shared/modes"

interface ChatTextAreaProps {
inputValue: string
setInputValue: (value: string) => void
textAreaDisabled: boolean
placeholderText: string
selectedImages: string[]
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>
onSend: () => void
onSelectImages: () => void
shouldDisableImages: boolean
onHeightChange?: (height: number) => void
mode: Mode
setMode: (value: Mode) => void
modeShortcutText: string
}

const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
(
{
inputValue,
setInputValue,
textAreaDisabled,
placeholderText,
selectedImages,
setSelectedImages,
onSend,
onSelectImages,
shouldDisableImages,
onHeightChange,
mode,
setMode,
modeShortcutText,
},
ref,
) => {
const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes, cwd } = useExtensionState()
const [gitCommits, setGitCommits] = useState<any[]>([])
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
const [showDropdown, setShowDropdown] = useState(false)

// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (showDropdown) {
setShowDropdown(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [showDropdown])

// Handle enhanced prompt response
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data
if (message.type === "enhancedPrompt") {
if (message.text) {
setInputValue(message.text)
}
setIsEnhancingPrompt(false)
} else if (message.type === "commitSearchResults") {
const commits = message.commits.map((commit: any) => ({
type: ContextMenuOptionType.Git,
value: commit.hash,
label: commit.subject,
description: `${commit.shortHash} by ${commit.author} on ${commit.date}`,
icon: "$(git-commit)",
}))
setGitCommits(commits)
}
}
window.addEventListener("message", messageHandler)
return () => window.removeEventListener("message", messageHandler)
}, [setInputValue, setIsEnhancingPrompt])

return (
<ChatTextAreaLayout>
{{
input: (
<ChatTextAreaInput
ref={ref}
inputValue={inputValue}
setInputValue={setInputValue}
textAreaDisabled={textAreaDisabled}
placeholderText={placeholderText}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
onSend={onSend}
onHeightChange={onHeightChange}
setMode={setMode}
customModes={customModes}
filePaths={filePaths}
openedTabs={openedTabs}
gitCommits={gitCommits}
shouldDisableImages={shouldDisableImages}
cwd={cwd}
/>
),
selections: (
<ChatTextAreaSelections
mode={mode}
setMode={setMode}
currentApiConfigName={currentApiConfigName}
listApiConfigMeta={listApiConfigMeta}
customModes={customModes}
modeShortcutText={modeShortcutText}
/>
),
actions: (
<ChatTextAreaActions
textAreaDisabled={textAreaDisabled}
shouldDisableImages={shouldDisableImages}
isEnhancingPrompt={isEnhancingPrompt}
inputValue={inputValue}
setInputValue={setInputValue}
onSelectImages={onSelectImages}
onSend={onSend}
setIsEnhancingPrompt={setIsEnhancingPrompt}
/>
),
}}
</ChatTextAreaLayout>
)
},
)

export default ChatTextArea
103 changes: 103 additions & 0 deletions webview-ui/src/components/chat/ChatTextArea/ChatTextAreaActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from "react"
import { vscode } from "../../../utils/vscode"
import { cn } from "@/lib/utils"

interface ChatTextAreaActionsProps {
textAreaDisabled: boolean
shouldDisableImages: boolean
isEnhancingPrompt: boolean
inputValue: string
setInputValue: (value: string) => void
onSelectImages: () => void
onSend: () => void
setIsEnhancingPrompt: (value: boolean) => void
}

const baseActionButton = [
"text-base relative inline-flex items-center justify-center",
"bg-transparent border-none outline-none opacity-50",
"p-1.5 rounded-md transition-all duration-200",
"min-w-[28px] min-h-[28px] cursor-pointer",
"text-vscode-foreground",
"hover:bg-[color:color-mix(in_srgb,var(--vscode-button-hoverBackground)_40%,transparent)]",
"hover:opacity-75",
]

const disabledButton = "disabled opacity-35 cursor-not-allowed grayscale-[30%] hover:bg-transparent"

const ChatTextAreaActions: React.FC<ChatTextAreaActionsProps> = ({
textAreaDisabled,
shouldDisableImages,
isEnhancingPrompt,
inputValue,
setInputValue,
onSelectImages,
onSend,
setIsEnhancingPrompt,
}) => {
const handleEnhancePrompt = () => {
if (!textAreaDisabled) {
const trimmedInput = inputValue.trim()
if (trimmedInput) {
setIsEnhancingPrompt(true)
const message = {
type: "enhancePrompt" as const,
text: trimmedInput,
}
vscode.postMessage(message)
} else {
const promptDescription =
"The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works."
setInputValue(promptDescription)
}
}
}

return (
<div className="flex items-center gap-0.5 flex-wrap justify-end min-w-0">
<div className="flex items-center gap-0.5 shrink-0">
<span
role="button"
aria-label="Enhance prompt with additional context"
title="Enhance prompt with additional context"
data-testid="enhance-prompt-button"
onClick={() => !textAreaDisabled && !isEnhancingPrompt && handleEnhancePrompt()}
className={cn(
"codicon codicon-sparkle",
baseActionButton,
"transition-all duration-300 z-[1] hover:brightness-120",
textAreaDisabled && disabledButton,
isEnhancingPrompt && [
"enhancing",
"!opacity-100 disabled cursor-progress",
"bg-gradient-to-r",
"from-vscode-button-background from-10%",
"via-[color:color-mix(in_srgb,var(--vscode-charts-yellow)_40%,var(--vscode-button-background))]",
"via-50%",
"to-vscode-button-background",
"to-90%",
"bg-[length:300%_100%]",
"animate-border-flow",
],
)}
/>
</div>
<span
role="button"
aria-label="Add images to message"
title="Add images to message"
onClick={() => !shouldDisableImages && onSelectImages()}
className={cn("codicon codicon-device-camera", baseActionButton, shouldDisableImages && disabledButton)}
/>
<span
role="button"
aria-label="Send message"
title="Send message"
onClick={() => !textAreaDisabled && onSend()}
className={cn("codicon codicon-send", baseActionButton, textAreaDisabled && disabledButton)}
/>
</div>
)
}

export default ChatTextAreaActions
Loading